コンパイル時に条件判定できるconsteval if文
C++ 23では、constexpr文脈として呼ばれているかを判断するconsteval if文により、constexpr関数から即時関数を呼び出せるようになりました。
C++ 23で導入されたconsteval if文を使うと、constexpr文脈におけるconsteval関数の呼び出しが可能になります。まずは、consteval if文の前提となるconstexprとconstevalについて、簡単におさらいしておきます。
constexprとconsteval
C++ 11で導入されたconstexprは、定数式を表現するための機能です。
constexprを利用すると、コンパイル時に決まる定数、コンパイル時に実行できる関数、コンパイル時にリテラルとして振る舞うクラスを定義できます。いずれもコンパイル時にというのがポイントで、実行フェーズでは最終的に評価済みのものを使うので高速化できます。
constexprが指定された関数は、コンパイル時と実行時の双方で評価されます。通常の関数としても、コンパイル時にのみ呼び出される関数としても使うことができるのです。
以下のリストは、べき乗を計算するpow関数をconstexpr関数として定義しています(1)。(2)ではconstexpr文脈でpowが呼び出されるので、pow_constはリテラルとしての125となります。
これに対して(3)では非constexpr文脈であり、通常の関数呼び出しとしてpow_dynamicは変数値としての125となります。
constexpr int pow(int base, int exp) { // (1)constexpr関数の定義
int result {1};
for (int i = 0; i < exp; ++i) {
result *= base;
}
return result;
}
…略…
constexpr int pow_const = pow(5, 3); // (2)constexpr文脈で呼び出す
cout << pow_const << endl; // 125
int num = 5;
int pow_dynamic = pow(num, 3); // (3)非constexpr文脈で呼び出す
cout << pow_dynamic << endl; // 125
これに加えて、C++ 20ではconstevalが導入されました。constevalを指定された関数はコンパイル時にのみ評価される関数となります。これを「即時関数」と言います。
即時関数の評価結果はconstexpr文脈で受け取る必要があり、即時関数を呼び出せるのは即時関数のみです。
以下のリストは、pow関数を即時関数として定義したので(1)(2)は問題ないものの、(3)ではコンパイルエラーになります。
consteval int pow(int base, int exp) { // (1)即時関数の定義
…略…
constexpr int pow_const = pow(5, 3); // (2)constexpr文脈で呼び出せる
int num = 5;
int pow_dynamic = pow(num, 3); // (3)非constexpr文脈ではコンパイルエラー
comstexpr関数の中でconsteval関数を呼び出すなど、組み合わせて使う際に不都合なところがあるため、定数式として評価されているかを判定できるconsteval if文が利用可能になりました。
consteval if文
consteval if文を使うと、以下のリストのようにコンパイル時と実行時のコードを分けることができます。
constexpr int fractional(int a) {
if consteval { // (1)コンパイル時か実行時か仕分ける
return (a == 0)? 1 : fractional(a - 1) * a; // (2)コンパイル時のコード
} else {
int r = 1; // (3)実行時のコード
while (a > 0) {
r *= a;
--a;
}
return r;
}
}
…略…
constexpr int static_fractional = fractional(5); // (4)constexpr文脈
cout << static_fractional << endl; // 120
int dynamic_fractional = fractional(5); // (5)非constexpr文脈
cout << dynamic_fractional << endl; // 120
fractional関数は階乗を求める関数です。constexprの指定で、コンパイル時でも実行時でも評価される関数になっています。
(1)のif consteval(consteval if文)により、コンパイル時には(2)のブロックが、実行時には(3)のブロックが、それぞれ評価されます。これにより、(4)ではconstexpr文脈(2)の評価結果、(5)では非constexpr文脈(3)の評価結果が、それぞれ得られることになります。
明示的なオブジェクトパラメータthis
C++ 23では、オブジェクトパラメータthisを、メンバ関数の引数に明示的に宣言できるようになりました。
C++ 23において、オブジェクトパラメータthisを明示的にメンバ関数の引数に指定できるようになりました。オブジェクトがconst/非constなのか、左辺値(&)なのか右辺値(&&)なのかを明示できるようになり、どのような文脈で呼ばれる関数なのかが分かりやすくなります。
[NOTE]左辺値と右辺値
左辺値(lvalue)は「特定の記憶場所を示す式」で、変数がその代表です。右辺値(rvalue)は「記憶位置を持たない値」で、関数の戻り値や計算式の評価結果などが相当します。代入演算子の左辺か右辺かには関係なく、あくまでも値の性質であるということを理解しましょう。なおC++ 11以降は、参照型「T&」は左辺値参照と呼び、「T&&」は右辺値参照と呼びます。
従来のthis――暗黙のthis
クラスや構造体のメンバ関数には、暗黙の引数としてthisが自動的に渡されるようになっており、thisを通じてオブジェクトのメンバへアクセスすることが可能です。これは「暗黙のthis」と呼ばれます。
以下のリストの関数は、引数にはthisはありませんが、処理内容にはthisが使われています。
struct person {
string name;
int age;
person(string n, int a) : name(n), age(a) {}
string get_name() const { // (1)
return this->name; // (2)
}
void set_name(string name) {
this->name = name;
}
}
…略…
person p("Nao Yamauchi", 30);
cout << p.get_name() << endl; // Nao Yamauchi
cout << p.get_age() << endl; // 30
p.set_name("Shino Onodera");
p.set_age(25);
cout << p.get_name() << endl; // Shino Onodera
cout << p.get_age() << endl; // 25
thisはポインタであり、(2)のようにアロー演算子(->)を使ってメンバにアクセスします。また、関数がオブジェクトを変更するかどうかは、(1)のように関数宣言自体にconst修飾子を付与します。
明示的なthis
オブジェクトパラメータthisを明示的にメンバ関数の引数に指定すると、以下のリストのようにオブジェクトのconst/非const、lvalue/rvalueを明示できます。
auto get_name(this const person& self) { // (1)明示的なthis(const)
return self.name; // (2)「self.」でアクセスする
}
void set_name(this person& self, string name) { // (3)明示的なthis(非const)
self.name = name;
}
明示的オブジェクトパラメータは、(1)のようにthisキーワードを付けて、あとは通常の引数と同様に宣言します。引数の名前は、他言語でも用いられるselfを使うことが多いようです。selfを使ったメンバーへのアクセスは、(2)のようにドット演算子(.)を使います。
(1)と(3)では、selfの型が異なります。(1)は参照のみで不変であるのでconst修飾されており、(3)は更新するためconst修飾子は付きません。このように、どのようにオブジェクトを操作するのかということが明確になり、オーバーロードも可能になるなどのメリットが生まれます。
なお、この機能の元となった提案書P0847R7では、機能の名称が「Deducing this」(推論するthis)となっていますが、意味的には「Explicit this」(明示的なthis)の方が通りがよさそうです。
まとめ
今回は、C++ 23における言語仕様面の強化/変更点を、consteval if文と明示的オブジェクトパラメータthisを中心に紹介しました。
次回は、今回に引き続き言語仕様面の強化/変更点を、静的なoperator()/operator[]、多次元対応operator[]を中心に紹介します。
