はじめに
C++にはconstexpr
という概念がある。
これまでよくわかっていなかったのだが、きちんと調べてconstexpr
を理解したつもりになったので、ここにまとめる。
(以下の話は、全てC++17以降を想定している。)
話の要点
constexpr
を使えない・使うべきでない主な場面
変数
-
const
でない変数 - クラスのメンバ変数
- 標準入力などの非
constexpr
関数を用いて計算する値 - 引数などの
constexpr
でない可能性がある値を用いて計算する値
関数
-
inline
化できない関数 - 引数でも
this
でもない非constexpr
な外側の変数を参照する操作を含む関数 - 引数でも
this
でもない外側に副作用を及ぼすような操作を含む関数
constexpr
を使うべき主な場面
- 上記以外全て
変数のconstexpr
変数におけるconstexpr
は、#define
などで作っていたようなコンパイル時定数を作るためのキーワードである。
#include <iostream>
// 標準入力から int 型の値を受け取って返す、(コンパイル時に値が定まらない)関数
auto get_value_from_stdin()
{
int v;
std::cin >> v;
return v;
}
int main(){
auto a = 1; // 普通の変数
const auto b = 2; // Const
constexpr auto c = 3; // Constexpr
a = 4; // OK、 a は後から書き換えてよい
// b = 5; // NG、 b は const なので書き換えてはいけない
// c = 6; // NG、 c は const なので書き換えてはいけない
std::cout << a << ", " << b << ", " << c << std::endl;
auto d = get_value_from_stdin(); // OK、 d は実行時に受け取った値
const auto e = get_value_from_stdin(); // OK、 e は実行時に受け取り、今後変更されない値
// constexpr auto f = get_value_from_stdin(); // NG、 f はコンパイル時に値が確定しなければならない
std::cout << d << ", " << e << /* ", " << f << */ std::endl;
return 0;
}
const
が「この変数は今後変更しませんし、変更しようとしたらコンパイルエラーにしてくださいね」という合図であるのに対し、変数に付けるconstexpr
は「この変数の値はコンパイル時に確定するし、確定しなければコンパイルエラーにしてくださいね」という合図である。
constexpr
変数はconst
変数としても扱われる。
コンパイラは、constexpr
がついた変数の値をコンパイル時に計算しようとする。もし計算できなければ、コンパイルエラーを吐く。
上のコードでは、「標準入力から受け取る」という操作がコンパイル時に行えないため、エラーとなっている。
では、どの操作が「コンパイル時に計算できる」のだろうか?
後述する通り、それは関数にconstexpr
キーワードがついているかどうかで判断されるのである。
関数のconstexpr
constexpr
変数への返り値の代入
constexpr
は関数にも付けることができる。
関数につけたconstexpr
キーワードは、「この関数はコンパイル時に計算することもできますよ」ということを表している。
#include <iostream>
// 常に 42 を返す関数
auto answer() { return 42; }
// 常に 42 を返す、 constexpr な関数
constexpr auto answer_constexpr() { return 42; }
// 常に 42 を返す、 constexpr な関数?
constexpr auto answer_print()
{
// std::cout << 42 << std::endl; // NG、 標準出力への書き出しはコンパイル時には行えない
return 42;
}
int main(){
// constexpr auto a = answer(); // NG、 answer 関数は constexpr 関数ではない
constexpr auto b = answer_constexpr(); // OK、 answer_constexpr 関数は constexpr 関数
// constexpr auto c = answer_stdin(); // NG、 answer_stdin 関数はコンパイルできない
std::cout << /* a << ", " << */ b << /* ", " << c << */ std::endl;
return 0;
}
constexpr
変数に入れられる値は、コンパイル時に計算できる値だけである。
だから、constexpr
キーワードのない関数の返り値をconstexpr
変数に入れようとすると、コンパイラは「この値はコンパイル時には計算できない」と考え、constexpr
変数には入れられないよと怒ってくれる。
また、constexpr
キーワードがある関数の返り値については、関数の中身を見て値を計算しようとし、その過程で同様に「コンパイル時には計算できない」式がひとつでもあれば、やはりconstexpr
変数には入れられないよと怒ってくれる。
また、コンパイル時に計算されているかどうか確認したければ、static_assert
を用いるという手もある。
// 常に 42 を返す関数
auto answer() { return 42; }
// 常に 42 を返す、 constexpr な関数
constexpr auto answer_constexpr() { return 42; }
int main(){
// static_assert(answer() || true); // NG、 answer() はコンパイル時に計算できない
static_assert(answer_constexpr() || true);
return 0;
}
constexpr
ではない引数を与える
constexpr
関数の結果は、常にコンパイル時に計算されるというわけではない。
#include <iostream>
// 標準入力から int 型の値を受け取って返す関数
auto get_value_from_stdin()
{
int v;
std::cin >> v;
return v;
}
// 常に 42 を返す、 constexpr な関数
constexpr auto answer_constexpr([[maybe_unused]] int question)
{
return 42;
}
int main(){
constexpr auto value_constexpr = 1; // Constexpr な値
const auto value_const = get_value_from_stdin(); // Const だが constexpr ではない値
constexpr auto a = answer_constexpr(value_constexpr); // OK、引数が constexpr なので、コンパイル時に値が確定する
// constexpr auto b = answer_constexpr(value_const); // NG、引数が constexpr な値ではないので、コンパイル時に値が確定せず、コンパイルエラー
const auto c = answer_constexpr(value_constexpr); // OK、引数が constexpr なので、コンパイル時に値が確定する
const auto d = answer_constexpr(value_const); // (1) OK、引数が constexpr な値ではないので、コンパイル時に値は確定しないが、コンパイルエラーにはならない
std::cout << a << ", " << /* b << ", " << */ c << ", " << d << std::endl;
return 0;
}
この例では、constexpr
関数の引数value2
がconstexpr
ではない。
この場合でもコンパイルは成功し、answer_constexpr
はあたかもconstexpr
キーワードのない関数かのように(少なくとも(1)の行では)振る舞う。
constexpr
キーワードはあくまで「コンパイル時に値が確定できる」ことを伝えるだけで、「コンパイル時にしか値を計算しない」というわけではない。
引数や返り値のconst
とconstexpr
関数
変数のconstexpr
がconst
を兼ねているのでわかりにくいのだが、関数のconstexpr
は引数や返り値のconst
とは一切関係がない。
#include <iostream>
// OK、常に 42 を返す、 constexpr な関数
constexpr auto answer_constexpr([[maybe_unused]] int question)
{
return 42;
}
// OK、常に 42 を返す、 constexpr な関数
constexpr auto answer_constexpr2([[maybe_unused]] const int question)
{
return 42;
}
// OK、与えられた引数に 1 を足して返す、 constexpr な関数
// 受け取る変数を直接書き換えて返すので何も const じゃないように見えるが、れっきとした constexpr 関数である
constexpr auto& answer_constexpr3(int& question)
{
question += 1;
return question;
}
// OK、 answer_constexpr3 に直接 constexpr な変数は渡せないが、
// 「const でない変数を渡して constexpr 関数を用いて計算する」こと自体は constexpr
constexpr auto use_answer_constexpr3(int question)
{
auto q = question;
q = answer_constexpr3(q);
return q;
}
int main(){
constexpr auto value = 41;
constexpr auto a = answer_constexpr(value);
constexpr auto b = answer_constexpr2(value);
constexpr auto c = use_answer_constexpr3(value);
std::cout << a << ", " << b << ", " << c << std::endl;
return 0;
}
上の例では、answer_constexpr3
関数は「変数の参照を受け取り、破壊的変更を加えて、その参照をconst
も付けずに返す」という、およそconst
性の欠片もない操作を行っている。
しかしながら、この操作は全て(少なくともC++14以降では)constexpr
で行ってよい操作なので、この関数は問題なくconstexpr
関数として作ることができる。
外部変数の参照
constexpr
関数で行なってはいけない操作は、主に引数以外の外部の変数を参照する操作である。
#include <iostream>
// 標準入力から int 型の値を受け取って返す、(コンパイル時に値が定まらない)関数
auto get_value_from_stdin()
{
int v;
std::cin >> v;
return v;
}
namespace sample
{
const auto outer = get_value_from_stdin(); // 外部の変数
constexpr auto outer_constexpr = 42; // Constexpr な外部の変数
// constexpr auto f_outer() { return outer; } // NG、外部の変数を参照しているので constexpr 関数にできない
constexpr auto f_outer_constexpr() { return outer_constexpr; } // OK、 constexpr な変数は参照してもよい
constexpr int& f_argument(int& x)
{
x += 1; // OK、引数になら色々やってもよい
return x;
}
constexpr auto use_f_argument()
{
int x = 41;
return f_argument(x);
}
}
int main(){
// constexpr auto a = sample::f_outer(); // NG、 f_outer はコンパイルできない
constexpr auto b = sample::f_outer_constexpr();
constexpr auto c = sample::use_f_argument();
std::cout << /* a << ", " << */ b << ", " << c << std::endl;
return 0;
}
メンバ関数のconstexpr
メンバ関数にもconstexpr
を付けることができる。
付ける基準は先ほどと同様、「コンパイル時に計算できるかどうか」である。
クラスC
のメンバ関数
class C
{
...
public:
...
auto f(int x, ...) { ... };
};
を、
class C {...};
auto f(C& this, int x, ...) { ... };
のように考えれば、constexpr
を付けるかどうかが判断できるはずだ。
#include <iostream>
class C
{
public:
C() = default;
constexpr C(int a): a(a) {} // メンバ変数の初期化もコンパイル時に可能
// OK、コンパイル時に計算可能
constexpr auto get() const
{
return a;
}
// OK、コンパイル時に計算可能
constexpr auto add(const int b)
{
a += b;
return a;
}
// NG、コンパイル時に標準出力への出力はできない
constexpr auto print() const
{
// std::cout << a << std::endl; // NG
return a;
}
private:
int a;
};
constexpr auto use_add(int a)
{
auto c = C(a);
c.add(42);
return c;
}
int main(){
constexpr auto c = C(42);
std::cout << c.get() << std::endl;
constexpr auto c2 = use_add(42);
std::cout << c2.get() << std::endl;
c2.print();
return 0;
}
分割ファイルとconstexpr
とinline
constexpr
関数は、コンパイル時に計算可能でなければならない。
コンパイル時というのは、それぞれの翻訳単位、すなわち各ファイルのコンパイル時に、どのファイルをコンパイルしているときであっても、関数が計算できなければならない、ということである。
従って、constexpr
関数の宣言だけをして、別ファイルで実装を行なう、といったことはできない。
実装されているファイル以外をコンパイルしているときに、中身が計算できないからである。
#include <iostream>
#include "c.hpp"
int main(){
// OK、 int C1::f(int&) は普通のメンバ関数
auto c1 = C1(41);
auto x1 = 1;
std::cout << c1.f(x1) << std::endl;
/* NG、 int C2::f(int&) は constexpr なのに実装がない
auto c2 = C2(41);
auto x2 = 1;
std::cout << c2.f(x2) << std::endl;
*/
return 0;
}
class C1
{
public:
constexpr C1(int a): a(a) {}
int f(int& x);
private:
int a;
};
class C2
{
public:
constexpr C2(int a): a(a) {}
constexpr int f(int& x);
private:
int a;
};
#include "c.hpp"
int C1::f(int& x)
{
x += a;
return x;
}
constexpr int C2::f(int& x)
{
x += a;
return x;
}
というか、constexpr
関数は自動的にinline
関数として扱われるので、こういうことになるのである。
あまり意識しなくていいとは思うが、constexpr
変数もinline
変数として扱われる。
constexpr
テンプレート関数
細かい話になるが、以下のコードはコンパイルできてしまう。
#include <iostream>
// やりたい放題、明らかに constexpr ではない関数
// 実際、これはコンパイルエラー
/*
constexpr auto print_and_get_int(int t)
{
std::cout << t << std::endl;
int v = 0;
std::cin >> v;
return v;
}
*/
// やりたい放題、明らかに constexpr ではないテンプレート関数
// constexpr キーワードをうっかり付けてしまったが、実はコンパイルエラーにはならない
template<typename T>
constexpr auto print_and_get(T t)
{
std::cout << t << std::endl;
int v = 0;
std::cin >> v;
return v;
}
int main(){
const auto a = print_and_get(42); // 残念ながら OK、constexprキーワードは無視される
// constexpr auto b = print_and_get(42); // NG、constexpr で受けるとコンパイルエラー
std::cout << a << /* ", " << b << */ std::endl;
return 0;
}
実は、constexpr
かつテンプレートな関数を作った場合、その関数を実体化した時にconstexpr
関数として不適格なことが判明しても、コンパイルエラーにはならず、単にその関数が非constexpr
関数として扱われるのだ。
この場合、print_and_get
関数はどのように実体化してもconstexpr
関数にはならない。が、コンパイルエラーにもならない。単にconstexpr
キーワードが無視される。
なので、テンプレート関数にconstexpr
キーワードがついていても、プログラマは「もしかしたらこの関数は(常に)constexpr
関数ではないかもしれない」と身構えなければならない。
おそらく、テンプレートのいくつかの場合でconstexpr
で、かつ他の場合でconstexpr
ではないような関数を作りたい、という需要があるからだと思うのだが、そこは特殊化で対応してほしかったという気持ちである。
constexpr
なラムダ式
ラムダ式(のoperator()
)も、関数と同様にconstexpr
に指定することができる。
というか、指定しなくても勝手にconstexpr
になってくれる。
#include <iostream>
// 標準入力から int 型の値を受け取って返す、(コンパイル時に値が定まらない)関数
auto get_value_from_stdin()
{
int v;
std::cin >> v;
return v;
}
int main(){
// 常に 42 を返す lambda 式
auto lambda = []{ return 42; };
// 常に 42 を返す、 constexpr を明示した lambda 式
auto lambda_constexpr = []() constexpr { return 42; };
// constexpr な変数はキャプチャ可能
constexpr auto outer_value = 42;
auto lambda_capture = [&outer_value]{ return outer_value; };
// constexpr でない変数をキャプチャした場合、 constexpr 関数にはならない
const auto outer_value2 = get_value_from_stdin();
auto lambda_bad = [&outer_value2]{ return outer_value2; };
constexpr auto a = lambda(); // constexpr 関数であることを明示しなくても使える
constexpr auto b = lambda_constexpr(); // 当然明示してもよい
constexpr auto c = lambda_capture(); // OK、この関数は constexpr 関数
// constexpr auto d = lambda_bad(); // NG、この関数は constexpr 関数ではない
std::cout << a << ", " << b << ", " << c << /* ", " << d << */ std::endl;
return 0;
}
まとめ
-
constexpr
は、変数と関数で挙動が大きく異なるので、同一視はしないほうがよい。 -
constexpr
関数とconst
修飾子にあんまり関係がない点にも注意したほうがよい。 - 複雑な関数の場合、「外部に影響を与えるか」「
inline
にできるか」の2点を考えれば、その関数がconstexpr
かどうかがわかるはずである。 - 思っているよりはるかに
constexpr
にできる関数の幅は広いので、どんどん使っていくべき。ただしテンプレート関数をconstexpr
にするときは注意が必要。