constexpr 関数

今日は、コンパイル時のプログラミングについての話を続けます。テンプレート メタプログラミング、型特性ライブラリの後、今日のトピックは constexpr です 特に関数。

なぜ私が constexpr について追加の記事を書くのか不思議に思うかもしれません . constexpr についてはすでにいくつかの記事を書いています。 ここ数年で。これが私の動機です。まず、constexpr の興味深い類似点を示します。 関数とテンプレート。次に、constexpr の強化された機能について書きたいと思います。 C++20 で。最後に、consteval についても説明します。 C++20 で。私の投稿でいくつかの理論が十分に詳しく説明されていない場合は、以前の投稿を参照します。新しいトピックに入る前に、短い要約から始めましょう。

短い要約

constexpr 典型的な C++ 構文を使用してコンパイル時にプログラミングできます。 constexpr を使用した定数式

変数

    • 暗黙の定数です。
    • 定数式で初期化する必要があります。
 constexpr double pi = 3.14;

関数

C++14 の constexpr 関数は非常に快適です。できる

  • 他の constexpr 関数を呼び出します。
  • 定数式で初期化する必要がある変数を持つことができます。
  • 条件式またはループを使用できます。
  • 暗黙のインラインです。
  • static または thread_local データを持つことはできません。

ユーザー定義型

  • 定数式であるコンストラクタが必要です。
  • 仮想関数は使用できません
  • 仮想基本クラスを持つことはできません。

constexpr 関数またはメソッドのルールは非常に単純です。つまり、両方の関数を呼び出します。

constexpr 関数は、定数式である機能にのみ依存できます。 constexpr 関数であることは、関数がコンパイル時に実行されることを意味しません。関数がコンパイル時に実行される可能性があることを示しています。 constexpr 関数は、ランタイムを実行することもできます。 constexpr 関数がコンパイル時または実行時に実行されるかどうかは、多くの場合、コンパイラと最適化レベルの問題です。コンパイル時に constexpr 関数 func を実行する必要があるコンテキストが 2 つあります。

