以下の内容はhttps://onihusube.hatenablog.com/entry/2025/04/20/205527より取得しました。


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

文書の一覧

全部で151本あります。

もくじ

N4991 2025 Sofia Meeting Invitation and Information

2025年6月16~21日にかけてブルガリアのソフィアで行われる予定の、WG21ミーティングのインフォメーション。

N4993 Working Draft, Programming Languages -- C++

C++26のワーキングドラフト第6弾

N4994 Editors' Report, Programming Languages -- C++

↑の変更点をまとめた文書。

P0260R11 C++ Concurrent Queues

標準ライブラリに並行キューを追加するための設計を練る提案。

以前の記事を参照

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

  • St. Louis会議におけるLEWG/SG1からのフィードバックを実装
  • コンセプトを説明専用にする
  • 非同期操作は、操作のreceiverschedulerで実行することを要求する
  • キューがcloseの場合は、非同期操作に対してset_errorを呼び出す
  • R9で誤って削除されていた、data-race freenessを復帰
  • bounded_queueにsequential constistentセマンティクスを要求する
  • experimental leftoversを削除

などです。

P0707R5 Metaclass functions for generative C++

リフレクション機能をベースとした、メタクラスの提案。

P2996R5(とP3294R1)ではコード要素のクエリとコード生成の両面から静的リフレクションの基本機能が整備されつつあります。そこでは、型のプロトタイプを作成してそれをベースに別の型をつくる、といったことが既に可能になっています。

// __prototype::widgetをベースにwidget型を生成する例
namespace __prototype { class widget { /*...*/ }; } 
consteval{ metafunc( ^^__prototype::widget ); } 

ここでのmetafuncは指定された型(のリフレクション)をベースに追加の指定を加えたうえで改めてwidget型の宣言を生成するようなメタ関数です。この提案では、このようなクラスの基本定義や制約をベースとしたクラス型そのものを構築するメタクラス機能を、この構文の構文糖として提供しようとするものです。

提案するメタクラス機能によって、上記コードは次のように簡潔に書くことができるようになります

// metafuncの指定する制約等によって、widget型を生成する
class(metafunc) widget{ /*...*/ }; 

これによるメリットは次のようなものです

  • 単なるクラスではなく特定の種類のクラスを書いている、という意図をより直接的に表現している
  • プロトタイプ型(最初の例の__prototype::widget)が不要になる

より具体的な例としては、インターフェースクラスの定義があります。インターフェースクラスの定義にはボイラープレートが多くあります。たとえば

// IFooインターフェースの手動定義
class IFoo { 
public: 
  virtual int f() = 0; 
  virtual void g(std::string) = 0; 
  virtual ~IFoo() = default; 
  IFoo() = default; 
  IFoo(IFoo const&) = delete; 
  void operator=(IFoo const&) = delete; 
};

インターフェース独自の関数のシグネチャ宣言を除いて、他のすべての部分はボイラープレートコードです。この手のボイラープレートコードを削減するのにリフレクションは非常に効力を発揮します。P2996R5とP3294R1でこのような同じIFooインターフェースを定義するコードは次のようになります

namespace __proto { 
  class IFoo { 
    int  f(); 
    void g(std::string); 
  }; 
}

consteval { interface(^^__proto::IFoo); }

interface()はこのために定義されたリフレクション関数ですが、これも含めてユーザー定義することができます。

widgetの例にはなりますが、すでにCompiler Explorerで動作するリンクがあります: godbolt

この提案のメタクラス機能でこのIFooインターフェース定義を書き直すと、次のようになります

class(interface) IFoo { 
  int  f(); 
  void g(std::string); 
};

よりシンプルであり、制約を表すメタ関数名によって宣言的になっています。

なお、提案しているメタクラス構文ではclass(...)のかっこの中にカンマ区切りで複数の関数を指定できます。これにより、複数の制約によるクラス定義生成が可能です

class(xxx, yyy, zzz) Widget { /*...*/ };

元々この提案は、リフレクション機能の部分も含めた機能としてのメタクラスの機能を提案していましたが、それはP2996R5とP3294R1でほぼ達成されているので、この提案ではそれらのリフレクションの上に構築するメタクラス機能を提案するようになりました。結果、この提案では次の2つのことのみを提案しています

  • クラス構文: class(xxx,yyy)
  • 制約を表すconstevalライブラリ関数群(こちらの事をメタクラスと呼んでいる)
    • interface: 純粋仮想関数のみを持つ抽象クラス型
    • polymorphic_base: コピーもムーブもできず、デストラクタが public + virtual または protected + 非仮想である純粋なポリモーフィック基底型
    • ordered: 全順序で順序付け可能な型であり、strong_orderingとなるoperator<=>を持つ
      • 他に: weakly_ordered、partially_ordered
    • copyable: コピー/ムーブ コンストラクタ/代入演算子を持つ型
    • basic_value: copyableであり、デフォルト構築可能かつデフォルトデストラクタを持ち、protectedvirtualな関数を持たない
    • value: orderedかつbasic_value
    • struct: publicなメンバだけを持つbasic_value。仮想関数やカスタム代入演算子を持たない
    • enum: 全てのメンバがpublicであり、orderedかつbasic_valueである
    • flag_enum: enumであり、各メンバはビット単位のsets/testが可能
    • union: 安全なタグ付き共用体
    • regex: リフレクション機能を用いた、CRTEスタイルのコンパイル正規表現
    • print: リフレクションをソースコードとしてコンパイル時に出力する

ただし、ライブラリ関数群は将来の提案としています。

この提案による別の恩恵として、特殊な型を追加したい場合に言語機能ではなくライブラリ機能として追加できるようになることがあります(例えば、enum classなどのような)。それにより、コア言語で標準化するには動機づけが弱いような型(影響範囲がごく小さいなど)をごく小さい変更でライブラリ機能として追加できるようになる可能性が開けます。また同時に、そのような特殊な型をユーザーが簡単に定義できるようになります。

P0876R18 fiber_context - fibers without scheduler

スタックフルコルーチンのためのコンテキストスイッチを担うクラス、fiber_contextの提案。

以前の記事を参照

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

  • ファイバー(fiber_contextオブジェクト)の状態として、準備完了(prepared)状態と中断(suspended)状態を区別する
  • resume_with()のエントリと復帰によって暗黙的に生じる2つのコンテキストスイッチを区別する
  • R17で不要になったcurrent_exception_within_fiber()を削除する

などです。

P1144R12 std::is_trivially_relocatable

オブジェクトの再配置(relocation)という操作を定義し、それをサポートするための基盤を整える提案。

以前の記事を参照

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

  • proseセクションの書き直し
  • 文言に不足していた前方宣言を追加
  • P2422を受けて、relocate(T*)から[[nodiscard]]を削除
  • [concept.relocatable]の修正
    • u2はオーバーラップする可能性のあるサブオブジェクトであってはならない
  • [uninitialized.relocate]と[specialized.relocate]の更新
    • "side effects might not happen,"から "do not happen"へ書き換え
    • memmove最適化が必須になった
      • これにより、trivially relocatableな型のrelocateが例外を送出しない事が保証される

などです。

P1255R14 A view of 0 or 1 elements: views::nullable And a concept to constrain maybes

任意のオブジェクトやstd::optional等のmaybeモナドな対象を要素数0か1のシーケンスに変換するRangeアダプタviews::maybe/views::nullableの提案。

以前の記事を参照

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

P1306R3 Expansion statements

コンパイル時にステートメントをループ生成することのできる、展開ステートメントの提案。

以前の記事を参照

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

  • rangeの展開は定数式に限ることを要求
  • break/continueによる制御フローのサポートを追加

などです。

break/continuetemplate for内部の生成コードで使用可能になるということではなく、template forによる展開ループの展開後のコードに対して作用します。breakは最後の展開の直後にジャンプし、continueは次の展開(ある場合)の先頭にジャンプします。

template for (auto v : {1,2,3,4,5,6,7,8,9}) {
  if (v % 2 == 0) continue;
  std::println("v: {} ", v);
  if (v % 5 == 0) break;
}

このコードのコンパイル結果は、実行すると次のような出力を行うコードを生成します

v: 1
v: 3
v: 5

少しイメージしづらいですが、template for内のbreak/continueは展開を行うループ(実際にループしているわけではないが)をスキップするのではなく、展開後の各展開要素の前後に対してジャンプするものです。gotoを使って無理やり書くと上記のコードは次のような展開結果を生成します

{
  {
    if (1 % 2 == 0) goto expansion_1;
    std::println("v: {} ", 1);
    if (1 % 5 == 0) goto template_for_end;
  }
expansion_1:
  {
    if (2 % 2 == 0) goto expansion_2;
    std::println("v: {} ", 2);
    if (2 % 5 == 0) goto template_for_end;
  }
expansion_2:
  {
    if (3 % 2 == 0) goto expansion_3;
    std::println("v: {} ", 3);
    if (3 % 5 == 0) goto template_for_end;
  }
expansion_3:
  {
    if (4 % 2 == 0) goto expansion_4;
    std::println("v: {} ", 4);
    if (4 % 5 == 0) goto template_for_end;
  }
expansion_4:
  {
    if (5 % 2 == 0) goto expansion_5;
    std::println("v: {} ", 5);
    if (5 % 5 == 0) goto template_for_end;
  }

expansion_5:
  // 以下略
  {
    ...
  }
  ...

template_for_end:
}

当然、実際にgotoが使用されるわけではありません。

この提案は、2025年1月の全体会議で採択され、C++26に導入されています。

P1494R4 Partial program correctness

因果関係を逆転するような過度な最適化を防止するためのバリアであるstd::observable()の提案。

以前の記事を参照

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

  • I/Oは自動的にobservable checkpointになる
  • 不足しているerroneous behaviorコンテキストを追加
  • 古い記述の削除
  • 文書の簡素化、明確化、修正

などです。

P1708R9 Basic Statistics

標準ライブラリにいくつかの統計関数を追加する提案。

以前の記事を参照

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

  • 重みづけあり/無しの分散と標準偏差の処理を更新
  • 重みづけ無しの歪度と尖度の処理を更新
  • 重みづけありの歪度と尖度は特殊すぎるため削除
  • 統計データ型を簡単に変換可能にするために関数オーバーロードを追加
  • 平均と分散(or標準偏差)を簡単に計算するための関数を追加
  • 複数の統計値の同時計算の実行時の計算量を軽減するためにAccumulatorオブジェクトを集約した
  • 多くの関数と引数名をより意味のあるものになるように変更

などです。

P1729R5 Text Parsing

std::formatの対となるテキストスキャン機能の提案。

以前の記事を参照

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

  • 予備的な文言の追加
  • 引数の処理と型消去の機能を見直し、コピーとムーブを適切に省略できるようにした
  • make_scan_result, make_scan_args, scan-arg-storeを再定義し、fill_scan_resultを追加
  • scanという名前についての説明を追加
  • fill+alignのロジックをより柔軟でわかりやすくした
  • 最大のフィールド幅を指定するためのprecision-specifierを追加
  • 最小のフィールド幅を指定するためのwidth-specifierを追加
  • scan_errorの成功状態を削除し、それら全体の表現型をexpected<void, scan_error>に置換
  • scan_error::value_out_of_rangeを、正と負のアンダーフロー/オーバーフローに対応する4つの列挙値に分割
  • scan_error::invalid_literal, scan_error::invalid_fill, scan_error::length_too_shortを追加
  • scanner::parseのエラー処理を改訂
  • scanner::parseexpected<iterator, scan_error>ではなくiteratorを直接返す
  • scan_format_string_errorを追加
  • scan_error::end_of_rangescan_error::end_of_inputにリネーム
  • ポインタの解析を追加
  • ローカライズされた数値がnumpunct::groupingで指定された正しい桁区切りをもつという要件を削除
  • ロケールとは別に、1000単位の桁区切り'専用のフラグに関する設計上の説明を削除
  • エラー処理の代替案に関する設計上の説明を削除
  • ユーザー定義型のスキャンの例を追加
  • フォーマット文字列における空白の意味をさらに明確化
  • std::expected::operator->がエラー状態のexpectedに対しては例外を送出するという例を修正
  • borrowed_tail_subrange_tを説明専用にする
  • scannable_rangeコンセプトを説明専用にする
  • scannable-rangevalue_typecharwchar_tのどちらかであるという要件を追加
  • 文書の見た目の調整

などです。

P1839R6 Accessing object representations

reinterpret_cast<char*>によるオブジェクト表現へのアクセスを未定義動作とならないようにする提案。

以前の記事を参照

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

  • htmlへ変更
  • N4988にリベース
  • オブジェクト表現を変更する際の不要な推測を削除
  • サブオブジェクトのオブジェクト表現の要素を包含オブジェクトのオブジェクト表現の要素とは異なるオブジェクトとした
  • オブジェクト表現のどのビットがindeterminateもしくはerroneousであるかを明確化
  • reinterpret_castの結果に対する曖昧さを一部取り除き、past-the-endポインタとstd::byte*へのキャストの場合の文言を追加
  • 連続していないオブジェクトのオブジェクト表現が、unsigned charのオブジェクトで構成されないようにした
  • 一部のサブオブジェクトのオブジェクト表現の修正
  • ポインタ演算の文言バグの修正

などです。

P1928R12 std::simd - Merge data-parallel types from the Parallelism TS 2

std::simd<T>をParallelism TS v2から標準ライブラリへ移す提案。

以前の記事を参照

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

  • simdおよびsimd_maskエイリアスのデフォルトサイズを修正
  • value-preservingを拡張し、全ての算術型からの変換を包含する
    • この自由度を利用して、ジェネレータコンストラクタを完全に制約し、ブロードキャストコンストラクタの仕様上の穴を埋める
  • ブロードキャストコンストラクタを修正し、constexpr-wrapper-like引数を算術型のみに制限
  • [simd.overview]と[simd.mask.overview]の注記を、[basic.fundamental]のintに関する注記で使用されている“architecture of the execution environment”という言葉を用いて修正
  • “…does not modify x”という文言中のbinary_opBinaryOperationに変更
  • [simd.reductions]のGENERALIZED_SUMに渡される入力の表記を改善
  • reduceの事前条件におけるsimdブロードキャストコンストラクタの使用修正
  • simd(mask)特殊化の有効化/無効化の意図を説明しようとしていた注記を削除
  • マスクされたreduceにおけるbinary_opidentity_elementの順序を変更
    • デフォルトのBinaryOperationとしてstd::plus<>を提供し、identity_elementにデフォルト引数を指定
  • simd_selectに“Equivalent to”という文言を使用し、simd-select-implのADLに例外を設ける
  • マスクされたreduceにおいて、二項演算が既知の5つの演算のどれでもない場合、単位元引数に対する制約を追加する
  • 説明専用のコンセプトreduction-binary-operationによるセマンティック制約を追加してreduceで使用
  • 単位元に関する事前条件を簡素化し、全ての可能なABIタグの代わりにsimd<T, 1>のみを使用する
  • simd_splitsimd_catに制約を追加し、basic_simd/basic_simd_mask型が有効化されていることを要求する
  • minmaxを“Equivalent to:”という文言を使用して指定する
  • simd_flagsを説明専用にすべきか、あるいはできるか、についての議論を追加
  • 説明専用にすべき型・変数・コンセプトはすべて、synopsisの前にある独自のセクションに移動

などです。

この提案はR15が2024年11月の全体会議で承認され、C++26に採択されています。

P2079R5 System execution context

ハードウェアの提供するコア数(スレッド数)に合わせた固定サイズのスレッドプールを提供するSchedulerの提案。

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

  • 提案の簡素化
  • 置き換え可能性(replaceabiilty)が利用可能かどうかは実装定義とする
  • 置き換え可能性サポートのためのAPIを実装定義とする
  • system_contextクラスの使用をget_system_scheduler()の直接呼び出しに置換
  • ユーザー向けAPIの更新
  • main()の外側でsystem_schedulerを使用することができるように、生存期間の保証を緩和

などです。

P2319R2 Prevent path presentation problems

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

以前の記事を参照

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

P2392R3 Pattern matching using is and as

現在のパターンマッチングをクリーンアップし、使用可能な所を広げる提案。

以前の記事を参照

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

  • P3332R0の内容を取り込み、パターンに一致するものに対して名前を付ける構文について、マッチの前ではなく後に書くようにする
    • R2での[_, _, _, x] is [a, b, c, _] =>is [a, b, c, _ x] =>と書ける
  • 2022年kona会議からのフィードバックの組み込み
    • 特に、宣言を許可するという詳細は提案の中核部分ではなく、簡単に削減/延期して残りの部分を進められることを強調
  • cppfrontによる先行実装を改善

などです。

この提案の(このリビジョンの)目的は、セクション2.1「general language support for is expressions」を前進させるための投票を促すことにあります。これは、段階的にまずis(の一般的な使用)を適用し、次にasを適用、そして最後にisasを用いたパターンマッチを適用、という順で作業を進めるためです。

これを受けてのEWGの投票では、is(及びas)をより言語で一般的に使用できるように先行させることにはコンセンサスが得られなかったようです。逆に、P2688の提案を進めることの方に若干のコンセンサスがありました。

P2434R2 Nondeterministic pointer provenance

現在のC++のポインタ意味論をポインタのprovenanceモデルに対して整合させるための提案。

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

  • P1494/N3128(C言語)との相互作用について説明
  • [basic.types.trivial]にタイトルを付加
  • “bit value”という用語を削除し、“value representation”に重点を置く
  • 未規定のポインタ比較について明確化
  • 実装の影響に関する議論を拡張

などです。

P2645R0 path_view: a design that took a wrong turn

提案中のpath_viewの問題点を指摘する提案。

path_viewstd::filesystem::pathviewstd::stringに対するstd::string_viewのようなクラス)となるクラス型です。この型は目下P1030で提案され議論中です。この提案は、その現在の設計について、今のまま標準化してしまうと過去の設計の失敗が永続化し修正不可能になるとして、問題点を指摘するものです。

具体的な問題点とは

  1. エンコーディングの問題
    • パス文字列のエンコーディングを採用し、内部表現にも使用している。これはfilesystem::pathとすら互換性が無い
    • P2319の提案では(主にwindowsの)コードページに依存する問題のあるAPIを削除しているが、path_viewは逆の事を行っている
    • これにより、std::format/std::printなどとの相互運用が困難になる
  2. 実装と使用の経験の不足
    • リファレンス実装には、提案されている新しいオーバーロードの多くが未実装
      • かつ、提案ではそれらの関数に対する文言が欠けている
  3. パフォーマンスの問題
    • path_viewを取るオーバーロードはオプトインしなければ使用可能ではないため、パフォーマンスの恩恵をそのままでは得られない
    • 遅延transcodingによって、複数回の呼び出しを行う場合にパフォーマンスへの影響が大きい
  4. フォーマットと出力の問題
    • path_viewはフォーマッタを提供していない
    • その設計によって、その実装には困難がある
      • path_viewは異なるエンコーディングによる複数の表現を使用しうるため、path_viewがどのエンコーディングで構築されたのかを知る方法がない
      • path_viewのバイナリ表現についての仕様が不十分であり、1つの実装内でもラウンドトリップが困難
  5. 複雑さ
    • 提案では、std::filesystem::pathを受け取る既存の関数に対して、path-view-likeを受け取るオーバーロードを追加するため、APIの数を単純に2倍にする
    • path_viewfilesystem::pathを構築できるものの直和的な型であり、viewではない

などを挙げており、これらの問題が解決されるまでは標準化すべきではない、としています。

P2664R8 Proposal to extend std::simd with permutation API

std::simdに、permute操作のサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、P3299R0のload_from()APIと整合させるためにgather_from()に引数を追加した(gather操作の一部として要素型変換を可能にするために)ことです。

P2688R3 Pattern Matching: match Expression

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

以前の記事を参照

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

  • マッチガード構文にかっこが必要になった
    • expr match pattern if ( condition )
    • マッチガード構文で初期化文と条件変数をサポート
  • 複数の値のマッチングをサポートしなくなった
    • この提案ではstd::tupleの機能を使用するようになったが、将来的にそれに変わる機能を追加予定
  • 含意演算子(Implication Operator)に関する注記セクションを追加
  • マッチ対象の生存期間延長に関するセクションを追加

などです。

P2719R1 Type-aware allocation and deallocation functions

型を指定する形のnew/delete演算子カスタマイズ方法の提案。

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

  • 実装経験より、ADLを実行する必要がなくなり、名前探索の仕様が簡素化された
  • 一貫性のために、クラス内定義T::operator newに対してもstd::type_identity引数を指定できるようになった
  • std::type_identityを第一引数にした
  • フィードバックと実装経験に基づき、設計上の選択と変更点を文書化

などです。

このリビジョンではnew/delete演算子宣言時のstd::type_identityの位置が2番目から先頭に変わっています

operator new(std::type_identity<T>, std::size_t, placement-args...)
operator new(std::type_identity<T>, std::size_t, std::align_val_t, placement-args...)

operator delete(std::type_identity<T>, void*)
operator delete(std::type_identity<T>, void*, std::size_t)
operator delete(std::type_identity<T>, void*, std::size_t, std::align_val_t)

これは、現在でも有効なテンプレート化されたnew/delete演算子宣言とのオーバロード解決時の混同を回避するためです。

例えば、現在でも次のようなテンプレートoperator new宣言は有効です

template <class ...Args>
void* operator new(std::size_t, Args...);

この時、2番目の引数にstd::type_identityタグを取るようにしていると、newオーバーロード解決時にこの2つがマッチしてしまいます。これを回避するために、先頭でstd::type_identityタグを取るようになりました。

また、R0ではここで提案しているnew/delete演算子がADLによって発見されることで名前空間で定義できるようになっていましたが、これは予期しない関連名前空間セットの増大やusing namespaceされた場合など、問題が多かったためここでは取り下げられています。

P2728R7 Unicode in the Library, Part 1: UTF Transcoding

標準ライブラリにユニコード文字列の相互変換サポートを追加する提案。

以前の記事を参照

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

  • null_sentinel_tのバグを修正
    • operator==boolを返すようにすることでsentinel_forの制約を満たさなくなる問題を修正
    • operator==input_iteratorを参照で受け取るようにすることで、コピー不可能なイテレータをサポートできない問題を修正
  • as_utfN()to_utfN()へ変更(変換が行われていることを表すため)
    • code unit viewとの対比を図る
  • utf_viewを、個別のto_utf8_view, to_utf16_view, to_utf32_viewクラスの実装詳細として使用される説明専用のutf-view-implクラスに修正
    • これにより、壊れていた推論補助が修正された
  • project_viewを削除し、その実装を個別のchar8_view, char16_view, char32_viewクラスにコピー
    • これにより、壊れていた推論補助が修正された
  • utf_iteratorutf-view-implクラスの説明専用メンバ型に変更
  • イテレータのunpackingメカニズムを削除し、トランスコード範囲が他のトランスコード範囲をラップする問題に対する別のソリューションに置き換え
    • ユーザーカスタマイズ性は落ちるがAPIが簡素化される
  • std::uc::formatを削除
  • 全てのコンセプトを説明専用に
  • transcoding_error_handlerカニズムを削除
  • トランスコードviewのイテレータ.success()メンバ関数から返されるtranscoding_error列挙体を使用した新しい方法に基づく新しいエラーハンドリングメカニズムの導入
  • Range Adaotir Cosureオブジェクトへポインタを渡す機能を削除
  • std::foramt()およびstd::ostreamの機能を削除

などです。

P2746R6 Deprecate and Replace Fenv Rounding Modes

浮動小数点環境の丸めモード指定関数std::fesetround()を非推奨化して置き換える提案。

以前の記事を参照

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

  • 実装に関する議論を追加
  • SG6では、この提案とP3375の関係について確認するまで文言の確定は延期された

などです。

P2769R3 get_element customization point object

tuple-likeなオブジェクトから特定のインデックスの要素を抜き出すget_elementCPOの提案。

以前の記事を参照

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

  • std::ranges::getの代替設計に関する議論を追加
  • get_elementの動作を構造化束縛と整合させる
  • tuple-likeコンセプトで値カテゴリをサポートする
  • pair-likeindex-pair-likeなメソッドをget_element
  • tuple_sizeget_element()を制約する

などです。

P2786R8 Trivial Relocatability For C++26

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

以前の記事を参照

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

  • Introductionより上の文書規約を抜粋
  • 新機能のより高度な紹介として、Basic Ideasを追加
  • この提案で定義されている様々な種類の型のサンプルをリストアップ
  • いくつかのサンプル関数の修正
  • FAQを拡張
    • relocate操作が例外を送出したらどうなるか?
    • is_trivially_replaceable型特性がないのはなぜか?
    • 非準拠の型をtrivially relocatableとしてマークするのはUB?
    • 非準拠の型をreplaceableとしてマークするのはUB?
  • この機能を適用してvectorを最適化する方法を説明

などです。

P2841R5 Concept and variable-template template-parameters

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

以前の記事を参照

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

  • CWGのレビューとEWGでの協議の結果、あるテンプレートが受けているコンセプトテンプレートパラメータは、その内に含まれているテンプレートにおいて包摂の対象外となった
  • 文言の修正

などです。

1つめの変更は、外側のテンプレートのテンプレートパラメータであるコンセプト同士の包摂関係は、そのテンプレート内部の制約された関数のオーバーロード解決において包摂判定に直接的には影響を与えなくなる、ということです。

例えば次のような場合

template <typename T>
concept A = true;

// BはAを包摂している
template <typename T>
concept B = A<T> && true;

// クラステンプレート
template <template <typename> concept Class1, template <typename> concept Class2>
struct S {
  static constexpr int g(Class1 auto) { return 1; } // # 5
  static constexpr int g(Class2 auto) { return 2; } // # 6
};

// #5と#6どちらが呼ばれる?
static_assert(S<A, B>::g(1) == 2); // ambiguous?

以前のリビジョンではこの場合にコンセプトABの包摂関係から#6が呼ばれる、としていました。このリビジョンでは、S::g()の外側のテンプレートのコンセプトテンプレートパラメータはS::g()のオーバロード解決時に包摂関係の対象外(包摂が成立しなくなる)となったため、呼び出しは曖昧になります。

これは、テンプレート引数に依存した形になるコンセプト半順序の導入を回避するためのものです。この導入はコンパイラのキャッシュ効率を低下させ、ユーザーのコンセプト半順序の理解をむずかしくする可能性があるとして、このリビジョンでは撤廃されました。

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

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

以前の記事を参照

このリビジョンでの変更は、文言へのフィードバックを適用したことです。

P2900R9 Contracts for C++

P2900R10 Contracts for C++

C++ 契約プログラミング機能の提案。

以前の記事を参照

R9での変更は

  • 契約注釈内での暗黙const化の対象を、自動記憶域期間の変数だけではなくすべての変数に拡大
    • P3261R1で提案されていたもの
  • “Design Principles”セクションに契約注釈の存在とセマンティクスを検出できるプログラムの存在を認める説明を追加
  • 文言の改善: ラムダ式が出現しうるコンテキストとして契約注釈内を追加

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

  • コルーチンでのpre(), post()サポートの追加
    • P2957R2(P3387R0?)で提案されていたもの

などです。

P2933R2 std::simd overloads for <bit> header

<bit>にあるビット演算を行う関数について、std::simd向けのオーバーロードを追加する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加した事です。

P2957R2 Contracts and coroutines

コルーチンに対して契約を有効化した場合に、各種の契約がどのように動作するのかについての提案。

以前の記事を参照

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

  • 関数引数がコルーチンフレーム内にコピーされる前に、事前条件が関数引数を参照することを必須とする
  • コルーチンの事後条件を許可することを提案するが、参照型ではない引数を使用する場合はill-formedとする
  • GCCでの参照実装について追記
  • 提案する文言を追加
  • ランプ関数の定義と意味を明確にするために、全体的に書き直し
  • 将来、パラメータキャプチャによって事後条件でコルーチンパラメータを参照できるようになる方法を提示しておく

などです。

この提案はすでにP2900(R10 or R12)にマージされているようです。

P2977R2 Build database files

ツール間で同じモジュールを生成するために必要となるコンパイルのための情報をファイルにまとめておく提案。

以前の記事を参照

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

  • C++以外の言語も指定できるように言語インジゲーターを追加
  • セット名を名前なし(null)にできるようになった
    • このようなセットは他のセットから参照できない
  • ベースライン引数を各翻訳単位ではなくセットに移動

などです。

ここでのセットとは、一連のソースファイルとそのコンパイル情報の集合の事です。依存ライブラリが全くない1つのモジュールをコンパイルする場合、そのモジュールを構成するソースファイルとコンパイル引数などからなります。

P2988R8 std::optional<T&>

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

以前の記事を参照

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

  • 文言のmandates/constraintの修正
  • T&のハッシュサポートを削除
  • 文言のレンダリングに関する注記
  • make_optional<T&>を修正

などです。

P2996R6 Reflection for C++26

P2996R7 Reflection for C++26

値ベースの静的リフレクションの提案。

以前の記事を参照

R6での変更は

  • “Emulating typeful reflection”の例を修正
  • consteval-only型のオブジェクトに対するリンケージ制約を削除
  • モジュールを扱いやすくした
    • 注入された宣言と注入された地点、および評価コンテキストを定義
    • TU-localな定義と関連定義を修正し、members_ofdefine_classの動作を明確化
    • これに関する非公式な説明を“Reachability and injected declarations”セクションに記載
  • type_oftypedef名のリフレクションを返さなくなった
    • “Handling Aliases”セクションに理由を記載
  • define_static_array, has_complete_definitionを追加
  • subobjects_ofaccessible_subobjects_ofを削除
    • P3293R1に移管
  • has_complete_definitionの観点からenumerators_ofの制約を指定
  • reflect_{value, object, function}の型テンプレートパラメータに対する制約はmandatesで表現される
  • コア言語の用語に合わせて、is_special_memberis_special_member_functionに変更
  • いくつかのメタ関数((u8)identifier_of, has_identifier, extract, data_member_spec, define_class, reflect_invoke, source_location_of)の文言を改訂
  • コア言語の文言にさらに追加と改訂を行った
  • いくつかの言葉の使い方を修正

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

  • accessible_members関数群を削除
  • get_public関数群を追加
  • 不足していたtuple/variantの特性を追加
  • 不足していたis_mutable_member関数を追加
  • (u8)operator_symbol_of関数を追加
  • std::meta::operatorsの列挙子名を追加
  • members_ofによって返されるリフレクションの順序の保証を強化
  • コア言語の文言の修正
  • is_user_providedとの完全性を保つために、is_user_declaredを追加

などです。

P2998R0 CTAD for function parameter types

関数テンプレートの呼び出し時の実引数推論において、CTADを利用した追加の候補を考慮するようにする提案。

関数テンプレートは一見、そのテンプレートパラメータの可能な置換のすべての候補からなるオーバーロードの集合の様に動作しますが、その呼び出しにおいては直接の引数と関数テンプレートの宣言に基づいて最も適切な置換を求めるためのルールに従って呼び出しが決定されており、関数テンプレートの呼び出し候補はかなり制限されています。

これにより、オーバーロードの検索範囲を狭め、意図しないテンプレートが選択されるのを防止していますが、そのルールにおいては型の変換がほとんど考慮されないことで、有用な使用例が制限されています。

#include <span>
#include <vector>

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

template <typename T>
void g(const std::vector<T>&);

template <typename T>
void h(std::span<T>);

void example() {
  std::vector<int> v;

  f(v); // OK, Tはstd::vector<int>に推論される
  g(v); // OK, Tはintに推論される
  h(v); // error: Tを推論できない
}

例えばこの例では、std::spanに関する事前知識があればh()の呼び出しは良い一致となることが分かりますが、現在のルールの下ではstd::vector<int>からstd::span<T>(のT)を推論できないため、この一致が確立されることはなく、コンパイルエラーになります。

しかし、言語の他のところでは、このような変換を考慮した推論が成功する場所があります

#include <span>
#include <vector>

std::vector<int> v;
std::span s = v; // OK, CTADによってsはstd::span<int, std::dynamic_extent>に推論

CTADのメカニズムを再利用して関数テンプレートのオーバーロード解決のルールに組み込むことによって、関数テンプレートの呼び出しにおいてもこのような推論を有効化することができ、それにより言語の一貫性も高まるとして、関数テンプレートのテンプレート引数推論においてCTADを利用した候補を考慮するようにしようとする提案です。

カニズムとしては、関数テンプレートの引数型からエイリアステンプレートを生成し、そのエイリアステンプレートの名前を型名(プレースホルダ)、関数テンプレート呼び出しの実引数を初期化子として宣言された変数の初期化を通してCTADを実行し、エイリアステンプレートのテンプレートパラメータとして推論される型を元の関数テンプレートのテンプレートパラメータとして取得することで関数テンプレートのテンプレートパラメータ推論を実行します。

例えば次のような関数テンプレート宣言と呼び出しがある時

template <typename V>
void f(std::tuple<std::string_view, V>);

void example() {
  std::pair<std::string, int> p = { "hello", 5 };
  
  f(p); // 現在は推論に失敗する
}

このf()の引数型とテンプレートパラメータをもちいてエイリアステンプレートを作成し

// 仮説のエイリアステンプレート
template <typename V>
using A = std::tuple<std::string_view, V>;

このエイリアステンプレートを用いてCTADが実行される形の変数初期化宣言を作成し

A x = p; // A deduced as A<int> (i.e. std::tuple<std::string_view, int>)

この宣言に対して行われるCTADの結果として得られるA<int>というテンプレートパラメータ推論結果から、元の関数テンプレートで対応するテンプレートパラメータをフィードバックすることで、f(p);の呼び出しからそのテンプレートパラメータはf<int>()に推論されます。

f(p); // OK, Vはintに推論される

提案文書より、std::spanの例

#include <span>
#include <vector>

template <typename T>
void f(std::span<T>);

template <typename T>
void g(const std::vector<T>&);

void example() {
  int x[] = { 1, 2, 3, 4, 5 };

  f(x); // 以前はill-formed、この提案ではTはintに推論される
  g({ 1, 2, 3, 4, 5 }); // 以前はill-formed、この提案ではTはintに推論される
}

ただし、この提案はCTADの様な推論を関数呼び出し時にその引数型(のプレースホルダ型)に対して行うことを提案するものではありません。

void f(std::pair p); // このような宣言を有効化することを提案しているわけではない

単純にCTADを利用するだけだとCTADの事情によってエラーとなるケースがあります

template <typename T>
void copy(std::span<const T> from, std::span<T> to);

void example(std::span<const int> src1, std::span<int> src2) {
  int dst[5];

  copy(src1, dst); // OK
  copy(src2, dst); // error: Tをsrc2から推論できない
}

この問題は、std::span<const T>エイリアステンプレートに対して使用される推論補助のテンプレートパラメータのconst有無が一致しないことから起きており、エイリアステンプレートをべた書きした時でも起こります

#include <span>

template <typename ElementType, std::size_t Extent = std::dynamic_extent>
using const_span = std::span<const ElementType, Extent>;

std::span<int> x;
const_span<int> y = x; // OK
const_span z = x; // error: std::span<int, std::dynamic_extent>からのconst_spanの推論補助が一致するものがない

この場合にconst_spanに対して生成される推論補助を見てみると

// span<const T>を期待
template <typename ElementType, std::size_t Extent>
  const_span(const std::span<const ElementType, Extent>&) -> std::span<const ElementType, Extent>
    requires deduces-const-span<std::span<const ElementType, Extent>>;

// 戻り値型が推論可能制約を満たさない
template <std::ranges::contiguous_range R>
  const_span(R&&) -> std::span<std::remove_reference_t<std::ranges::range_reference_t<R>>>
    requires deduces-const-span<std::span<std::remove_reference_t<std::ranges::range_reference_t<R>>>>;

deduces-const-spanconst_spanのテンプレート引数が型引数から推論可能であることを要求する制約です。1つ目の候補はxspan<int>であるのに対してspan<const int>を期待しているため失敗し、2つ目の候補はstd::span<int>const_spanの特殊化として表現できないため制約を満たさず失敗します。したがって、使用可能な推論補助が無く、CTADも失敗します。

この問題はエイリアステンプレートに対して推論補助を指定できるようにすることで解決することができるため、この提案ではこれも一緒に提案しています

template <typename ElementType, std::size_t Extent>
const_span(const std::span<ElementType, Extent>&) -> const_span<ElementType, Extent>;

void example() {
  std::span<int> x;

  const_span z = x; // ok、追加した推論補助が使用される
}

template <typename T>
void copy(const_span<T> from, std::span<T> to);

void example(std::span<const int> src1, std::span<int> src2) {
  int dst[5];

  copy(src1, dst); // OK
  copy(src2, dst); // OK
}

ただしこの提案は後方互換性に影響を与えないものではありません

template <typename T>
struct X {};

template <typename T>
struct Y
{
    Y(X<T>);
};

template <typename T>
void f(X<T>, float); // #1

template <typename T>
void f(Y<T>, int); // #2

f(X<short>(), 5); // 以前はok、#1が選択されていた。この提案後曖昧になる

このようなコードは現在、X<short>()からY<T>を推論できないため#2が選ばれることがない一方で、X<T>は推論可能であるため#1が選択され、エラーはありません。しかし、この提案の後では、X<short>()からY<T>を推論できるようになる(CTADによってコンストラクタを用いた推論補助が生成され、それによってTを推論可能になる)ため、#1も#2もマッチしてしまい、曖昧になります。

P3019R10 Vocabulary Types for Composite Class Design

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

以前の記事を参照

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

  • explicit変換コンストラクタの名称を単一引数コンストラクタに変更
  • 間接的な変換コンストラクタの名称を完全転送代入に修正

などです。

P3045R2 Quantities and units library

P3045R3 Quantities and units library

物理量と単位を扱うライブラリ機能の提案。

以前の記事を参照

R2,R3での変更はあまりに多いので省略します...

P3049R1 node-handles for lists

リストに対してノードハンドルのサポートを追加する提案。

以前の記事を参照

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

  • コンテナ間のノードハンドル互換性に関するサブセクションを書き直し
  • std::listそのものがノードハンドル型として適格ではない理由についてのサブセクションの追加

などです。

P3070R1 Formatting enums

列挙型の値を簡単にstd::formatにアダプトさせるための、format_asの提案。

以前の記事を参照

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

P3081R0 Core safety Profiles: Specification, adoptability, and impact

P3038で提案されている安全性プロファイルの、より具体的な意味論についての提案。

