テンプレートパラメータとしてのリテラル型クラス
まずリテラル型クラスとは何でしょうか?
その前に constexpr の説明に寄り道しましょう。
というのも わりとモダンな C++ の話をするとどうしても constexpr という言葉がでてきます。template に関する話題ならなおさらです。
せっかくなので、ここで constexpr についておさらいしましょう。
その後でリテラル型クラスの話にもどります。
constexpr という修飾子
constexpr というのは関数と変数を修飾することができ、
- コンパイル時に呼べる(評価できる)関数。実行時にも呼べる。
- コンパイル時に値が確定する変数
であることを示します。
constexpr int foo();
constexpr int i = 100;
expression (式、表現) を expr と略して書くのはプログラマ界隈(と一部の数学界隈)の古くからの慣習です。構文解析プログラムを書く場合は expr は避けて通れません。expression なんて書くとトーシローかと言われます。
コンパイル時分岐
そして、 if のあとに書く constexpr はコンパイル時分岐のステートメントになります。
普通はテンプレートの中で書きます。(テンプレート外でも書けるが、あまり意味がない)
たとえば、関数テンプレートでポインタを受け取る場合と、そうでない場合で違う処理を書きたい場合は、C++14 までは以下のようにテンプレートのオーバーロードを行っていました。
// 非ポインタ版(通常の型)
template <typename T>
void print_value(T v) {
std::cout << v << std::endl;
}
// ポインタ版(オーバーロード)
template <typename T>
void print_value(T* v) {
if (v) {
std::cout << *v << std::endl;
} else {
std::cout << "nullptr" << std::endl;
}
}
これはこれでいいんですが、C++17 以降では
template <typename T>
void print_value(T v) {
if constexpr (std::is_pointer_v<T>) {
// Tがポインタの時
if ( v ) {
std::cout << *v << std::endl;
} else {
std::cout << "nullptr" << std::endl;
}
} else {
// Tがポインタでない時
std::cout << v << std::endl;
}
}
このように書けます。
T の型はコンパイル時には決定しているので、if constexpr () の判定式が偽になる部分はコンパイルされません。分岐するコードも生成されません。
上のコードをよく見てください。v がポインタであるときの *v は合法ですが、そうでないときには *v は合法ではありません。
ポインタ以外が渡されたときに、
if constexpr (std::is_pointer_v<T>) のブロック内はコンパイルされず、そこに *v という不法行為があってもそれは許容されます。
オーバーロード版と違って print_value という関数テンプレートは1つ書くだけでよく意図が明確になり、if constexpr 以外のコードは共通化できるというメリットがあります。
この if constexpr の条件式には、「定数式(Constant Expression)」、つまりコンパイル時に値が確定する式を書く必要があります。
以前のC++では、リテラル(数字の 10 など)や sizeof くらいしか使えませんでしたが、現代のC++では constexpr 修飾子が付いた変数や関数を自由に組み合わせることができます。
関数に constexpr を付けることは、コンパイラに対して「この関数は定数計算のルールに従って書いたので、コンパイル時にも実行していいですよ」と宣言し、その能力を解放する(通行証を与える)役割を持っています。
constexpr のついた関数は実行時にしかできないことをしていないかコンパイラは厳密にチェックしてくれます。
たとえ中身が return 1 + 1; だけの単純な関数であっても、constexpr が付いていなければ、その戻り値を if constexpr の条件や配列のサイズ(int arr[func()])に使うことはできません。
consteval と constinit
変数と関数を修飾する constexpr は C++11 で導入されましたが、C++20 では consteval と constinit も導入されました。
- consteval : 関数にしか指定できません。指定された関数はコンパイル時にしか評価されません。constexpr な関数と違うのは constexprは実行時にも呼ぶことができるが、consteval は実行時には呼べないことです。ちなみに eval も古くからプログラマ界隈で使われている略語ですね。(evaluate 評価する)
- constinit : 変数にしか指定できません。変数がコンパイル時に初期化されることを保証するものです。constexpr と同じと思われるかもしれませんが、constexpr な変数は const ですが、constinit は 非 const な変数なので実行時に値を変更することができます。
constinit の「コンパイル時の初期化を保証する」とはどういうことでしょうか?
void foo( int value )
{
static int runtime_value = value;
static const int compiletime_101 = 101;
}
というコードがあるとき、runtime_value が初期化されるタイミングはfoo()が最初に呼ばれたときです。つまり1回めの foo の仮引数 value を設定するオブジェクトコードをコンパイラは作ります。
それに対して compiletime_101 はコンパイル時に値が確定し、プログラムがメモリにロードされたとき、main 関数が呼ばれる前には101が設定されています。
そして、constinit は compiletime_101 と同様にコンパイル時に値が確定することを保証するために以下のようなコードの ※2 の行はコンパイルエラーとなります。
void foo( int value )
{
static int runtime_value = value;
static const int compiletime_101 = 101;
// ↓ ※2 コンパイルエラー runtime_value は実行時に決まる
constinit static int compile_init = runtime_value + 1;
// ↓ これはOK
constinit static int compile_init_ok = compiletime_101 + 1;
}
C/C++は複数のソースファイルに静的変数が定義されているとき、初期化される順番を指定することはできません。
ということは、ある静的変数の初期値が別のソースファイルで定義された静的変数の値に依存していると意図通りの値にならないことがあり、コンパイルリンクしただけでは、そのバグに気づかないということが起こっていました。
constinit は初期化がコンパイル時となることを保証できないときにはコンパイルエラーによってそれを教えてくれます。
std::vector<T>::push_back()
さて、ここで恐ろしい話をひとつ。
C++20 から std::vector<T>::push_back() は constexpr となりました。
そう聞いて、脳内に???が充満しませんか?
以下のコードは合法です。
#include <vector>
#include <numeric>
constexpr int get_sum_of_sequence( int n ) {
std::vector<int> v; // コンパイル時に動的メモリ確保が行われる
for (int i = 1 ; i <= n; ++i ) {
v.push_back( i );
}
return std::accumulate( v.begin(), v.end(), 0 );
}
int main()
{
// result = 1 + 2 + 3 + 4 + 5 = 15
static constinit int result = get_sum_of_sequence( 5 );
}
このコードの場合 result という変数はコンパイルの時点で 15 という値で初期化されることが確定しています。
コンパイル中に、コンパイラの脳内で std::vector<int> vというインスタンスがつくられ、push_back が 5回行われ std::accumulate を呼び15 という結果が得られるとその値で変数を初期化するオブジェクトコードが作られます。
もちろん std::accumulate も constexpr です。
言い換えると、コンパイラはコンパイル中にこれを暗算しているわけです。暗算中に(仮想的な)ヒープから std::vector<int> のためのバッファを確保し、std::accumulate のループも回しているのです。
コンパイラの脳内には constexpr なコードを実行できる仮想環境があるのです。
藤井聡太さんでしょうか?
リテラル型クラスの説明に戻ります。
(つづく)