以下の内容はhttps://onihusube.hatenablog.com/entry/2025/02/24/215818より取得しました。


[C++]WG21月次提案文書を眺める(2024年09月)

文書の一覧

全部で51本あります。

もくじ

N4990 Business Plan and Convener's Report

ビジネスユーザ向けのC++およびWG21の現状報告書。

P0472R2 Put std::monostate in <utility>

std::monostate<utility>からも利用できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、std::nullptr_tをその代替として使用しない理由の説明と、提案する文言の書き直しなどです。

std::nullptr_tをその用途に使わない理由として、std::monostateの方が誤用が少ないからと説明しています。std::nullptr_tは任意のポインタ型に対してコピー可能で、ストリーム出力(C/C++双方)も動作します。std::monostateはどのような代入や変換も不可能であるためこのような誤用の可能性が著しく低く、何もできないことを表す型として適しています(Cのストリームには出力できるようです)。

P1030R7 std::filesystem::path_view

パス文字列を所有せず参照するstd::filesystem::path_viewの提案。

以前の記事を参照

このリビジョンでの変更は

  • 最新のWDにリベース
  • render_*()メンバ関数const修飾されていなかったのを修正
  • c_str()の要件はnull終端のみとなり、その他の事前条件は不要になった
  • [[nodiscard]]を削除
  • <filesystem>から分離され、<path_view>ヘッダを新設し移動
  • path-view-likeによって、ソースコードの変更なしに既存のコードがこの提案で追加された新しいフリー関数を呼び出すことは無いことの証明を追記
  • path_viewに対して将来的にABIを破損することなく新しいエンコーディングを追加できることの証明を追記
  • filesystem::pathのフォーマッタを複製する形でフォーマッタを追加
  • エラーコード引数を持つフリー関数の多くがnoexceptである理由についての文言を追加
  • path_viewの使用例を追加
  • path_viewの比較演算子を追加
  • <=>演算子と削除定義されている演算子の相互作用に関するテストを実施
  • filesystem::pathをとるiosteream関数についての文言を調整

などです。

P1061R9 Structured Bindings can introduce a Pack

構造化束縛可能なオブジェクトをパラメータパックに変換可能にする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整と実装経験セクションを更新したことです。

この提案はこの次のリビジョンが2024年11月の全体会議で採択されています。

P2019R7 Thread attributes

std::thread/std::jthreadにおいて、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。

このリビジョンでの変更は

  • char8_tサポートを削除
    • スレッド名の指定でchar8_t文字列を指定できていたが禁止
    • wchar_t/char16_t/char32_tのサポートは意味がないとしてされていなかったがchar8_tはされていたものの、議論不足のための措置であり、将来に追加する事にして別の提案で議論する
    • ABI互換性確保のために、名前を受け取るAPIではbasic_string_view<T>を取るようにする

などです。

P2287R3 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

以前の記事を参照

このリビジョンでの変更は

  • 指示子(designator)なしで基底クラスのサブオブジェクトを直接初期化する構文の追加
  • 実装経験の追記

このリビジョンではR2で可能だった構文に加えて、基底クラスのサブオブジェクトを直接初期化する事を許可しました。特に、直接のメンバを指示付き初期化しながら、基底クラスのサブオブジェクトは通常の初期化を行う、ということが可能になっています。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  // R0で提案されていた構文、R2で削除(このリビジョンでも不可
  B b1{:A = {.a = 1}, b = 2};
  B b2{:A{.a = 1}, b = 2};
  B b3{:A{1}, .b{2}};

  // R1で追加され、R2でも可能な構文
  B b4{.a = 1, .b = 2};
  B b5{.a{1}, .b{2}};

  // このリビジョンで追加された構文
  B b6{A{1}, .b = 2};
}

特に、派生クラスと基底クラス(どちらも集成体型とする)で同じメンバ変数名を持つ場合に区別して初期化することができるようになります

struct D { int x; };
struct E : D { int x; };

int main() {
  auto e1 = E{.x=1};         // E::xを1で初期化、D::xではない
  auto e2 = E{{.x=1}, .x=2}; // D::xを1で、E::xを2で初期化(R3で追加
  auto e3 = E{D{1}, .x=2};   // 同上(R3で追加
}

また、この形式によって非集成体の基底クラスが含まれている場合にそのクラスだけ非指示付き初期化によって初期化することができるようになります

struct C : std::string { int c; };

// どちらも同じ意味、ok
auto c1 = C{"hello", .c=3};
auto c2 = C{{"hello"}, .c=3};

ただし、指示付き初期化によって初期化できるのは集成体型のみです。

struct A { int a; };
struct B : A { int b; };
struct C : A { C(); int c; };
struct D : C { int d; };

A{.a=1};       // C++20からok
B{.a=1, .b=2}; // この提案、'a' は集成体型の直接のメンバであり、Aは基底クラス
C{.c=1};       // error: Cは集成体型ではない
D{.a=1};       // error: 'a' は集成体型の直接のメンバだが、中間の基底クラスCは集成体型ではない

この提案では、現在の指示付き初期化時の識別子に対する制約を拡張して、指示付き初期化しようとしている型Tの直接の非静的メンバ変数に加えて、全ての間接メンバ(集成体型基底クラスの非静的メンバ変数名)もその対象に加えています。

また、このリビジョンでは、指示付き初期化において基底クラス名を指定して初期化する方法を考え出すことをやめており、メンバを指定する間接的な初期化方法をサポートすることを目指しています(ただし、それは直交した問題であり後からでも可能としています)。

P2319R1 Prevent path presentation problems

filesystem::path.string()メンバ関数を非推奨にする提案。

以前の記事を参照

このリビジョンでの変更は、.string()メンバ関数の非推奨化のみに主眼を置いて、代替関数の追加をやめたことです。

P2688R2 Pattern Matching: match Expression

C++へのパターンマッチング導入に向けて、別提案との基本構造の違いに焦点を当てた議論。

以前の記事を参照

このリビジョンでの変更は

  • 実装経験の追加
  • 提案する文言を追加
  • match演算子の優先順位を定義
  • リフレクションベースのtuple-like/variant-likeなプロトコルを提案してないことを決定

などです。

P2786R7 Trivial Relocatability For C++26

trivially relocatableをサポートするための提案。

以前の記事を参照