P3038ではC++の安全性をオプトイン方式で向上させていくための機能であるプロファイルについて提案されています。そこでは最初期のプロファイルとしていくつかのものが例示されて、その概要が述べられていました。

この提案は、P3038で例示されていたうちのいくつかのものと追加のものについて、より具体的な動作や制約についてを決定しようとするものです。ここでは特に、緊急安全性プロファイルとしてC++コードの安全性を確保するために必要なプロファイルについて焦点を当てており、型・境界・初期化・ライフタイムの安全性に対処するための4+1つのプロファイルを提案しています。

  1. type_safetyプロファイル
    • 型安全性に関するルール: キャスト、共用体、可変長引数など
  2. bounds_safetyプロファイル
    • ポインタの添え字演算の禁止や、大域的な範囲チェックなど
  3. initialization_safetyプロファイル
    • 初期化を必須にする
  4. lifetime_safetyプロファイル
    • 大域的なnull参照チェック、ライフタイムの静的な解析(所有権・ダングリング・無効化)など
  5. arithmetic_safetyプロファイル
    • 縮小変換、符号有無の差異による変換、オーバーフローなど

これらのプロファイルは有効にした場合はそのプロファイルで定義されたルールがコンパイラによって強制されます(従わないコードはコンパイルエラーになる)。プロファイルが指定されたコード内でのオプトアウトも可能ですが、オプトアウトは明示的である必要があります。例えば、[[enforce(P)]]でプロファイルPが適用される場合、[[suppress(P)]]のようにして部分的に(特定のスコープで)プロファイルを適用しないことを指定します。

各プロファイル内でのルール違反は、次の3種類の戦術によって対処されます

戦術 概要 UX 手作業でのコード変更
Fix 効率的かつ実現可能な場合、コードに意図された安全なセマンティクスを与える。
これにより、既存のコードのバグは再コンパイルするだけで修正される。
コードを再コンパイルするだけで、バグが自動的に修正される 不要
Reject Fixが不可能な場合、コンパイル時に違反を診断し可能であれば修正案を示す。 コンパイルが通れば安全 必要
Check FixもRejectも不可能な場合、実行時に違反を診断し、プログラムがその違反の処理方法をカスタマイズできるようにする コードを再コンパイルするだけで、既存のコードのバグを自動的に診断する 不要

Fix/Rejectの場合、内容によっては信頼性の高い自動ソースコード現代化を実行できる可能性があります。例えば、Cの可変長引数関数を可変長テンプレート関数に直したり、間違ったキャストの置換/削除などを挙げています(これをModernizeと呼んでいます)。

これら4つの戦略により、安全性プロファイル機能には2段階の採用レベルを設定可能になります。

  1. apply(P): Fix + Check + Modernize
    • 手作業でのコード変更を必要としないモード
    • 既存のコードはそのまま動作し、プロファイルを有効にして再コンパイルするだけで安全性が向上する
  2. enforce(P): apply(P) + Reject
    • 安全ではないコードがコンパイルエラーとなるため、コードの修正が必要になる
    • 安全性プロファイルの恩恵をフルに得られる

各プロファイルのより詳細な動作にはここでは踏み込みませんが、提案よりプロファイルの概要をまとめた表を貼っておきます

プロファイル ルール 戦術: Fix/Reject/Check
type reinterpret_cast in all cases (Type.1.1) – R –
type const_cast in all cases (Type.3) F(M) R –
type static_cast problematic cases (Type.1.2, Type.1.3, Type.1.4, Type.2) F(M) R C
type dynamic_cast redundancy and performance (Type.1.3, Type.1.4) F(M) – –
type (c_style)cast problematic cases (Type.4) F(M) R C
type functional_style(cast) problematic cases (Type.4) F(M) R C
type union in all cases (Type.7) F – C
type va_arg in all cases (Type.8) – R(M) –
bounds Pointer arithmetic in all cases (Bounds.1, Bounds.3) – R –
bounds Array-to-pointer decay in all cases (Bounds.3) – R –
bounds Subscript checking including arrays/vector/span/etc. (Bounds.4) – – C
initialization Member variables (Type.6) – R –
initialization Non-member variables (Type.5) – R –
lifetime Manual memory management in all cases (Lifetime.1) – R –
lifetime Pointer/iterator dangling static analysis (Lifetime.1) – R –
lifetime Null checking (Lifetime.1) F – C
arithmetic Narrowing/lossy conversions implicit cases ([P3038R0] §12) – R(M) –
arithmetic Signedness promotions implicit cases ([P3038R0] §12) – R(M) –
arithmetic Lossy conversions via arithmetic overflow ([P3038R0] §12) – – C

F(M)やR(M)となっているところは、Fix/Rejectに加えてModernizeが適用可能である場合がある事を表しています。

また、C++の実装に対して関連する新しい規範的な要件(normative requirements)を要求できるようにすることも提案しています。具体的には

  1. 選択された特定の信頼性が高く自動化可能なソースコードの現代化を、個別のツールやサードパーティツールではなくC++実装の一部として提供すること
  2. 全てのモードにおいて、実行時のパフォーマンス保証付きでdynamic_castサポートを要求することにより、dynamic_cast自体を改善する(あるいは、ABIを維持するために広く採用可能な代替案を提供する)

の2点です。

この提案では既存のC++コードベースにできるだけ変更を加えることなく安全性を向上させる(100%の安全性を達成するのではなく、96%程度の安全性を達成する)事に特に重点を置いています。そのために、C++実装(コンパイラのこと)にコードの自動現代化(意味的に同一なより現代的かつ安全な記述への自動置換)を要求しようとしています。

P3091R3 Better lookups for map and unordered_map

連想コンテナからキーによって要素を検索するより便利な関数の提案。

以前の記事を参照

このリビジョンでの変更は、P2988R7と重複する文言を削除、P1255で提案されているvalue_orの機能強化に関する文言を削除、などです。

P3094R4 std::basic_fixed_string

P3094R5 std::basic_fixed_string

NTTPとして使用可能なコンパイル時文字列型であるstd::basic_fixed_stringの提案。

以前の記事を参照

R4での変更は

  • 文言からstd::を削除
  • イテレータサポートと要素アクセスの章を追加
  • 説明専用コンセプトone-ofを削除
  • operator+から不要なconstを削除

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

  • 不変性を明示するように脚注を修正
  • char_traitsのサポートを削除し、関連する章を追加

などです。

P3098R0 Contracts for C++: Postcondition captures

契約プログラミング機能の事後条件構文において、値のキャプチャができるようにする提案。

P2900R8の契約プログラミング機能の事後条件からは、関数が呼び出された時点の古い値を参照することができません。これによって、コンテナのpush_back()のような関数の制約のように、その関数で変更される前の値を参照する必要のある事後条件を記述することができません。この提案は、関数の事後条件においてその関数が呼び出された時点の値をキャプチャしておいて参照することができるようにしようとするものです。

// コンテナのpush_back()の事後条件の記述の例
void push_back(T&&)
  post [old_size = size()] (size() == old_size + 1);

この例のようにキャプチャの構文はラムダ式のものを踏襲していますが、そのセマンティクスはかなり異なっており、より契約プログラミング機能に合わせたものになっています。

まず許可されるキャプチャの種類は、初期化キャプチャと明示的なコピーキャプチャのみです。デフォルトキャプチャや参照キャプチャは許可されません。

// ✅ 明示的なコピーキャプチャ
int min(int x, int y)
  post [x, y] (r: r <= x && r <= y);

// ✅ ↑と等価な初期化キャプチャ
int min(int x, int y)
  post [x=x, y=y] (r: r <= x && r <= y)

// ❌ デフォルトキャプチャ
int min(int x, int y)
  post [=] (r: r <= x && r <= y);

// ❌ 参照キャプチャ
int min(int x, int y)
  post [&x, &y] (r: r <= x && r <= y);
   
// ✅ 初期化キャプチャによる参照キャプチャ
int min(int x, int y)
  post [&x=x, &y=y] (r: r <= x && r <= y)

また、thisのキャプチャは効果が無く(メンバ関数の事後条件からはそのクラスのメンバにアクセス可能であるため)、*thisのキャプチャ(thisオブジェクト全体のコピー)は有効です。

キャプチャを行える契約注釈は事後条件(post)のみで、事前条件とアサーションにおいては許可されません。

このような事後条件におけるキャプチャのタイミングは事前条件が全て評価された後となります。

int f(int x)
  post [x] (r: r != x)  // xのキャプチャはpre()の評価の後
  pre (x > 0);

キャプチャが複数ある場合に、それが構築される順番と破棄される順番、すなわちキャプチャのライフタイムにはいくつかのオプションがあります。例えば次のようなコードにおいて

void f()
  post [a = get_a(), b = get_b()] (a == b) #1
  post [c = get_c(), d = get_d()] (c == d); #2

キャプチャの構築と破棄、述語評価のタイミングは

  1. a, b, c, dの順で構築、f()の本体を実行、#1の評価、#2の評価、d, b, c, aの順で破棄
  2. c, d, a, bの順で構築、f()の本体を実行、#1の評価、b, aの順で破棄、#2の評価、d, cの順で破棄
  3. a, b, c, dの順で構築、f()の本体を実行、#2の評価、d, cの順で破棄、#1の評価、b, aの順で破棄

この3つのオプションにはトレードオフがあります

項目 オプション1 オプション2 オプション3
事後条件の評価順序 宣言順 宣言順 逆順
キャプチャの構築順序 宣言順 宣言順ではない 宣言順
キャプチャの破棄タイミング 全ての事後条件評価後 使用直後 使用直後
Procedural interfaceへのマッピング
↑のマッピングでの事後条件の順序保持
実装上の懸念
P2900の変更 不要 不要 必要

この提案では、オプション2を推奨しています。

ラムダ式とも、契約述語の他の部分とも異なり、事後条件でキャプチャされたものは非constとなります。

void increment (Iterator& iter)
  post [iter_old = iter] (++iter_old == iter); // ✅

これは、事後条件におけるキャプチャが完全にローカルなもの(変更を外部から観測できない)であり、述語を記述する以上その変更はより明白なものになるためです。

メンバ関数の事後条件からコピーキャプチャできるのは関数引数のみです。

namespace X {
  int i = 0;
  
  int f1() [i] post(r: r > i);      // ❌ 非ローカル変数iをキャプチャできない
  int f2(int j) [j] post(r: r > j); // ✅
};

これは、ラムダ式のセマンティクスを踏襲したものであると同時に、名前のシャドウィングの影響がグローバルな変数ではより大きくなるためです。

最後に、P2900R8ではコルーチンに対する事前・事後条件の指定が許可されたものの、事後条件からその引数を使用することが出来ません。これは、コルーチンの引数はコルーチンステートにすべてムーブされるためで、事後条件からの引数の使用を許可するとムーブ後状態を読み取る可能性があるためです。

この提案のキャプチャであれば、コルーチンの事後条件から引数を使用する場合に、コピーして使用するのか参照するのかを明示的に選択できるようになります。

generator<int> sequence(int from, int to)
  pre (from <= to)
  post [from, to] (g : g.size() == to - from + 1);
  // 関数が呼び出された時の引数のコピーを参照

generator<int> sequence(int from, int to)
  pre (from <= to)
  post [&from=from, &to=to] (g : g.size() == to - from + 1);
  // 元の引数オブジェクトを参照(おそらく危険)

また、通常の関数のように事後条件から使用する関数引数をconstにする必要もなくなります。

この提案は、最初の契約プログラミング機能に間に合わせることを望んでいますが、初期の機能セット(MVP)に必須ではないともしています。

P3100R1 Undefined and erroneous behaviour are contract violations

UB(及びEB)を契約違反として扱うようにする提案。

以前の記事を参照

このリビジョンでの変更は明示的ではないですが

  • P2900R9へリベース
  • いくつかの図の追加
  • 未定義動作についてより詳細に分析・分類を行った"Defining undefined behaviour"セクションを追加
  • 参考文献リストの更新
  • Erroneous behaviourの扱いの明確化
    • Erroneous behaviourはimplicit contract violationと同一概念であるとして、EBをimplicit contract violationで完全に置き換える
    • P3232R0のstd::erroneous()contract_assert(false)の短縮形になる
  • structurally narrow implicit preconditions概念の明確化
    • 安全なフォールバックを持たないUBの持つ事前条件
    • この事前条件はignore/observeセマンティクスで評価できない
  • ライブラリの列挙型の値の調整
    • std::contracts::assertion_kind::implicitの値は明示的に初期化されていないケースを検出できるようにするために、値に意味を持たせない
    • implicit contract violation時のstd::contracts::contract_violation::detection_mode()の戻り値は未規定とする
      • 実装が任意の緩和策を採用し、それを表現するのに最適な検出モードを選択する自由を与える

などのようです。

P3125R1 constexpr pointer tagging

タグ付きポインタをサポートするためのライブラリ機能の提案。

以前の記事を参照

このリビジョンでの変更は、既存の実装に基づいて設計と提案を説明するようにしたことです。

このリビジョンでは、以前に具体的な定義が定まっていなかったstd::tagged_pointerの定義が実装経験を基にして追加されています。

template <typename Pointee, typename TagType, typename Schema>
class tagged_ptr {
    dirty_pointer _pointer{nullptr};

public:
    using schema = typename Schema::schema<Pointee, TagType>;
    using clean_pointer = schema::clean_pointer;
    using dirty_pointer = schema::dirty_pointer; 
    using tag_type = schema::tag_type;
    
    using difference_type = std::pointer_traits<clean_pointer>::difference_type;
    using element_type = std::pointer_traits<clean_pointer>::element_type;
    
    // constructors
    tagged_ptr() = default;
    constexpr tagged_ptr(nullptr_t) noexcept;
    tagged_ptr(const tagged_ptr &) = default;
    tagged_ptr(tagged_ptr &&) = default;
    
    // to construct from already tagged pointer from external facility
    constexpr tagged_ptr(std::already_tagged_t, dirty_pointer ptr) noexcept;
    
    // to store and tag pointer (only if eligible)
    constexpr tagged_ptr(clean_pointer ptr, tag_type tag);
    
    // to store and tag pointer for over-aligned pointers which wouldn't be eligible otherwise
    template <size_t Alignment> constexpr tagged_ptr(std::overaligned_pointer_t<Alignment>, clean_pointer ptr, tag_type tag);
    
    // to communicate knowledge about unused bits in pointer to make it eligible
    template <uintptr_t UnusedBits> constexpr tagged_ptr(std::known_unused_bits_t<UnusedBits>, clean_pointer ptr, tag_type tag);
    
    // destructor
    ~tagged_ptr() = default;
    
    // accessors
    constexpr dirty_pointer unsafe_dirty_pointer() const noexcept;
    constexpr clean_pointer aliasing_pointer() const noexcept; // fast-path if available otherwise .pointer()
    constexpr clean_pointer pointer() const noexcept;
    constexpr tag_type tag() const noexcept;
    
    constexpr decltype(auto) operator*() const noexcept; // *pointer()
    constexpr clean_pointer operator->() const noexcept; // *pointer()
    constexpr decltype(auto) operator[](auto... args) const noexcept; // only for multidimensional arrays
    constexpr decltype(auto) operator[](difference_type diff) const noexcept; // only for arrays
    
    // support for tuple protocol to access [pointer(), tag()]
    template <size_t I> friend constexpr decltype(auto) get(tagged_ptr _pair) noexcept; 
    
    constexpr explicit operator bool() const noexcept; // pointer() != nullptr
    
    // swap
    friend constexpr void swap(tagged_ptr & lhs, tagged_ptr & rhs) noexcept;
    
    // all the things which makes this ordinary-ish pointer
    constexpr auto & operator++() noexcept;
    constexpr auto operator++(int) noexcept;
    constexpr auto & operator--() noexcept;
    constexpr auto operator--(int) noexcept;
    constexpr auto & operator+=(difference_type diff) noexcept;
    constexpr auto & operator-=(difference_type diff) noexcept;
    friend constexpr auto operator+(tagged_ptr lhs, difference_type diff) noexcept;
    friend constexpr auto operator+(difference_type diff, tagged_ptr rhs) noexcept;
    friend constexpr auto operator-(tagged_ptr lhs, difference_type diff) noexcept;
    friend constexpr auto operator-(difference_type diff, tagged_ptr rhs) noexcept;
    
    // difference only of pointer() - pointer()
    friend constexpr ptrdiff_t operator-(tagged_ptr lhs, tagged_ptr rhs) noexcept;
    
    // comparing {pointer(), tag()} <=> {pointer(), tag()}
    friend constexpr auto operator<=>(tagged_ptr lhs, tagged_ptr rhs) noexcept;
    friend bool operator==(tagged_ptr, tagged_ptr) = default;
    
    // comparing only pointer() part
    friend constexpr auto operator<=>(tagged_ptr lhs, clean_pointer rhs) noexcept;
    friend constexpr bool operator==(tagged_ptr lhs, clean_pointer rhs) noexcept;
    friend constexpr bool operator==(tagged_ptr lhs, nullptr_t rhs) noexcept; // same as operator bool
};

3番目のテンプレート引数Schemaは、ポインタのエンコード方法とエンコードされたポインタから元のポインタ値とタグを復元する方法を指定する特性型です。それは、tagged_ptr<int, bool, alignment_low_bits_tag>のように指定して使用して、Schema型の入れ子クラステンプレートとしてSchema::schema<Pointee, Tsg>が引き出せる必要があります。

// 単純なビットマスクによってタグを利用できるようにするSchema実装の例
template <uintptr_t Mask>
struct bitmask_tag {

  template <typename Pointee, typename Tag>
  struct schema {
    using clean_pointer = Pointee *;
    using dirty_pointer = void *; // it's still pointer, but you can't dereference it
    using tag_type = Tag;
    
    static constexpr auto used_bits = Mask;

    static constexpr dirty_pointer encode_pointer_with_tag(clean_pointer ptr, tag_type value) noexcept {
      #if __has_builtin(magical_pointer_tagging_builtin)
        return static_cast<dirty_pointer>(magical_pointer_tagging_builtin(ptr, static_cast<uintptr_t>(value), Mask));
      #else
        return reinterpret_cast<dirty_pointer>((reinterpret_cast<uintptr_t>(ptr) & static_cast<uintptr_t>(Mask)) | (static_cast<uintptr_t>(value) & ~static_cast<uintptr_t>(Mask)));
      #endif
    }
    static constexpr clean_pointer recover_pointer(dirty_pointer tptr) noexcept {
      #if __has_builtin(magical_pointer_recovering_builtin)
        return static_cast<clean_pointer>(magical_pointer_recovering_builtin(tptr, ~Mask));
      #else
        return reinterpret_cast<clean_pointer>(reinterpret_cast<uintptr_t>(tptr) & ~static_cast<uintptr_t>(Mask));
      #endif
    }
    static constexpr tag_type recover_tag(dirty_pointer tptr) noexcept {
      #if __has_builtin(magical_tag_recovering_builtin)
        return static_cast<tag_type>(magical_tag_recovering_builtin(tptr, Mask));
      #else
        return static_cast<tag_type>(reinterpret_cast<uintptr_t>(tptr) & static_cast<uintptr_t>(Mask));
      #endif
    }
  };
};

Schema型の提供すべき機能はencode_pointer_with_tag(), recover_pointer(), recover_tag()およびオプションのrecover_aliasing_pointer()の3+1つの関数です。これらの関数によってタグの埋め込みと分離を実装します。

P3138R3 views::cache_latest

入力範囲の現在の要素をキャッシュするRangeアダプタ、views::cache_latestの提案。

以前の記事を参照

このリビジョンでの変更は、名前をcache_lastからcache_latestに変更したことです。

P3149R6 async_scope -- Creating scopes for non-sequential concurrency

P2300のExecutorライブラリについて、並列数が実行時に決まる場合の並行処理のハンドリングを安全に行うための機能を提供する提案。

以前の記事を参照

このリビジョンでの変更は、R4で報告された問題に対処したことです。

問題とは、spawn()(及びspawn_future())を実装するための基本操作としてnest()を使用することで発生しうる、spawn()に提供されるアロケータへの生存期間外のアクセスを防止するために、生成された作業を追跡するcounting_scopeが使用されていることで、ネストした(spawn()によって開始された子操作の)senderの破棄に伴うメモリ解放と、他の待機中senderによるアロケータ破棄が競合する可能性がある、というものです。

R5ではこの問題の説明といくつかのソリューションが提示されており、このリビジョンではそのうちのオプション4を選択しました。それは、counting_scopeに代わる新しい参照カウント基本演算を定義し、それらに基づいてnest(), spawn(), spawn_future()を再定義する、というものです。

以前のasync_scope_tokenの定義

template <class Token, class Sender>
concept async_scope_token =
    copyable<Token> &&
    is_nothrow_move_constructible_v<Token> &&
    is_nothrow_move_assignable_v<Token> &&
    is_nothrow_copy_constructible_v<Token> &&
    is_nothrow_copy_assignable_v<Token> &&
    sender<Sender> &&
    requires(Token token, Sender&& snd) {
      { token.nest(std::forward<Sender>(snd)) } -> sender;
    };

に対して、このリビジョンでの変更は、トークンのnest()execution::nest()に移動され、このexecution::nest()の観点から再定義され、次のようになります

template <class Assoc>
concept async_scope_association =
    semiregular<Assoc> &&
    requires(const Assoc& assoc) {
        { static_cast<bool>(assoc) } noexcept;
    };

template <class Token>
concept async_scope_token =
    copyable<Token> &&
    requires(Token token) {
        { token.try_associate() } -> async_scope_association;
    };

execution::nestトークンとネストするsenderを渡すことでスコープとsenderを関連付け、トークンの.try_associate()によってその関連付け状態を表すasync_scope_associationオブジェクトを取得します。async_scope_associationオブジェクトはbool変換によってその関連付けの状態を取得することができて、trueを返す場合に“engaged”すなわち関連付けが有効であることを表します。

このasync_scope_associationオブジェクトから観測することのできる関連付け状態をチェックし、またasync_scope_associationオブジェクトが保持する関連付け状態がネスト操作のリソース解放が完全に完了してから“disengaged”(false)となることによって、親側でのリソース解放の競合を防止します。

nest(), spawn(), spawn_future()はこの新しいasync_scope_tokenを用いて定義され、表現されるようになります。

P3152R0 Add missing constructors and assignment for indirect and polymorphic

P3019で提案されているindirectpolymorphicに欠けているコンストラクタと代入演算子を追加する提案。

indirect<T>polymorphic<T>はヒープ領域上に構築されたオブジェクトをその領域も含めて値のセマンティクスの下で扱うためのクラス型です。この提案では、それらの型に対する追加のコンストラクタと代入演算子を提案しています。

  • コンストラク
    • 単一引数コンストラク
      • std::optionalstd::variantが持つような、TそのものかTに変換可能な型を受け取る単一引数コンストラクタを追加する
      • explicitとし、アロケータ対応のものも追加する
    • Initializer-listを取るコンストラク
      • こちらもstd::optionalstd::variantと同様に、std::initializer_listを取るコンストラクタを追加する
      • explicitとし、アロケータ対応のものも追加する
  • indirectにのみ、完全転送代入演算子を追加する
    • 単一引数コンストラクタの代入演算子版、受け取った一つの値を完全転送して内部の値に代入する
    • polymorphicは型消去を行っていることから、対応する代入演算子の存在が静的に分からないので追加しない

特に、2つ目の代入演算子についてはindirectの中身がムーブされた可能性がある場合の後で、中身が無い状態になったindirectオブジェクトに値を代入するコードを場合分けを必要とせずに書くことができるようになるメリットがあります。

/// このようなコードを
indirect<int> i;
foo(i);  // iからムーブされうる

// *iが安全かどうかに応じて分岐する
if (!i.valueless_after_move()) {
  *i = 5;
} else {
  i = indirect(5);
}

/// こう書ける
indirect<int> i;
foo(i); // iからムーブされうる

i = 5;  // iの状態によらず新しい値を代入する

P3019とは個別にレビューできるようにするために、別の提案にしているようです。

P3160R2 An allocator-aware inplace_vector

提案中のinplace_vectorにアロケータサポートを追加する提案。

以前の記事を参照

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

  • SG14からのフィードバックにより、std::allocatorをフリースタンディングに指定
    • これにより、inplace_vector<T, allocator<T>>もフリースタンディングになる
  • P0843R14がWDに採択されたことに伴って、最新のWDにリベース
  • motivation, exploratory descriptions, alternative designs等のセクションの大部分を削除

などです。

R2も引き続いてLEWGでの明確な指示を得られていませんが、SG14では一定の支持があるようです。

P3179R3 C++ parallel range algorithms

RangeアルゴリズムExecutionPolicyに対応させる提案。

以前の記事を参照

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

  • iteratorを出力用として使用する
  • 文言の追加

などです。

P3227R0 Contracts for C++: Fixing the contract violation handling API

契約違反ハンドラが呼び出されている状況をより詳細に取得するためのAPIを追加する提案。

無視あるいは即時終了以外のセマンティクスによって契約注釈が評価され、契約違反が起きた時、あるいは条件式が例外を送出したとき、契約違反ハンドラが呼び出されます。違反ハンドラにはcontract_violation型のオブジェクトが渡され、ここから違反に関する情報を得ることができます。違反ハンドラはユーザが置き換えることもできて、契約違反に伴う処理をカスタマイズすることができます。

