静的なoperator()/operator[]
C++ 23では、関数オブジェクトやラムダ式の定義において、静的な(staticな)operator() および operator[] が利用可能になりました。
C++ 23では、operator()/operator[]関数にstatic修飾子を付与して、オブジェクトの状態に関与しない演算子として定義できるようになりました。
基本的なoperator()とoperator[]
まずは、これらの関数について簡単におさらいします。operator()は関数呼び出し演算子、operator[]は添え字演算子のオーバーロードです。これらの関数は、通常は暗黙のオブジェクトパラメータとしてthisを受け取り、自身と引数の演算結果を返します。
以下のリストは、float型の配列に相当する構造体を定義し、演算子をオーバーロードして関数オブジェクトや添え字によるアクセスを可能にする例です。
struct float_array {
float data[10]; // 1次元配列
float operator()(int index) { // 関数オブジェクトの定義
return this->data[index]; // thisでアクセスできる
}
float& operator[](int index) { // 添え字演算子の定義
return this->data[index];
}
};
…略…
float_array arr = {1.0f, 2.0f, 3.0f, 4.0f};
cout << arr(0) + arr(1) + arr(2) + arr(3) << endl; // 10、関数オブジェクト
arr[2] = 10.0f; // 添え字で代入
cout << arr[2] << endl; // 10
static operator()とoperator[]
C++23で導入されたstatic修飾子を付けたoperator()とoperator[]を使うと、演算子がオブジェクトの状態を必要としない場合に、thisポインタの受け渡しに要するコストを省くことができます。
struct positive {
static bool operator()(int x) { // staticを付与
return x > 0; // thisは使えない
}
static bool operator[](int x) {
return x > 0;
}
};
…略…
positive p;
cout << p(10) << endl; // 1
cout << p[-10] << endl; // 0
ラムダ式においてもstatic修飾子を付与できます。ラムダ式は、内部的には匿名構造体におけるoperator()関数のオーバーロードとして定義されるためです。
auto pf = [](int x) static -> bool { return x > 0; };
cout << pf(10) << endl; // 1
多次元対応の添え字演算子operator[]
C++ 23では、添え字演算子operator[]が多次元対応になり、多次元配列などを直感的に定義できるようになりました。
これまで、ユーザー定義型で多次元配列などの挙動を実現するには、array[1][3] のように演算子を重ねるか、array(1, 3) のように関数呼び出し演算子で代用する必要がありました。C++23からは、array[1, 3] という自然な記述で要素にアクセスできます。
従来の多次元配列
C++の標準的な配列は基本的に1次元であり、多次元配列を表現する場合は「配列の配列」として宣言します。
float array[2][3] = { // 2次元配列(3×2)を初期化
{1.0f, 2.0f, 3.0f},
{4.0f, 5.0f, 6.0f}
};
cout << array[1][2] << endl; // 行、列の順に添え字を指定する
この例では「2行×3列」の配列という構造になります。要素を指定するには、array[行のインデックス][列のインデックス]のように、二重添え字を用います。
このとき、プログラマは「この配列が何行何列で、インデックスが行優先か列優先か」「どの引数が行でどの引数が列か」を常に意識してコーディングしないと、意図しない位置を参照したり、入れ替えを間違えたりといった不具合を起こしやすくなります。
例えば行と列を逆に扱ってしまう、あるいはメモリ上の並び(つまり「行ごとに要素が並んでいる」)を考慮せずループを回すと、パフォーマンスや正しさに影響することもあります。
多次元対応のoperator[]
C++23の多次元対応 operator[] を使うと、内部構造を隠蔽しつつ、自由度の高い多次元アクセスを実装できます。
struct array_2d {
float data[3 * 2]; // (1)実データは6要素の1次元配列で構成
float& operator[](int x, int y) { // (2)多次元対応のoperator[]を定義
return data[x + y * 3];
}
};
…略…
array_2d array = { // 配列を初期化
1.0f, 2.0f, 3.0f,
4.0f, 5.0f, 6.0f
};
cout << array[2, 1] << endl; // (3)配列の添え字としてアクセスできる
多次元対応の添え字演算子operator[]を使うとき、(1)のように実際の配列は多次元である必要はありません。(2)の添え字演算子のオーバーロードで、2つの引数を持つoperator[]関数を定義することで、(3)のように2つの添え字を指定できるようになります。
この例では、1番目の引数を列(x)、2番目を行(y)とみなし、「列+行×3」(3は行当たりの列数)の演算によって適切な要素を返しています。
operator()による多次元対応
多次元対応のoperator[]が使えるようになるまでは、関数呼び出し演算子operator()による代替手段(ブラケットではなく丸カッコ)がありました。
float& operator()(int x, int y) {
return data[x + y * 3];
}
cout << array(2, 1) << endl; // 10
array(2, 1) = 7.0f;
cout << array(2, 1) << endl; // 7
しかし、丸カッコによるアクセスは「それがコンテナの要素参照なのか、あるいは何らかの計算処理(関数呼び出し)なのか」がコードから判別しづらいという難点がありました。C++23で [] が多次元対応したことで、「これはコンテナの要素へのアクセスである」という意図を明確に示すことができるようになります。