このリビジョンでの変更は

  • EWG/LEWGでの懸念の提起に対処するために大幅に書き直し
  • trivial relocatabilityの提示と議論を簡素化
  • swapに関する議論を統合(P3239R0から
  • 動作の変更
    • ユーザー定義のムーブ代入演算子によって、型が暗黙的にtrivial relocatableにならないようになった
    • コンテキスト依存キーワード
      • 改訂されたセマンティクスを適切に表現するために、memberwise_trivially_relocatableという新しい名前が付けられた
      • オプトインのみ
      • 基底クラスとメンバのrelocatabilityを推定する
    • コンテキスト依存キーワードの後に述語が続かないため、オプトアウトの方法はない
    • 新しいrelocate()関数は非トリビアルな型と定数評価をサポートする
  • P3239R0からの動作の変更
    • trivial swappabilityは置換可能であることとtrivially relocatableに基づいている
    • memberwise_replaceable(コンテキスト依存キーワード)の追加
    • swap_value_representations()関数と新しいプロパティにより、std::swapを最適化する

などです。

P2835R5 Expose std::atomic_ref's object address

P2835R6 Expose std::atomic_ref's object address

std::atomic_refが参照しているオブジェクトのアドレスを取得できるようにする提案。

以前の記事を参照

R5での変更は

  • P3309とP3323を考慮して更新
  • 戻り値型をT*に戻す
    • constexprをサポートする唯一の設計であるため
  • 既存のポインタを返すAPIを参考に名前を変更

このリビジョンでの変更は

  • LEWGは戻り値型としてT*を確認
  • LEWGは名前としてaddress()を選択

などです。

P2841R4 Concept and variable-template template-parameters

コンセプトを受け取るためのテンプレートテンプレートパラメータ構文の提案。

以前の記事を参照

このリビジョンでの変更は

  • テンプレートコンセプトパラメータを参照する関数テンプレートは、包摂の対象外とした
  • 制約の正規化のセクションを改善
  • 例を改善して追加
  • 新しい文法要素の名前を変更
  • テンプレートパラメータとそのパックを紹介するセクションを追加
  • id式を使用しないようにテンプレート引数の文言を変更
  • コンセプト依存制約の導入
  • CWGフィードバックへの対応

などです。

P2846R3 reserve_hint: Eagerly reserving memory for not-quite-sized lazy ranges

遅延評価のため要素数が確定しない range の ranges::to を行う際に、推定の要素数をヒントとして知らせる ranges::reserve_hint CPO を追加する提案。

以前の記事を参照

このリビジョンでの変更は、reserve_intstd::vectorで使用することを義務付ける文言を追加したことです。

P2879R0 Proposal of std::dump

指定された任意個数の引数をスペース区切りで出力する関数であるstd::dump()の提案。

ここで提案されているstd::dump()std::print()のラッパーであり、std::dump(arg1, arg2, ..., argn)std::println("{} {} ... {}", arg1, arg2, ..., argn)と等価になります。

std::dump(“Hello, World!”); // output: Hello, World!
std::dump(2 + 2);           // output: 4
std::dump(1, 2, 3, 4, 5);   // output: 1 2 3 4 5

int x = 10, y = 20, z = 30;
std::dump(x, y, z);         // output: 10 20 30

モチベーションとしては

  • 他のプログラミング言語での既存の慣習とする
    • Pythonprint(a, b, c)std::dump(a, b, c)と書ける
  • 簡単なテスト、デモ、実験プログラムでの利用
    • 短いプログラムや一時的なコードで、変数の値を手軽に出力するのに便利
  • コード例の簡潔化
    • std::print({}, {}, {}, ...)std::cout << ... << ...等のように余計な文字列を省いて例示できる
  • デバッグの補助
    • デバッガを使用せずに実行時に変数値を確認するための簡単なコードとして使用可能
    • デバッガが使えない環境や、リアルタイム制約がある環境(一時停止で動作が変わる環境)において、一時的なprintデバッグのために活用できる
  • 科学計算での利用
    • 行が改行で区切られ、数値がスペースで区切られた形式は、行列や表の一般的な形式であり、科学計算での利用に適している
      • これはstd::dump()が生成する形式
  • Unixツールとの連携
    • UNIX環境で一般的なトークンベースのCLIツールはstd::dump()が生成するシンプルな形式で動作する

などが挙げられています。

この提案はLEWGIにおいて時間をかけることに合意が得られませんでした。

P2945R1 Additional format specifiers for time_point

<chrono>time_point型のフォーマット指定を追加する提案。

以前の記事を参照

このリビジョンでの変更は、既存のコードの意味を変更するオプションをすべて削除したことです。

R0では%Sオプションの動作の変更(秒を2桁で出力し、ミリ秒未満を出力しないようにする)を提案していましたが、このリビジョンでは削除されたため、この提案は純粋な機能拡張のみとなりました。

P2988R7 std::optional<T&>

std::optionalが参照を保持することができるようにする提案。

以前の記事を参照

このリビジョンでの変更は

  • 右辺値参照型の特殊化(std::optional<T&&>)を削除したこと
  • 変換代入演算子の追加
  • 変換in-placeコンストラクタの追加

などです。

P3016R4 Resolve inconsistencies in begin/end for valarray and braced initializer lists

std::valarrayと初期化子リストに対してstd::beginstd::cbeginを呼んだ場合の他のコンテナ等との一貫しない振る舞いを修正する提案。

以前の記事を参照

このリビジョンでの変更は

  • LWG Issue 4131の変更(Issue解決)の文面を追加
    • 提案はしていない

などです。

P3019R9 Vocabulary Types for Composite Class Design

動的メモリ領域に構築されたオブジェクトを扱うためのクラス型の提案。

以前の記事を参照

このリビジョンでの変更は

  • コンストラクタの順序を変更
  • indirectに変換代入演算子を追加
  • indirectpolymorphicに変換コンストラクタを追加
  • indirectpolymorphicに初期化子リストコンストラクタを追加
  • ‘heap’や‘free-store’等の用語の使用を‘dynamically-allocated storage’に変更

などです。

P3037R3 constexpr std::shared_ptr

std::shared_ptrを定数式でも使える様にする提案。

以前の記事を参照

このリビジョンでの変更は

  • reinterpret_pointer_castからconstexprを取り除いた
  • 関連提案への参照の追加
  • libc++ベースの2つ目の実装経験の追加

などです。

除外されたのは、例外やreinterpret_castなどの定数式では実行できない操作を含むものです。

P3074R4 trivial unions (was std::uninitialized<T>)

定数式において、要素の遅延初期化のために共用体を用いるコードを動作するようにする提案。

以前の記事を参照

このリビジョンでの変更は、以前に提案していた2つのうちの1つ"Just make it work"の提案に絞ったことと、実装経験を追加したことです。

"Just make it work"の提案は前のリビジョンのtrivial unionの機能性を現在のunionのまま有効化するものです。すなわち

  • デフォルトコンストラクタは無条件でトリビアル
    • デフォルトメンバ初期化子がある場合、削除される
  • 最初のメンバがimplicit-lifetime typeである場合、デフォルトコンストラクタはそのメンバの生存期間を開始しそれをアクティブメンバとする
    • 初期化はされない
  • デフォルトコンストラクタは無条件でトリビアル

の3点が、現在の構文のままで動作が変更されるようになります。

// トリビアルデフォルトコンストラクタを持つ (sの生存期間を開始しない、初期化もされていない)
// トリビアルデストラクタを持つ
// (現在: デフォルトコンストラクタとデフォルトデストラクタはどちらも削除される)
union U1 { string s; };

// デフォルトコンストラクタは定義されるもののトリビアルではない
// デストラクタは削除される
// (現在: デストラクタは削除される)
union U2 { string s = "hello"; }

// トリビアルデフォルトコンストラクタを持つ(sの生存期間を開始する
// トリビアルデストラクタを持つ
// (現在: デフォルトコンストラクタとデフォルトデストラクタはどちらも削除される)
union U3 { string s[10]; }

この提案でほしかったものは、定数式で使用可能な遅延初期化用ストレージでした。単一メンバの共用体は遅延初期化用ストレージは供給可能なものの定数式で使用可能ではなくコンストラクタ/デストラクタを定義するとトリビアル性が失われるという問題がありましたが、この提案によりメンバ型に関わらずコンストラクタ/デストラクタを定義しなくてもunionとしてのトリビアルなそれら(なにもしない)が宣言されるようになるとともに、implicit-lifetime typeである場合に生存期間を開始するようになることで定数式でも使用可能になります。

P3096R3 Function Parameter Reflection in Reflection for C++26

C++26に向けた静的リフレクションに対して、関数仮引数に対するリフレクションを追加する提案。

以前の記事を参照

このリビジョンでの変更は

  • 提案する文言の改善
  • P2996R5の内容を反映

などです。

P3128R1 Graph Library: Algorithms

P3128R2 Graph Library: Algorithms

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、提案するグラフアルゴリズムについてまとめた文書。

R1での変更は

このリビジョンでの変更は、コントリビューターを追加したことです。

P3210R2 A Postcondition is a Pattern Match

事後条件の構文をパターンマッチングの構文に親和させる提案。

以前の記事を参照

このリビジョンでの変更は

  • 提案1の内容を、P2737のキーワード構文を採用するものではなく、P2688のバインディング構文に一致させる、に変更
  • 他の部分をそれに合わせて変更
  • モチベーションの明確化

などです。

参照する提案が変わっただけで、提案の内容そのものは大きく変化していません。

変更後の内容もSG21でのコンセンサスを得られず、リジェクトされています。

P3245R2 Allow [[nodiscard]] in type alias declarations

[[nodiscard]] 属性を型エイリアス宣言で使用できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言を追加したことです。

この提案は、EWGのレビューにおいてコンセンサスを得られず、リジェクトされています。

P3248R2 Require [u]intptr_t

(u)intptr_tを必須にする提案。

以前の記事を参照

このリビジョンでの変更は、SG1での投票結果を追記したことです。

P3290R2 Integrating Existing Assertions With Contracts

既存のアサーション機構に契約プログラミング機能を統合する提案。

以前の記事を参照

このリビジョンでの変更は、バグや説明の修正とライブラリAPIの使用例を追加したことです。

P3295R1 Freestanding constexpr containers and constexpr exception types

フリースタンディング環境の定数式において、std::vector等を使用可能にする提案。

以前の記事を参照

このリビジョンでの変更は

  • 実装経験についての議論を追加
  • <stdexcept>のすべての例外型を追加
  • デフォルト構築されたコンテナとoperator deleteについての議論を追加
  • 残りの非constexpr関数について追記
  • アロケータ要件の難しさについて議論を追加
  • constexpr例外の提案とアロケータの提案とのコンフリクトについて追記
  • 文言の改善

などです。

P3299R1 Range constructors for std::simd

std::simdrangeコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は

  • 初期化に使用する範囲のサイズが実行時にしか分からない場合で、サイズのミスマッチが検出された場合の動作をerroneous behaviourとする方針の説明の追加
    • 足りない場合、余った要素はデフォルト初期化し、読み出しはEBとする
  • 例を追加

などです。

P3309R2 constexpr atomic and atomic_ref

std::atomic/stomic_refconstexpr化する提案。

以前の記事を参照

このリビジョンでの変更は、wait(), notify()関数の動作についての説明を追加したことです。

コンパイル時はシングルスレッドであるため、wait()は何もせず、notify()は現在値と異なる値で待機するとデッドロック(定数式におけるループ制限によってコンパイルエラー)します。

P3335R1 Structured Core Options

コンパイラフロントエンドの共通コマンドラインオプション構文の提案。

以前の記事を参照

このリビジョンでの変更は

  • デバッグオプションを削除
  • 最適化オプションのうちsafeを削除
  • ファイル名をキーとして使用しないようにする
  • singly-typedな値のみを使用する

などです。

P3371R1 Fix C++26 by making the rank-1, rank-2, rank-k, and rank-2k updates consistent with the BLAS

<linalg>の一部の関数を対応するBLAS関数の仕様と整合させる提案。

以前の記事を参照

このリビジョンでの変更は

  • 提案する文言の変更
    • symmetric/hermitian rank-k/rank-2k update 関数を上書きと更新のオーバーロードに変更するのに加えて、rank-1およびrank-2 update系関数も同様に変更する
    • 追加する全ての更新オーバーロードについて、入力行列のC(またはA)がEエイリアスすることを許可
    • 追加するsymmetric/hermitian updateのオーバーロードについて、関数がE引数にC(またはA)と同じ方法でアクセスすることを指定
      • 同じ方法とは例えば、上三角行列や下三角行列として
    • hermitian rank-1/rank-k update関数に必要な、スケーリング係数を非複素数に制限するための説明専用コンセプトnoncomplexを追加
  • 文言の変更に合わせてタイトルと概要を変更
  • rank-1/rank-2 update関数をrank-k/rank-2k update関数と同様に変更する理由を説明するセクションを追加
  • hermitian_matrix_vector_producthermitian_matrix_producttriangular_matrix_productおよびtriangular_*_solve関数を更新しない理由を説明するセクションの追加
  • 実行時ではなく、コンパイル時に一部のスケーリング係数を非複素数に制限する理由を説明するセクションを追加
  • 説明セクションを拡充し、再編成

などです。

R0での対象は、rank-kおよびrank-2k update系の関数

  • linalg::symmetric_matrix_rank_k_update(): C := C + αAA^T
  • linalg::hermitian_matrix_rank_k_update(): C := C + αAA^H
  • linalg::symmetric_matrix_rank_2k_update(): C := C + αAB^H + αBA^H
  • linalg::hermitian_matrix_rank_2k_update(): C := C + αABH + ᾱBAHᾱα複素共役

のみでしたが、このリビジョンではrank-1およびrank-2 update系の関数

  • symmetric_matrix_rank_1_update(): A := A + αxx^T
  • hermetian_matrix_rank_1_update(): A := A + αxx^H
  • symmetric_matrix_rank_2_update(): A := A + αxy^T + αyx^T
  • hermitian_matrix_rank_2_update(): A := A + αxy^H + ᾱxy^H

も対象に加わりました。これらの関数は現在無条件の更新を行っており、上書き動作が無くスケーリング係数βを一項目のAに適用する方法がありません(これもやはり、対応するBLASのルーチンと動作が一致しない)。そのため、rank-kおよびrank-2kの関数と同様に、デフォルトの動作を上書きに変更したうえで、更新オーバーロードを追加することを提案しています。

結局、このリビジョンでは次の3つの事を提案しています

  1. rank-1, rank-2, rank-2, rank-2k update系関数に、更新オーバーロードを追加
  2. 現在のrank-1, rank-2, rank-2, rank-2k update系関数の動作を、無条件更新から上書きに変更
  3. hermitian_rank_1_update(), hermitian_rank_k_update()においては、Scalarテンプレートパラメータを非複素数になるように制限する
    • これにより、このupdate動作がエルミートになることが数学的に保証される

2と3はどちらも破壊的変更となるため、C++26正式策定までにこの作業を完了する必要がある、としています。

P3372R1 constexpr containers and adapters

標準のコンテナを全てconstexprにする提案。

以前の記事を参照

このリビジョンでの変更は、std::less<T*>の実装における懸念について追記したことです。

std::less<T*>の実装における懸念とは、std::hash<T*>と同様の問題で、定数式におけるポインタの実装定義全順序比較結果が実行時のものと一致する保証が無い(実行時のアドレス値を知る方法がないので当然)というものです。

P3375R0 Reproducible floating-point results

計算結果の再現性の保証された浮動小数点数型の提案。

C++標準では、浮動小数点数の動作(計算)のほとんどの部分は実装定義とされており、同じ計算式が異なるプラットフォームで同じ結果をもたらすとは限りません。

例えば次のようなコードは実装によって異なる結果を生成します。

int main() {
    float line_0x(0.f);
    float line_0y(7.f);

    float normal_x(0.57f);
    float normal_y(0.8f);

    float px(10.f);
    float py(0.f);

    float v2p_x = px - line_0x;
    float v2p_y = py - line_0y;

    float distance = v2p_x * normal_x + v2p_y * normal_y;

    float direction_x = normal_x * distance;
    float direction_y = normal_y * distance;

    float proj_vector_x = px - direction_x;
    float proj_vector_y = py - direction_y;

    std::cout << "distance:      " << distance << std::endl;
    std::cout << "proj_vector_y: " << proj_vector_y << std::endl;
}

オプションなしでNvidia C++ compiler 24.5でビルドすると

distance:       0.0999997
proj_vector_y: -0.0799998

オプションなしでclang 18.1.0でビルドすると

distance:       0.0999999
proj_vector_y: -0.0799999

-march=skylakeを指定してclangでビルドすると

distance:       0.1
proj_vector_y: -0.08

のようになります。これは出力が異なっているのではなく、実際にビットレベルで異なった結果となっています。特に、最適化を有効にするとプログラム中で実行される浮動小数点演算の性質・数・順序が実装ごとに異なり、これによって丸め演算の回数や順序が異なってくることから、プラットフォームによって異なった値を生成します。

ISO/IEC 60559:2020(IEEE 754)では浮動小数点演算の再現可能性についても指定しているものの、それを達成するには言語標準・実装・およびユーザーの強力が必要としています。しかし、C++では規格からして再現可能な浮動小数点演算をサポートしていません。

一部のアプリケーションにおいては浮動小数点演算の再現性が重要となる場合があります。例えば、ゲーム開発においては、クロスプラットフォームで展開する場合が多いため、異なるプラットフォーム間で浮動小数点演算結果が一致するようになるとコードの移植性を向上させることができます。あるいは、オンラインマルチプレイが可能なゲームにおいては、より簡単に異なるプラットフォーム間でデータを交換することができます。

科学計算では実装が浮動小数点演算に対して行う最適化(命令の融合や並べ替え)が計算結果に致命的な丸め誤差を導入してしまい、計算結果が不正確なものになる場合があります。これを回避・あるいは制御するためには、計算の順序が厳密に指定されていることが重要になります。

この提案では、再現性のある浮動小数点演算を実現するために、新しいライブラリ実装の浮動小数点数型を追加することを提案しています。

namespace std {
  // 丸めモードの指定
  enum class iec_559_rounding_mode : unspecified;                                                                             // freestanding
  inline constexpr iec_559_rounding_mode iec_559_round_ties_to_even = iec_559_rounding_mode::round_ties_to_even;              // freestanding
  inline constexpr iec_559_rounding_mode iec_559_round_toward_positive = iec_559_rounding_mode::round_toward_positive;        // freestanding
  inline constexpr iec_559_rounding_mode iec_559_round_toward_negative = iec_559_rounding_mode::round_toward_negative;        // freestanding
  inline constexpr iec_559_rounding_mode iec_559_round_toward_zero = iec_559_rounding_mode::round_toward_zero;                // freestanding


  // 再現性のある浮動小数点数型
  template<class T, iec_559_rounding_mode R = round_ties_to_even>
  class strict_float {
  public:
    using value_type = T;

    constexpr strict_float(T);
    constexpr strict_float(const strict_float&) = default;
    template<class X, iec_559_rounding_mode Y>
    constexpr explicit strict_float(const strict_float&);

    constexpr operator value_type() const;

    constexpr strict_float& operator= (const T&);
    constexpr strict_float& operator+=(const T&);
    constexpr strict_float& operator-=(const T&);
    constexpr strict_float& operator*=(const T&);
    constexpr strict_float& operator/=(const T&);
  };
}

提案されているのはstrict_float<T, R>というクラステンプレートで、T浮動小数点数型(IEE754交換形式であることが規定されるfloat16_t, float32_t, float64_t, float128_tのいずれか)を指定し、Rに丸めモードの指定(iec_559_rounding_mode列挙型の値)を指定します。

この型は浮動小数点数Tのごく薄いラッパであり、この型の値に対する操作(計算)は実行時にソースコード上での順序と意味を保持する事(並べ替えや融合が許可されないこと)が要求され、保証されます。すなわち、再現可能性という性質はこの型の暗黙のプロパティとして(ISO/IEC 60559:2020に準拠した形で)指定されます。

計算における丸めモードに関してはiec_559_rounding_mode列挙型の値として型に埋め込まれており、演算子オーバーロードによってその丸めを使用する計算が静的に指定されます。

namespace std {
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator+(strict_float<T, R>, <T, R>);              // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator+(strict_float<T, R>, T);                   // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator+(T, strict_float<T, R>);                   // freestanding
  
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator-(strict_float<T, R>, <T, R>);              // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator-(strict_float<T, R>, T);                   // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator-(T, strict_float<T, R>);                   // freestanding
  
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator*(strict_float<T, R>, <T, R>);              // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator*(strict_float<T, R>, T);                   // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator*(T, strict_float<T, R>);                   // freestanding
  
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator/(strict_float<T, R>, <T, R>);              // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator/(strict_float<T, R>, T);                   // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator/(T, strict_float<T, R>);                   // freestanding
  
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator+(strict_float<T, R>);                      // freestanding
  template<class T, iec_559_rounding_mode R>
  constexpr strict_float<T, R> operator-(strict_float<T, R>);                      // freestanding
}

この提案ではひとまず、四則演算のみをサポートする最小限の演算のみを提案しています。<cmath>にある関数などはアルゴリズムや丸めが指定されていないためそのまま利用できず、そのための作業は多大なものになるためです。また同様に、異なる形式の浮動小数点数間の再現可能な変換も将来のサポートとしています。

なお、よく似た提案がP2746R5で提案されていますが、P2746がFenvによる丸めモードの廃止と置換(よく似たクラステンプレートによる)だけを提案するものであるのに対して、この提案はさらに操作の融合や並べ替えを禁止することで計算の再現可能性を達成することを目指すものです。

P3379R0 Constrain std::expected equality operators

std::expected==演算子の制約の指定を変更する提案。

P2944R3(reference_wrapper==比較の動作修正提案)ではそのメインの提案とはほとんど関係ない編集上の提案としてpair, tuple, optional, variantの比較演算子の制約についての指定がMandatesだったのをConstraintsに修正しました。

しかし、その時にstd::expectedが漏れていたため、この提案では同様の変更を提案しています。

std::optionalについては、P2944R3の変更が行われた際に挙動に差が出てしまったようで、それがLWG Issue 4072で修正されています(Mandatesの場合はill-formedになっていて代わりの候補の探索が行われなかった用法が、Constraintsになったことによってオーバーロード候補から外れて代わりの候補が探索されることで有効になってしまう問題)。ここでは、その修正と同じく追加の制約を行うことで同様の問題に対処しています。

この提案はこのリビジョンがそのまま2024年11月の全体会議で承認され、C++26WDに適用されています。

P3380R0 Extending support for class types as non-type template parameters

NTTPとして扱えるクラス型の制限を拡張する提案。

この提案のアイデアはP2484R0で以前に提案されたものをベースにして発展させたものです。

クラス型のNTTPを許可する際に最も問題になることは、NTTP値の等価性をどのように判定するかということです。例えば次のようなコードにおいて

// NTTPとして使用可能なクラス型とする
struct C { ... };

template<atuo NTTP>
struct X {};

static_assert(std::same_as<X<C{}>, X<C{}>>);  // 常にパスする?

最後のアサーションが常に成功することが重要です。これらはコード上から見た時に同じ型であり、これが同じ型とならない場合は余計なインスタンス化が行われていることになり、関数のインターフェースに現れる場合に一見同じな二つの宣言が異なるオーバーロードになってしまうなど、ODRの問題を引き起こすためです。これは実装的にはマングル名の問題であり、NTTP値をマングリングする時は同じ値は同じ文字列にマングルされる必要があります。

C++20でNTTPとして使用可能な構造的型 (structural type) という型の分類は、この問題が起こらないと思われる要件をまとめたものです。しかし、全てのメンバがpublicである必要があるために多くのクラス型(特に型の構造以上の意味論を持つクラス型)はNTTPとして使用することができません。

privateメンバを持つクラス型は単にそのサブオブジェクトに分割して等価性を判断するということが必ずしも正しくなく、その型の持つ意味論を考慮する必要がある場合があります。

この提案では、シリアル化(Serialization)・正規化(Normalization)・逆シリアル化(Deserialization)の3ステップによってNTTP値を構築することによって、NTTPの等価性判定に意味論も反映する仕組みを提案するとともに、それをクラス型で宣言するための構文を提案するものです。

1つ目のステップであるシリアル化とは、(NTTPとして使用する)値Vを受け取り、それを構造的型のタプルへ分解する過程です。結果の型はそれ自体も構造的型である必要があるため、この分解は再起的に行われます(スカラ型か左辺値参照型に行きつくと終了)。

これはつまりC++20時点の仕様と実装がやっていることであり、クラス型は0個以上のスカラ型によって構成されているものとして扱って、そのスカラ型の等価性によってクラス型の等価性を定義しようとするものです。

ただしprivateメンバを持つ型などでは、そのクラス型の等価性判定のためにどのようにシリアライズを行えば良いのか(どのような型に分解するのが適切なのか)をコンパイラに伝える必要があります。

その手段さえ用意すれば、std::tuplestd::string_viewなどの型ではこのシリアル化だけでNTTPの等価性問題をクリアできます。ただし、これだけでは不十分な型もあります。

例えば次のような小さな文字列型があり

class SmallString {
  char data[32];
  int length = 0; // always <= 32

public:
  // the usual string API here, including
  SmallString() = default;

  constexpr auto data() const -> char const* {
    return data;
  }

  constexpr auto push_back(char c) -> void {
    assert(length < 31);
    data[length] = c;
    ++length;
  }

  constexpr auto pop_back() -> void {
    assert(length > 0);
    --length;
  }
};

この型がNTTPとして使用可能であるとして、次のような関数がある時

// SmallStringをNTTPで受ける型
template <SmallString S>
struct C { };

constexpr auto f() -> SmallString {
  auto s = SmallString();
  s.push_back('x');
  return s;
}

constexpr auto g() -> SmallString {
  auto s = f();
  s.push_back('y');
  s.pop_back(); // yは配列上に残ったままになる
  return s;
}

SmallStringクラスの意味論的には、f()g()の返す文字列型は同じ値を持ちます(x1文字からなる文字列)。しかし、そのデータ配列の内容は異なり、最初にゼロクリアされているとしてもg()の配列にはyが記録されたままになっています。このため、単にシリアル化の結果のみから等価性を判定(C++20のデフォルト、サブオブジェクト列の等価性によって判定)すると、C<f()>C<g()>は異なる型になります。

ここではその意味論を反映する必要がありそうで、シリアル化をカスタムすることでdata[0]からdata[length - 1]までの文字のみを等価性判定に参加させた方がいいような気がします。ただしこの方法はまた別の問題を孕んでいます

template <SmallString S>
constexpr auto bad() -> int {
  if constexpr (S.data()[1] == 'y') {
    return 0;
  } else {
    return 1;
  }
}

この関数を用いた、bad<f()>()bad<g()>()01のどちらの値を返すでしょうか?あるいは両方でしょうか?今はカスタムのシリアル化によってC<f()>C<g()>は同一であるようにしているため、これはODR違反になります。

つまり、デフォルトのシリアル化を選ぶと等しさが正しくなくなり(意味を反映できず)、カスタムのシリアル化を選ぶとODR違反が発生します。

この問題を回避するためのステップが正規化です。ここでの正規化は他のところで使用される場合と同じ意味で、本質的に同じであるものを同じになるようにする作業です。例えばこの場合、カスタムのシリアル化を行った後で、data[]全体をゼロクリアしてからlength個の要素をコピーしてその値により等価性判定を行うようにすることで、C<f()>C<g()>は同一になりかつbad<f()>()bad<g()>()はどちらも1を返すようになります。

この正規化の実際の作業は型ごとに必要性や方法が異なりえます。

ただし、これを経てもなおまだ十分ではない型が存在します。その代表例はstd::vectorをはじめとする可変長コンテナです。

std::vectorの実装はとても単純には次のようになっています

template <typename T>
class vector {
  T* begin_;
  size_t size_;
  size_t capacity_;
};

ポインタのNTTP等価性判定はそのポインタの値によって行われ、ポインタ値が等しい時に等価であるとされますが、std::vectorの場合はその要素によって判定を行う必要があります(ポインタ値が異なる場合でもstd::vector的には等価な場合がありうる)。std::vectorをはじめとするコンテナのシリアル化と正規化においては、可変長の要素列を取り扱う必要があります。

ここまでなら前のSmallStringと話は変わらないのですが、std::vectorの場合はキャパシティというプロパティがあります。2つのstd::vectorの全要素が完全に同一だったとしても、キャパシティの値は異なる可能性があります。しかし単にキャパシティの値を等価性判定に参加させただけでは不十分です。なぜなら、キャパシティの値は実態に合っている必要があるためで、すなわち確保されている配列の全体が等価性判定に参加してしまい、先ほどと同じ問題が起こります。

逆シリアル化はこれを上手く取り扱うためのステップで、(カスタム/デフォルトの)シリアル化を行った結果をデシリアライズして実際のNTTP値を構成することによってこの問題を解決します。逆シリアル化は再構築をおこなうことによって正規化を暗黙に実行しており、これによって正規化の方法を慎重に指定する負担を軽減する事もできます。

また、コンパイラは次のようなチェックを実行してこれらの3ステップの過程が健全であることを確認することもできます

serialize(deserialize(serialize(v))) === serialize(v)

ユーザーがコード内で見るNTTP値は、元の値はをシリアルライズしてデシリアライズ(シリアル化結果をクラスに渡して再構築)した値であり、この結果得られる値はNTTP値として等価であるとみなされる元の型のすべての値に対して信頼できる単一のテンプレート引数となります(同値類の代表元みたいなものになる)。

P2484R0では、このカスタムのシリアル化と逆シリアル化を含むNTTP仕様の設計を提案するものではありましたが、可変長コンテナをサポートできていないなどの問題がありました。この提案ではそれを考慮して正規化というステップを入れることによってこれを解消しています。

残った問題は、このシリアル化と逆シリアル化をどのように行うか、中間表現をどうするか、という点です。std::vectorstd::tupleをシリアル化するには新しいstd::vectorstd::tupleが必要になり、表現型についても設計しなければなりません。

この提案では、これに静的リフレクションを活用することを提案しています。すなわち、シリアル化の表現はstd::meta::infoで行い、シリアル化の結果はstd::meta::infoの配列になります。逆シリアル化はこれを受け取るコンストラクタによって行います。

SmallStringの場合、カスタムのシリアル化(length個分の要素だけをシリアル化)は次のようになります

class SmallString {
  char data[32];
  int length;

  // 自身をstd::meta::infoの配列に分解
  consteval auto operator template() const -> std::vector<std::meta::info> {
    std::vector<std::meta::info> repr;

    // lengthこの範囲の要素だけをシリアライズする
    for (int i = 0; i < length; ++i) {
      repr.push_back(std::meta::reflect_value(data[i]));  // meta::infoに値を保持する
    }
    
    return repr;
  }
};

そして、逆シリアル化(+正規化)は次のように実装されます

class SmallString {

  ...

  // オーバーロード解決を容易にするためにタグ付きコンストラクタにする
  consteval SmallString(std::meta::from_template_t, std::vector<std::meta::info> repr)
    : data{} // 配列をまずゼロクリアしておく(正規化)
    , length(repr.size())
  {
    // 各要素を復帰する
    for (int i = 0; i < length; ++i) {
      data[i] = extract<char>(repr[i]);
    }
  }
};

std::vectorの場合もこれと同様に実装できます

template <typename T>
class vector {
  T* begin_;
  size_t size_;
  size_t capacity_;

  // vectorのシリアライズ表現型
  struct Repr {
    std::unique_ptr<std::meta::info const[]> p;
    size_t n;

    consteval auto data() const -> std::meta::info const* {
      return p.get();
    }

    consteval auto size() const -> size_t {
      return n;
    }
  };

  // シリアライズ処理
  consteval auto operator template() const -> Repr {
    auto data = std::make_unique<std::meta::info const[]>(size_);

    // 要素の値を保持しておく
    for (size_t i = 0; i < size_; ++i) {
      data[i] = std::meta::reflect_value(begin_[i]);
    }

    // サイズと一緒に保存
    return Repr{
      .p=std::move(data),
      .n=size_,
    };
  }

  // デシリアライズ処理
  consteval vector(std::meta::from_template_t, Repr r) {
    // キャパシティの値はサイズと同一になる
    begin_ = std::allocator<T>::allocate(r.size());
    size_ = capacity_ = r.size();

    // 要素を復帰
    for (size_t i = 0; i < size_; ++i) {
      ::new (begin_ + i) T(extract<T>(r.p[i]));
    }
  }
};

ここでは、シリアル化の結果としてReprという型を返しています。シリアル化の結果はmeta::infoの配列として扱えればよくstd::vectorである必要はありません(というかstd::vectorstd::vectorのためには使用できない)。求められているのは、.data()const std::meta::info*に変換可能であり、.size()でその要素数が取得できることです。

このアプローチは非常に強力である一方で、一部の型に対しては目的は達せられるものの最適とは言えない部分があります。例えばoptionalであり

template <typename T>
class Optional {
  union { T value; };
  bool engaged;

  // null reflectionを用いることで長さ0 or 1の配列を表現できる(vector<info>は必要ない
  struct Repr {
    std::meta::info r;

    explicit operator bool() const {
      return r != std::meta::info();
    }

    consteval auto data() const -> std::meta::info const* {
      return *this ? &r : nullptr;
    }
    consteval auto size() const -> size_t {
      return *this ? 1 : 0;
    }
  };

  consteval auto operator template() -> Repr {
    if (engaged) {
      return Repr{.r=meta::reflect_value(value)};
    } else {
      return Repr{.r=meta::info()};
    }
  }

  consteval Optional(meta::from_template_t, Repr repr)
    : engaged(repr)
  {
    if (engaged) {
      ::new (&value) T(extract<T>(repr.r));
    }
  }
};

tupleもそうです

template <typename... Ts>
class Tuple {
  // let's assume this syntax works (because the details are not important here)
  Ts... elems;

  // Note that here we're returning an array instead of a vector
  // just to demonstrate that we can
  consteval auto operator template() -> array<meta::info, sizeof...(Ts)> {
    array<meta::info, sizeof...(Ts)> repr;
    size_t idx = 0;
    template for (constexpr auto mem : nonstatic_data_members_of(^Tuple) {
      // references and pointers have different rules for
      // template-argument-equivalence, and thus we need to
      // capture those differences... differently
      if (type_is_reference(type_of(mem))) {
          repr[idx++] = reflect_object(this->[:mem:]);
      } else {
          repr[idx++] = reflect_value(this->[:mem:]);
      }
    }
    return repr;
  }

  consteval Tuple(meta::from_template_t tag,
                  array<meta::info, sizeof...(Ts)> repr)
      : Tuple(tag, std::make_index_sequence<sizeof...(Ts)>(), repr)
  { }

  template <size_t... Is>
  consteval Tuple(meta::from_template_t,
                  index_sequence<Is...>,
                  array<meta::info, sizeof...(Ts)> repr)
      : elems(extract<Ts>(repr[Is]))...
  { }
}

これらの型の場合はいずれも、メンバ毎の等価性によって自身の等価性を表現しているにすぎません。すなわち、(C++20とは異なるが)その動作はデフォルトです(シリアライズのみで十分)。したがって、この場合はデフォルト実装が利用可能であるはずです。

template <typename... Ts>
class Tuple {
  Ts... elems;

  consteval auto operator template() = default;
  consteval Tuple(meta::from_template_t, auto repr) = default;
}

ただしこれはまだ微妙(1つの操作に2つの宣言がいる、正規化を行う必要がある型で役に立たないなど)な部分があります。この提案ではoperator templateの戻り値型voidであるかどうかによって、その意味を変えるようにしています。

operator templateの戻り値型voidである場合、正規化を行う必要があるものの、そのシリアル化および逆シリアル化はデフォルト(メンバ毎のもの)であり、カスタムの逆シリアル化が必要ないことを表します。

template <typename T>
class Optional {
  union { T value; };
  bool engaged;

  consteval auto operator template() -> void { }
};

template <typename... Ts>
class Tuple {
  Ts... elems;

  consteval auto operator template() -> void { }
}

この戻り値型voidoperator templateは、C++20に欠けていた、メンバ毎の等価性によって等価性が判定可能だがprivateなメンバを持ってしまっているクラスに対するオプトイン構文になります。

SmallStringの場合も実装は単純になります

class SmallString {
  char data[32];
  int length;

  consteval auto operator template() -> void {
    // 正規化方法のみを書く(シリアル化と逆シリアル化はデフォルトで良い
    std::fill(this->data + this->length, this->data + 32, '\0');
  }
};

この提案では最終的に

  • コア言語
    • テンプレート表現関数 (operator template)の導入: クラス型Tは次の2つの形式のいずれかでconstevaloperator templateを提供できる
      1. voidを返す: すべての基底クラスと非静的データメンバは構造型でなければならず、mutableであってはならない
      2. Rを返す:
        • R.data()std::meta::info const*に変換可能で、R.size()size_tに変換可能である必要がある
        • T(std::meta::from_template, R)が有効な式である必要がある
    • テンプレート引数の正規化の概念の導入
      • 構造型Tの値vは、以下のようにテンプレート引数として正規化されます
        1. Tスカラー型または左辺値参照型の場合、何も行わない
        2. Tが配列型の場合、配列のすべての要素がテンプレート引数として正規化される
        3. Tがクラス型の場合
          • Tvoidを返すテンプレート表現関数を提供する場合、その関数がv上で呼び出され、vのすべてのサブオブジェクトがテンプレート引数として正規化される
          • Tstd::meta::infoの範囲を返すテンプレート表現関数を提供する場合、新しい値T(std::meta::from_template, v.operator template())vの代わりに使用される
          • Tがテンプレート表現関数を提供しない場合、vのすべてのサブオブジェクトがテンプレート引数として正規化される
    • P2996のstd::meta::reflect_valueの意味を、引数に対してテンプレート引数の正規化を実行するように変更
    • 構造的型の定義の拡張
      • 構造的型は、スカラー型、左辺値参照型、または要素型が構造的型である配列型
      • または、以下のプロパティを持つリテラルクラス型
        • クラスが資格のあるテンプレート表現関数を持つ
        • あるいは
          • すべての基底クラスと非静的データメンバがpublicかつ非mutableであり
          • すべての基底クラスと非静的データメンバの型が構造的型である
    • テンプレート引数として等価(template-argument-equivalent)の定義の拡張
      • 2つの値がテンプレート引数として等価であるのは、それらが同じ型であり、次の場合
        • [...]
        • どちらもクラス型Tであり
          • Tが非voidを返す資格のあるテンプレート表現関数を持つクラス型である場合、2つの値に対してテンプレート表現関数を呼び出した結果であるr1r2について、r1.size() == r2.size()であり、0 <= i < r1.size()の各iについて、r1.data()[i] == r2.data()[i]である
          • それ以外の場合、対応する直接のサブオブジェクトと参照メンバがテンプレート引数として等価である
    • クラス型の非型テンプレートパラメータを初期化する際に、テンプレート引数の正規化を実行するようにする
  • ライブラリ
    • 新しいタグ型std::meta::from_template_tと、その値std::meta::from_templateを追加
    • 新しい型特性std::is_structuralを追加
    • 次のライブラリ型すべてに、制約付きのconsteval void operator template() { }(つまり、正規化なしのデフォルトのサブオブジェクトごとのシリアル化)を追加
      • std::tuple<Ts...>
      • std::optional<T>
      • std::expected<T, E>
      • std::variant<Ts...>
      • std::basic_string_view<CharT, Traits>
      • std::span<T, Extent>
      • std::chrono::duration<Rep, Period>
      • std::chrono::time_point<Clock, Duration>

を提案しています。

std::meta::infoの配列を返すoperator templateはリフレクション提案に依存していますが、voidを返す方には依存はありません。前者はあらゆる型をサポートするために(特に可変長コンテナ型のために)必要なソリューションであり、後者は前者の略記であるものの多くの一般的な型をカバーしています。そのため、リフレクションの進行とは関係なく、voidを返す形式は検討しておくことができます。

P3381R0 Syntax for Reflection

リフレクション構文のための演算子として^^を使用する提案。

P2996で提案されているC++26に向けたリフレクション機能においては、任意のエンティティからリフレクション情報を取り出す演算子として^を使用しています(auto info = ^T;のように使用)。しかし、Objective-C++におけるブロック拡張の構文と衝突していることが指摘されました。

例えば次のような構文は

type-id(^ident)();

2つの解釈があり得ます

  • type-idを返し、引数を取らないブロックを保持するidentという名前の変数
  • std::meta::infotype-idにキャストして、operator()を呼び出す

1つ目がObjective-C++におけるブロック拡張であり、このことはP3294R1のトークンシーケンス構文(^{ ... })が導入されるとさらに影響が大きくなります。

このために、この提案は^の代替となる演算子を探索し、提案するものです。

構文候補には次の三種類が考えられます

  1. キーワード
  2. 1文字(の演算子
  3. 複数文字

とはいえ元々、Reflection TSではリフレクション構文としてreflexprというキーワードを用いていたものの、これが構文的に重いために^に移行した経緯があり、キーワードという選択肢はあり得ません(可能なキーワードは2,3文字では済まないため)。したがって、残るのは1文字以上の演算子です。

入力しやすく、導入の負担が少ない文字(基本文字集号に新しく文字をくわえる必要のない文字、つまりASCII範囲の記号)であり、C++でまだ使用されて居ないものとすると、1文字の候補はかなり限られます。

候補 検討結果 使えそう?
#e Cマクロで既に使用されており、意味の変更ができない
$e 一部のコンパイラが識別子として $ を拡張で使用可能にしているため、$T が型 T のリフレクションと $ で始まる識別子のどちらにも解釈できてしまう
%e テンプレート引数で使う場合に<% が代替トーク{ と解釈されるため、C<%T> のような構文が使えない。ただし、() や空白を使うことで回避可能ではある。 😞
,e 検討の余地なし
/e 使用法によっては、コメント開始の ///* に見た目が近い ✅ 
:e C<:T> が代替トーク[ と解釈されるため、C[:T:] のような構文が使えない
=e 代入演算子として一般的に使用されすぎているため、リフレクション演算子としてのオーバーロードは(意味的に)難しい
?e 条件演算子との曖昧さは避けられるものの、他の言語での述語との関連性が強い。また、パターンマッチングでの ? の使用とも競合する。 🤷
@e Objective-C拡張機能として既に使用されている(@property, @dynamic など)。 @(e) はボックス式、@[e]@{e} はコンテナリテラルとして解釈されてしまうため回避も難しい
\e 文字列内での補間と競合する。また、ユニバーサル文字名との衝突もある(\u0 がUCNとして解釈される)
`e Markdownでのインラインコードブロックで使われるため、エスケープが難しい。また、特に優れた点もない
|e 使用可能で、曖昧さもなく、代替トークンの一部でもない。ただし、数学的な背景を持つ人々にとっては、絶対値のように見える可能性がある

結局、一文字の候補で残ったものは次4つです

  • /e
  • \|e
  • ?e: パターンマッチングと競合する
  • %e: 代替トークンとの重複を受け入れるならあり

複数文字の場合は自由度がさらに広がり、候補はほぼいくらでも存在します。筆者の方の一人は簡単なユーティリティを作成し、見栄えを比較して検討したようです。

  • ^^e: ^2文字なら衝突はない、1文字増えるだけ
  • ^[e]: オペランドを角かっこで囲む。スプライシングと対照的になるものの、構文が重い
  • ${e} or $(e): トークンシーケンスの構文としてはあまり適していない(内側にさらに波かっこが入るため)
  • /\e: 大きい^\eと同じ問題がある

この提案では結局、^^を選択し、これを提案しています。

2文字であればそれほど重くはなく、見た目の問題がある1文字の使用可能な演算子を再検討する必要性を感じるほどコストは高くなく、またreflexprという10文字よりは十分に短い、という理由のようです。

この提案の内容はR0発行の時点ですでにclang/EDGにて実装されているようです。

そして、この提案は大きな反対なくP2996R8にて適用されています。

P3382R0 Coarse clocks and resolutions

より解像度の粗い時計型を追加する提案。

プログラム中でタイムスタンプを取得したい場合というのは比較的よくあり、C++では<chrono>の時計型のClock::now()で取得することができます。しかし、これらの時計型は通常マイクロ秒あるいはそれ以上の解像度を持っており、このような高い解像度の時刻取得にはコストがかかります。一部のユースケースにおいてはそのような高い解像度は必要なく、100msから秒の解像度で十分な場合があります。たとえば

  • HTTP Dateヘッダの生成: 秒単位で十分
  • 定期実行される処理の周期の指定: 周期の単位が秒より大きい場合、秒単位で十分
  • 大きめのタイムアウト値: 大規模データベースへのクエリなど、分単位の時間がかかる場合のタイムアウト判定には秒単位以上で十分
  • 統計: 応答時間のパーセンタイルや平均をカウントする場合、高い解像度は不要

一部のプラットフォームではこのような需要に応えられる粗いクロックが用意されており、標準ライブラリの時計型よりも70倍以上高速になる場合があります。また、パフォーマンスを重視する一部のプラットフォームでは既に粗いクロックが使用されています。そのような場所では、次のように必要な場合にのみ高い解像度の時刻を取得するようなフォールバックが良く行われています

bool is_expired(std::chrono::steady_clock::time_point deadline) {
  // 粗い時刻とその時計の解像度から大雑把に判定
  auto max_time = coarse_steady_clock() + coarse_steady_clock_resolution();
  if (max_time < deadline) {
    return false;
  }

  // 正確な判定にはより高い解像度の時刻を使用
  return deadline < std::chrono::steady_clock::now()
}

この提案は、より粗い解像度で時刻を取得できる時計型を標準ライブラリに追加しようとするものです。

提案されているのはcoarse_steady_clockcoarse_system_clockの2種類で、これらはcoarse_を取り除いた名前の時計型に対応する性質を持つより粗い解像度の時計型です。

namespace std::chrono {
  // より粗いsteady_clock
  class coarse_steady_clock {
  public:
    using rep        = steady_clock::rep;
    using period     = steady_clock::period;
    using duration   = steady_clock::duration;
    using time_point = steady_clock::time_point;
    static constexpr bool is_steady = true;

    static time_point now() noexcept;
    static duration resolution() noexcept;
  };
  
  // より粗いsystem_clock
  class coarse_system_clock {
  public:
    using rep        = system_clock::rep;
    using period     = system_clock::period;
    using duration   = system_clock::duration;
    using time_point = system_clock::time_point;
    static constexpr bool is_steady = system_clock::is_steady;

    static time_point now() noexcept;
    static duration resolution() noexcept;

    // mapping to/from C type time_t
    static time_t      to_time_t(const time_point& t) noexcept {
      return system_clock::to_time_t(t);
    }
    static time_point  from_time_t(time_t t) noexcept { 
      return system_clock::from_time_t(t);
    }
  };
}

また、Cpp17TrivialClockを変更して.resolution()メンバからその時計型の解像度を取得することができるようにして、既存の時計型にも.resolution()を追加します。

この2つの時計型において、durationtime_pointの型が元の時計型と同じにされているのは、利便性のためです。例えば先ほどのフォールバックのサンプルコードのように、同じdurationtime_point型を使用しておけば、元の時計型の時刻と簡単に組み合わせて使用できるようになります。

P3383R0 mdspan.at()

std::mdspan.at()メンバ関数を追加する提案。

std::mdspanの添え字アクセスは境界チェックが行われないアクセスであり、境界チェックを行う場合ユーザーが明示的に行わなければなりません。一方で、添え字アクセス可能な標準のコンテナは全て.at()メンバ関数を備えています(std::spanも含めて)。

この提案は、安全性と一貫性のために、std::mdspanにも.at()メンバ関数を追加しようとするものです。

namespace std {
  
  template<
    class ElementType,
    class Extents,
    class LayoutPolicy = layout_right,
    class AccessorPolicy = default_accessor<ElementType>>
  class mdspan {

    ...

    // 提案されているat()の宣言例
    template<class... OtherIndexTypes>
      constexpr reference at(OtherIndexTypes... indices) const;
    
    template<class OtherIndexType>
      constexpr reference at(span<OtherIndexType, rank()> indices) const;

    template<class OtherIndexType>
      constexpr reference at(const array<OtherIndexType, rank()>& indices) const;

    ...
  };
}

この意味論は他のコンテナの.at()とほとんど同じです。

この提案の内容はすでにdmspanの参照実装であるkokkos/mdspanにて実装されています。

P3384R0 __COUNTER__

__COUNTER__マクロを標準化する提案。

__COUNTER__マクロは、翻訳単位ごとに0から始まり展開され度にインクリメントされる、整数リテラルへと展開されるマクロです。これは、プリプロセッサメタプログラミングで使用され、ユニークな名前やインデックスを生成するのに使用されます。

このマクロは多くのCおよびC++コンパイラで拡張として使用可能ではあるもののCでもC++でも標準化されてはいません。これにより、そのセマンティクスや移植性には何ら保証がありません。そのため、移植性が重要なコードベースにおいてはマクロによって使用可能かどうか検出して使用するか、使用しないように注意されています。

それでもCおよびC++のコードベースの両方で広く使用されているため、移植性とセマンティクスの保証を提供するために既存の実装を取り込む形で__COUNTER__マクロを標準化しようとする提案です。

int main() {
  int a = __COUNTER__ ; // 0
  int b = __COUNTER__ ; // 1
  int c = __COUNTER__  + __COUNTER__; // 5
}

__COUNTER__マクロの展開結果は整数リテラルになるため厳密にはマクロの結果そのものに型は無いのですが、この提案では少なくとも2^32 - 1の値を表現できるように実装することを推奨しており、その実装定義の最大値に達したらコンパイルエラーにするようにしています。

この手のマクロにありがちで問題になるのは、巧妙に使用するとODR違反コードを生成できてしまうことです。

// foo.hpp
#define CONCAT_IMPL(x, y) x##y
#define CONCAT(x, y) CONCAT_IMPL(x, y)
#define NEW_VAR(name) CONCAT(name, __COUNTER__)

inline void foo() {
  int NEW_VAR(x) = 2; // __COUNTER__マクロの値次第で、変数名が異なる
}

// a.cpp
#include "foo.hpp"

// b.cpp
int x = __COUNTER__;
#include "foo.hpp"  // a.cppのインクルード内容と異なるfoo()が定義されてしまう

既存の実装ではこのような問題に対して特別に検出して警告を発するなどの事は行っておらず、この提案でも特にケアをしていません。解決のためにはモジュールを使用する(__COUNTER__マクロはモジュールローカルであるため)か、ODRフレンドリーな名前_(同じスコープで何度も再使用できる)を使用する事を推奨しています。

これ以外の部分でも、既存の実装の動作やセマンティクスに準じた機能になっています。

P3385R0 Attributes reflection

リフレクションにおいて、エンティティに指定されている属性の情報を取得・付加できるようにする提案。

P2996で提案中の静的リフレクション機能には、属性に関するリフレクション(付加されている属性の情報を取得する or 属性情報のリフレクションを用いて属性を付加する)の機能が欠けています。標準属性は現在でも広く使用されており、今後も増加していくことが予想されるため、その重要性は時間とともに増していきます。

この提案は、P2996の静的リフレクションをベースとして、そこに属性に関するリフレクションの機能を追加することを提案するものです。

次のサンプルコードは提案のモチベーションを端的に示したものです

enum class Result {
  success,
  warn,
  fail,
};

struct [[nodiscard]] StrictNormalize {
  static constexpr bool operator() (Result status) {
    return status == Result::success;
  }
};

template <class F> 
[[ [: ^F :] ]] // [[ nodiscard ]] に展開される
auto transform(auto... args) {
  return F()(args...);
};

int main() {
  transform<StrictNormalize>(Result::success); // "nodiscard"による警告が表示される
  bool isOk = transform<StrictNormalize>(Result::success); // OK、警告なし
}

transformの定義においては、呼び出し可能な型Fに付加されている属性を取得して自身にも付加します。この例では、StrictNormalizeに付加されている[[nodiscard]]を復元しています。

このような属性のイントロスペクションはP2237で提案されているようなコードインジェクション機能の利用時に特に重要になることが予想されます。例えば、[[deprecated]]メンバを選択的にスキップするなどです

struct User {
  [[deprecated]] std::string name;
  std::string uuidv5;

  [[deprecated]] std::string country;
  std::string countryIsoCode;
};

template<class T>
constexpr std::vector<std::meta::info> liveMembers(const T& user) {
  std::vector<std::meta::info> liveMembers;
  
  // [[deprecated]]属性のリフレクションを取得しておく
  auto deprecatedAttribute = std::meta::attributes_of(^[[deprecated]])[0];

  auto keepLive = [&] <auto r> {
    // [[deprecated]]指定されているメンバを無視する
    if (!std::ranges::any_of(
      attributes_of(^T),
      [deprecatedAttribute] (auto meta) { meta == deprecatedAttributes; }
    )) {
      liveMembers.push_back(r);
    }
  };

  // T の[[deprecated]]ではないメンバのリフレクションだけをliveMembersに入れていく
  template for (auto member : std::meta::members_of(^T)) {
    keepLive(member);
  }

  return os;
}

// Migratedのユーザーはdeprecatedなメンバをサポートしない
struct MigratedUser;
std::meta::define_class(^MigratedUser, liveMembers(currentUser));

このために、ここでは次の変更を提案しています

  • std::meta::info
    • 属性をリフレクション可能なプロパティとしてサポートする
    • これにより、std::meta::info 型の値で属性を表現できるようになる
  • リフレクション演算子 ^
    • 属性に対するリフレクションをサポートする。
      • ^[[deprecated]]を有効にする
    • 結果の値は、属性エンティティの関連情報が埋め込まれたリフレクション値(std::meta::info型の値)
    • 標準属性でない場合、この式はill-formed
  • スプライサー [[ [: r :] ]]
    • 属性が許可されているコンテキストで、r(属性のリフレクション)に映されている属性に対応する属性リストを生成できるようにする
    • 例えば、[[ [: ^ErrorCode :] ]] のように記述することで、ErrorCode型の属性を別の型に付与できる
    • 文法として参照しているattribute-listalignasをカバーしていないが、これは別の提案で議論する予定
  • メタ関数
    • attributes_of(info) -> std::vector<info>: 指定されたリフレクション値に付随する各属性を表すstd::meta::info のシーケンスを返す
    • is_attribute(info) -> bool: 指定されたリフレクション値が属性を指している場合に true を返す
    • 属性名の取得: 属性のリフレクションから、属性トークンに対応する文字列や、属性を識別するのに役立つテキストを返す
      • name_of(info) -> string_view
      • u8name_of(info) -> u8string_view
      • qualified_name_of(info) -> string_view
      • u8qualified_name_of(info) -> u8string_view
      • display_name_of(info) -> string_view
      • u8display_name_of(info) -> u8string_view
    • data_member_spec()define_class()
      • 属性をサポートできるようにdata_member_options_tを拡張する

属性のリフレクションというと、標準属性と非標準属性(特に、ユーザー定義属性)の2種類の話がありますが、この提案では標準属性のサポートに主眼を置きそれのみを提案しています。ただ、標準属性へこれらのサポートを拡大することを念頭に置いて機能は設計されているようです。

P3388R0 When Do You Know connect Doesn't Throw?

execution::conectによる操作が例外を送出するかどうかを早期に判定できるようにする提案。

std::executionにおいて、非同期操作を表現するsenderreceiverconnectしてoperation_stateを取得し、それに対してstartすることによってそれが表す非同期操作を開始することができます。現在の仕様ではconnectの操作は例外を送出しうる一方で、startの操作が例外を送出するのは禁止されています。

std::executionを利用する際にやることは簡単に次の3ステップに分割できます

  1. senderreceiverの選択
  2. 1で選択したsenderreceiverの接続(connect
  3. 2の結果のoperation_statestartする

このうち、1と2のフェーズは例外を送出する可能性がありますが、3は例外を送出しません。start時に送出される例外はoperation_stateの(connectされている)receiverのエラーチャネルを通じて伝播される必要があります。

さらに、このステップはループしてネストする可能性があります。すなわち、3で開始された非同期操作の内部でさらにこのステップが実行され、その非同期コンテキスト内から非同期操作を開始する、といったことができます。この場合、ネストした1と2および3で生じる例外は全て、execution::set_errorによってreceiverのエラーチャネルで伝播される必要があります。

通常の同期関数が例外を送出するかどうかはnoexceptによって宣言し、取得することができます。同様に、senderも例外を送出する場合としない場合があり、送出する場合はexecution::completion_signatures_of_tを介して照会できる完了シグネチャsenderの返す値の型。3チャネル分ある)のリスト内にstd::set_error_t(std::exception_ptr)が含まれているかどうかによって宣言し、取得することができます。

そして、senderによる非同期操作が例外を投げないことが分かる場合、後続のsenderではその完了シグネチャstd::set_error_t(std::exception_ptr)を含まない事によって自身も例外を送出しないことを選択でき、これは例外をハンドリングするコードの削減等の最適化に繋がります。

すなわち、ネストして非同期操作が構成され開始される場合に、そこからの例外をexecution::set_errorによって伝達する必要があるのかどうかをより早期に判断できれば、その外側のsenderも例外を伝播する必要があるかどうかをコンパイル時に決定可能になります。そして、現在はそのようにはなっていません。

ステップ1はsenderreceiverの構築時の例外であるためこれは制限できませんが、ステップ2の場合は制限を少し強めることでこれが可能になります。この提案はそれを検討し、提案しています。

この提案では次の2つの事を提案しています

  1. receiverコンセプトの一部として、receiverが例外を投げずにムーブ可能であることを要求する
  2. 次のいずれかによって、特定の環境を持つreceiverに対して、senderconnectメンバ関数が例外を送出しないことを判断する仕組みを導入する
    • あるsender型のオブジェクトをあるreceiver型(R1)のオブジェクトにconnectしても例外を送出しない場合、そのsender型のオブジェクトを、R1と同じ関連環境型を持つ別のreceiver型のオブジェクトに接続しても例外を送出しない。という意味論要件を追加
    • 上記に加えて、別の環境型が全く同じクエリをサポートし、全く同じ型を生成すると仮定して、異なる関連環境型を提供できるように要件を強化
    • sender型に対して、特定の関連環境型を持つreceiver型がその型に対して、例外を送出せずにconnect可能かをクエリできるsender版クエリ関数を導入する

の2つの事を提案しています。

senderが例外を送出するかを判定するのに使用可能なexecution::completion_signatures_of_tは、senderそのものか(receiverの)環境(Environment)に対して照会を行うものです。connect操作の実態はsender型のメンバ関数として実装されるものなので、これはconnect内部で接続されようとしているreceiverの環境を取得してクエリを行います。この時、sender型の操作は接続されようとしているreceiver型の型そのものにはアクセスしません。これはコンパイル時の型情報の相互循環参照を回避するための仕様です。

すなわち、senderから見たreceiverの重要かつ唯一のプロパティはこの環境そのものです。そして、環境とはコンパイル時のKey-Valueストアであり、サポートしているクエリの種類とクエリの返す結果によって特徴づけられ、あるsenderから見た時にそれらの性質が同じ環境は(型やreceiverが異なったとしても)同じであるとみなすことができます。

ただ、connect操作ではsender型のメンバ関数内で引数としてreceiverを受け取って、自身及びその(接続されようとしている)receiverをコピーないしムーブしてoperation_stateを生成する必要があります。つまり、connect操作で例外を送出する場合とは、基本的にはこの時に呼ばれるコピー/ムーブコンストラクタが例外を送出する場合です。

自身のムーブ/コピーコンストラクタが例外を送出するどうかについては自身の性質なのですぐにわかりますが、receiver型がどうかどうかはその型の性質を参照しないと分かりません。しかし、前述のように直接参照することができず、この性質は環境からも取得できないため、senderから見た時のreceiverの具体的な型の唯一の重要な性質となります。

コピーについてはconnectメンバ関数の引数型を値で受けるようにすることで、connect操作の呼び出し側の責任にすることができます。したがって、問題となるのはムーブコンストラクタの例外送出に関する性質のみです。

この時、receiverコンセプトの要件の一部として、receiver型のムーブコンストラクタからの例外送出が禁止されていれば、この問題は解消し、senderから見たreceiverの唯一の性質は環境のみであるという状態が復元されます。

提案している2つのことはこれらのような理由によります。提案2の3つの選択肢は、その制限の強さと確実性の度合いによるものです。

P3389R0 Of Operation States and Their Lifetimes (LEWG Presentation 2024-09-10)

P3373R0の紹介スライド。

P3373R0では、std::executionoperation_stateのライフタイムについて、現状の最長を取るものよりも短くできるようにすることを提案しています。このスライドはその内容を簡単に説明するもので、簡単なサンプルコードとイメージ図を用いて説明されていて分かりやすくなっています。

P3390R0 Safe C++

C++に借用チェック(Borrow checking)をはじめとする静的解析を備えたsafeコンテキストを導入する提案。

C++のメモリ安全性保証の欠如に対する圧力は近年急速に高まっており、Rustをはじめとするより安全な言語への移行が推奨されるなど、C++の将来性が脅かされつつあります。この提案はRust言語の設計からインスピレーションを得たメモリ安全性保証を持ち良く知られる未定義動作の無いC++のサブセットを追加することで、C++のスーパーセット言語への発展を提案するものです。

この提案では次のようなことを実現しようとしています

  • 安全なサブセットを持つC++のスーパーセットの提案
    • 安全なサブセットでは、未定義動作が禁止される
  • 言語の安全な部分とそうでない部分は明確に区別される
  • 安全なサブセットは引き続き有用である必要がある
    • 共用体やポインタなどの有用だが安全ではない機能を削除する場合は、選択型(choice type)や借用などの安全な代替機能を提供する
  • 新しいシステム(この提案のスーパーセットを実装したコンパイラなど)は、既存のコードを壊さない

この文書は、C++を拡張してメモリ安全性を高める提案をしています。具体的には、Rustのような所有権と借用安全性を導入し、以下のような機能を提供します:

このために、安全なサブセットとなるsafeコンテキストと、そこにおいて次のような機能を追加しようとしています

  • 借用チェック: use-after-freeやイテレータ無効化を防ぐ
  • リロケーションとアフィン型システム: オブジェクトのムーブや初期化における型安全性を確保
  • 選択型とパターンマッチング: 安全で型安全なユニオンの代替を提供
  • 安全な標準ライブラリ(std2): ライフタイムを意識したライブラリで、不安全なAPIへの露出を減らす
  • スレッド安全性: sendsyncのような機能でデータ競合を防ぐ
  • ランタイムチェック: 上記のような静的解析では検出できない不健全な動作を実行時に検出する(範囲外アクセスなど)

提案文書より、サンプルコード

// safety関連の機能をonにする
#feature on safety

// 安全な標準ライブラリをインクルード
#include <std2.h>

// safe指定子付きmain()
// UBにつながる操作を禁止する
int main() safe {
  std2::vector<int> vec { 11, 15, 20 };

  for(int x : vec) {
    // Ill-formed. mutate of vec invalidates iterator in ranged-for.
    if(x % 2) {
      // safetyコンテキストでは、左辺値はデフォルトでimmutable
      mut vec.push_back(x);
    }

    std2::println(x);
  }
}

この例は、イテレータの無効化というよくあるバグを静的に検出するものです。

#feature on safetyは翻訳単位ごとに、この提案のsafeコンテキストを有効にするためのオプトインスイッチです。これが無い翻訳単位は既存のC++コードとして解釈され、これによって既存のコードに影響を及ぼさないようになっています。

#include <std2.h>はこの提案の機能を使用して記述された、安全な標準ライブラリをインクルードします。これによって、安全ではないAPIに晒されるのを防止し、安全性を強化します。

int main() safeでは、main関数にsafe指定子を付加することによって関数内を安全なコンテキストとして指定します。安全なコンテキストでは、未定義動作を引き起こす可能性のある操作(ポインタの間接参照など)が禁止されます。

std2::vectorはこの提案の機能を使用して記述された安全なstd::vecotrであり、ライフタイムパラメータを認識するため借用チェックはライフタイムを持つ要素型にまで及びます。また、initializer_listも所有権オブジェクトモデルをサポートするstd2::initializer_listが使用されています。

範囲forは従来のものと同じですが、安全なコンテキストではライフタイムパラメータを使用して実装されたイテレータslice_iterator)が使用され、イテレータの無効化に対して安全になっています。

mut vec.push_back(x)では値をvectorに追加していますが、安全なコンテキストでは左辺値はデフォルトでconstであるため、変更を行うにはmutを使用して可変借用を取得して可変な参照を得る必要があります。安全なコンテキストでは、オブジェクトに対する変更は明示的になります。

そして、このpush_back()の呼び出しは、範囲forの実行中初期化されて生存しているvectorslice_iteratorがライフタイム制約を持つオブジェクトを変更することによって、そのイテレータを無効化し、これを借用チェッカーが検出することでコンパイルエラーになります。

安全なunique_ptrであるstd2::boxの例

#feature on safety
#include <std2.h>

int main() safe {
  // pは未初期化
  std2::box<std2::string_view> p;

  // Error: pは未初期化
  println(*p);

  // pが初期化される
  p = std2::box<std2::string_view>("Hello Safety");

  // Ok.
  println(*p);

  // pはqにムーブ(リロケーション)され、pは未初期状態になる
  auto q = rel p;

  // Error: pは未初期化
  println(*p);
}

アフィン型システムとリロケーションによって、未初期化状態での使用を防止しムーブを型安全に実行することができます。

安全なoptionalの実装例

template<class T+>
choice optional
{
  default none,
  [[safety::unwrap]] some(T);

  template<class E>
  expected<T, E> ok_or(self, E e) noexcept safe {
    return match(self) -> expected<T, E> {
      .some(t) => .ok(rel t);
      .none    => .err(rel e);
    };
  }

  T expect(self, str msg) noexcept safe {
    return match(self) -> T {
      .some(t) => rel t;
      .none    => panic(msg);
    };
  }

  T unwrap(self) noexcept safe {
    return match(self) -> T {
      .some(t) => rel t;
      .none    => panic("{} is none".format(optional~string));
    };
  }
};

choiceは選択型とよばれ、パターンマッチングでのみアクセス可能なファーストクラスのタグ付き共用体です。このような型の値はパターンマッチングを介してのみアクセスされることで、危険なアクセスを禁止します。

#feature on safety
#include <std2.h>

choice Value {
  i32(int),
  f32(float),
  f64(double),
  str(std2::string)
};

void print(Value val) safe {
  match(val) {
    // パターンマッチング時にマッチングの網羅性を要求することで、パターンマッチング内で型安全性のバグは起こらない
    .i32(i32) => std2::println(i32);
    .f32(f32) => std2::println(f32);
    .f64(f64) => std2::println(f64);
    .str(str) => std2::println(str);
  };
}

int main() safe {
  print(.i32(5));
  print(.f32(101.3f));
  print(.f64(3.15159));
  print(.str("Hello safety"));
}

std2::mutexの例。共有可変状態がミューテックスを通してのみアクセスされることをコンパイル時に保証する(sync/sendインターフェース)。

#feature on safety
#include <std2.h>
#include <chrono>

using namespace std2;

// mutexはsyncであり、arc<mutex<string>>はsendである
void entry_point(arc<mutex<string>> data, int thread_id) safe {
  // dataのロック(ミューテックス)を取得
  // lock_guardがスコープ外になるとロックが解放される
  auto lock_guard = data->lock();

  // 文字列データの可変借用を取得する
  // lock_guardがスコープ外になるとこれはダングリング参照になり、借用チェッカーによってアクセスが防止される
  string^ s = mut lock_guard.borrow();

  // 共有状態へ書き込み
  s.append("🔥");

  // 共有状態を出力する前にロックを解除する
  // これにより、sはダングリング参照になり、ロックの外部でsにアクセス仕様とするとエラーになる
  //drp lock_guard;

  // 共有状態を出力する前にデータを破棄する
  // data->lock()からのdataの借用がmut lock_guard.borrow()を介してprintln(*s)で使用されていることでsは有効に保持されており(されなければならず
  // sの有効な使用の前にdataを削除すると借用エラーになる
  //drp data;

  // 更新された共有状態を出力
  println(*s);

  // Sleep 1s before returning.
  unsafe { std::this_thread::sleep_for(std::chrono::seconds(1)); }
}

int main() safe {
  // arc - Shared ownership.
  // mutex - Shared mutable access.
  arc<mutex<string>> shared_data(string("Hello world - "));

  // Launch 10 threads.
  vector<thread> threads { };
  for(int i : 10) {
    mut threads.push_back(thread(entry_point, cpy shared_data, i));
  }

  // rel threadsによって取得されるinto_iteratorでは、thread::join()の呼び出しのためにvectorから要素をリロケーションすることを許可する
  for(thread t : rel threads) {
    t rel.join();
  }
}

ライフタイムの制約によってlock_guardが生存中にのみ共有状態への可変アクセスが許可され、これによってデータ競合のリスクは自然に回避されます。

ここで、drp lock_guard;コメントアウトを外すと、その行でミューテックスのロックが解放され以降の共有状態アクセスは危険なものになるのですが、借用チェッカーはそれを認識し、sの使用は期限切れの借用に依存していることが分かるため、println(*s);の行でコンパイルエラーになります。

同じようにdrp data;コメントアウトを解除してdataを破棄すると参照カウンタの減算によって共有状態が破棄される可能性があります。しかし、data->lock()で取得された共有借用の存在によりdataに有効期間の制約が生じ、これに違反していることを借用チェッカが検出するためコンパイルエラーになります。

範囲外アクセスの実行時チェック例。

#feature on safety
#include <std2.h>

int main() safe {
  std2::vector<int> vec { 1, 2, 3, 4 };
  size_t index = 10;

  // Panic on out-of-bounds vector subscript.
  int x = vec[index];
}

借用チェッカをはじめとする静的解析で検出できない健全性の問題は、実行時チェックによって検出します。この提案では、実行時エラーが検出されるとpanicを起こすことでプログラムを終了させます。

この例では、vectorの範囲外アクセスによってpanicが起こり、未定義動作が発生する前にプログラムが終了します。

この提案の内容はcircleというコンパイラC++の拡張言語)によって3年をかけて実装されており、このようなスーパーセット拡張が不可能ではないことを証明したうえで提案されています。SG23の投票ではこの提案に時間をかけて議論していくことにほぼ反対なく合意が取れています。

ただし、プロファイル提案と機能が被っている部分があり、どちらを優先すべきかについてはプロファイルを優先する方が好まれているようです(次点で両方)。

P3391R0 constexpr std::format

std::format()constexpr化する提案。

P2741R3の採択によって、static_assert()のエラーメッセージとしてstd::stringを渡せるようになりました。ただし、ここに渡すstd::stringを生成するために便利なstd::format()は定数式では使用できません。

std::format()は内部的に型消去を用いてフォーマット対象引数を扱っていた(const T*からconst void*へのキャストが必要だった)ために定数式で使用可能にすることはできませんでしたが、これはP2738R1の採択によって可能になっており、さらにはそれをplacement newを用いて構築することもできるようになっています(P2747R2)。

これにより、std::format()constexpr化する事を妨げるものは無くなっているため、ここではそれを提案しています。

ただ、そのままconstexprを付加するだけで終わるほど簡単ではなく、定数式で使用可能な型のうち2種類の型ではそれが困難なことが分かっています。

1つは浮動小数点数型です。std::format()においては整数型と浮動小数点数型の文字列化にstd::to_chars()を使用しています(実装は直接使用していないかもしれませんが、やっていることは同じです)。しかし、std::to_chars()オーバーロードのうち定数式で使用可能なのは整数型のもののみで浮動小数点数型のオーバーロードは使用可能ではありません。

これはstd::to_chars()が文字列化のアルゴリズムを明示的に指定していないことや、そのようなアルゴリズムが定数式で実行可能ではないことなどの事情があり、整数型がconstexpr指定された時点(P2291R3)でも現在でも浮動小数点数型のオーバーロードconstexprにすることはできないようです。

もう1つは<chrono>の型で、こちらはstd::basic_ostringstream<char>を介してストリーミングされるのと同様にフォーマットされるように規定されており、当然iostreamの型(および内部のフォーマット実装)にはconstexprを付加できません。

これを動作させるには<chrono>型のフォーマットの動作を変更する必要があると思われます。

この提案では、どちらの型に対しても更なる議論を行わずに、これらの型と定数式で使用可能ではない型(stacktrace_entryfilesystem::pathなど)を除いた、既存の標準のフォーマット可能な型についてのみ、コンパイル時のフォーマットを有効化することを提案しています。

{fmt}ライブラリでは<chrono>型を除いてconstexprformat()がすでにサポートされているほか、libstdc++の実装においても関数にconstexprを付加するほかは2か所の変更のみでこの提案の内容を実装可能だったようです。

P3392R0 Do not promise support for function syntax of operators

標準ライブラリの演算子オーバーロードは関数構文での呼び出しをサポートしていないことを明確化する提案。

オーバーロードされた演算子@a @ b@a等のように演算子として使用する他に、関数構文(a.operator@(b) or operator@(a, b))によっても呼び出すことができます。標準ライブラリの演算子についても同様ではありますが、この提案はそれをサポートしていない(演算子としての使用のみが可能である)事を明確化しようとするものです。

その意図は、標準ライブラリが演算子の実装方法(メンバ/非メンバオーバーロード)を変更する権利を持つことを留保する事にあります。そして、ユーザーの期待にそぐわない将来/未来の動作に依存したコードにおける問題の発生を防止するために、このことを明確化しようとしています。

ただし、operator->new/delete演算子(配列版含む)はこの例外とされます。operator->は再起的に呼び出して結果を取得するためにメンバ構文(a.operator->())による呼び出しが許可されており、new/delete演算子は直接呼び出すことでメモリの確保・解放のみを行うことができるためです。

P3396R0 std::execution wording fixes

std::executionに関する規格文言の問題修正をまとめた提案。

これは、std::execution提案(P2300)に関するGithubのIssueトラッカー(https://github.com/cplusplus/)において、残ったままになっていた問題(LWG Issue相当)について、効率的な処理のために1つの提案にまとめたものです。

ここでは次の10件の問題とその解決案が提案されています

  1. The preconditions on run_loop::run() are too strict
  2. noexcept clause of basic-state constructor is incomplete
  3. Definition of an async operation’s environment’s ownership seems incorrect
  4. scheduler semantic requirements imply swapability but concept does not require it
  5. operation-state-task exposition-only type does not need a move constructor
  6. [exec.general] Wording for AS-EXCEPT-PTR should use 'Precondition:' instead of 'Mandates:' for runtime properties
  7. [exec.schedule.from] Potential access to destroyed state in impls-for::complete
  8. scheduler concept should require that move-constructor does not exit with an exception
  9. [exec.bulk] wording should indicate that f is called with i = [0, …, shape-1]
  10. The use of JOIN-ENV leads to inefficient implementations of composed environments

それぞれの詳細は提案を参照してください。

P3397R0 Clarify requirements on extended floating point types

拡張浮動小数点数型の算術演算について、ISO/IEC 60559に準拠すべきかどうかを明確にする提案。

C++23ではstd::float32_tなどの拡張浮動小数点数型が導入されました。std::bfloat16_tを除いて、これらの型はISO/IEC 60559(IEEE 754)の定める交換形式と対応する表現を持つように指定されています。

ただ、ISO/IEC 60559では浮動小数点数型の表現やフォーマットだけでなく、算術演算の再現性についても指定しています。そこでは、浮動小数点数型に対する算術演算が実行されたハードウェアやソフトウェアによらず、それらをどう組み合わされて実行された時でも同じ計算について同じ結果を生成することを要求するほか、丸めも正しく行うことを要求しています。良く知られているように、C++の標準浮動小数点数型はこの要件を全く満たしていません。

この提案が問題にしているのは、拡張浮動小数点数型についても同じことがいえるのか?という点です。std::float32_t等の型が定義される場合、その性質はISO/IEC 60559で指定される、というように規定していますが、これがフォーマット(表現)のみなのか、算術演算についてもなのかが不透明です。

委員会の中でもこの解釈は割れているようで、ある小グループではこの規定は表現にのみ適用されるという合意がされている一方で、SG6は逆に演算にも適用されるという投票を行ったことがあります。

もし仮に、表現に加えて演算についてもISO/IEC 60559に準拠する場合、その実装は既存の標準浮動小数点数型とは全く異なる実装が必要になり、<cmath>の関数群も同様に厳格な実装が求められます。そしてその実装では最適化は著しく制限され、拡張浮動小数点数型は標準浮動小数点数型と比較するとパフォーマンスで劣るようになるでしょう。特に、一部のハードウェアではサポートされていない場合もあるため、ソフトウェアによる実装になる場合もあります。これは、拡張浮動小数点数型が導入されたきっかけが機械学習におけるパフォーマンス目的であるという文脈にはそぐわない解釈です。

しかし、それでもなお現在の規格の文面は拡張浮動小数点数型のISO/IEC 60559への準拠が「表現のみ」なのか「演算も」なのかが曖昧であるため、これを明確化する必要があります。この提案ではそれについて文言を修正することを提案しています。

方向性について合意が取れたものではないですが、拡張浮動小数点数型の経緯などから、現在のリビジョンでは「表現のみ」の解釈を明確化する方向で文面を調整する提案を行っています。

P3398R0 User specified type decay

あるクラス型について、推論される型を指定するためのdecays_to(T)の提案。

この提案のモチベーションは、未発表の文字列補完提案(fリテラル)におけるパフォーマンス低下やダングリングの問題を回避する事にあります。fリテラルはその使用がstd::format()の呼び出しになる事を要求するリテラルで、f""の形の文字列リテラル内の{}内を置換フィールドとしてその内部の文字列については識別子名としてそのコンテキストから拾ってくるものです。

// fリテラルによって構築される型
template<...>
struct formatted_string decays_to(std::string) {
  ...
};

std::string x = "x";
auto s = f"value {x + "y"}";  // ok "xy"
// sは"xy"という文字列を保持するstd::string型、ダングリングの心配はない

// std::printの新しいオーバーロード
extern void std::print(const formatted_string& s);

std::print(f"value {x + "y"}"); // パフォーマンスの劣化が無い(std::stringの生成をしない)

fリテラルの生成結果がstd::stringである場合、std::cout << std::format(...)と同じパフォーマンスの問題が発生します。それを回避するために、fリテラルの結果はフォーマットに必要な情報(format_argsformat_string)を保持している独自の型(例のformatted_string)となっています。ただし、この場合はfリテラルの結果をautoで受けたりするとダングリング参照を生成してしまいます。

この場合に、fリテラルの結果をautoで受けるような場合にのみ、その結果をstd::stringに変換してしまうための仕組みがこの提案です。

型の定義時にdecays_to(T)と指定(final等と同じ)しておくことで、autoやテンプレートパラメータの推論時にこの指定した型Tに推論されるように指定します。

上記の例だと、formatted_string型にdecays_to(std::string)と指定されていることで、auto s = f"value {x + "y"}"で推論されるsの型はstd::stringになり、formatted_string型がstd::stringに暗黙変換可能であることによってfリテラルの結果はダングリング化する前にstd::stringに変換され、以降安全に使用可能になります。

std::printにはformatted_string型を直接受け取るオーバーロードが用意されることで、formatted_string型を直接受け取ることができるため、fリテラルの出力のためにstd::stringが逐一生成されることはありません。

この提案の内容はまた、式テンプレートにおけるよく知られた問題に対処することもできるようになります。式テンプレートは、型に式の構造を埋め込んでしまうことで式の構造を保存して、結果が必要になった時に初めて実際の計算を行うようにします。これにより、複雑な計算を実行する際の中間変数やコピーを削減することができます。

例えばC++の行列演算ライブラリの実装においては式テンプレートを用いて行列演算の実行を遅延評価することが行われますが、式テンプレートという技法を知らない場合に、式テンプレートが適用されている演算の結果をautoで受けたりテンプレートパラメータとして推論されるところににそのまま渡してしまったりすると、計算結果ではなく式テンプレートの中間構造型が得られてしまいます。この型は本来欲しかった結果型と同じように扱うことができなかったり、そのまま結果取得をしようと何度もアクセスすると、アクセスの旅に再計算が走ってしまうなどの問題があります。

template<typename L, typename R, typename Op>
class Binop<L, R, Op> decays_to(Matrix) {
public:
  Binop(explicit const& L lhs, explicit const& R rhs) :
    lhs(lhs), rhs(rhs)
  {}

  operator Matrix() {
    Matrix ret;
    for (int r = 0; r < lhs.height(); r++)
    for (int c = 0; c < lhs.width(); c++)
    ret[r, c] = operator[](r, c);
    return ret;
  }

  double operator[](size_t r, size_t c) const { return op(lhs[r, c], rhs[r, c]); }
  size_t width() const { return lhs.width(); }
  size_t height() const { return lhs.height(); }

private:
  L lhs;
  R rhs;
  OP op;
};

template<typename LHS, typename RHS>
explicit auto operator+(LHS&& lhs, RHS&& rhs) {
  return Binop<LHS, RHS, std::plus<>>(lhs, rhs);
}

Matrix a, b, c;

Matrix d = a + b + c;     // 両方の加算は要素ごとにまとめて行われる(遅延評価が効いている)
auto e = a + b + c;       // eはMatrix型
explicit auto p = a + b;  // decayを無効化する(Binop型が得られる
auto f = p + c;           // fはMatrix型であり、この計算のパフォーマンスはd,eと同じ

式テンプレートにおける計算の中間型にこの提案のdecays_toによって結果となる行列型を指定しておくことで、autoで受けられた時に中間型ではなく結果の行列型として取得させることができます。

この提案ではexplicitを提案するdecays_toの抑制指示に使用しており、この例のoperator+は戻り値型推論時にdecayすることなく中間のBinop型をそのまま返し、変数宣言においてexplicit autoとすることでdecays_toの指示する型ではなく本来の型に推論させることができます。

あるクラス型Cに対してdecays_to(T)と指定している時にauto推論などでCオブジェクトの型がTに推論される場合、あくまで結果型がTに推論されるようにするところまでがこの提案で、C -> Tへの変換については従来通りの扱いとなり、変換が可能ではない場合はコンパイルエラーとなります。

また、decltype(auto)の場合はdecays_to(T)に対してTが推論されますが、それ以外の場合(const auto&const T等の推論時)はプレイスホルダ型もしくはテンプレートパラメータに付加されているCV・参照修飾がTに対して適用されます。

decays_to(T)によってTに推論されるのは、型推論が実行されるすべてのコンテキストで行われます(関数戻り値型推論や、構造化束縛などを含む)。

同様に、型推論が行われるすべてのコンテキストにおいて、推論対象の型を表すもの(テンプレートパラメータやプレイスホルダ型)に対してexplicitを付加しておくことで、この機能による推論を無効化して本来の型を得ることができます。

template<typename T>
void f(explicit const T& x);


f<std::string>(f"Not deduced {"and not decayed"}"; // Call f<std::string>

最後に、この機能に関する型特性としてstd::decayを更新し、std::decaydecays_to(T)指定されている型に対してはTを帰す様に更新し、ある型にdecays_to(T)が指定されているかどうかを取得する型特性std::has_decays_to<T>の追加を提案しています。

この提案はEWGiのレビューにおいてさらに時間をかけて議論する合意を得られませんでした。

P3401R0 Enrich Creation Functions for the Pointer-Semantics-Based Polymorphism Library - Proxy

Proxyを構築するユーティリティ関数の提案。

Proxyとは、P3086で提案されている仮想関数と継承を利用しないで動的なポリモルフィズムを実現しようとするライブラリです。この提案はそこから構築に関するユーティリティを分離したものです。

提案されているの次の3つです

  • make_proxy(): 指定された値によってproxyオブジェクトを構築する際に、SBOを有効化して構築する
  • allocate_proxy(): カスタムアロケータを用いてproxyオブジェクトを構築する。std::allocate_sharedと同じ使用感
  • make_proxy_inplace: std::optionalと同様に、与えられた値を自身のストレージ内に保存するSBOポインタを提供します。inplace proxiable targetというコンセプトを満たす型に対して使用できます.

proxyクラスは通常ヒープを用いてその値を保持しますが、内部バッファに収まる場合はSBO(small buffer optimization)によってメモリ確保を回避することができます。ただしそれが可能な型のサイズなどは実装詳細でありユーザーが気にするべきことではなく、自動的に判定してほしいものがあります。ところが、proxyのコンストラクタは保持する型Tのポインタを受け入れるようになっているため、proxyに渡された時点で動的メモリ確保は完了しています。

例えばyearというクラスの値をproxyで保持するためには次のように書きます

struct year {
  static constexpr proxiable_ptr_constraints constraints{
    .max_size = sizeof(void*[2]),
    .max_align = alignof(void*[2]),
    .copyability = constraint_level::none,
    .relocatability = constraint_level::nothrow,
    .destructibility = constraint_level::nothrow
  };
  // other members to meet the facade name-requirement.
};

std::proxy<year> CreateYear() {
  return std::make_unique<int>(2024); // std::proxy<year>へ暗黙変換
}

intの値などは明らかにSBOの対象ですが、ユーザーが指定したポインタを受け取る以上proxyの型内部でSBOを自動適用することができません。そこで、この提案のファクトリ関数make_proxy()でそれを行うようにします。

std::proxy<year> CreateYear() {
  return std::make_proxy<year>(2024); // 動的メモリ確保されない
}

make_proxy()は内部で渡された値がSBO可能かどうかによってSBOを適用するしないを自動で判定したうえでproxyオブジェクトを構築するものです。SBOが適用されない場合、ヒープ領域に確保されたうえで、所有権管理が行われます(おそらくunique_ptrによって)。

proxyは通常所有権を引き取らないため渡すポインタのリソースの管理はユーザーの責任ですが、make_proxy()を使うとproxyにそれを委ねることができます。この場合にメモリの確保をカスタマイズしようとすると再び手動でリソース管理を行わなければならなくなるため、アロケータも一緒に渡すようにするのがallocate_proxy()です。

auto CreateHugeYear(){
  // sizeof(std::array<int, 1000>) is usually greater than the max size defined in facade,
  // calling allocate proxy has no limitation to the size and alignment of the target
  using HugeYearData = std::array<int, 1000>;

  return std::allocate_proxy<year, HugeYearData>(std::allocator<HugeYearData>{});
}

この関数はちょうどstd::allocate_shared()とよく似た使用感になります。

make_proxy_inplace()はinplace構築を行うmake_proxyです。これはstd::optionalのinplaceコンストラクタに対応するstd::make_optional()と同じような役割のものです。

先ほどのmake_proxy()の例に対して

auto CreateYear() {
  return std::make_proxy_inplace<year, int>(2024);
}

この提案の内容はP3086で提案されているライブラリ機能をベースとして、その構築を便利にするためのユーティリティだけを分離して提案しているものです。

P3402R0 A Safety Profile Verifying Class Initialization

クラスのすべてのサブオブジェクトが初期化されていることを保証するプロファイルの提案。

この提案はP3274で提案されているプロファイル機能の1つとして、クラスを構築した後でそのクラスのすべてのサブオブジェクト(メンバ変数と基底クラス)が何かしら初期化済みであることを強制し保証するプロファイルを提案するものです。

提案するのは[[Profiles::enable(initialization)]]というプロファイル属性で、クラスの定義に対して付加します。

struct [[Profiles::enable(initialization)]] parent1 {
  int i;
  parent1() = default; // iが初期化されていないためプロファイルに準拠していない(parent1は未検証)
};

struct [[Profiles::enable(initialization)]] child1 : public parent {
  int j;
  child1() : parent1(), j(42) {} // child1自体はプロファイル準拠している
}

// 検証済みのクラスではない
// プロファイルが指定されれば準拠できる
struct parent2 {
  int i = 0;
  parent2() = default;
};

struct [[Profiles::enable(initialization)]] child2 : public parent2 {
  int j;
  child2() : parent2(), j(42) {} // parent2が検証済みではないため、child2も未検証
}

スカラ型もしくはこのプロファイルが指定されているクラス型は検証済みのクラスと呼びます。検証済みのクラスのすべてのコンストラクタは次のプロパティを満たす必要があります

  • 全ての基底クラスは検証済み
  • 全ての基底クラスは、その非静的メンバ変数が読み取られる前に初期化されている
  • 全ての検証済みメンバ変数は、その非静的メンバ変数が読み取られる前に初期化されている
  • 全ての検証済みメンバ変数は、全てのパスで初期化されている
  • 明示的に除外されているメンバ変数やそのメンバ変数は読み取れない
  • 全ての非静的メンバ変数が初期化されるまで、オブジェクト引数(this or 明示的オブジェクト引数)の使用は制限される
    • 制限されている場合、オブジェクト引数は非静的メンバ変数へのアクセスにのみ使用できる(初期化のため)
  • 全ての非静的メンバ変数が初期化されるまで、コンストラクタは検証済みのクラスのコンストラクタのみを呼び出すことができる
    • それまでは、他の関数の呼び出しは禁止される
  • 関数呼び出しの戻り値は、直接的にも間接的にもメンバ変数に代入することはできない
  • 検証においては、デフォルト初期化は何かを初期化しているとはみなされない

任意の関数内において非静的メンバ変数が参照されている場合、次の場合を除いてそれは読み取られたとみなされます

  • 評価されないオペランドで使用されている
  • 破棄された文の一部である
  • 代入式の左オペランドである
  • コンストラクタ呼び出しの受け取りオブジェクトである

提案文書より、サンプルコード

struct [[Profiles::enable(initialization)]] clazz1 {
  int i;
  int j;
  int z = 0;

  clazz1() {
    i = 123;

    if (nondet) {
      j = 456;
    }
    // 非準拠: jは全てのパスで初期化されていない
  }
};
struct [[Profiles::enable(initialization)]] clazz2 {
  int i;
  int j;

  clazz2() : i(j), j(42) {} // 非準拠: jが初期化される前に読み取られる
};
struct [[Profiles::enable(initialization)]] clazz3 {
  int i;
  int j;

  // 準拠できているものの、良くない形
  clazz3() {
    this->i = 0;
    this->j = 42;
  }
};
struct pod {
  int i;
  int j;
};

struct [[Profiles::enable(initialization)]] clazz4 {
  pod p;
  clazz4() = default; // pはデフォルト初期化のため、非準拠
};

struct [[Profiles::enable(initialization)]] clazz5 {
  pod p{};
  clazz5() = default; // pは値初期化されており、準拠している
}


struct [[Profiles::enable(initialization)]] clazz6 {
  pod podFactory() {
    pod p;    // pはデフォルト初期化されている
    return p;
  }

  clazz6() : p(podFactory()) {} ; // 非準拠、pは関数戻り値で初期化されている
}
struct [[Profiles::enable(initialization)]] clazz7 {
  int i;
  int j;

  clazz7(int i) : i(i), j() {}; // jは値初期化されており、準拠している
};

クラステンプレートの場合は、インスタンス化にあたって検証作業が行われます

class NotAnnotated{/**/};
class [[Profiles::enable(initialization)]] Annotated {/**/};

template<typename T>
class [[Profiles::enable(initialization)]] AnnotatedTemplate {
  T field = T();
};


void foo() {
  AnnotatedTemplate<NotAnnotated> nat {}; // 非準拠、検証されていないクラスのコンストラクタを呼び出している
  AnnotatedTemplate<Annotated>     at {}; // 準拠している
}

このプロファイルに準拠しているクラス型は、そのコンストラクタ呼び出しの後で全てのメンバ変数と基底クラスが初期化済みであることが保証され、その観点からは安全に使用することができます(明確ではないですが、検証に失敗するとコンパイルエラーになるはずです)。

また、提案するプロファイルの下で特定のデータメンバを検証から除外するためには、[[indeterminate]]属性を利用することを提案しています(この属性自体はC++23で追加されたもの)。

おわり

この記事のMarkdownソース

次月分(2024/08)の記事執筆のお手伝いを募集しています: https://github.com/onihusube/blog/issues/45




以上の内容はhttps://onihusube.hatenablog.com/entry/2025/02/24/215818より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14