C++20 の 2 つの新しいキーワード:consteval と constinit

C++20 では、consteval と constinit という 2 つの新しいキーワードがあります。 consteval はコンパイル時に実行される関数を生成し、constinit は変数がコンパイル時に初期化されることを保証します。

consteval と constinit に関する以前の簡単な説明を読むと、両方の指定子が constexpr に非常に似ているという印象を受けるかもしれません。要するに、あなたは正しいです。キーワード consteval、constinit、constexpr、および古き良き const を比較する前に、新しい指定子 consteval と constinit を紹介する必要があります。

コンステバル

consteval int sqr(int n) {
 return n * n;
}

consteval は、いわゆる即時関数を作成します。即時関数を呼び出すたびに、コンパイル時の定数が作成されます。もっと直接的に言えば。 consteval (即時) 関数はコンパイル時に実行されます。

consteval は、割り当てまたは割り当て解除を行うデストラクタまたは関数には適用できません。宣言では、consteval、constexpr、または constinit 指定子の 1 つだけを使用できます。即時関数 (consteval) は暗黙的なインラインであり、constexpr 関数の要件を満たす必要があります。

C++14 の constexpr 関数の要件、つまり consteval 関数は次のとおりです。

constexpr 関数は

  • 条件付きジャンプ命令またはループ命令がある
  • 複数の指示がある
  • constexp 関数を呼び出します。 consteval 関数は constexpr 関数のみを呼び出すことができ、その逆はできません。
  • 定数式で初期化する必要がある基本的なデータ型を持っている

constexpr 関数は static または thread_local データを持つことはできません。 try ブロックも goto 命令も使用できません。

プログラム constevalSqr.cpp は、consteval 関数 sqr を適用します。

// constevalSqr.cpp

#include <iostream>

consteval int sqr(int n) {
 return n * n;
}

int main() {
 
 std::cout << "sqr(5): " << sqr(5) << std::endl; // (1)
 
 const int a = 5; // (2)
 std::cout << "sqr(a): " << sqr(a) << std::endl; 

 int b = 5; // (3)
 // std::cout << "sqr(b): " << sqr(b) << std::endl; ERROR

}

5 は定数式で、関数 sqr (1) の引数として使用できます。

変数a(2)についても同様である。 a などの定数変数は、定数式で初期化すると、定数式で使用できます。

b (3) は定数式ではありません。したがって、sqr(b) の呼び出しは無効です。

真新しい GCC11 と Compiler Explorer のおかげで、ここにプログラムの出力があります。

constinit

constinit は、静的ストレージ期間またはスレッド ストレージ期間を持つ変数に適用できます。

  • グローバル (名前空間) 変数、静的変数、または静的クラス メンバーには、静的な保存期間があります。これらのオブジェクトは、プログラムの開始時に割り当てられ、プログラムの終了時に割り当て解除されます。
  • thread_local 変数にはスレッド保存期間があります。スレッド ローカル データは、このデータを使用するスレッドごとに作成されます。 thread_local データはスレッドに排他的に属します。それらは最初の使用時に作成され、その存続期間はそれが属するスレッドの存続期間にバインドされます。多くの場合、スレッド ローカル データはスレッド ローカル ストレージと呼ばれます。

constinit は、この種の変数 (静的保存期間またはスレッド保存期間) がコンパイル時に初期化されることを保証します。

// constinitSqr.cpp

#include <iostream>

consteval int sqr(int n) {
 return n * n;
}

 constexpr auto res1 = sqr(5); 
 constinit auto res2 = sqr(5); 

int main() {

 std::cout << "sqr(5): " << res1 << std::endl;
 std::cout << "sqr(5): " << res2 << std::endl;
 
 constinit thread_local auto res3 = sqr(5); 
 std::cout << "sqr(5): " << res3 << std::endl;

}

res1 と res2 には静的な保存期間があります。 res3 にはスレッド保存期間があります。

