文書の一覧
全部で51本あります。
もくじ
- N4990 Business Plan and Convener's Report
- P0472R2 Put std::monostate in <utility>
- P1030R7 std::filesystem::path_view
- P1061R9 Structured Bindings can introduce a Pack
- P2019R7 Thread attributes
- P2287R3 Designated-initializers for base classes
- P2319R1 Prevent path presentation problems
- P2688R2 Pattern Matching: match Expression
- P2786R7 Trivial Relocatability For C++26
- P2835R5 Expose std::atomic_ref's object address
- P2835R6 Expose std::atomic_ref's object address
- P2841R4 Concept and variable-template template-parameters
- P2846R3 reserve_hint: Eagerly reserving memory for not-quite-sized lazy ranges
- P2879R0 Proposal of std::dump
- P2945R1 Additional format specifiers for time_point
- P2988R7 std::optional<T&>
- P3016R4 Resolve inconsistencies in begin/end for valarray and braced initializer lists
- P3019R9 Vocabulary Types for Composite Class Design
- P3037R3 constexpr std::shared_ptr
- P3074R4 trivial unions (was std::uninitialized<T>)
- P3096R3 Function Parameter Reflection in Reflection for C++26
- P3128R1 Graph Library: Algorithms
- P3128R2 Graph Library: Algorithms
- P3210R2 A Postcondition is a Pattern Match
- P3245R2 Allow [[nodiscard]] in type alias declarations
- P3248R2 Require [u]intptr_t
- P3290R2 Integrating Existing Assertions With Contracts
- P3295R1 Freestanding constexpr containers and constexpr exception types
- P3299R1 Range constructors for std::simd
- P3309R2 constexpr atomic and atomic_ref
- P3335R1 Structured Core Options
- P3371R1 Fix C++26 by making the rank-1, rank-2, rank-k, and rank-2k updates consistent with the BLAS
- P3372R1 constexpr containers and adapters
- P3375R0 Reproducible floating-point results
- P3379R0 Constrain std::expected equality operators
- P3380R0 Extending support for class types as non-type template parameters
- P3381R0 Syntax for Reflection
- P3382R0 Coarse clocks and resolutions
- P3383R0 mdspan.at()
- P3384R0 __COUNTER__
- P3385R0 Attributes reflection
- P3388R0 When Do You Know connect Doesn't Throw?
- P3389R0 Of Operation States and Their Lifetimes (LEWG Presentation 2024-09-10)
- P3390R0 Safe C++
- P3391R0 constexpr std::format
- P3392R0 Do not promise support for function syntax of operators
- P3396R0 std::execution wording fixes
- P3397R0 Clarify requirements on extended floating point types
- P3398R0 User specified type decay
- P3401R0 Enrich Creation Functions for the Pointer-Semantics-Based Polymorphism Library - Proxy
- P3402R0 A Safety Profile Verifying Class Initialization
- おわり
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の提案。
以前の記事を参照
- P1030R4
std::filesystem::path_view- WG21月次提案文書を眺める(2020年12月) - P1030R5
std::filesystem::path_view- WG21月次提案文書を眺める(2022年09月) - P1030R6
std::filesystem::path_view- WG21月次提案文書を眺める(2023年07月)
このリビジョンでの変更は
- 最新の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
構造化束縛可能なオブジェクトをパラメータパックに変換可能にする提案。
以前の記事を参照
- P1061R2 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2022年04月)
- P1061R3 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2022年10月)
- P1061R4 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2023年02月)
- P1061R5 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2023年05月)
- P1061R6 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2023年12月)
- P1061R7 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2024年02月)
- P1061R8 Structured Bindings can introduce a Pack - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は、提案する文言の調整と実装経験セクションを更新したことです。
この提案はこの次のリビジョンが2024年11月の全体会議で採択されています。
P2019R7 Thread attributes
std::thread/std::jthreadにおいて、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。
- P2019R1 Usability improvements for
std::thread- [C++]WG21月次提案文書を眺める(2022年08月) - P2019R2 Usability improvements for
std::thread- [C++]WG21月次提案文書を眺める(2022年10月) - P2019R3 Thread attributes - [C++]WG21月次提案文書を眺める(2023年05月)
- P2019R4 Thread attributes - [C++]WG21月次提案文書を眺める(2023年10月)
- P2019R5 Thread attributes - [C++]WG21月次提案文書を眺める(2024年01月)
- P2019R6 Thread attributes - [C++]WG21月次提案文書を眺める(2024年05月)
このリビジョンでの変更は
char8_tサポートを削除- スレッド名の指定で
char8_t文字列を指定できていたが禁止 wchar_t/char16_t/char32_tのサポートは意味がないとしてされていなかったがchar8_tはされていたものの、議論不足のための措置であり、将来に追加する事にして別の提案で議論する- ABI互換性確保のために、名前を受け取るAPIでは
basic_string_view<T>を取るようにする
- スレッド名の指定で
などです。
P2287R3 Designated-initializers for base classes
基底クラスに対して指示付初期化できるようにする提案。
以前の記事を参照
- P2287R0 Designated-initializers for base classes - [C++]WG21月次提案文書を眺める(2021年1月)
- P2287R1 Designated-initializers for base classes - [C++]WG21月次提案文書を眺める(2021年2月)
- P2287R2 Designated-initializers for base classes - [C++]WG21月次提案文書を眺める(2023年4月)
このリビジョンでの変更は
- 指示子(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++へのパターンマッチング導入に向けて、別提案との基本構造の違いに焦点を当てた議論。
以前の記事を参照
- P2688R0 Pattern Matching Discussion for Kona 2022 - WG21月次提案文書を眺める(2022年10月)
- P2688R1 Pattern Matching:
matchExpression - WG21月次提案文書を眺める(2024年02月)
このリビジョンでの変更は
などです。
P2786R7 Trivial Relocatability For C++26
trivially relocatableをサポートするための提案。
以前の記事を参照
- P2786R0 Trivial relocatability options - WG21月次提案文書を眺める(2023年02月)
- P2786R1 Trivial relocatability options - WG21月次提案文書を眺める(2023年05月)
- P2786R2 Trivial relocatability options - WG21月次提案文書を眺める(2023年07月)
- P2786R3 Trivial relocatability options - WG21月次提案文書を眺める(2023年10月)
- P2786R4 Trivial relocatability options - WG21月次提案文書を眺める(2024年02月)
- P2786R5 Trivial relocatability options - WG21月次提案文書を眺める(2024年04月)
- P2786R6 Trivial relocatability options - WG21月次提案文書を眺める(2024年05月)
このリビジョンでの変更は
- EWG/LEWGでの懸念の提起に対処するために大幅に書き直し
- trivial relocatabilityの提示と議論を簡素化
- swapに関する議論を統合(P3239R0から
- 動作の変更
- 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が参照しているオブジェクトのアドレスを取得できるようにする提案。
以前の記事を参照
- P2835R0 Expose
std::atomic_ref's object address - WG21月次提案文書を眺める(2023年05月) - P2835R1 Expose
std::atomic_ref's object address - WG21月次提案文書を眺める(2023年07月) - P2835R2 Expose
std::atomic_ref's object address - WG21月次提案文書を眺める(2024年01月) - P2835R3 Expose
std::atomic_ref's object address - WG21月次提案文書を眺める(2024年02月) - P2835R4 Expose
std::atomic_ref's object address - WG21月次提案文書を眺める(2024年05月)
R5での変更は
- P3309とP3323を考慮して更新
- 戻り値型を
T*に戻すconstexprをサポートする唯一の設計であるため
- 既存のポインタを返すAPIを参考に名前を変更
このリビジョンでの変更は
- LEWGは戻り値型として
T*を確認 - LEWGは名前として
address()を選択
などです。
P2841R4 Concept and variable-template template-parameters
コンセプトを受け取るためのテンプレートテンプレートパラメータ構文の提案。
以前の記事を参照
- P2841R0 Concept Template Parameters - WG21月次提案文書を眺める(2023年05月)
- P2841R1 Concept Template Parameters - WG21月次提案文書を眺める(2023年10月)
- P2841R2 Concept Template Parameters - WG21月次提案文書を眺める(2024年04月)
- P2841R3 Concept Template Parameters - WG21月次提案文書を眺める(2024年05月)
このリビジョンでの変更は
- テンプレートコンセプトパラメータを参照する関数テンプレートは、包摂の対象外とした
- 制約の正規化のセクションを改善
- 例を改善して追加
- 新しい文法要素の名前を変更
- テンプレートパラメータとそのパックを紹介するセクションを追加
- id式を使用しないようにテンプレート引数の文言を変更
- コンセプト依存制約の導入
- CWGフィードバックへの対応
などです。
P2846R3 reserve_hint: Eagerly reserving memory for not-quite-sized lazy ranges
遅延評価のため要素数が確定しない range の ranges::to を行う際に、推定の要素数をヒントとして知らせる ranges::reserve_hint CPO を追加する提案。
以前の記事を参照
- P2846R0
size_hint: Eagerly reserving memory for not-quite-sized lazy ranges - WG21月次提案文書を眺める(2023年05月) - P2846R1
size_hint: Eagerly reserving memory for not-quite-sized lazy ranges - WG21月次提案文書を眺める(2023年09月) - P2846R2
reserve_hint: Eagerly reserving memory for not-quite-sized lazy ranges - WG21月次提案文書を眺める(2024年05月)
このリビジョンでの変更は、reserve_intをstd::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
モチベーションとしては
- 他のプログラミング言語での既存の慣習とする
- Pythonの
print(a, b, c)はstd::dump(a, b, c)と書ける
- Pythonの
- 簡単なテスト、デモ、実験プログラムでの利用
- 短いプログラムや一時的なコードで、変数の値を手軽に出力するのに便利
- コード例の簡潔化
-
std::print({}, {}, {}, ...)やstd::cout << ... << ...等のように余計な文字列を省いて例示できる
-
- デバッグの補助
- デバッガを使用せずに実行時に変数値を確認するための簡単なコードとして使用可能
- デバッガが使えない環境や、リアルタイム制約がある環境(一時停止で動作が変わる環境)において、一時的なprintデバッグのために活用できる
- 科学計算での利用
- 行が改行で区切られ、数値がスペースで区切られた形式は、行列や表の一般的な形式であり、科学計算での利用に適している
- これは
std::dump()が生成する形式
- これは
- 行が改行で区切られ、数値がスペースで区切られた形式は、行列や表の一般的な形式であり、科学計算での利用に適している
- Unixツールとの連携
などが挙げられています。
この提案はLEWGIにおいて時間をかけることに合意が得られませんでした。
P2945R1 Additional format specifiers for time_point
<chrono>のtime_point型のフォーマット指定を追加する提案。
以前の記事を参照
このリビジョンでの変更は、既存のコードの意味を変更するオプションをすべて削除したことです。
R0では%Sオプションの動作の変更(秒を2桁で出力し、ミリ秒未満を出力しないようにする)を提案していましたが、このリビジョンでは削除されたため、この提案は純粋な機能拡張のみとなりました。
P2988R7 std::optional<T&>
std::optionalが参照を保持することができるようにする提案。
以前の記事を参照
- P2988R0
std::optional<T&>- WG21月次提案文書を眺める(2023年10月) - P2988R1
std::optional<T&>- WG21月次提案文書を眺める(2024年01月) - P2988R3
std::optional<T&>- WG21月次提案文書を眺める(2024年02月) - P2988R4
std::optional<T&>- WG21月次提案文書を眺める(2024年04月) - P2988R5
std::optional<T&>- WG21月次提案文書を眺める(2024年05月) - P2988R6
std::optional<T&>- WG21月次提案文書を眺める(2024年08月)
このリビジョンでの変更は
などです。
P3016R4 Resolve inconsistencies in begin/end for valarray and braced initializer lists
std::valarrayと初期化子リストに対してstd::beginとstd::cbeginを呼んだ場合の他のコンテナ等との一貫しない振る舞いを修正する提案。
以前の記事を参照
- P3016R0 Resolve inconsistencies in
begin/endforvalarrayand braced initializer lists - WG21月次提案文書を眺める(2023年10月) - P3016R1 Resolve inconsistencies in begin/end for valarray and braced initializer lists - WG21月次提案文書を眺める(2023年12月)
- P3016R2 Resolve inconsistencies in begin/end for valarray and braced initializer lists - WG21月次提案文書を眺める(2023年12月)
- P3016R3 Resolve inconsistencies in begin/end for valarray and braced initializer lists - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- LWG Issue 4131の変更(Issue解決)の文面を追加
- 提案はしていない
などです。
P3019R9 Vocabulary Types for Composite Class Design
動的メモリ領域に構築されたオブジェクトを扱うためのクラス型の提案。
以前の記事を参照
- P3019R0 Vocabulary Types for Composite Class Design - WG21月次提案文書を眺める(2023年10月)
- P3019R3 Vocabulary Types for Composite Class Design - WG21月次提案文書を眺める(2023年12月)
- P3019R6 Vocabulary Types for Composite Class Design - WG21月次提案文書を眺める(2024年02月)
- P3019R8 Vocabulary Types for Composite Class Design - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- コンストラクタの順序を変更
indirectに変換代入演算子を追加indirectとpolymorphicに変換コンストラクタを追加indirectとpolymorphicに初期化子リストコンストラクタを追加- ‘heap’や‘free-store’等の用語の使用を‘dynamically-allocated storage’に変更
などです。
P3037R3 constexpr std::shared_ptr
std::shared_ptrを定数式でも使える様にする提案。
以前の記事を参照
- P3037R0
constexpr std::shared_ptr- WG21月次提案文書を眺める(2023年12月) - P3037R1
constexpr std::shared_ptr- WG21月次提案文書を眺める(2024年04月) - P3037R2
constexpr std::shared_ptr- WG21月次提案文書を眺める(2024年07月)
このリビジョンでの変更は
reinterpret_pointer_castからconstexprを取り除いた- 関連提案への参照の追加
- libc++ベースの2つ目の実装経験の追加
などです。
除外されたのは、例外やreinterpret_castなどの定数式では実行できない操作を含むものです。
P3074R4 trivial unions (was std::uninitialized<T>)
定数式において、要素の遅延初期化のために共用体を用いるコードを動作するようにする提案。
以前の記事を参照
- P3074R0 constexpr union lifetime - WG21月次提案文書を眺める(2023年12月)
- P3074R2
std::uninitialized<T>- WG21月次提案文書を眺める(2024年02月) - P3074R3 trivial union (was std::uninitialized
) - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は、以前に提案していた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に向けた静的リフレクションに対して、関数仮引数に対するリフレクションを追加する提案。
以前の記事を参照
- P3096R0 Function Parameter Reflection in Reflection for C++26 - WG21月次提案文書を眺める(2024年02月)
- P3096R1 Function Parameter Reflection in Reflection for C++26 - WG21月次提案文書を眺める(2024年05月)
- P3096R2 Function Parameter Reflection in Reflection for C++26 - WG21月次提案文書を眺める(2024年07月)
このリビジョンでの変更は
- 提案する文言の改善
- P2996R5の内容を反映
などです。
P3128R1 Graph Library: Algorithms
↓
P3128R2 Graph Library: Algorithms
グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、提案するグラフアルゴリズムについてまとめた文書。
R1での変更は
- Traversalセクションを追加し、幅優先探索アルゴリズムとトポロジカルソートアルゴリズムを移動。また、深さ優先探索アルゴリズムを追加
- ダイクストラ法とベルマン–フォード法のアルゴリズムの改訂
- ベルマン–フォード法のアルゴリズムにおいて、負の重みサイクルを検出する機能を追加
このリビジョンでの変更は、コントリビューターを追加したことです。
P3210R2 A Postcondition is a Pattern Match
事後条件の構文をパターンマッチングの構文に親和させる提案。
以前の記事を参照
- P3210R0 A Postcondition is a Pattern Match - WG21月次提案文書を眺める(2024年04月)
- P3210R0 A Postcondition is a Pattern Match - WG21月次提案文書を眺める(2024年05月)
このリビジョンでの変更は
- 提案1の内容を、P2737のキーワード構文を採用するものではなく、P2688のバインディング構文に一致させる、に変更
- 他の部分をそれに合わせて変更
- モチベーションの明確化
などです。
参照する提案が変わっただけで、提案の内容そのものは大きく変化していません。
変更後の内容もSG21でのコンセンサスを得られず、リジェクトされています。
P3245R2 Allow [[nodiscard]] in type alias declarations
[[nodiscard]] 属性を型エイリアス宣言で使用できるようにする提案。
以前の記事を参照
- P3245R0 Allow
[[nodiscard]]in type alias declarations - WG21月次提案文書を眺める(2024年04月) - P3245R1 Allow
[[nodiscard]]in type alias declarations - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は、提案する文言を追加したことです。
この提案は、EWGのレビューにおいてコンセンサスを得られず、リジェクトされています。
P3248R2 Require [u]intptr_t
(u)intptr_tを必須にする提案。
以前の記事を参照
- P3248R0 Require [u]intptr_t - WG21月次提案文書を眺める(2024年05月)
- P3248R1 Require [u]intptr_t - WG21月次提案文書を眺める(2024年07月)
このリビジョンでの変更は、SG1での投票結果を追記したことです。
P3290R2 Integrating Existing Assertions With Contracts
既存のアサーション機構に契約プログラミング機能を統合する提案。
以前の記事を参照
- P3290R0 Integrating Existing Assertions With Contracts - WG21月次提案文書を眺める(2024年05月)
- P3290R0 Integrating Existing Assertions With Contracts - WG21月次提案文書を眺める(2024年07月)
このリビジョンでの変更は、バグや説明の修正とライブラリAPIの使用例を追加したことです。
P3295R1 Freestanding constexpr containers and constexpr exception types
フリースタンディング環境の定数式において、std::vector等を使用可能にする提案。
以前の記事を参照
このリビジョンでの変更は
- 実装経験についての議論を追加
<stdexcept>のすべての例外型を追加- デフォルト構築されたコンテナと
operator deleteについての議論を追加 - 残りの非
constexpr関数について追記 - アロケータ要件の難しさについて議論を追加
constexpr例外の提案とアロケータの提案とのコンフリクトについて追記- 文言の改善
などです。
P3299R1 Range constructors for std::simd
std::simdにrangeコンストラクタを追加する提案。
以前の記事を参照
このリビジョンでの変更は
- 初期化に使用する範囲のサイズが実行時にしか分からない場合で、サイズのミスマッチが検出された場合の動作をerroneous behaviourとする方針の説明の追加
- 足りない場合、余った要素はデフォルト初期化し、読み出しはEBとする
- 例を追加
などです。
P3309R2 constexpr atomic and atomic_ref
std::atomic/stomic_refをconstexpr化する提案。
以前の記事を参照
- P3309R0
constexpratomicandatomic_ref- WG21月次提案文書を眺める(2024年05月) - P3309R1
constexpratomicandatomic_ref- WG21月次提案文書を眺める(2024年07月)
このリビジョンでの変更は、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/hermitianrank-k/rank-2k update 関数を上書きと更新のオーバーロードに変更するのに加えて、rank-1およびrank-2 update系関数も同様に変更する- 追加する全ての更新オーバーロードについて、入力行列の
C(またはA)がEとエイリアスすることを許可 - 追加する
symmetric/hermitianupdateのオーバーロードについて、関数がE引数にC(またはA)と同じ方法でアクセスすることを指定- 同じ方法とは例えば、上三角行列や下三角行列として
hermitianrank-1/rank-k update関数に必要な、スケーリング係数を非複素数に制限するための説明専用コンセプトnoncomplexを追加
- 文言の変更に合わせてタイトルと概要を変更
- rank-1/rank-2 update関数をrank-k/rank-2k update関数と同様に変更する理由を説明するセクションを追加
hermitian_matrix_vector_product、hermitian_matrix_product、triangular_matrix_productおよびtriangular_*_solve関数を更新しない理由を説明するセクションの追加- 実行時ではなく、コンパイル時に一部のスケーリング係数を非複素数に制限する理由を説明するセクションを追加
- 説明セクションを拡充し、再編成
などです。
R0での対象は、rank-kおよびrank-2k update系の関数
linalg::symmetric_matrix_rank_k_update():C := C + αAA^Tlinalg::hermitian_matrix_rank_k_update():C := C + αAA^Hlinalg::symmetric_matrix_rank_2k_update():C := C + αAB^H + αBA^Hlinalg::hermitian_matrix_rank_2k_update():C := C + αABH + ᾱBAH(ᾱはαの複素共役
のみでしたが、このリビジョンではrank-1およびrank-2 update系の関数
symmetric_matrix_rank_1_update():A := A + αxx^Thermetian_matrix_rank_1_update():A := A + αxx^Hsymmetric_matrix_rank_2_update():A := A + αxy^T + αyx^Thermitian_matrix_rank_2_update():A := A + αxy^H + ᾱxy^H
も対象に加わりました。これらの関数は現在無条件の更新を行っており、上書き動作が無くスケーリング係数βを一項目のAに適用する方法がありません(これもやはり、対応するBLASのルーチンと動作が一致しない)。そのため、rank-kおよびrank-2kの関数と同様に、デフォルトの動作を上書きに変更したうえで、更新オーバーロードを追加することを提案しています。
結局、このリビジョンでは次の3つの事を提案しています
- rank-1, rank-2, rank-2, rank-2k update系関数に、更新オーバーロードを追加
- 追加される関数は、
matrix_product()の更新オーバーロードと類似している
- 追加される関数は、
- 現在のrank-1, rank-2, rank-2, rank-2k update系関数の動作を、無条件更新から上書きに変更
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による丸めモードの廃止と置換(よく似たクラステンプレートによる)だけを提案するものであるのに対して、この提案はさらに操作の融合や並べ替えを禁止することで計算の再現可能性を達成することを目指すものです。
- 浮動小数点演算の結果が環境依存なのはどんなときか - Zenn
- P2746R5 Deprecate and Replace Fenv Rounding Modes - WG21月次提案文書を眺める(2024年04月)
- P3375 進行状況
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に適用されています。
- C++20標準ライブラリ仕様:Constraints/Mandates/Preconditions - yohhoyの日記
- LWG Issue 4072. std::optional comparisons: constrain harder
- P3379 進行状況
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::tupleやstd::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()>()は0と1のどちらの値を返すでしょうか?あるいは両方でしょうか?今はカスタムのシリアル化によって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::vectorやstd::tupleをシリアル化するには新しいstd::vectorやstd::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::vectorはstd::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 { } }
この戻り値型voidのoperator 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つの形式のいずれかでconstevalなoperator templateを提供できるvoidを返す: すべての基底クラスと非静的データメンバは構造型でなければならず、mutableであってはならないRを返す:R.data()はstd::meta::info const*に変換可能で、R.size()はsize_tに変換可能である必要があるT(std::meta::from_template, R)が有効な式である必要がある
- テンプレート引数の正規化の概念の導入
- 構造型
Tの値vは、以下のようにテンプレート引数として正規化されますTがスカラー型または左辺値参照型の場合、何も行わないTが配列型の場合、配列のすべての要素がテンプレート引数として正規化されるTがクラス型の場合Tがvoidを返すテンプレート表現関数を提供する場合、その関数がv上で呼び出され、vのすべてのサブオブジェクトがテンプレート引数として正規化されるTがstd::meta::infoの範囲を返すテンプレート表現関数を提供する場合、新しい値T(std::meta::from_template, v.operator template())がvの代わりに使用されるTがテンプレート表現関数を提供しない場合、vのすべてのサブオブジェクトがテンプレート引数として正規化される
- 構造型
- P2996の
std::meta::reflect_valueの意味を、引数に対してテンプレート引数の正規化を実行するように変更 - 構造的型の定義の拡張
- テンプレート引数として等価(template-argument-equivalent)の定義の拡張
- 2つの値がテンプレート引数として等価であるのは、それらが同じ型であり、次の場合
- [...]
- どちらもクラス型
TでありTが非voidを返す資格のあるテンプレート表現関数を持つクラス型である場合、2つの値に対してテンプレート表現関数を呼び出した結果であるr1とr2について、r1.size() == r2.size()であり、0 <= i < r1.size()の各iについて、r1.data()[i] == r2.data()[i]である- それ以外の場合、対応する直接のサブオブジェクトと参照メンバがテンプレート引数として等価である
- 2つの値がテンプレート引数として等価であるのは、それらが同じ型であり、次の場合
- クラス型の非型テンプレートパラメータを初期化する際に、テンプレート引数の正規化を実行するようにする
- テンプレート表現関数 (
- ライブラリ
- 新しいタグ型
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を返す形式は検討しておくことができます。
- C++20 非型テンプレートパラメータとしてクラス型を許可する [P0732R2] - cpprefjp
- P2484R0 Extending class types as non-type template parameters - WG21月次提案文書を眺める(2021年11月)
- P3380 進行状況
P3381R0 Syntax for Reflection
リフレクション構文のための演算子として^^を使用する提案。
P2996で提案されているC++26に向けたリフレクション機能においては、任意のエンティティからリフレクション情報を取り出す演算子として^を使用しています(auto info = ^T;のように使用)。しかし、Objective-C++におけるブロック拡張の構文と衝突していることが指摘されました。
例えば次のような構文は
type-id(^ident)();
2つの解釈があり得ます
type-idを返し、引数を取らないブロックを保持するidentという名前の変数std::meta::infoをtype-idにキャストして、operator()を呼び出す
1つ目がObjective-C++におけるブロック拡張であり、このことはP3294R1のトークンシーケンス構文(^{ ... })が導入されるとさらに影響が大きくなります。
このために、この提案は^の代替となる演算子を探索し、提案するものです。
構文候補には次の三種類が考えられます
- キーワード
- 1文字(の演算子
- 複数文字
とはいえ元々、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: 大きい^、\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_clockとcoarse_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つの時計型において、durationとtime_pointの型が元の時計型と同じにされているのは、利便性のためです。例えば先ほどのフォールバックのサンプルコードのように、同じdurationとtime_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にて実装されています。
- Add element access via
at()tostd::mdspanby stephanlachnit · Pull Request #302 · kokkos/mdspan - P3383 進行状況
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-listはalignasをカバーしていないが、これは別の提案で議論する予定
- 属性が許可されているコンテキストで、
- メタ関数
attributes_of(info) -> std::vector<info>: 指定されたリフレクション値に付随する各属性を表すstd::meta::infoのシーケンスを返すis_attribute(info) -> bool: 指定されたリフレクション値が属性を指している場合にtrueを返す- 属性名の取得: 属性のリフレクションから、属性トークンに対応する文字列や、属性を識別するのに役立つテキストを返す
name_of(info) -> string_viewu8name_of(info) -> u8string_viewqualified_name_of(info) -> string_viewu8qualified_name_of(info) -> u8string_viewdisplay_name_of(info) -> string_viewu8display_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において、非同期操作を表現するsenderはreceiverとconnectしてoperation_stateを取得し、それに対してstartすることによってそれが表す非同期操作を開始することができます。現在の仕様ではconnectの操作は例外を送出しうる一方で、startの操作が例外を送出するのは禁止されています。
std::executionを利用する際にやることは簡単に次の3ステップに分割できます
senderとreceiverの選択- 1で選択した
senderとreceiverの接続(connect) - 2の結果の
operation_stateをstartする
このうち、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はsenderとreceiverの構築時の例外であるためこれは制限できませんが、ステップ2の場合は制限を少し強めることでこれが可能になります。この提案はそれを検討し、提案しています。
この提案では次の2つの事を提案しています
receiverコンセプトの一部として、receiverが例外を投げずにムーブ可能であることを要求する- 次のいずれかによって、特定の環境を持つ
receiverに対して、senderのconnectメンバ関数が例外を送出しないことを判断する仕組みを導入する- ある
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::executionのoperation_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への露出を減らす - スレッド安全性:
sendやsyncのような機能でデータ競合を防ぐ - ランタイムチェック: 上記のような静的解析では検出できない不健全な動作を実行時に検出する(範囲外アクセスなど)
提案文書より、サンプルコード
// 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の実行中初期化されて生存しているvectorのslice_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_entryやfilesystem::pathなど)を除いた、既存の標準のフォーマット可能な型についてのみ、コンパイル時のフォーマットを有効化することを提案しています。
{fmt}ライブラリでは<chrono>型を除いてconstexprなformat()がすでにサポートされているほか、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件の問題とその解決案が提案されています
- The preconditions on
run_loop::run()are too strict noexceptclause of basic-state constructor is incomplete- Definition of an async operation’s environment’s ownership seems incorrect
- scheduler semantic requirements imply swapability but concept does not require it
operation-state-taskexposition-only type does not need a move constructor- [exec.general] Wording for AS-EXCEPT-PTR should use 'Precondition:' instead of 'Mandates:' for runtime properties
- [exec.schedule.from] Potential access to destroyed state in
impls-for::complete - scheduler concept should require that move-constructor does not exit with an exception
- [exec.bulk] wording should indicate that f is called with i = [0, …, shape-1]
- 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_argsとformat_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::decayがdecays_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も未検証 }
スカラ型もしくはこのプロファイルが指定されているクラス型は検証済みのクラスと呼びます。検証済みのクラスのすべてのコンストラクタは次のプロパティを満たす必要があります
- 全ての基底クラスは検証済み
- 全ての基底クラスは、その非静的メンバ変数が読み取られる前に初期化されている
- 全ての検証済みメンバ変数は、その非静的メンバ変数が読み取られる前に初期化されている
- 全ての検証済みメンバ変数は、全てのパスで初期化されている
- 明示的に除外されているメンバ変数やそのメンバ変数は読み取れない
- 全ての非静的メンバ変数が初期化されるまで、オブジェクト引数(
thisor 明示的オブジェクト引数)の使用は制限される- 制限されている場合、オブジェクト引数は非静的メンバ変数へのアクセスにのみ使用できる(初期化のため)
- 全ての非静的メンバ変数が初期化されるまで、コンストラクタは検証済みのクラスのコンストラクタのみを呼び出すことができる
- それまでは、他の関数の呼び出しは禁止される
- 関数呼び出しの戻り値は、直接的にも間接的にもメンバ変数に代入することはできない
- 検証においては、デフォルト初期化は何かを初期化しているとはみなされない
任意の関数内において非静的メンバ変数が参照されている場合、次の場合を除いてそれは読み取られたとみなされます
提案文書より、サンプルコード
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で追加されたもの)。
おわり
次月分(2024/08)の記事執筆のお手伝いを募集しています: https://github.com/onihusube/blog/issues/45