<オール>
  • constexpr 関数は、コンパイル時に評価されるコンテキストで実行されます。これは、type-traits ライブラリや C 配列の初期化などの static_assert 式にすることができます。
  • constexpr 関数の値は constexpr で要求されます :constexpr auto res =func(5);
  • ここに理論の小さな例があります。プログラム constexpr14.cpp は、2 つの数値の大公約数を計算します。

    // constexpr14.cpp
    
    #include <iostream>
    
    constexpr auto gcd(int a, int b){
     while (b != 0){
     auto t= b;
     b= a % b;
     a= t;
     }
     return a;
    }
    
    int main(){
     
     std::cout << '\n';
     
     constexpr int i= gcd(11, 121); // (1)
     
     int a= 11;
     int b= 121;
     int j= gcd(a, b); // (2)
    
     std::cout << "gcd(11,121): " << i << '\n';
     std::cout << "gcd(a,b): " << j << '\n';
     
     std::cout << '\n';
     
    }
    

    行 (1) はコンパイル時に結果 i を計算し、行 (2) は実行時に j を計算します。 j を constexpr として宣言すると、コンパイラは文句を言います:constexpr int j =gcd(a, b)。問題は、int の a と b が定数式ではないことです。

    プログラムの出力に驚かないでください。

    驚きは今始まるかもしれません。 Compiler Explorer の魔法をお見せしましょう。

    プログラム constexpr14.cpp の行 (1) は、次の式の定数 11 に要約されます:mov DWORD PTR[rbp-4], 11 (スクリーンショットの行 33)。対照的に、行 (2) は関数呼び出しです:call gcd(int, int) (スクリーンショットの行 41)。

    この要約の後、constexpr の類似点を続けましょう。 関数とテンプレートのメタプログラミング。

    テンプレート メタプログラミング

    constexpr 関数は、テンプレート メタプログラミングと多くの共通点があります。テンプレートのメタプログラミングに慣れていない場合は、次の 3 つの以前の投稿を参考にしてください。

    • テンプレート メタプログラミング - すべての始まり
    • テンプレート メタプログラミング - 仕組み
    • テンプレート メタプログラミング - ハイブリッド プログラミング

    constexpr を比較した全体像を次に示します。 テンプレート メタプログラミングによる関数:

    表にいくつかコメントを追加したい.

    • テンプレート メタプログラムはコンパイル時に実行されますが、constexpr 関数はコンパイル時または実行時に実行できます。
    • テンプレート メタプログラムの引数は、int などの型、非型のどちらでもかまいません。 、またはテンプレート。
    • コンパイル時には状態がないため、変更はありません。これは、テンプレート メタプログラミングが純粋な関数型スタイルでプログラミングされていることを意味します。機能的なスタイルの観点からの特徴は次のとおりです。
      • テンプレート メタプログラミングでは、値を変更する代わりに、毎回新しい値を返します。
      • コンパイル時に i などの変数をインクリメントして for ループを制御することはできません:for (int i; i <= 10; ++i) .したがって、テンプレート メタプログラミングはループを再帰に置き換えます。
      • テンプレートのメタプログラミングでは、条件付き実行はテンプレートの特殊化に置き換えられます。

    確かに、この比較は非常に簡潔でした。メタ関数 (「テンプレート メタプログラミング - 仕組み」を参照) と constexpr 関数の図による比較は、未解決の問題に答えるはずです。どちらの関数も数値の階乗を計算します。

    • constexpr 関数の関数引数は、メタ関数のテンプレート引数に対応します。

    • constexpr 関数は変数を持ち、それらを変更できます。メタ関数は新しい値を生成します。

    • メタ関数は再帰を使用してループをシミュレートします。

    • 終了条件の代わりに、メタ関数はテンプレートの完全な特殊化を使用してループを終了します。さらに、メタ関数は部分的または完全な特殊化を使用して、if ステートメントなどの条件付き実行を実行します。

    • 更新された値 res の代わりに、メタ関数は反復ごとに新しい値を生成します。

    • メタ関数には return ステートメントがありません。この値を戻り値として使用します。

    constexpr 関数とテンプレートには、さらに多くの共通点があります。

    テンプレートのインスタンス化

    繰り返しになりますが、テンプレートのインスタンス化について詳しく知りたい場合は、以前の記事「テンプレートのインスタンス化」をお読みください。重要な事実だけを強調させてください.

    isSmaller などのテンプレート は 2 回構文チェックされます:

    template<typename T>
    bool isSmaller(T fir, T sec){
     return fir < sec;
    }
    
    isSmaller(5, 10); // (1)
    
    std::unordered_set<int> set1;
    std::unordered_set<int> set2;
    isSmaller(set1, set2); // (2)
    

    • まず、テンプレート定義の構文がチェックされます。このチェックは必須ではありませんが、許可されており、通常はコンパイラによって行われます。
    • 次に、コンパイラは関数の引数からテンプレートの引数を推測します。このプロセスでは、テンプレート引数ごとに具象関数を作成し、その構文をチェックします。 std::unordered_set<int の場合、このインスタンス化プロセスは失敗します> (2) データ型が <演算子をサポートしていないため。

    constexpr 関数の構文も 2 回チェックされます。

    constexpr auto gcd(int a, int b){
     while (b != 0){
     auto t= b;
     b= a % b;
     a= t;
     }
     return a;
    }
    
    
    constexpr int i= gcd(11, 121); // (1)
     
    int a= 11;
    int b= 121;
    constexpr int j= gcd(a, b); // (2)

    • 最初に、コンパイラは関数 gcd が コンパイル時に実行される可能性があります。これは、基本的に、呼び出された関数などの constexpr 関数のすべての依存関係が constexpr でなければならないことを意味します。 .
    • コンパイラは gcd の呼び出しごとにチェックする必要があります 引数が定数式であること。したがって、最初の呼び出し (1) は有効ですが、2 番目の呼び出し (2) は有効ではありません。

    最後に、テンプレートと constexpr 関数は、定義の可視性に関しても非常に似ています。

    可視性

    テンプレートをインスタンス化するときは、その定義が表示されている必要があります。 constexpr についても同様です 関数。 constexpr を呼び出したとき 関数、その定義は可視でなければなりません。

    What's Next?

    次の投稿では、constexpr について書きます。 C++20 の関数と C++20 のキーワード consteval .