ユーザ定義違反ハンドラの場合、違反ハンドラの呼び出され方やその状況に応じて、次の2つの事を判定したい場合があります

  1. 違反ハンドラの呼び出しが正常にリターンした場合、契約違反処理メカニズムはプログラムを終了しようとするかどうか
    • もしプログラムが終了されようとしていてそれが望ましくない場合、例外を送出することによって違反処理メカニズムから脱出することができる
  2. 違反ハンドラはなぜ呼び出されたのか?(条件がfalseになったのか、条件式が例外を送出したのか?
    • 例外が送出されていた場合、その例外を取得して処理/再送出する必要がある

現在のMVP仕様(P2900R8)ではこのようなクエリが可能なようにすることを意図されているものの、現在のAPIでは実際には可能になっていません。この提案は、違反ハンドラの引数であるcontract_violation型のオブジェクトを通してこれらの事をクエリできるようにすることを提案するものです。

終了セマンティクスの検出

現在でも、semantic()メンバ関数から違反ハンドラの呼び出された元の契約注釈のセマンティクスを取得する事はでき、これはevaluation_semantic列挙型の値を返します

enum class evaluation_semantic : unspecified {
  enforce = 1,
  observe = 2,
  // additional implementation-defined enumerators
};

標準の4つのセマンティクスを全て網羅していないのに加えて、仕様では契約注釈のセマンティクスとして実装定義のものを許可しています。したがって、この列挙型の値によって違反ハンドラリターン後のプログラムが終了するかどうかを判断できるのは、実装が追加のセマンティクスを提供していない場合に限ります。そして、実装が追加のセマンティクスを提供する場合、そのセマンティクスが違反ハンドラのリターン後にプログラムを終了するのかどうかを取得する移植可能な方法はありません。

void handle_contract_violation(const& contract_violation) {
  // 実装定義のセマンティクスで呼ばれている場合、このチェックだけでは不十分
  if (violation.semantic() == evaluation_semantic::enforce) {
    throw DoNotTerminate();
  }

  // ここに来ているときでもこの後でプログラムを終了する可能性がある
}

実装定義の契約違反セマンティクスは必ずしもコンパイラの拡張だけで提供されるものではなく、MVP仕様は各種サニタイザーをはじめとする外部のツールが違反ハンドラを利用することを織り込んでおり、この場合違反ハンドラの呼び出しセマンティクスはリンク時に決定されます。

したがって、semantic()から得られる列挙値だけから違反ハンドラ後のプログラムの状態を知ることはできないため、この提案ではそれを専用にクエリするbool値関数is_terminating()を提案しています。

// 使用例
void handle_contract_violation(const& contract_violation violation) {
  if (violation.is_terminating()) {
    // プログラム終了を回避する
    throw DoNotTerminate();
  }

  // ここに来ている場合、このままリターンしてもプログラムは継続される
}

この関数は、現在の契約評価セマンティクスが違反ハンドラがリターンした後にプログラムを終了する場合にtrueを返す関数です(このようなセマンティクスをterminating semanticと呼んでいる)。標準の中だとenforceセマンティクスが該当するほか、サニタイザーが違反ハンドラを利用する場合はこの関数がtrueを返すようなcontract_violationオブジェクトを渡す必要があります。

条件式からの例外の処理

現在のMVP仕様では、違反ハンドラの呼び出しが契約条件式の評価時に送出された例外によるものかどうかを判断し、その例外を処理するために次のような呪文コードを記述する必要があります

void handle_contract_violation (contract_violation& violation) {
  if (violation.detection_mode() == detection_mode::evaluation_exception) {
    my::handle(std::current_exception());
  }
}

名前空間を省略しないようにするとさらに長くなります。

std::current_exception()を無条件で呼び出すことでこれを効率的に実行できると考えるかもしれませんが、契約条件の評価はcatch節でも行われる可能性があるため(特にcontract_assert())、無関係の例外オブジェクトを取得してしまう可能性があります。

この現在のAPIはユーザーエクスペリエンスが著しく低いため、違反ハンドラ内でその契約条件の評価に関する例外オブジェクトだけを取得するための専用のAPIevaluation_exception()を追加することを提案しています。この関数は、現在の違反ハンドラの呼び出しのきっかけとなった例外送出に関する例外オブジェクトがある場合はそのexception_ptrを返し、ない場合は空のexception_ptrを返します。

void handle_contract_violation (contract_violation& violation) {
  // 現在の契約の評価に関する例外があればそれを取得する
  if (auto ex = violation.evaluation_exception()) {
    my::handle(ex);
  }
}

その他の提案

現在のMVP仕様では、違反後にプログラムを終了するセマンティクス(enforce/quick_enforce)のことをenforcing semanticsと呼んでいますが、これをterminating semanticに変更することを提案しています。enforcing semanticsの意味は、検出された契約違反を超えてプログラムを継続することを許可しない、の意味とされていますが、これは必ずしも、契約違反時にプログラムを終了しようとする、と同義ではないためです(例えば、例外送出や無限ループへの移行によってもプログラムの継続を止めることができ、これはenforcingだがterminatingではないため)。この提案の1つめのis_terminating()は(enforcingではなく)terminatingを検出することを意図しています。

is_terminating()の追加によってsemantic()メンバおよびその戻り値を違反ハンドラの呼び出し状態の判定のために利用することには完全に意味がなくなり、evaluation_exception()の追加によってdetection_mode()メンバおよびその戻り値を違反ハンドラの呼び出し状態の判定のために利用することには同じく意味がなくなりました。

このうち特に、evaluation_semantic列挙型の値は、ログの出力や契約注釈に対するラベルで利用されることが目論まれているためここでは削除を提案していません。しかし、この列挙値を違反ハンドラの呼び出し状態の判定に使用するのはもはや間違っているため、その目的で使用しないことを明記しておくことを提案しています(同様に、他の場所で使用とログ出力用途のためにdetection_mode列挙型も削除しようとしていません)。

また、evaluation_semantic列挙型の値を契約注釈に対するラベルで使いやすくするために、4つの標準セマンティクスに対する値を追加しておくことも提案しています。

P3237R1 Matrix Representation of Contract Semantics

契約注釈のセマンティクスについて、特性の行列(表)によってセマンティクスを指定しようとする提案。

以前の記事を参照

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

  • プロパティ(契約セマンティクスを表す行列の列の表現)をビットフィールドではなくビットマスクで表現
  • 一部のセクションの更新
  • Compiler Explorerコード例を追加
  • セマンティクスのプロパティは常に単一のbool値で表現されるようになった
  • SG21セクションに関する質問を追加
  • 失敗した先行技術に関するQ&A段落を追加

などです。

この提案はSG21において指示を得られずリジェクトされています。

P3261R0 Revisiting const-ification in Contract Assertions

P3261R1 Revisiting const-ification in Contract Assertions

契約注釈中での式の暗黙const化について、代替の設計を探る提案。

P2900R8のContarcs MVP仕様では、契約注釈の中から外の変数を使用する場合に暗黙にconst化して使用されます。より正確には

  1. 構造化束縛を含む、自動記憶域期間をもつ変数の名前を指定するid式は、暗黙的にconstとして扱われる
  2. *thisは暗黙的にconstとして扱われる

のようになります。

これは、契約注釈の存在およびその評価はプログラムの他の部分の正しさに影響を与えない、という設計原則によって、契約述語の評価が破壊的(つまり、契約述語の評価そのものがプログラムの状態を壊してしまうこと)にならないようにするために、契約述語の評価時に引き起こされる副作用を削減するための措置の一つです。

ただし、これはC++言語の他の部分とは明確に異なるデフォルト動作であるため、この設計については懸念の声が上げられています。

この提案は、これらの懸念を払拭すべく、const化の対象や方法をより細分化して導入することを検討し、その代替案を提案するものです。

1. const化の対象となる式に関する提案

  1. const化しない(警告のみ)
  2. 代入・インクリメント・デクリメントのオペランドconst
  3. const化の対象となるオペランドを変更する可能性のある操作を禁止
  4. 同等のconst操作が無い場合の操作について禁止
  5. 常にconst操作を選択する
    • const対象の式は常にcosntとして扱われ、constオーバーロードが選択される
    • これは、P2900R8の現状

2. const化を適用するエンティティ

  1. 最小限のconst
    • 次のものをconst
      • 自動記憶域期間をもつ変数の名前を指定するid式
      • 明示的または暗黙的に使用されるthisおよび*this
      • 対応する変数がconst化される場合の構造化束縛
      • 対応する変数がconst化される場合のかっこに囲まれた式
    • これは、P2900R8の現状
  2. ブロックスコープの非自動記憶域期間の変数
    • 1に加えて、静的/スレッドローカル記憶域期間を持つ同じブロックスコープの変数の名前を指定するid式、にもconst化を適用
  3. グローバルスコープの非自動記憶域期間の変数
    • 2に加えて、静的/スレッドローカル記憶域期間を持つ名前空間/クラススコープの変数の名前を指定するid式、にもconst化を適用
    • 変数を表す全てのid式がconst化される

3. 深いconstを適用するか否か

ただし、ユーザー定義の深いconstを提供する手段が無いため、ここの項目はユーザー定義のポインタ型に対しては完全に適用されません。

  1. 参照メンバと可変メンバ
    • 左側のオペランドconst化対象の式であるメンバアクセス式にconst化を適用
  2. ポインタの間接参照

提案ではこれらの選択肢について個別に利点や欠点、既に報告されている問題点のソリューションとしての満足度などを分析しています。また、このconst化を無効化するオプトイン構文についても検討しています。

  • unconst演算子の追加
  • const化を抑制する契約注釈のラベル
    • no_constificationなど
  • ↑に加えて、const化を明示的に有効化するラベルも追加する
    • no_constification/constification
    • ラベルが無ければconst化を適用しないようにする

この提案の結論としては、P3071R1で導入されたP2900R8の現状(1-5)が過度な誤検出なしに問題のあるケースを弾くことのできる唯一のオプションだと評価しています。そのうえで、次のものは導入を検討する価値があるとしています

  • 提案1-1: const化しない(警告のみ)
  • 提案1-5 + 提案2-1
    • つまり現状維持
  • 提案1-5 + 提案2-3
    • 全変数const

また、const化の方向性について合意が高まり懸念が克服できれば、const化を無効化するオプトイン構文を導入するのが適切であるとも推奨しています。

P3271R1 Function Types with Usage (Contracts for Function Pointers)

契約注釈を指定可能な新しい関数型の提案。

以前の記事を参照

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

  • 仮想関数に関する補足を削除(意味をなさなくなったため
  • 用語の改善
    • “function usage type”を“function type with usage”に変更
  • R0で提案されていた代替構文であるキーワードベース構文へ以降
    • 以前は文脈依存キーワード
  • “similar”の定義を関数型にまで拡張する戦略を採用
  • 関連する2つの機能“declarations of appropriate usage”に関する考察を追加
    • この提案の機能に基づくライブラリ機能に関する考察を追加

この提案では利用法付き関数型(function type with usage)の宣言が次のようになります

function_usage fancy_op:( const int x )->( int r )
  pre( x >= 0 )
  post( r >= 0 )
  post( r <= x );

function_usageは新しいキーワードであり、以前は関数型の後ろにusage文脈依存キーワードを使用していました。

P3287R1 Exploration of namespaces for std::simd

std::simdに関するAPIを標準ライブラリ中にどのように配置するのかについてを探る提案。

以前の記事を参照

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

  • stdusing宣言を追加する新しい代替案8を追加
  • P3299で提案されているように、copy_fromload_fromへ変更
  • 命名に関する議論を独立したセクションへ移動
  • サブ名前空間に関する根拠を追記

以前の記事のリスト番号とこのリビジョンでの代替案番号は一つずれています(現状のstd::simdの構造が代替案0になる形)。

  1. 全ての関数をsimdプレフィックス付きの非メンバ関数にする
  2. 全ての関数をsimdプレフィックスなしの非メンバ関数にする
  3. 型以外の全てを名前空間に入れる
  4. 全ての非メンバ関数を隠蔽フレンドにする
  5. 全てを単一の名前空間に入れる
  6. 明らかなオーバーロード以外の全てを単一のネームスペースに入れる
  7. simdを単一のネームスペースに、SIMDジェネリックインターフェースは別のネームスペースに配置する
  8. 全てを単一の名前空間に入れて、using宣言をstd直下に置く
    • 利点
      • 新しい名前空間から自由に名前を取得できる
      • ADLが機能する
      • 一貫性があるため、ユーザーは「std::simd名前空間にあるものはsimdで動作する。simd用の関数を探すときは、std::simd名前空間を探す」ということを学ぶだけでよい
    • SIMDジェネリックプログラミングが機能する
    • 欠点
      • クラステンプレート名std::simd::simdが少しぎこちない(代わりの名前を考えることもできる

新しい代替案8は代替案5と6を組み合わせたものです。筆者の方は7と8が気に入っているようです。

P3293R1 Splicing a base class subobject

リフレクション機能において、基底クラスのサブオブジェクトへアクセスする簡単な方法を提供する提案。

以前の記事を参照

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

  • &[:base:]は仮想基底クラスに対して機能しないことを明確化
  • 配列についての言及
  • 文言の追加

などです。

この提案はEWG/LEWGのレビューをパスして、CWG/LWGでレビュー中です。

P3294R2 Code Injection with Token Sequences

トークンシーケンスを用いてコード注入によるコンパイル時コード生成機能の提案。

以前の記事を参照

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

  • 提案の整理
  • fragmentsに関する理解の修正
  • LoggingVectorの実装を修正し、デモンストレーション
  • fragmentsとToken Sequencesの違いに関するセクションを追加

などです。

P3295R2 Freestanding constexpr containers and constexpr exception types

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

以前の記事を参照

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

  • P3160とP3418のアロケータとの整合性の向上
  • P3421のconstevalデストラクタについて言及

などです。

P3296R2 let_async_scope

提案中のcounting_scopeの問題を修正する提案。

以前の記事を参照

このリビジョンでの変更は明確ではないですが

  • エラー伝播に関する明確化
  • それらに合わせて文言の更新

などのようです。

エラー処理に関しては、let_async_scopeに渡された関数が例外を送出した場合、または非同期スコープに関連付けられたいずれかのsenderがエラーで完了(set_error()チャネルへ出力)した場合、その例外またはエラー結果がlet_async_scopeの返すsenderの完了としてそのまま使用されます。これはlet_async_scopeに渡した関数が正常に終了した場合も同様で、この場合は戻り値が破棄されエラーのリターンが優先されます。

スコープに関連付けられているsenderから複数のエラーが発生した場合、そのうちの一つが選択されて使用されます(ただしどのように選択されるかは未規定)。したがって、スコープに関連付けられた全てのsenderは同じエラーシグネチャを持つ必要があります。

P3298R1 Implicit user-defined conversion functions as operator.()

ユーザー定義の変換関数としてのoperator.()の提案。

以前の記事を参照

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

  • 文脈依存implicitキーワードのパースに関する説明を改善
  • 名前探索とオーバロード解決ルールへの追加事項の説明を書き換え
  • 列挙型を返す場合についての新たな説明を追加
  • constimplicit変換関数とconst戻り値型に関する新たな説明を追加
  • implicit変換関数の戻り値型でもあるクラスを継承できないという新たな注記を追加
  • universal_refrelocの相乗効果に関する 注記を追加
  • N4035への参照をP3398に更新

などです。

P3299R2 Range constructors for std::simd

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

以前の記事を参照

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

  • St Louis '24 meetingのアンケートを追記
  • 文言を追加
  • std::ranges_toに関する注記を追加
  • 多数の注記・例・説明を追加

などです。

P3310R3 Solving partial ordering issues introduced by P0522R0

P3310R4 Solving issues introduced by P0522R0

P0552R0の影響を緩和するための提案。

以前の記事を参照

R3での変更は

  • 問題1
    • 半順序にのみ適用されるように、変更の効果を制限

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

などです。

P3324R0 Attributes for namespace aliases, template parameters, and lambda captures

現在属性の指定が許可されていないものに対して、属性の指定を可能にする提案。

C++の構文上、ほぼすべての名前やエンティティの宣言には属性を指定することができるようになっており、新機能が追加される際も構文は属性の指定を考慮して決定されます。しかし、一部例外があります。

  • 名前空間エイリアス
  • 型テンプレートパラメータ
  • テンプレートテンプレートパラメータ
  • ラムダキャプチャ

これらのものには現在属性を指定することができません。しかし、規格文書や関連する提案、CWG/LWG Issueなどを調査しても、これらのものが属性を指定できないようになっている理由は明確ではなく、単に忘れられているだけだと推測されます。

この提案は、これらのものにも属性を指定できるように構文スペースを確保することを提案するものです。提案ではそれぞれ、次のように関連する文法を調整しています

  • 名前空間エイリアス
    • attribute-specifier-seqnamespaceの直後に出現することを許可
  • 型テンプレートパラメータとテンプレートテンプレートパラメータ
    • attribute-specifier-seqがパラメータ宣言の先頭で出現することを許可
      • NTTPおよび関数引数の宣言と同じ
    • attribute-specifier-seqがパラメータ宣言内の識別子名の後に出現することを許可
      • NTTPおよび関数引数の宣言と同じ
  • ラムダキャプチャ
    • attribute-specifier-seqが明示的キャプチャ宣言内の識別子名の後に出現することを許可
      • ただし、キャプチャ宣言の先頭には出現できない
        • attribute-specifier-seqの一般的な規則に則っている
    • attribute-specifier-seqが明示的キャプチャ宣言内のthis,*thisの後に出現することを許可
      • ただし、キャプチャ宣言の先頭には出現できない
    • attribute-specifier-seqがデフォルトキャプチャ宣言内の&,=の後に出現することを許可
      • ただし、キャプチャ宣言の先頭には出現できない

また同時に、一部の属性についてこれらのもので使用可能であることを明確にします

  • [[deprecated]]: 名前空間エイリアスに指定したときに、効果をもつようにする
  • [[maybe_unused]]: ラムダキャプチャに指定したときに、対応する非静的データメンバに対して適用されるようにする
  • [[maybe_unused]]: NTTPに加えて型テンプレートパラメータとテンプレートテンプレートパラメータに対しても適用されるようにする
  • [[no_unique_address]]: 型テンプレートパラメータとテンプレートテンプレートパラメータに指定したときに、効果を持つようにする

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

// 名前空間エイリアス
namespace ns {}
namespace deprecated nsa1 = ns; // ✅ 以前はng
namespace nsa2 deprecated = ns; // ❌ この場所の属性の配置は許可されない

// 型テンプレートパラメータとテンプレートテンプレートパラメータ
template<maybe_unused int nttp> // ✅ 現在でも
void ft1();
template<int nttp maybe_unused> // ✅ 現在でも
void ft2();
template<maybe_unused typename ttp> // ✅ 以前はng
void ft3();
template<typename ttp maybe_unused> // ✅ 以前はng
void ft4();
template<maybe_unused template<typename> class tttp> // ✅ 以前はng
void ft5();
template<template<typename> class tttp maybe_unused> // ✅ 以前はng
void ft6();

// ラムダキャプチャ
struct S {
  void mf(int p) {
    [ maybe_unused = ]{}();     // ❌ この場所の属性の配置は許可されない
    [ = maybe_unused ]{}();     // ✅ ただし無意味なmaybe_unusedの指定、以前はng
    [ maybe_unused & ]{}();     // ❌ この場所の属性の配置は許可されない
    [ & maybe_unused ]{}();     // ✅ ただし無意味なmaybe_unusedの指定、以前はng
    [ maybe_unused p ]{}();     // ❌ この場所の属性の配置は許可されない
    [ p maybe_unused ]{}();     // ✅ 以前はng
    [ maybe_unused this ]{}();  // ❌ この場所の属性の配置は許可されない
    [ this maybe_unused ]{}();  // ✅ 以前はng
    [ maybe_unused *this ]{}(); // ❌ この場所の属性の配置は許可されない
    [ *this maybe_unused ]{}(); // ✅ 以前はng

    [ no_unique_address = ]{}();     // ❌ この場所の属性の配置は許可されない
    [ = no_unique_address ]{}();     // ✅ 以前はng
    [ no_unique_address & ]{}();     // ❌ この場所の属性の配置は許可されない
    [ & no_unique_address ]{}();     // ✅ 以前はng
    [ no_unique_address p ]{}();     // ❌ この場所の属性の配置は許可されない
    [ p no_unique_address ]{}();     // ✅ 以前はng
    [ no_unique_address this ]{}();  // ❌ この場所の属性の配置は許可されない
    [ this no_unique_address ]{}();  // ✅ 以前はng
    [ no_unique_address *this ]{}(); // ❌ この場所の属性の配置は許可されない
    [ *this no_unique_address ]{}(); // ✅ 以前はng
  }
};

ラムダキャプチャの場合、[[maybe_unused]][[no_unique_address]]の指定によってそのクロージャ型のデータレイアウトを調整するために使用できる可能性があり、テンプレートパラメータについては使用していないことを明示的にするために[[maybe_unused]]属性を使用することは有効である可能性があります。どちらも現在はできませんが、この提案によって可能になります。

P3327R0 Contract assertions on function pointers

関数ポインタに対する契約注釈の設計について検討する文書。

現在のContracts MVP提案(P2900R9)では、契約注釈がなされた関数のアドレスを関数ポインタに入れて呼び出した場合でもその契約条件はチェックされることを要求しています。しかし一方で、関数ポインタに対する契約注釈を行うことができません。関数ポインタがC++において広く使用されていることを鑑みると、関数ポインタに対する契約注釈は有用である可能性があります。

関数ポインタに対する契約注釈はコンパイラベンダからも契約プログラミング機能の一機能としてかなり重要視されていますが、現在のところこれを提案する文書は提出されておらず、その設計についての議論もあまり行われていません。

この提案は関数ポインタに対する契約注釈について、そのユースケースを提示するとともに、その設計について検討するものです。

この文章では、可能な構文として3種類のものを挙げています

1. 契約注釈を宣言に付加する

関数とラムダ式の宣言に使用されている契約注釈の文法(function-contract-specifier-seq)を再利用して、関数ポインタの宣言に対して付加します。

// (メンバ)関数ポインタ宣言への契約注釈
int (*nonnegative_fptr)(int i) pre (i >= 0); // OK
int (*X::nonnegative_memfptr)(int i) pre (i >= 0); // OK

// typedef/using宣言への契約注釈
typedef int (*nonnegative_fptr_t)(int i) pre (i >= 0); // OK
using nonnegative_fptr_t = int(*)(int i) pre (i >= 0); // OK

2. 契約注釈を型に付加する

P2900R9の文法は関数ポインタ型に対して有効にすることができません(prepostが関数名と曖昧になるため)。一方で、typedef/usingでは有効化することができます。

// preが関数名としてもパースできる
int (*ftpr)(int i) pre (i >= 0) get_nonnegative_fptr(); // Error

// typedef宣言ならok
typedef int (*nonnegative_fptr_t)(int i) pre (i >= 0);

nonnegative_fptr_t get_nonnegative_fptr(); // OK

関数ポインタ型を使用可能な他のコンテキストでも同様であり、代わりにtypedef/usingを使用するようにすることで関数ポインタ型への契約注釈を疑似的に達成できます。

3. 契約注釈を式に付加する

契約注釈を、(メンバ)関数ポインタとして評価される式に対して付加するアイデアを考えることもできます。

int (*)(int) get_fptr();

void test(int i) {
  get_fptr() (i);
//^^^^^^^^^^
//この式に対して契約注釈を付加する
}

ただし、P2900の構文はこれをサポートしておらず、検討された他の構文でもこれは不可能です。比較的自由度の高めな属性構文でさえも式に対しては適用できないため、これは実現不可能だと思われます。

これらの2つ(+1)の候補に対して、どのように動作すべきかのセマンティクスを指定する必要があります。この文章では

  1. コンパイル時のセマンティクス
    • どの組み合わせがwell-formedか?
  2. 実行時のセマンティクス
    • ポインタを介した呼び出し時にどの契約注釈をチェックすべきか?

の2つの部分に大別して、いくつかのパターンに分けて検討しています。

  1. 関数ポインタと関数の契約注釈が同じ場合
  2. 関数ポインタが関数の契約注釈を無視する場合(注釈無しポインタに注釈あり関数アドレスを代入した場合)
  3. 関数ポインタが契約注釈を追加する場合(注釈ありポインタに注釈なし関数アドレスを代入した場合)
  4. 関数の契約と関数ポインタの契約が異なる場合(どちらにも契約があるが、一致しない場合)
  5. 中間契約(関数ポインタを別の関数ポインタへ代入する場合
  6. 契約の伝播
  7. テンプレートと型推論

1. 関数ポインタと関数の契約注釈が同じ場合

int f(int i) pre (i >= 0);

int (*fptr)(int i) pre (i >= 0);

void test(int i) {
  fptr = f;
  fptr(i);
}

このようなケースにおける通常の期待は、このコードがエラーにならずfptrを介してfを呼び出した場合でもその契約条件がチェックされることでしょう。ただしこの場合には、関数と関数ポインタの契約注釈が正確に一致することを要求すべきかが問題となります。これは以前のContractsの提案でも検討された事がありますが、最終的には除外されました。

一致を定義しチェックできたとしても、それを厳密に要求するほど関数ポインタに格納可能な関数の潜在的な集合は小さくなります。同じことを行うものの契約が異なる関数は存在し得ますが(内部アルゴリズムの違いなど)、そのような関数群は同じ関数ポインタに格納できなくなります。

もう一つの懸念は、2つのエンティティが同じ契約注釈を持つかどうかによってSFINAE出来ない事を確認することです。これは、P2900の仕様が要求している契約注釈の存在は他のコンパイル時機能から認知できない、という原則に反するためです。

2. 関数ポインタが関数の契約注釈を無視する場合(注釈無しポインタに注釈あり関数アドレスを代入した場合)

int f(int i) pre (i >= 0);

int (*fptr)(int i);

void test(int i) {
  fptr = f;
  fptr(i);
}

このような代入を許可しない場合、ユースケースの一部を満たすことはできますが、P2900の仕様に対する破壊的変更となります。

許可する場合、fptrを介したfの呼び出しがfの事前条件を評価するかどうかが問題となります。この場合、P2900の現在の動作とユースケースから、評価するようにすることが唯一の選択と思われます。すなわち、関数の契約注釈はそのポインタを通して呼ばれるかどうかに関係なく、常に呼び出しに伴って評価されます。

3. 関数ポインタが契約注釈を追加する場合(注釈ありポインタに注釈なし関数アドレスを代入した場合)

int f(int i);
int (*fptr)(int i) pre (i >= 0);

void test(int i) {
  fptr = f;
  fptr(i);
}

この場合、このような関数ポインタへの契約注釈を許可するとして、このような代入を許可するかどうか、とこのfptrを介した呼び出しで関数ポインタに指定された契約注釈を評価するかどうか、を決定する必要があります。

このような代入を許可する場合、fptrを介した呼び出しで契約注釈を評価することが合理的な答えになります。それを行わないなら、関数ポインタへの契約注釈を行う意味が無いためです。

この方向性の意味するところの一つは、関数ポインタの契約注釈は必然的にそのポインタ固有のものとなるため、何らかの方法でポインタと一緒に格納する必要があるということです。型、値、あるいは宣言の一部としてなど、その選択にはトレードオフと制限があります。

4. 関数の契約と関数ポインタの契約が異なる場合(どちらにも契約があるが、一致しない場合)

int f(int i)
  pre (i % 2 == 0)      // must call with even integer
  post (r: r % 2 == 0); // guaranteed to return even integer

int (*fptr)(int i)
  pre (i > 0)
  post (r: r != 0);

void test(int i) {
  fptr = f;
  fptr(i);
}

この場合も3の場合と同様に、このような関数ポインタへの契約注釈を許可するとして、このような代入を許可するかどうか、とこのfptrを介した呼び出しで関数ポインタに指定された契約注釈を評価するかどうか、を決定する必要があります。

2と3の場合に代入を禁止することを選択すると、このケースも自然に禁止されます。

代入を許可する場合、2と3のケースの結論から、fptrを介した呼び出しは関連するすべての契約注釈(fのものとfptrのものの両方)を評価する必要があることが分かります。

すると続いて、それらの関連する契約をどの順番で評価するか、および2つの関連するエンティティにおいて許可される契約注釈の種類に制限はあるか、が問題になります。これには2つの選択肢があります

  • Caller-facing and callee-facing contracts model (Contracts MVP)
    • 契約注釈をCaller-facing(呼び出し元の契約)とcallee-facing(呼び出し先の契約)に分けて考える
      • Caller-facing(呼び出し元の契約): 関数ポインタの契約
      • callee-facing(呼び出し先の契約): 関数の契約
    • すなわち、呼び出し元の事前条件 -> 呼び出し先の事前条件 -> 関数本体 -> 呼び出し先の事後条件 -> 呼び出し元の事後条件、の順に評価される
    • これは、P2900R9の仮想関数に対する契約注釈の評価においての考え方と同じ
  • Substitution principle model
    • Eiffel、D、Adaにおいて仮想関数に使用される契約モデル
      • Assertion Redeclaration ruleを順守するもの
    • 関数ポインタの契約注釈だけを評価すれば、指している関数の契約も満たされる
    • P2900では仮想関数にこのモデルを採用しておらず、関数ポインタでだけ採用すると仕様の矛盾が生じる
    • 事前・事後条件の包含をどう判定するか(強制するのか)という問題も生じる

5. 中間契約(関数ポインタを別の関数ポインタへ代入する場合

ある関数ポインタを別の関数ポインタへ代入する場合で、それぞれ異なる契約注釈を持つ場合

int f(int i) /*契約注釈はある場合とない場合がある*/;

int (*fptr1)(int i)
  pre (i % 2 == 0)      // must call with even integer
  post (r: r % 2 == 0); // guaranteed to return even integer

int (*fptr2)(int i)
  pre (i > 0)
  post (r: r != 0);

void test(int i) {
  fptr1 = f;
  fptr2 = fptr1;  // (1)
  fptr2(i);       // (2)
}

(1)の代入はコンパイル可能かどうか、(2)の呼び出しはどのアサーションを評価するか、が問題となります。この場合、4での結論からfptr2fの契約は評価されるべきですが、fptr1の契約(中間契約)がどうなるのか?というように言い換えられます。この場合の中間契約は無限に存在しえます。

中間契約の扱いに関しては複数の方向性があります

  1. 呼び出し元の契約と一致させる
    • 呼び出し元と呼び出し先の契約の一致を強制する場合、必然的に中間契約の一致も要求される
  2. 暗黙的に中間契約を削除
    • 仮想関数の場合と同様に、呼び出しに直接関与しない契約を考慮しない
  3. 明示的に中間契約を削除
    • 呼び出し元の契約と中間契約が一致していない場合をill-formedとし、明示的な構文によってオプトインする
  4. 中間契約を評価する
    • 実行時に中間契約のチェーンを構築し、トラバースする必要がある

6. 契約の伝播

関数ポインタを直接呼び出して使用する場合、その関数ポインタになされた契約注釈はある種分かっており、その関数ポインタの宣言時になされた契約です。問題は、この情報は関数ポインタの式を通じて伝播するべきか、するとしたらどのように伝播するべきか、という点です。

struct C {
  auto get_fptr() { return fptr; }
private:
  typedef int (*fptr_t)(int x) pre (x > 0); // (1) privateメンバになされた契約

  fptr_t fptr;
};

void test(int i) {
  C c;
  c.get_fptr()(i); // (2) ここで事前条件チェックを有効にするには、(1)の契約が式を通じて伝播する必要がある
}

このような例を禁止することは、prvalueに契約を付加できない(上記c.get_fptr())ことや、契約注釈を暗黙的に削除する事はユーザーにとってはおそらく予想外のこととなる事から、実行可能とは言えません。

したがって、このような伝播を許可することが唯一の選択肢の様です。これが実現可能か、どのように実現可能かは、選択された仕様戦略によって異なります。

7. テンプレートと型推論

// Pointer to function that accepts only positive numbers:
typedef int (*positive_fptr_t)(int i) pre(i > 0);

positive_fptr_t positive_fptr = f;

void test(int i) {
  std::function<positive_fptr_t> positive_f = positive_fptr;
  positive_f(i); // (1)

  std::vector<positive_fptr_t> positive_fs = {positive_fptr};
  positive_fs.front()(i); // (2)
  
 std::invoke(positive_fptr, i); // (3)
}

この場合、(1)(2)の地点で契約チェックが行われるかどうかが問題となります。また、(3)の場合はテンプレート引数推論を介してもそれが行われるかどうかが問題となります。

さらに、異なる契約がなされた2つの同じシグネチャの関数ポインタは、異なるテンプレートのインスタンス化を引き起こすかどうかについても問題となります。

// Pointer to function that accepts only positive numbers:
typedef int (*positive_fptr_t)(int i) pre(i > 0);
positive_fptr_t positive_fptr = f;

// Pointer to function that accepts only negative numbers:
typedef int (*negative_fptr_t)(int i) pre(i < 0);
positive_fptr_t negative_fptr = g;

void test(int i) {
  std::invoke(positive_fptr, i);
  std::invoke(positive_fptr, i); // same or different template instantiation?

  std::vector v = {positive_fptr, negative_fptr}; // (4)
}

(4)ではCTADは失敗するべきでしょうか?そうではない場合、このvのいずれかの要素にアクセスして呼び出しを行った場合にチェックされるのはどの契約注釈になるでしょう?

これらの問題の解答は、仕様戦略と密接に関連しています。

仕様戦略

最後に、関数ポインタに独自の契約注釈を持たせるためにその情報をどこに格納するか、の仕様戦略について3つのものをオプションとして挙げています。

  1. 関数ポインタ型の一部
    • 契約注釈が事なる関数ポインタ型は異なる型となる
    • 利点
      • 型システムを通して呼び出し元と呼び出し先の契約の一致を要求したり、異なることを許可する型変換規則を定義できる
        • 中間契約も同様
      • 契約注釈は型を通して自然に伝播する
    • 欠点
      • 契約注釈が異なるだけでテンプレートのインスタンス化が別々に行われる
      • 型に契約注釈の情報が埋め込まれることでこれはABIの一部となり、仕様の変更が難しくなる
        • P2900の原則に反する
      • 解決すべき問題が多い
  2. 関数ポインタの値の一部
    • 契約注釈をポインタの値にエンコードする
    • 利点
      • 動的な契約注釈のチェーンを実現できる
      • 式の評価を通して契約を伝播させられる
    • 欠点
      • 契約注釈を使用するかによらず、関数ポインタに一定のオーバーヘッドを埋め込むことになる
    • 実装戦略
      • サンク
        • 契約注釈をチェックしてから元の関数を呼び出す中間関数を暗黙に作成し、そのアドレスをポインタに入れる
      • ワイドポインタ(ファットポインタ)
  3. 関数ポインタ変数宣言のプロパティ
    • 契約注釈は関数ポインタ宣言のプロパティとする
    • C++17以前のnoexceptalignasと同様のアプローチ
    • 利点
      • ポインタの型や値を変更することによる影響を回避できる
    • 欠点
      • 現在のC++の規則では、中間契約を伝播したり、式を通じて契約を伝播させることができない
      • 契約注釈をテンプレートに伝播させる方法がない

これらの仕様戦略(実装戦略)はそれぞれ異なる性質と課題を持っており、それが可能にするセマンティクスの選択肢とトレードオフとを慎重に検討する必要があります。

この文書はあくまで、関数ポインタに対する契約注釈という機能のために考えるべきことの整理や検討を行っているのみで、それそのものを提案しているわけではありません。その機能の提案はこの文書をベースとして他の提案で議論されることを想定しています。

P3334R0 Cross Static Variables

ある静的変数を、同じテンプレートの異なるインスタンス化で共有できるようにする機能の提案。

関数テンプレート、またはクラステンプレート内で宣言されたstatic変数は通常、そのインスタンス化毎に実体が異なっています。このため、そのような静的変数をテンプレートがインスタンス化された回数のカウントに直接使用するようなことは現在できません。

この提案ではそれを可能にするために、テンプレートで共通の(各インスタンス化で共有された)static変数を作成可能にする機能を追加しようとするものです。

このような機能のユースケースには例えば

  • アロケータ
  • 共有リソース
    • ロガーに使用する出力ストリームなど
  • 共有統計

などがあります。

現在これらの事を達成するにはグローバル変数を使用せざるを得ませんが、この提案の機能では、グローバル変数の使用を回避しつつ同じテンプレートのインスタンス化の内側でのみ使用可能に制限された静的変数を得ることができます。

この提案ではまだ具体的な構文を決定しておらず、次の6つの選択肢を提示しています

  1. staticにブール型の引数を追加する:static(true) int n = 5;
    • static(true)でこの提案の機能、static(false)で現在の動作
    • 短所
      • bool引数は混乱の元かもしれない(trueの意味が明確ではない
      • 式の記述を許可しない場合、他のところと一貫していない
  2. staticキーワードの前に新しいキーワードを追加する:cross static int n = 5;
    • 短所
      • 新しいキーワードの追加は通常好ましくない
        • 議論に時間がかかる
      • 既存のコードベースに登場しない名前の取得はかなり困難
    • 長所
      • 意味が明確(キーワードを適切に選択した場合
      • 追加するキーワードは明確に静的変数宣言に関係し、テンプレート引数に依存していないことが明確になる
  3. 新しいキーワードcross_staticを追加する: cross_static int n = 5;
    • 長所
      • この提案の目的以外には使用できない
      • 2つの単語で構成されたキーワードは1つのものよりもコードベースへの影響が低い
    • 短所
      • crossキーワードの別の使用方法がある場合、cross ~cross_staticで一貫しなくなる
  4. staticキーワードを2回連続で使用する:static static int n = 5;
    • 長所
      • 新しいキーワードが必要ない
    • 短所
      • ここまでの選択肢ほど明確ではない
        • 教育コストがかかる
      • 同じキーワードの繰り返しは間違いが起こりやすくなる
  5. staticを使用せずに、完全に新しいキーワードを使用する
    • persistentinherentなど
    • 長所
      • パースが簡単
    • 短所
      • staticとのつながりが明確ではない
      • 既存のコードベースで使用されている可能性が高い
        • 単一キーワードだと、文脈依存として使用するのも難しくなる
      • コンパイラはこの新しいキーワードとstaticの併用に対してエラーを発せなければならない
  6. staticに対する新しい属性を使用する:[[cross]] static int n = 5;
    • 長所
      • パースが容易で、曖昧さがない
      • 属性内ではキーワードの競合が発生しない
    • 短所
      • 属性の無視可能性によって、コンパイラバージョンの違いで意味が異なる

このような変数の事をクロスstatic変数(cross-static variable)と呼んでいます。セマンティクスとしては追加で次のような特性を持ちます

  • 次のものの間で一意のアドレスをもつ
  • 通常の関数内static変数のように、スレッドセーフな遅延初期化を持つ

実装としては、現在の関数内static変数を活用して、クロスstatic変数毎にその他実体の定義を含む隠し関数を生成して、その変数への参照としてアクセスを提供することで実装できるとしています。

この提案は、SG7でのレビューにおいて関心を集めることができず、否決されています。

P3335R2 Structured Core Options

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

以前の記事を参照

このリビジョンでの変更は、文言と対応するJSONスキーマの変更を追加したことです。

P3346R0 thread_local means fiber-specific

スレッドローカル変数がファイバーに対してもローカルになるようにする提案。

P0876では、ファイバーと呼ばれる軽量スレッド(スタックフルコルーチン)を実装するための基盤となるライブラリ機能(fiber_context)が提案され、将来のC++への導入に向けて議論されています。

ファイバーはあくまでスレッドではないためthread_local変数はファイバー間で区別されず、ある1つのスレッドで実行される複数のファイバーはそのスレッドのthread_local変数の変更を認識することができます。とはいえ、ファイバーは1つのスレッドで同時に1つしか実行されないため、競合が起こることはありません。

ただし、既存のthread_local変数を使用しているライブラリなどをファイバー並行した際に、このことが問題となる可能性があります。そのため、この提案では、thread_local変数のセマンティクスをファイバーにまで拡張することを提案しています。

この提案後には、thread_local変数はスレッド毎に異なる値を持つだけでなく、ファイバー毎にも異なる値を持つようになります。これにより、既存のthread_local変数を使用しているコードは自然にそのままスレッドからファイバーへ移行することができます。

また逆に、同じスレッド上で実行されるファイバー間で共有されるスレッドローカル変数が欲しいという需要に応えるために、thread_specific_ptrというライブラリ機能を導入することも提案しています。これは、Boost.Threadで開発されて出荷されているライブラリであり、ライブラリでエミュレートされたスレッドローカル変数です。こちらはファイバーを認識しないため、スレッドローカルでありつつファイバー間で共通な変数を使用することができます。

この提案はSG1におけるレビューで支持を得られず、リジェクトされています。

P3348R1 C++26 should refer to C23 not C17

Cへの参照をC17からC23へ更新する提案。

以前の記事を参照

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

  • P2537R2の[cstdarg.syn]への変更を取り込み
  • [cctype.syn], [cwctype.syn], [cuchar.syn], [c.mb.wcs]に不足していた変更を追加
  • timespec_getres()を追加

などです。

P3349R0 Converting contiguous iterators to pointers

contiguous_iteratorをポインタとして扱うことができるようにするために、標準ライブラリにおける要件を強化する提案。

contiguous_iteratorstd::to_address()を使用してポインタに変換することができます。これによってstd::copystd::memmoveにするような最適化が可能になるものの、std::contiguous_iteratorの持つ要件は単にイテレータの各要素がメモリ上で連続的に配列されていることを確認するだけにとどまっており、イテレータによる反復をポインタを用いた処理に置き換えることを許可していません。

例えば、特定の要素に到達した時に例外を送出することで終了するthrowing_iteratorのようなものを考えてみます。これは単なるイテレータラッパであるため、contiguousな範囲から作成されればthrowing_iteratorcontiguous_iteratorになります。

int data[4]{1,2,3,4};
try {
  // data+2に到達すると例外を送出する
  ranges::for_each(throwing_iterator(data, data+2), data+4,
                   [](int i){ assert(i != 3); });
} catch (...) {
}

この例におけるassertは発動することは無いわけですが、このようなイテレータとしての副作用を考慮してcontiguous_iteratorのcontiguous性の活用が妨げられているとすると、contiguous_iteratorrandom_access_iteratorと異なる部分が無くなってしまうため、存在意義がありません。

この提案では、contiguous_iteratorのcontiguous性を標準ライブラリがより活用できるように、標準の文言を調整しようとするものです。

具体的には、標準ライブラリの実装がcontiguousな範囲を扱う際に、イテレータによる反復をポインタによる反復に置き換えてることを許可する指定を明示的についかします(実装は、空ではない隣接範囲r[to_address(begin(r)), to_address(begin(r))+size(r))に置き換えることを許可する)。これによって、標準ライブラリの実装はcontiguous_iteratorイテレータとしてのAPIが持つ副作用(インクリメントや間接参照時の副作用)を考慮しないようになるとともに、ユーザーは標準ライブラリ実装がそれを考慮することに依存できなくなります。

この提案は2025年1月に行われた全体会議で承認され、C++26に導入されています。

P3351R1 views::scan

状態を持つ関数を使用可能なviews::transformであるRangeアダプタ、views::scanの提案。

以前の記事を参照

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

  • 文言修正
    • end()に不足していた noexceptを追加
    • scan_viewの制約を独自のコンセプトにリファクタリングし、range_reference_t<R>からの代入可能性とfの呼び出し結果の両方をチェックするようにした
    • regular_invocableinvocableに修正
    • scan_viewのコンストラクタ内および関数呼び出し時にmoveを使用する
  • N4988へのリベース

などです。

P3352R0 Taming the Demons (C++ version) - Undefined Behavior and Partial Program Correctness

未定義動作がプログラム全体の動作を未定義にしないようにする提案。

現在のC++標準のルールにおいては、プログラムの実行グラフのどこかで未定義動作が発生すると、そのプログラム全体の動作について標準は何の要件も課さなくなります(これがいわゆる鼻から悪魔)。特に、未定義動作が発生する地点よりも前のコードにさえもその動作は及び、タイムトラベル最適化を引き起こす事が知られています。この提案では、これを抽象ルール(abstract rule)と呼んでいます。

対してここで提案されているのは、未定義動作が発生した場合に未定義動作を引き起こす操作自体に要件を課さないようにするにとどめて、他のwell-formedな動作はそのまま(well-formedのまま)にします。この提案しているルールを具象ルール(concrete rule)と呼んでいます。

現在の抽象ルールはコンパイラの最適化を妨げないためのルールであると認識されており、具象ルールはそれに対する破壊的変更に見えます。しかし実際のところ、抽象ルールとはコンパイラのバグ的な動作も含めて包含する形で成立したものであり、現在のコンパイラは具象ルールに基づいて動作しているため、具象ルールへの変更は既存慣行の標準化にすぎません。

また、抽象ルールの下で可能になるとされている望ましい最適化も、具象ルールによって妨げられる望ましい最適化も実際には存在せず、抽象ルールの下では不可能だった最適化すら存在することが、長年の経験と分析によって明らかにされています(WG14のUndefined Behavior study groupによる作業)。提案では難解ながらもその説明があります。

ただし、具象ルールであっても未定義動作が発生した地点以降の動作については変わらず何の要件も課しません。これは、未定義動作の後に発生する動作とはそれがどのようなものであっても、未定義動作と区別をつけることができないためです。そのため、具象ルールで変化するのは未定義動作が起きた地点より前のプログラムについての動作の保証のみです。

そのため、具象ルールは実際のところ現在および将来の実装にほとんど影響するものではなく、具象ルールによってほとんどの操作の状態が変化するわけでもありません。それでもなおこの提案を導入するメリットとして、次の事を挙げています

  • 具象ルールは抽象ルールよりも単純で安全であり、教えやすくコードの動作の予測が簡単になる
    • 難解な抽象ルールが原因で発生するコンパイラのバグが存在している
  • 具象ルールはC23で導入済みであり、C23のルールと一致する。CとC++は実装によって同じ中間表現へ変換されるフロントエンド言語であり、最適化はその中間表現に対して行われます。そのため、最適化の動作を制御するルールを共有していることは理にかなっている
  • 具象ルールによって、未定義動作を含むプログラムの部分的な正しさを示すのがかなり簡単になる
    • 抽象ルールはかなり難解な一方、具象ルールはプログラムと実装の正しさ/誤りを証明するための基礎として遥かに簡単なもの
  • 具象ルールはvolatile操作のユーザーにとって望ましい、より現実的な結果をもたらす
    • これは、volatile操作のユーザーから明示的に要求されたものでもある
  • 抽象ルールから具象ルールへの変更によって、望ましい最適化が妨げられるようになるなど、見落としがある可能性がある。そのような最適化が発見されて調査され、WG21と実装の協力によってそれが標準の欠陥なのかコンパイラのバグなのかを決定できるようになる
    • 抽象ルールではそのような発見の可能性を低くし、潜在的コンパイラのバグさえも規格準拠とすることをデフォルトにしている

この提案のベースは、CのUndefined Behavior study groupで長年作業され調査されてきた結論をC23標準に適用したN3128にあり、この提案はC++がそれに追随することを提案するものです。

ところでC++にはこの提案に非常に関連する提案として、観測可能なチェックポイントという最適化を提案するP1494があります(2025年1月にC++26に採択済み)。P1494では一部の操作を観測可能なチェックポイントとして定義して、観測可能なチェックポイントを超えて未定義動作が波及しないようにすることを提案しています。P1494のことばでいうなら、この提案はプログラム内のすべての場所を観測可能なチェックポイントにするものであり、P1494の上位互換と言えます(そして、この提案で述べられているように、それは現在の実装の動作そのものです)。

P3355R1 Fix submdspan for C++26

C++26std::submdspanへの修正提案。

以前の記事を参照

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

  • ユーザー定義のペア型をスライス指定として扱う、機能を削除
    • この機能は別の提案で扱う
  • __cpp_lib_submdspanの値をインクリメント
  • 単位strideを持つすべてのスライス指定子型に対して、文言マクロを使用する
    • "type S is a unit-stride slice for M"という文言で指定される型Sのこと

などです。

この提案は2024年11月の全体会議C++26に向けて採択されています。

P3367R0 constexpr coroutines

コンパイル時でもコルーチンを動作可能にする提案。

std::generatorのようなコルーチンはRangeアダプタの簡易な実装に有用であり、コンパイル時に使用できるようになるとより便利です。この提案は、単純にコルーチンの定数式での実行を許可する提案です。

サンプルコード

template <typename T>
constexpr auto fib() -> std::generator<T> {
  T a = 0;
  T b = 1;
  co_yield a;
  do {
      co_yield b;
      auto tmp = b;
      b += a;
      a = tmp;
  } while (true);
}

template <typename T, size_t N>
constexpr auto calculate_fibonnaci() {
  auto res = std::array<T, N>{};
  std::ranges::copy(fib<T>() | std::views::take(N), res.begin());
  return res;
}

constexpr auto cached_fibonnaci = calculate_fibonnaci<unsigned, 20>();

この提案では、特に複雑な仕様を追加することなく、現在定数式で禁止されているコルーチン(await式やyield式)の動作を単に許可しています。これにより、既存のコルーチンに対してconstexprを付加するだけで、そのコルーチンは定数式で実行可能になります(定数式でできないことをしていなければ)。

ただし、ある定数式の評価内で生成されたコルーチン(のステート)は、その式の評価内で解放されていなければならず、そうでなければコンパイルエラーになります(すなわち、コルーチンステートを実行時に持ち出すことはできません)。これは現在のコンパイル時の動的メモリ確保と解放についてのものと同じルールです。

筆者の方はこれをclangのフォークで試験実装したうえで提案しており、少しの実装負荷があったものの実装そのものに大きな障害は無かったようです。

EWG/CWGのレビューにおいて、実装の負荷の割に需要が小さいのではないか(特にC++26のタイムフレームの中では)という指摘があったようで、C++26に入れずにC++29に向けて検討されているようです。

P3370R1 Add new library headers from C23

C23で追加された新しいライブラリヘッダ<stdbit.h><stdchkint.h>C++にも追加する提案。

以前の記事を参照

このリビジョンでの変更はLEWGレビューからのフィードバックを適用したことです。

この提案は2024年11月の全体会議で承認され、C++26に採択されています。

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

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

以前の記事を参照

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

  • Hermitian matrix rank-1 and rank-k update系関数では、スケーリング係数alphaの型を制限しない。代わりにreal-if-needed(alpha)を使用するようにアルゴリズムを定義する
  • 説明専用のnoncomplexコンセプトを削除
  • R1で導入された説明専用のpossibly-packed-in-matrix that was introducedコンセプトを削除
    • 制限が厳しすぎるため

などです。

P3372R2 constexpr containers and adapters

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

以前の記事を参照

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

  • node-handle::mapped()constexprを削除
  • 実装セクションのceilf()に関する記述を削除(この関数はC++23以降constexprであるため
  • ハッシュ化とそれが含まれていない理由について説明を追記
  • libstdc++の実装可能性に関する情報を追加
  • 既存のコードへのこの提案の影響に関する説明を拡張

などです。

この提案は2024年11月の全体会議で承認され、C++26に向けて採択されています。

P3375R1 Reproducible floating-point results

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

以前の記事を参照

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

  • 貢献者リストを更新
  • IEEE-754:2019とISO/IEC 60559:2020の関係を明確化
  • 同期を拡張し、非結合性と可変エラー要件を明確化
  • 銀行業務のユースケースを追加
  • ゲームのシミュレーションにおいて、ビット単位で同一の結果が必要であることを明確化
  • 幾何学ユースケースを追加
  • C規格のAnnex F.3 パラグラフ20を強調
  • non-goalsについて概説
  • 正しい丸めを行う数学関数の提供に関するセクションを追加
  • 背景情報として参考文献を追加

などです。

P3376R0 Contract assertions versus static analysis and 'safety'

Contractsが安全性と静的解析とどのように相互作用するかについて考察する文書。

おおむね、以前のP3362R0の主張(静的解析の観点から、Contracts MVPの仕様は十分ではない)に反論するものです。

内容は

  • 安全性という言葉から、システム安全性とUB安全性とを分離し区別する
    • この二つは異なる目標
  • P2900のContracts MVPは既に有用であること
    • 2つの価値をもたらしている
      • 契約注釈の標準化された表記法
      • 契約注釈を指定する場所の標準化
    • 異なるツールが1つの表記を認識することができ、その分析を単一の翻訳単位内で完了することができる
  • P2680R1/P3362R0のコンベア関数+非緩和契約はバグが多く、基本的な問題に対処できていない
  • 緩和契約と非緩和契約の適用順序について
    • 緩和契約を先に入れてしまうことは主張されているほど悪いことなのか、不明

等を説明しています。

P2680R1/P3362R0の主張は現時点で実行可能ではなく、将来的にも実行可能かどうか分からず、主張されているほどのメリットをもたらさず、P2900R8のContracts MVPの仕様は主張されているほど役に立たないわけでもない、ということが主張されています。

P3378R0 constexpr exception types

例外型をconstexpr指定する提案。

P3068にて定数式における例外送出が議論されており、この提案は順調に進行しています(2024年11月の全体会議で採択されています)。しかし、関連する例外型を定数式で使用可能にするための作業は遅れており、この提案はそれを行うためのものです。

P3068でも例外型の一部は定数式で使用可能となるように提案されていますが全てではなく、ここでは既に定数式で使用可能な機能(特にstd::vector/std::string)が実行時に使用している例外を定数式でも同様に使用可能にすることを目的としています。

具体的には、次の例外型の全てのメンバ関数およびコンストラクタとデストラクタにconstexprを付加することを提案しています

  • logic_error
  • domain_error
  • invalid_argument
  • length_error
  • out_of_range
  • runtime_error
  • range_error
  • overflow_error
  • underflow_error
  • bad_optional_access
  • bad_variant_access
  • bad_expected_access
  • format_error

筆者の方は提案の内容をlibc++で実装しており、実装上のポイントなどを報告しています(障害となるような困難さはないようです)。

P3385R1 Attributes reflection

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

以前の記事を参照

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

  • P2996R5へリベース
  • ignorabilityセクションを書き換え、一連のルールについて言及
  • std::meta::info==の引数句について議論を追加
  • reflect expressionにおけるnull文について議論を追加

などです。

この提案では、例え実装が(意味的に)属性を無視するとしても、関連属性についてのリフレクションが可能であるべき、としています。

P3386R0 Static Analysis of Contracts with P2900

P2900のContracts仕様が現在のままでも静的解析に十分に役立つことを説明した文書。

P3376R0とほぼ同じく、P3362R0に反論するものです。

現在の静的解析環境について簡単に紹介し、標準的な契約注釈の構文が導入されることで現在の静的解析の制限となっている次のような問題

  • マクロベースの機能の制限
  • 事前条件と事後条件が関数宣言に表示されないことによる制限
    • 関数の呼び出し元に事前条件と事後条件が表示できないため、その解析のためには追加の特注のツールが必要となる
  • アサーションマクロの多様性
    • 静的解析ツールが認識するアサーションマクロにはassert()を除くと共通のものは通常ない
    • clangのanalyzer_noreturn属性のような方法が提供されることがあるもののデファクトは無く、このようなコラボレーションが必要になることそのものがアサーションマクロと静的解析両方の使用を妨げている

が、全てP2900では解消されるとしています。

その上で、P3362R0(P2680R1)で提案されているような追加の変更はむしろ不必要であるか望ましくない、としています。その理由としてP2680R1の提案の個別機能について

  • 契約注釈内でUBを定義済みにする
    • 例えば、符号付整数型のオーバーフローを定義してラップアラウンド(符号なしと同じ動作)にすると、通常のC++コードとの動作の不一致によって、むしろ契約条件を記述したプログラマの意図を損なう可能性がある
    • 単純な算術演算による事前条件が、意図しない広範囲の値で満たされるようになってしまうことが起こりうる
  • 契約注釈からの副作用の除去
    • このような性質は通常再帰的なものではなく(ADL等によって容易に評価コーンの外側に飛び出す)、実行時に保証するのは現実的ではない
    • constメンバ関数、またはconstメンバ関数mutableメンバ、のような現在のC++で一般的に使用されるコードパターンを許可するにはさらに複雑な分析が必要となり、より現実的ではない
    • そのような評価コーンの外側に飛び出しうるコードを禁止すると、契約条件式の自由度が大きく制限され、契約プログラミング機能自体の採用の妨げになる
  • シンボリックな述語の実行時評価
    • ポインタが有効なオブジェクトを指しているかを判定するobject_address()のような述語を契約注釈に自動で挿入する
    • 契約注釈はプログラムのある時点での状態を判定するものであり、その安定性を保証するものではない
    • ポインタの有効性の判定は実行時であっても困難な場合が多く存在し、この判定結果の品質は実装品質に大きく依存する
    • 組み込みの言語機能が実装品質に大きく依存するとユーザーエクスペリエンスが悪くなり、採用を妨げる
    • このようなシンボリックな述語は、評価されない契約注釈で使用するのに適している(P2900にはない機能)
  • 契約注釈に使用可能な関数をマーキングされたものに限定する
    • 契約注釈内で使用可能な関数をコンベア関数([[conveyor]]付きのもの)に限定する
      • UBの定義済み化や副作用の禁止などが導入される
    • constメンバ関数の使用は実質的に禁止され、既存のほとんどの関数は契約注釈で使用できなくなるため、ユーザーは同じような関数を2種類記述することになる
    • 静的解析ツールはその要件を満たす述語を認識し、そうでないものを無視する機能を既に備えているためこのような制限は不要
      • 柔軟なツールとより広い実行時述語を作成するユーザーの機会が奪われる

等を挙げています。

また、P2900をベースとした将来の進化の方向性についても紹介し、P3362R0(P2680R1)で言われているように進化を妨げることにはならないとしています。

P3387R0 Contract assertions on coroutines

コルーチンにおける契約注釈の指定を許可する提案。

P2900R8のContracts MVP仕様では、コルーチンに対する契約注釈(pre()/post())の指定を禁止しています(contract_assertの使用は可能)。これは、コルーチンに対する事前条件と事後条件がどのように解釈されどのように振舞うべきか、が未確定であるためです。

これまでの議論等は以前の記事を参照

この提案は、P2957での議論をベースとして、以前の提案で検討されていなかったオプションや設計空間を徹底的に調査し、コルーチンとContractsの両方の設計目標と設計原則を最もよく満たすソリューションの確立と文言の提案、をするものです。

コルーチンに対する事前・事後条件の指定は、コルーチンのランプ関数に対する指定とすることを基本としています。コルーチンのランプ関数とは、ユーザー側から見た時にコルーチンを初期化して初期サスペンドポイントで中断させた状態でユーザ側へ返す、コルーチンのファクトリ関数のような役割をする暗黙的な関数です。この提案では、コルーチンの事前条件はランプ関数の引数およびその時点におけるプログラム状態に対する契約となり、事後条件はランプ関数の戻り値(co_yieldの値ではない)に対する契約となります。

コルーチンに対する事前・事後条件をランプ関数の契約としてコルーチンの実装詳細(コルーチン動作を行う部分そのもの)とは切り離して考えることで、コルーチンに対する契約注釈の振る舞いを通常の関数にかなり近くすることができます。

それでも一点問題となるのは、コルーチンのランプ関数においては関数引数が常にconstではない、という点です。通常の関数においては、事後条件から(非参照)関数引数を参照する際に全ての関数宣言においてその引数はconstである必要があります。これは事後条件で引数に対する意味のある条件を記述するために必要な事前条件です。

しかし、コルーチンのランプ関数においては、コルーチン引数をコルーチンステート内にムーブして保存し、なおかつその際にランプ関数が実質的に引数のトップレベconstを除去して動作します。これにより、コルーチンではその引数は常に非constであり、事後条件から参照するとムーブ後の値を参照しかねません。そのため、コルーチンの事後条件からその非参照引数を参照する場合の動作をどうするかを考え、選択する必要があります。

これらの事をベースとして、この提案では次の4 + 3のソリューションを提示しています

  1. 全てのコルーチンで事前・事後条件を許可しない(P2900R8の現在
  2. 事前条件は許可するが事後条件は許可しない(P2957R1で提案
  3. 事前条件に加えて事後条件も許可し、事後条件からの非参照引数のODR使用を禁止する(P2957R2で代替提案
  4. 事前条件に加えて事後条件も許可し、事後条件から使用される非参照引数はコルーチンステートのものを参照する
  5. 事前条件に加えて事後条件も許可し、事後条件から使用される非参照引数は元のオブジェクトを参照し
    1. ランプ関数ではコルーチンステートへ引数をムーブでは無くコピーする、もしくは
    2. 引数型が非トリビアルなムーブコンストラクタを持つ場合(ムーブ後状態を導入しうる場合)、ill-formedとする、もしくは
    3. 何の制限も導入しない(ムーブ後の値を参照しうる

そのうえで、順守すべきContractsとコルーチンの設計目標としてつぎの7点を挙げています

  1. コルーチンでも事前条件と事後条件を許可する
  2. コルーチン性を実装の詳細として扱う
  3. 引数のムーブ後状態を事後条件で公開しない
  4. Contracts Prime Directive を満たす
    • Contracts Prime Directiveとは、既存のプログラムに契約注釈を追加してもそのプログラムの正確性は影響を受けない、というもの。契約注釈の存在がプログラムのコンパイル時/実行時のセマンティクスに影響を与えない
  5. 事前条件と事後条件の間に新たな非一貫性を導入しない
  6. リモートのコード破壊を助長しない
    • リモートのコード破壊とは、ある部分のコード変更が、一見無関係な別の部分に悪影響を与えることで、コンパイルエラーや実行時バグを引き起こすこと
    • 例えば、コピーコンストラクタだけを持つ型にムーブコンストラクタを導入した結果、ムーブ後状態が生まれることで実行時バグを引き起こす、など(これはソリューション5.3で起こりうる
  7. 事前条件と事後条件の呼び出し側チェックをサポートする

これらの事を表に並べて比較すると

設計原則/ソリューション 1 2 3 4 5.1 5.2 5.3
1. コルーチンでも事前条件を許可する
1. コルーチンでも事後条件を許可する
2. コルーチン性を実装の詳細として扱う
3. 引数のムーブ後状態を事後条件で公開しない
4. Contracts Prime Directive を満たす
5. 事前条件と事後条件の間に新たな非一貫性を導入しない
6. リモートのコード破壊を助長しない
7. 事前条件と事後条件の呼び出し側チェックをサポートする

❓のところはどちらともとれる(判断が分かれる)事を表しています。

この表から明らかなように、全ての設計目標を満たしつつコルーチンとContractsの両方の設計原則と互換性があるソリューションは案3のみです。この結果から、この提案ではコルーチンにおける契約注釈として案3、すなわち

  • コルーチンにおいて事前条件と事後条件を許可する
  • コルーチンにおける事前事後条件は、そのランプ関数に対する契約となる
  • コルーチン事後条件から非参照関数引数をODR使用すると、ill-formed

を提案しています。

P3394R0 Annotations for Reflection

C++任意のエンティティ(宣言)に対して静的リフレクションのためのアノテーションを付加できるようにする提案。

静的リフレクション機能をより活用するためには、リフレクション対象のエンティティに対して何らかの注釈を行い、その注釈をリフレクションから取得できるようにする必要があることが指摘されてきました。

この提案は、属性を拡張した注釈を導入することでこれを可能にしようとしています。注釈のための構文は次のようなものです

struct C {
  [[=1]] int a;
};

[[ ... ]]の中に=exprという形の属性を指定することで注釈としています。構文スペースとしては属性の物を利用していますが、これは属性ではありません。

=exprexprは定数式である必要があり、std::meta::infoを通してその値が取得されるためその型はstructural typeである必要があります。

この提案の注釈が属性構文を使用しつつも属性ではないのは、属性が無視可能であることに加えて、属性を反射(^^)した後で何を返すべきかが明確ではないためです。例えば^^[[nodiscard, gnu::always_inline]]の結果のリフレクション値は文字列を返すか、P3294のトークンシーケンスを返すかですが、どちらにしてもあまり使いやすくはありません(ライブラリで追加のパースが必要になるため)。

その一方で、注釈に値を指定して、注釈のリフレクションがその値を返すようにすると非常に便利であることが分かったため、ここでは注釈(リフレクションを介して値をユーザーに返す)を属性(値を保存しない)と区別して導入しています。これにより、属性のリフレクションはそのために有用な別の性質を考えることができます。

加えて、この注釈のリフレクションを扱うためのライブラリメタ関数をいくつか追加しています。

namespace std::meta {
  // リフレクション値が注釈を反映しているかどうかを調べる
  consteval bool is_annotation(info);

  // 特定のエンティティのアノテーションをすべて取得する
  consteval vector<info> annotations_of(info item);            // (1)
  // `type_of(a) == type`となる全ての注釈aを返す
  consteval vector<info> annotations_of(info item, info type); // (2) 
  // `dealias(type_of(a)) == ^^T`となる注釈aがあれば、それを返す
  template<class T>
  consteval optional<T> annotation_of(info item);

  // `dealias(type_of(a)) == ^^T`となる注釈aがあるかどうかを調べる
  template<class T>
    consteval bool has_annotation(info item);                 // (3)
  // `value_of(a) == reflect_value(value)`となる注釈aがあるかどうかを調べる
  template<class T>
    consteval bool has_annotation(info item, T const& value); // (4)

  // 宣言に注釈を付加する
  consteval info annotate(info item,
                          info value,
                          source_location loc = source_location::current());
}

その他の構文の例など

// 注釈は繰り返し指定可能
[[=42, =42]] int x;
static_assert(annotations_of(^x).size() == 2);

// 同じエンティティの複数の宣言で指定された注釈は蓄積される
[[=42]] int f();
[[=24]] int f();
static_assert(annotations_of(^f).size() == 2);

// 注釈は属性と同じ場所に指定可能だが、type-specifier-seqの属性指定可能な場所
// およびempty-declarationには指定できない
struct [[=0]] S {};  // ✅: Sの注釈
[[=42]] int f();     // ✅: f()の注釈.
int f[[=0]] f();     // ✅: 同上
int [[=24]] f();     // ❌: 戻り値型intには指定できない
[[=123]];            // ❌: 空の宣言には指定できない

// 注釈は属性内のusingの後に指定できない
[[using clang: amdgpu_waves_per_eu, =nick("weapon")]]
int select_footgun(int);  // ❌
// 分ければok
[[using clang: amdgpu_waves_per_eu]] [[=nick("weapon")]]
int select_footgun(int);  // ✅

ユースケースとしては例えば、Rustのclapというコマンドライン引数パーサーライブラリと同じコンセプトのライブラリの作成を挙げています。

struct Args {
  [[=clap::Help("Name of the person to greet")]]
  =clap::Short, =clap::Long
  std::string name;

  [[=clap::Help("Number of times to greet")]]
  =clap::Short, =clap::Long
  int count = 1;
};


int main(int argc, char** argv) {
  Args args = clap::parse<Args>(argc, argv);

  for (int i = 0; i < args.count; ++i) {
      std::cout << "Hello " << args.name << '\n';
  }
}

Help,Short,Longはメンバ変数に対応するコマンドライン引数がどのように使用できるかを指定するために必要な追加の注釈です。

実行の例

$ demo -h
USAGE:
  demo [-?|-h|--help] [-n|--name <name>] [-c|--count <count>]

Display usage information.

OPTIONS, ARGUMENTS:
  -?, -h, --help
  -n, --name <name>       Name of the person to greet
  -c, --count <count>     Number of times to greet

$ demo -n wg21 --count 3
Hello wg21
Hello wg21
Hello wg21

他にも、pytestのparametrize testやRustのserdeのようなSerializationライブラリの作成が例に挙げられています。

P3399R0 Adjusting Electronic Polls to a Hybrid Workflow

C++標準化作業における電子投票Electronic Poll)のプロセスを調整する提案。

C++の標準化における電子投票はCOVID19のパンデミックをきっかけに導入されたもので、現在のC++標準化委員会がハイブリッドモード(対面+オンライン)で動いていることから非常に活用されています。ただし、導入当初から色々変わってきているため、そのプロセスを調整しようとするのがこの提案です。

とはいえ、電子投票プロセスは電話会議の決定を確認するのに役立つことが経験的に実証されているため、ここではそれを行わないようにすることを提案しているわけではありません。

ここで提案されているのは、「対面(ハイブリッド)会議で行われた決定、電子投票を行うことでプロセスが遅れる可能性のある提案(各作業部会の議長の裁量に委ねられる)については、電子投票プロセスをスキップ可能にする」ことです。ただし、スキップに際して議長はスキップに対する反対意見がないこととリアルタイムの投票結果がコンセンサスに達していることを確認する必要があります。

この理由としては

  1. パンデミック前は電子投票は行われていなかった
  2. 現在の対面(ハイブリッド)会議はオンラインで開催されており、参加のハードルはかなり下がっている。そのため、提案に関心のある人はそこまで労力をかけずともリアルタイムの投票に参加できるはず
  3. 設計サイクルの終了間際に、手続き上の遅延によって修正や改善が3年遅れる可能性を可能な限り下げておきたい
  4. 設計グループがレビューする提案は同じ週に文言グループもレビューする可能性が高い(電子投票はそれに間に合わない)。これまでは、慎重に計画を立てたうえで必要な場合には電子投票をスキップする投票を行って電子投票を回避してきた
  5. 議長の裁量で電子投票をスキップ可能にする

等を挙げています。

この提案は少なくとも、LEWGのポリシーとして採択され、運用されていくようです。

P3402R1 A Safety Profile Verifying Class Initialization

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

以前の記事を参照

このリビジョンでの変更は明示的ではありますが

  • Introductionセクションの細分化
    • "Industry Demand"と"Profile Guarantees"に分割
  • Verified classの定義の変更
  • Acceptable inputsの定義の追加
  • 文書構成の変更
  • Verification of Constructorsの拡充
  • Idioms to Considerセクションへの項目追加
    • "Discipline with const & and const * Parameters"
    • "Non-Initializing Default Initialization"
  • P3274R0からの乖離について追記
  • Straw Pollsセクションの追加

などです。

P3403R0 The Undefined Behavior Question

未定義動作がそれが発生した地点よりも前のプログラムに影響を与えるべきか?を問う質問書。

この提案は、C23で採択済みのN3128をC++でも採択すべきか、あるいはC++の関連提案P1494とP3352(上の方で紹介済)のどちらを採択すべきか、という問いをかなり要約して、未定義動作がそれが発生した地点よりも前のプログラムに影響を与える事を許可すべきか?という問いとして委員会に対して投げかけているものです。

P3404R0 std::at : Range-checked accesses to arbitrary containers

カスタマイゼーションポイントオブジェクトであるstd::atの提案。

.begin()に対するstd::ranges::begin()のように、ユーザー定義のものも含めた様々なコンテナに対して統一的に.at()を使用できると便利であるため、一部のコンテナだけが持つ.at()関数をフリー関数かつカスタマイゼーションポイントオブジェクトとして提供しようとする提案です。

std::at.at()関数と同じメリット(範囲チェック付アクセス)を持ち、より汎用的に使用できます。提案では次の3つのケースをハンドルするようにされています

  1. 既に.at()を持つコンテナ: メンバの.at()を呼び出す
  2. 生配列: std::array::at()とほぼ同じ範囲チェック付きアクセスを提供
  3. ユーザー定義型: ADLでat()関数をルックアップして呼び出す

範囲外アクセスの場合は例外を送出するのは既存の.at()の動作と変わりません。このために、このCPOはフリースタンディング指定できません。

P3405R0 Out-of-order designated initializers

指示付き初期化において、指示子の順番がメンバ変数の宣言順と一致していなくてもよくする提案。

C++20で許可された集成体初期化における指示付き初期化では、指示子(.mem_var = init)の登場順が初期化している集成体型のメンバの宣言順と一致している必要があります。指示付き初期化はCから輸入された機能ですが、Cにはこのような制限はなく、これはC++側で意図的に入れられている制限の一つです。

// Annex Cにあるサンプルコード
struct A { int x, y; };
struct B { struct A a; }

// Cでは有効, C++でも有効
struct A a = {.x = 1, .y = 2};
// Cでは有効, C++では禁止 (out-of-order initializers)
struct A a = {.y = 1, .x = 2};
// Cでは有効, C++では禁止 (array initializers)
int arr[3] = {[1] = 5};
// Cでは有効, C++では禁止 (nested initializers)
struct B b = {.a.x = 0};
// Cでは有効, C++では禁止 (mixed initializers)
struct A a = {.x = 1, 2};

指示付き初期化は構造体を初期化するコードの可読性を向上させることができます。特に構造体のメンバ変数の数が多い場合、指示付き初期化なしで正しくメンバを初期化していることを示すには手作りのコメントか外部のツールの補助なしでは達成困難です。また、指示付き初期化をそのまま応用して簡易な名前付き引数を実現することもできます。構造体の初期化時同様に、関数呼び出しにおいてどの引数に何を渡しているのかを明確にする効果があります。

struct named_arguments {
  int foo;
  std::string option;
  bool flag;
  int bar;
};

void func(named_arguments);

// `.bar`は`.flag`の前にあるとエラーになる
func({ .foo=123, .bar=456, .flag=true });

ただし、この有用性は名前の出現順にほぼ自由度が無い事によって低下しています。

同様の応用に、ビルダーパターンにおける状態入力として使用するものもあります。この場合、よく使用されるメソッドチェーンによる方法と比較して読みやすさが向上するほか、メソッドチェーンでは困難だった2重呼び出しが自然に防止されます(提案にサンプルコードがあります)。

このように、指示付き初期化によって可能になるイディオムの有用性を損ねており、この事実は使用するユーザーにとって驚くべき制限となっています。

C++においてこのような制限がなされている理由は主に次の2点です

  1. オブジェクト構築と破棄の順序の規則に従うため
    • C++ではオブジェクトは宣言順に構築され、宣言順に破棄される、というルールがある
    • コンストラクタとデストラクタの呼び出しにおいてはこの順序が重要となる
    • Cではほぼ保証がない
  2. 集成体初期化における初期化子の評価順序の規定のため
    • C++におけるリスト初期化({...}による初期化)では、初期化子の評価順序がその出現順として規定されている
    • Cでは保証がない

指示付き初期化における順不同の指示子を許可するためには

  1. 上記2種類の順序のどちらかもしくは両方を、言語の他の部分と異なる順序にする
  2. 順序がずれたフィールド用に中間一時オブジェクトを利用する

のいずれかの方法を取る必要があり、この提案ではこれらのアプローチによる方法を3種類提案しています。

A. トリビアルデストラクタを持つ型でのみ許可する

トリビアルデストラクタは何もしないデストラクタであり、その動作は観測不可能で、呼び出しを省略することすら許可されています。そのためトリビアルデストラクタを持つ型ではメンバの破棄順序を気にする必要がなくなるため、メンバの初期化順序も重要ではなくなります。

// デストラクタはトリビアル
struct D {
  int a;
  int b;
};

void foo() {
  // d.b -> d.a の順で構築(リスト初期化の評価順序を順守する)
  D d{.b = 456, .a = 123};
  // 破棄の順序は観測不可能
}

リスト初期化のルールに従って指示子の登場順に初期化することができ、コンストラクタ初期化子リストにあるような罠(構築順はコードでの初期化順によらない)も回避されます。

集成体初期化においてはあるメンバの初期化のために同じクラス内の別のメンバを使用できないため、この構築順序の変更には危険なものはありません。

B. 一時オブジェクトを通して初期化する

トリビアルデストラクタを持たない型に対しても順不同の指示付き初期化を許可し、2つの順序の一貫性を維持したい場合に取れる唯一の方法は、コンパイラが一時変数を挿入して、それを介した2段階の初期化を実行することです。

// デストラクタは非トリビアル
struct E {
  std::string a;
  std::string b;
};

void foo() {
  // この初期化は
  E e{.b = "bbb", .a = "aaa"};

  // 次のような初期化と同じ
  std::string __b_tmp = "bbb";
  std::string __a_tmp = "aaa";
  E e{.a = std::move(__a_tmp), .b = std::move(__b_tmp)};
  // 初期化子の評価順序は左から右(登場順)になり
  // メンバの構築順は宣言順に、破棄順序は宣言と逆順になる
}

このような一時オブジェクトを用いた初期化は、トリビアルデストラクタを持たない型かつ指示子の登場順が宣言順と異なる場合にのみ行うようにする必要があります。また、各メンバはムーブ可能でなければオーバーヘッドが生じます。

参照型のメンバなどを考慮すると、ルールはもう少し複雑になります。

C. リスト初期化において、初期化順序をメンバの宣言順に強制する

コンストラクタの初期化子リストと同様に、初期化の順序を常にメンバの宣言順で行います。

struct E {
  std::string a;
  std::string b;
};

void foo() {
  // aの初期化子 -> bの初期化子の順で評価され、初期化される
  E e{.b = "bbb", .a = "aaa"};

  // 上の初期化はこう書いた時と同じになる
  E e{.a = "aaa", .b = "bbb"};
}

各初期化子(指示子)はその登場順ではなく、初期化しようとしているクラス型のメンバ変数の宣言順に評価されます。これはリスト初期化における初期化子評価順序とは一貫しません。

ただし、コンストラクタ初期化子リストにおけるこれと同様の振る舞いはしばしばC++の罠として知られています。集成体初期化においてはあるメンバの初期化のために同じクラス内の別のメンバを使用できないため危険性は低いですが、メンバのコンストラクタ次第では予期しない結果をもたらす可能性があります。

この動作は既にclangにおいて実装されています(警告はでる)。

著者の方は、A+Bを最も望ましいとしており、時点でA, B, Cの順で優位としています(他の組み合わせは意味がなく、組み合わせないのならばこの順で悪影響が少ないため)。

次の表は、4つのオプションを比較したものです

はい、各ソリューションを比較した表をMarkdown形式で出力します。

比較項目 (A)+(B) (A) (B) (C)
すべての型で順不同の指定初期化子が利用可能か ⚠️1 or 2 ⚠️2 ⚠️1
フィールドの破棄順序が宣言の逆順を維持するか
フィールドの破棄順序が構築の逆順を維持するか ✅3 ✅3
フィールドの構築順序が宣言順を維持するか ✅3 ✅3
リスト初期化の評価順序を維持するか
順不同の初期化子で一時オブジェクトが作成されるか ⚠️4
Clangに実装されているか ❌️ ❌️ ❌️ ✅5

注釈の意味

  1. 非参照型の(順不同で指示子が指定された)メンバ変数がムーブ構築可能である場合に限る
  2. 集成体型がトリビアルデストラクタを持つ場合に限る
  3. トリビアルデストラクタを持つ型では変更されるが、観測不可能
  4. 非自明なデストラクタを持つ型のみ
  5. 拡張機能として。警告を発するが使用可能

この提案ではA+Bのオプションを推しているものの、提案の目的は可能な設計空間を探り、潜在的な需要を測ることを目的としているため、厳密には何も提案していません。

P3406R0 We need better performance testing

ゼロオーバーヘッド原則をより重視すべき、という提言。

ここでは、過去の機能の設計(符号なし整数型の採用、範囲for、RTTI、例外、など)を例に挙げて、ゼロオーバーヘッド原則をより重視していれば今とは違った理解や設計が得られた可能性がある事を紹介し、ハードウェアがかなり高性能化して効率の問題は解消された現在においても、C++の長所は難しいタスクを効率的かつエレガントに実行できることであり、この長所を失ってはならない、としています。

そのためここでは、主に委員会における提案とその議論について、その提案をゼロオーバーヘッド原則の観点から議論した文書が添付され、より理想的には測定に基づいたものであるようにすることを提案(提言)しています。

この提案に基づくと、言語・ライブラリ両方で、新機能の提案は、その一般的な使用法においては現在のワークアラウンドや代替機能案と比較してオーバーヘッドが発生せず、最適化やパフォーマンスチューニングを妨げるものでもないことを示す必要があります。

本来的には、ゼロオーバーヘッド原則と新機能との関連を突き詰めることはパフォーマンスだけの問題ではなく、一般的なユースケースがなんであるか、それをコードとしてどう表現するのが最適なのか、といった議論を明確にすることにも繋がります。そのため、ゼロオーバーヘッド原則への準拠・影響は、それ自体が重要な設計ポイントであるはずです。

P3407R0 Make idiomatic usage of offsetof well-defined

C++におけるoffsetofマクロのCとの非互換を解消するために、C++におけるポインタの制限を緩和する提案。

2重リンクリストのような侵入的なデータ構造は、C++では継承を使用することで簡単に書くことができますが、クラスとその継承の概念を持たないCにおいてはコンポジションとポインタ演算を使用して実装されます。そこではoffsetofの典型的な使用法を見ることができます

struct ListNode {
  struct ListNode* prev;
  struct ListNode* next;
};

typedef struct {
  int data;
  struct ListNode node;
} Foo;

Foo* next_foo(Foo* foo) {
  struct ListNode* next_node = foo->node;

  // Foo::nodeのアドレスからそれを包含するFooへのポインタを得る
  return (Foo*)((char*)next_node - offsetof(Foo, node));  // C++17以降、UB
}

あるサブオブジェクトを指すポインタをchar*にキャストし、そのサブオブジェクトのそれを包含する構造体内でのオフセット値を求め、それらの減算によって囲む型へのポインタを得るこのイディオムは、container_ofあるいはよく似た名前のマクロとしてまとめられ、よく使用されています。

とはいえこのイディオムは、C++17以降C++でのみ未定義動作となります。これには2つの理由があります。

1つは、(char*)next_nodeのようにchar*へポインタをキャストしても、charの配列を指すポインタが得られないため、その配列内をシークすることを意図したコードはUBになります。

2つ目は、P0137R1で導入されたポインタの到達可能性の制約によって、このようにサブオブジェクトからそれを包含する型のポインタを得る、といったことができないことです。

とても簡単に言うと、C++17以降のC++のポインタモデルでは、クラスのポインタからそのサブオブジェクト(より内側)のポインタへの変換、あるいは同じ配列内の要素のポインタから別の要素のサブオブジェクトのポインタへの変換は、外側のポインタとpointer-interconvertibleなポインタからでもより内側のポインタへは到達可能とされます。

一方で逆向きの変換、すなわちクラスのサブオブジェクトのポインタからその外側のクラスへのポインタ、あるいは配列の要素そのもののポインタからそれを包含する配列オブジェクトへのポインタ、への変換は(ほとんどの場合)pointer-interconvertibleではないことから到達可能ではないため、ポインタの変換は許可されずそのようなポインタの間接参照は未定義動作となります。

例えば次のようなコードにおいて

struct S {
  int a[2];
  int data;
};

void f1(int* p);

int f2() {
  S s;
  s.data = 1;
  
  f1(&s.a[0]);

  return s.data;  // 1を返す
}

// f1()が次のように定義されている場合
void f1(int* p) {
  reinterpret_cast<S*>(reinterpret_cast<int (*)[2]>(p))->data = 2;
}

この時、f1()の呼び出しは未定義動作となります。なぜなら、配列オブジェクトs.a全体へのポインタはその要素s.a[0]のポインタとpointer-interconvertibleではないため、f1()における内側のreinterpret_castは型はint (*)[2]でありながら依然としてs.a[0](単一のint要素値)を指すポインタであり続け、それをさらにS*reinterpret_castしてもまた型が間違っているポインタ(s.a[0]を指すS*型のポインタ)が生成されます (pointer-interconvertibleなのは、標準レイアウトにおいて最初のメンバのポインタから囲むクラスのポインタへの変換、つまりs.aのポインタからSのポインタへの変換のみであり、s.a[0]のポインタからSのポインタへの変換は許可されない)。このようなポインタの間接参照は未定義動作となります。

そのため、コンパイラf1(&s.a[0])の呼び出しにおいてs.dataが変更されることは無いと仮定でき(その仮定が破られればUBなので)、さいごのreturn文は1を返します。

1つ目の問題はP1839R6(まだ提案中)にて対処されていますが、2つ目の問題の対処は意図的に避けられていました。この提案は2つ目の問題を解決しようとするものです。

複雑な提案ですが、簡単にまとめると変更は次の2点のようです

  • P1839R6ではオブジェクト表現をunsigned char(あるいはstd::byte)の配列として読み取る(それらのポインタによってオブジェクト内の任意の位置へアクセス可能なポインタを生成できる)事を許可しようとしているが、この提案ではそれにcharを追加する
    • container_ofマクロは典型的にはchar*が使用されているため
    • char*のポインタによってオブジェクト表現をトラバースできるようになる
  • P1839R6ではサブオブジェクトが独自のオブジェクト表現を持つことを提案しているのに対して、この提案では全ての完全なオブジェクトは独自のオブジェクト表現を持つことを提案している
    • これによって、サブオブジェクトのオブジェクト表現は不要になり、オブジェクトのどこか一部を指すcharのポインタからオブジェクト内の任意の位置を指すポインタを生成でき、またそのアクセス(値の読み出し)が行えるようになる
    • pointer-interconvertibleの定義をこのことを考慮して変更することで、サブオブジェクトへのポインタからそれを包含する型のポインタへの変換が到達可能になる

この提案の影響はとても簡単には、次のようなコードにおいて 

struct S {
  int x;
  int y;
};

void f4(int* p);

int f3() {
  S s;
  s.x = 1;
  
  f4(&s.y); // s.yのポインタを介してs.xのポインタに到達可能になるため、s.xが変更されうる

  return s.x * s.x; // 1ではないかもしれない
}

f4()s.xを変更する可能性がある、ように(コンパイラが認識するように)しようとするものです。これによって、冒頭のoffsetofの典型的な使用例はC++でも未定義動作ではなくなります。

P3409R0 Enabling more efficient stop-token based cancellation of senders

stop_tokenを用いたsenderのキャンセルをより効率化する提案。

std::executionsenderアルゴリズムでは、stop_token(正確にはstoppable_tokenコンセプトを満たす型)を用いた非同期操作のキャンセルをサポートしています。それをサポートするsenderでは接続時にrecieverの環境からのクエリによってstop_tokenを取得し、stop_callbackによってキャンセル時の処理を登録します。senderアルゴリズム|によっていくつもチェーンされている場合、各段の(キャンセルをサポートする)senderがそれぞれstop_callbackを登録していくことになります。

std::executionではデフォルトのstop_tokenとしてstd::inplace_stop_token(動的割り当てを回避したstop_token)が使用されていますが、この型は任意個数のコールバックの登録を受け付けるために双方向連結リストを内部に持っており、登録と解除に当たっては何らかの同期(ミューテックスやスピンロックなど)を必要とするため、登録と解除にはオーバーヘッドがかかります。

また、このキャンセル時コールバックは非同期操作がキャンセルされることなく完了した場合にはその外側のoperation_stateが破棄される前に解除しておく必要があります。キャンセルが稀であり、ほとんどの場合その操作はすぐに終了することが分かっているような操作の場合、キャンセルサポートのためにこのコールバックを登録・解除するオーバーヘッドの影響は相対的に大きくなります。

非同期操作の全体で有限個の子操作があり、各操作がそれぞれ1つだけコールバックを登録することが予め分かっている場合、より効率的なstoppable_tokenの実装を選択できる可能性があります。

この提案はstd::executionに対して、そのような効率的なstoppable_tokenを使用できるようにするとともにその実装を追加し、既存の一部のアルゴリズムをそれを使用するように変更するものです。

この提案の変更は次のものです

  1. std::get_stop_token()が最大一つのstop-callbackしかサポートしないstoppable_token型を返すことを許可する
  2. 最大一つのstop-callbackしかサポートしないstd::single_inplace_stop_source型を定義
  3. それぞれが最大一つのstop-callbackしかサポートしないN個の個別のstop-tokenを提供する、std::finite_inplace_stop_source<N>型を定義
    • stop-sourceを介して停止要求を送ると、N個のstop-token全てに送信される
  4. split()アルゴリズムを変更して、std::inplace_stop_sourceの代わりにstd::single_inplace_stop_sourceを使用するようにする
  5. when_all()アルゴリズムを変更して、std::inplace_stop_sourceの代わりにstd::finite_inplace_stop_sourceを使用するようにする

現状のデフォルトのstd::inplace_stop_sourceではこれに対して任意個数のコールバックを登録することができますが、この提案による制限を後から追加すると、それを前提として書かれたsenderアルゴリズムへの破壊的変更となるほか、使用するstopソースをstd::single_inplace_stop_sourceへ変更するとoperation_state型のレイアウト変更を招き、これも破壊的変更となります。このため、この提案では、これをC++26に導入することを推奨しています。

P3411R0 any_view

viewを型消去するためのviewany_viewの提案。

C++23まででRangeライブラリが大幅に強化されており、Rangeアダプタによって範囲に対する操作をより直観的に記述することができるようになっています。

// in MyClass.hpp
class MyClass {
  std::unordered_map<Key, Widget> widgets_;
public:
  auto getWidgets() {
    return widgets_ | std::views::values
                    | std::views::filter([](const auto&){ /*...*/ });
  }
};

これは非常に便利な機能ですが、テンプレートを駆使して高度な実装がなされているために、テンプレートパラメータを介してパイプライン化された各アダプタや元の範囲などの実装詳細が露出しており、特に、カプセル化の境界にあるインターフェースでは実装詳細が漏洩するという欠点があります。この例では、戻り値型(autoで推論されているその実際の型)を通してそれが利用者側に漏れています。

大規模なコードベースなどでは、翻訳単位を分離しソースファイル内に依存関係を閉じ込めることでソースコード間の依存関係を最小化し、並列コンパイルやインクリメンタルビルドを効きやすくすることが良く行われますが、Rangeライブラリのview型は型名にその内部情報が全て乗ってしまうため、それを妨げコンパイル時間を増大させます。

例えば上の例のgetWidgets()の戻り値型は次のような型になります

std::ranges::filter_view<
  std::ranges::elements_view<
    std::ranges::ref_view<std::unordered_map<Key, Widget>>,
    1>,
  MyClass::getWidgets()::<lambda(const auto:11&)> >

このような型は一度記述するだけでも難しいのですが、種類が増えたり使う場所が増えたり、またコードが変更されたりすることによって維持が困難になります。

テンプレートメタプログラミングにおいてはこれと同じようなことがしばしば問題になり、その解決方法の一つとして型消去という方法が知られています。std::functionstd::anystd::span等標準ライブラリには特定の特性を持つ型を受け入れることのできる型がいくつかあります。

この提案では、Rangeライブラリのview型専門の型消去ラッパ型であるany_viewを提案するものです。

型消去ラッパは一見インターフェースが簡素でも、std::fucntionとそのファミリに見られるように設計選択ポイントがいくつもあり、ユーザーに変わって多くの事を決定する必要があります。コピー可能性やムーブ可能性だけでなく、Rangeの場合はRangeのカテゴリに加えてその参照/値型、borrowed・common・sized等の多くの性質についてを決定する必要があります。

そのために、ここで提案するany_viewは、Boost.RangeやRange-v3にある類似機能の設計を参考にして、次のような構造になっています

// any_viewの性質を指定する列挙値
enum class any_view_options {
  input = 1,
  forward = 3,
  bidirectional = 7,
  random_access = 15,
  contiguous = 31,
  sized = 32,         // size_rangeとするかどうか
  borrowed = 64,      // borrowed_rangeとするかどうか
  move_only = 128     // ムーブオンリーとするかどうか(デフォルトはコピーオンリー
};

// any_view本体
template <class Value,
          any_view_options Opts = any_view_options::input,
          class Ref = Value &,
          class RValueRef = add_rvalue_reference_t<remove_reference_t<Ref>>,
          class Diff = ptrdiff_t>
class any_view {
  class iterator; // exposition-only
  class sentinel; // exposition-only

  ...
  
public:

  template <class View>
    requires(!std::same_as<View, any_view> && std::ranges::view<View> &&
             view_options_constraint<View>())
  any_view(View view);

  any_view(const any_view &) requires (!(Opts & any_view_options::move_only));

  any_view(any_view &&) = default;

  any_view &operator=(const any_view &) requires (!(Opts & any_view_options::move_only));

  any_view &operator=(any_view &&);

  iterator begin();
  sentinel end();

  size_t size() const requires(Opts & any_view_options::sized);
};

template <class Value, any_view_options Opts, class Ref, class RValueRef,
          class Diff>
inline constexpr bool
    enable_borrowed_range<any_view<Value, Opts, Ref, RValueRef, Diff>> =
        Opts & any_view_options::borrowed;

any_view_options列挙値によってany_viewの性質を指定します。any_view_optionsはビットマスクになっており、|(本来の使い方)によって欲しいany_viewの性質を指定することができます。例えば

using MyView = std::ranges::any_view<Widget, std::ranges::any_view_options::bidirectional | std::ranges::any_view_options::sized>;

のようにすると、MyViewは値型がWidget、参照型がWidget&bidirectional_rangeかつsized_rangeになり、そのような範囲についてその具体的な型によらずに保持できます。これを使用して先ほどの例の戻り値型を指定することができます。

// in MyClass.hpp
class MyClass {
  std::unordered_map<Key, Widget> widgets_;
public:
  MyView getWidgets() {
    return widgets_ | std::views::values
                    | std::views::filter([](const auto&){ /*...*/ });
  }
};

このような使われ方からも分かるかもしれませんが、any_viewは受け取った範囲を所有して保持します。とはいえこの例では、大本の範囲(widgets_)はref_viewを通して参照されているため、あくまで保持されているのはRangeアダプタの部分だけではあります。このラップ対象viewを所有するという性質から、ムーブオンリーviewcontiguous_rangeサポートが必要とされます。

そのほかの性質としては

  • const-iterable: ❌
    • filter_viewdrop_while_viewなどの、キャッシュを使用するviewが使用できなくなるため
    • any_view_optionsを通してサポート可能ではあるが、実装が複雑化するため提案しない
  • common_range: ❌
    • 設定で有効化すると、標準のviewがほとんど使用できなくなるため
    • 実装が複雑化する事を回避
    • any_view<...> | views::commonを使用することを推奨
  • constexpr対応: ❌
    • SBO許可のため
  • 空の状態: なし
    • swap、コピー代入、ムーブ代入、ムーブ構築には強い例外安全性保障を提供する
    • ラップ対象のview型のこれらの操作がnoexceptではない場合、ヒープに保持することで危険な操作の呼び出しを回避する

また、提案ではany_viewのパフォーマンスについてベンチマーク結果とともに詳しく報告されています。

P3412R0 String interpolation

std::format/std::print向けの引数となるフォーマット文字列と対象引数列の組を生成する、文字列補完リテラルの提案。

C++20で導入されたstd::formatでは、pythonの文字列フォーマット機能をベースにした便利な文字列フォーマットが使用でき、同様のフォーマット文字列構文がそのままstd::printでも使用できます。

int calculate(int);

std::string stringify(std::string_view prefix, int bits) {
  return std::format("{}:{}: got {} for {:#06x}", prefix, __LINE__, calculate(bits), bits);
}

void display(std::string_view prefix, int bits) {
  std::print("{}:{}: got {} for {:#06x}", prefix, __LINE__, calculate(bits), bits);
}

しかし、このフォーマット文字列による指定はまだ改善の余地があり、文字列内にフォーマット対象の式を直接埋め込むことでより直接的かつシンプルに指定できるようになります。これはpythonでもf-stringsという機能として広く使用されています。この提案は、そのC++版であるfリテラルを提案するものです。

fリテラルによって、上記の例は次のように書き直せます

int calculate(int);

std::string stringify(std::string_view prefix, int bits) {
  return f"{prefix}-{__LINE__}: got {calculate(bits)} for {bits:#06x}";
}

void display(std::string_view prefix, int bits) {
  std::print(f"{prefix}-{__LINE__}: got {calculate(bits)} for {bits:#06x}");
}

fリテラルによって、どの引数(フォーマット対象の式)を文字列のどこに入れたいのかが明確になるとともに、フォーマット文字列と引数が分かれていることによる冗長性も削減されています。

これと同等の機能は多くのプログラミング言語に存在しており、便利に使用されています。

ここでのfリテラルは文字列リテラルの一種であり、文字列補完処理の大半はプリプロセッサによって処理されます。プリプロセッサでは、各fリテラル文字列をパースして、{}内の文字列を抽出した後文字列そのものは取り除き、抽出した文字列は通常のトークンとして抽出され、それらのものはstd::make_formatted_string()の引数として渡されて、fリテラルstd::make_formatted_string()の呼び出しに置換されます。

// このfリテラル
f"{prefix}-{__LINE__}: got {calculate(bits)} for {bits:#06x}"

// こう展開される
std::make_formatted_string("{}-{}: got {} for {:#06x}", (prefix), (__LINE__), (calculate(bits)), (bits))

std::make_formatted_string()は新しく追加される関数で、このように受け取った引数からstd::basic_formatted_string<CharT, Args...>型のオブジェクトを返す関数です

// Defined in <format>
template<typename... Args>
auto make_formatted_string(std::format_string<Args...> fmt, Args&&... as) {
  return formatted_string<Args...>(fmt, as...);
}

template<typename... Args>
auto make_formatted_string(std::wformat_string<Args...> fmt, Args&&... as) {
  return formatted_string<Args...>(fmt, as...);
}

std::basic_formatted_string<CharT, Args...>型も新しく追加される型で、フォーマット文字列とフォーマット対象式の結果の値を保持しておく型です。

// Defined in <format>
template<typename CharT, typename... Args>
struct basic_formatted_string decays_to(basic_string) {

  basic_formatted_string(basic_format_string<CharT, Args...> fmt, const Args&... as)
    : args(make_format_args<__select_fc_t<CharT>>(as...)), literal(fmt.get())
  {}

  implicit operator string() const { return vformat(literal, args); }

  decltype(make_format_args<__select_fc_t<CharT>>(declval<const Args&>()...)) args;
  
  basic_string_view<CharT> literal; // Bike-sheddable!
};

この型の定義例では、未導入の機能(decays_to()implicit operator T())を含んでいますがこれはダングリングの防止と利便性向上のためのもので、それらのメリットは無くなるものの無しでも定義可能です。

std::basic_formatted_string<CharT, Args...>型はstd::stringへの変換演算子を備えているため明示的なstd::format()呼び出しを省略して同等のフォーマット済み文字列を得ることができます。

そして最後に、std::print()/std::println()ostream<<に対してこのstd::basic_formatted_string<CharT, Args...>を取るオーバーロードを追加し、std::format()の呼び出し無しで(中間のstd::string生成無しで)の出力をサポートします

// Defined in <print>
template<typename... Args>
void print(FILE* stream, const formatted_string<Args...>& fs);
template<typename... Args>
void println(FILE* stream, const formatted_string<Args...>& fs);

template<typename... Args>
void print(const formatted_string<Args...>& fs) { print(stdout, fs); }
template<typename... Args>
void println(const formatted_string<Args...>& fs) { println(stdout, fs); }
// Defined in <ostream>
template<typename CharT, typename... Args>
basic_ostream<CharT>& operator<<(basic_ostream<CharT>& os, const basic_formatted_string<CharT, Args...>& fs) {
  vformat_to(ostream_iterator<CharT, CharT>(os), fs.literal, fs.args);
  return os;
}

template<typename... Args>
void print(ostream& os, const formatted_string<Args...>& fs) {
  os << fs;
}

template<typename... Args>
void println(ostream& os, const formatted_string<Args...>& fs) {
  os << fs << "\n";
}

fリテラルstd::format()の呼び出しに展開されずにこのように少し遠回りをすることで、中間std::stringが必要になることによるパフォーマンスの低下問題を回避しています。

リテラル各部の名称

  f"The result is { get_result() :{width}.3}"
// ^~~~~~~~~~~ f-string-literal ~~~~~~~~~~~~^
// or f-literal

  f"The result is { get_result() :{width}.3}"
//                ^~~~~~~~~~~~~~~~~~~~~~~~~^
//                       |
//                extraction-field

  f"The result is { get_result() :{width}.3}"
//                 ^~~~~~~~~~~~^  ^~~~~~~~^
//                       |           |
//           expression-field      format-specifier

  f"The result is { get_result() :{width}.3}"
//                                 ^~~~^
//                                   |
//                        nested-expression-field

fリテラルから式として抽出されるのはexpression-fieldのみです(ネストしたものも含めて)。これにより、{expr:fomart-option}の様に記述しておくことでフォーマットのオプションを指定しておくことができます。

その他細かい仕様

  • 抽出された各expression-field()で囲まれてstd::make_formatted_string()の引数として渡される
  • expression-field内でのプリプロセッシングディレクティブの使用は禁止
  • 他の文字列リテラルに対するプリフィックスとの混合をサポート
    • Lf"..."u8fR"..."など
    • ただし、u, U, u8std::fomrat()がサポートしていないので意味がない
  • ユーザー定義リテラルはサポートしない
    • 考慮されずに、make_formatted_string()の呼び出しの後に付加される形になる(エラーになるはず
  • fリテラル中のでマクロ展開は、expression-field内部でのみ通常通り処理される
    • 展開はfリテラルの処理の前に行われる
    • ただし、fリテラルの終了or区切りの}:はマクロ展開によって導入できない
  • fリテラルと文字列リテラルの連結はサポートされ、抽出された式は全ての文字列リテラルを連結したものの後に配置される形になる

また、pythonのf-stringsでサポートされている変数名=の形のフォーマット指定を特別扱いする機能もサポートされる予定です

f"{x=}";

// このように置換される
std::make_formatted_string("x={}", x);

これについては&MyClass::operator=のような指定がある場合と被るという問題がありますが、最後の=の一つ前のトークンがoperatorではない場合にのみこの機能が有効化されます。

P3413R0 A more flexible optional::value_or (else!)

optional/expected.value_or()を補完する関数の提案。

この提案はP2218R0の引継ぎとなる提案です。基本的なモチベーションや設計はそのままに、std::expectedに対しても同様の提案をしています。

ここで提案されているのは

  • .value_or_construct()の追加
    • 有効値を保持していない場合、引数の値から有効値のオブジェクトを構築して返す
  • .value_or_else()の追加
    • 有効値を保持していない場合、引数のcallableを呼び出してその戻り値を返す
      • .value_or_construct()の遅延評価版

の2点です。

std::optional<T>std::expected<T, E>に対して、次のようなシグネチャで追加しようとしています

template<class ... Args>
constexpr T value_or_construct( Args &&... args ) const &;
template<class ... Args>
constexpr T value_or_construct( Args &&... args ) &&;

template<class U, class ... Args>
constexpr T value_or_construct (initializer_list <U> il, Args&&... args>) const &;
template<class U, class ... Args>
constexpr T value_or_construct (initializer_list <U> il, Args&&... args>) &&;

template <class F>
constexpr T value_or_else (F&& f) const &;
template <class F>
constexpr T value_or_else (F&& f) &&;

P3415R0 Range interface in std::optional breaks code!

std::optionalrange化を元に戻す提案。

P3168R2ではstd::optionalrangeコンセプトを満たすようにすることで範囲for文で使用可能になるようにしています。すでにこの提案はC++26に向けて採択済みです。

この提案では、これが既存のコードを壊しているとしてrevertするように提案しています。

std::optional<T>には2つのモデルがあり、人によってどちらに捉えるかが異なっていながら、これがきちんと認識されていません。

  1. Tのコンテナとしてのモデル
    • std::optionalを0または1個の要素を持つコンテナとして見るモデル
  2. T + nulloptとしてのモデル
    • std::optional<T>Tの要素の集合に無効値を表す追加の値を添加した集合を表す型としてのモデル

このことは現在でも混同されています。この2つのモデルはある程度は共存できるものの特定の状況下では矛盾が生じています。その一例として、提案では比較可能性を挙げています(コンテナとその要素型の値は直接比較できないが、std::optionalではできてしまう。これはコンテナとしてのモデルの観点からは予想外)。range化することで、この2つのモデルの乖離をさらに広げる事になります。

これはメンタルモデルからの解釈的な話ですが、より実際の影響として大きいのは、std::optionalが突然rangeになることによって既存のコードの動作が変更されることです。これはstd::formatにおいて標準ライブラリ自体にも存在(フォーマッタ特殊化によってエラーになるようにしている)していますが、またBoost.JSONで起こりうるとして報告されています

std::optional<int> o1 = 16;
json::value v1 = json::value_from(o1);             // (1) store optional as JSON value (integer)
std::string s = json::serialize(v1);               // (2) serialize to text: `16`
json::value v2 = json::parse(s);                   // (3) parse text to JSON value (integer)
auto o2 = json::value_to<std::optional<int>>(v2);  // (4) convert JSON value integer to optional

Boost.JSONでは、C++コードの値をJSONの値に変換する際に、オーバロード解決に基づいた分類と判定を行っています。C++コードで範囲であると判定された型はJSONの配列としてエンコードされ、そうではない値として判定されたものはJSONで非配列の値としてエンコードされます。Boost.JSONでは配列の判定の方が先に行われているため、std::optionalrange化するとこの判定が壊れる可能性があります。

この変更は意味論の変更であり、その影響はかなり深刻なものになります。在野のコードでも同様の問題は多数起こるで事が予想され、この非互換性の影響を吸収するのはC++標準ではなく既存コードベースを管理している人々です。

この変更の動機の一つとしては、if文の代わりにforを使用することにありますが、それはあまりメリットではなく、その目的にはパターンマッチングの方が適しています。したがって、この既存コードへの影響のコストにメリットが見合わない、としています。

この提案はLEWGのレビューにおいてリジェクトされています。

P3416R0 exception_ptr_cast: Add && = delete overload

提案中のexception_ptr_castに右辺値を受け取るオーバーロードdelete定義する提案。

exception_ptr_cast()はP2927R2で提案中の、exception_ptrから例外オブジェクトの取得を試みる関数です。

// 現在の宣言
namespace std {
  template<class T>
  const T* exception_ptr_cast(const exception_ptr& e) noexcept;
}

現在の関数定義はexception_ptrconst左辺値参照を受け取るようになっていて、ここには右辺値を渡すことができてしまいます。そしてそれにはuse after freeの危険があります。

if (auto *ex = std::exception_ptr_cast<std::system_error>(std::current_exception())) {
  handle(ex->code); // <--- Use after free!!!
}

std::current_exception()は現在の例外オブジェクトを指すexception_ptrを取得する関数であり、戻り値はprvalueで返されるためそのままexception_ptr_cast()に渡すことができます。

そして、std::current_exception()は例外オブジェクトをコピーして保持するexception_ptrを帰すことが許可されています。するとその場合、そのコピーされた例外オブジェクトはそのexception_ptrのデストラクタで破棄されてしまいます。

Windowsでは例外オブジェクトがスタック上に配置される都合から、exception_ptrは例外オブジェクトのコピーを保持しており、std::current_exception()はそのようなexception_ptrを作成して返します。上記のコードではifの中に入った時にはすでにstd::current_exception()の返したexception_ptrの寿命は尽きており、それが保持していた例外オブジェクトのコピーを指していたexはダングリングポインタになっています(この時、大本の例外オブジェクトはまだ生存しています)。

このような危険な誤用を防ぐために、exception_ptr_cast()に右辺値参照を取るオーバーロードを追加したうえでそれをdelete定義しておくことを提案しています。

namespace std {
  template<class T>
  const T* exception_ptr_cast(const exception_ptr& e) noexcept;

  // 追加する宣言
  template <class E>
  void exception_ptr_cast(const exception_ptr&&) = delete;
}

P3417R0 Improving the handling of exceptions thrown from contract predicates

契約条件式の評価中に送出された例外と、そうでない例外を区別して違反ハンドラで処理するようにする提案。

P2900R9のContracts MVP仕様では、契約条件式の評価中に例外が送出された場合でも違反ハンドラが起動し、違反ハンドラ内でstd::current_exception()を使用してその例外を処理することができます。

しかし、std::current_exception()は例外の送出元に関係なく現在の例外を取得するため、その判定には追加の面倒なコードが必要になります。P3227R0(上の方)ではこれを解消するために、契約条件式の評価に伴って送出された例外を取得するための専用の関数をcontract_violation型に追加することを提案しています

void handle_contract_violation (contract_violation& violation) {
  // 現在の契約の評価に関する例外があればそれを取得する
  if (auto ex = violation.evaluation_exception()) {
    my::handle(ex);
  }
}

これによってライブラリの使用感は改善されますが、ここにはまだ問題があります。

違反ハンドラの中からはstd::current_exception().evaluation_exception()も両方使用可能であり、契約条件式の評価に伴って例外が送出された場合は両方ともその例外オブジェクトを指すexception_ptrを取得します。しかしそうではない場合、すなわち別の例外の処理中に(catch節の中で)違反ハンドラが呼ばれている場合、std::current_exception()はその例外オブジェクトを指すexception_ptrを返しますが、.evaluation_exception()nullexception_ptrを返します。

この動作は契約条件式の評価に伴う例外を例外スタック上で個別に扱うことを暗に示しており、P2900の原則である契約条件式の評価はプログラムの状態に影響を与えない、というものに反している可能性があります。

それだけでなく、違反ハンドラ内でcurrent_exception()を再スローすると何が起こるか?という疑問が生まれます

void handle_contract_violation (contract_violation& violation) {
  if (auto ex = std::current_exception()) {
    std::rethrow_exception(ex); // or just `throw;`
    // what happens now?
  }
  
  ...
}

P2900の仕様では、契約条件式の評価に伴って例外が送出されていた場合はその例外を再送出し、そうでない場合(別の例外を処理するcatch節の中で違反が起きた場合)はそこで処理中だった全く関係のない例外が送出されます。このことは、違反ハンドラからの例外送出という1つの動作に対して、2つの異なるユースケースがそれを行う可能性があることを示しています。

catch節の中で契約違反が起きた場合に、そのcatch節はもはや続行不可能であるとして、違反ハンドラから例外送出(再スロー)して、より上位のユーザーコードのハンドラに処理を委ねる、という場合があり得ます。この場合、その例外のコンテキストは違反ハンドラからは不明です。

一方で、契約条件式の評価に伴って例外が送出された場合に、その例外を特定の方法で処理するために違反ハンドラから再スローする、という場合があります。この場合、その例外のコンテキストを違反ハンドラは分かっています(すなわち、契約条件式の評価に失敗したことを表す例外)。

違反ハンドラから送出されうるこれら2種類の例外は、その性質が異なっているため、異なる処理戦略が必要なはずです。そのため、それぞれの例外の送出は異なる方法で行われる必要があります。

この対処(2つの種類の例外の区別)のためにこの提案では、P2900の言語仕様への変更を提案しています。具体的には、契約条件式の評価中に送出された例外のハンドラとして契約違反ハンドラを呼び出すのではなく、契約条件式の評価中に送出された例外は違反ハンドラの呼び出し前に一旦処理されたものとして扱うことを提案しています。

これにより、P2900R9の契約違反処理プロセスの疑似コード

// 各契約アサーションはこのようなコードによって処理されることを表す
bool violation = false;

try {
  // predicateは契約条件式の結果
  violation = !predicate;
}
catch (...) {
  // 契約条件式の評価中に例外が送出された場合
  // detection_modeを`evaluation_exception`にセット
  handle_contract_violation(...);
}
if (violation) {
  // 契約違反が起きた場合
  // detection_modeを`predicate_false`にセット
  handle_contract_violation(...);
}

は、この提案によって次のように変更されます

bool violation = false;
try {
  _violation = !predicate;
}
catch (...) {
  // 契約条件式の評価中に例外が送出された場合
  violation = false;
  // 現在の例外をevaluation_exception()から取得できるようにしておく
}
if (violation) {
  handle_contract_violation(...);
}

この上で、違反ハンドラ内での通常の例外の再スロー(上記コードやthrow;)は契約条件由来ではない現在処理中の例外を再送出します。一方、契約条件式の評価からの例外を再送出する場合には、次のようにP3227R0の機能を使用して開発者の意図を明示して行う必要があります

std::rethrow_exception(violation.evaluation_exception());

これにより、実装はこの提案のセマンティクスの変更を上記疑似コードのように実装するか、例外スタック上で異なる例外として扱うかを選択することができます。しかしその実装戦略に関係なく、ユーザーから観測される動作は同じになり、契約条件式由来の例外を通常のものとは区別して扱うことで一貫するようになります。これは現在のP2900R9の設計原則にも則っているはずです。

P3419R0 Reflection Syntax Options Summary

リフレクション構文候補の利点欠点をまとめた文書。

P3381R0では^に代わる構文候補が探索され、最終的には^^が選択されました。この文書では、スプライスなど他の構文も含めたすべての構文の考えられる候補をリストアップし、利点欠点の比較を行っています。

P3420R0 Reflection of Templates

テンプレートそのものに対するリフレクションの提案。

P2996R7で提案中の静的リフレクション機能では、C++の基本的なエンティティに対するリフレクションとスプライスをサポートしていますが、テンプレートそのもの(インスタンス化前のテンプレート)に対するリフレクションはサポートされていません。C++の既存コードベースでテンプレートが多用されていることを考えるとこのことはリフレクション機能に対する大きな制約となります。

インスタンス化済みのクラステンプレートであっても、そのメンバ関数にはテンプレートメンバ関数が含まれていることがほとんどであるため、インスタンス化済みのクラステンプレートに対してのリフレクションをサポートするだけでは不十分です。例えば、std::vector<T>はコンストラクタテンプレートに加えてinsertemplace_backなどのメンバ関数テンプレートが定義されています。したがって、C++宣言に対するリフレクション機能が有用であると考えられる限り、テンプレートのリフレクションは不可欠です。

また、リフレクションの本質的な目標として、型のコピーを作成する事の出来るidentity()というメタ関数があります。この関数はクラス型を受けてその意味的に同一なコピーを作成するという単純なものですが、(出力がリフレクションであることによって)その複製に対して何らかの変更(名前の変更やメンバの追加削除など)を加えるという重要な機能を提供します。

class Widget {
  int a;
public:
  template <class T> requires (std::is_convertible_v<T, int>)
  Widget(const T&);
  const Widget& f(const Widget&) const &;
  template <class T>
  void g();
};

// identity()による型コピー
consteval {
  identity(^^Widget, "Gadget");
}

// Same effect as this handwritten code:
// class Gadget {
//   int a;
// public:
//   template <class T> requires (std::is_convertible_v<T, int>)
//   Gadget(const T&);
//   const Gadget& f(const Gadget&) const &;
//   template <class T>
//   void g();
// };

このようなメタ関数をライブラリサイドで(言語組み込みではなく)実現するためには、リフレクションによって型をその構成要素に分解するとともに、それを異なるコンテキストで再構成するという能力が必要になります。これはリフレクションの完全性(C++コードのあらゆるものを読み取り、生成する能力がある状態)の証明であり、多くの応用が想定されます。

このような機能を実現するためには、全てのテンプレート(クラステンプレートとその特殊化、関数テンプレート、変数テンプレート、エイリアステンプレート)のリフレクションが必須となります。しかし、テンプレートそのものはテンプレートパラメータが確定する前は部分的にしか意味解析が完了しておらず、テンプレートパラメータに依存する部分を何らかのリフレクションとして得ることについては特有の課題があります。

現在のP2996で提案されているリフレクション機能は非テンプレート(インスタンス化済みのテンプレート)を対象としたものであるため、テンプレートに対して使用することができません。この提案は、P2996をベースとしてテンプレートに対するリフレクションを有効化する設計を提案するものです。

多くのテンプレートはテンプレートそのものの定義時には意味解析が完了していないことからリフレクション(std::meta::info)を得ることができないことを考慮して、この提案ではテンプレートの構成要素のリフレクションの表現としてP3294で提案中のトークンシーケンスを使用します。トークンシーケンスはC++のコード辺(トークン辺・列)のリフレクションであり、それが取得された段階ではまだ意味解析が求められず、トークンシーケンスをどこかに注入してC++コードを生成する際まで遅延されます。

このトークンシーケンスの性質はテンプレートのリフレクションの要求にマッチしており、トークンシーケンス機能はこの提案を実現するための重要な実現手段です。利点としては

  • 単純さ
    • C++テンプレートの宣言と置換および名前探索のルールは非常に複雑であり、テンプレートの個別の機能ごとにそれに特化したリフレクションを追加すると言語の複雑性が増大してしまう
    • 一方、トークンシーケンスはC++ソースコードから作成され、C++ソースコードで構成されるものであり、テンプレート宣言の任意の部分を表現するための単純かつ統一的で効果的な手段
  • 相互運用性
    • P2394で提案されているトークンシーケンスに関するすべての機能を活用することができる
  • コード生成機能
    • リフレクションの重要な側面は、既存の機能をコピーして何らかの要素を追加する新しいコードを生成することで、既存コードにフックできること
    • トークンシーケンスを利用することで、既存のテンプレートに対するそのような操作は、コード生成にトークンシーケンスを使用するのと同じ程度にシンプルで簡単になる
  • 実装の容易さ
    • トークンシーケンスは重いインフラや特定の実装設計を必要とせず、比較的簡単に実装できる
    • さらに、次に説明するAs-Ifルールによって、コンパイラに重要な自由度と裁量を与えることができる

等が挙げられています。

この提案は基本的に言語の変更を必要とせず、専用のメタ関数を多数追加する形で実現されています。それらのメタ関数のほとんどはトークンシーケンスを返します。しかしそこで問題となるのは、実装によってはコンパイルの早い段階でソースコードトークンシーケンスを棄てている場合があることです。その理由としては例えば

  1. ソースコード内で同じ意味を持ちながら異なる表記になっているものを正規化する
    • 例えば、template<class T>template<typename T>に置き換えたり、その逆を行ったりする
  2. トークン化の際に簡単なフロントエンド処理(これもある種の正規化)を行う
    • +1011に置き換えるなど
  3. 特定の型を早期に検索し、それらをその内部表現に置き換える
    • 定数畳み込みが完了し、デフォルト引数が置換された状態
    • ある種のキャッシュ引き当て
  4. テンプレートの解析を積極的に行い、早い段階でそのトークン列を破棄する

このようなことが行われるかは実装によって異なっており、いずれかでも行われている場合は元のソースコードに完全に一致するトークンシーケンスの生成は困難であり、それを要求してしまうと実装にかなりの負担が生じます。

このためにこの提案では、As-Ifルールとしてトークンシーケンスを返すメタ関数の実装に関する最小要求を規定します

特に重要なのは1つ目の要求で、ユーザコードではメタ関数から返されるトークンシーケンスを構成するトークン列が元のソースコードのものと完全に一致することを期待すべきではありません。唯一の保証は、そのトークンシーケンスをスプライスバック(あるいは注入)したときに、元のソースコードと全く同じ振る舞いをするということだけです。

この提案ではテンプレートのリフレクションに対して、そこからテンプレート内の各構成要素に対応するトークンシーケンスを取得するためのメタ関数群を提案しています。

提案する関数の宣言

namespace std::meta {
  //  Multiple explicit specializations and/or partial specializations
  consteval auto template_alternatives_of(info) -> vector<info>;

  // Template parameter normalization prefix
  constexpr string_view template_parameter_prefix;

  // Template parameter list
  consteval auto template_parameters_of(info) -> vector<info>;

  // Attributes - extension of semantics in P3385
  consteval auto attributes_of(info) -> vector<info>;

  // Template-level requires clause
  consteval auto requires_clause_of(info) -> info;

  // Is this the reflection of the primary template declaration?
  consteval auto is_primary_template(info) -> bool;

  // Arguments of an explicit specialization or partial specialization
  consteval auto specialization_arguments_of(info) -> vector<info>;

  // Bases of a class template
  consteval auto template_bases_of(info) -> vector<info>;

  // Type of a data member in a class template or of a variable template
  consteval auto template_data_type(info);

  // Type of a data member in a class template
  consteval auto template_data_initializer(info) -> info;

  // Declarator part of an alias template
  consteval auto alias_template_declarator(info) -> info;

  // Inline specifier present?
  consteval auto is_inline(info) -> bool;

  // cvref qualifiers and others - extensions of semantics in P2996
  consteval auto is_const(info) -> bool;

  consteval auto is_explicit(info) -> bool;

  consteval auto is_volatile(info) -> bool;

  consteval auto is_rvalue_reference_qualified(info) -> bool;

  consteval auto is_lvalue_reference_qualified(info) -> bool;

  consteval auto is_static_member(info) -> bool;

  consteval auto has_static_linkage(info) -> bool;

  consteval auto is_noexcept(info) -> bool;

  // predicate for `noexcept`
  consteval auto noexcept_of(info) -> info;

  // `explicit` present? - extension of P2996
  // predicate for `explicit`
  consteval auto explicit_specifier_of(info) -> info;

  // `constexpr` present?
  consteval auto is_declared_constexpr(info) -> bool;

  // `consteval` present?
  consteval auto is_declared_consteval(info) -> bool;

  // Function template return type
  consteval auto template_return_type_of(info) -> info;

  // Function template parameter normalization prefix
  constexpr string_view function_parameter_prefix;

  // Function parameters
  consteval auto template_function_parameters_of(info) -> vector<info>;

  // Trailing `requires`
  consteval auto trailing_requires_clause_of(info) -> info;

  // Function template body
  consteval auto body_of(info) -> info;
}

このように細かくテンプレートの情報を取得できるようにすることで、簡単に元の宣言に基づいたコードの調整を行うようなリフレクションコードを記述することができ、無限のカスタマイズ性を実現します。

提案文書より、任意の関数func()に対して、logged::func()を生成する例

consteval void make_logged(std::meta::info f) {
  queue_injection(^^{
    namespace logged {
      // copy_signature: fの表す関数(テンプレート)のシグネチャに対応するトークンシーケンスを生成する
      \tokens(copy_signature(f, name_of(f))) {
          // params_of: fの仮引数名のコンマ区切りリストのトークンシーケンスを生成
          logger("Calling " + name_of(f) + " with arguments: ", \tokens(params_of(f)));
          // make_fwd_call: fの表す関数に、その引数を転送して呼び出すトークンシーケンスを生成
          return \tokens(make_fwd_call(f));
      }
    }  // end manespace logged
  });
}

template <typename T>
  requires std::is_copy_constructible_v<T>
void fun(const T& value) { ... }

consteval {
  make_logged(^^fun);
}

// Equivalent hand-written code:
// namespace logged {
//     template <typename T>
//     requires std::is_copy_constructible_v<T>
//     void fun(const T& value) {
//         logger("Call to ", "fun", " with arguments: ", value);
//         return fun<T>(std::forward<T>(value));
//     }
// }

ここで使用されている3つのメタ関数copy_signature(), params_of(), make_fwd_call()はユーザーが作成する必要のあるメタ関数ですが、ここで提案されているメタ関数を用いると簡単に作成することができます。

using meta = std::meta;

consteval copy_signature(meta::info func_refl, meta::info new_name) {
  meta::info result = ^^{};

  // テンプレートパラメータ宣言をコピー
  auto template_params = meta::template_parameters_of(func_refl);
  if (template_params.empty() == false) {
    result += ^^{template<};
    
    for (auto param : template_params) {
      result += param;
    }

    result += ^^{>};

    // 前置requires節をコピー
    result += meta::requires_clause_of(func_refl);
  }

  // 属性をコピー
  result += meta::attributes_of(func_refl)

  // static/inlineをコピー
  if (meta::has_static_linkage(func_refl)) {
    result += ^^{static};
  }
  if (meta::is_inline(func_refl)) {
    result += ^^{inline};
  }

  // constexpr/constevalをコピー
  if (meta::is_declared_constexpr(func_refl)) {
    result += ^^{constexpr};
  } else if (meta::is_declared_consteval(func_refl)) {
    result += ^^{consteval};
  }

  // 後置戻り値型にしておく
  result += ^^{auto};

  // 関数名をセット
  result += new_name;

  // パラメータリストをコピー
  result += ^^{(};
  for (auto param : meta::template_function_parameters_of(func_refl)) {
    result += param;
  }
  result += ^^{)};

  // noexcept指定をコピー
  result += meta::noexcept_of(func_refl);

  // 後置requires節をコピー
  result += meta::trailing_requires_clause_of(func_refl);

  // 戻り値型のコピー
  result += ^^{->};
  result += meta::template_return_type_of(func_refl);

  // 関数本体をコピー
  result += meta::body_of(func_refl);

  return result;
}

P3421R0 Consteval destructors

デストラクタにconsteval指定をできるようにする提案。

std::vectorはリフレクションによってコンパイル時により活用されることがほぼ確実になっていますが、デストラクタがconstexprでしかないことによって、空のstd::vectorのデストラクタ実行が実行時に行われる可能性があり、この場合コンパイル時のポインタ値に対してdeleteを読んでしまう可能性があります。

constevalデストラクタがあればそのようなことは起こらず、コンパイルエラーとして検出されます。

また、クラスをコンパイル時にのみ動作するよう記述する場合、すべてのメンバ変数にconstevalを付加するかもしれません。しかしこの場合、デストラクタだけはconstexprでしかなくなり、そこからはconsteval関数を呼び出せないという問題に遭遇するかもしれません。constevalデストラクタによって、コンパイル時にのみ動作するクラス記述が簡単になります。

この提案は、P3295(constexprコンテナ)とP2996(静的リフレクション)の2つの提案によって動機づけられており、これが採択された場合P3295でフリースタンディング化を指定している多くの型のデストラクタがconstevalになる予定です。

P3422R0 Allow main function in named modules

名前付きモジュールでmain()を定義することができるようにする提案。

モジュールがある場合でもmain()関数はかなり特別扱いされており、main()を名前付きモジュールに属するようにする(モジュールファイル中で宣言する)事はできません。また、言語リンケージ(extern "C"など)によって、モジュールファイル内のmain()のリンケージを変更(グローバルモジュールに属するようにする)することもできません。

これによって起こる不便なことの一つは、名前付きモジュールに属するエクスポートされていないもののテストを記述する際に、そのモジュールの一部として記述することができないことです。

/// モジュールAのプライマリインターフェース単位
export module A;

// エクスポートされていないクラス
class non_exported { ... };

export template <class C>
class exported {
  non_exported ...;
  ...
};

/// モジュールAの実装単位
module A; // or `module A:test_non_exported;` to make it more expressive.
int main() {
  // エクスポートされていないもののテストを行う
  non_exported ...;
  ...
}

あるいはこのようなmain()はテストフレームワークが暗黙に定義するかもしれません。いずれにせよ、現在モジュールファイル内でmain()を定義することはできず、これは規格違反となります。

また、より単純に、他のファイルが全て名前付きモジュールによって構成されているコードベースにおいて、main()を定義するためだけのために非モジュールのソースコードを一つ追加しなければならないのは単純に面倒というのものあります。

この提案では、現在の規則を修正して、グローバルスコープのmain()関数はどこで宣言されていたとしてもグローバルモジュールに属するようにすることを提案しています。

なお、GCC/Clangのモジュールの実装においては、どちらのコンパイラmain()に関する現在の規則を正しく実装しておらず、実質的にこの提案の目的を達成しています。この既存のプラクティスを標準化することで、既存の実装のある種のバグは少しの修正で正しく準拠し、望ましいものになります。また、この修正は以前に禁止されていたことを許可するものでもあり、破壊的な影響はないはずです。

こうした事情もあり、この提案はC++20へのDRとすることを提案しています(DRとなることはCWGで承認されています)。

P3423R0 Extending User-Generated Diagnostic Messages

コンパイル時に診断メッセージを指定することのできる機能に対する文字列の制約を共通化する提案。

C++26ではP2741R3の採択によってstatic_assertのエラーメッセージに対してユーザー提供の任意の文字列(以前は文字列リテラルのみ)を指定できるようになりました。さらに、このstatic_assertの診断メッセージの文字列はstd::string_viewのような具体的な型ではなく、文字列リテラルであるか、.size().data()を持つ任意の式(任意の型のオブジェクト)として指定されています(その結果はcontiguousなコンテナに期待するそれと同じであることに加えて、エンコーディング等の要件もあります)。

これは言語機能がライブラリ機能であるstd::string_viewに依存しないようにした結果ですが、これによってstatic_assertの診断メッセージ文字列にはかなりの自由度が生まれています。

この提案は、コンパイル時に診断メッセージ文字列を指定可能な他の機能についても、文字列リテラルに加えてユーザー提供の任意の文字列を指定可能にするとともに、そのユーザー提供の任意の文字列の制限を全ての言語機能で共通化しようとするものです。

ここでユーザー提供の任意の文字列を指定可能にする事が提案されているのは次のものです

  • = delete(msg)
  • [[deprecated(msg)]]
  • [[nodiscard(msg)]]

そして、static_assert(expr, msg)も含めて、msgに指定可能な文字列の規定としてdiagnostic-messageを指定するようにします。diagnostic-messageは、現在static_assertmsgの文字列の制約として仕様されているstatic_assert-messageをリネームして定義します。

提案文書より、サードパーティライブラリでのユースケースの例

namespace my_lib {
  // std::fixed_stringはP3094で提案中
  constexpr std::fixed_string lib_name = "myLib";

  [[nodiscard(lib_name + ": api()'s return value should not be discarded because ...")]]
  void api();

  struct [[deprecated(lib_name + ": class S is deprecated because ...")]] S;
}

P3425R0 Reducing operation-state sizes for subobject child operations

operation_stateのサイズを削減可能にする提案。

std::executionにおけるoperation_stateは、senderによって記述された非同期操作に対して、何らかのreceiverを接続(connect)することによって作成される、非同期操作の実行直前の状態です。

operation_stateの構造はsenderアルゴリズムの構成を反映した入れ子構造になっており、各層が1つのsenderに対応しています(この層もまたoperation_stateとなります)。そして、各層(sender/operation_state)にはreceiverオブジェクトが保存されますが、これは1つ上の層(親)のsenderから提供されたreceiverであり、通常それは単に親のoperation_stateオブジェクトへのポインタを保持するシンプルなものです。

このように保存されているreceiverオブジェクトを最も内側(=先頭の操作)のsenderから順に呼び出していく(各層で保存された親operation_stateへのポインタをデリファレンスしていく)ことによって処理の結果を通知するわけですが、この構造は最も内側のoperation_stateから最も外側のoperation_stateに向かって、その単方向リンクリストをなしています(このリストは直列ではなく、ツリー構造になりえます)。そして、このリンクリストの各要素は結局最も外側のoperation_stateオブジェクトのレイアウト内にあるため、メモリ上で連続したある範囲内にあり、一定のオフセットによって配置されています。

すなわち、operation_stateに内在しているリンクリスト構造は、ポインタの間接参照によるチェーンではなく、ポインタのオフセット計算によって置き換えることができるはずです。そうすると、各層で保存されている親operation_stateへのポインタも不要になります。これにより

  • operation_stateツリー内の各層におけるポインタが1つ削減され、全体的なoperation_stateのサイズが削減される
    • パディングを考慮するとさらに削減効果がありうる
  • receiverの環境クエリなど、operation_stateのリンクリストを辿る操作(各層でのポインタのデリファレンスのチェーン)を現在のoperation_stateのアドレスからのオフセット計算で置き換えられる

これにより、メモリフットプリントの削減が見込めますが、副次的な効果として

等のメリットも想定されます。

例として、次のようなsenderチェーンがあるとき

when_all(
  then(                            // then_op#3
    then(                          // then_op#2
      then(                        // then_op#1
        schedule(thread_pool),
        f),
      g),
    h),
  then(                            // then_op#6
    then(                          // then_op#5
      then(                        // then_op#4
        schedule(thread_pool),
        a),
      b),
    c))

これに対して何らかのreceiverを接続した後のoperation_stateオブジェクトのレイアウトは次のようになります

                        A
+-----------------------|-----------------+
| when_all_op           |  A              |
| - rcvr (parent_op*) --'  |              |
| - ref_count              |              |
| - stop_source            |              |
| - stop_callback          |              |        
| - result_tuple           |              |
| +------------------------|------------+ |
| | then_op#3              | A          | |
| | - rcvr (when_all_op*) -' |          | |
| | - h                      |          | |
| | +------------------------|--------+ | |
| | | then_op#2              | A      | | |
| | | - rcvr (then_op#3*) ---' |      | | |
| | | - g                      |      | | |
| | | +------------------------|----+ | | |
| | | | then_op#1              | A  | | | |
| | | | - rcvr (then_op#2*) ---' |  | | | |
| | | | - f                      |  | | | |
| | | | +------------------------|+ | | | |
| | | | | schedule_op            || | | | |
| | | | | - rcvr (then_op#1*) ---'| | | | |
| | | | | - thread_pool*          | | | | |
| | | | | - stop_callback         | | | | |
| | | | | - ...                   | | | | |
| | | | +-------------------------+ | | | |
| | | +-----------------------------+ | | |
| | +---------------------------------+ | |
| +-------------------------------------+ |
| +-------------------------------------+ |
| | then_op#6                           | |
| | - rcvr (when_all_op*)               | |
| | - ... (similar to above)            | |
| +-------------------------------------+ |
+-----------------------------------------+

ここでは、when_allに直接渡されている2つのsenderのうち1つ目のものにフォーカスしてレイアウトを示しています。

このoperation_stateの各層が指しているreceiverとは実質的に一番外側に接続されたもの(この図では見えていない、Aで示されているもの)であり、各層では親のoperation_stateへのポインタを保存し、それを順繰りに辿っていくことで一番外側のAまで到達します。

まずこの各層のreceiveroperation_stateへのポインタ)は合計で8個保存されており、ポインタのサイズが8バイトの環境ではこのサイズオーバーヘッドは64バイトになります。

各層からreceiverを使用する(環境のクエリや完了の通知)場合は、このポインタ(rcvr)のリンクリストを辿ってAを取得する必要がありますが、それには各層でのポインタのデリファレンスが伴いますが、これはメモリアクセスなのでその分遅延が生じます。

この提案はこれを解消できるようにすることを目指したものです。ここで提案されているのは、大きく次の2つの事です

  1. 親のoperation_stateと子のoperation_stateが共に最適化(ここで述べた事)をサポートしている場合に、最適化を適用するために必要なネゴシエーションを行うためのプロトコルの定義
  2. このプロトコルをP2300R10(C++26 std::execution)に適用する

この提案の後では、上の例のレイアウトは次のようになります

                   A              
+------------------|---+------A-----------+ 
| when_all_op      |   |      |           |
| - (maybe?) rcvr -'   |      |           |
| - ref_count          |      |           |
| - stop_source    <---'      | -72 bytes |
| - stop_callback   +16 bytes |           |
| - result_tuple              |           |
| +----------------------A----+---------+ |
| | then_op#3            | -16 bytes    | |
| | - h                  |              | |
| | +-----------------A--+------------+ | |
| | | then_op#2       | -4 bytes      | | |
| | | - g             |               | | |
| | | +-------------A-+-------------+ | | |
| | | | then_op#1   | -8 bytes      | | | |
| | | | - f         |               | | | |
| | | | +-----------+-------------+ | | | |
| | | | | schedule_op             | | | | |
| | | | | - thread_pool*          | | | | |
| | | | | - stop_callback         | | | | |
| | | | | - ...                   | | | | |
| | | | +-------------------------+ | | | |
| | | +-----------------------------+ | | |
| | +---------------------------------+ | |
| +-------------------------------------+ |
| +-------------------------------------+ |
| | then_op#6                           | |
| | - ... (similar to above)            | |
| +-------------------------------------+ |
+-----------------------------------------+

この場合、各層ではreceiverAを取得するために、自身のアドレスからのオフセット計算により直接最も外側のoperation_stateのアドレスを取得します(この提案のプロトコルにより、このための情報を静的関数の形で個のoperation_stateに挿入します)。これにより、ポインタを各層で保存する必要がなくなり、リンクリストを辿る間接参照のチェーンも不要になります。

提案しているプロトコルは具体的には、receiver型に対して導入される静的関数make_receiver_for()によります

// receiverの例示用の実装
struct some_receiver {
  // 子のoperation_stateのアドレスからオンデマンドでreceiverを構築するファクトリ関数
  static some_receiver make_receiver_for(child_op_state* op) noexcept;

  // その他receiverの関連捜査
  void set_value(auto&&... vs) noexcept;
  void set_error(auto&& e) noexcept;
  void set_stopped() noexcept;
  some_env get_env() const noexcept;
};

あるsenderでは、接続されたreceiver型がこのmake_receiver_for()を持つ場合は、接続されたreceiverオブジェクトを保存する代わりにこのファクトリ関数経由でreceiverを取得するようにすることで、上で述べていた最適化が可能になります。

そして、このような要件を満たすreceiverを表す新しいコンセプトinlinable_receiverを追加します

namespace std::execution {
  template<typename T, typename ChildOp>
  concept inlinable_receiver =
    receiver<T> &&
    requires(ChildOp* op) {
      { T::make_receiver_for(op) } noexcept -> std::same_as<T>;
    };
}

これを活用するoperation_stateの実装例は単純には次のようになります

// 通常のreceiver用
template<typename Receiver>
class my_op_state {
public:
  my_op_state(Receiver r) noexcept : rcvr_(std::move(r)) {}
  void start() noexcept;

private:
  Receiver& get_receiver() noexcept { return rcvr_; }
  Receiver rcvr_;
};

// inlinable_receiver用
template<typename Receiver>
  requires inlinable_receiver<Receiver, my_op_state<Receiver>>
class my_op_state<Receiver> {
  my_op_state(maybe_unused Receiver r) noexcept {}
  void start() noexcept;

private:
  Receiver get_receiver() noexcept { return Receiver::make_receiver_for(this); }
  // NOTE: No 'Receiver' data-member.
};

この例からも分かるように、ここで提案されているプロトコルはオプションであり、最適化が利用可能で有効にするためには親と子のoperation_stateの双方でオプトインが必要になります。

この例の様な実装は冗長な部分が多いため、オプションの提案としてプロトコルに関する実装をまとめたヘルパクラスを追加することを提案しています

// In <execution> header
namespace std::execution {

  template<typename Derived, receiver Receiver>
  class inlinable_operation_state {
  protected:
    explicit inlinable_operation_state(Receiver r)
      noexcept(std::is_nothrow_move_constructible_v<Receiver>)
      : rcvr_(std::move(r)) {}

    Receiver& get_receiver() noexcept { return rcvr_; }

  private:
    Receiver rcvr_; // exposition-only
  };

  template<typename Derived, receiver Receiver>
    requires inlinable_receiver<Receiver, Derived>
  class inlinable_operation_state<Derived, Receiver> {
  protected:
    explicit inlinable_operation_state(Receiver r) noexcept {}

    Receiver get_receiver() noexcept {
      return Receiver::make_receiver_for(static_cast<Derived*>(this));
    }
  };
}

operation_state実装者はこのinlinable_operation_stateクラスを継承しておくことで、ほぼ追加の実装を必要とせずに最適化にオプトインできます。

P3427R0 Hazard Pointer Synchronous Reclamation

ハザードポインタライブラリにSynchronous Reclamation機能拡張を追加する提案。

この提案は以前のP3151R1で提案されていた拡張機能のうち、Synchronous Reclamationだけに的を絞ったものです。

現在(C++26)のハザードポインタ機能はAsynchronous Reclamationに対応しており、これはretire()されたオブジェクトがいつ削除される(リソース解放される)かの保証がなく(いつかはされるものの)、共有リソースのデリータが後で利用できなくなるリソースに依存するような場合に使用できないほか、そのようなケースに該当しないことに注意する必要があります。

共有リソースのデリータが後で利用できなくなるリソースに依存するような場合、とは例えばデリータがファイルやネットワーク接続などに依存している場合です。Asynchronous Reclamationではretire()したリソースがいつ削除されるのかは分からないため、削除されるまでそれに必要なリソースが存在していることを保証するか、そのような場合に使わないようにする必要がありました。

Synchronous Reclamationとは、このような場合にもある程度削除されるタイミングの保証を行うことで、ハザードポインタによる安全なリソース管理を行おうとするものです。

この提案では、オブジェクトコホートObject Cohorts)という概念を使用して、Synchronous Reclamationを提供しようとしています。オブジェクトコホートは保護可能なオブジェクトの集合であり、オブジェクトコホートのデストラクタが呼び出されるまでの間に、オブジェクトコホートのメンバとなっているオブジェクトの削除(リソース解放)が行われることを保証します。

// コホートクラス
class hazard_pointer_cohort {
  hazard_pointer_cohort() noexcept;
  hazard_pointer_cohort(const hazard_pointer_cohort&) = delete;
  hazard_pointer_cohort(hazard_pointer_cohort&&) = delete;
  hazard_pointer_cohort& operator=(const hazard_pointer_cohort&) = delete;
  hazard_pointer_cohort& operator=(hazard_pointer_cohort&&) = delete;
  ~hazard_pointer_cohort();
};

template <class T, class D = default_delete<T>>
class hazard_pointer_obj_base {
public:

  // hazard_pointer_obj_baseに retire() + コホートへの登録 を行う関数を追加
  void retire_to_cohort(hazard_pointer_cohort&, D d = D()) noexcept;
};

// retire()済みハザードポインタのAsynchronous Reclamationを明示的に実行する
// ただし、すべてのretire()済みリソースが解放されることを保証するわけではない
void hazard_pointer_asynchronous_reclamation() noexcept;

Synchronous Reclamationを実現するもう一つの方法にはretire()済みである全てのオブジェクトのデリータの実行完了を保証する、グローバルクリーンアップというものがあります。この方法はある時点でretire()済みである全てのハザードポインタを同期的に破棄する必要があるためオーバーヘッドが高く、実用的ではありません。また、グローバルクリーンアップのアプローチだと、その機能を使用しない場合でもハザードポインタ実装にオーバーヘッドを追加することになります。

グローバルクリーンアップに対してコホートの利点はパフォーマンスにあり、グローバルクリーンアップと比較して保証が弱く柔軟性に欠けるもののパフォーマンスが重要なシーンで使用することができ、実用性とパフォーマンスのバランスに優れています。そのため、ここではグローバルクリーンアップアプローチは提案せず、コホートのアプローチだけを提案しています。

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

template <class T>
class Container {
  class Obj : hazard_pointer_obj_base<Obj> {
    T data;
    /* etc */
  };

  hazard_pointer_cohort cohort_;

  void insert(T data) {
    // ハザードポインタ初期化
    Obj* obj = new Obj(data);

    // コンテナへのデータ追加処理
    ...
  }

  void erase(Args args) {
    Obj* obj = find(args);
    // objをコンテナから削除
    obj->retire_to_cohort(cohort_);
    
    // 現在スレッド負荷軽減のために、スレッドプールに削除処理を移管
    exucutor_.add([] {
      hazard_pointer_asynchronous_reclamation();
    });
  }
};

// コンテナの要素型とする
class B {
  ...

  // Deleter(デストラクタ)は異なるライフタイムを持つ別のリソースに依存している
  ~B() { use_resource_XYZ(); }
};

// デリータが依存するリソースの確保
make_resource_XYZ();

{
  Container<B> container;
  container.insert(b);
  container.erase(b);

  // container.cohort_のデストラクタが実行される前に、containerに登録したものは削除済み
}

// この前に、containerに登録したオブジェクトbのリソースは解放済みであることが保証される
destroy_resource_XYZ();

この拡張によって、ハザードポインタを使用するもの(上記のコンテナのようなもの、リストやハッシュマップなど)では、そのオブジェクトと独立したライフタイムを持つリソースに依存しないような型だけでなく、任意の型を要素やキーとして使用できるようになり、汎用性を向上させることができます。

この提案の機能はすでにFollyというオープンソースのライブラリにおいて実装され、広く使用されているようです。

P3428R0 Hazard Pointer Batches

ハザードポインタライブラリに複数のハザードポインタをまとめて構築・破棄する機能拡張を追加する提案。

この提案は以前のP3151R1で提案されていた拡張機能のうち、Batches of Hazard pointersだけに的を絞ったものです。

空ではないハザードポインタの構築と破棄には、通常スレッドローカルストレージへのアクセスが発生するため、僅かとは言え時間がかかります(一桁ns)。現在(C++26)のハザードポインタライブラリでは1個づつの構築と破棄しかサポートしていませんが、これを纏めて行うことができるようになればハザードポインタの構築と破棄にかかるレイテンシを削減することができます。この提案は、そのための機能拡張を提案するものです。

具体的には、それを行う専用の関数のペアを追加しようとしています。

// 空のhazard_pointerオブジェクトの範囲(span)を受け取って、それらを空ではない状態にする
void make_hazard_pointer_batch(std::span<hazard_pointer> span);

// 空ではないhazard_pointerオブジェクトの範囲(span)を受け取って、それらを空の状態にする
void empty_hazard_pointer_batch(std::span<hazard_pointer> span) noexcept;

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

{
  // 空の状態のハザードポインタ配列
  hazard_pointer hp[3];

  // 3つのハザードポインタを同時に初期化(空ではなくする
  make_hazard_pointer_batch(hp);

  assert(!hp[0].empty());
  assert(!hp[1].empty());
  assert(!hp[2].empty());

  // src は atomic<T*>
  T* ptr = hp[0].protect(src);

  // ptrを介してsrcを使用する処理
  ...

  // 3つのハザードポインタを同時に空にする
  empty_hazard_pointer_batch(hp);

} // 3つのハザードポインタが順番に破棄される

この提案の機能はすでにFollyというオープンソースのライブラリにおいて実装され、広く使用されているようです。

P3429R0 Reflection header should minimize standard library dependencies

静的リフレクション機能のライブラリ部分について、他のヘッダへの依存関係を減らす提案。

P2996で提案中の静的リフレクション機能では、^^で取り出したエンティティのリフレクションに対して、ライブラリのconsteval関数群を使用して情報を取り出したり加工したりしてコード生成を行います。このライブラリ関数は<meta>というヘッダにまとめられており、ここの関数群は基本的にユーザーが作成できないものばかりなので、<type_traits><coroutine><initializer_list>などの半言語サポートヘッダと扱いは同じなかなりプリミティブなものです。

しかし、<meta>の関数群はstd::stringstd::vectorをはじめとして、std::string_viewstd::optionalなどの標準ライブラリ機能を使用しており、標準ライブラリヘッダへの依存を回避することは行われていません。

ゲーム業界など、C++の標準ライブラリをなるべく使用しないC++ユーザーグループがあり、そのようなケースにおいてもほぼ言語機能であるリフレクションを使用可能にするために、<meta>ヘッダの標準ライブラリへの依存を減らそうというのが、この提案です。

標準ライブラリへの依存を減らすことには、それ以外にも次のようなメリットがあります

  • 依存ヘッダの減少によるコンパイル時間の削減
    • リフレクション機能はプロジェクト内のほぼすべての翻訳単位に含まれることになるコアなライブラリで使用されることが想定されるが、<ranges>のようなコンパイルコストの高いヘッダに依存すると、プロジェクト全体のコンパイル時間増大を招く
    • モジュールは普及に時間がかかるため、この解決には間に合わない
  • よりシンプルなクラスを使用することによるコンパイル時間の削減
    • std::vector等のレイアウトやその動作(メモリ確保など)を制御できない型を、より効率的でコンパイラ実装者にとって扱いやすい同等の型に変更することで、コンパイル時間を削減できる
  • 一貫性
    • std::source_location::file_name()string_viewではなくconst char*を返し、std::contracts::contract_violation::comment()も同様。なぜ、std::meta::identifier_of()std::string_viewを返すのか?

提案の変更は次の点です

  1. <meta>の文言からのインクルードリストの削除
    • <initializer_list>, <compare>およびstd::size_tへの依存は除く
  2. reflection_rangeコンセプトを説明専用にする
  3. reflection_rangeコンセプトを範囲forでの使用に十分な最小のものにする
  4. ranges::input_rangeの仕様を置換(3と同様)
  5. std::vecotrの使用をstd::meta::info_arrayに置換
    • std::arrayに近い実装定義の型
  6. std::spanの使用をstd::initializer_listに置換
  7. 引数におけるstd::[u8]string_viewの使用を汎用引数(任意の文字範囲)に置換
  8. 戻り値におけるstd::[u8]string_viewの使用をconst char[8_t]*に置換
  9. data_member_specの再設計
    • std::optionalstd::string等に依存している
    • 単一の型から、セッターと組み合わせた複数のファクトリ関数と、ビルダーオブジェクト型へ変更

この提案のリストは一部にセットになっているものがあるものの、ほぼ個別に検討していくことができます。

ただし、ここで提案しているのはあくまで実装がそれを選択可能にする、ということで、標準ライブラリへ依存しないことを強制することを提案していません。それもあり、この変更後でもユーザーコードへの影響は大きくないはずです。

P3430R0 simd issues: explicit, unsequenced, identity-element position, and members of disabled simd

std::simd(P1928)のLWGレビューで見つかったIssueとその解決についてまとめた提案。

  1. コンストラクタのexplicitについて
    1. ブロードキャストコンストラクタを条件付きexplicitにする
    2. intrinsic型(実装定義ベクトル型)との間の変換を暗黙変換にする(explicitを外す)
  2. ジェネレータコンストラクタの呼び出し順序不定に関する規定(とnoexcept)を削除
  3. マスク付きのstd::reduceオーバーロードにおいて、identity_elementbinary_opの引数順を入れ替える
  4. basic_simdbasic_simd_maskの無効な特殊化に対して、size静的メンバ変数を復帰する

これらのIssueはいずれもLWGのレビュー中に見つかったもので、設計レベルの変更となるためLEWGでの確認が必要なものです。作業効率化のために、この提案にひとまとめにされてLEWGに提出されています。

P3433R0 Allocator Support for Operation States

operation_state型において、アロケータを認識し伝播可能にする提案。

std::executionsenderアルゴリズムでは自由に指定可能な引数を受け取るものがあり、そこに渡される引数の中にはアロケータのカスタマイズに対応している物(allocator awareな型)があります。これらのアルゴリズムでは、アロケータを注意深く使用することでグローバルなnew/deleteの呼び出しを回避できる可能性があります。

int values[] = { 1, 2, 3 };

auto s{
  ex::just(std::span(values))
  | ex::let_value(allocator_aware_fun([](auto alloc, std::span<int> v){
      return ex::just(std::pmr::vector<int>(v.begin(), v.end(), alloc));
    }))
  | ex::then([](auto&& v) noexcept {
      for (auto x: v){ std::cout << x << ", "; }
      std::cout << "\n";
    })
};

allocator_aware_funはクラステンプレートであり、ラップした関数オブジェクトに対してその最初の引数として注入するアロケータを管理するものです。

最初のjustはより実際には何らかのデータ(ネットワークから受信したものなど)を返す非同期処理に対応し、最後のthenは実際には直ぐに実行され完了するのではなく、後のどこかのタイミングで入力データ(values)にアクセスする非同期操作となります。したがって、データの生存期間が問題となる場合はコンテナに移し替えておくと安全です。ただしそのままではデータはヒープに配置されることになります。

これらのsenderチェーンに対してアロケータを注入するのに適した場所は、最後に実行される前の状態であるoperation_state型(上記例のs)であり、そのreceiverの環境を通してアロケータをconnect時に注入することができます。

しかし現在の仕様では、それぞれのsenderがそれぞれの状態(ユーザーからの値)をoperation_state内部に保存する際に、単にムーブするだけであるため、アロケータの伝播が不十分であり、operation_stateで指定されるアロケータとは通常一致しません。

この提案は、operation_state型のオブジェクトの構築時にアロケータの認識と伝播を行うようにすることで、senderアルゴリズムにおいてアロケータのカスタマイズがより自然に行えるようにしようとするものです。

具体的には、operation_stateの構築時(connect時)に、receiverの環境にアロケータが存在している場合、入力senderoperation_state内部にムーブする際に、std::make_obj_using_allocator()を使用してuses allocator constructionによるムーブ構築によって値を移動し保存するようにします。これにより、アロケータを使用しない型については普通のムーブ、アロケータをカスタマイズ可能な型についてはアロケータを伝播させながらムーブ構築、のようなことが適当的に行われるようになります。

P3435R0 Reflection and meta-programming

P2996とは異なった設計の静的リフレクションの提案。

この提案のP2996と異なる点は

  • 型付けされたAPI
    • ^によるリフレクションの結果は、decl, expr, typeという3つの型を返す(P2966はinfoのみ)
      • さらに、これらの追加のプロパティを表現するための型identifier, name, source_location, template_argument等が用意される
  • 式のリフレクションと構築をサポート
    • P2966では未サポート
  • リフレクション結果型の範囲は、専用かつ組み込みのコンテナ型
    • expr_list, type_list, decl_listなど
    • 実装が簡単だったのに加えて、これらの型はNTTPとして使用可能
    • リフレクションAPIの標準ライブラリへの依存が回避される
  • コードフラグメントによるコード生成機能

P2996では、言語構造をリフレクション型に反映してしまうとその事実が言語の進化を妨げることになるとして、リフレクション結果型はstd::meta::infoというコンパイル時にのみ使用可能な単一の不透明型を使用しています。これに対してこの提案では、弱い型付けのAPIは不便であり、言語プロパティのカテゴリは歴史を通じて安定しているため大きな変更があることは考える必要はないとして、エンティティの大まかな種別ごとに対応したリフレクション結果型を用意しています。

また、この提案ではスプライス以上のコード生成機能として、値キャプチャ付きのコードフラグメントを提案しています。コードフラグメントは意味解析前のコード辺であり、任意の場所に注入してコード生成を担います。トークンシーケンスとよく似たコンセプトのものですが、トークンシーケンスよりもよりC++のコードに近い状態のものです。

この提案のフラグメントは^から始まるラムダ式の様な構文によって取得されます。例えば

// 二項+のフラグメントを得る
consteval expr make_add(expr l, expr r) {
  return ^ [l, r] (%l + %r);
}

// ラムダ式による転送ラッパのフラグメントを得る
consteval expr lift(overload_set os) {
  return ^ [os] ( [] (auto&&... args) { return (%os)(args...); } );
}

のようになり、こうして取得したフラグメントは%によって別のコンテキストに注入できます

%make_add(l, r)

このように、このコードフラグメントによるコード生成は式の生成をサポートしています(P2966のスプライスではサポートされていません)。

提案文書より、列挙値の文字列変換のサンプルコード

consteval void gen_enum_to_string(function_builder& b, decl d) {
  // constexpr_map: リフレクション専用のコンパイル時map
  constexpr_map<expr, expr> map;
  
  // 列挙型のリフレクションdから各列挙子の情報を取り出す
  for (auto e : children(d)) {
    map.try_emplace( underlying_value(d), make_literal_expr(identifier_of(d)) );
  }
  
  for (auto m : map) {
    // case文のコードフラグメントをbuilderに注入していく
    b << ^ [m] {
      case %m.first : return %m.second;
    };
  }
}

template <class T>
  requires is_enum(^T)
std::string_view enum_to_string(T val) {
  switch(std::to_underlying(val)) {
    // Tの各列挙値に対応するcase文を注入する
    %gen_enum_to_string(^T);
    default : return "<invalid>";
  }
}

SG7でのレビューと投票では、この提案の方向性はあまり支持されていないようです(提案そのものが廃案になったわけではなさそうですが)。

安全性に関わるUBを取り除く提案。

この提案は、C++の安全性の向上のために、constexprコンパイル時に行うことができている種類のUBに対して実行時にもそれを回避(UBにつながる操作の禁止 or UBの動作規定)しようとするものです。内容は次のようなものです

  • 可能であればデフォルトで防止し、それが叶わない場合は安全プロファイルで防止する
    • コストが十分に小さければ、言語のデフォルトで防止
    • それ以外の場合、安全プロファイルが有効になっている場合は防止
      • UBにつながる可能性のある操作の禁止
      • UB時の動作を指定する
  • オプトアウトの方法を提供する

例えば整数演算のオーバーフローの場合、まずデフォルトで防止することはあまりにも現実的ではないので、プロファイルで防止することになります。arithmetic_safetyプロファイルがオンになっている場合、オーバーフローする可能性のある整数演算をすべてチェックします。また、bounds_safetyプロファイルがオンになっている場合境界チェック外の添字演算につながる可能性のある整数オーバーフローのみをチェックします。

P3437R0 Proposed default principles: Reflect C++, Generate C++

リフレクションとコード生成機能の設計に関する原則の提案。

C++26に向けて静的リフレクション機能が進行しており、それを受けてリフレクションに関する機能拡張の提案がいくつか出てきています。この提案は、現在の設計、あるいは今後出てくるであろう関連提案の設計の指針となる設計原則を提案するものです。

提案している原則は単純で、リフレクション(C++ソースコードを読み取りメタ情報を取得する機能)とコード生成(リフレクション結果を用いてソースコード機械的に生成する機能)は、少なくともデフォルトではC++ソースコードそのものであるべき、というものです。具体的には

  1. リフレクションはC++ソースコードを反射する
    • リフレクションは人間のコード読みの代理であり、その主な目的はプログラマが、人間のプログラマが読むコードと同じものを読み取ることのできるコード、を記述できるようにすることにある
  2. コード生成はC++ソースコードを生成する
    • コード生成は人間のコーディングの代理であり、その主な目的はプログラマが、人間のプログラマと同じものを記述できるコード、を記述できるようにすることにある

というものです。

原則1はすなわち、人間が見ることのできる(書くことのできる)ソースコードに関するメタ情報をリフレクションでは全て抽出可能でなければならないということです。例えば、属性のリフレクションが可能であるべきか?という(過去にあった)問いについては、この原則によりYesと答えることができます。また、リフレクションはクラスのアクセス指定子のデフォルトを読み取れるべき(現在できない)という結論も得られます。

原則2によって、アクセス指定子を無視する現在のスプライスの仕様には問題があることが導かれます。obj.xobj.[:reflect_x:]はアクセス指定子の面で同じセマンティクスを持っておらず、このような機能はデフォルトであるべきではありません。しかし、認めないのではなく、このような通常のC++ソースコードと同じ結果をもたらさないようなコード生成は非デフォルトの構文を使用する事を推奨しています。

また同様に、コード生成中(コード生成後のコード)でのみ有効な言語機能も避けるべきです。これは、そのような言語機能がある場合、当然リフレクションの外でも需要があるためです。そのような機能は通常の言語機能として追加することで、手書きの場合とコード生成時の両方で自然に使用可能になります。

原則2に従う提案としてはトークンシーケンス(P3294R1)を挙げています。

そして、原則2に従えば、コンパイラはコード生成後のソースコードをテキストファイル等の形式で出力可能であるはずです(このファイルをコンパイル可能なはずです)。

関連して追加で、「ツールはソースコードを必要とする」というものも挙げており、これは、手書きであるかコード生成結果であるかにかかわらず最終的なソースコードを表示できる必要がある、というものです。これによって、デバッガは生成されたコード内にステップインすることができ、IDEは生成結果を何らかの形でプログラマに提示できます。

P3438R0 Make integral overloads of std::to_string constexpr

整数からstd::stringへ変換するstd::to_string()constexprにする提案。

C++20からstd::stringconstexpr対応しており、定数式で使用することができます。しかし、整数値から文字列への変換のために通常よく使用されるstd::to_string()を使用することはできず、std::to_chars()を使用することになります。しかし、std::to_chars()は出力文字サイズ分のバッファをあらかじめ用意しておかなければならないため、少し面倒があります。

constexpr std::string my_to_string(int v) {
  // +1 for minus, +1 for digits10
  constexpr size_t bufsize{std::numeric_limits <int>::digits10 + 2};
  char buf[bufsize];
  
  const auto res = std::to_chars(buf, buf + bufsize, v);

  return std::string(buf, res.ptr);
}

// 整数範囲の各整数値に、指定した文字列をサフィックスとして付加するviewを返す
consteval auto addSuffix(std::string_view suffix) {
  return std::views::transform(
    [suffix](auto i) {
      return my_to_string(i).append(suffix);
    }
  );
}

この提案は、定数式における整数値から文字列への変換をより容易に行うために、std::to_stringconstexpr指定して定数式で使用可能にしようとするものです。

この提案によって、先ほどのサンプルは次のように単純化されます

consteval auto addSuffix(std::string_view suffix) {
  return std::views::transform(
    [suffix](auto i) {
      return to_string(i).append(suffix);
    }
  );
}

筆者の方は提案をlibc++のフォークで実装しており、そこではto_chars()を使用して変換するように実装しており、特に問題は無かったとのことです(おそらく上記のmy_to_string()と同じ処理になると思われます)。

ただし、ここで提案しているのは整数型を取るオーバーロードに対してのみで、浮動小数点数型のオーバーロードに対しては何の変更も提案していません(to_chars()浮動小数点数型を取るオーバーロードconstexpr未対応なため)。

P3439R0 Chained comparisons: Safe, correct, efficient

誤って書かれることの多い、連鎖比較を意図通りに動作するようにする提案。

連鎖比較(Chained comparisons)とはmin <= index_expression < maxの様な比較の記述で、これは意図通りの動作をしないもののコンパイルエラーにもなりません。例えば、0 <= 100 < 10という式はtrue < 10と同じであり、これはtrueとなります。このような記述はバグではあるものの、数値の範囲チェックを書く場合は代わりにmin <= index_expression && index_expression < maxのように書く必要があり、これの短縮版ともみなすことができます。そして何より、間違って書いてもコンパイルエラーにならずに値によっては結果が同じになるため、既存コードベースでこのバグは比較的よく観察されます。

P0893R1においてそれは以前に報告され、min <= index_expression && index_expression < maxの様に綴っているコードは既存のコードベースでかなり観測されるため、この短縮記法として連鎖比較をバグではなくするように提案されていました。それは以前は却下されましたが、現在C++コードベースの安全性の向上が重要視されるようになっており、この提案では改めてそれを提案しています。

ここでは次のことを提案しています

  • 同じ方向の比較演算子が連続して現れ(かっこに囲まれていない)、コピー可能で文脈的にbool変換可能な式、の連鎖を正しくかつ効率的にする
    • < <=> >===のいずれか一種類だけからなり、連鎖している
    • 例えば、整数値によるmin <= index_expression < maxのような式はmin <= index_expression && index_expression < maxと等価になり、index_expressionの評価は一度だけ行われる
  • <または<=演算子と、>または>=演算子が混在する、(かっこに囲まれていない)文脈的にbool変換可能な式、の連鎖をill-formedとする
    • あるいは、erroneous behaviorとする
  • < <= > >=を用いた畳み込み式をill-formedとする
    • あるいは、erroneous behaviorとする
    • 畳み込み式によっておこる連鎖比較が有用ではなくバグの原因となるため
  • これら以外の連鎖比較式については現状通りとする

以前のP0893R1そのままではなく、erroneous behaviorという言語のツールとcppfrontによる実装経験、および既存の演算子オーバーロード等によるコードに影響を与えないようにするなどの修正が加えられています。

P3440R0 Add n_elements named constructor to std::simd

先頭N個のビットが立ったsimd_maskを作成するためのファクトリ関数を追加する提案。

この提案はstd::simd(P1928R8、C++26採択済み)についての機能拡張提案です。

動的に長さの決まるデータブロックをstd::simdによって順番に処理していく場合、ほとんど必ず最後のデータブロックはstd::simdオブジェクトの(SIMDレジスタの)全体を埋めず、最初のいくつかの要素までの分しかない、ということが発生します。この場合、この最後のブロックは先頭からいくつか分だけ埋められ、残りは空の状態のstd::simdオブジェクトによって処理されます。例えば次のようになります

void fn(float* ptr, std::size_t count) {
  // SIMDレジスタをフルに埋められる場合の処理ループ
  auto wholeBlocks = count / simd<float>::size;
  for (int i=0; wholeBlocks; ++i) {
    auto block = simd<float>(i * simd<float>::size);
    
    process(block);  // Process an entire simd-worth of data.
  }

  // 一番最後のブロックの処理(SIMDレジスタ長に対して半端な数の要素が余った場合)
  auto remainder = count % simd<float>::size;
  if (remainder > 0) {
    // マスクを使用して後ろの要素を無視するようにする
    simd_mask<float> remainderMask ([=count](auto idx) { return idx < count; });

    auto remainderBlock =
      simd_load<simd<float>>(ptr + (count - remainder), remainder, simd_default_init_flag);
    
    process(remainderBlock, remainderMask); // Do the work on part of the SIMD only.
  }
}

そのような最後のブロックが現れる場合、この例の後ろのifのように、最後の要素数Nに対して[0, N)ビットのみがアクティブであるようなSIMDマスクを使用して処理します。

しかし、このマスクを作る方法にはいくつかの方法があります

int numRemainderBits = ...;

// 何らかのsimd<float>オブジェクトのiotaが入力としてあるとして

// コンパクトに書けるが、float比較を処理するために実行時の変換が必要になる
auto remainder1 = simd<float>::iota() < numRemainderBits; // Create an n-element mask.

// 実行時の変換を明示的に回避する
auto tmp = simd<uint32_t>::iota() < numRemainderBits; // Create an n-element mask.
auto remainder2 = simd_mask<float>(tmp); // Convert to the correct type of mask.

// 整数ビットからマスクを構築する
// compact mask machinesで効率的なコードが生成される
// 64を超える要素に対するマスクは型を変更せずに処理できない
auto m = (uint64_t(1) << numRemainderBits) - 1;
auto remainder3 = simd_mask<float>(m);

現状の問題点は、このような単純なマスクの生成のために使用可能な単一の移植可能な方法がないことです。また、この3つの方法を手書きする場合でも、コーナーケースのハンドリングでバグを埋め込む可能性があります。例えば、最後の方法は整数型が適切に構築されていなければならず、整数型の幅が小さいと無言で失敗する可能性があり、前2つの例ではsimd<uint8_t>::iota() < nのように書いた場合にsimd<uint8_t>に256以上要素があると失敗します。

冒頭の様なよくあるケースにおいて使用可能なマスクを生成する際のこのような問題を回避するために、この提案ではそのために使用可能なマスク(simd_maskオブジェクト)を得るためのファクトリ関数を追加する事を提案しています。

static constexpr basic_simd_mask basic_simd_mask::n_elements(simd-size-type count);

これは、[0, count)がアクティブなマスクを表すbasic_simd_maskオブジェクトを返すbasic_simd_maskの静的メンバ関数です。countが0の場合は空(全てのビットが非アクティブ)のマスクを返し、マスクサイズより大きい場合は全てのビットがアクティブなマスクを返します。また、このマスクの作成は実装によって最適な方法で実装されることが期待されます。

P3441R0 Rename simd_split to simd_chunk

std::simdsimd_splitという機能の名前をsimd_chunkに変更する提案。

std::simd_split<T>()関数は、std::simdオブジェクトを型Tのタプルに分解するもので、この時にその要素の過不足が無いように分解しようとするものです。

// 入力simdオブジェクト
simd<float, 19> x;

// 出力型
using IntoType = simd<float, 8>;

// simd_split()の実行
auto t = simd_split<IntoType>(x);
// get<0>(t) will be of type simd<float, 8>
// get<1>(t) will be of type simd<float, 8>
// get<2>(t) will be of type simd<float, 3> - the remainder

std:simdrangeであると見た時に、Rangeアダプタでこの動作に合致するのはstd::views::chunkです。一方、名前が合致するstd::views::splitはこの動作とは少し異なった分割を行います。

そのため、既存ライブラリ機能名との類推性から、simd_splitsimd_chunkに変更しようとする提案です。

また追加で、simd_chunksimd_split)のデフォルトの用法に対して、分割したい数を指定する関数simd_chunk_n()を追加することも提案しています。

simd<float, 19> x;

// Break into AVX2-sized pieces:
auto t = simd_chunk<simd<float, 8>>(x);

// Break into AVX2-sized pieces:
auto t = simd_chunk_n<8>(x);

simd_chunk()ユースケースは、より大きなstd::simdオブジェクトをネイティブのサイズ(環境のSIMDレジスタ長)に合ったピースに分割するというものです。この場合、ネイティブのサイズは既知ですが、simd_chunk<T>()の使用時にはTとしてstd::simdのフルの型名を書かなければなりません。これに対して、分割したい数を直接指定するのがsimd_chunk_n<N>()です。これは、1つのテンプレートパラメータで型とNTTPを区別して受けることができないため、別名関数として提案しています。

P3442R0 [[invalidate_dereferencing]] attribute

関数に渡されるポインタが無効化されることをコンパイラに通知するための属性、[[invalidate_dereferencing]]の提案。

例えば次のような典型的なメモリ確保から解放までのコードにおいて

auto p = std::malloc(sizeof(T)); 
// ... 
// pの指すsizeof(T)バイトの領域を何らかのデータ(ファイルやネットワークから読み取ったものなど)で埋める
// ... 
T* q = std::start_lifetime_as<T>(p);  // 読み出し準備(オブジェクトの生存期間を開始する)

// ... use q ... 

q->~T(); // qの指す領域のオブジェクトを破棄

std::free(p); // q(p)の指す領域を解放する
// 以降、*pや*qを使用することは誤りだが、その診断は必ずしも求められない

最後のstd::free()後にその領域を刺しているポインタp, qの使用(特に間接参照)は確実に謝ったコードですが、コンパイラは必ずしもこれを検出できないため、必ずしもコンパイルエラー等の形で診断されません。

この提案は、このような場合にコンパイラに対してポインタの領域が解放されることを通知するための新しい属性[[invalidate_dereferencing]]を追加するものです。

[[invalidate_dereferencing]]属性は、ポインタを受け取って何かする関数で、その関数がリターンした後にその渡したポインタを無効化するような関数のそのようなポインタ引数に対して指定して使用するものです。

namespace std { 
  std::free(invalidate_dereferencing void *); 
}

[[invalidate_dereferencing]]属性は関数を記述する側が使用し、呼び出し側ではその関数を使用する際に渡した後のポインタを間接参照してしまう場合にコンパイラの診断を得ることができます。

コンパイラは、この属性があることによってより深いフロー解析などを必要とせずにコンパイラの使用が危険であることを知ることができ、危険な使用に対して何らかの診断を発行しやすくなります。

ただし、この提案では標準ライブラリの関数がこの属性を使用するべき、ということは提案していません。

P3443R0 Reflection on SG21 2024 Process

Contracts MVP提案の議論が拙速になっているのではないかと疑問を呈する文書。

Contracts MVP提案(P2900R8)はC++26への導入を目指して活発な作業が続いており、特に今年に入って以降は関連する提案が数多く提出されています。しかし、提案の提出からレビューと投票までの期間が短すぎ、かつ関連する提案が多いことによって、提案の内容を吟味して議論して必要なら代替案を提示したり問題点を指摘したり、といった事を行っている時間が足りなかった、と筆者の方は感じているようです。

この文書では、P2900に関連するSG21のプロセスをデータによって明確化し、P2900の今後の取り組みに当たってこのことを考慮に入れるようにすることにあります。ただし、この文書はP2900を否定する意図のものではなく、あくまでその議論のプロセスを振り返ろうとするものです。

文章中には提案の提出日時や議論の議事録などを参照してデータを収集し、まとめています。

データによると、過去10ヵ月(2023年12月~2024年10月)で63本の提案が提出されており、その50%以上が1週間未満でレビューと投票を終えてP2900へマージされるかが決定しており、さらに75%のものは2週間以内にP2900に取り込まれています。

P3444R0 Memory safety without lifetime parameters

生存期間引数なしの安全な参照型の提案。

P3390R0で提案されているSafe C++と呼ばれるより安全なC++言語の取り組みにおいては、生存期間引数(lifetime parameter)を使用して参照のライフタイムに制約を付けます。しかし、これは一種のアノテーションであるためユーザーが明示的に指示する必要がある他、これの導入は言語の型システムを複雑化させます。

#feature on safety

// 関数引数には異なる生存期間引数がある
// 戻り値型はxの生存期間によって制約される
auto f1/(a, b)(int^/a x, int^/b y, bool pred) safe -> int^/a {
  // Error:
  // auto f1/(a, b)(int^/a, int^/b) -> int^/a は生存期間がbであるオブジェクトを返すが、bはaより長く有効ではない
  return pred ? x : y;
}

// 関数引数は共通の生存期間引数を持つ
auto f2/(a)(int^/a x, int^/a y, bool pred) safe -> int^/a {
  // Ok
  return pred ? x : y;
}

// Error:
// 戻り値型int^には生存期間省略を使用できない
auto f3(int^ x, int^ y) safe -> int^;

この提案ではそれに対して、そのような追加の注釈無しで同等の生存期間安全性を実現するsafe reference(安全参照)について説明しています。

#feature on safety

// New elision rules:
// All parameters are constrained by a common lifetime.
// The common lifetime constrains the return type.
int% f4(int% x, int% y, bool pred) safe {
  // Can return either x or y, because they outlive the common lifetime
  // and the common lifetime outlives the result object.
  return pred ? x : y;
}

安全参照はT%で宣言される参照で、生存期間注釈無しで同等の生存期間解析を行うものです。その生存期間の判定は関数の構成によって行われ、フリー関数の場合は戻り値の安全参照は引数のうちの最も短い生存期間によって制約され、非静的メンバ関数の場合は暗黙のオブジェクトパラメータ(thisオブジェクト)によって制約されます。

#feature on safety

const int% f1(const int% x, const int% y, bool pred) safe {
  // 戻り値の参照は全ての参照引数(の生存期間)によって制約を受ける
  // ここではxとyがその対象
  return pred ? x : y;  // ok
}

struct Obj {
  const int% f2(const int% arg) const % safe {
    // 非静的メンバ関数の場合、戻り値の参照は暗黙のオブジェクト引数によって制約を受ける
    // self(this)は戻り値の参照よりも長く生存するため、メンバxを返してもok
    return %x;  // ok
  }

  const int% f3(const int% arg) const % safe {
    // Error: argは戻り値の参照よりも長く生存しない
    return arg;
  }

  const int% f4(const self%, const int% arg) safe {
    // 明示的オブジェクト引数によって、f4()はフリー関数として扱われる
    return arg; // ok
  }

  int x;
};

int main() {
  int x = 1, y = 2;
  f1(x, y, true); // OK

  Obj obj { };
  obj.f2(x);  // OK
  obj.f3(x);  // Error
  obj.f4(x);  // OK.
}

このT%ではこれに加えて排他性を強制します。すなわち

  • T%は可変安全参照
    • 同じ参照先への参照として同時に複数存在できない
  • const T%は共有安全参照
    • 同じ参照先への参照として同時に複数存在できる
    • ただし、可変安全参照とは同時に存在できない

この安全参照が生存期間引数無しで同等の解析が可能であるにもかかわらず、既存の参照型に対してそうしないのは、既存の参照型にはこの排他性の強制が無く、それを新規に導入することも困難なためです。

#include <vector>

void f1(std::vector<float>& vec, float& x) {
  // vecとxがエイリアスしている場合、このpush_back()はxを無効化しうる
  vec.push_back(6);

  // 潜在的なUB: xはpush_back()呼び出しによって無効化されている可能性がある
  x = 6;
}

int main() {
  std::vector<float> vec { 1.0f };

  // Legacy references permit aliasing.
  f1(vec, vec[0]);
}
#feature on safety
#include <cstdint>

template<typename T>
class Vec {
public:
  void push_back(T value) % safe;

  const T% operator[](size_t idx) const % safe;
        T% operator[](size_t idx)       % safe;
};

void f2(Vec<float>% vec, float% x) safe {
  // push_back()はxを無効しうるか?
  // No、排他性によりvecとxのエイリアシングは防止される
  vec.push_back(7);

  // 常に安全になる(xがvecの要素を指すことは無い
  *x = 7;
}

int main() safe {
  Vec<float> vec { };
  mut vec.push_back(1);

  // Ill-formed: vecの可変借用を取得した直後にもう一つ取得しようとしている
  f2(mut vec, mut vec[0]);
}

既存の参照型を用いたコードは排他性の規則に則って書かれていない事が圧倒的であるため、既存の参照型に対してこれを強制すると例えSafe C++がオプトインであってもコードの書き換えが必要となります。

ただし、安全参照は第一級の参照として使用することができず、特にクラスメンバにすることができません。なぜなら、これをサポートしようとすると参照の参照(参照をメンバに持つオブジェクトの参照)を扱わなければならなくなるものの、その生存期間制約を扱おうとすると結局生存期間注釈が必要となるためです。しかし、現在のC++でも多数のイテレータ型やview型が有効に使用されている状況を考えればこのサポートは必須です。

したがって、この提案の結論としては生存期間引数の導入が不可欠である、というものです。

ここでの安全参照の保証を得るために必要な生存期間やフロー解析は、生存期間引数がある場合に行うこととほとんど同じです。すなわち、異なるのは表層のインターフェースのみで、実体としては生存期間引数の導入に関わらず実装負荷は同等です。

また、Rust言語に強く表われているように、C++の要求を満たしながら(ガベージコレクションなしで)第一級の安全参照を実現するための、現在の世界における唯一の方法は生存期間引数であり、今のところこれ以外の選択肢は見つかっていません。

確かに、生存期間引数は言語構造に新たな自由度を導入し、型システムを複雑化し、C++言語そのものも大きく変化させますが、Rust言語の成功に見られるようにこのアプローチは実現不可能でも実行不可能なものでもなく、むしろC++の要求を満たしながらその安全性を引き上げるための実現可能な唯一のアプローチでもあり、C++の将来にとって必要不可欠なものです。

もしC++言語およびコミュニティが、生存期間引数によって導入される僅かな不便さを理由にその導入を拒否し続けることは、(他の手段が無いため)安全性の軽視と同義であり、ソフトウェア品質への配慮を主張できなくなります。

(結局、この提案としてはP3390R0の提案の生存期間引数を導入すべき、というものでここで説明されている安全参照を導入しようとするものではありません)。

P3445R0 Add utilities for easier type/bit casting in std::simd

std::simdにおける2種類のキャストを簡単に行うための関数を追加する提案。

ここでの2種類のキャストは

  • 型キャスト: 要素数は維持したまま、要素型のみを変更する
  • ビットキャスト: オブジェクト表現を維持したまま、要素数と要素型を変更する

の事です。std::simdを使用したコードではこの2つのキャストが頻出するものの、記法は少し煩雑であるため、これを簡単に行うための関数を用意しておくことでユーザビリティや可読性の向上などを図るものです。

型キャストのための主な方法として、std::simdのコンストラクタを使用する方法とstatic_castを使用する方法の2つがありますが、どちらも宛先のstd::simd型(特殊化)をフルに用意しなければならず、かなり面倒になります

template<typename T, typename ABI>
auto incrementAsFloat1(const basic_simd<T, ABI>& x) {
  // コンストラクタの使用、rebind_simd_tは変換先の型を求めるユーティリティ
  return rebind_simd_t<float, basic_simd<T, ABI>>(x) + 1.0f;
}

template<typename T, typename ABI>
auto incrementAsFloat2(const basic_simd<T, ABI>& x) {
  // static_castの使用
  using OUT = simd<float, basic_simd<T, ABI>::size>;
  return static_cast<OUT>(x) + 1.0f;
}

この提案ではこのようなキャストをシンプルに行うための関数としてsimd_cast()を提案しています

template<typename T, typename ABI>
auto incrementAsFloat(const basic_simd<T, ABI>& x) {
  return simd_cast<float>(x) + 1.0f;
}

simd_cast<T>(x)は、std::simd型のオブジェクトxの要素型をTにキャストした別のstd::simdオブジェクトを返すものです。上記の方法及びその他の方法に比べて、圧倒的に単純に記述することができます。

ビットキャストのためには、スカラ型と同様にstd::bit_castを使用できます。しかしこちらのキャストでも、変換後の型をフルに求めておく必要があります。

// 複素数型のsimdオブジェクトを受けて何かする関数
template<typename T, typename ABI>
auto fn(const basic_simd<std::complex<T>, ABI>& x) {
  // 複素数の1要素をfloat2要素として読み替える
  // 要素数は倍になる
  constexpr int numNativeCmplxElements = simd<std::complex<float>>::size;
  using AsFloat = simd<float, numNativeCmplxElements * 2>;

  // ビットキャストにより複素数配列をfloat配列として読み取り
  auto asT = std::bit_cast<AsFloat>(x);

  // 何か処理
  auto result = ...; // e.g., call an Intel intrinsic like _mm512_fmsubadd_ps

  // 再び複素数simdに戻す
  return std::bit_cast<basic_simd<std::complex<T>, ABI>>(result);
}

この提案では、このための関数としてsimd_bit_cast()を提案しています

template<typename T, typename ABI>
auto fn(const basic_simd<std::complex<T>, ABI>& x) {
  // ビットキャストにより複素数配列をT型の配列として読み取り
  auto asT = simd_bit_cast<T>(x);

  // 何か処理
  auto result = ...; // e.g., call an Intel intrinsic like _mm512_fmsubadd_ps

  // 再び複素数simdに戻す
  return simd_bit_cast<std::complex<T>>(result);
}

simd_bit_cast<T>(x)は、std::simd型のオブジェクトxの配列ビットをそのまま読み替えて得られたTの配列によるstd:simdオブジェクトを返す関数です。こちらも、記述がかなり単純になります。

どちらの関数もユーザー定義することは難しくないですが、各所で再発明されることが想定され、あるとかなり便利な関数であるとして、標準で用意しておくことを提案しています。

P3446R0 Profile invalidation - eliminating dangling pointers

invalidationプロファイルの設計についての文書。

invalidationプロファイルは議論中のプロファイル提案のプロファイルの一種であり、ポインタやイテレータの無効化についての保証を提供しようとするものです。

invalidationプロファイルは次のようなものです(P3274の仕様形式)

  • 定義: 無効化されたポインタまたはイテレータを介したアクセスを禁止する
  • 初期バージョン
    • コンテナ(何らかの値を保持するもの)の要素へのポインタ(参照、プロクシ参照)が取得された場合、そのコンテナの非const関数の呼び出しを禁止する
    • 誤検知を回避するためには[[noninvalidating]]を使用する
    • 初期のバージョンでは、コンテナの要素の一つへのポインタを無効化する可能性のあるコンテナの関数呼び出しを含む、直線的なコードのみを許可する
  • 備考
    • 完全な一般化には、型分析とフロー分析の両方を含む本格的な静的解析が必要
    • ここでのポインタはオブジェクトを参照するものを意味し、コンテナは値を保持できるものを意味する
      • ここのコンテキストでは、std::jthreadはコンテナ

invalidationプロファイルはメモリ安全性にとって不可欠ですが、実装するにはコンパイラ内での静的解析が必要になります。また、invalidationプロファイルは仕様として規定するのが最も難しく、他のプロファイルに比べて静的解析の必要性も大きくなります。

この文書は、現段階の設計の概要を記したもので、まだ仕様と呼べるものにはなっていない設計メモです。ここでは

  1. ポインタの有効性
    • 「ポインタ」の定義
    • 削除されたポインタ
    • スコープ外へのポインタのエスケープ
    • エスケープ」を意図したポインタ
    • ポインタのエスケープの伝播
    • 複数のポインタ引数
    • 変数とメンバ
    • 制御フローの回避
    • クラス階層
  2. コンテナに対する無効化操作
    • 無効化の伝播
    • 表記
    • 「コンテナ」の定義
  3. 所有権
  4. エイリアス

などの項目について、invalidationプロファイルの設計と実装の観点から簡単な説明がなされています。

P3447R0 Profiles syntax

プロファイル機能の構文の提案。

プロファイル機能は、現在C++29に向けて検討中のC++コードの安全性を高めるために、プロファイルによって指定される特定の保証(型安全やリソース安全性など)をオプトインで要求するためのシステムです。プロファイルによる保証はコンパイル時の制限と実行時チェックによって提供されます。

プロファイル機能は、保証の要求をコード全体で一括制御するのではなくオプトインによって明確に指定されたコード内の部分部分で有効化できるようにし、なおかつプロファイルの指定によってC++のある機能が非互換に変更されることなく、結果のプログラムがC++標準として有効なものであり続けることを目的としています。

プロファイルの構想や概要は以前の提案で提示されていましたが、その構文は固まったものではなく、この提案は改めてプロファイル機能の構文を提案するものです。

プロファイル機能の構文には属性構文を使用し、[[profiles::...(p)]]のような構文を提案しています。...の部分にはプロファイルpについての要求を指定し、次の4つが使用できます

  1. profiles::enable
    • 場所: モジュール宣言、スコープ
    • 意味: 対象の領域内でプロファイルpを強制する
  2. profiles::enforce
    • 場所: importディレクティブ、スコープ
    • 意味: インポートされたモジュールもしくは指定されたスコープ内でプロファイルpを強制する
  3. profiles::suppress
    • 場所: importディレクティブ、スコープ
    • 意味: インポートされたモジュールもしくは指定されたスコープ内でプロファイルpを抑制する
  4. profiles::require
    • 場所: モジュールをインポートするimportディレクティブ、スコープ
    • 意味: インポートするモジュールがプロファイルpを適用してコンパイルされていなければインポートを失敗させる

また、プロファイルの部分的な実装と段階的な導入を容易にするために、[[profiles::enable(ranges, experimental)]]のように指定して実験バージョンのプロファイル有効化方法を提供しています。

使用可能なプロファイルpとしては、まず最初に次のものを導入しておくことを提案しています

  • algorithms
  • arithmetic
  • casting
  • concurrency
  • initialization
  • invalidation
  • pointers
  • ranges
  • RAII
  • type
  • union

これらの初期プロファイルはP3274R0で示されていたものでもあり、そちらでは根拠や提供する保証について詳しく述べられています。

構文として属性を採用しているのは、属性の無視可能性によって、様々なコンパイラコンパイルする必要があるコードベースにおいてプロファイル機能への対応状況でその指定を切り替える必要性を無くすためです。これによって、既存のコードベースに対してプロファイル機能を適用する障壁を下げています。

さらに、一部のプロファイルにおいて誤検知を低減するための補助的な属性をいくつか提案しています

  • Profiles: initialized
    • [[uninitialized]]: 意図的に初期化していない変数に対して指定して、初期化必須の制約を回避する
  • Profiles: invalidate: 非const引数(this経由も含む)の無効化を制限する
    • [[owner]]: ポインタに対するdeleteを行う必要があることを明確化
    • [[not_local]]: 関数から返されるポインタがその引数に依存していないことを明確化
    • [[not_returned]]: オブジェクトの一部が関数の戻り値に使用されないことを明確化
    • [[invalidating]]: invalidateプロファイルの抑制

属性構文を使用することについてはSG23で合意が取れており、この提案による構文自体もSG23で合意が得られているようです。

P3449R0 constexpr std::generator

std::generatorconstexpr対応する提案。

少し上のP3367ではコルーチンを定数式で使用可能にすることが提案されていましたが、ここではそれをベースとしてさらにstd::generatorを定数式で使用可能にすることを提案しています。モチベーションはほぼ共通で、std::generatorは実行時に有用なので同様にコンパイル時にも有用、というものです。

P3450R0 Extending is_within_lifetime

std::is_within_lifetime()関数でダウンキャストが可能かどうかをチェックできるように拡張する提案。

std::is_within_lifetime()は定数式中でのみ使用できる関数で。定数式内で渡されたポインタを使用可能かどうか(ポインタの参照先に生存期間中のオブジェクトが存在しているかどうか)をbool値で返す関数です。主に、共用体のアクティブメンバを定数式中で判定できるようにするために導入されました。

union U {
  int n;
  float f;
}

constexpr f(U& u) {
  if (std::is_within_lifetime(&u.n)) {
    // U::nがアクティブメンバ
    ...
  } else if (std::is_within_lifetime(&u.f)) {
    // U::fがアクティブメンバ
    ...
  } else {
    // アクティブメンバが存在しない
    ...
  }
}

定数式中で非アクティブメンバにアクセスしてしまうととにかくコンパイルエラーになるため、共用体のどのメンバがアクティブかを検査する方法が必要になったためC++26で追加されました。

その後、筆者の方がstd::format()constexpr化の作業を行っていた際に、この関数のモチベーションとよく似た問題に遭遇したようです。

まず、継承関係にある2つのクラス型(BaseDerived)があり、実行時にはBase型のオブジェクトを使用しているもののコンパイル時にはDerived型のオブジェクトを使用する処理があります。その場所では定数式の場合のみ受けたBase型のオブジェクトをダウンキャストしてDerived型として扱っています。これは定数式においては常にアップキャストされたDerived型のオブジェクトが来るので問題ありませんでした。

しかし、作業を続けて行ってstd::format()constexpr対応させる段になって、その処理にBase型オブジェクトが直接渡す必要が出てきました。この場合、ダウンキャストは出来ないためコンパイルエラーとなってしまいます。

この問題はコンパイル時にのみ発生するため、コンパイラはダウンキャストが安全かどうか、すなわちBase*Derived*にキャストした後のポインタの参照先に実際にDerived型オブジェクトが存在しているかどうか、を判定することができます(そのためにエラーが起きています)。

このシチュエーションはstd::is_within_lifetime()の解決したかった問題と同じですが、std::is_within_lifetime()はこのような場合に使用できるようになっていませんでした。この提案では、std::is_within_lifetime()でダウンキャストが安全かどうかをチェックできるようにしようとするものです。

提案ではまず、std::is_within_lifetime()の宣言を変更し

// 現在の宣言
template<class T>
consteval bool is_within_lifetime(const T* p) noexcept;

// この提案による変更
template<class U = void, class T>
consteval bool is_within_lifetime(const T* p) noexcept;

元々の判定に加えて、pconst U*に変換可能なオブジェクトを指しているかどうかも判定するようになります(両方が満たされた場合にtrueを返す)。

より正確には、pが生存期間内にあるオブジェクトを指していること、とstatic_cast<const U*>(p)が定数式であるか、の論理積を返します。Uvoidの場合は後者は常に満たされます。また、後者が満たされる状況では前者も必ず満たされています。

P3451R0 A Suggestion for Reflection Access Control

クラスのアクセシビリティをリフレクションのプロパティとして扱うようにする提案。

C++26に向けて議論されている静的リフレクション機能(P2996R6)では、クラスのアクセス制御についてが問題となっています。問題とは、リフレクション(^^)によってクラスのプライベートメンバや基底クラスサブオブジェクトのリフレクション(meta::info)を取得することができますが、それを用いてコード生成する際(主にスプライス[: refl :])にアクセス可能性をどう扱うべきかです。

  1. privateメンバ/基底クラスの存在を観察するためのアクセス権をユーザーに与えたい
    • いくつかのユースケースにとって重要であり、メタ情報として観察するだけなら問題ない
  2. 一方で、スプライスにおけるアクセスは実際のアクセシビリティを考慮して制限したい
    • privateメンバの読み取りもだが、そこへ書き込むとクラスの不変条件が破られ問題が発生する可能性が高い

accessible_members_of()などのメタ関数の時点でアクセスチェックを行ってしまうと、プライベートサブオブジェクトの観察そのものが行えなくなります。しかし、スプライスの時点でアクセスチェックを行うと、言語の他の部分の動作と異なった振る舞いになってしまいます。

class C {
private:
  int i;

public:
  // iへの参照を返す
  auto ref() -> int& { return i; }

  // iのメンバポインタを返す
  static consteval auto pmd() { return &C::i; }

  // iのリフレクションを返す
  static consteval auto refl() { return ^^C::i; }
};

void use(int);

void demo(C c) {
  use(c.i);     // error: アクセスできない
  use(c.ref()); // ok: アクセス可能な場所で参照を取得している

  use(c.*&C::i);    // error: アクセスできない
  use(c.*C::pmd()); // ok: アクセス可能な場所でメンバポインタを取得している

  use(c.[:^^C::i:]);    // error: アクセスできない
  use(c.[:C::refl():]); // okであるべき: アクセス可能な場所でリフレクションを取得している
}

スプライスの時点でアクセスチェックを行う場合、この最後の例c.[:C::refl():]がエラーになります。これは許可したいわけです。

この提案では、この問題の解決のために、エンティティのアクセシビリティをリフレクション(meta::info)の基本プロパティの一部として扱うようにすることを提案しています。 リフレクションのプロパティとはそれの取得元になったエンティティの持つプロパティそのものですが、これまではアクセシビリティがその一部として扱われていなかったため、まずアクセシビリティをリフレクションのプロパティとして位置づけます。

そして、members_of()等の関数ではそこで取得されるメンバのリフレクションに対してアクセシビリティのプロパティを設定するようにします(現在はaccessible_members_of()get_public_members()のような関数とその結果としてアクセシビリティが扱われている)。

そのうえで、スプライシングにおいてはこのアクセシビリティのプロパティをチェックしてアクセス可能ではない場合にエラーするようにします。

class C {
private:
  int i;

public:
  // これがこのコンテキストで動作するように修正されると仮定
  static_assert(nonstatic_data_members_of(^^C).size() == 1);

  // 当然、このコンテキストではCのメンバに無制限でアクセスできる
  static_assert(is_accessible(nonstatic_data_members_of(^^C)[0]));


  // この関数内のコンテキストで`^^C::i`はアクセス可能であり、そのアクセシビリティは結果のリフレクションに記憶される
  static consteval auto get_refl() { return ^^C::i; }
};

void demo(C c) {
  // アクセシビリティに関係なくメタ情報は取得可能
  static_assert(nonstatic_data_members_of(^^C).size() == 1);

  // この方法でしか`C::i`のリフレクションを取得できない(`^^C::i`はアクセスできない)
  constexpr auto r = nonstatic_data_members_of(^^C)[0];

  // rはprivate
  static_assert(is_private(r));

  // 他のプロパティも観察可能
  static_assert(type_of(r) == ^^int);
  static_assert(identifier_of(r) == "i");

  // しかし、アクセス可能ではない
  static_assert(not is_accessible(r));

  // したがって、rはスプライスできない
  int i = c.[:r:]; // error

  // しかし、`get_refl()`から取得したリフレクションでは可能
  constexpr auto r2 = C::get_refl();
  static_assert(is_private(r2));
  static_assert(is_accessible(r2)); // 取得した場所でのアクセシビリティが伝播している
  int j = c.[:r2:]; // ok

  // これでも可能
  int k = c.[: force_accessibility(r) :];
}

consteval bool is_accessible(info)はリフレクションからアクセシビリティbool値で取得する関数であり、consteval info force_accessibility(info)アクセシビリティを無視したリフレクションを返す関数です。

アクセシビリティがリフレクションのプロパティの一部となることで、リフレクションを取得した場所におけるアクセシビリティがリフレクションに反映されるようになります。それにより、スプライスでアクセスチェックを行ったとしても、言語の他の部分(メンバポインタなど)と一貫した動作をするようになります。また、アクセシビリティを無視する手段はオプトインになり、なおかつforce_accessibility()という存在感のある関数名としてコードに表示されるようになります。

この提案においてc.[:r:]が機能するためには

  • is_accessible(r)trueとなる
  • そのスプライスのコンテキストにおいて、rが示しているサブオブジェクトにアクセス可能である場合
    • rのエンティティがサブオブジェクトになっているクラス内部など

の少なくともどちらか一方を満たす場合、となります。

まとめると、ここでの提案は次のものです

  1. 基底クラスのサブオブジェクトと全てのメンバのリフレクションに対して、アクセシビリティの概念を追加
    • members_of()関数(とそのファミリ)が返すリフレクションは、呼び出し元が呼び出し地点でそのサブオブジェクト/メンバにアクセスできる場合にis_accessibleを満たします
  2. 新しいメタ関数の追加
    • consteval bool is_accessible(info r);
      • rがそれが生成された時点でアクセス可能ではなかったサブオブジェクト/メンバのリフレクションである場合にのみfalseを返す
      • つまり、リフレクションのアクセシビリティプロパティを取得する
    • consteval info force_accessibility(info r);
    • consteval info make_inaccessible(info r);
  3. [:r:]スプライス)は次のいずれかの場合に有効となる
    • is_accessible(r)trueとなる
    • そのスプライスのコンテキストにおいて、rが示しているサブオブジェクトにアクセス可能である場合
  4. get_public_~()関数ファミリを削除する
    • この提案の下では不要になり、こちらの方が問題をよりよく解決できるため

アクセシビリティがリフレクションのプロパティの一部となる場合、1つ疑問になるのはそれが等価性にどう反映されるか、という点です(std::meta::infoオブジェクトは==によって比較可能)。

class C {
  int i;

public:
  static consteval auto get() { return ^^C::i; }
};

// Cの外側で取得されたC::iのリフレクション
constexpr auto outer = nonstatic_data_members_of(^^C)[0];
// Cの内部で取得されたC::iのリフレクション
constexpr auto inner = C::get();

// アクセシビリティが異なる
static_assert(!is_accessible(outer));
static_assert(is_accessible(inner));

// 等価性はどうなる?
static_assert(outer == inner); // ???(ここではとりあえずfalseとすることを提案している

これに関しては筆者の方も決めかねているようで、どちら(trueとなるかfalseとなるか)に進むこともできるだろうとしているものの、型エイリアスというプロパティが等価性に反映されている現状からそれよりも強いプロパティであるアクセシビリティも等価性に反映されるべきであるとして、リフレクション値の等価性にはアクセシビリティの一致も含まれるようにすることを提案しています。

なお、make_inaccessible()の主なユースケースはここにあり、アクセシビリティプロパティを一貫させて等価比較を行うために使用することを想定しているようです(force_accessibility()でそれを行うと、使用理由が曖昧になるため関数を分けていると思われます)。

P3454R0 Revising Atomic Max/Min Operations

std::atomicの比較操作について、カスタム比較を使用できるようにする提案。

C++26に対して、P0493R5にてstd::atomicに対して値の大小関係によって書き込みを行うAPIfetch_max()/fetch_min()が追加されました。この提案は、このAPIをさらに改善することを目指すものです。

この提案の高レベルな動機づけとして、次の項目が掲げられています

  1. 不要な書き込みを回避するためのパフォーマンスの最適化
    • P0493R5のfetch_max()/fetch_min()は、更新が必要ない場合でも値のストア(書き込み)が行われる
    • 最大値/最小値が変更されない場合、値のストアを省略できるはず
  2. 様々なアーキテクチャ間での実装の柔軟性向上
    • この提案では、ハードウェアに応じて追加のストアをするかしないかを選択できる
    • 一方、P0493R5の仕様では、ストアを無条件に強制するため、様々なアーキテクチャにおいて最適化が阻害される
  3. アトミック操作の実際の使用パターンとの整合性向上
    • 既存のProducer-Consumerパターンにおいては、データのリリース(最大値の設定など)においてfetch_add()等の他の操作が使用されている
    • 最大値が前の操作で設定されている場合、値が更新されなければ追加のリリースストアは不要
      • 前の操作によってデータの可視性が既に確保されている
  4. 上級ユーザー向けの、メモリ順序のより正確な制御方法の提供

これらのための具体的な提案は次のものです

  1. compare_exchange_strong()/compare_exchange_weak()を、比較関数オブジェクトを受け入れるように拡張
  2. 追加したオーバロードでは、提供された比較関数オブジェクトを使用するように仕様を調整する
  3. 既存のオーバーロードの動作を再指定
// 提案するインターフェースの例
template<class T, class Comparison>
bool compare_exchange_strong(Comparison&& cmp, T& expected, T desired, memory_order success, memory_order failure) noexcept;

この変更により比較ベースアトミック操作のより一般化されたAPIが提供されるようになり、min/maxやより複雑な比較をベースとした操作、カスタム同期ロジックなど様々なユースケースに対応できるようになり、それらの個別のニーズに対応するための専用のAPIを一々議論し追加する必要性が回避されます。例えば、「もし現在の値がexpectedよりも〇〇(cmp(current, expected)true)だったら、新しい値desiredに更新する」に該当するような複雑な条件を持った操作を、この追加されたAPIによって記述できるようになります。

そして、この操作では条件が満たされた場合にのみ値の更新を規定するため、P0493R5のfetch_max()/fetch_min()における常にストア強制という問題が回避されます(してもいいししなくてもいい、になる)。無論、fetch_max()/fetch_min()操作もこのAPIによって記述可能です。

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

std::atomic<int> atom{5};
int expected = 3;
int desired = 10;

// もしatomがexpected未満の場合、desiredに更新する
atom.compare_exchange_strong(std::less{}, expected, desired, std::memory_order_acq_rel, std::memory_order_acquire);

// fetch_max()の実装例
auto fetch_max = [&atom](int val) {
  int expected = atom.load();
  while (!atom.compare_exchange_weak(std::less{}, expected, val)) {}
  return expected;
};

ここで提案されている変更はP0493R5のfetch_max()/fetch_min()の問題を回避するために別の関数に対して大きな変更を加えるものですが、代替案としてfetch_max()/fetch_min()そのものの仕様を変更して条件付きストアを行うようにするものも検討されています。提案では細かく比較検討した結果、より汎用的で既存のC++ atomicの設計との一貫性があるこれらの変更によるアプローチを提案しています。ただし、代替案にも利点はあるため両方のアプローチに対してフィードバックを求めています。

P3455R0 SG14: Low Latency/Games/Embedded/Financial Trading virtual Meeting Minutes 2024/6/12-2024/10/9

SG14において、2024/6/12から2024/10/9の間に行われたオンラインミーティングの議事録。

どのような提案がレビューされ、どのような議論があったのかが簡単に書かれています。

P3456R0 system_scheduler on Win32, Darwin and Linux

system_schedulerに対して、OSの提供するスレッドプールの持つ機能を組み込む提案。

system_schedulerはP2079R4で提案されている、std::executionフレームワークによるスレッドプール機能です。

一方でWindowsMacOSではシステムでスレッドプールを提供しており、そこでは次のような機能が提供されます

  1. schedule: 実行のためにワークアイテムを投入する
  2. schedule_at: 特定の時刻(あるいは一定の時間遅延後)にワークアイテムを実行するようにスケジューリングする
  3. defer: 特定のワークアイテムが完了した直後、までワークアイテムの実行を遅延する
    • boost.asioとP0443のみサポート
  4. bulk: バルク実行のサポート
  5. i/o: I/O操作の完了ハンドラがスレッドプールで実行されるようにする
  6. priorities: ワークアイテムに優先度を設定できる

これらの機能がP2079R4とasioおよびOSの実装でのサポート状況をまとめたのが次の表です

機能\実装 p2079r4 Windows MacOS Asio
schedule
schedule_at
defer
bulk
i/o
priorities

このように、P2079R4のスレッドプール機能はシステムのスレッドプールが提供している機能をカバーしきれていません。このため、system_schedulerがこれらのOSのスレッドプールを内部実装として使用する場合、直接使用した時と比べて利便性やパフォーマンスで劣るものになってしまいます。この提案では、OSのスレッドプールが提供する機能をsystem_schedulerでもサポートすることで、その機能性やパフォーマンスとstd::executionの利便性を同時に利用可能にしようとするものです。

P2079R4のsystem_schedulerはインターフェースだけを示すと次のようになっています

system_scheduler get_system_scheduler();

class system_scheduler() {
public:
  system_scheduler() = delete;
  bool operator==(const system_scheduler&) const noexcept;
  std::execution::forward_progress_guarantee get_forward_progress_guarantee() noexcept;

  // 通常のワークアイテム投入
  sender auto schedule();

  // バルク実行
  sender auto bulk(integral auto i, auto f);
};

この提案では、これを次のように拡張しようとしています

// 優先度を指定する列挙型
enum class system_scheduler_priority {
  background
  low,
  normal,
  high,
};

// system_scheduler取得時に優先度を指定可能にする
system_scheduler get_system_scheduler(system_scheduler_priority priority = system_scheduler_priority::normal);

class system_scheduler() {
public:
  system_scheduler() = delete;
  bool operator==(const system_scheduler&) const noexcept;
  std::execution::forward_progress_guarantee get_forward_progress_guarantee() noexcept;

  using native_handle_type = implementation-defined;
  native_handle_type native_handle();

  // 通常のワークアイテム投入
  sender auto schedule();

  // 遅延実行
  sender auto defer();
  
  // バルク実行
  sender auto bulk(integral auto i, auto f);

  // 指定時間経過後にワークアイテム投入
  template <typename Rep, typename Ratio>
  schedule_after_sender system_scheduler::schedule_after(
     std::chrono::duration<Rep, Ratio> delay,
     milliseconds window = {}) const noexcept;

  // 指定時刻にワークアイテム投入
  template <typename Clock, typename Duration>
  schedule_at_sender system_scheduler::schedule_at(
     std::chrono::time_point<Clock, Duration>& abs_time, 
     milliseconds window = {}) const noexcept;
};

I/O処理の完了ハンドラのサポートに関しては、I/Oの様々側面をカバー可能なI/Oライブラリスイートを直ぐに確立することは困難であるため、システムがサポートする場合に利用可能にするためにnative_handle()を公開しておくことで対応しています。

これらの機能に加えて次の機能についてもサポートが議論されています

  • 弾力性のあるスレッドプール
  • ワークアイテムのキャンセル

提案では、これらの機能が実装ごとにどのようにサポートされ、そこからどのように実装されるかを簡単に示しています。

P3457R0 SG19: Machine Learning virtual Meeting Minutes to 2024/06/13-2024/10/10

2024年6月13日から10/10日の間に行われた、SG19のオンラインミーティングの議事録。

どのようなことを議論したのかが簡単に記載されています。

P3460R0 Contracts Implementors Report

GCC/Clangにおける、Contracts MVP(P2900)の実装状況等の報告書。

P2900R8のほとんどの部分が既にGCC/Clangによって実装されているようで、これはその経験をP2900にフィードバックするための報告書です。

どちらの場合においても実装に当たって致命的な困難さや問題等は無く、P2900R8の仕様が既に十分に実装可能であることが示されています。

Clangでは標準ライブラリ実装(libc++)への導入も行われており、そこではconst化が問題になることは無く、むしろバグ発見に貢献したことが報告されています。

どちらの実装もCompiler Explorerで利用可能になっており、更なるフィードバックを求めています。

P3465R0 Pursue P1179 as a Lifetime TS

P1179R1をライフタイムTSとして発行する提案。

プログラミング言語の安全性に関する関心が大きく高まっており、それを受けてC++に対しても安全性を向上に関する提案がいくつも提出されています。P1179R1は2019年に内部的に発行された提案であり、今までは具体的な議論はされて来ませんでした。P1179R1のアプローチには

  • C++を他の何かに変えてしまうのではなく、C++の現在の言語機能やイディオム、ガイダンス等を採用する
  • 既存のC++コードに対して、追加の注釈をほぼ必要としない
  • C++ Core Guidelinesで推奨されている
  • C++及びその他言語の静的解析の専門家の支援を受けて作成されている
  • 既に2つのベンダ(Microsoft, JetBrains)によって部分的に実装されている
    • 他の提案と比べて最も実験的ではない

などの利点があります。

特に強調されているのは、P1179は他言語のモデルをC++に移植しようとしたり、C++からの出口を追及したりするなど、C++C++ではなくするようなものではなく、既存のC++のコードほぼそのままにライフタイムの安全性を導入することのできる互換性のあるアプローチであることです。

P1179の作業は95%が完了しているものの停滞しており、委員会の後押しがあれば残りの5%を完成させてその実装を完了し、使用経験を積むことができるとしています。そして、TSとして公開することでそれが実現できます。

P3466R0 (Re)affirm design principles for future C++ evolution

EWGにおける言語機能の設計原則の提案。

静的リフレクション機能と型とメモリの安全性に関する提案によってC++は大きな進化の時を迎えていますが、そのような局面にあってもC++の進化が可能な限りまとまりと一貫性を保ち、C++C++のまま最高の価値を提供していけるようにするために、LEWGが採用しているのと同様の設計原則のポリシーをEWGでも採用しようとするものです。

提案されている原則は、まずD&Eのセクション4.5にあるもの

  1. Cおよび以前のC++とのリンク互換性を維持する
  2. Cおよび以前のC++との不必要な非互換性がない
  3. C++より低レベルな言語の余地を残さない(アセンブラを除く)
  4. 使わないものにコストを支払わない(セロオーバーヘッド原則)
  5. 疑問がある場合に手動で制御可能な手段が用意されている

を再確認するとともに、次の原則を追加することを提案しています

  1. 一般的な機能を好み、狭い範囲でのみ使用可能な特殊な機能を避ける
  2. 意図を直接的に表現する機能を好む: 「どのように、ではなく、なにを」
  3. 採用可能性: ウィルスアノテーションを必要とする機能を追加しない
    • JAVAの検査例外のような、ここで使用するためにはこれを使用する他のところにも追加する、事を要求する機能
  4. 採用可能性: 大量の注釈を必要とする機能を追加しない
  5. デフォルトで実装詳細を漏洩させる機能は避ける
  6. 組み込みの言語機能よりも、constevalライブラリ機能を好む

この提案はEWGの投票においてコンセンサスを得ているようです。

P3467R0 2024-10 Library Evolution Polls

2024年7月に行われる予定の、LEWGにおける投票の予定。

次の提案が投票にかけられる予定です

最後のものを除いて、これらはC++26導入を目指してLWGに転送するための投票です。

P3469R0 Virtual deducing this

明示的オブジェクト引数を持つ仮想関数を許可する提案。

明示的オブジェクト引数とは、C++23で導入されたdeducing thisと専ら呼ばれている機能の事です。明示的オブジェクト引数を持つ関数は非静的メンバ関数でありながら仮想関数にすることはできませんでした。

非静的メンバ関数でありながら仮想関数にできないという矛盾は、筆者の方にとってはかなり予想外であったようで、その制限を解除してメンタルモデルの簡素化を図るとともに言語の機能を向上させようとするのがこの提案です。

ユースケースの一つとして挙げられているのは、クラスの多態的なクローン処理の自動実装です。基底クラスのポインタから呼び出された時でも、その実行時の動的型によってクラスのコピーをするような関数で、通常は次のようにクラス階層のすべての場所に実装を挟んでいく必要があります

struct Animal {
  virtual Animal *clone() const {
    return new Animal(*this);
  }
};

struct Cat : public Animal {
  virtual Cat *clone() const {
    return new Cat(*this);
  }
};

struct Cow : public Animal {
  virtual Cow *clone() const {
    return new Cow(*this);
  }
}

struct SiameseCat : public Cat {
  // 実装忘れ
};

struct Dog : public Animal {
  // コピペミス
  virtual Animal *clone() const {
    return new Animal(*this);
  }
};

このような実装は単純でありながらも面倒でミスが発生しやすいボイラープレートコードになりがちです。deducing thisでvirtualを許可すると、これは次のように書けます

struct Animal {
  // ベースとなる仮想関数宣言
  virtual Animal *clone() const;

  // virtual もしくは overrideを指定することを推奨
  template<typename Self>
  virtual Self *clone(this Self const &self) override {
    return new Self(self);
  }
};

struct Cat : public Animal {
};

struct Cow : public Animal {
}

struct SiameseCat : public Cat {
};

struct Dog : public Animal {
};

仮想関数でありながら、派生クラスでのオーバライドは必要ありません。いつも通りに基底クラスのポインタから仮想関数を呼び出すと、その実際のオブジェクト型で実装された関数が呼び出され、deducing thisの場合、thisパラメータの型はその動的型が推論されています。

上の例の場合、通常の仮想関数の宣言virtual Animal *clone() const;に対応する(同じシグネチャとなり定義する)形で明示的オブジェクト引数を持つ関数テンプレートvirtual Self *clone(this Self const &self)が宣言されています。これによって、Animalクラス自身及びその派生クラスにおいて、この仮想関数clone()の定義が発生します。各派生クラスで定義される際、明示的オブジェクト引数の型はそれぞれの派生クラス型で推論されることになり、それによって継承するだけで各派生先での自動実装が完了します。各派生クラスで実装されたclone()関数はthisパラメータとしてそのクラス型オブジェクトを取りながらも、仮想関数のオーバーライドであるため、基底クラスのポインタから呼び出された場合でも通常通りにその動的型で定義されたものが呼び出されます。

これは、明示的オブジェクト引数を持つ仮想関数テンプレート、によって実行されており、通常の仮想関数では達成できません。

this引数がテンプレートであることによって、コンセプトを用いて制約することで定義される派生クラスの性質に応じた分岐ができるほか、もとのdeducing thisのモチベーションだった値カテゴリとCV修飾によるオーバロードを1つにまとめることもできます。

提案にはこれを利用してVisitorパターンを実装する例が紹介されています。

P3470R0 Interface-Unit-Only Module Library Support

インターフェースオンリーなモジュールを可能にする提案。

現在のC++モジュールの仕様は実質的に、名前付きモジュールのインポート可能な単位ごとに正確に一つのオブジェクトが生成される、事を要求しています。これによって、モジュールをインポートする側のコードではモジュールからのコードに対するコード生成を行わなくてもよくなり、中間のオブジェクトファイルのサイズとコンパイル時間の両面でメリットがあります。

このような最適化スぺースには維持する要望もあり、これを維持することには合意が取れているようです。

現在でもインターフェースオンリーなモジュールは作成可能ですが、この要求のためにインターフェースオンリーなモジュールでも1つの翻訳単位をなし、そのモジュールからエクスポートされているエンティティはそのモジュール内でコード生成されており、インポートした側で生成されません。

この提案で言っているインターフェースオンリーなモジュールとは現在のヘッダオンリーライブラリに近いもので、インターフェースオンリーなモジュールにおいてはモジュールからエクスポートされているエンティティについてはモジュール側ではなくインポートした側でコード生成を行うようにしたものです。

しかしこの場合、一体だれがそのモジュールからエクスポートされているエンティティのコード生成を担うのかが不明確になります。提案文書で例示されていますが、あるプログラムが動的ライブラリと静的ライブラリを介して複雑な形でインターフェースオンリーなモジュールに依存している場合、どこでコード生成をするようにしても問題があります。特に、インターフェースオンリーなモジュールを直接使用する場所(動的or静的ライブラリ)でコード生成をするようにした場合、それが動的ライブラリと静的ライブラリの形で最終的にプログラムにリンクされていると、同じ名前(マングル名)のエンティティが衝突することでリンカエラーを起こします。両方動的ライブラリの場合、実行時未定義動作でエラーにすらならない可能性があります。

これらの事情により、現在のC++モジュールの仕様ではインターフェースオンリーなモジュール(この提案の意味での)は許可されておらず、「ビルドシステムが外部ライブラリのオブジェクトを生成することを期待すべきではない」という合意がなされています。

現在のモジュールの仕様は難解に記述されてはいますが、かなり特定のABIを暗黙的に指定しています。それは既存のABI(Itanium ABIやWindows ABIなどのC++ ABI)の上にある仕様であり、Modules ABIとでもいうべきものです。前述のモジュール内のエンティティはモジュール内でコード生成されるということもこのModules ABIで指定されているところによります。

この提案では、このModules ABIに対してインターフェースオンリーなモジュールをサポートするための別のABIを用意して、コード上からどのABIを使用するかを選べるようにすることで、現在のABIに変更を加えることなくインターフェースオンリーなモジュールをサポートすることを提案しています。ただし、完全に分岐するものではなく、主にオブジェクト生成(コード生成)の主体がモジュールではなくインポートした側に移るようになる他はModules ABIと仕様を共有しています。

追加するインターフェースオンリーなモジュールのためのABIでは、モジュール内でエクスポートされているエンティティのコード生成の責任はインポートした側が担います。ただし、その際にそのシンボルはWeakシンボルとして生成されるようにします。これによって最終的なリンク時には(それらシンボルは当然同一であるとして)Weakシンボルは束ねられて1つだけが使用されることでリンクエラーは回避され、仮に複数の動的ライブラリから見えていても未定義動作にはなりません。これは、現在のヘッダオンリーライブラリで使用されている手法とほぼ同じです。

ABIの切り替え(どちらのABIを選択するか)は、モジュール宣言において構文で行うことを提案しており、そのような構文の候補を2つ挙げています

  1. 属性
    • export moduleのモジュール宣言に対して特定の属性を指定することで、インターフェースオンリーモジュールであることを宣言する
    • 属性はすでに指定できるので、属性とその効果を指定するようにするだけで(言語の他の部分の変更はなく)有効化できる
  2. export inline module
    • inlineキーワードを指定してモジュール宣言を行うことで、インターフェースオンリーモジュールであることを宣言する
    • 少なくとも構文定義は変更が必要になり、相対的に変更が重い

どちらの候補も純粋に構文の違いのみであり、達成したい意味論については同じです。

SG15によるレビューでは、インターフェースオンリーなモジュールの実現可能性についてはこの提案の方向性が支持されたものの、その需要については不透明であり、エコシステムからのより強い需要が無い限りはこの提案を検討しないこ合意が取れました。そのため、この提案の追及は一時停止されます。

P3471R0 Standard library hardening

標準ライブラリに堅牢化モードを導入する提案。

ここ数年のC++に対する安全性圧力の高まりを受けて、C++標準への提案にも安全性を高める機能や未定義動作を解消するような提案が相次いでいます。しかし、セキュリティ上重要なC++コードの量を考えると、コストのかかるソリューションを既存のコードに導入するのは現実的ではありません。既存のコードに対して安全性の向上をもたらすためには、標準化のコストが低く、利用するユーザーの手間がほとんどないソリューションが必要です。

この提案では標準ライブラリ内の未定義動作に対するそのようなソリューションとして、標準ライブラリの一部の事前条件に対して堅牢化モードを導入する提案です。

ここで提案されている堅牢化された事前条件(hardened precondition)とは、現在の標準ライブラリで指定されている事前条件(Precondition)の一部に対して、Hardenedという文字列を追加するものです。そして、堅牢化された事前条件は堅牢化が有効になっている場合に実行時にチェックされ、違反している場合はプログラムを終了させます。

この提案ではまだ、具体的にどの事前条件を堅牢化するかは指定していませんが、初期セットとしてコンテナ等の範囲外アクセスについて指定している事前条件に焦点を当てることを推奨しています。具体的には

  • シーケンスコンテナのアクセサ関数
    • std::span, std::mdspan, std::string, std::string_view及び存在しない要素にアクセスしうるその他のクラスの関数を含む
  • コンテナが空ではないと仮定するシーケンスコンテナ及び文字列クラスの変更を伴う関数
    • pop_back()など
  • オブジェクトが空ではないことを仮定するstd::optional, std::expectedのアクセサ

これらの事前条件チェックはメモリに関するセキュリティ脆弱性を向上させることができるためユーザーにとって価値が高い一方で、実装及びチェックのコストが低いものです。

堅牢化モードを有効化する方法は実装定義としています。コンパイラフラグやライブラリマクロを想定していますが、この提案の採択後には一つの方法に収斂するだろうとしています。

また、違反が検出された場合のプログラム終了の方法も実装定義としています。提案では、Contractsのアサーションを利用することも有効な実装戦略であると述べています。ただし、この堅牢化はContractsとは独立したものであり、特に評価セマンティクスのメカニズムとは独立しており、有効化されている場合は確実にチェックされ違反があればプログラム終了一択となり、堅牢化対象ではない事前条件には影響がありません。

この堅牢化モード相当の機能は既存の標準ライブラリの3実装がいずれもデバッグモードのような形で用意しており、これこそがこの提案の様な機能の需要を示しています。しかし、その提供はあくまで実装定義であり実行時チェックの範囲や有効化する方法などには移植性がありません。既存の実装がすでに提供している安全性の保証を標準で要求することで、このような取り組みがクロスプラットフォームになり、その有用性が向上します。

この提案の内容は、ほぼこれを先行実装していたlibc++のHardening Modesの経験を基にしています。

P3472R0 Make fiber_context::can_resume() const

fiber_context::can_resume()constメンバ関数にする提案。

P0876R16で提案中のfiber_contextは、スタックフルコルーチン(ファイバーとも呼ばれる)を実現するためのコンテキストスイッチを管理するためのクラス型です。

fiber_context::can_resume()は、そのfiber_contextオブジェクトに紐づいているファイバーが現在の(呼び出された)スレッドで再開することができるかを取得する関数です。R16時点の仕様ではこの関数は意図的にconstメンバ関数にはなっていません。これは、少なくとも一つの実装では内部コンテキストの切り替えが必要となることが(Boost.Contextの経験で)分かっているため、それを考慮してのものです。

しかしこれにより、この関数を並行的に呼び出すためには追加の同期が必要になります。この関数は他の関数の事前条件としても使用されていますが、提案中の契約プログラミング機能においては契約注釈内では*thisは暗黙constとなるため、この場合の事前条件を契約注釈ではそのまま表現できません。

また、実際にはあらゆる実装でconstメンバ関数として実装することができるはずであるとして(リファレンス実装はそのようにしている)、この提案ではこの関数をconstメンバ関数にすることを提案しています。

P3473R0 Splicing Should Respect Access Control

リフレクションのスプライシングがアクセスチェックを無視しないようにする提案。

P2996R5時点での静的リフレクションにおけるスプライシング[:r:])の仕様では、rが示すエンティティがクラスの非publicメンバであったとしてもアクセスチェックが行われないため、通常不可能なアクセスをリフレクションを介すことで行うことができてしまいます。

#include <experimental/meta>
#include <iostream>
#include <utility>

class S {
  int priv;
public:
  S() : priv(0) {}
};

consteval auto member_named(std::string_view name) {
  // Sの非静的メンバの中から`name`と同じ名前のリフレクションを取り出す
  for (std::meta::info field : nonstatic_data_members_of(^S)) {
    if (has_identifier(field) && identifier_of(field) == name)
      return field;
  }

  std::unreachable();
}

int main() {
  S s;

  // S::privはプライベートメンバだが、アクセス出来てしまう
  s.[:member_named("priv"):] = 42;    // ok
  return s.[:member_named("priv"):];  // ok
}

この設計については議論があり、この提案はこれをデフォルトでは禁止すべきとするものです。

その理由としては

  1. 現在の言語機能で回避することのできる唯一の方法は欠陥であり、設計の意図ではなかったはず
  2. アクセス制御を無視した書き込みは危険であり、確実に誤用される
  3. プライベートメンバの読み取りアクセスは、特にマルチスレッドコードで安全ではなくなる
  4. プライベートメンバが公開されてしまうことで、プライベートなものがAPIの一部になってしまう
    • それにより、コードの変更がより難しくなる
  5. C++はこれまで、アクセス制御を無視することのできるツールを意図的に避けてきた、これからもそうすべき

等が挙げられています。

なお、この提案ではアクセスチェックを全面的に禁止しようとしているようで、オプトインの回避方法のようなものも提案してはいません。

P3474R0 std::arguments

コマンドライン引数を取得するためのライブラリ機能の提案。

ここで提案されているコマンドライン引数取得のためのライブラリ機能では次の3つの問題を解決することを目指しています

  1. エンコーディング
  2. main()の外(前)でのアクセス
  3. 現代的なインターフェース
    • 配列へのポインタとその長さを引数として渡すインターフェースは、std::spanなどの最新の機能によって過去のものになっている
    • 現在の教育性の悪いインターフェースに代わる、より教えやすいインターフェースを導入する

この機能はあくまで、argcargvという現在のコマンドライン引数のインターフェースをこれらの問題を解消した新しいものに置き換えることを目指すもので、コマンドライン引数パーサーを提案しているわけではありません。

この提案では、std::argumentsstd::argumentという2つのクラスを提案しています。

std::argumentは1つのコマンドライン引数を表す型です。これはfilesystem::pathの設計を反映したエンコーディングに中立的なコマンドライン引数表現型になっています。

namespace std {
  class argument {
  public:
    using value_type  = /* see below */;
    using string_type = basic_string<value_type>;
    using string_view_type = basic_string_view<value_type>;

    // [arguments.argument.native], native observers
    const string_view_type native() const noexcept;
    const string_type      native_string() const;
    const value_type*      c_str() const noexcept;
    explicit operator string_type() const;
    explicit operator string_view_type() const noexcept;

    // [arguments.argument.obs], converting observers
    template<class EcharT, class traits = char_traits<EcharT>,
              class Allocator = allocator<EcharT>>
      basic_string<EcharT, traits, Allocator>
        string(const Allocator& a = Allocator()) const;
    std::string    string() const;
    std::wstring   wstring() const;
    std::u8string  u8string() const;
    std::u16string u16string() const;
    std::u32string u32string() const;

    // [arguments.argument.compare], comparison
    friend bool operator==(const argument& lhs, const argument& rhs) noexcept;
    friend strong_ordering operator<=>(const argument& lhs, const argument& rhs) noexcept;

    // [arguments.argument.ins], inserter
    template<class charT, class traits>
      friend basic_ostream<charT, traits>&
        operator<<(basic_ostream<charT, traits>& os, const argument& a);
  };

  // [arguments.argument.fmt], formatter
  template<typename charT>
    struct formatter<argument, charT>
      : formatter<argument::string_view_type, charT> {
        template<class FormatContext>
          typename FormatContext::iterator
            format(const argument& argument, FormatContext& ctx) const;
    };
}

そして、std::argumentsstd::argumentのコンテナであり、conststd::span<std::argument>に近いインターフェースを持っています。

namespace std {
  template<class Allocator = allocator<argument>>
  class arguments {
  public:
    using value_type = const argument;
    using size_type = size_t;
    using difference_type = ptrdiff_t;
    using pointer = value_type*;
    using const_pointer = value_type*;
    using reference = value_type&;
    using const_reference = value_type&;
    using const_iterator = /* implementation-defined */; // see [arguments.view.iterators]
    using iterator = const_iterator;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
    using reverse_iterator = const_reverse_iterator;

    // [arguments.view.cons], constructors
    arguments() noexcept(noexcept(Allocator())) : arguments(Allocator()) {}
    explicit arguments(const Allocator&);

    // [arguments.view.access], access
    reference operator[](size_type index) const noexcept;
    reference at(size_type index) const;

    // [arguments.view.obs], observers
    size_type size() const noexcept;
    bool empty() const noexcept;

    // [arguments.view.iterators], iterators
    const_iterator begin() const noexcept;
    const_iterator end() const noexcept;

    const_iterator cbegin() const noexcept;
    const_iterator cend() const noexcept;

    const_reverse_iterator rbegin() const noexcept;
    const_reverse_iterator rend() const noexcept;

    const_reverse_iterator crbegin() const noexcept;
    const_reverse_iterator crend() const noexcept;
  };
}

std::argumentsをデフォルト構築すると、そのプログラムにおけるコマンドライン引数を表すように自らを初期化します。その際、単にコマンドライン引数を参照するだけではなく、実装次第ですが専用のメモリを確保して所有する場合があります(そのため、アロケータサポートを持っています)。std::argumentも同様に、実装次第で文字列ビューである場合もあれば文字列を所有している場合もあります。

std::argumentsは完全に読み取り専用のクラスとして設計されており、一度このクラスを初期化した後で直接argvの内容を書き換えた時に反映されるかは実装定義とされています。

std::argumentsコマンドライン引数の配列であり、各要素としてstd::argumentが1つのコマンドライン引数を表しており、std::argumentからは文字列としてC++エンコーディングに合わせた形でコマンドライン引数文字列を取得することができます。

#include <arguments>

int main() {
  // コマンドライン引数を取得
  std::arguments cmd_args{};

  // サイズを取得
  std::println("Number of command line arguments: {}", cmd_args.size());

  // 全てのコマンドライン引数をイテレーション
  for (const auto [index, arg] : cmd_args | std::views::drop(1) | std::views::enumrate) {
    std::println("{:d}: {:s}", index, arg);
  }

  // 特定のエンコーディングで取得する
  std::u8string u8arg1 = cmd_args[1].u8string();
}

解決しようとしている問題にあったように、std::argumentsmain()の外(動的初期化中の関数呼び出し内など)でコマンドライン引数を取得する事をサポートしています。

P3475R0 Defang and deprecate memory_order::consume

memory_order_consumeを非推奨化する提案。

memory_order_consumeはアトミックなメモリアクセスの際のメモリオーダーの指定の一つで、下から2番目に弱い順序付けによる読み出しを行うものです。

しかし、C++23時点のmemory_order_consumeは非推奨指定されており、その定義が有用なものではないことは広く認められています。さらに、ほとんどのコンパイラmemory_order_consumeによる読み出しをmemory_order_acquireとして実行するように実装しているようです。

ただし一方で、memory_order_consume相当のアトミック読み出しは一部の大規模なコードベースにおいて重要なユースケースがあり、頻繁に使用されてもいます。例えばLinuxカーネルではRCUが良く利用されていますが、ARMやPowerのようなアーキテクチャにおいてRCUのメモリオーダーが過度な制約を受けるのを回避するにはmemory_order_consumeのような順序付けが必要であり、Androidのコアコード内でも使用実績があります。

さらにもう一方で、既存のmemory_order_consumeに関する文言から恩恵を受けられるような新しい機能が導入される可能性は低く、memory_order_consumeに関する文言ごと削除すべきという強い主張もあります。その理由としては

  1. memory_order_consumeに関する問題は10年前から認識されており、多くの議論や代替案の検討が行われたもののいずれも合意に至ることができなかった。
    • 現在の仕様では深刻な問題すらも修正されていないため、非推奨となっている
  2. memory_order_consumeに関する仕様はメモリモデルを著しく複雑化しており、数学的な学術研究の世界では(その仕様が破綻していることが分かっているため)あえて無視されている
    • それにより、学術研究の成果を標準に翻訳することを妨げているため、削除することによってそのような複雑さを取り除くことができる
  3. 広く使用されているCPUアーキテクチャではもはやmemory_order_consumeは必要ではない
    • x86ではmemory_order_consumeは有益ではなく、ARMでもmemory_order_acquire(およびmemory_order_seq_cst)のハードウェアサポートがある
      • RISC Vも同じ方向に向かっている
    • 依然として有用なのはPowerとGPUくらいのもの
  4. 実際には、移植性を捨ててmemory_order_acquireを非常に慎重に使用することが残りのユースケース(PowerとGPU)において勝利を収めている

この提案はこの主張を支持し、これらの理由を持ってmemory_order_consumeを削除することを提案しています。

提案では、memory_order_consumeに関する文言とともに、その列挙値そのものや[[carries_dependency]]kill_dependency()を削除することを提案しています。ただし、Annex Dにはkill_dependency()memory_order_consumememory_order_acquireとして非推奨ながらも残しておくようにすることを提案しています。

P3476R0 Slides for P2688R2 - Pattern Matching: match Expression

P2688の紹介スライド。

P2688で提案されているmatch式によるパターンマッチングの機能性についての紹介と、実装経験に基づいた設計に関する説明がスライドとしてまとまっています。

P3477R0 There are exactly 8 bits in a byte

1バイトを8ビットであると規定するようにする提案。

C++の主要な3実装もほとんどすべてのOSも、1バイトは8ビットであることを仮定しており、なにより一般のC++プログラマのほとんどがそれを疑っていません。にもかかわらず、C++(Cも)では1バイトは必ずしも8ビットであるとはされておらず、CHAR_BITという1バイトのビット数を表すマクロの値は実装定義とされています。

CHAR_BITが8を返さない実装が仮にあったとしても、世の中のほとんどのC++プログラムはCHAR_BITが8であることをかなり強く仮定しているため、そのような実装では既存のC++のコードの動作には何らかの問題が生じると思われます。

昔のプロセッサや一部のDSPなど、現代でも一応1バイトが8ビットではないアーキテクチャは存在しているようですが、問題なのはその存在ではなく、そのようなアーキテクチャC++を考慮しているかどうか、そして最新のC++はそのようなアーキテクチャを考慮するかどうか、にあります。

この提案では、そのようなアーキテクチャを考慮しないものとして、1バイトを8ビットであると規格に焼き付けることを提案しています。

なお、この提案は結局LEWGでもEWGでも合意には至れず現状維持となりました。

P3478R0 Constification should not be part of the MVP

契約注釈から参照される変数のconst化を削除する提案。

P2900R8のContracts MVP仕様では、契約注釈内から参照されるローカルなもの(ローカル変数や関数引数、*this)が暗黙的にconstとして扱われます。これは、契約注釈内での副作用を防止することを意図したものです。しかし、これは他のC++コードとはデフォルトの挙動が大きく異なる部分でもあり、かなり物議を醸しています。

この提案は、その契約注釈内での変数const化という仕様をP2900から削除することを提案するものです。

理由としては

  1. 契約注釈内の式に新しいセマンティクスを与えるものである
    • 多くのユーザにとって驚きであり、契約機能の採用に悪影響を及ぼす
  2. それによる利点は非常に小さく、そのメリットは通常、契約注釈内の式のセマンティクスを変更することのない優れた方法で提供可能
  3. オーバーロード解決の結果を変更してしまう
  4. 契約注釈内の式の安全性を向上させる提案が他にある
    • 我々は、将来的なより良いモードの追加を妨げる些細な改善に落ち着くのではなく、代替評価モードのフル機能セットに取り組むべき
      • たとえば、P3285R0

等を挙げています。

筆者の方は、C++26にContractsが導入されることを望んでいるもののconst化がそこに含まれるのであればそれは望ましいものではなく、const化はC++ユーザーを対象にした実験でありその影響を図るにはデータが足りていないため、const化をContracts MVPに入れたいならTSにすべき、と述べています。

この提案の主張は、EWGの投票によって否決されています(とはいえ、賛成票は決して少なくはありません)。

P3479R0 Enabling C pragma support in C++

Cの浮動少数点数関連のプラグマをC++でも使用できるようにする提案。

C言語にはコンパイラフラグよりもローカルな方法で浮動小数点演算の動作等をカスタマイズすることのできる浮動小数点プラグマが用意されています。これらのプラグマについてはC++には導入されておらず、それがどのようにC++コードで使用できるかについては何も規定がありません。

そのようなプラグマは#pragma STDCから始まるもので、その後に丸めモードを指定するものと、ON, OFF, DEFAULTのいずれかを指定したうえでさらに効果についての指定をするものがあります。プラグマはファイルスコープで指定してその翻訳単位全体に適用することも、ブロックの先頭で指定してそのブロック内にだけ適用することもできます。

C23では次のものが使用可能です

  • CX_LIMITED_RANGE
    • 精度の低下やNaNの特殊処理等を考慮せず、複素数の計算を数式通りに実行する
  • FENV_ACCESS
    • 浮動小数点環境へのアクセスを行うことを宣言するもの
    • これが指定されていない場合、浮動小数点例外フラグの値は未定義となり、丸めモードがデフォルトではない場合は未定義動作となる
  • FENV_DEC_ROUND/FENV_ROUND
    • ブロックに対してその内部での丸めモードを指定する
    • FENV_ACCESSOFFでもこれらのフラグは正しく動作する
    • FENV_DEC_ROUNDは10進浮動小数点数に、FENV_ROUNDは2進浮動小数点数に作用する
  • FP_CONTRACT
    • 浮動小数点数の式を、中間丸めなしで1つの式に縮約できるようにする
    • もっとも一般的な例は、a*b+cfma(a, b, c)に縮約すること

これらのプラグマをC++に持ってくるときの問題点は、C++特有のものとどのように相互作用するかという点です。この提案では、C++特有の問題が起こりうるコンテキスト(クラススコープやテンプレートパラメータ等の宣言内など)でのこれらプラグマの使用を禁止し、ほぼCで指定されている範囲内でのみ使用可能になるようにすることを提案しています。

namespace A {
  #pragma STDC FENV_ACCESS ON // ill-formed
}

class B {
  #pragma STDC FENV_ACCESS ON // ill-formed
};

extern "C" {
  #pragma STDC FENV_ACCESS ON // not ill-formed, continues to end of the file
}

void d() {
  #pragma STDC FENV_ACCESS ON // not ill-formed, continues to end of function
  if (0.1 + 0.2 == 0.3) {
    #pragma STDC FENV_ACCESS OFF // not ill-formed
  }
}

template <
#pragma STDC FENV_ACCESS ON // ill-formed
float f = 0.1 + 0.2>
void e(
#pragma STDC FENV_ACCESS ON // ill-formed
float g = f);

これは言葉で言うと、そこでなされた宣言がグローバル名前空間に属することになる場所、もしくは複合ステートメント内の全ての明示的な宣言およびステートメントの前、となります。

なおこの提案は、C++の準拠実装がこれらプラグマを必ずサポートしなければならないとするものではなく、Cにないプラグマを追加しようとするものでもありません。

Clangはこの提案の内容をほぼ実装しているほか、MSVCもより限定的なプラグマを実装しています。

P3480R0 std::simd is a range

std::simdrangeにする提案。

std::simdのベースとなったParallelism TS2はC++17をベースとして設計されていました。C++17時点のイテレータはおおむね関節参照結果が左辺値を返すかどうかによってそのカテゴリが決定されていましたが、std::basic_simd及びstd::basic_simd_maskは共にそのサブオブジェクトとしてSIMD要素型を保持しているわけではないため、*[]もどちらもprvalueを返します。これによって、実際にはランダムアクセス相当ではあるもののC++17のInputeIteratorにしかなれませんでした。

このような不一致があったため、std::basic_simd及びstd::basic_simd_maskイテレータインターフェースを追加して範囲として扱おうとする提案はこれまでありませんでした。

しかし、C++20のイテレータコンセプトでは、関節参照結果の値カテゴリはイテレータカテゴリに影響を与え無くなり、ランダムアクセスイテレータであったとしてもprvalueを返すことができます。そのため、この提案ではstd::basic_simd及びstd::basic_simd_maskrangerandom_access_range)とすることを提案しています。

筆者の方は、std::basic_simd及びstd::basic_simd_maskrangeにすることで得られるメリットはあってもデメリットはないのではないか、と考えているようです。range化するメリットとは、std::simdを既存の標準ライブラリで使用できるようになることで、特にC++20/23のRangeライブラリの恩恵を受けられることにあります。

std::simd<int> v = ...;

// P1928R12
for (int i = 0; i < v.size(); ++i) {
  do_something(v[i]);
}

// この提案
for (auto x : v) {
  do_something(x);
}

// この提案
std::ranges::for_each(v, [](auto x) {
  do_something(x);
});

// この提案
v | std::views::filter([](auto x) { return x > 0; }) | std::ranges::to<std::vector>();

std::basic_simd及びstd::basic_simd_maskは読み取り専用であるため、その入力(SIMDレジスタへの値のストア)にはあまり役に立たないものの、その出力(SIMDレジスタからの値の読み出し、後処理)においてはこの例のようにRangeアダプタやアルゴリズムが役立つはずです。

P3481R0 Summarizing std::execution::bulk() issues

std::execution::bulk()の改善提案。

std::execution::bulk()はP2300でC++26に導入された並行処理フレームワークライブラリの一部で、指定された並行操作を指定された数実行することを表すsenderアダプタであり、すなわち処理のバルク実行を行うためのものです。

namespace std::execution {
  // 関数として書いた時のシグネチャ
  template<
    execution::sender Predecessor,
    typename ExecutionPolicy,
    std::integral Size,
    std::invocable<Size, values-sent-by(Predecessor)...> Func
  >
  execution::sender auto bulk(Predecessor pred, Size size, Func f);
}

これは、先行操作predに続く操作として、指定された関数fを、size個数分バルク実行することを表すsenderを返します。これは、バルク実行を実装(ハードウェア)に応じて最適な形で実行可能にすることを意図するものです。なお、実際にはCPOとして定義されるため、このような分かりやすい関数の形で定義されてはいません。

この提案はP2300のIssueトラッカーに残されたbulk()に関するIssueをまとめて、その解決を図るものです。この提案では次のような点を明確化することを目的としています

  • bulk()のデフォルトの代替実装を許可すること
  • bulk()は指定された関数の同時呼び出しを行うこと
  • bulk()はキャンセルをサポートすること
  • bulk()が非シーケンシャルな実行を行う場合の例外の挙動を明確に定義する
  • bulk()に指定する関数(オブジェクト)はコピー可能である必要があること
  • bulk()に実行ポリシーを指定できるようにする
  • bulk()は多数の小さな反復処理を行うユースケースに適している

この提案による変更は次のものです

  • bulk_chunkd()を追加し、これを基本的な操作とする
    • 実装は指定された関数を並列実行する際に、チャンク単位での実行が可能になる
      • チャンク処理における局所性を活用できるようになる
    • bulk()のデフォルト実装はbulk_chunkd()へ移譲するようにして、bulk()はカスタマイゼーションポイントとして実行方法のカスタマイズを許可する
  • bulk()およびbulk_chunkd()が実行ポリシーを受け取れるようにする
    • 通常の(非バルクアルゴリズムの)実行がseqであることを考えると、bulk()およびbulk_chunkd()parpar_unseqになる
    • しかし、実装によってどちらがデフォルトであるべきかが異なるのでデフォルト値を埋め込むのは避けて、代わりにbulk()およびbulk_chunkd()の呼び出しごとに実行ポリシーを指定するようにする
  • bulk()のデフォルト実装の制約の緩和
    • 現在のbulk()のデフォルト実装の制約は厳しすぎ、逐次実行することを指定している
    • 指定された関数が指定された範囲(インデックス区間)全体に対して呼び出されることを保証しつつ、その方法は実装定義とする
      • これにより、実装はアルゴリズムを微調整して想定される使用法やハードウェアに最適な形で実行できる
      • このような緩い仕様要求によって次のことが可能になる
        • 指定された関数を並行的に呼び出す
        • 先行senderによって生成された値(先行操作の結果値)をdecayコピーする
        • バルク処理アルゴリズム内でキャンセルを処理する
    • 同時に、指定された関数はコピー可能であることを要求する
    • また、これらの事はデフォルト実装であるbulk_chunkd()の最小要求でもある
  • bulk()における例外処理の明確化
    • バルク実行時には呼び出し中の関数から並行的に複数の例外が送出される可能性があり、これをどう伝播させるかが問題となる
    • その場合の例外伝播方法として、関数の呼び出しで送出された例外のうち任意のものを選択する、とする
      • 例えば、最初に送出されたものを選択する、など

これらによる変更後のシグネチャは次のようになります

namespace std::execution {
  // NEW: algorithm to be used as a basis operation
  template<
    execution::sender Predecessor,
    typename ExecutionPolicy,
    std::integral Size,
    std::invocable<Size, Size, values-sent-by(Predecessor)...> Func
  >
  execution::sender auto bulk_chunked(Predecessor pred, ExecutionPolicy&& pol, Size size, Func f);

  template<
    execution::sender Predecessor,
    typename ExecutionPolicy,
    std::integral Size,
    std::invocable<Size, values-sent-by(Predecessor)...> Func
  >
  execution::sender auto bulk(
    Predecessor pred,
    ExecutionPolicy&& pol, // NEW
    Size size,
    Func f)
  {
    // Default implementation
    return bulk_chunked(
      std::forward<Predecessor>(pred),
      std::forward<ExecutionPolicy>(pol),
      size,
      [func=std::move(func)]<typename... Vs>(Size begin, Size end, Vs&... vs) noexcept(std::is_nothrow_invocable_v<Func, Size, Vs&&...>)
      {
        while (begin != end) {
          f(begin++, std::forward<Vs>(vs)...);
        }
      }
    );
  }
}

やはりどちらも実際にはCPOとして定義されるため、実際にこのような関数の形で定義されるわけではありません。

この修正は後から適用すると破壊的となる可能性があるため、C++26サイクル中に適用することが望ましい、としています。

おわり

この記事のMarkdownソース




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

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