次に、const、constexpr、consteval、および constinit の違いについて説明します。最初に関数の実行について書き、次に変数の初期化について書きます。

関数の実行

次のプログラム consteval.cpp には、2 乗関数の 3 つのバージョンがあります。

// consteval.cpp

#include <iostream>

int sqrRunTime(int n) {
 return n * n;
}

consteval int sqrCompileTime(int n) {
 return n * n;
}

constexpr int sqrRunOrCompileTime(int n) {
 return n * n;
}

int main() {

 // constexpr int prod1 = sqrRunTime(100); ERROR (1)
 constexpr int prod2 = sqrCompileTime(100);
 constexpr int prod3 = sqrRunOrCompileTime(100);
 
 int x = 100;
 
 int prod4 = sqrRunTime(x); 
 // int prod5 = sqrCompileTime(x); ERROR (2)
 int prod6 = sqrRunOrCompileTime(x);

}

その名の通り。通常の関数 sqrRunTime は実行時に実行されます。 consteval 関数 sqrCompileTime はコンパイル時に実行されます。 constexpr 関数 sqrRunOrCompileTime は、コンパイル時または実行時に実行できます。したがって、sqrRunTime (1) を使用してコンパイル時に結果を求めるとエラーになるか、sqrCompileTime (2) の引数として非定数式を使用するとエラーになります。

constexpr 関数 sqrRunOrCompileTime と consteval 関数 sqrCompileTime の違いは、sqrRunOrCompileTime は、コンテキストがコンパイル時の評価を必要とする場合にのみコンパイル時に実行する必要があることです。

static_assert(sqrRunOrCompileTime(10) == 100); // compile-time (1)
int arrayNewWithConstExpressioFunction[sqrRunOrCompileTime(100)]; // compile-time (1)
constexpr int prod = sqrRunOrCompileTime(100); // compile-time (1)

int a = 100;
int runTime = sqrRunOrCompileTime(a); // run-time (2)

int runTimeOrCompiletime = sqrRunOrCompileTime(100); // run-time or compile-time (3)

int allwaysCompileTime = sqrCompileTime(100); // compile-time (4)

最初の 3 行 (1) では、コンパイル時の評価が必要です。 a は定数式ではないため、行 (2) は実行時にのみ評価できます。クリティカルラインは (3) です。この関数は、コンパイル時または実行時に実行できます。コンパイル時または実行時に実行されるかどうかは、コンパイラまたは最適化レベルに依存する場合があります。この観察は、行 (4) には当てはまりません。 consteval 関数は常にコンパイル時に実行されます。

変数の初期化

次のプログラム constexprConstinit.cpp では、const、constexpr、および constint を比較しています。

// constexprConstinit.cpp

#include <iostream>

constexpr int constexprVal = 1000;
constinit int constinitVal = 1000;

int incrementMe(int val){ return ++val;}

int main() {

 auto val = 1000;
 const auto res = incrementMe(val); // (1) 
 std::cout << "res: " << res << std::endl;
 
// std::cout << "res: " << ++res << std::endl; ERROR (2) // std::cout << "++constexprVal++: " << ++constexprVal << std::endl; ERROR (2) std::cout << "++constinitVal++: " << ++constinitVal << std::endl; // (3) constexpr auto localConstexpr = 1000; // (4) // constinit auto localConstinit = 1000; ERROR }

const 変数 (1) のみが実行時に初期化されます。 constexpr および constinit 変数はコンパイル時に初期化されます。

constinit (3) は、const (2) や constexpr(2) などの constness を意味しません。 constexpr (4) または const (1) で宣言された変数はローカルとして作成できますが、constinit で宣言された変数は作成できません。

次は?

異なる翻訳単位での静的変数の初期化には重大な問題があります。ある静的変数の初期化が別の静的変数に依存している場合、それは定義されておらず、どの順序で初期化されますか。手短に言うと、私の次の投稿は、静的初期化順序の大失敗と、それを constinit で解決する方法についてです。