assume属性
C++23では、コンパイラに対して最適化のヒントを与える [[assume]]属性が導入されました。これにより、特定の条件下での実行速度の向上や、バイナリサイズの削減(コンパクト化)が可能になります。
[[assume]] 属性は、コード上の特定の箇所において「ある条件が必ず真である」ことをコンパイラに伝えます。これは特定の関数や変数に付与するのではなく、以下のように「空の文」に対して記述します。
int divide_by_64(int x)
{
[[assume(x >= 0)]]; // xが0以上であることを仮定する
return x / 64;
}
この属性は、引数の式がtrueに評価されることを期待します。もしfalseに評価される場合には、結果は不定となります。コンパイラに最適化(gccなら-O3オプションなど)を指示し実行すると、以下のリストのコードは、最初のcoutは問題なく出力されますが、2番目のcoutでは何も出力されません。
このように出力が消失する可能性があるだけでなく、プログラムがクラッシュする可能性もあるなど、仮定から外れたコードは予期せぬ結果を招くことを押さえておいてください。
cout << divide_by_64(64) << endl; // 期待通り:2 cout << divide_by_64(-64) << endl; // 期待外れ:何も出力されない
アセンブリレベルでの最適化
なぜこのようになるのかは、アセンブリコードを出力させてみると分かります(gccでは-Sオプション)。以下のリストは、assume属性なしとありの場合のアセンブリコード出力です。
_Z12divide_by_64i:
.LFB4083:
.seh_endprologue
testl %ecx, %ecx ; 引数(被除数)が負数であるかの判定
leal 63(%rcx), %eax ; 負数の場合の補正値をあらかじめ用意しておく
cmovns %ecx, %eax ; 正数ならそのまま使う
sarl $6, %eax ; 算術的右シフト(6ビット)
ret
除算においては、被除数が負である場合に期待する結果を得るために、除数から1を引いた数(ここでは63)をあらかじめ被除数に加えるという前処理が必要です。これに対して、assume属性ありの場合は以下のリストのようになります。
_Z12divide_by_64i:
.LFB4083:
.seh_endprologue
movl %ecx, %eax
sarl $6, %eax ; 補正なしで即座に右シフト
ret
assume属性により被除数が0以上であることが仮定されているので、負数である場合の前処理が省かれています。これにより、マシンコードがコンパクトになり、かつ不要な計算処理がなくなることによる速度向上が期待できます。
式評価の副作用
[[assume]]属性の中に記述された式は、最適化のヒントとしてのみ利用され、実際に実行(評価)されるわけではありません。そのため、式の中で変数を操作しても副作用は発生しません。
int as_is(int x)
{
[[assume(++x == 64)]]; // xはインクリメントされない
return x;
}
この例では、コンパイラは「この関数が呼ばれるとき、++xが64になる(=つまりxは63である)」と仮定して最適化を行います。結果として、「return x;」は単に定数63を返すコードに置き換わる可能性があります。
エスケープシーケンスの区切り文字
C++ 23では、文字列リテラル中で8進数や16進数などで文字を指定する際に文字波括弧{}で囲む区切り文字が利用可能になりました。これにより、エスケープシーケンスの範囲が明確になり、記述の曖昧さが排除されるとともに可読性が向上しています。
これまでは、8進数や16進数で文字を表現する場合、以下のルールに従う必要がありました。
cout << "\100\608\1000" << endl; // 8進数:\に続く1~3桁 cout << "\x45\x56" "7" << endl; // 16進数:\xに続く任意の桁(型による制限あり) auto str1 = u8"\U0001F1FA"; cout << reinterpret_cast<const char*>(str1) << endl; // Unicode:\u(4桁)、または\U(8桁)
しかし、これらのルールには以下の不便さがありました。
- 8進数:3桁に達するか、0~7以外の文字が出現するまでを範囲とするため、後続の文字との区別がつきにくい。
- 16進数:16進数として解釈可能な文字(0-F)が続く限り、どこまでもエスケープシーケンスと見なされる。そのため、直後に「7」という文字を置きたい場合、コード例のように文字列リテラルを一度区切る必要があった。
- Unicode:コードポイントが5桁の場合でも、上位桁を0で埋めて8桁(\U)にする必要があった。
こういった背景から、C++ 23では、エスケープシーケンスの範囲を明確にできる区切り文字「{ }」が利用可能になりました。上記のリストは、以下のように書き換えることができます(出力結果は同じ)。
cout << "\o{100}\o{60}8\o{100}0" << endl; // 8進数:\o{ }を使用
cout << "\x{45}\x{56}7" << endl; // 16進数:\x{ }を使用(後続の文字"7"と混同されない)
auto str2 = u8"\u{1F1FA}"; // Unicode:\u{ }を使用(桁数の固定が不要)
cout << reinterpret_cast<const char*>(str2) << endl;
8進数の場合は、新たに導入された\oを前置することに注意してください。これにより、どこまでが数値指定なのかが一目でわかるようになり、Unicode指定においてもゼロ埋めの手間がなくなりました。
名前付きユニバーサルキャラクタ名
Unicodeに関連して、C++23ではコードポイントの代わりに「キャラクタ名」で指定できる \N{ } も導入されました。
auto str3 = u8"\N{LATIN SMALL LETTER A WITH BREVE}\N{COMBINING ACUTE ACCENT}";
cout << reinterpret_cast<const char*>(str3) << endl;
これによって、難解なコードポイントの数値を調べることなく、標準的な名称を用いてUnicode文字を埋め込むことが可能になりました。利用可能な名前の種類は膨大なので、詳細はUAX44-LM2などを参照してください。
まとめ
今回は、C++ 23における言語仕様面の強化/変更点を、静的なoperator()/operator[]、多次元対応operator[]、assume属性、エスケープシーケンスの区切り文字を中心に紹介しました。
次回は、Rangesライブラリと、コルーチンベースのジェネレータstd::generatorを紹介します。
