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

文書の一覧

全部で122本あります。

もくじ

P0149R1 Generalised member pointers

メンバポインタの表現可能な範囲を拡張する提案。

現在のメンバポインタはあるクラス型の直接のメンバに対するもののみを表現することができ、これはクラスの先頭アドレスからのオフセットによって実装されています。

同様に、クラスの先頭からの固定のオフセットに存在するもののメンバポインタで表現できないものとして、非仮想基底クラスとメンバのメンバ、メンバの非仮想基底クラスなどがあります。

この制限は以前にも何度か指摘されたものの、実装の制約などから来るものではなく誰も提案していないからそうなっていない、というだけの制限のようです。

この提案は、メンバポインタの現在の制限を緩和して、上記のものを表現できるようにしようとするものです。ここで提案されているのは次の3つです

  1. メンバ変数型へのアップキャスト
  2. メンバのメンバへのポインタの作成
  3. 基底クラスサブオブジェクトへのポインタの作成

1つ目は、メンバポインタを、そのメンバの型の基底クラスにアップキャストする変換を許可することです。次のコードの最後のものがそれにあたります

struct A {};
struct B : A{};
struct C { B b; };


C c;
B*     to_b   = &c.b;   // OK、Normal pointer
B C::* c_to_b = &C::b;  // OK、C++98 member pointer
A*     to_a   = to_b;   // OK、C++98 implicit upcast
A C::* c_to_a = c_to_b; // NG、この提案ではok

ただし、キャスト先が仮想基底クラスの場合は実行時型情報の必要性やABI破壊の懸念などの理由から、ここでは提案されていません。また、これに対応するダウンキャスト(c_to_a -> c_to_bのキャスト)もstatic_castによって許可するようにすることを提案しています。

2つ目は、メンバのメンバへのポインタを取得するための構文を用意するものです。そのために、.[].*の3つの演算子を拡張して、

  • .: 「クラス型T2T1型メンバへのポインタ」型の式と「型T3T2型メンバ」を指定する識別子名、に対して.が適用された場合、「型T3T1のメンバへのポインタ」型の値になる
    • E1.*(E2.identifier)という式は(E1.*E2).identifierと等価である必要がある
      • E1, E2T1, T2型の式、式全体の型はT3
  • []: 「クラス型T1の型T2の配列メンバへのポインタ」型の式に対して[]が適用された場合、「クラス型T1T2型メンバへのポインタ」型の値になる
    • E1.*(E2[E3])という式は(E1.*E2)[E3]と等価である必要がある
      • E1, E2T1, T2型の式、E3T2の有効なインデックス
  • .*: 「クラス型T2T1型メンバへのポインタ」型の式と「クラス型T3T2型メンバへのポインタ」型の式、に対して.*が適用された場合、「クラス型T3T1のメンバへのポインタ」型の値になる
    • E1.*(E2.*E3)という式は(E1.*E2).*E3と等価である必要がある
      • E1, E2T1, T2型の式、式全体の型はT3
struct A { int i; };
struct B {
  constexpr B(){};

  A a{};
  int is[42]{};
};

constexpr A B::* ap = &B::a;
constexpr int (B::*isp)[42] = &B::is;
constexpr int A::*ip = &A::i;
constexpr B b;

constexpr auto& i_1 = (b.*ap).i;      // OK, C++98
constexpr auto& i_2 = b.*(ap.i);      // NG、この提案ではok
constexpr auto& is7_1 = (b.*isp)[7];  // OK, C++98
constexpr auto& is7_2 = b.*(isp[7]);  // NG、この提案ではok
constexpr auto& i_3 = (b.*ap).*ip;    // OK, C++98
constexpr auto& i_4 = b.*(ap.*ip);    // NG、この提案ではok

static_assert(&i_1 == &i_2);          // NG、この提案ではok
static_assert(&i_1 == &i_3);          // NG、この提案ではok
static_assert(&i_1 == &i_4);          // OK, C++17
static_assert(&is7_1 == &is7_2);      // NG、この提案ではok

ここではメンバポインタの取得ではなくメンバポインタを介したオブジェクト参照の取得までやっているので、少しわかりづらいかもしれません。.の場合はT2 = B, T1 = A, T3 = int[]の場合はT2 = B, T1 = int.*の場合はT2 = A, T1 = int, T3 = Bと読み替えると少しわかりやすいかもしれません・・・

最後は、クラス型の基底クラスサブオブジェクトを指すメンバポインタを生成する構文です。

struct A {};

template <int N>
struct B : A {};

struct C : B<0>, B<1> {};

この場合に、Cの基底クラスAのサブオブジェクトを指すA C::*のメンバポインタを作成できるようにしたいわけです。しかし、基底クラスのサブオブジェクトを指定する構文はないので、それを用意する必要があります。この提案ではそれを用意せずに、最初の場合のメンバポインタのアップキャストの場合の延長で、thisのアップキャストによって基底クラスサブオブジェクトへのメンバポインタを生成することを提案しています。

そのための構文には&T::thisというものを提案しており、これの結果は型TTメンバへのポインタを形成し、それが暗黙的にA C::*などにアップキャストされることで基底クラスサブオブジェクトへのポインタを得ることができます。

A    A::* a_to_a    = &A::this; // この提案のthisサブオブジェクトへのメンバポインタ取得
A B<1>::* b1_to_a   = a_to_a;   // C++98 class type downcast
A    C::* c_to_b1_a = b1_to_a;  // C++98 class type downcast

C    C::* c_to_c    = &C::this; // この提案のthisサブオブジェクトへのメンバポインタ取得
B<1> C::* c_to_b1   = c_to_c;   // この提案のメンバ型のアップキャスト
A    C::* c_to_b1_a = c_to_b1;  // この提案のメンバ型のアップキャスト

A    C::* c_to_b1_a = &B<1>::this; // 上記2つの提案機能の組み合わせ

当然ですが、これは仮想基底クラスに対しては使用できません。

現在のメンバポインタの実装は一般的に、クラス先頭アドレスからのオフセットとして表現されているため、これらの機能は比較的容易に実装できるはずだとしています。ただし、仮想基底クラスへのポインタは単なるオフセット以上のものを持つため、この方法では表現可能ではありません。

しかしなぜか、MSVCは仮想基底クラスへのメンバポインタ的な機能を実装しているようで、そこでは単なるオフセット以上の複雑な表現を持っています。そして、ここで提案されている機能とMSVCのこれは本質的に非互換なものなので、MSVCの機能に対して段階的な廃止を推奨しています。

P0260R14 C++ Concurrent Queues

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

以前の記事を参照

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

  • 寄せられたフィードバックに基づく文言更新
  • ノンブロッキング関数の例を追加
  • std::expectedについての説明を追加
  • async senderの動作についての説明を追加

などです。

P0876R19 fiber_context - fibers without scheduler

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

以前の記事を参照

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

  • 例外状態テストプログラムを付録に移動
  • Windows/Linuxにおいてlibstdc++を使用してファイバー固有の例外処理を正しく動作させるBoost.Contextのパッチへのリンクを追加
  • ファイバーを用いて構築された6つの製品レベルライブラリへのリンクを追加

などです。

P1030R8 std::filesystem::path_view

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

以前の記事を参照

このリビジョンでの変更は、pathのフォーマットオプションに??path_viewのバックエンドがbyteの場合のみパスをエスケープ済み文字列としてフォーマットする)を追加したことです。

この提案はLEWGでレビューされていましたが、機能を支持する人がいないとのことで追及が停止されています。

P1839R7 Accessing object representations

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

以前の記事を参照

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

  • volatileオブジェクトではオブジェクト表現へのアクセスは許可されなくなった
  • std::launderは同じ型のオブジェクトが複数存在する場合に未規定の選択を返すようになった
    • また、Tcv std::byteまたはその配列型である場合も許可される
    • これにより、オブジェクト表現へのポインタまたはその配列へのポインタを取得できるようになる
  • synthesized object表現の要素の生存期間は、構築と破棄の全期間をカバーするものと見做されるようになった
  • synthesized object表現は初期化する必要がないこと(つまり、適切なストレージが確保され次第生存期間が開始される)と、そのストレージが実際のオブジェクト内にネストされたオブジェクトによって再利用されるものとはみなされないこと、を明確にした
  • p(b)の記述において、非アクティブな共用体メンバとそのサブオブジェクトが除外されるようになった
    • そのため、可能な場合はアクティブな共用体メンバが取得され、そうでない場合そのビットはパティングビットとして扱われる
  • 未規定のオブジェクト表現ビットは安定的(値は未規定のまま)であり、不確定でもエラー性でもない事が明示的に指定されるようになった
  • char/charの配列のオブジェクト表現の要素の値を決定する際に、“congruent to”という言葉を使用するようにした
  • サブオブジェクトのオブジェクト表現は、もはや囲むオブジェクトのオブジェクト表現内にネストされているとはみなされない
    • 代わりに、オブジェクト表現がストレージを共有し、他のオブジェクトのストレージを再利用しないことを許可する明示的な例外を設けた
    • また、synthesized object表現はストレージを提供しないことを明確にする
  • non-potentially-overlappingサブオブジェクトのオブジェクト表現に関する文言の改善
  • unsigned charの完全な配列のみがそれ自身のオブジェクト表現として許可される理由の説明の追加
  • strict aliasing ruleは、オブジェクト表現へのアクセスが元のオブジェクトではなくunsigned charオブジェクトへのアクセスになった事を反映するように変更された
  • char*へのキャストによってオブジェクト表現の要素へのポインタを取得する、という文言を復元
    • R3で誤って削除されていた
  • [expr.static.cast]p13から“would be well-formed”である条件を削除
    • const性を破棄するケースは段落の前部分でチェックされる

などです。

P2079R6 System execution context

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

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

  • 2024/11のWrocław会議におけるSG1/LEWGのフィードバックを反映
  • delegatee schedulersに関する文言を削除
  • system_schedulersenderは、std::exception_ptr引数を用いてset_errorで完了できるようになった
  • set_stopped_tに対するsystem_schedulerquery(get_completion_scheduler_t)オーバーロードを削除
  • system_schedulerの特殊メンバ関数noexceptを付加
  • system senderクラスに左辺値connectを追加
  • system senderクラスにsender_concept, completion_signatures, get_envを追加し、senderとする
  • replaceabilityを義務付ける
  • replaceabilityメカニズムを定義する(実行時とリンク時の両方)
  • replaceabilityのAPIを追加
  • Specificationセクションを追加

などです。

P2414R5 Pointer lifetime-end zap proposed solutions

Pointer lifetime-end zapと呼ばれる問題の解決策の提案。

以前の記事を参照

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

  • P2434への参照を更新
  • Martin Uecker氏を著者から貢献者リストへ移動(本人の要望
  • SG1からのフィードバックを適用
    • usable_ptr<T>エイリアスを追加
    • usable_ptr<T>operator*の宣言を修正
    • volatile atomic<T>へのキャストコードを修正
    • P2434R1の“words of power”について由来を調査したものの発見できなかった
  • EWGからのフィードバックを適用
  • N4993のWDに対応するために、“representation bytes”から“value representation”へ更新
  • ポインタのprovenanceが確立される前に、オブジェクトの複数のインスタンスが作成および削除される可能性を考慮して、“prospective pointer value”の定義を更新

  • P2414 進行状況

P2434R3 Nondeterministic pointer provenance

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

以前の記事を参照

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

  • N5001にリベース
  • 文言中の%pを処理
  • エイリアシングの例を強化
  • アライメントに関する議論を(軽微に)修正
  • 編集手順の明確化

などです。

P2654R1 Modules and Macros

標準ライブラリで提供されるマクロを、標準ライブラリモジュール(std.compat)からエクスポートするようにする提案。

以前の記事を参照

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

  • C23でATOMIC_VAR_INITが削除されたが、ATOMIC_FLAG_INITは削除されていないことを反映
  • offsetofvar-argsの提案をより適切に説明
  • <cassert>contract_assertによって不要になったと結論付けた
  • Cストリームオブジェクトに対するP3208R0の作業への参照を追加
  • Cスタイルの可変長引数関数に関するP3550R0の作業への参照を追加
  • マクロのカテゴリを中心に提案を再構成

などです。

P2663R6 Proposal to support interleaved complex values in std::simd

std::simdstd::complexをサポートできるようにする提案。

以前の記事を参照

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

  • P1928R13で現在使用されているスタイルに合致するように文言を更新
  • LWGレビューコメントへの対応
  • 再確認が必要なLEWG設計イシューをいくつか指摘

などです。

P2664R9 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

  • P1928R15の変更に合わせて、若干の名称変更
  • basic_simd_maskの引数として要素サイズではなく型が渡されていたミスを修正
  • gather_frompartial_gather_from/unchecked_gather_fromに拡張し、それぞれの動作を適切に反映した
    • scatter関数についても同様

などです。

P2688R5 Pattern Matching: match Expression

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

以前の記事を参照

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

  • 文言の改善
  • Alternative Patternの説明を、std::castからADLによるtry_castに変更

などです。

P2719R2 Type-aware allocation and deallocation functions

P2719R3 Type-aware allocation and deallocation functions

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

R2での変更は、提案する文言を追加したことです。

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

  • EWGレビュー用に設計上の選択肢を追加
    • 定数式の評価について
    • 修飾子の削除について
    • 一致する宣言スコープの強制について
  • コルーチンとの相互作用について言及
  • より包括的な文言に改善
    • 機能テストマクロを追加
    • [expr.new]と[expr.delete]の型同一性パラメータの内容をより適切に説明
    • 型を認識するoperator deleteはdestroying operator deleteにはならないことを明示的に記述
    • 定数式で型を認識するoperator newを使用できるようにするための文言を追加
  • Brian, Corentin, Richard氏からのフィードバックを反映

などです。

P2746R7 Deprecate and Replace Fenv Rounding Modes

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

以前の記事を参照

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

  • いくつかの整理
  • 実装に関する議論を追加
    • 実装作業は未完了
  • SG6がこの提案とP3375との関係について確認するまで文言の確定は延期
    • この確認によっては、ここに記載された詳細の一部が影響を受ける可能性がある

などです。

P2758R4 Emitting messages at compile time

コンパイル時に任意の診断メッセージを出力できるようにする提案。

以前の記事を参照

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

  • 文言変更
  • CWG/LEWGへの再ターゲット
  • 適切な文言とエスカレーション問題への対処のため、constexpr-erroneousの概念を導入

などです。

constexpr-erroneousの概念は、定数式におけるエラーを識別し、その式の評価が実行時に延期されることなくコンパイルエラーにするための規格上の仕組みを指す言葉です。

この提案では、constexpr_error_str()が呼ばれるとコンパイルエラーを起こして定数式を中断するとともに指定されたメッセージをエラーメッセージとして出力しますが、通常の定数式は必ずしも定数評価が必要でない場所では定数式の評価が行き詰まると実行時に評価するようにフォールバックしてコンパイルエラーにはならない場合があります。例えば静的記憶域期間の変数の初期化式があり、定数初期化ができない場合は動的初期化にフォールバックします。

constexpr_error_str()が示すエラーはこのようなフォールバックの対象外のエラーであり、なおかつ確実にそこでコンパイルを止めることを意図しています。この種類の失敗を識別するために、constexpr-erroneous (value)というコンパイル時のエラーカテゴリを追加し、constexpr_error_str()はconstexpr-erroneousカテゴリのエラーを引き起こすように指定されます。

P2806R3 do expressions

値を返せるスコープを導入するdo式の提案。

以前の記事を参照

このリビジョンでの変更は、P3549R0 Diverging Expressions におけるdivergenceに関する長い議論の文言と参照を追加したことです。

P2825R4 Overload resolution hook: declcall( unevaluated-call-expression )

与えられた式が呼び出す関数の関数ポインタを取得する言語機能の提案。

以前の記事を参照

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

  • 提案する文言を追加し、レビューによって修正
  • メンバ関数へのdevirtualizedポインタを追加

などです。

P2830R8 Standardized Constexpr Type Ordering

P2830R9 Standardized Constexpr Type Ordering

std::type_info::before()constexprにする提案。

以前の記事を参照

R8での変更は、LWGメーリングリストでの指摘に伴う文言の修正です。

このリビジョンでの変更は、LWGレビューによるeditorialな文言修正です。

P2841R6 Concept and variable-template template-parameters

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

以前の記事を参照

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

  • コンセプトテンプレートパラメータに制約を行えないことを指定
  • concept-nameが依存関係にある場合、concept-idは値に依存することを指定
  • テンプレートテンプレートパラメータがコンセプト依存の制約(concept dependent constraint)によって制約されないことを指定

などです。

テンプレートテンプレートパラメータがコンセプト依存の制約(concept dependent constraint)によって制約されないことを指定

この変更は次のような場合に対処するためのものです

template <
  template <typename> concept C,
  template <C> class TT
>
struct A {
  static int x;
};

template <
  template <typename> concept C,
  template <C> class TT
>
int A<C, TT>::x = 0; // Aを指名することができない(R5まで

C++におけるテンプレートの半順序のルールは歴史的に、「置換前に、テンプレート引数に依存せずに決定可能」でした。この例では、TTはコンセプトCによって制約されるテンプレートパラメータを一つ持つ型であることを表していますが、具体的なCが渡されるまではテンプレートパラメータを取ることとその数くらいしか分からず、Aの特殊化が複数ある場合のマッチングにおける半順序をCが渡される前に決定することが困難になります。

そのためこの提案では、このようなCの使用を禁止することでこのような状況が起こらないようにして、C++のテンプレートにおける半順序のルールとその実装の複雑化を回避しています。

P2843R1 Preprocessing is never undefined

プリプロセッサに存在する未定義動作を取り除く提案。

以前の記事を参照

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

  • コンパイル中のUBに関する根拠をintroductionに組み込み
  • 関連領域の未解決のコア言語Issueのリンクを追加
  • target audienceにCのliaisonを追加
  • P2621R2採択後のWDにリベース
  • 採択済みの提案についての議論をすべて削除
  • conflict resolutionに関するサブセクションを削除
  • 提案された文言をN5001にリベース
  • definedテストにおけるコンパイラの適合性を更新
    • 全てのコンパイラがこの提案の推奨事項に準拠している
  • UBからIFNDRへの残りのケースにおける診断を強化する初期分析を完了
  • ライブラリによるキーワードの再define禁止をプリプロセッサ仕様に盛り込む事を提案
  • プリプロセッサ仕様で使用されているものを含む、禁止されるマクロ名のリストを完成させることを提案
  • ill-formed commentsの診断を提案
  • 未解決のコア言語Issueに関する文言にセクションを追加
  • この提案が採択された後の実装の変更について、コンパイラ毎の概要を追記

などです。

P2883R1 offsetof Should Be A Keyword In C++26

offsetofキーワード化し、offsetofマクロの機能を言語機能とする提案。

以前の記事を参照

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

  • EWG, Varna 2023のレビューを記録
  • レビューを受けてのモチベーションの強化
  • CWG2784に関する議論を追加
  • リフレクションを用いたライブラリソリューションについて議論
  • P1839R6とそのフォローアップであるP3407R0についての議論を追加
  • 使用経験あるいはそれに近い情報について追加
  • 提案する文言を追加

などです。

この提案はEWGにおけるレビューにおいてコンセンサスを得ることができず、一旦追及は停止されています。

P2899R0 Contracts for C++ - Rationale

P2900のContracts提案に関する、議論の流れや設計根拠についてまとめた文書。

P2900では文言の他には、結果として決定された設計についての説明が記述されていますが、その設計に至った経緯などについてはほぼ記述されていません。それらは個別の提案とその議論および投票などに散らばっており、その数の多さも相まって追うのはかなり困難を極めます。

この文書は、P2900の設計に至るまでの提案とその議論の流れ、各提案や機能についての議論と投票結果などをまとめた文書です。

文書は"Overview"と"ProposedDesign"の2つのセクションに分かれています。

"Overview"では、Contractsのモチベーションや目標、契約プログラミングそのものについて、およびC++20以前からP2900(C++26 Contracts)に至るまでの提案と議論の流れがまとめられています。

"ProposedDesign"では、P2900の各セクション/サブセクションごとにその動機の要約や設計根拠の説明、設計上の決定の履歴、および関連する提案文書へのリンクがまとめられています。

また、この文書はP2900の今後の更新に追随して、その変更に関する同様のまとめを追記していく予定です。

かなり重厚な文書ですが、読み通すことでC++26のContractsの設計について理解することができるでしょう。

P2900R13 Contracts for C++

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

以前の記事を参照

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

  • prepostmainや違反ハンドラなどの特別な関数にも適用できることを明確化
  • 文言の見直し

などです。

P2933R3 Extend header function with overloads for std::simd

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

以前の記事を参照

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

  • 概要に不足していた文言を追加
  • 詳細な動作説明をP1928の最近の更新スタイルと文言に合わせて変更
  • 要素が整数・符号なしなどであるSIMD型を検出するコンセプトを追加
  • rotl/rotrオーバーロードを追加し、第二引数にスカラを指定できるようにした

などです。

P2952R2 auto& operator=(X&&) = default

defaultな特殊メンバ関数の戻り値型をauto&で宣言できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、drafting noteの誤りを修正したことなどです。

P2953R1 Forbid defaulting operator=(X&&) &&

右辺値修飾されたdefault代入演算子を禁止する提案。

以前の記事を参照

このリビジョンでの変更は、EWGにおける投票結果を追加したことです。

P2971R3 Implication for C++

記号論理における含意記号の振る舞いをする=>演算子の提案。

以前の記事を参照

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

  • 全体を通した編集上の修正
  • トークンの選択についてのサブセクションを追加
  • N4993へリベース

などです。

P2988R9 std::optional<T&>

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

以前の記事を参照

このリビジョンでの変更は、optional<U&>からoptional<T>へのムーブ代入時に参照先のU&をムーブしてしまうバグを修正したことなどです。

P2996R9 Reflection for C++26

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

以前の記事を参照

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

  • value_of()/extract()consteval関数内のローカル変数のリフレクションで使用可能であることを保証
  • define_aggregate()は現在定義中のクラスには使用できない事を明示的に指定
  • members_of(closure-type)は未規定のリフレクションのシーケンスを返す
  • 定数評価中の評価に強い順序付けを導入
  • static_assert宣言のアサーションは単純な定数評価式ではない
  • コアとなる文言の変更
    • 宣言の注入を 副作用として分類
      • 関連するIFNDR条件を[expr.const]から削除
    • [lex.phases]の修正
      • R7で導入された“semantically follows”関係をリファクタリングし、単純な定数評価式が副作用と到達可能性によって評価される条件を定義する
      • “semantically sequenced”関係を、注入された宣言をill-formedにする[expr.const]の条件にインライン化する
  • 注入された宣言のルールを表す[expr.const]の条件の例を改善
  • ライブラリの文言変更
    • 型特性の文言を若干改善
      • type_of, alignment_of, bit_size_of, data_member_spec, define_aggregate
    • consteval関数内のローカル変数にvalue_of()を適用できないバグを修正
    • substituteの注記において、プレースホルダ型を推論するために必要な場合はインスタンス化がトリガーされる可能性があることを明確化
    • クロージャ型のうち、どのメンバがmembers-of-reachableであるかを明確化
    • 完全クラスコンテキストにおけるmembers-of-reachableの文言バグを修正し、members-of-precedesに名称変更
    • define_aggregate()から冪等性を削除
  • R8の改訂履歴を充実

などです。

P3019R12 Vocabulary Types for Composite Class Design

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

以前の記事を参照

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

  • indirectの概要を修正、デフォルトコンストラクタにexplicitを追加
  • indirectpolymorphicの仕様において、“may only be X”を“may be X only”に置き換える
  • Tが不完全型になる可能性がある場合のTに対するconstraintsをmandatesにする
  • type_traitsによって暗黙的に要求される完全型要求を削除
  • indirectにおけるTはコピーコンストラクタに対してのみcopy-constructibleである必要がある
  • 制約と不完全型のサポートに関する説明を追加
  • <=>の仕様を修正し、synth-three-way-resultを使用する
  • indirectoperator==のconstraintsをmandatesにする
  • indirectoperator<=>のconstraintsを削除
  • 制約と比較に関する設計変更を反映するために、non-technical specificationセクションを更新

などです。

P3045R5 Quantities and units library

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

以前の記事を参照

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

  • ハードウェア電圧測定値の読み出しの例の説明を修正
  • text_encodingcharacter_setにリネーム
  • mp_units名前空間stdに置き換え
  • absoluteヘルパをpointにリネーム
  • 式テンプレートをsymbolic expressionsにリネーム
  • 使用例を追加
  • “Minimal Viable Product (MVP) scope”を"Core Library Framework scope"に変更
  • half_high_dotにスペースを出力する機能を追加
  • QuantitySpecOfUnitOfのコンセプトを簡素化
  • QuantityOfQuantityPointOfコンセプトをReferenceOfで制約
  • 数量サブカテゴリを超える変換が"Nested quantity kinds"セクションに追加
  • P2830R7を"Simplifying the resulting symbolic expressions"セクションで紹介
  • テキスト出力時には、スケール付き単位は[...]ではなく(...)で囲まれるようになった
  • 単位のフォーマットのセクションから無効な[ISO/IEC 80000]引用符を削除
  • "Symbols of common units"セクションに明示的な変換の例を追加
  • symbol_textUTF-8プリントルールを追加
  • unit-symbol-solidus代替文法を追加
  • std-format-specの章に拡張機能を追加
  • テキスト出力に関する未解決の質問を追加

などです。

P3070R2 Formatting enums

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

以前の記事を参照

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

  • Wording, Alternatives Considered, Acknowledgementsのセクションを追加
  • std::byteフォーマットついては別の提案で議論することになった

などです。

P3081R1 Core safety profiles for C++26

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

以前の記事を参照

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

  • SG23/EWGのフィードバックを適用
  • initialization safety profile を type safety profileに統合
  • 議論の的になっていた部分を延期
    • static_castdynamic_castで処理したり、dynamic_castのパフォーマンス保証など
  • 様々な改善を適用
    • std::byteへのreinterpret_castやポインタからuintptr_tへのキャストの許可など
  • SG23の推奨に従って、非ローカルな生存期間解析をP3465R1へ分割
  • 文言の追加

などです。

P3086R3 Proxy: A Pointer-Semantics-Based Polymorphism Library

静的な多態的プログラミングのためのユーティリティ、"Proxy"の提案。

以前の記事を参照

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

  • モチベーションを更新
  • 9つの名前付き要件を追加
  • proxy_indirect_accessorクラステンプレートを追加
  • proxy::invoke(), proxy::reflect()をフリー関数に変更
  • proxyアクセシビリティサポートを追加
  • proxy::operator bool(), proxy::operator->(), proxy::operator*()を追加
  • std::swap(proxy, proxy)の定義をフレンド関数に変更

などです。

P3094R6 std::basic_fixed_string

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

以前の記事を参照

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

  • convertible_to<charT>same_as<charT>に変更
  • structural typesについては、P2484R0をP3380R1に置換
  • 関連提案を更新
  • std::string_literalの章を追加
  • 文字列リテラルからのコンストラクタがP3491R0で拡張
  • inplace_stringの章に投票結果を追加
  • Compiler Explorerのリンクを更新

などです。

P3111R3 Atomic Reduction Operations

std::atomicにアトミックリダクション操作を追加する提案。

以前の記事を参照

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

  • atomic reduction sequenceの置換を表にリファクタリング
  • 整数リダクション操作の場合と同じ有効な置換を提供するために、"as if"整数演算を利用したアトミックリダクションシーケンスの置換に代替の表現を提供する

などです。

P3125R2 constexpr pointer tagging

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

以前の記事を参照

このリビジョンでの変更は、設計の改善と文言の追加などです。

P3139R1 Pointer cast for unique_ptr

std::const_pointer_caststd::dynamic_pointer_caststd::unique_ptrオーバーロードを追加する提案。

以前の記事を参照

このリビジョンでの変更は、仮想デストラクタの代わりにdestroying deleteを許可したことです。

P3148R1 Formatting of chrono Time Values

chronoの値のフォーマットに関する問題を解決する提案。

以前の記事を参照

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

  • ゼロパディング問題(12時間表示の細部)に対する異なるアプローチを提案
  • 精度指定子の位置と動作に関する議論に対する改善

などです。

このリビジョンでの提案内容はそれぞれ次のようになります、

時間単位([h])の表示オプション

add? 指定子 意味 29h + 15min 3h 16h
%H 時間、0埋め 29 03 16
+ %K 24時間時計の時間、0埋め 05 03 16
+ %k 時間、端数 29.25 3 16
%I 12時間時計の時間、0埋め 05 03 04
+ %i 12時間時計の時間 5 3 4

(第一列の + は提案しているものであることを表します)

分単位([min])の表示オプション

add? 指定子 意味 15min + 45s 3min
%M 60進数の分、0埋め 15 03
+ %f 分、端数 15.75 3

秒単位([sec])の表示オプション

add? 指定子 意味 90s + 500ms 3s + 500ms
%S 60進数の秒、0埋め 30.500 03.500
+ %s 秒、端数 90.500 3.500

複合表示オプション

add? 指定子 同等表現 意味 15h+25min+45s
%T %H:%M:%S 24時間時計表示、秒の端数まで 15:25:45
+ %N %i:%M:%S 12時間時計表示、秒の端数まで 3:25:45
%R %H:%M 24時間時計表示、秒以下なし 15:25
+ %P %i:%M 12時間時計表示、秒以下なし 3:25
%r :%I:%M:%S %p %N+0埋め+AM/PM 03:25:45 PM
+ %l :%i:%M:%S %p %N+AM/PM 3:25:45 PM

精度の指定に関してはP2945に委譲されているようです。

P3164R3 Early Diagnostics for Sender Expressions

提案中のExecutorライブラリにおいて、senderチェーンのエラーを早期に報告するようにする提案。

以前の記事を参照

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

  • 最新のWDにリベース
  • transform_completion_signaturesの再指定を削除して、型エラーを伝播する
    • 型エラーについてはP3557で対処される
  • stopped_as_optionalを調整して、その子sendersingle-senderコンセプトを満たすことを必須とし、single-senderコンセプトを変更してnon-dependent senderでも適切に動作するようにする
  • [exec.snd.general]にsenderアルゴリズムのユーザー定義カスタマイズによって、デフォルト実装ではnon-dependent senderが生成されることを保証する要件を追加
  • non-dependent senderの作成をサポートするために、説明専用のbasic-senderヘルパを追加
  • 説明専用のsender-ofコンセプトを更新し、non-dependent senderも処理できるようにする
    • つまり、sender-of<Sndr, int>sender_in<Sndr>を包摂する
  • run_loopschedulerに対してscheduleを呼び出した際に返されるsenderがnon-dependentであることを指定する

などです。

P3176R1 The Oxford variadic comma

前にカンマが無い省略仮引数引数(...)の使用を非推奨にする提案。

以前の記事を参照

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

  • N4981へリベース
  • "Editorial changes"と"Normative changes"のサブセクションを"§ 7 Proposed wording"へインライン化
  • parameter-declaration-clauseの拡張案の順序を変更
  • 新しい段落を追加せず、既存の段落を更新する

などです。

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

P3179R5 C++ parallel range algorithms

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

以前の記事を参照

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

  • [algorithm.syn]に必要な文言を追加
  • 変更がない場合でも、その文脈のアルゴリズムの説明を提案文言に含めるようにする
  • random-access-sized-rangeコンセプトを説明専用にする
  • 既存の機能テストマクロをバンプする
  • ExecutionPolicyを短縮し、文言内のexecution-policyコンセプトを斜体で表示する
  • 出力として範囲を使用するアルゴリズムについては、result_last(または類似名)を使用する
    • ただし、"Issues to address"/"in Out-of-scope"に記載がない限り
  • fill/generateアルゴリズムファミリを修正し、設計の一貫性を保つためにoutput_iterator/output_rangeの代わりにindirectly_writableを使用する
  • copy_nアルゴリズムファミリにsentinel-for-outputを追加
  • "design overview"に説明専用コンセプトの簡単な説明を追加
  • 設計上の問題のため、rotate_copyをスコープ外にする

などです。

P3206R0 A sender query for completion behaviour

senderの完了動作についてのクエリ方法の提案。

P2300以前、P2257R0ではsenderに対するプロパティの指定とそのクエリに関する一般化された方法が提案されていました。P2300への移行の際にsenderに対するプロパティ指定が削除されたことでそのクエリも同時に削除されていました。senderに対するプロパティとは例えば、処理の優先度やブロッキングの有無などです。

この提案は、そのようなsenderのプロパティの一つである完了に関する動作についてのクエリ方法を提案するものです。

完了に関する動作とは、senderexecution::start()したときにそのsenderで表現されている非同期操作がどのように完了するかということです。そのような動作には

  • インライン: startとしたのと同じスレッドで処理が完了する
  • 同期: startがリターンする前に処理が完了する
  • 非同期: 処理の完了はstartのリターン以降かつ、それとは事なるスレッドで発生する

の3つがあります。

この3つの状態のいずれで完了するのかを静的に知ることができれば、receiverとの接続後のoperation_stateの状態などを最適化できる可能性があります。例えば、インライン/同期のいずれかで完了することが分かっているのであれば、startの呼び出しに同期はいらず、operation_stateの生存期間管理のための追加の作業が不要になります。

コルーチンのawaitable型はawait_ready()が返すbool値によってこの情報を伝達するチャネルを持っていますが、std::executionにはそれがありません。この提案は、senderに対してそのようなクエリAPIを追加しようとするものです。

提案するAPIは、execution::get_completion_behaviour(sndr,env)です。これはsendersndr)と接続するreceiverの環境envによるカスタマイゼーションポイントであり、次のいずれかの値を返すものです

  • completion_behaviour::inline_completion: インライン完了
    • 接続されたreceiverの完了シグナルは、execution::start呼び出しのリターン前に呼び出しスレッドで発生する
  • completion_behaviour::synchronous: 同期完了
    • 接続されたreceiverの完了シグナルの発生とexecution::start呼び出しのリターンの間には、happens-before関係が成立する
  • completion_behaviour::asynchronous: 非同期完了
    • 接続されたreceiverの完了シグナルの発生は、execution::start呼び出しのリターン前に呼び出し元スレッドで発生することは無い
  • completion_behaviour::unknown: 不明
    • 完了の動作は不明

このカスタマイズポイントとしては、sender型のメンバ関数としてenvを受け取る形で実装するか、型エイリアスとして定義するかの2つを提案しています。

// メンバ関数として実装
template<class InputSender>
struct example_sender_adaptor {
  // [...]
  template <class Self,class...Env>
    requires (sizeof...(Env) <=1)
  constexpr auto get_completion_behaviour(this Self&& self,Env&&...env) noexcept {
    return execution::get_completion_behaviour(std::forward_like<Self>(self.sender), std::forward<Env>(env)...);
  }

  InputSender sender;
};

// 型エイリアスとして実装(環境に関係なくsender型のみで完了動作が決まる場合
struct example_sender1 {
  // [...]
  using completion_behaviour = constant<execution::completion_behaviour::asynchronous>;
};

senderに対して完了動作をクエリ可能(特に静的にクエリ可能)になることによって、例えばsync_wait()では同期プリミティブが不要になり、repeatの様なアルゴリズムsenderの処理を繰り返すアルゴリズム)では入力senderの完了(set_value)後にそのsenderの操作を呼び出すようなsenderを構成してそれを実行する必要がありますが、インライン/同期完了であることが分かっていれば、単なるwhileループによって実行可能になります。

さらに、senderをコルーチンで扱うためにawaitableに変換するas_awaitableにおいてはこれまでsenderの完了動作について知る方法が無かったため、ネストしたコルーチン(sender)に対してsymmetric transferを適用することができませんでしたが、このクエリがあればそれを可能にする機会が得られるようになります。

また、提案では既存のsenderアルゴリズムに対してこのクエリを実装した場合にどのような値を返すべきかの検討もしています。

  • senderファクトリ
    • schedule(run_loop::scheduler)
      • asynchronous
    • just(), just_error(), just_stopped()
      • inline_completion
    • read_env()
      • inline_completion
  • senderアダプタ
    • finally(sender1, sender2), continues_on(sender, scheduler), starts_on(scheduler, sender)
      • finally(sender1, sender2)に対して、min(get_completion_behaviour(sender1, env), get_completion_behaviour(sender2, env))
    • then(sender, fn), upon_error(sender, fn), upon_stopped(sender, fn)
      • get_completion_behaviour(sender, env)
    • let_value(sender, fn), let_error(sender, fn), let_stopped(sender, fn)
      • min(get_completion_behaviour(sender, env), get_completion_behaviour(rs, env)...)
        • rs...fnが返しうる結果sender
    • into_variant(sender), stopped_as_optional(sender)
      • get_completion_behaviour(sender, env)
    • bulk(sender, shape, fn)
      • get_completion_behaviour(sender, env)
    • when_all(senders...)
      • min(get_completion_behaviour(senders, env)...)
    • split(sender)
      • 動的な値を返す
        • split(sender)の呼び出しがsplitの新しいコピーを返す前に入力操作が完了していれば、インライン完了
        • それ以外の場合、get_completion_behaviour(sender, env)

ここでは、unknown < asynchronous < synchronous < always_inlineの順に順序付けしたうえで、minはこの順序の上で小さい方を返すものです。

P3229R0 Making erroneous behaviour compatible with Contracts

Erroneous Behavior(EB)をContracts都のフレームワークに取り込む提案。

この提案は、P3100R1で提案されているUB/EBをContractsの下で統一的に扱うという構想の第一歩となるもので、まずEBをContractsの枠組みに取り込もうとするものです。

P3100R1については以前の記事を参照

EBと契約はどちらも、プログラムの実行中に欠陥が検出されたもののその時点での動作はwell-definedである(UBではない)という、というプログラムの欠陥への対処として同じ方向性の概念を示しています。しかし、C++26においてこれらは別々のツールとして機能的な関連性をほとんど持たない状態で併存しています。

EBが発生した場合に実装取ることのできる動作オプションと契約の評価セマンティクスの間には一定の互換性があります

  • 診断を発行して終了する: デフォルトの違反ハンドラの下でenforceセマンティクスを使用することと同型
  • 診断を発行せず終了する: quick-enforceセマンティクスを使用することと同型
  • 診断を発行して継続する: observeセマンティクスを使用することと同型
  • 誤った値をチェックせず継続する: ignoreセマンティクスを使用することと同等

違反ハンドラ周りの仕様を無視すれば、誤った値(erroneous values)を生成する式Eの評価は次のような関数の呼び出しと同等になります

auto eval_E()
  post (r: !is_erroneous(r))
{
  return E;
}

これらの観点から、この2つのアプローチはかなり類似したものであり、ほとんどその用語が違うだけであることが分かります

  • Erroneous behaviour: well-definedではあるものの誤っている動作を検出し、その動作が発生する条件を明記する
  • Contract assertions: 明示的にその動作が誤りであるという条件を指定し、違反が発生した場合に生じる動作(EBの場合もあればUBの場合もある)の実装をユーザーに委ねる

これらの比較から、契約違反後(違反ハンドラ呼び出し後)のwell-definedな動作とはErroneous behaviourそのものであり、ある操作についてEBを指定する条件とは、その操作についての契約アサーションに他ならないことが分かります。唯一異なる点は、EBは契約違反後のwell-definedな動作であるのに対して、契約違反後に継続する場合はUBになる場合もある(ignore/observeセマンティクスの場合)という点です。

ただし、どうしても両機能が異なる点がいくつかあります

  1. 違反ハンドラ
    • 診断メッセージ出力はどちらにおいても推奨される動作だが、契約の場合は違反ハンドラの差し替えによってユーザーがこの挙動をオーバーライドできるのに対して、EBでは提供されていない
  2. 終了方法の規定
    • 契約注釈が終了するセマンティクスで評価された場合、適合する3つの終了モードが指定されているが、EBでは指定されていない
      • 終了モードとは、std::terminate()std::abort()、実装定義の方法(トラップ命令など)、の3つ
  3. その後の状態
    • 契約違反によってプログラムが終了する場合、契約アサーションの評価の一部として違反が処理された直後に終了する
    • EBでは、その操作の後の未規定の時点、とされる
      • EBの後、プログラムは事実上のエラー状態となり、その後いつでも突然終了する可能性がある
      • これをDamocles semanticと呼ぶらしい
  4. 定数評価とSFINAE
    • EBはコア定数式とはみなされないため、それによってEBの有無によるSFINAEが可能
    • 契約アサーションの存在及び評価は他のコンパイル時プロパティに影響を及ぼさないようになっているため、SFINAEは不可能

この提案は、EBをContractsの枠組みに取り込むために、これらの矛盾をC++26のうちに解消しておこうとするものです。

提案している変更はつぎの5点です

  1. 暗黙の契約アサーション(Implicit contract assertions)の導入
    • P2900で提案されている契約アサーションを明示的な契約アサーションとして、暗黙の契約アサーションと区別する
    • 暗黙の契約アサーションはコード内で参照することができず、実装によってプログラムに挿入される
      • それ以外の点は両者で同じ仕様を共有する
    • ある組み込みの操作によるEBの発生は、その操作が誤った値を返さないという暗黙の契約アサーションが破られている、とみなす
      • 標準仕様からEBの概念を削除し、代わりに組み込みの操作の評価は誤った値を生成しないという暗黙の事後条件を持つものとする
  2. 違反ハンドラAPIの拡張
    • 違反ハンドラから暗黙の契約アサーションを識別できるように、contract_violationAPIを拡張する
    • .kind()の返す列挙値にimplicitを追加する
      • 現在はpre, post, assertの3つ
    • .detection_mode()の返す列挙値にerroneous_valueを追加する
      • 現在はpredicate_false, evaluation_exceptionの2つ
      • 誤った値が生成されたという判断は実装定義の方法によって行われるため、必ずしもbool述語の評価によらないため区別する
  3. Damocles semantic を “sticky” erroneous values に置き換える
    • 現在の仕様では、誤った値(erroneous values)が生成されるとその値そのものは有害ではないものの、その後のプログラムの動作はエラー状態となり、終了のリスクが上昇する
    • 誤った値がデータに固着し伝播することで、その値を使用する後続の各操作が誤った値を生成するようにする
      • すなわちそのような各操作は暗黙の契約アサーションを破ることになるが、実装はこれらのアサーションの評価をignoreセマンティクスでおこなうことができる
    • これにより、予期しない終了の脅威に晒されるのを防止し、実装の柔軟性を保ちつつ、バグの特定を最も都合の良いタイミングで行える
  4. noexceptとの相互作用の再定義
    • int x;のように宣言されたx(未初期化)に対してnoexcept(x + 1)という式は、現在のC++ではwell-defined(評価されない文脈なので、不定値アクセスは起こらない)
    • しかし、EBが起こっているためこの提案ではx + 1の評価は例外を送出しうる。従って、noexcept(x + 1)trueに評価されることは例外を送出しないことを表さない
    • P3541R1でこの問題が詳細に議論されており、SG21での議論の結果、noexcept演算子trueを返す場合の意味を「契約違反が無い限り例外を送出しない」に変更する方向性が合意された
      • この提案もこれに従う
  5. 定数評価の変更
    • 明示的な契約アサーションの定数式における評価時に使用される3つのセマンティクス(ignore, observe, enforce)にはそれぞれユースケースがある
    • 一方暗黙の契約アサーションにはそれは当てはまらず、定数評価中には誤った値は生成されないという現在の動作を踏まえて、定数評価中の暗黙の契約アサーションの評価セマンティクスをenforceに固定する
    • これにより、EBが起こるかどうかに応じてSFINAEすることは不可能になる

1の変更のみによって、先に挙げたEBと契約の間の異なる点は解消されます。

この提案はC++26への導入を目指していますが、EWGでのレビューにおいて拒否されているため、一旦追及は停止されています。P3100R1は引き続き検討中なのでそちらで継続するものと思われます

P3289R1 Consteval blocks

宣言のコンテキストで任意の定数式を実行するためのブロックの提案。

以前の記事を参照

このリビジョンでの変更は、constevalブロックをstatic_assertと区別するために文言を更新したことです。

この提案はP2996R10にマージされ、リフレクション機能の一部として標準に導入される予定です。

P3347R1 Invalid/Prospective Pointer Operations

無効化されたポインタに対する一部の演算を明示的に許可する提案。

以前の記事を参照

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

  • LEWGIレビューへの対応
    • 概要を簡潔にし、詳細を“Background”セクションへ移動
    • N4933のWDへの対応として、“representation bytes”を“value representation”へ変更
  • 参考文献の更新

などです。

P3348R2 C++26 should refer to C23 not C17

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

以前の記事を参照

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

  • 浮動小数点環境に関する関連提案P3479R0とP2746R6へのリンクを追加
  • SG6のレビューにおいて、浮動小数点数に関する部分を別の提案へ移すことが決定
  • <cstdlib>, <cstring>, <ctime>へ不足していた宣言を追加し、TODOメモに対応
  • std::time_putの脚注の削除(C99で削除されていたが残っていた)は編集上の処理によって対応された(のでここでの対応は不要になった
  • N5001へリベース

などです。

P3351R2 views::scan

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

以前の記事を参照

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

  • SG9レビューに基づいて、全体の再設計
    • views::scanは引数シーケンスとセマンティクスの両面でstd::inclusive_scanと同等になった
    • views::prescanを削除(指示がなく、STLでの経験もなかったため
    • prior artsセクションを再構成
    • prior artsセクションにSTLアルゴリズムを追加し、表のフォーマットを変更
    • KotlinやHaskellでの例を追加
    • 引数シーケンスの変更により、範囲と初期値のコンフリクトが問題ではなくなった
      • これにより、with-initial-valueバージョンが通常のバージョンのオーバーロードになるのを防止していたワークアラウンドが不要になり、削除された
      • そして、scanpartial_scanはオプション引数で初期値を取るようになった
    • 並行範囲アルゴリズム全般との関係を考察するセクションの追加
    • 文言に対する質問を、対象読者に基づいて異なるグループに分割した
  • 投票結果セクションとレビュー結果セクションをいくつか追加
  • 参照型に関する軽微な修正と議論を追加
  • P1729R5との命名競合に関する議論を追加
  • いくつかの文言の修正
  • P2846R5の採択を見越して、reserve_hintメンバを追加
  • N5001へのリベース

などです。

このリビジョンでのサンプルコード

import std;

int main() {
  std::vector vec = {1, 2, 3, 4, 5};

  std::println("{}", vec | views::scan(std::plus{})); // [1, 3, 6, 10, 15]
  std::println("{}", vec | views::partial_sum);       // [1, 3, 6, 10, 15]
  std::println("{}", vec | views::scan(std::plus{}, 10)); // [11, 13, 16, 20, 25]
  std::println("{}", vec | views::partial_sum(10));       // [11, 13, 16, 20, 25]
}

P3367R3 constexpr coroutines

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

以前の記事を参照

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

  • モチベーションの文章を追加
  • AST変換アプローチの例を追加

などです。

P3373R1 Of Operation States and Their Lifetimes

P2300のstd::executionにおいて、構成された非同期アルゴリズムの内部状態の生存期間を特定の場合に限り短くする提案。

以前の記事を参照

このリビジョンでの変更は、具体的な行動方針を提案したことです。

この提案では次の2種類のsenderアルゴリズムに対して提案する変更(ネストした場合のoperation_state生存期間の短縮)を行うことを提案しています

  • let_value, let_error, let_stopped
    • ただし次のタイミングで、先行するoperation_stateの生存期間を終了する
      • これらのアルゴリズムによって送信された値が永続化された後
      • 後続操作のためのsenderを取得するために提供された呼び出し可能オブジェクトを呼び出す前
  • split
    • 次のタイミングでサブoperation_stateの生存期間を終了する
      • splitによって送信された値が永続化された後
      • splitsenderに接続して開始することで開始された操作が、その値で完了する前

ただし、splitに関してはそれそのものがどういう形でC++26で出荷されるか不安定という事情があります。

P3374R1 Adding formatter for fpos

fpos<mbstate_t>のフォーマッタを追加する提案。

以前の記事を参照

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

  • 状態を、回復可能な記述子ではなくboolでフォーマットする
  • いくつかの文言の軽微な問題の修正

などです。

P3375R2 Reproducible floating-point results

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

以前の記事を参照

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

  • 貢献者リストを更新
  • status quoセクションを更新し、コンパイル浮動小数点環境とリテラルの解析に関する議論を含めた
  • 関連する他の提案についての議論を追加
  • ストレージと属性から構成されるコア型に焦点を当てた、新たな調査の方向性に関するセクションを追加
  • ライブラリ型の提案と文言を撤回
  • コア型の提案を拡張
  • 未解決の質問を更新

などです。

P3385R3 Attributes reflection

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

以前の記事を参照

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

  • 属性のスコープについて解決
  • 属性の引数について解決

などです。

属性のリフレクションにおいては、非標準の属性の扱いが問題になります。特に、属性の無視可能性のルール(これは長年の議論の的であり、問題になっている)に抵触してしまうと提案が頓挫する可能性があります。そのためこの提案では、非標準属性を考慮に入れつつも属性の無視可能性について新しい規定を追加しないようにするために、次のself-consistencyルールを指定しています

  1. 無視された属性へのリフレクションは^^[[]]と区別がつかない
  2. 宣言に属性をスプライシングすることは、それらの属性を手動で付加した場合と区別がつかない

このルールは、標準属性については規範的(なるべくリフレクション動作を規定する)であり、非標準属性については寛容的(使おうと思えば使える)であろうとするものです。

この提案の初期のリビジョンでは属性の引数節を意図的に無視していましたが、そのサポートの必要性が全会一致で示されました。その際問題となるのは標準属性では任意の引数を取れる[[assume(expr)]]のみであり、これは空文に対する属性として使用されるものであり取得するには明示的に直接クエリするしかないため、考慮しないことにしました。

非標準属性では任意の引数を取ることができますが、現状任意の引数シーケンスをサポートする標準的な方法が存在していません。おそらくtoken sequence(P3294)のような表現にするしかないと思われますが、その議論はまだ始まったばかりです。

そのためこの提案では、標準属性をカバーするのに十分な算術型または文字列リテラル引数のサポートのみを必須とすることで現実的なユースケースをカバーする方向で提案しています(その他の引数のサポートは実装定義)。

P3388R1 When Do You Know connect Doesn't Throw?

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

以前の記事を参照

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

  • 具体的な提案を追加
  • 代替案リストを削除
  • 文言の追加

などです。

このリビジョンでは、以前に提示していた2つのオプションのうち、「receiverコンセプトの一部として、receiverが例外を投げずにムーブ可能であることを要求する」方向性を提案として採用しています。追加の意味論要件として「あるsender型のインスタンスをあるreceiver型のインスタンスに接続しても例外を発生しない場合、そのsender型のインスタンスを任意のreceiver型のインスタンスに接続する際も、同じ関連環境型であれば例外は発生しない」を追加しています。

P3394R1 Annotations for Reflection

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

以前の記事を参照

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

P3395R0 Formatting of std::error_code

std::error_codeをフォーマット可能にする提案。

std::error_codeはostreamへの出力が可能です。

std::error_code ec;
auto size = std::filesystem::file_size("nonexistent", ec);
std::cout << ec;
generic:2

しかし、std::printでは出力できません。

しかし、ioマニピュレータが適用されるのはカテゴリ名の方だけであるなど、おかしな動作をする部分があります。

std::cout << std::left << std::setw(12) << ec;
generic     :2

この提案は、std::error_codeをフォーマット可能(print可能)にするとともに、より使いやすいフォーマット指定を可能にしようとするものです。

基本のフォーマットは<<と同じです

std::print("{}\n", ec);
generic:2

しかし、フォーマットオプションの指定は文字列全体にかかります

std::print("[{:>12}]\n", ec);
generic:2

そして、エラーメッセージを出力可能するオプションを用意しています

std::print("{:s}\n", ec);
No such file or directory

ただし、エラーメッセージの実際の文字列は実装定義です。

このエラーメッセージ出力の最大の問題は、標準ライブラリ実装とそれが動作するプラットフォームによってメッセージのエンコーディングがまちまちである点です(標準の仕様では未規定)。次の表はそれを比較したものです

プラットフォーム\ライブラリ実装 libstdc++ libc++ Microsoft STL
POSIX strerror strerror -
Windows strerror/ACP strerror ordinary literals/ACP

strerrorはCロケールエンコーディングを表し、ACPはActive Code Pageの略です。

この表からも分かる通り、エンコーディングは異なる場合があり、実際に異なることが多いと思われます。この提案では、Cロケールエンコーディング(実行文字集合)を使用することを提案しています。

なお、この提案の内容は{fmt}ライブラリですでに実装されています。

P3400R0 Specifying Contract Assertion Properties with Labels

契約アサーションに対してラベルを指定する機能の提案。

契約アサーションの実行時の動作はセマンティクスというもので指定されており、セマンティクスはおおむねグローバルに決定されます(規定としては実装定義だが、実体はコンパイラオプションで一括指定する)。しかし、より細かい単位でセマンティクスを制御し、あるいは契約アサーションの特定の振る舞いについてを同様にローカルに制御したいユースケースがあります。

P2900のC++26 Contractsはこのようなユースケースを把握しつつも、最初のMVPとしてそのような機能を提供していませんでした。ただ、以前の提案からの1つの機能の候補として、アサーションにラベルを指定することでアサーションのきめ細かな制御を行う方向性の機能を導入していくことについてはおおむね合意されていました。

この提案は、C++29以降のContracts機能拡張として、契約アサーションにラベルを指定することでアサーションの振る舞いについてユーザーが細かく制御できるようにしようとするものです。

提案の内容は次のものです

  • コア言語
    • ラベルを指定するための構文
      • pre<label>(expr)のように指定する
        • ラベル(label)はassertion-control-expressionという定数式(言語キーワードではない)
    • ラベルオブジェクトの型の定義
      • ラベルに使用されるオブジェクトはAssertion-Control Objectsと呼ばれる
      • assertion_control_objectコンセプトを満たす型のオブジェクトをラベルとして使用できる
    • ラベル専用のusing文の追加
      • assertion-control-using-directives / assertion-control-using-declarations
        • 導入される名前はラベル構文からの探索時にのみ可視になる
      • contract_assert using namespace std::contracts::labels;のようになる
    • 複合ラベル演算
      • Assertion-Combination Operator としてoperator|オーバーロードを提供
      • ラベルの合成を可能にする
      • 複合されたラベルは combined assertion-control objects と呼ばれる
    • 違反ハンドラからのラベルの取得
      • contract_violation::control_object()から違反が起きたアサーションに指定されているラベルのポインタを取得する
        • ただし、この関数はラベル型がポリモルフィックである場合にのみnullptr以外を返す
      • 複合ラベルにおいて特定のラベルが含まれているかを調べるために、get_constituent_label()を提供
    • 環境ラベル
      • 名前空間スコープやクラススコープで使用されている契約アサーションに対して一括でラベルを指定する
      • そのようなラベルはAmbient-Control Objectsと呼ばれる
      • contract_assert implicit <label>;のように指定
    • ラベルオブジェクトによって制御可能なプロパティ(全てオプトイン
      • 許可するラベルの指定方法
        • allowed_semantics(静的)メンバ変数によって、許可するセマンティクスのリストを指定する
          • 型は、std::contracts::evaluation_semantic列挙型
      • 評価時のセマンティクス選択方法
        • 現在のセマンティクス種別を受け取って評価に使用するセマンティクス種別を返すcompute_semantic()
      • 呼び出す違反ハンドラを指定する方法
        • handle_contract_violation()で特定の違反ハンドラを呼び出すように指定する
          • 定義しないかfalseを返すことでグローバルの違反ハンドラを呼び出すように指定できる
      • ラベルの次元指定
        • ::dimensions入れ子型でラベルの次元を表現し、複合時に共通する次元を持つラベルがある場合にコンパイルエラーにする
        • あるラベル同士が相互に排他的である場合にそれを指定する
      • 呼び出し側の契約アサーション(Caller-Facing)と呼び出し先の契約アサーション(Callee-Facing)の制御
        • 仮想関数の契約アサーションでは、インターフェースに付加された契約が呼び出し側アサーション、動的に呼ばれるオーバーライドの契約アサーションが呼び出し先アサーションとして区別され、両方をチェックするようにする方向性が選好されている
        • この場合、ある仮想関数に付与されている契約アサーションがどちらのアサーションとして選択されるべきかを指定するラベルを作成できる
        • ::caller_facing/::callee_facing入れ子型を定義しておくことで識別する
  • 標準ライブラリ
    • 標準ライブラリのラベル
      • セマンティクスを明示的に指定するラベル
      • 拒否するセマンティクスを明示的に指定するラベル
      • レビューラベル
        • review
        • 契約アサーション導入時のセマンティクス指定として使用されるラベル
      • その他機能についてのラベル
    • コア言語の暗黙の契約アサーションのラベル
      • 暗黙の契約アサーションで共通して使用されているラベルと同じ意味を持つラベル
      • 特定のカテゴリの暗黙契約アサーションに作用することのできる環境ラベル(ambient-control objects)
    • プロファイルに関するラベル
      • 各標準プロファイルに関連付けられたラベル
      • 逆に、あるプロファイルが導入する契約アサーションには対応するラベルが付加される

ラベルはC++のキーワードや何らかの専用言語機能で提供されるのではなく、クラスがのオブジェクトとして、契約アサーションに続く<>の中に指定できます。

// ラベルの定義
struct my_label_t {};
constexpr my_label_t my_label;

void f(int i)
  pre<my_label>( i > 0 ); // ラベルの指定

このようなラベルオブジェクトはAssertion-Control Objectsと呼ばれ、その型と値によって契約アサーションの動作を細かく制御することができます。ラベルの指定(Assertion-Control Objects)自体は定数式の結果である必要があります。

ラベルは|によって合成して指定することができます。

struct my_label_2_t {};
constexpr my_label_2_t my_label_2;

void g(int i)
  pre<my_label | my_label_2>(i > 0 ); // 複合ラベルの指定

これらのAssertion-Control Objectsが提供する性質によって、それが指定された契約アサーションの振る舞いを細かく制御します。例えば、契約違反が起きたら継続しないようにするラベルは次のように構成できます

struct enforce_or_quick_enforce_t {
  static constexpr evaluation_semantic_set allowed_semantics =
    evaluation_semantic_set(evaluation_semantic::enforce,
                            evaluation_semantic::quick_enforce);
};

constexpr enforce_or_quick_enforce = {};

契約違反が起きた場合に呼ばれる違反ハンドラはまずライブラリが指定するものを呼び出すようにするラベルは次のように構成できます。

struct my_library_violation_handler_t {
  // この関数自体がカスタムの違反ハンドラ
  std::true_type handle_contract_violation(const std::contracts::contract_violation& violation) {
    // violationオブジェクトに対して何らかの操作を行う
    ...

    return {};  // trueを返すと次の違反ハンドラを呼び出しに行く(複合ラベルではなければグローバルの違反ハンドラが呼ばれる)
                // falseを返す(あるいはvoid戻り値型)とそこで違反のハンドリングは終了する
  }
};

constexpr my_library_violation_handler = {};

この2つのラベルの性質を両方備えたラベルはラベルを合成することで得られます。

constexpr auto my_lib_assertion = std::contracts::combine_labels(enforce_or_quick_enforce, my_library_violation_handler);

そして、このように作成したラベルは通常のラベル同様に指定して使用することができます。

void f()
  pre<my_lib_assertion>(true)
  post<my_lib_assertion>(true)
{
  contract_assert<my_lib_assertion>(true);
}

ラベルが指定されている契約アサーションの動作はグローバルの指定の影響を受けず、そのラベルオブジェクトに指定された振る舞いによって評価されます。

P3402R2 A Safety Profile Verifying Initialization

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

以前の記事を参照

このリビジョンでの変更はあまり明示的ではありませんが、クラス以外の初期化全般を取り扱うようにしたことの様です。

P3407R1 Make idiomatic usage of offsetof well-defined

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

以前の記事を参照

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

  • P1839R6のレビュー中に発見された文言の改善を適用
    • これには、volatileオブジェクトのオブジェクト表現へのアクセスの禁止が含まれる
  • 文書の可読性の向上
  • 非standard-layout型とcontainer_ofの標準化の可能性に関する議論を追加
  • ここで議論されている3つの主要な設計案を比較する表を追加

などです。

P3411R1 any_view

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

以前の記事を参照

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

  • パフォーマンステストの更新
    • -O3オプションをすべて使用する
    • eagerアルゴリズムでは範囲を使用しない
    • 関数の引数でvectorany_viewを使用する新しいベンチマークテストケースを追加
  • テンプレートパラメータの再設計
    • any_view<Foo>any_view<const Foo>は問題なく動作するはず
    • 4つの異なる代替設計を提示

などです。

P3412R1 String interpolation

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

以前の記事を参照

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

  • basic_formatted_string構造体を削除
    • これにより、P3298およびP3398への依存を回避できる
  • printオーバーロードを削除し、代わりにプログラマprint時にxリテラルを使用できるようにする
    • fリテラルstd::string/std::wstringを直接生成する
  • fリテラルを更新std::make_formatted_stringまたはstd::formatではなく、__FORMAT__への関数呼び出しを生成するようにする
    • これにより、標準ライブラリのフォーマット機能に依存しない使用を可能にする
  • 他の名前について検討

などです。

このリビジョンでは、fリテラルの基本的な使用感は変わりませんが、展開は変わっています

f"Weird, but OK: {1 < 2, 2 > 1}"
// Transformed to:
__FORMAT__("Weird, but OK: {}", (1 < 2, 2 > 1))

int values[] = {3, 7, 1, 19, 2 };

f"Reversed: {std::set<int, std::greater<>>(values, values + 5)}"
// Transformed to:
__FORMAT__("Reversed: {}", (std::set<int, std::greater<>>(values, values + 5)))

f"{x=}";
// translates to:
__FORMAT__("x={}", x);

展開結果の__FORMAT__はまだ暫定的な名前ですが、この意図は標準ライブラリを使用しないような実装においてもユーザーが対応する関数を定義することによってfリテラルを使用可能にすることにあります。

標準ライブラリとしては、次のようなstd::formatに転送する実装を提供します

template<typename... Args>
std::string __FORMAT__(std::format_string<Args...> lit, Args&&... args) {
  return std::format(std::move(lit), std::forward<Args>(args)...);
}

template<typename... Args>
std::string __FORMAT__(std::wformat_string<Args...> lit, Args&&... args) {
  return std::format(std::move(lit), std::forward<Args>(args)...);
}

ただし、この目的を達するためには、__FORMAT__は最終的に他の識別子と衝突しないような十分にユニークな名前にする必要があります。stdの中に入れると名前は単純化できますが、ユーザー定義を許可するために手続きが必要になります。そのほか特殊な関数名(ユーザー定義リテラルlikeなものなど)を使用する方法などが挙げられていますが、どうするのかまだ決定していません。

このリビジョンで追加されたxリテラルは、展開結果を__FORMAT__の呼び出しで囲まないものです。これにより、中間のstd::string生成を省いて直接的にstd::printに渡すことができます

std::print(x"Weird, but OK: {1 < 2, 2 > 1}");
// Transformed to:
std::print("Weird, but OK: {}", (1 < 2, 2 > 1));

P3420R1 Reflection of Templates

テンプレートそのものに対するリフレクションの提案。

以前の記事を参照

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

  • 関数宣言の一部をトークンシーケンスとして返すAPIを、宣言を受け取り変更された宣言を返す関数型APIに置き換え
  • 実装の容易性を考慮し、依存識別子と非依存識別子が混在する可能性のあるコードをトークンシーケンスとして返すメタ関数を削除
  • これらの結果、以前に提案されていたas-ifルールは不要になった

などです。

P3423R1 Extending User-Generated Diagnostic Messages

コンパイル時に診断メッセージを指定することのできる機能に対する文字列の制約を共通化する提案。

以前の記事を参照

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

  • 属性のインスタンス化のタイミングについて議論を追加
    • 属性の引数節が無効な置換をもたらした場合にSFINAEが発生すべきか
  • 投票セクションを追加
  • EWGに転送されたことに伴うAudienceの変更
  • N5001へリベース

などです。

P3425R1 Reducing operation-state sizes for subobject child operations

operation_stateのサイズを削減可能にする提案。

以前の記事を参照

このリビジョンでの変更は明確ではないですが、提案する文言を追加したことがメインの様です。

P3430R2 simd issues: explicit, unsequenced, identity-element position, and members of disabled simd

std::simd(P1928)のLWGレビューで見つかったIssueとその解決についてまとめた提案。

以前の記事を参照

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

  • Issue 4について説得力のある例が見つからなかったため、問題なしとした
  • 提案の全ての未反映の変更を統合した、Wordingセクションを追加
  • 機能テストマクロのバンプ

などです。

P3431R0 Deprecate const-qualifier on begin/end of views

標準ライブラリのviewconst-iterableを非推奨にする提案。

標準ライブラリのview型、特にrangeアダプタの結果型のview型のbegin()/end()メンバ関数にはconstオーバーロードが用意されています。これによって、そのviewオブジェクトがconst修飾されているときでもイテレーションが可能になります。この性質をconst-iterableと呼びます。

しかし、viewは参照セマンティクスを持つものであるため、そのconst性は要素のconstを意味していません。また、一部のview型はその特有の事情からconst-iterableではなく、通常const-iterableであるようなview型でもforward_rangeではないinput_rangeに対してはconst-iterableではなくなります。

これらの事によって、viewconst-iterable性はしばしば落とし穴となります。

例えば次のコードのように、const参照によってrangeを受け取っている場合

template <typename Rng>
void do_something(const Rng& rng) {
  for (auto& x : rng) {
    …
  }
}

これはふつうのコードであれば推奨されるスタイルですが、ことrangeに限ってはそうではなく、const-iterableではないview型を渡すと想定外にエラーとなります。正しくは次のように、rangeを転送参照で受け取って、イテレーション時にconst参照で受けるのがベストです

template <typename Rng>
void do_something(Rng&& rng) {
  for (const auto& x : rng) {
      …
  }
}

viewconst-iterable性によって、このようなコードはおおむね正しく動くように見えてしまいます。しかし、const-iterableであっても要素はconstとは限らないためいずれにせよそれは想定通りではなく、このような間違ったコードは早期にコンパイルエラーとなるべきです。

さらに、規格あるいはviewの実装を見に行くと分かりますが、ほとんどのview型はconst-iterableサポートのためにとても複雑なbegin()/end()の実装を持っています。const-iterable性が不要になれば、それらはかなり簡易化されるはずです。

そして、view型がconstでもrangeとして扱える事のメリットはかなり小さい可能性があります。viewの型はかなり複雑であり、入力のrangeやそのほかの引数によって容易に変化します。そのため、viewオブジェクトに再代入したいケースというのはかなり少ないはずです(そもそも代入できないため)。また、const化することでスレッド間での共有アクセスが可能になると考える人がいるかもしれませんが、まずconst-iterableは要素のconstを意味しないためその期待はあまり満たされません。さらに、viewの構築は通常かなり軽量であるため、各スレッドそれぞれで構築するようにしてもそれほどオーバーヘッドにはならないでしょう。

また、viewオブジェクトはRangeアダプタの結果に代表されるようにごく短い間しか存在しないものであり、ローカル変数に保存されることがあってもその実体のスコープは狭く、クラスメンバやより広いスコープを持つ変数に保存されることはめったにありません。

これらの理由により、この提案では既存の標準ライブラリviewconst-iterable性を非推奨にしようとする提案です。

viewからconst-iterable性を取り除くことで、viewconstにすることの是非やconst-iterable性そのものについてなどを考える必要がなくなり、それについての利用者の混乱を解消することができます。

この提案では次のメンバ関数constオーバーロード

  • begin()
  • end()
  • empty()
  • cbegin()
  • cend()
  • operator bool()
  • data()
  • size()
  • front()
  • back()
  • operator[]

次のview型に対して非推奨とすることを提案しています

  • std::ranges::ref_view
  • std::ranges::owning_view
  • std::ranges::as_rvalue_view
  • std::ranges::transform_view
  • std::ranges::take_view
  • std::ranges::take_while_view
  • std::ranges::drop_view
  • std::ranges::drop_while_view
  • std::ranges::join_view
  • std::ranges::join_with_view
  • std::ranges::lazy_split_view
  • std::ranges::common_view
  • std::ranges::reverse_view
  • std::ranges::as_const_view
  • std::ranges::elements_view
  • std::ranges::enumerate_view
  • std::ranges::zip_view
  • std::ranges::zip_transform_view
  • std::ranges::adjacent_view
  • std::ranges::adjacent_transform_view
  • std::ranges::chunk_view
  • std::ranges::slide_view
  • std::ranges::cartesian_product_view
  • std::ranges::concat_view

これ以外のview

  • std::span/std::string_view: const/非constで広く使用されており、再代入が有効な型であるため、破壊的変更になる
  • std::ranges::view_interface: 下記のviewで使用されている
  • std::ranges::iota_view: 適切な型名を持つRangeファクトリである
  • std::ranges::repeat_view: 適切な型名を持つRangeファクトリである
  • std::ranges::empty_view: 全てのメンバ関数static
  • std::ranges::single_view: 深いconst性を持つ

に対してはconst-iterable性を維持しようとしています。

とはいえ、この除外されているもの以外のものでも、現在const-iterableであるものについてはconstオーバーロードを削除してしまうと破壊的変更となるため、ここでは非推奨にとどめています。

P3439R1 Chained comparisons: Safe, correct, efficient

誤って書かれることの多い、連鎖比較を意図通りに動作するようにする提案。

以前の記事を参照

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

  • 文言の追加
  • 畳み込み式のサポートを追加
  • トリガー(判定)のルールを、「個々の二項比較」ではなく「書き換えられた式全体が有効であり、文脈的にboolに変換可能である」に変更
  • Q&Aセクションを追加し、次の項目を追加
    • 以前の実装可能性に関する懸念事項への対応
    • ユーザー定義の数学型が問題なく動作すること
    • 廃止期間を設けるべきかどうか
    • 追加の利点

などです。

P3475R1 Defang and deprecate memory_order::consume

memory_order_consumeを非推奨化する提案。

以前の記事を参照

このリビジョンでの変更は、より詳細な文言を追加したことです。

この提案はすでにEWGの投票をパスしてCWGでレビューされています。

P3477R2 There are exactly 8 bits in a byte

1バイトを8ビットであると規定するようにする提案。

以前の記事を参照

このリビジョンでの変更は、SG22での検討結果を追記し、SG22をaudienceから外したことのみです。

この提案についてはWG14(C)も関心を持つ可能性があるとされ、SG22の観点からの懸念点は報告されなかったようです。

P3480R3 std::simd is a range

std::simdrangeにする提案。

以前の記事を参照

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

  • タプルインターフェースのサポートの必要性について質問を追加
  • [simd.iterator]の配置場所を明確化
  • 適切な表現に修正
  • default_sentinel_tでの<=>比較を修正
  • 機能テストマクロのバンプは必要?

などです。

P3481R1 std::execution::bulk() issues

std::execution::bulk()の改善提案。

以前の記事を参照

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

  • SG1のレビューからのフィードバックを適用
  • 文言セクションを追加

などです。

P3491R1 define_static_{string,object,array}

コンパイル時に構築した文字列や配列などを静的ストレージに昇格させて実行時に持ち越せるようにするライブラリ機能の提案。

以前の記事を参照

このリビジョンでの変更は文言の改善のみです。

P3496R0 Immediate-Escalating Expressions

定数式における動的メモリ確保の制限を少しだけ緩和する提案。

この提案は、以前にP3032R2で提案されていた内容の一部を抽出したものです。P3032については以前の記事を参照

P3032では次の2つの事が提案されていました

  1. 定数式にならない即時関数呼び出しが定数式になるように、それを含むより大きな式を定数式として扱う
  2. 定数式における動的メモリ確保に関して、開放のタイミングが同じ評価内にある場合に加えて、同じ直接のコンテキスト内にある場合も許可する

この提案では、このうち1だけを改めて提案しなおしています。

そのモチベーションは共通しており、リフレクション機能で頻繁に問題になる事が想定されているので予め解決しておこうとするものです。

P3032での問題は次のようなコードでエラーが発生することでした

enum E { a1, a2, a3 };

constexpr int f2() {
  return enumerators_of(^E).size(); // ng
}

int main() {
  constexpr int r2 = f2();
  return r2;
}

これはenumerators_of(^E).size()の式全体がconstevalコンテキスト(即時コンテキスト)にならない(enumerators_of(^E)の呼び出しだけがなる)ことで、enumerators_ofの戻り値のstd::vectorの解放がそのコンテキスト内で完了しないとみなされてしまうためにエラーになっています。

P3032及びここでの提案は、このような場合にenumerators_of(^E).size()全体がconstevalコンテキストに昇格するようにしようとするものでした。

このような一部の定数式の特別扱いはルールが複雑になる割に回避が簡単であるとしてP3032の初期のリビジョンでは提案されていませんでした。この提案は、その実装経験のフィードバックとともに、現在の規格内での先行例を指摘しています。

すなわち、式E1が式E2の部分式であってE1は定数式ではないがE2は定数式である、ような場合に全体が定数式ではないとしてエラーにしないケースが規格上で2か所存在しています。

1つは、consteval関数の呼び出しそのものです。この場合、consteval関数の名前の使用そのものは定数式ではないものの、consteval関数の呼び出しは定数式になる場合があります。

consteval int id(int i) { return i; }

/* not constexpr */ void f(int x) {
    auto a = id;    // error、名前の使用
    auto b = id(1); // ok、定数引数による呼び出し
    auto c = id(x); // error、非定数引数による呼び出し
}

idという式そのものは定数式ではありませんが、id(1)は定数式になります。

もう一つの例は、集成体においてデフォルトメンバ初期化子に非即時呼び出しなconsteval関数呼び出しが含まれている場合、集成体初期化式が定数式であればデフォルトメンバ初期化子のconsteval関数呼び出しも即時呼び出しになります。

// id()は先穂の例と共通

struct A {
  int x;
  int y = id(x);  // 引数が非定数式なため即時呼び出しではない
};

template<class T>
constexpr int k(int) {  // A(42)は定数式ではあるものの即時呼び出しではないため、k<int>は即時関数ではない
  return A(42).y; // ok
}

A::yのデフォルトメンバ初期化子のid(x)は内部的に定数式ではないものの、それが呼ばれるA(42)は定数式であるため、その外側がconstevalコンテキストではなくても定数式となり許可されています。

これらの事を根拠にこの提案ではimmediate-escalating expressionの規則を修正して、consteval関数呼び出しが定数式で囲まれていない場合にのみimmediate-escalating expressionとなるようにしようとしています。

enumerators_of(^E).size()(現在この全体がconstevalコンテキストではない)については

  • enumerators_ofはconsteval-only式(consteval関数であるため
  • ^Eもconsteval-only式(meta::infoがconsteval-only型であるため
  • enumerators_of(^E)の呼び出しもconsteval-only式
    • ただし、戻り値のvectorがこのconstevalコンテキスト内で破棄されないため、これは定数式ではない
    • そのため即時呼び出しではない
  • より大きな式enumerators_of(^E).size()は定数式であり、constevalな非定数式を部分式にもつため、この式は即時呼び出しとなる(ようになる
    • これにより、enumerators_of(^E)そのものはimmediate-escalating expressionではなくなる

のようにして許可されます。

かなり回りくどいですが、現在の仕様ではenumerators_of(^E)の呼び出しだけがimmediate-escalating expressionになってしまい、そのコンテキスト内で戻り値のstd::vectorが破棄されなければならない、という制約が付加されてしまうことで問題が起きています。この提案後には、その制約が破られることで非定数式となった部分式かつconsteval関数呼び出しを囲む定数式全体が即時呼び出しとなるようになることで、enumerators_of(^E)の戻り値の使用可能なスコープが囲む式全体まで広がり、これによってエラーが解消されます。

なお、この提案の内容はP2564R3(immediate-escalating expressionを導入した提案)に対するDR(すなわちC++23に対するDR)とすることを意図しています。

P3499R0 Exploring strict contract predicates

非緩和契約の設計についての提案。

非緩和契約(あるいは厳密な契約)とは、P2680R1やP3285R0で提案されている契約条件の評価において未定義動作を起こさないようにするための仕組みの事です。これは主に、未定義動作に繋がる操作を禁止するとともに、一部の未定義動作を定義済み動作にすることによって行われます。

非緩和契約のような制限の無い契約の事を緩和契約と呼んでおり、これはP2900R13の契約仕様のセマンティクスに合致しています。しかし、P2900R13のContracts仕様には非緩和契約は含まれておらず、そのことがEWGにおける継続的な反対の主な対象となっています。

現状の非緩和契約の概念はアイデアのみの段階でその仕様や実装が無く、実現可能性が不透明です。P3376R0およびP3386R0ではP3285R0の非緩和契約について検討を行っていますが、ほとんど実用的な契約条件を表現できない事などの問題が報告されており、SG21では非緩和契約は少なくともC++26に向けては追及しないことが決定されています。

この提案では、非緩和契約の設計について検討し、その最初の仕様の策定を目指そうとするものです。

まず出発点として、データ競合を除く未定義動作が確実に起こらないことが保証可能な述語を記述できる式を特定します

  • 算術型または列挙型のリテラル
  • 算術型または列挙型の非volatile変数を指すid-expression(変数名を指定する式
    • ポインタや参照は含まれない
  • 単項演算子+, -, !を使用した式(unary-expression)
  • 二項演算子+, -, /, %, *, !, ,, ^, |, ||, &, &&, <<, >>を使用した式(binary-expression)
  • 単項演算子?, !を使用した条件式(conditional-expression)
  • <, >, <=, >=を使用した式(relational-expression)
  • ==, !=を使用した式(equality-expression)
  • <=>を使用した式(compare-expression)
  • 算術型または列挙型のコア定数式

かなり小さな集合ではありますが、将来的に拡張していくことができます。

これらの式による述語は予測不可能な未定義動作を起こさず、副作用を持たないものです。そのため、非緩和契約で使用可能な述語をこれら楽しきにのみ制限することで非緩和契約の一つの目標は達成されます。そして、これらの式で発生が予測される未定義動作は次のものに絞られます

  • 符号付整数型のオーバーフロー/アンダーフロー
  • 浮動小数点数値の、その値を表現できない型への変換
  • ゼロ除算
  • 負の値でのシフト
  • 型のビット幅以上のシフト量によるシフト

非緩和契約ではこれらの未定義動作を定義済み動作に変換する必要があります。P3285R0で提案されていた飽和演算やラップアラウンド演算による動作の書き換えは、契約外での動作と異なることによってバグの隠蔽や見落としに繋がることが指摘されているため望ましい選択肢ではなく、この提案では未定義動作の発生を契約違反として扱うアプローチを推奨しています。

これは、P3100R1/P3229R0の方向性と合致しており、よりきめ細かい契約違反ハンドリングを可能にする方向性です。

最後に、非緩和契約の構文については、P2900の現在のデフォルトを変更せずにpre strict (x)のようにオプトインなものとすることを提案しています。これは、反対意見はあったもののEWGで合意済みの事です。

これらの事項を文言にエンコードすることによって非緩和契約の仕様は策定可能となりそうです。しかし、この仕様にはかなりの制限があります。例えば

  • 組み込みの算術型・列挙型以外の値に対する演算は使用できない
  • ポインタのデリファレンスはできない
  • 参照を使用できない
  • あらゆるオブジェクトに対するメンバ関数呼び出しはできない

結局この提案の結論としては、非緩和契約の方向性が有効であるとは思えない、というものです。ただ、依然として求める声が多い非緩和契約がどのようなものになるのかについてのこれらの検討がP2900R13のコンセンサスを高めるものになることが期待されています。

P3500R0 Are Contracts "safe"?

C++ Contracts提案の安全性に対する懸念点からの反対意見に対しての解消のための説明文書。

P2900R13で提案中のContracts機能に対してはいくつか懸念点が繰り返し提起されています。

まず一つは評価セマンティクスが柔軟であることによって、契約違反が起きた後に実行が継続されてしまうことです。これにより、プログラムの実行が未定義動作に突入する可能性があります。

T& MyVector::operator[] (size_t index) 
  pre (index < size())
{ 
  return _data[index];  // UB if index >= size() 
} 

この例では、契約チェックがobserveセマンティクスで行われている場合、未定義動作が発生します。

もう一つは契約アサーションが任意のbool式を受け取れることによって、契約条件式自体の評価に伴って未定義動作が発生しうることです。

int f(int a) {  
  return a + 100;  
}

int g(int a) 
  pre (f(a) > a);

この例では、f()内での符号付整数の加算がオーバーフローすることはないとコンパイラが仮定することができ、その場合g()の事前条件は常に満たされるため、契約の評価セマンティクスによらずg()の事前条件チェックを省略することができます。

これらの問題により、P2900のContracts機能は安全ではないから同意できない/修正すべきだ、という意見が根強くあります。この文書は、それらの人々を説得するためのもので、C++ Contracts機能が何を目的としているか、P2900が何をしようとしているか、C++26にContractsが必要な理由、などを説明するものです。

要約すると

  1. 「安全」という言葉の意味とここでの定義
    • 「安全(性)」という言葉は人によって異なる意味で使用されており、この共通理解が欠如している
    • 安全には主に次の3つの意味があり、区別する必要がある
      1. 言語安全性
      2. 機能安全性
      3. セキュリティ
    • これら3つの概念は正確性によって強く関連付けられている
      • 正確なコードは3つの安全性を満たしているが、逆は必ずしも成り立たない(3つの安全性を満たしても正確性が満たされないことがある)
    • 開発者の究極の目的は正確性を最大化することにあり、それは3つの安全性のいずれかあるいはすべてだけを追求していては達成できない
    • Contractsがターゲットとしているのは、プログラムの正確性を向上させること
      • これは、現在C++に行われている言語機能についての提案の中で、明示的に正確性をターゲットにする唯一の機能である
  2. 懸念の解決策について
    • プログラムはその対象とする領域によって要件が異なり、enforceセマンティクスが最適である場合もあれば、observe/ignoreが最適である場合もある
      • 必要に応じてその動作を変更できることはContracts機能を実用的にするために重要
    • そのうえで上記2つの懸念点を解消するアイデアはいくつか提案されている
      • always_terminateラベルなどのラベルによるセマンティクス指定
      • 厳格なContracts
    • どちらのアイデアC++26のタイムフレームでの実現は難しいものの、これらの拡張を待つためにP2900の採択を遅らせるのはより大きな問題がある
  3. C++26 Contractsの必要性について
    • WG21はソフトウェアの安全性(機能安全性/セキュリティ)を優先する道義的責任を負っている
    • しかし、機能安全性もセキュリティも言語安全性だけでは達成できず、ソフトウェアの問題の多くは未定義動作よりもむしろ論理的なバグや設計上の欠陥によって引き起こされている
    • Contractsは正確性を向上させるための機能であり、コード内の各所でプログラム状態をチェックできるようにするツールを提供することでプログラムの正確性を向上させ、それによって機能安全性/セキュリティを向上させようとするもの
      • 言語安全性の向上に全く関係しない訳では無いが、その主目的は言語安全性の向上をターゲットにしていない
      • Contracts機能は言語安全性を向上させようとするほかの機能と相補的な関係にある
    • P2900はC++ Contractsの最初の一歩(MVP)であり、C++29以降の機能の基盤となるもの
      • P2900を遅らせることは、「WG21は言語安全性の向上という近視眼的な部分に焦点を当てて、プログラムの正確性を向上させるという基本的な価値の向上を重視せず、機能安全性/セキュリティについても言語安全性に関連しない領域において重視していない」というメッセージをC++コミュニティに発することになる
      • C++の将来をこのような方向に導くのは無責任かつ非論理的であり、言語安全性の保証とContracts機能は相補的なものである
    • また、C++にContractsが導入されるまでに時間がかかればかかるほど、Contractsに投資する人々/企業が撤退してしまう
      • C++26に間に合わなければさらに3年遅れることになり、最悪C++にContractsが全く存在しない未来が来うる
    • C++ Contracts機能は20年以上に渡って開発されておりP2900はその集大成である
      • P2900だけでも5年以上にわたって開発されており、その設計のあらゆる側面が詳細に検討・調査され、文書化されている
      • 設計は安定しており、2つの主要なコンパイラGCC/Clang)における実装があり、SG21/LEWG/EWGにおいてC++26への承認が得られており、2025年2月の会議で全体投票が予定されている

のようなことが説明されています。

P3501R0 The ad-dressing of cats

規格文書内でのアドレスという言葉の意味と使用を明確化する提案。

規格内ではアドレスという言葉には明確な定義がなく、暗黙的な定義の下で使用されています。それによって例えば、ある場所([intro.memory]/1)では「全てのバイトは一意のアドレスを持つ」というように使用されている一方で、別の場所([basic.compound]/3)ではポインタはメモリのバイトの「アドレスを表す」のように使用されています。他にも、&演算子の記述のために使用されていたり、ポインタそのものの事を指して使用されていたりしているようです。

そして、アドレスという単語を使用すべきなのに使用していないことによって導入されている欠陥が一つあります。[defns.order.ptr]では、std::less<void*>等で実装定義のポインタ値の狭義全順序による順序付けを許可することを指定していますが、アドレスという言葉を使用せずに曖昧に規定していることによって、これはライブラリサイドでは実装不可能になってしまっています。例えば

  • std::less<void*>では、配列へのポインタとその配列の先頭要素のポインタを区別できない(比較不能)可能性がある
  • std::less<int*>では、あるintオブジェクトの次を指すポインタ(&x+1)と、そのオブジェクトの直後に配置されている別のintオブジェクトへのポインタを区別できない可能性がある

実装定義の全順序によるポインタ値の順序付けがサポートされている場合、これらのケースでも比較可能であり、ライブラリの比較関数オブジェクトでも同様なはずです。しかし、アドレスという言葉の曖昧性やその順序付けの曖昧性により、それを正しく指定できていません。

この提案では、アドレスという言葉をしっかりと定義して、使用すべきところではその定義の下で使用しすべきでない所ではほかの適切な言葉におきかえるようにしようとするものです。

この提案ではまず、アドレスはメモリバイトに対する(実装定義の)全順序付きの不透明ラベルであるとして定義しています。そのうえで、全てのポインタ値(関数ポインタやnullptrを含む)をこの定義の下でのアドレスという言葉を関連付けるようにすることで、ポインタ値に対する(実装定義の)全順序を導入します。そして、ポインタに対する比較をこのアドレスという言葉を使用して定義することで、ポインタの順序付けを実装定義の全順序の上で定義し、同様にライブラリの比較関数オブジェクト(std::less等)における結果も実装定義の全順序の上で定義するようにします。

また、定数評価中に比較(<)が利用可能なポインタに対して“ordered”という言葉を導入し、比較の定義でこれを使用することで、定数式で使用できない比較とできる比較を区別するようにしています。

同時に、混同されていたポインタとアドレスという言葉の使用方法をこの定義の下で明確化し変更しています。

この提案はほとんど言葉の使用方法の調整のみですが、(意図するライブラリ比較と言語比較の不整合解消を除いて)唯一オブジェクトの一つ後ろを指すポインタに関する比較が未規定ではなくなった点だけはセマンティクスの変更となります。

P3506R0 P2900 Is Still not Ready for C++26

Microsoftによる、P2900R11のContracts仕様に対する反対意見の表明文書。

この文書はP3173R0での主張を現在の状況を踏まえて更新するものです。

P3173R0については以前の記事を参照

P3173R0で主張されていた3点については状況はあまり変わっていません。仮想関数における契約のサポートは追加されていますが、この文書では複雑すぎであり仮想関数の長年の使用方法に則っていない、として不十分としています。

ここでは追加で

  • コルーチンの契約サポートは実装と使用の経験が必要
  • 標準ライブラリでの契約機能の使用については、既存のアサートをcontract_assertに置き換えただけであり、事前条件や事後条件を使用していない
    • このため、実用性の懸念は未解決
  • 契約条件の評価中に発生した例外を違反ハンドラがハンドリングしてしまうのは、多くの点で有害
    • 契約条件評価をtry-catchで囲むことになるので、バイナリサイズや実行時コストの点で不利
    • 例外を自然に伝播させるためには違反ハンドラの置換が必要になる
  • 契約注釈内での外部の変数のconst化はそれ自体が問題を発生させる部分的な解決策でしかなく、混乱をもたらしている
    • より良い解決策はconst化を削除すること

などの問題点を挙げています。

これらの問題点が解消されない限り、引き続きMicrosoftC++26にP2900を組み込むのに反対する、としています。

P3516R0 Uninitialized algorithms for relocation

未初期化メモリに対するリロケーションアルゴリズムのライブラリ機能の提案。

C++26に向けて、言語で(トリビアル)リロケーションの定義を提供するとともに、それを行うためのライブラリ操作を導入しようとしています(P2786/P1144)。その作業の最先端はP2786R11で行われていますが、そこでは最近機能のスコープを絞るためにライブラリのAPIの大半が分離されました。

この提案は、P2786が提供しようとしているリロケーションの概念を用いて、未初期化メモリに対するリロケーションによる初期化を行うアルゴリズムを提案するものです。

提案するのは主に次の3つの関数を基本としたものです

  • std::uninitialized_relocate()
  • std::uninitialized_relocate_n()
  • std::uninitialized_relocate_backward()
  • これらのRange版と並列版

これらのアルゴリズムは、ある未初期化なメモリ範囲を、別の範囲からそれぞれの要素をリロケートして初期化するものです。いずれの関数も入出力範囲のオーバーラップを許可し、途中で例外が送出された場合は入出力範囲のすべての要素が破棄されます。そして、このアルゴリズムが正常に完了した場合は入力の範囲に要素は含まれなくなり、未初期化状態になります。

この提案ではリロケーションという概念の定義などを提供しようとはしておらず、リロケーションをムーブ+破棄であると仮定してそのas-ifによってリロケーション動作を指定しています。これによって、P2786がマージされていない状況でもリロケーションAPIを提供することができ、P2786がマージされてもそのより高度なリロケーションの定義を自動で利用することができます。

提案文書より、vector::erase()で使用する例

// 今まで
constexpr iterator
vector<T>::erase(iterator first, iterator last) {
  if (first == last)
    return last;

  auto new_end = std::move(last, end(), first);
  std::destroy(new_end, end());

  end_ -= (last - first);
  return first;
}
 // この提案
constexpr iterator
vector<T>::erase(iterator first, iterator last) {
  if (first == last)
    return last;

  // Destroy the range being erased and relocate the
  // tail of the vector into the created gap.
  std::destroy(first, last);
  std::uninitialized_relocate(last, end(), first);  // 👈

  end_ -= (last - first);
  return first;
}

文書にはもう少しstd::vectorで使用する例が載っています。

リロケーション操作は通常、単なるムーブ代入よりも効率的であるため、これをいれておくことでC++26以降のライブラリのパフォーマンスの最適化を図ることができます。

P3527R1 Pattern Matching: variant-like and std::expected

パターンマッチングでstd::expectedを使用できるようにする提案。

以前の記事を参照

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

  • std::expectedstd::variant互換にすることでハンドリングするようにした
  • std::visitの変更を提案しなくなった

などだと思われます。

この提案では、std::expectedに対して次の変更を適用することで、match式においてstd::variantと同等に扱えるようにしています

  • .index()を追加
  • std::variant_size/std::variant_alternativestd::expectedで動作する特殊化を追加
  • std::expectedで動作するstd::getstd::size_tオーバーロードを追加

また、パターンマッチングに限らない場所でより使用感を近づけるために、オプションとして次の事を提案しています

  • std::expectedで動作するstd::holds_alternative()を追加
  • std::expectedで動作するstd::getの型指定するオーバーロードを追加
  • std::expectedで動作するstd::get_ifオーバーロードを追加

std::visitについては、2つしか選択肢がないことやパターンマッチング導入後に不要になることから変更を提案していません。

P3533R1 constexpr virtual inheritance

定数式で、仮想継承をしているクラス型を扱えるようにする提案。

以前の記事を参照

このリビジョンでの変更は、constexprコルーチンよりも前に採択される可能性に備えて代替の文言を用意したことです。

P3534R0 Avoid UB When Compiling Code That Violates Library Specification

コンパイル時に起こるタイプのライブラリのUBについて、IFNDRに置き換える提案。

標準ライブラリの仕様には、ライブラリが想定しないような使用方法がなされた場合にUBとなる規定が一部に存在しています。そのようなライブラリのUBのうち、一部のものは未定義動作が発生するのが実行時ではなくコンパイル時であることが分かっている場合があります。この提案は、そのようなケースのUBという言葉の使用を(ill-formed, no diagnostic required)に置き換えようとするものです。

その対象の例としては、ユーザーが標準ライブラリ内テンプレートの特殊化を追加することを禁止する規定です。これは現在UBとされていますが、このUBはコンパイル時に発生し、実行時に起こるわけではありません(それを追加することでどのテンプレート特殊化が選択されるか分からなくなることでUBとなるが、それが起こるのはコンパイル時のみ)。

実行時ではIFNDR(プログラム全体に対して一切要件を課さなくなる)よりもUB(違反が発生した部分のみの動作が未定義となる)の方が動作の指定としては優れているというか、マシなものです。しかし、コンパイル時は逆で、コンパイル時のUBはコンパイル処理そのものの動作を無制限のリスク要因としてしまうため、この様な規定方法はおそらく標準の意図するところではなく、通常必要とはされないはずです。そのためこの提案では、標準ライブラリのコンパイル時に発生するタイプのUBの使用をIFNDRに置き換えようとしています。

P3541R1 Violation handlers vs noexcept

違反ハンドラ(契約注釈)とnoexcept演算子の相互作用について明確にすることを求める提案。

以前の記事を参照

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

  • noexceptに加えて、スタック巻き戻しの保証についても考慮するようになった
  • noexcept演算子noexcept指定子が別の問題であることを明確にするために、議論を再構成した

などです。

このリビジョンでは、違反ハンドラからの例外送出を予告なしの例外送出(unannounced throw)と呼称し、次のようなコードにおいてスタック巻き戻しがどうなるかを問うています。

void set_positive(int* pValue)
{
  std::lock_guard _ {_mutex}; 
  _result = (*pValue > 0);  // i
}

iの行の式では現在例外送出されることがないため、実装はスタックの巻き戻しを考慮せずにmutexのアンロックを通常のフローに従って配置できます。しかし、P3081R0の世界では予告なしの例外送出時にこのようなところでも例外が送出される可能性があるため、その場合にどうするかを指定する必要があります。

それは、実行時のペナルティを受け入れてデストラクタ呼び出しを保証するか、デストラクタの呼び出しが保証されない別の種類のスタック巻き戻し機構が存在することになるか、のどちらかです。あるいは、実装が違反ハンドラを呼び出すか呼び出さないかの2択ではなく、呼び出さない・通常のスタック巻き戻しを伴う呼び出し・別のスタック巻き戻しを伴う呼び出し、の3種類から選択できるようにすべきでは?としています。

予告なしの例外送出においてこのようなプログラムの挙動が不明瞭である場合、バグからの復帰という意味では(例外の代わりに)未定義動作が起きている現状とあまり変化がありません。したがって、違反ハンドラからの例外送出時のプログラムの動作について(このスタック巻き戻しも含めて)明確に指定する必要がある、としています。

P3546R0 Explicit return type deduction for std::numeric_limits and numbers

std::numeric_limits<numbers>で型の指定を省略可能にする機能の提案。

std::numeric_limitsによって何らかの定数を取得する時、典型的には次のようなコードを書くことになりますが、欲しい型名を2回書くことになります。

float f1 = std::numeric_limits<float>::max();

変数宣言であれば、変数の型名はautoで受けることもできます。しかし、非静的メンバ変数の宣言や関数引数の宣言ではautoを使用できないため2回書かざるを得ません。

/* (1)- For structs */
struct F {
  float f1 = std::numeric_limits<float>::max();
};

/* (2)- For functions */
void func1( float f = std::numeric_limits<float>::max());

この提案はstd::deduceというタグ型を導入し、それを利用することによってこの2つ目の型の指定を省略可能にしようとするものです。

/* (1)- For structs */
struct F {
  float f1 = std::numeric_limits<float>::max();       // Before
  float f2 = std::numeric_limits<std::deduce>::max(); // After
};

/* (2)- For functions */
void func1(float f = std::numeric_limits<float>::max());       // Before
void func2(float f = std::numeric_limits<std::deduce>::max()); // After

元の型名よりもたいていの場合長くなりますが、変数の型名を変更したくなった場合に両方とも変更する必要が無くなるメリットがあります。また、std::deduceという特徴的な名前はそれが使用されている個所を目立たせ検索しやすくする効果もあります。

このstd::deduceは純粋なライブラリタグ型であり、std::numeric_limitsの場合は例えば次のように実装できます

namespace std {
  template<>
  struct numeric_limits<std::deduce> {
    struct max {

      // テンプレート型変換演算子
      template<typename Out>
      operator Out() {
        return numeric_limits< Out >::max();
      }

      /* ... */
    };
  };
}

変数テンプレートの場合も同様のテクニックによって実装可能です

inline constexpr auto M_ONES = 1.111111111111111111111111111111111111111L;

template< typename T = double >
struct m_ones_v_c {
  constexpr operator T() const {
    return M_ONES;
  }
};

template<>
struct m_ones_v_c<std::deduce> {
  template< typename Out >
  constexpr operator Out() const {
    return M_ONES;
  }
};

template<typename T>
inline constexpr auto m_ones = m_ones_v_c<T>{};

inline constexpr auto m_ones = m_ones_v_c<double>{};

これらの実装はおそらく、既存の使用方法に対してAPIの破壊的変更を伴わないものです。

提案ではstd::deduce<utility>に配置し、std::numeric_limits<numbers>に対してこれを使用可能にする変更を適用しようとしています。

P3547R0 Modeling Access Control With Reflection

クラスメンバのリフレクションにおいて、アクセスコンテキストを明示的に指定するようにする提案。

クラスメンバのリフレクションにおけるクエリにおいて、クラスのアクセス制御を考慮すべきかという問題が古くから議論の対象となってきました。P2996では一貫して次のような方向性の下で設計されています

  • リフレクション対象のクラスの全てのメンバのリフレクションを取得できる
  • 任意のエンティティのリフレクションに対して、プロパティをクエリできる
  • リフレクションされたエンティティを名前探索やアクセス制御に頼らずに、スプライシングによって直接参照する

これにより、members_of()によってクラスの全メンバのリフレクションを取得し、identifier_oftype_of等の術語によってそれらのプロパティを照会し、スプライシングによって様々なコンテキストでメンバを使用することができています。

しかし、P2996のレビューが進むにつれて、クラスのアクセス制御の扱いについて委員会内部でも様々な流派が存在することが分かってきました。

  • 多くの人は、クラスのイントロスペクションを許容するモデルはリフレクションの絶対要件であるとしている
  • ユーザーはプライベートメンバのリフレクションがそのまま取得できることに驚くかもしれず、オプトインにすべきだという意見も少なからず表明された
  • 呼び出しコンテキストがアクセスできるすべてのメンバのリフレクションを取得できるべき、という指摘もあった
  • アクセスできないメンバのリフレクションを取得できるべきではない、という人もわずかに居る

これらの立場の中には相容れないものもあるため、もはやすべての人々の意向を満たす解決策は存在していません。

この提案はこれらの懸念の一部を解消するために、access_contextという型の値によってアクセスコンテキストを表現し、それを値としてやり取りすることによってクエリが取得できるアクセスコンテキストを制御できるようにしようとするものです。

この提案では例えば、members_of()は第2引数にアクセスコンテキストを取るようにします

consteval auto members_of(info cls, access_context ctx) -> vector<info>;

std::meta::access_context::unchecked()を使用することで現在のP2996の動作である無制限のアクセス制御と同等のコンテキストを取得できます

using std::meta::access_context;

class Cls {
private:
  static constexpr int priv = 42;
};

constexpr std::meta::info m = members_of(^^Cls, access_context::unchecked())[0];
static_assert(identifier_of(m) == "priv");
static_assert([:m:] == 42);

他には、現在のコンテキスト(呼び出し元の最も内側のコンテキスト)を取得するstd::meta::access_context::current()、グローバル名前空間からのアクセスコンテキストを取得するstd::meta::access_context::unprivileged()の3つが用意され、access_contextの値はこれら3つの関数のいずれかから取得され、他の方法で構成できません。

これらの基本メタ関数を使用して作成されるメタ関数では、std::meta::access_contextの値を受け取ってそれを使用するようにすることで同等のアクセス制御の指定を行えます

consteval auto constructors_of(std::meta::info cls,
                               std::meta::access_context ctx) {
  return std::vector(std::from_range,
                     members_of(cls, ctx) | std::meta::is_constructor);
}

このようにすることで、一つの関数で様々なアクセス制御の指定に対応することができ、P2996R7で導入されたget_public_members系の関数名でアクセス制御の指定を行う必要がなくなります。

protectedメンバや基底クラスメンバへのアクセス制御には複雑なものがあるので、これだけだと対応できません。例えば次のようなクラスがある時

struct Base {
protected:
  int prot;
};

struct Derived : Base {
  void fn();
};

Derived::fn()の定義内からは、protメンバへアクセスできる一方で、そのメンバへのポインタを形成することはできず、Baseのスコープを指定する必要があります

void fn(){
  this->prot = 42;  // ok

  auto mptr = &Derived::prot; // ok
  auto mptr = &Base::prot;    // ng
}

このような複雑なアクセス制御のセマンティクスを解決できなかったことが、この提案の元になったstd::meta::access_contextAPIをP2996R7から削除させることになりました。

この問題の解決のために、std::meta::access_context.via()メンバ関数を追加し、そこにスコープを指定するクラスのリフレクションを渡すことで、上記のようなセマンティクスの再現を可能とします

void Derived::fn() {
  using std::meta::access_context;
  constexpr auto ctx1 = access_context::current();  // 現在のアクセスコンテキスト
  constexpr auto ctx2 = ctx1.via(^^Derived);        // ctx2にはDerivedクラスをスコープとして指定

  static_assert(nonstatic_data_members_of(^^Base, ctx1).size() == 0);
    // スコープクラスが指定されない場合第1引数のクラス名が使用され、Baseのスコープでアクセス可能なBaseの非静的データメンバをクエリする
    // が、コンテキストはDerivedであり、Baseのprotectedメンバにアクセスできない
    // Derivedのコンテキストからは、Base::protにアクセスできない

  static_assert(nonstatic_data_members_of(^^Base, ctx2).size() == 1);
    // ok、Derivedのスコープを使用し、DerivedのコンテキストでBaseの非静的データメンバをクエリする
    // Derivedのコンテキストから、Derived::protにアクセスできる
}

このaccess_contextクラスの構造は次のようになります

class access_context {
  consteval access_context(info scope, info naming_class) noexcept
      : scope{scope}, naming_class{naming_class} { }

public:
  const info scope;  // exposition only
  const info naming_class;  // exposition only

  consteval access_context() noexcept : scope{^^::}, naming_class{} { };
  consteval access_context(const access_context &) noexcept = default;
  consteval access_context(access_context &&) noexcept = default;

  static consteval access_context current() noexcept {
    return {__metafunction(detail::__metafn_access_context), {}};
  }

  static consteval access_context unprivileged() noexcept {
    return access_context{};
  }

  static consteval access_context unchecked() noexcept {
    return access_context{{}, {}};
  }

  consteval access_context via(info cls) const {
    if (!is_class_type(cls))
      throw "naming class must be a reflection of a class type";

    return access_context{scope, cls};
  }
};

この提案ではまた、あるメンバのリフレクションが指定されたコンテキストでアクセス可能かどうかをクエリするメタ関数is_accessible()を追加しています。これを用いると、指定したコンテキストからアクセス可能なメンバのリフレクションのみを取得する処理が簡単に書けます

consteval auto has_inaccessible_nonstatic_data_members(info cls,
                                                       access_context ctx) -> bool {
  return !std::ranges::all_of(members_of(cls, std::meta::access_context::unchecked()),
                              [=](info r) { return is_accessible(r, ctx); });
}

consteval auto has_inaccessible_bases(info cls, access_context ctx) -> bool {
  return !std::ranges::all_of(bases_of(cls, std::meta::access_context::unchecked()),
                              [=](info r) { return is_accessible(r, ctx); });
}

この2つの関数は標準ライブラリで用意していてほしいという要望があったため、この提案に含まれています。

以前に同様の懸念に対処しようとしていた提案(P3451R0/P3473R0)ではクエリ時ではなくスプライシング[:r:])時にアクセスチェックを行うことによってアクセス制御をリフレクションに反映させようとしていました。しかし、クエリそのものは出来てしまい、リフレクションさえ取得できればアクセス制御を回避することが可能なメタ関数が多数あるため、そのアプローチではアクセス制御の懸念の解消には不十分でした。

この提案では、クエリの段階でアクセス制御を考慮し調整できるようにすることで、アクセス制御のリフレクションへの組み込みをより確実に行うことができます。なおかつ、access_context::unchecked()などの関数はコード上で目立つ(検索が容易である)ため、コード作成者の意図を誤解しづらくなっています。

P3548R0 P1030 std::filesystem::path_view forward progress options

path_viewに対する懸念に対しての解決の提案。

P2645では、提案中のpath_viewに対して標準化に当たってのいくつかの懸念点が指定されていました。これについてLEWGで活発な議論が行われ、そこでは問題は主にpath_viewが内部パス文字列表現のためにcharを使用することによってWindowsANSIエンコーディングを使用してしまう事、に絞られたようです。

この問題によってpath/path_view -> パス文字列 -> path/path_viewのようなラウンドトリップを行った場合に、元に戻らなくなる(同じパスを表さなくなる)ことが懸念されています。

しかし、std::filesystem::pathにおいては、内部パス表現は渡されたパスのビット列を正確にネイティブのAPIに渡すことを意図したものであり、そもそもラウンドトリップをサポートし切ることはあまり目指されてはいないようです。path_viewにおいてもその設計を踏襲しており、内部ビット表現にcharを使用するのは特定のエンコーディングを仮定するのではなく、渡されたパス文字列のビットを(システムにとって)正しく保持することを目的とするものです。

そして、path_viewにおいては、保持するパス文字列ビットを人間にとってのパス文字列に変換する際には、それをUTF-8エンコーディングとして読み取るという設計になっており、これをfilesystem::pathにも適用すべきとしています。

この提案では、LEWGにおける議論及びその後の調査によって導かれた解決策を4つ提示しています。

  1. ネイティブファイルシステムエンコーディングcharではないプラットフォームにおけるchar内部表現サポートを削除する
  2. 全てのプラットフォームでchar内部表現サポートを削除する
    • 利点
      • プラットフォーム間でpath_viewの一貫性を確保できる
    • 欠点
      • pathへのchar文字列入力は可能だが、path_viewではできなくなる
      • pathの内部表現がcharである場合、pathからpath_viewへの変換は可能であるため、意外性がある
  3. path_viewの表現はそのままで、path_viewのフォーマッターはpathのフォーマッタと同じ動作をする
    • 利点
      • エンコード周りの処理はpathpath_viewで同程度の複雑さになり、これは一貫している
    • 欠点
      • これは過去の誤りを助長するもので、誤りを悪化させ続けることを解消するのに役立たない
    • 備考
  4. C++26(予定の)path_viewの表現はそのままで、フォーマッタをC++29まで延期する
    • 利点
      • path_viewはソースエンコーディングの認識を保持するのに対して、pathは忘れるため、一部のコーナーケースにおいて人間にとっての最適性という意味でより優れたフォーマッタを設計できる可能性がある
    • 欠点
      • pathpath_viewのフォーマッタの一対一対応が損なわれる可能性がある

筆者の方は案3、すなわち現状維持を押しています。

また、P2645で別の懸念点であったパフォーマンスの問題について、ベンチマークが提供されています

P3549R0 Diverging expressions

値を生成しない式を表現するための特別な型(ボトム型)を追加する提案。

値を生成しない式とはvoid型の式の事ではなく、制御フローを脱出することでいかなる値を生成することもない式の事です。このような式の事を発散する式(Diverging Expression)とも呼び、現在のC++には発散する式が2種類存在します

  • throw
  • [[noreturn]]関数の呼び出し式

この2つの式はどちらも発散する式ではありますが、その扱いは微妙に異なっています。例えば条件演算子において違いを観測することができます

int x1 = condition ? 42 : throw std::runtime_error("oops"); // ok
int x2 = condition ? 42 : std::terminate(); // ng

条件演算子による式は2つの分岐の型を統合した型を持つ必要がありますが、片方のオペランドthrow式の場合その型はもう一方のオペランドから決定されます。一方で[[noreturn]]関数の呼び出し式も同様に発散する式ではあるものの、このような特別扱いはされないためその関数の戻り値型が式の型となり、そこから条件演算子の型を決定しようとするため、上記の例ではエラーになります。

これを回避するにはたとえば次のように書くことができます

int x = condition ? 42 : throw (std::terminate(), 0);

これはthrow式の評価より前にstd::terminate()が呼ばれることで当初の意図と同様に動作します。

このことは提案中のパターンマッチングにおいても同様であり、throw式は特別扱いされる一方で[[noreturn]]関数はその関数の戻り値型が取得されます。

// 理想、動作しない
void f(int i) {
  int j = i match {
    0 => 0;
    _ => std::terminate();  // int と void でmatch式の型が統合されない
  };
  use(j);
}

// throw式によるワークアラウンド
void g(int i) {
  int j = i match {
    0 => 0;
    _ => throw (std::terminate(), 0);
  };
  use(j);
}

// do式(提案中)と明示的な型指定によるワークアラウンド
void h(int i) {
  int j = i match {
    0 => 0;
    _ => do -> int {
      std::terminate();
    };
  };
  use(j);
}

この提案は、発散する式をその種類の違いによらず型システムで認識できるようにすることで、このような問題を解決しようとするものです。

一つの方法としては式の持つプロパティに式の発散有無を追加してそれを確認することです。しかし、この方法はスケールせず、規定が複雑になるとともに式の種類が増えた場合に一貫性を持って対応するのが難しくなります(後述のdo式がまさに該当します)。

この提案では別の解決策として、ボトム型と呼ばれる種類の特別な型を追加しようとしています。

ボトム型とは、まさにこのような発散する式を表すためにほかの言語ですでに使用されている特殊な型です。ボトム型は具体的なオブジェクトを構築することができず、他の任意の型に変換することができます。例えば、任意の型Tに対してTにもT&にも変換することができ、このような性質は通常のコードでは再現できません。また、auto(*)(T) -> noreturn_tauto(*)(T) -> Uに変換することができます(実際にはC++ではこの変換をいつも許可することはできませんが)。

ここではそれを仮にstd::noreturn_tとして、次の事を提案しています

  • 値を持たない式、すなわち無条件に発散する式を表すための新しい型std::noreturn_tを追加する
  • std::noreturn_tは任意の型に変換可能
  • std::noreturn_tは構築可能ではなく、あらゆる方法で変換・生成できない
  • std::noreturn_t(*)(Args...)は任意の型Rに対してR(*)(Args...)に変換可能
    • ただし、この変換は変換元のポインタが定数式である場合のみ許可される
  • 現在[[noreturn]] void f()と指定されている標準ライブラリ関数をstd::noreturn_t f()に変更する
  • throw式の型をvoidからstd::noreturn_tに変更する
  • throw式の条件演算子における例外規定を削除する

これによって、式の発散を型システムで扱えるようになります。

例えば条件演算子において

condition ? 42 : throw std::runtime_error("oops")

この式の2番目のオペランド42)の型はintであり、3番目のオペランドの型はstd::noreturn_tとなります。std::noreturn_tは任意の型に変換可能であるため、この条件演算子の式の型はintになります。この過程でthrow式を特別扱いする必要はありません。

同様に

condition ? 42 : std::terminate()

これも先ほどと同じ推論により式の型はintになります。

ボトム型はこれ以外にも、いくつか有用な使用法があるかもしれません。

1つは、std::function<std::noreturn_t()>の様な関数型の宣言と使用が可能になることで、処理が完了しない(正常にリターンしない)コールバックをstd::functionなどで扱うことができるようになります。

もう1つはstd::expected<T, std::noreturn_t>のように使用して、エラーが起こらないことを表すことです(あるいは逆にstd::expected<std::noreturn_t, E>として必ず失敗することを表現することもできます。これは一見意味がなさそうに見えますが、規約や一貫性のためなどで関数の戻り値型をstd::expectedで統一しているような場合に、必ず成功する/失敗する処理を型によって表現可能になります。

この場合、std::noreturn_tを指定された特殊化はその値を保持する必要がないためかなり単純な実装による最適化が可能になります。

最後は、value_typeを提供できないValueless Rangesというカテゴリのrangeを定義するのに使用できることです。例えば抽象基底クラス(A型とすると)による範囲を生成したくなったとき、referenceとしてA&を指定して返すことはできるものの、value_typeとしてAを指定できません。おそらく使用されるまでは問題にならないものの、使用されてしまうと抽象クラスがオブジェクトを構築できないためにエラーになります。rangevalue_typeは一部のアルゴリズムが要求します。

他にも、空の配列を構造体末尾に持つ可変長構造体のような型などvalue_typeとして使用ができないような型をrangeの要素型にすることは現在困難です。特に、必ずしもいつもコンパイルエラーにならないことがあります。std::noreturn_tvalue_typeに指定することでこれが解決され、value_typeがないことを表現できるようになるとともに、value_typeを構築しようとすると確実にエラーになります。

さらにこの提案では、P2806で提案中のdo式においても、同じルールを適用することを提案しています。その際

std::terminate()
do { std::terminate(); }
do { log::error("hasta la vista"); std::terminate(); }

これらの式のいずれもを発散する式として(型がstd::noreturn_tになるように)扱うようにすることを提案しています。

現在のdo式は最終行の分を暗黙のreturn文にしないためここには特別扱いが必要で、do_returnがなく戻り値型指定もされていない場合にdo式の最後の文が発散しないかをチェックするようにすることを提案しています。

P3550R0 Imports cannot ...

Cの可変長引数機能をC++で使用できないようにする提案。

名前付きモジュールからはマクロはエクスポートされないため、import std;しただけではCの可変長引数にアクセスすることができません。

import std;

void c_style_variadic_arg_func(int arg1, ...) {
  ::va_list arg_list; // ok、これはマクロではない
  va_start(arg_list, arg1); // ng、これはマクロ

  int arg2 = va_arg(arg_list, int);  // ng、これもマクロ

  va_end(arg_list); // ng、これもマクロ
}

これを機能させるには<cstdarg>もしくは<stdarg.h>のどちらかのヘッダのインクルードが必要になります。

一方で、Cの可変長引数機能は上記例からも明らかなように、使用するのが難しく間違って使用するのが簡単な機能であり、この機能の使用にはセキュリティリスクがあります。そして、C++には可変引数テンプレートという完全かつ型安全な代替機能があり、もはやCの可変長引数機能を使用する理由はほとんどなくなっています。

この提案は、モジュールコードでCの可変長引数機能を使用できるようにするよりも、この機能のサポートを縮小あるいは削除することを提案するものです。主に次の2つの事を提案しています

  • 関数がグローバル名前空間にあり、extern "C"リンケージを持たない場合、Cの可変長引数による関数宣言を非推奨とする
  • C++標準ライブラリから、<cstdarg><stdarg.h>ヘッダを削除する

printf()scanf()等の関数は非推奨としてサポートされ続けます。一方で、2つ目の変更によってC++プログラムがCの可変長引数機能を使用する能力が失われ、これによってC++コンパイルできないCのコードが増加することになります。提案ではこれをC++のセキュリティを確保するためのコストであるとしています。ヘッダを削除せずに非推奨とする穏健なアプローチはセキュリティホールを開いたままにするとして推奨していません。

P3552R0 Add a Coroutine Lazy Type

std::executionに対応したコルーチンlazy型の提案。

lazy型は他言語やライブラリなどではTask<T>型とよく呼ばれているものに該当します。非同期処理の戻り値型として使用して、lazy<T>オブジェクトにco_awaitすることで非同期操作の再開および完了待機を行い、完了したらその結果を取り出して待機元に返します。

これは以前にP2506R0等で提案されていたstd::lazyと同等のものでもありますが、それらとの違いはstd::execution(P2300)を考慮していることです。それによって、単にコルーチンとして扱えるだけでなくsenderとして扱えることもでき、lazyコルーチンの内部でsenderとコルーチンをco_awaitしたりすることができます。

// lazyコルーチン
auto f1() -> std::execution::lazy<int>;

// senderを返す何か
auto f2() -> std::execution::sender {
  // 例えば
  return std::execution::just(1);
}

auto example() -> std::execution::lazy<int> {
  // これもできるし
  int r1 = co_await f1();
  int r2 = co_await f2();
  
  // これもできる
  auto [r3] = std::execution::sync_wait(f1());
  auto [r4] = std::execution::sync_wait(f2());

  co_return 0;
}

int main() {
  // senderとして実行
  auto [r] = std::execution::sync_wait(example());

  return r;
}

この提案において、std::executionを踏まえたコルーチンタスク機能として提供すべきとしている機能は次のものです

  1. コルーチンタスクは awaiter/awaitableフレンドリーでなければならない
    • ライブラリ提供・ユーザー提供に関わらず、awaitableco_awaitできなければならない
  2. senderの完了シグネチャを書き換えることによって、コルーチンタスクでのsender利用を容易にする
    • 3つの完了チャネルをそれぞれコルーチンにおける完了・エラー・中断に翻訳する
  3. コルーチンタスクはsenderフレンドリーでなければならない
    • 非同期コードは今後、senderを使用して書かれco_awaitを使用して待機されることが多くなると思われる
    • 現在の実装(unifex/stdexecなど)では、複数のset_value完了シグネチャを持つsenderco_awaitをサポートしていない
  4. コルーチンタスクはデフォルトでスケジューラアフィニティを持つべき
    • コルーチンは通常、中断したコンテキストと異なるコンテキストで再開されると問題を起こしやすい
    • コルーチンタスクは、中断したスケジューラと同じスケジューラ(実行コンテキスト)で再開する
  5. コルーチンタスクはアロケータサポートを提供する必要がある
    • コルーチンを利用する場合、少なくともコルーチンフレーム分のメモリ確保が必要になり、それを回避する最適化は常に利用可能であるわけではない
    • new/deleteがサポートされていない環境でも使用可能にするために、アロケータによってメモリ確保方法をカスタマイズできる必要がある
  6. 子操作においてユーザーがクエリ可能な環境を提供する
    • コルーチンタスク内でsenderco_awaitする場合、そこで接続されるreceiverを介して環境を提供する
    • この環境は例えば、子操作に停止要求を転送したり、子操作がget_scheduler()をクエリする際にそれをカスタムしたりするのに使用できる
  7. コルーチンタスクはキャンセルされたことを表現できるべき
    • 子操作に提供したreceiverに対してset_stopped()が呼ばれたことを親操作のコルーチン状態として表現できる
  8. 例外送出を伴わずに、内部のエラー発生を表現できると良い
  9. 例外送出によるコルーチンからの脱出を表現するために、コルーチンタスクの完了シグネチャにはset_error_t(std::exception_ptr)を含める
    • コルーチンタスクに対してコルーチンが例外を送出しないことを表明できれば、これを回避することもできる
  10. 数の子操作のco_awaitによって発生しうるスタックオーバーフローを防止する
    • 適切なスケジューラを用いてスタックオーバーフローを防止する
  11. コルーチン終了時の非同期クリーンアップを何かしらの方法でスケジュールできると有用な場合がある
  12. 標準lazyコルーチンは、ユーザーのニーズを100%満たすことを目指さない
    • ユーザーがその特定のニーズに沿うlazy/task型を実装する場合でも有用な汎用のコンポーネントを定義・使用する

例えば、子操作senderに対して環境カスタマイズを提供するために、std::lazyは2つ目のテンプレートパラメータで環境についてのカスタマイズを行えるようにすることが提案されています。

namespace std::execution {
  template<typename T = void, typename Context = ...>
  class lazy {
    ...
  };
}

2つ目のコンテキストパラメータによって例えば

  • 子操作の環境カスタマイズ
  • スケジューラアフィニティを無効化する
  • アロケータをカスタマイズする
  • コルーチンがnoexceptであることを指定する
  • 追加のエラー型を指定する

などのカスタマイズを行うことができます。

例えばアロケータをカスタマイズする例

using ex = std::execution;

struct allocator_aware_context {
  using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
};

template <typename...A>
auto fun(int value, A&&...) -> ex::lazy<int, allocator_aware_context> {
  
  // 環境を介して渡されたアロケータを取得できる
  auto alloc = co_await ex::read_env(ex::get_allocator);
  // co_await内で用意され接続されるreceiverを介して、`lazy<T, C>`の`C`の指定する環境にアクセスできる
  
  use(alloc);

  co_return value;
}

int main() {
  // アロケータを渡さずに実行
  ex::sync_wait(fun(17));

  // アロケータを渡して実行
  using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
  ex::sync_wait(fun(17, std::allocator_arg, allocator_type()));
}

この例に示されているように、コルーチンタスクの環境はlazyの第2テンプレートパラメータで指定されたコンテキストをベースとして、通常のstd::executionプロトコルに従った形でセット/クエリを行うことができます

struct context {
  int value{};

  context(auto const& env)
    : value(get_value(env))
  {}
  
  int query(get_value_t const&) const noexcept { return this->value; }
};

auto f() -> ex::lazy<void, context> {
  auto sched(co_await ex::read_env(get_scheduler));
  auto value(co_await ex::read_env(get_value));

  std::cout << "value=" << value << "\n";
  // ...
}

int main() {
  ex::sync_wait(
    ex::write_env(
      f(),
      ex::make_env(get_value, 42)
    )
  );
}

この例では、contextで指定される環境に対してさらに書き込みを行った環境を、lazyコルーチン中からクエリして取得しています。

スケジューラアフィニティを実現するためのスケジューラの取得もこの環境クエリによって行われます。具体的には、lazyコルーチン本体内でsenderco_awaitされるときに、使用するreceiverの環境(これはコンテキストパラメータの指定が反映されている)から通常のget_scheduler(get_env(rcvr))クエリによってスケジューラを取得し、待機されたコルーチンの再開時(ネストコルーチンの完了時)にawait_transform()continues_on()することで元のコンテキストのスケジューラを復帰します。

// await_transform() でのスケジューラ復帰の例
template <ex::sender Sender>
auto await_transform(Sender&& sndr) noexcept {
  return ex::as_awaitable_sender(
    ex::continues_on(std::forward<Sender>(sndr), this->scheduler);
  );
}

await_transform()はプロミス型で実装され、コルーチン中断時(ネストコルーチンのco_await時)に取得したスケジューラはプロミス型で保持されます。その型はコンテキストパラメータCからC::scheduler_typeで指定されるか、デフォルトではany_scheduler(型消去スケジューラ型)が使用されます。

lazyコルーチン内でsenderco_awaitする場合(co_await sndr)、senderの各チャネルは自動的にコルーチンの完了動作にマッピングされます。これはプロミス型のawait_transform()内でas_awaitable(sndr)(これは既存のライブラリ機能)を用いて行われ、そこでは接続されているレシーバーrcvrを用いて次のようにマッピングされます

  • set_stopped(std::move(rcvr))(キャンセル)
    • プロミス型の.unhandled_stopped()が呼び出され、待機中コルーチン(親コルーチン)は再開されない
    • lazy自身(親コルーチン)もset_stopped_t()で完了する
  • set_error(std::move(rcvr), error)(エラー)
    • 待機中コルーチンは再開され、co_await sndr式は例外としてerrorを送出する
  • set_value(std::move(rcvr), a...)(完了)
    • co_await sndr式は次のいずれかの結果を生成する
      1. set_value()の引数リスト(a...)が空の場合、結果はvoid
      2. 引数リストが1要素のみの場合、結果はa
      3. それ以外の場合、結果はstd::tuple(a...)

なお、lazyコルーチンではco_yieldはサポートされないか、エラー報告にのみ使用される予定です。

set_value_t完了シグネチャにはある程度の自由度があり、持たない場合と複数持つ場合があり得ます。持たない場合、待機対象のsenderset_stopped()/set_error()のどちらかで完了することになるため、co_await sndrの結果はそのまま中断し再開されないか例外を送出するかのどちらかです。

// lazyコルーチン内でsenderをco_awaitする例
auto fun() -> lazy<> {
  co_await ex::just();                              // void
  auto v = co_await ex::just(0);                    // int
  auto[i, b, c] = co_await ex::just(0, true, 'c');  // tuple<int, bool, char>

  try { co_await ex::just_error(0); } catch (int) {} // exception
  co_await ex::just_stopped();                       // cancel: never resumed
}

set_value_t完了シグネチャを複数持つ場合、as_awaitable(sndr)がエラーになることでそのようなco_await式もエラーになります。この場合、co_await into_variant(sndr);のようにして複数のset_value_t完了シグネチャを単一に集約することでサポートされます。

並行キューのpop操作の例(キューが閉じていなければ取り出した値を、キューが閉じられていればvoidで完了する)

auto pop_demo(auto& queue) -> lazy<>{
  // auto value = co_await queue.async_pop(); // doesn't work
  std::optional v0 = co_await (queue.async_pop() | into_optional);
  std::optional v1 = co_await into_optional(queue.async_pop());
}

into_optional()の場合、set_value_t完了シグネチャのうち値を持つものを有効値に(完了値が複数の場合はstd::optional<std::tuple<T...>>型)、値を持たないものを無効値にマッピングします。

他の類似物としてはinto_expectedを候補に挙げています。ユースケースに応じてこれらをユーザーが適宜使い分けることで場所によって使いやすい完了シグネチャを選択できることを目的としており、この動作をas_awaitable()でハードコードしてしまうことを避けています。

環境からストップトークンを取得できるため、コルーチンタスク(及びネストしたsender操作)はそのトークンに応じて適応的にキャンセルを行える必要があります。この場合はset_stopped()で完了すればよいため、co_await ex::just_stopped();で自然にコルーチンをキャンセル(し、呼出し元に処理を戻す)できます。

ネストしたsenderset_errorで完了した場合、あるいは直接的に例外が送出された場合、コルーチンの境界で例外はキャッチされset_error_t(std::exception_ptr)の完了にマッピングされます。これはプロミス型のunhandled_exception()で行われます。

この方法には

  1. サポートされるエラー完了型がset_error_t(std::exception_ptr)のみ
    • 他のエラー報告方法もサポートされるのが望ましい
  2. エラー報告のために例外を送出しなければならない
    • 例外が無効化されている場合、エラー報告の手段がなくなる
  3. std::exception_ptrからエラー情報を取得するためには再スローしなければならない
  4. 例外が利用できない場合にset_error_t(std::exception_ptr)コンパイルエラーとなる可能性がある

これらの問題に対処するために、まずエラー完了シグネチャをコンテキストパラメータCを通してカスタマイズできるようにします。C::error_signatures型が宣言されている場合(この型はset_error_t<E>のリストとなるcompletion_signatures<>の特殊化である必要がある)、それをエラー完了型として使用するようにします。

そのうえで、コルーチン本体からエラー報告するためにまず、エラーを識別するためのwith_error<E>クラステンプレートを用意して

// 例えばこんな単純な実装で良い
template <typename E>
struct with_error{ E error; };

コルーチンタスクから明示的にエラーを返すための構文として次の3つを提示しています

  1. co_return with_error{err};
    • co_returnするとコルーチンはそこで終了し、オプションとして引数を渡すこともできる
      • プロミス型のreturn_value()内でset_error()マッピングする
    • lazy<void, C>lazy<>)の場合に使用できない
  2. co_await with_error{err};
    • awaiterawait_suspend()が呼び出された時点でコルーチンは待機状態となり、安全に破棄可能になる。これはset_stopped()マッピングと同じ仕組みを用いている
    • コルーチンからエラーを返すのと、ネストしたコルーチン/senderの完了を待機するのが同じ構文になってしまう
  3. co_yield with_error{err};
    • プロミス型の.yield_value()に転送され、そこからawaiterを返すことができる
    • awaiterawait_suspend()の呼び出しによってコルーチンは中断され、そこでset_errorマッピング可能
    • エラーでコルーチンを完了させる目的でco_yieldを使用するのはco_awaitよりも適している

この提案ではどれを選択してもいませんが、3の方法を推しているようです。

このように、このlazy型は以前のものに比較するとかなりstd::executionとの親和性が向上していることが分かります。ただし、今の時点では具体的な決定はまだ先ではあるので、設計は変化する可能性があります。

この機能(std::lazy)は後のリビジョンでstd::taskとなり、C++26に採択されたようです。

P3554R0 Non-transient allocation with vector and basic_string

std::stringstd::vectorに限定した、非一時的なメモリ割り当てを許可する提案。

現在、定数式で動的確保したメモリ領域は同じ定数式の中で解放しなければなりません。これによって、定数式で使用したstd::vectorstd::stringを実行時に持ち越すことはできなくなっています。これは現在でも不便ですが、C++26でリフレクションが利用可能になるとさらに不便さが増大する事が予想されます。

定数式で確保したメモリ領域を実行時に持ち越せるようにする(Non-transient allocation: 非一時的なメモリ割り当て)事についてもC++20時点から議論されているものの、合意を得られた一般的な設計はまだ存在していません。この提案では、一般的な設計以前にとにかくstd::stringstd::vectorだけをその制限から解放して、定数式で使用したstd::string/std::vectorオブジェクトをそのまま実行時でも利用できるようにしようとする提案です。

提案ではまず、次の条件を全て満たす場合のコア定数式Eの評価中に発生するメモリ割り当てAconstexpr-persistentとして定義します

  1. Eは定数式で使用可能なオブジェクトOの初期化するために使用されている
  2. AEの評価内で解放されない
  3. Aeligible for constexpr-persistentである

そのうえで、定数式におけるnew/std::allocator<T>::allocate()の規定にある「定数式の評価中に確保した領域はその評価の内部で解放されなければならない」という規定を「定数式の評価中に確保した領域は、それがconstexpr-persistentであるか、(さもなければ)その評価の内部で解放されなければならない」のように緩和して指定しなおします。

そして、std::vector(特殊化)およびstd::stringの要素のために割り当てたメモリ領域をeligible for constexpr-persistentである、と規定します。

これにより、std::string/std::vectorが定数式にで割り当てメモリ領域はeligible for constexpr-persistentとなり、eligible for constexpr-persistentであるような割り当てはconstexpr-persistentであるためその定数式の評価内で解放されている必要は無くなります。

そして、定数式で使用したstd::string/std::vectorオブジェクトは、追加の操作なくそのまま実行時に引き継ぐことができます。

将来より一般的な非一時的なメモリ割り当ての方法が導入された時でも、std::string/std::vectorは間違いなくその対象になることが分かっています。しかし、それを待っているといつになるか分からないため、細かい規定を省略しつつstd::string/std::vectorだけはとにかく定数式から実行時に引き継ぐことができるようにしようとしています。

P3555R0 An infinite range concept

終端の無い範囲を検出するためのコンセプトの提案。

std::views::iota(1)のように、終端の無い無限の範囲は割と簡単に得ることができます。このような範囲であってもRangeアダプタで使用することができるのですが、長さが無限であるという性質によって一部のアダプタがうまく動作しないことがあります。

auto a = views::iota(0) | views::reverse;
a.begin(); // 無限ループ

views::iota(0)common_rangeではないため番兵から逆に進行することができず、views::reverseによるviewからのイテレータ取得時(begin()呼び出し時)に先頭から終端までイテレータを進行させることによって最後の要素を検出しようとします。しかし、views::iota(0)は無限の範囲なのでそれは無限ループします。

このケースでは番兵型がunreachable_sentinel_tであることを検出することで回避可能ですが、これが別のアダプタで隠蔽されていると結局同じ問題が発生します(views::zip(views::iota(0), views::iota(1))など)。この場合に、範囲の無限性を検出することができれば、このようなバグを静的に回避することができます。

範囲の無限性を検出することのもう一つのモチベーションは、P3179R4で提案されている並列アルゴリズムによるバイナリtransformにおける入力範囲長の自由度の確保があります。バイナリtransformranges::transform(execution::par, rng1, rng2, output, fn)の様に2本の範囲を入力として使用するtransformであり、この場合に両方の入力範囲がsized_rangeであることを要求します。これは安全のためとのことですが、どちらかの範囲に整数インデックス(すなわちviews::iota(1))や定数列を渡して、もう片方の入力データによって入力長を指定する、の様なユースケースが存在しているため、この仕様は少し物議を醸しています。

この場合にも、範囲の無限性を事前に検出できれば、安全に入力範囲の長さを計算することができるようになります。

最後に、範囲の無限性を検出できればinfinite_range | views::take(n)のような式をinfinite_range | views::unchecked_take(n)(P3230R1で提案されているもの)に最適化するようなことが簡単にできるようになります(takeされる長さnよりも入力範囲が長いことが分かるため)。

これらのモチベーションより、この提案では範囲の性質としての無限性を検出するためのinfinite_rangeコンセプトを提案しています。

この提案では、Range-v3で定義されている範囲の濃度の概念を簡易化したものによって、infinite_rangeを定義づけています。

  • 既知の有限長: sized_range
  • 無限長: infinite_range
  • 不明: !sized_rangeかつ!infinite_range

濃度のモデルが簡略化されていることで!infinite_rangeは必ずしも有限長を意味しなくなっています(有限長 or 検出できなかった無限長、を表す)。一方で、infinite_rangeが満たされている場合はその範囲は確実に無限長であることを表しています。

範囲の無限性は必ずしも確実に検出できないので、infinite_rangeには何かしらのオプトインが必要になります。提案ではその2つのオプションを提示しています

  1. enable_infinite_range
    • enable_borrowed_rangeと同様の方法
  2. size()が特定のタグ型を返す
    • 無限範囲はsized_rangeではないので整数値を返さなくても問題ない

1の方法の例

namespace std::ranges {
  template <class R>
  constexpr bool enable_infinite_range = same_as<sentinel_t<R>, unreachable_sentinel_t>;

  template <class R>
  concept infinite_range = range<R> && enable_infinite_range<remove_cvref_t<R>>;
}

// ユーザーコードで
template <>
constexpr bool std::ranges::enable_infinite_range<my_infinite_range> = true;

template <class R>
constexpr bool std::ranges::enable_infinite_range<my_range_adaptor_that_does_not_affect_cardinality<R>> = std::ranges::infinite_range<R>;

2の方法の例

namespace std { // or std::ranges
  struct infinite_tag_t {};
  inline constexpr infinite_tag_t infinite_tag;
}
namespace std::ranges {
  template <class R>
  concept infinite_range = range<R> && (
    same_as<seninel_t<R>, unreachable_sentinel_t>
    || requires(R&& r) { { r.size() } -> same_as<infinite_tag_t>; }
    || requires(R&& r) { { /*ADL*/size(r) } -> same_as<infinite_tag_t>; }
  );
}

// ユーザーコードで
class my_infinite_range {
public:
  auto size() const { return std::infinite_tag; }
};

template <class R>
class my_range_adaptor_that_does_not_affect_cardinality {
  R _r;

public:
  auto size() const requires std::ranges::sized_range<R> { return std::ranges::size(_r); }
  auto size() const requires std::ranges::infinite_range<R> { return std::infinite_tag; }
};

2の方法の方が、view型のように他の範囲をラップする場合にこの性質を伝播させやすいというメリットがあるため、ここでは2の方法を押しています。

namespace detail {
  auto potentially_infinite_size(std::ranges::sized_range auto&& r) { return std::ranges::size(r); }
  auto potentially_infinite_size(std::ranges::infinite_range auto&&) { return std::infinite_tag; }

  template <class R>
  concept enable_size = requires(R&& r) { detail::potentially_infinite_size(r); };
}

template <class R>
class my_range_adaptor_that_does_not_affect_cardinality {
  R _r;

public:
  // size()をこのように定義することで伝播を自動化できる
  auto size() const requires detail::enable_size<R> { return detail::potentially_infinite_size(_r); }
};

ちなみにどの方法であっても、その番兵型がunreachable_sentinel_tであるような範囲は自動的に無限長であると識別されます。

現在のアダプタの中だと、次のものは基底の範囲がinfinite_rangeならばinfinite_rangeになるようにオプトインが必要であるとしています

  • ref_view
  • owning_view
  • as_rvalue_view
  • transform_view
  • drop_view
  • lazy_split_view
  • split_view
  • common_view
  • reverse_view
  • as_const_view
  • elements_view
  • enumerate_view
  • adjacent_view
  • adjacent_transform_view
  • chunk_view
  • slide_view
  • chunk_by_view
  • stride_view
  • cache_latest_view

そして、次のものに関しては濃度が不明であるか確実に有限であるため、変更は不要としています

  • filter_view (unknown cardinality)
  • take_view (finite)
  • take_while_view (unknown cardinality)
  • drop_while_view (unknown cardinality)

残ったもののうち

  • join_view/join_with_view
    • 入力範囲の外側の範囲がinfinite_rangeならばinfinite_rangeになるようにする
  • concat_view
    • 少なくとも一つの範囲がinfinite_rangeならばinfinite_rangeになるようにする
  • zip_view/zip_transform_view
    • 全ての範囲がinfinite_rangeならばinfinite_rangeになるようにする

とすることを提案しています。なお、言及されていないものは変更を提案していません。

P3557R0 High-Quality Sender Diagnostics with Constexpr Exceptions

std::executionにおける、エラーメッセージ出力を改善する提案。

P3164では、senderreceiverと接続されるよりも前、senderによる処理のチェーンが組まれた段階でsender同士の接続関係についての型チェックが可能なタイプのsender(非依存型のsender)についてより早期の型チェックとエラー報告を提案していました。

この提案はそれに続いてstd::executionにおけるエラー報告を改善しようとするもので、senderの完了シグネチャ(completion signatures、3つのチャネルが返す型)の計算における型エラーを定数式における例外を用いて報告するようにするものです。

senderは標準のsenderファクトリ/アダプタが返すもの以外にも、ユーザーが自由に定義することができ、ユーザー定義senderが標準の要求するsenderの基準に達していない場合、その使用時に様々なエラーが発生します。この提案によれば、そのようなエラーの中で最も長大かつ意味不明なエラーが発生しやすいのはsenderの完了シグネチャの計算時であるとのことで、そこでのエラーをC++26で可能になった定数式における例外送出を用いて直接的にユーザーに報告するようにしようとしています。

1. get_completion_signatures()をCPOからconsteval関数テンプレートへ変更

P2300では、完了シグネチャget_completion_signatures(sndr, env)の型(関数呼び出し式の型)によってのみ決定され、関数呼び出しそのものは重要ではなく式は評価されません。

定数式において例外によってエラー報告するために、この式を定数式において実行し、その内部で完了シグネチャ計算とエラー報告を行うように変更します。そのため、get_completion_signatures<Sndr, Env...>()の様な呼び出しに変更し、この呼び出し式を定数式で実行します。

これによるget_completion_signatures()の定義の変更は次のようになります

現在 この提案
inline constexpr struct get_completion_signatures_t {
  template <class Sndr, class... Env>
  auto operator()(Sndr&&, Env&&...) const -> see below;
} get_completion_signatures {};
template <class Sndr, class... Env>
consteval auto get_completion_signatures()
  -> valid-completion-signatures auto;
1.2. get_completion_signatures()カスタマイズ方法の変更

get_completion_signatures()はユーザー定義senderメンバ関数としてのカスタマイズが可能でしたが上記変更に伴ってカスタマイズ方法を変更します。現在thisであるsenderと環境envを受け取る非staticメンバ関数であるところを、それらの型をテンプレートパラメータで受け取るstaticメンバ関数に変更します。

現在 この提案
struct my_sender {
  template <class Self, class... Env>
    requires some-predicate<Self, Env...>
  auto get_completion_signatures(this Self&&, Env&&) {
    return completion_signatures</* … */>();
  }
  ...
};
struct my_sender {
  template <class Self, class... Env>
  static constexpr auto get_completion_signatures() {
    if constexpr (!some-predicate<Self, Env...>) {
      throw a-helpful-diagnostic(); // <--- LOOK!
    }
    return completion_signatures</* … */>();
  }
  ...
};

2. sender_inコンセプトにおいて、get_completion_signatures()が定数式であることをチェックする

上記の変更を反映するために、sender_inコンセプト(sender_in<sndr, env>は環境envにおいてsenderであるsndrが非同期操作を構成できることを表す)を調整します。

具体的には、get_completion_signatures<sndr, env>()が定数式で評価可能であるかどうかをチェックするようにします

現在 この提案
template<class Sndr, class... Env>
  concept sender_in =
    sender<Sndr> &&
    (sizeof...(Env) <= 1)
    (queryable<Env> &&...) &&
    requires (Sndr&& sndr, Env&&... env) {
      { get_completion_signatures(std::forward<Sndr>(sndr), std::forward<Env>(env)...) }
        -> valid-completion-signatures;
    };
template <auto>
concept is-constant = true; // exposition only

template<class Sndr, class... Env>
  concept sender_in =
    sender<Sndr> &&
    (sizeof...(Env) <= 1)
    (queryable<Env> &&...) &&
    is-constant<get_completion_signatures<Sndr, Env...>()>;

3. basic-senderクラステンプレートにおいて、get_completion_signatures()env引数無しで呼ばれた場合にエラーとする条件を指定する

basic-senderクラステンプレートは標準のsenderアルゴリズムの規定のために使用されているクラステンプレートです。上記の変更においてこのクラスのget_completion_signatures()が例外を送出する場合を指定するために、basic-sender::get_completion_signatures<Sndr>()がill-formedとなる条件を明示的に指定するようにします。

4. 依存型senderを定義するdependent_senderコンセプトを追加する

ユーザーがsenderを作成する場合、P3164がそうしているように非依存型senderに対して早期の型チェックを行う事を簡易にサポートするために、非依存型senderを識別するために、依存型senderを表すコンセプトdependent_senderを追加します。

namespace std::execution {
  template<class Sndr>
  consteval bool is-dependent-sender-helper() try {           // exposition only
    get_completion_signatures<Sndr>();
    return false;
  } catch (dependent-sender-error&) {
    return true;
  }

  template<class Sndr>
  concept dependent_sender =
    sender<Sndr> && bool_constant<is-dependent-sender-helper<Sndr>()>::value;
}

依存型senderenvがなければ完了シグネチャを求めることができないので、get_completion_signatures<Sndr>()は例外を送出するようになります。dependent-sender-errorは有効な依存型senderが単にenvが欠けているために例外を送出していることを表す専用の例外型であり(これによって、型エラーを起こしている非依存型sender特別できる)、get_completion_signatures<Sndr>()がこの例外を送出している場合はSndrが依存型senderであると識別することができます。

5. transform_completion_signaturesコンセプトを削除する

これはオプショナルだとされています。

transform_completion_signaturesは完了シグネチャの型変換を簡易化するためのユーティリティメタ関数ですが、この提案の変更に伴ってこれはstd::applyif constexpr+ラムダ式などで代替が容易になるため、不要になるとして削除を提案しています。

これらの変更(1~4まで)はメインの提案ですが、このほかにいくつかオプションの提案も提示されています。

P3558R0 Core Language Contracts By Default

未定義動作のContractsによるハンドリングを将来のC++のデフォルトにする提案。

この提案はP3100R1で提示された未定義動作を契約違反として捉えることで契約プログラミング機能の枠組みで扱うようにする方向性を推進し、さらにそれを将来のC++におけるデフォルトとすることを提案しています。

P3100については以前の記事を参照

このメリットとしては次のことが挙げられています

  • C++エコシステムはどんなプラットフォームでもそのまま安全に使用できる
    • 経験の浅い開発者や初学者が、複雑なコンパイラスイッチを学習することなく間違いが診断される
    • ユーザーの学習サイクルおよびソフトウェアのライフサイクルの早い段階で、様々な欠陥が明らかになる
    • このデフォルトの言語安全性は、必要に応じて無効化することもできる
  • パフォーマンスが重要なプログラムにおいても、それが求められない場所で簡単にチェックを有効化できる
    • パフォーマンスが重要なプログラムを保守する人は、それを達成するためのコンパイラオプションに精通しているため、このデフォルトを無効化することも容易に行える
    • 一方で、パフォーマンスが求められない場所(デバッグ時や単体テストなど)においては逆にその無効化を行わない様にすることで簡単にチェックを有効化でき、最終的に出荷される高性能なリリースビルドにおける品質を向上させられる
  • デフォルトでチェックを有効化されていれば、何もしなくてもバグを検出できる
    • バグがあるか分からない状況においては、追加のチェックを有効化するべき指示がない
    • デフォルトで有効化されていれば、その指示や判断無しにバグが検出される
    • 逆に、デフォルトで有効化されている場合、実行時パフォーマンスの低下という明確な証拠が示されることで無効化判断を行うことができる
  • コードの変更が必要ない
    • 違反ハンドラの呼び出しおよびその動作を除いて、P3100R1で提案されている実行時チェックを有効化するためにソースコードに何らの変更も必要ない
    • C++26/29で記述される新しいプログラムだけでなく、このチェックがデフォルト化された新しいコンパイラで既存のコードをコンパイルするだけで、チェックが既存のプログラムにも適用される
  • 違反ハンドラを活用できる
    • (実装がサポートしていれば)違反ハンドラの差し替えによってwell-definedなフォールバックを動作を指定できるため、ある実行時点でバグが検出されず修正されない場合でもリスクを軽減できる
    • ソースコードの変更を必要とせず、erroneous behaviorの恩恵を受けることができる
      • このEBは言語仕様において指定する必要がない

提案では懸念点についても考慮されています

  • 実行時検査のオーバーヘッド
    • コア言語の事前条件をチェックするオーバーヘッドは一様ではなく、条件によって異なる
    • コンパイラで行われた/進行中の様々な実験によって、そのようなチェックのほとんどは典型的なプログラムの有用性に影響を与えるほどのものではない
    • チェックを現在のassumeセマンティクスに切り替える必要性の程度については実装経験によってのみ明らかになる
  • 例外を送出しうる違反ハンドラ
    • 違反ハンドラが例外を送出できることによって、任意の契約注釈から例外送出される可能性が発生してしまい、コードを悪い状態にする可能性がある事が懸念されている
    • ただ、P3100R1のチェックで対処されない場合、その現在の状態は未定義動作であり、さらに悪い状態である
      • 多くの種類の未定義動作において実際に観測される動作範囲は実際には合理的に限定されており、未定義動作の結果として例外を送出することはまずありえない
        • そのため、例外を送出する状況は未定義動作よりも悪い状態ではない
    • 既存のコードが、nullptrデリファレンスからのコアダンプといったような未定義動作の特定の振る舞いに依存している可能性も指摘されており、このような動作を保証されたもの(あるいはそのように)のように扱うのは賢明ではないものの、ハイラムの法則からそのような動作の変更はある程度真剣に検討する必要がある
      • しかし一方で、この提案のデフォルト動作はまさにそのような動作を提供するものになる。例外を送出する違反ハンドラを選択するのは専門家ユーザーであり、そのようなユーザーこそがこのような違反ハンドラの柔軟性を最も必要としている
  • 例外処理のオーバーヘッド
    • 違反ハンドラからの例外送出のために、例外を処理する追加のコードが生成される事もオーバーヘッドとなりうる
    • これについては、コンパイラが違反ハンドラのnoexceptを要求するオプションを実装し、それによって関連するオーバーヘッドの無い翻訳単位を生成できるようにすることを強く推奨する
    • コンパイラとリンカは、この方法でビルドされた翻訳単位ではインストールされた違反ハンドラがnoexceptであることを強制する必要がある

これらの事を踏まえて、ここでは次の3つの事を提案しています

  1. プロファイル提案から、実行時チェックに関する機能を削除する
    • プロファイルが契約アサーションを導入したり、契約アサーションの評価セマンティクスを強制したりしないようにする
    • プロファイル機能は、特定の場所での言語機能の制限と未定義動作に繋がる操作の実行時検査、という二つのものを提供しようとしているが、その実行時検査の部分についてはこの提案およびP3100R1の方向性と衝突し、かつ領域が重複している
    • そのため、プロファイル提案の実行時検査に関する部分はこちらの提案に移管する
  2. P3100R1の標準化を推進する
    • P3100R1の完全な仕様策定を継続し、P2900R13の後にこれを標準化する
  3. enforceセマンティクスの推奨
    • P3100R1では、コア言語の事前条件について他の指定が無い場合、デフォルトの評価セマンティクスとしてenforceを推奨する

対象にしているものが今日の未定義動作であるので、実際のところP3100及びこの提案の内容についてはすぐにでも作業を始めることができます。ひとまず必要な作業としては、プロファイル提案のように重複や衝突がある提案においてこのことを考慮するようにすることです。

P3559R0 Trivial relocation: One trait or two?

P2786の提案するis_trivially_relocatable型特性の意味について反対する文書。

C++26に向けて、リロケーションという操作をサポートするための議論が進行しています。そのために、コア言語にてリロケーションという操作と型がリロケーション可能な場合の条件等を定義しようとしています。特に、トリビアルリロケーションというリロケーションの特別な場合の操作に重点が置かれています。

リロケーションとは、クラスオブジェクトのビット列を単にコピーすることによってオブジェクトの移動を行うことであり、それはムーブ+元オブジェクトの破棄と同等の操作として定義されます。トリビアルリロケーションはさらに、単なるmemcpyのみによってリロケーションをすることを言います。どちらの場合でも、元のオブジェクトにもうアクセスしないような場合(例えばstd::vectorのキャパシティ伸長時など)に使用することで、ムーブよりもさらに効率的なオブジェクト移動を行うことができます。

C++26に向けたリロケーションの本命の提案はP2786R11であり、ここでは型がリロケーション可能かどうかを検出する型特性として2つのものが提案されています。

  1. std::is_replaceable_v<T>: Tのムーブ代入は、ムーブ構築+ムーブ元オブジェクトの破棄、によって行える
  2. std::is_trivially_relocatable_v<T>: 上記操作はmemcpyと同等である

一方で、P2786よりも前に最初にリロケーションを提案していたP1144では、同じ名前の型特性std::is_trivially_relocatable_v<T>を提供しているものの、その意味はTのムーブ代入がmemcpyと同等であること、を表しています。すなわち、P1144のstd::is_trivially_relocatable_v<T>はP2786のそれと異なる意味を持っており、std::is_replaceable_v<T> && std::is_trivially_relocatable_v<T>の両方を満たして初めて同等の意味になります(P2786では、std::is_trivially_relocatable_v<T>ならばstd::is_replaceable_v<T>であるとは限らない)。

リロケーションの提案としてはP1144の方がかなり先行して公開(2018年)されていたため、リロケーションという最適化を独自に先んじて利用していた(当然未定義動作の危険性と隣り合わせですが)在野のライブラリはこのP1144のセマンティクスに基づく形で実装されている場合が多く、そこではライブラリ独自のis_trivially_relocatable_vを定義し、標準で利用可能になったらstd::is_trivially_relocatable_vに切り替えるような実装が既に行われています。

// 定義側
namespace lib {
  // Define a trait for property P,
  // by the name is_trivially_relocatable.
  template <class T>
  struct is_trivially_relocatable :
  #if __cpp_lib_trivially_relocatable // P1144
    std::is_trivially_relocatable<T> {};
  #else
    std::is_trivially_copyable<T> {};
  #endif
}

...

// 利用側
if constexpr (lib::is_trivially_relocatable<T>::value) {
  ~~~~ memcpy ~~~~
}

このような既存先行実装のis_trivially_relocatableのセマンティクスはP2786のものと異なっているため、このようなコードはP2786が今のまま採択された後ではその2つの型特性を用いて書き直す必要があります。

// 定義側
namespace lib {
  // Define a trait for property P,
  // by the name is_trivially_relocatable.
  template <class T>
  struct is_trivially_relocatable :
  #if __cplusplus >= 20XXYYL  // P2786
    // Unfortunately the STL's version is weaker than ours
    std::bool_constant<std::is_trivially_relocatable_v<T> && std::is_replaceable_v<T>> {};
  #else
    std::is_trivially_copyable<T> {};
  #endif
}

...

// 利用側
if constexpr (lib::is_trivially_relocatable<T>::value) {
  ~~~~ memcpy ~~~~
}

あるいは利用側で2つの型特性を満たすように書き換えてもいいですが、これを行わないと異なる意味において型のトリビアルリロケーション可能性を判定してしまう可能性があり、それは未定義動作となるため対応されない場合既存の実装はそのままバグを抱えることになります。

そして、P2786のis_trivially_relocatableのセマンティクスは既存のライブラリが仮定するP1144のものとは異なっているため、このような対応は必須の作業となります。

この提案は、これらの問題からトリビアルリロケーションの型特性について、P2786のものではなくP1144のものを採用する事を主張するものです。

このことが問題となる既存のライブラリのほとんどはP1144の提供するis_trivially_relocatableのセマンティクスに基づいているため、P1144の型特性を採用すればこれらの問題はほぼ回避されます。

また、既存のライブラリ実装を見ても、P2786のようなセマンティクスでis_trivially_relocatableを定義しているライブラリはわずかであり、is_replaceableに至っては実装例がありません。このことからも、P1144のis_trivially_relocatableのセマンティクスが一般に望まれているものだと言えます。

P3560R0 Error Handling in Reflection

静的リフレクション機能におけるエラーハンドリング方法として例外を採用すべきとする提案。

C++26を目指してP2996で提案中の静的リフレクション機能においては、ライブラリのconstevalメタ関数がエラーを報告する手段を決定する必要性が指摘されています。その方法として次の4つを提示していました

  1. 無効なリフレクション(std::meta::info)を返す
  2. 固有のエラー型Eに対して、std::expected<T, E>を返す
  3. 定数式ではなくする
  4. Eの例外を送出する

1は全てのメタ関数がstd::meta::infoを返すわけではない(std::vector<std::meta::info>string_viewを返すものがある)ため問題の解決にならず、2はハンドリングのためのコードの構文上の負担が大きいため最適ではなく、3はエラーから復帰するための方法が提供されない、といった難点があります。結局検討の末、定数式において例外を送出するのが最適であるとされました。

この決定がなされた当時(P2996R1)はまだ定数式で例外送出は許可されていなかったためP2996では3を採用していました。しかし、P3086R6がWDに採択されたことでそれが可能になったため、この提案はオプション4(例外)をリフレクションにおけるエラー報告メカニズムとして採用することを提案しています。

例外によってリフレクションのエラーを報告するようにする場合、メタ関数の戻り値型はそのままにしておくことができます。また、std::expected<T, E>のように案ラップにユーザーの手間をかける必要はなく、catchすることでハンドリングすることができます。

実行時においてはバイナリサイズ増大やパフォーマンス低下のデメリットから例外は敬遠されがちですが、コンパイル時に使用する場合は例外のデメリットはほぼ関係なくなり、その他の方法と比較してメリットが勝ります。

提案では、リフレクション専用の例外型として次のような型を提案しています

namespace std::meta {

  class exception {
  public:
      consteval exception(u8string_view what,
                          info from,
                          source_location where = source_location::current());

      // エラーに関する文字列
      consteval u8string_view what() const;

      // エラーが発生した関数/関数テンプレートのリフレクション
      consteval info from() const;

      // エラーが発生した関数の呼び出し位置
      consteval source_location where() const;
  };
}

コンパイル時にリフレクションのためのエラー報告として使用され、実行時に使用されないことから、この例外型はstd::exceptionの派生クラスではなく、その必要がありません。

エラー文字列としてu8string_viewを使用しているのは、ソースコードで使用されうる識別子をすべて表現するためにエンコーディングとしてUTF-8を使用し、なおかつそれを型システムで表現するためです。ただし、char8_t型のライブラリサポートが万全ではないため、これによってエラーメッセージ文字列の生成方法が難しくなるという欠点もあります。

しかし、std::string_viewを使用するようにする場合、識別子名の表現可能性によって別の問題が生じてしまいます。例えばstd::meta::identifier_of(x)の実装において例外を使用するようにし、std::meta::exceptionstring_viewしかサポートしていないとすると

consteval string_view identifier_of(info x) {
  // xは識別子を反映していない
  if( !has_identifier(x) ) {
    throw meta::exception(u8"entity has no identifier", ^^identifier_of, ...);
  }

  auto id = u8identifier_of(x);

  // 識別子がリテラルエンコーディングで表現可能ではない
  if( !is_representable(id) ) {
    throw meta::exception(u8"identifier '"s + id + u8"'is not representable", ^^identifier_of, ...);
  }

  // convert id to the literal encoding and return it
}

この時、2つ目のエラーケースにおいてエラーメッセージにエラーを起こした識別子を入れようとするのは自然ですが、そもそもそれができないからエラーを報告しようとしているため、これは結局不可能です。

標準ライブラリがchar8_tをサポートしていないという問題はサポートするようにすれば解決できるため、u8string_viewを採用するのが現在のP2996R8の設計と一貫していて最適だとしています。

提案文書より、クラス型のリフレクションのみを受け入れるようにする例

consteval auto user_fn(info type, source_location where = source_location::current()) {
  if( !is_class_type(type) ) {
    throw std::meta::exception(u8"not a class type", ^^user_fn, where);
  }

  // carry on
}

提案では、P2996R8にあるメタ関数でエラーを報告する必要があるものは基本的に例外を使用するように変更していますが、std::meta::define_aggregate()だけはエラーからの復帰が困難(そのために必要な保証を提供するコストに見合わない)であるとして、現状のままエラーは非定数式になるとしています。

P3561R0 Index based coproduct operations on variant, and library wording

std::variantに対してインデックスに基づくvisit()操作を追加する提案。

std::variantvisit()操作はvariantの候補型に同じ型が2つ以上あると上手く動作しません。visit()は関数オーバーロードとその解決によって静的に内包する値に応じた処理の呼び分けを行うため、異なる候補型に対しては同じオーバーロードがマッチングしてしまいます。

std::variant<int, int, int> v{std::in_place_index<0>, 10}; // ok

// 候補型毎の呼び分けができない(どのintの値も同じ関数を呼び出す)
std::visit(v, [](int i) { ... });  // ok

// これは曖昧になりエラー
std::visit(v, overloaded{
                [](int i) { ... },
                [](int j) { ... },
                [](int k) { ... }
              });  // ng

このように、variantの候補型に同じ型が含まれていてもその型毎に個別の処理を行いたいような場合、visit()を使用することはできず、一番良い方法はswitch文を使用することです。

std::variant<int, int, int> v{std::in_place_index<0>, 10}; // ok

switch (v.index()) {
  case 0: a = std::get<0>(v); break;
  case 1: a = std::get<1>(v); break;
  case 2: a = std::get<2>(v); break;
  default: throw std::bad_variant_access();
}

これはかなり煩雑なコードですが、現状これよりもきれいな形で記述することができません。

この提案では、それに対してインデックスベースのケースマッチングを行う操作を提案しています。この操作においては、呼び出す対象の型のインデックス(variantテンプレートパラメータの先頭からのインデックス)とその型のための処理を組にしたものを用いてvisit()することによって、variantが2つ以上の同じ型を候補型として持っていたとしても呼び分けることを可能にします。

std::variant<int, int, int> v{std::in_place_index<0>, 10}; // ok

// variantの候補型毎に呼び出す処理をその順番で指定して、それによるオーバーロード関数オブジェクトを返す
auto compute = invoke_cases(
  [](int i) -> int { return i; },
  [](std::string const &s) -> int { return s.length(); },
  [](int j) -> int { return j + 100; }
);

std::println("result: {}", compute(v));

invoke_cases()はここで提案されている操作の一つで、あるvariantに対してその値に応じて適用したい処理を渡して、variantオブジェクトを受けてそれらの呼び出しを適切にディスパッチする関数オブジェクトを返します。この時、後から渡されるvariantオブジェクトの実行時の状態に応じて呼び出す関数を決定するのはインデックスベースであり、.index()に対してinvoke_cases()に渡した関数の先頭からのインデックスと(両方0-indexed)一致する関数を選択して、その値を渡す形で呼び出します。

variant<T0, T1, ..., Tn>に対してinvoke_cases(f0, f1, ..., fn)fiTiの値で呼び出し可能である必要があります。invoke_cases()では渡された関数の引数順でのインデックスを暗黙的に対象variantでのテンプレートパラメータ順の指定と見做すことで、処理とインデックスの紐づけを自動化しています。

この提案では、このような操作を微妙な性質の違いから次の6種類提案しています

  • visit_invoke()
    • 対象のvariantオブジェクトと、その各候補型に対して呼び出す処理をまとめたtupleを受け取り、variantのアクティブなインデックスに対応する値と処理(tupleの先頭からのインデックスでマッチング)を呼び出す
  • visit_apply()
    • 対象のvariantオブジェクトと、その各候補型に対して呼び出す処理をまとめたtupleを受け取り、variantのアクティブなインデックスに対応する値と処理(tupleの先頭からのインデックスでマッチング)を呼び出す
    • この時、呼び出しにはstd::apply()を使用する
      • variantの候補型はtuple-likeでなければならない
  • visit_apply_cases()
    • 対象のvariantオブジェクトと、その各候補型に対して呼び出す処理をその候補型の数受け取り、受け取った処理をtupleにまとめてvisit_apply()を呼び出す
  • apply_cases()
    • 対象のvariantオブジェクトの各候補型に対して呼び出す処理をその候補型の数受け取り、variantオブジェクトを引数に取り受け取った処理によってvisit_apply()を呼び出す呼び出し可能ラッパオブジェクトを返す
  • visit_invoke_cases()
    • 対象のvariantオブジェクトと、その各候補型に対して呼び出す処理をその候補型の数受け取り、受け取った処理をtupleにまとめてvisit_invoke()を呼び出す
  • invoke_cases()
    • 対象のvariantオブジェクトの各候補型に対して呼び出す処理をその候補型の数受け取り、variantオブジェクトを引数に取り受け取った処理によってvisit_invoke()を呼び出す呼び出し可能ラッパオブジェクトを返す

これらの関数の命名は、渡す関数が単一の引数を取るものである場合にinvoke、複数の引数を取る場合はapplyを使用しており、可変長引数によって処理を受けるラッパである場合はcasesを使用しています。visitは呼び出し対象のvariantオブジェクトを直接取るものに対して使用されます(visitではないものは関数オブジェクトを返す)。

関数 処理の引数の数 variantを受け取る 処理の渡し方 結果
visit_invoke() 1 yes tuple 呼び出し結果
visit_apply() 複数 yes tuple 呼び出し結果
visit_apply_cases() 複数 yes 可変長引数 呼び出し結果
apply_cases() 複数 no 可変長引数 関数オブジェクト
visit_invoke_cases() 1 yes 可変長引数 呼び出し結果
invoke_cases() 1 no 可変長引数 関数オブジェクト

少し順番は前後しますが、サンプルコード

std::variant<int, int, int> v{std::in_place_index<0>, 10}; // ok

// 対象variantオブジェクトと、候補型毎の処理をその順番で渡してvisit
auto result = visit_invoke_cases(
  v,
  [](int i) -> int { return i; },
  [](std::string const &s) -> int { return s.length(); },
  [](int j) -> int { return j + 100; }
);

std::println("result: {}", result);
std::variant<int, int, int> v{std::in_place_index<0>, 10}; // ok

// 対象variantオブジェクトと、候補型毎の処理をその順番でtupleに詰めて渡してvisit
auto result = visit_invoke(
  v,
  std::make_tuple(
    [](int i) -> int { return i; },
    [](std::string const &s) -> int { return s.length(); },
    [](int j) -> int { return j + 100; }
  )
);

std::println("result: {}", result);
using message = std::tuple<int, int>;
using message2 = std::pair<int, std::string>;

std::variant<message, message2> args{
  std::in_place_index<0>, std::make_tuple(3, 4)
};

std::variant<message, message2> args2{
  std::in_place_index<1>, std::make_pair(3, "teststring")
};

// variantの候補型毎に呼び出す処理をその順番で指定して、それによるオーバーロード関数オブジェクトを返す
// 候補型はtuple-likeであり、処理はその要素を全て順序通りに受け取れる必要がある
auto analyze = apply_cases(
  [](int x, int y) -> int { return x + y; },
  [](int x, std::string const &y) -> int { return x + y.length(); }
);

int result = analyze(args);
int result2 = analyze(args2);
using message = std::tuple<int, int>;
using message2 = std::pair<int, std::string>;

std::variant<message, message2> args{
  std::in_place_index<0>, std::make_tuple(3, 4)
};

std::variant<message, message2> args2{
  std::in_place_index<1>, std::make_pair(3, "teststring")
};

// 対象variantオブジェクトと、候補型毎の処理をその順番で渡してvisit
// 候補型はtuple-likeであり、処理はその要素を全て順序通りに受け取れる必要がある
int result = visit_apply_cases(
  args,
  [](int x, int y) -> int { return x + y; },
  [](int x, std::string const &y) -> int { return x + y.length(); }
);
using message = std::tuple<int, int>;
using message2 = std::pair<int, std::string>;

std::variant<message, message2> args{
  std::in_place_index<0>, std::make_tuple(3, 4)
};

std::variant<message, message2> args2{
  std::in_place_index<1>, std::make_pair(3, "teststring")
};

auto tup = std::make_tuple(
  [](int x, int y)-> int { return x + y; },
  [](int x, std::string const &y)-> int { return x + y.length(); }
);

// 対象variantオブジェクトと、候補型毎の処理をその順番でtupleに詰めて渡してvisit
// 候補型はtuple-likeであり、処理はその要素を全て順序通りに受け取れる必要がある
int result4 = visit_apply(args, tup);
int result5 = visit_apply(args2, tup);

このような操作は圏論における余積(coproduct)から来ているものの様で、それについての数学的な対応付けの説明が提案ではなされています。

P3564R0 Make the concurrent forward progress guarantee usable in bulk

std::execution::bulkアルゴリズムが保証する並行進行保証を改善する提案。

std::execution::bulkは、senderによって表現される処理を指定されたshapeの下でバルク実行するものです。ただし、現在(P2300R10)の規定ではstd::execution::bulkによる処理は一つの実行エージェント上で逐次実行されるため、使用されるschedulerが並行進行保証としてconcurrentstd::execution::get_forward_progress_guaranteeクエリの結果で最も強い保証)を示したとしてもbulk実行時に処理の並行実行を仮定できません。

たとえば、個々の処理がブロッキングによる同期を行う場合、一つの実行エージェント上で実行される2つの処理はデッドロックなく実行できません。

この問題については、以前のP3481も参照すると理解が深まるかもしれません

この提案は、std::execution::bulkにおいて実行される各処理が異なる実行エージェント上で処理されることを規定するようにすることで、この問題を解消しようとするものです。

例えば、シングルスレッド実行コンテキスト上のschedulerは、bulkをシーケンシャルなforループによって実行することになります。この場合、実行エージェントとは1ループであり、各ループを別の実行エージェントとして扱うことで、このようなbulk実装は最大でparallel前進保証を提供することができます(parallel保証の場合、いずれかの実行エージェントがブロックするとデッドロックする可能性があります)。

この提案の言う実行エージェントとはスレッドよりもより汎用的な概念であり、スレッドのように他の実行エージェントと並行して作業できるエンティティ、と定義されており、必ずしもハードウェアやOSの提供する機能にマッピングされるものではありません。実行エージェントは、並行処理における処理の実行単位を抽象的に指定するためのものです。

そして、schedulerに対するstd::execution::get_forward_progress_guaranteeクエリで得られる前進保証(forward progress guarantee)は、そのschedulerが作成した個別の実行エージェントの処理の前進を指定するものです。

std::execution::bulkにおいて実行される各処理が異なる実行エージェント上で処理される」とは、「使用されるschedulerの持つ前進保証(forward progress guarantee)が個々の処理の呼び出しに適用される」という意味です。現在のbulkは実質的に逐次実行を指定しているためこれを満たしておらず、またこれによってschedulerconcurrentな前進保証を提供していたとしてもparallelまでの保証しか提供することができません。

bulkをカスタマイズする場合でも現在の仕様では使用される実行エージェントの数が明確に規定されていないため、N個の処理にN未満の実行エージェントが用意される場合(タイリングが行われる)、やはりparallelまでの保証しか提供することができません。

bulkが可能な最も強い前進保証(schedulerの保証するもの、最大でconcurrent)を持つべき理由としては

  • ユーザーの意図表現の自由
    • ユーザーは、bulk内の各処理間でブロッキング同期を必要とするアルゴリズムを特定のschedulerに依存せずに記述したいと考えているが、現在のところその方法がない
  • アルゴリズムの複雑さの軽減
    • bulkの全処理の一括同期をサポートすることで、ハードウェアの提供する実行エージェントの一括起動サポートを受けられるようになる
      • このコストは起動する数に依存しない
    • サポートできない場合、ユーザーはwhen_all再帰的に呼び出すなどによってN個のエージェントを起動するためにlog N回の起動を行うことになる
  • SG1からの要請
    • SG1では全会一致で、イテレーションごとに実行エージェントを起動するbulkのバージョンの必要性に合意している
  • 独自の機能としてのbulk
    • N個の実行エージェント上で実行されるバルク処理は、ループの並列化とは異なる他に類を見ない機能である
  • 並列プログラミングモデルとの整合性
    • すでに普及している並行プログラミングモデル(OpenMP、MPI、HPXなど)を提供するソリューションでは、ユーザーがconcurrent前進保証を要求する場合のためのAPIが個別に用意されている事が多い
  • 基本操作としてのbulk
    • bulkは基本操作であり、非同期並行アルゴリズムを構成するために使用できるscheduler上の最小の操作セットの1つ

等が挙げられています。

このためにこの提案では次の3つのことを提案しています

  1. bulkの各関数呼び出し(各処理)が個別の実行エージェント上で実行されるように指定する
    • bulkfor_eachの単なる別記法ではなく、並行アルゴリズムを実装するために実行エージェントを作成する方法であり、bulkはあるscheduler上の他のいかなる処理とも異なる前進保証を持つべきではない
      • 各処理はschedulerの保証と同じ前進保証を持つべき
      • 最大でconcurrent保証を提供可能であるべき
  2. デフォルトのbulkconcurrentな前進保証を提供するscheduler上で呼び出すことはill-formed
    • デフォルトのbulkでは、N = 1の場合にしかconcurrentな前進保証を提供できないため
  3. 指定されたエージェント数に対して、カスタマイズbulkが前進保証を満たせない場合、実行時に失敗することを許可する
    • これにより、bulkは任意のNに対して提供可能な最も強力な前進保証の提供を約束し続けられる
    • この失敗は、scheduler固有のエラー完了を通して伝達される
      • エラー型は実装定義、例外でなくてもよい

提案にはbulkについての長い歴史的な議論へのリンクや、bulkによるループ並列化(並行for_each)の実装例などが記載されています。

この提案は、LEWGの投票において方向性に合意が取れ、P3481R2にマージされてC++26導入を目指しています。

P3565R0 Virtual floating-point values

C++における浮動小数点数演算のセマンティクスについての提案。

現在のC++の規定では、浮動小数点数演算の結果についてほぼ何も保証していません。演算の精度は実装定義とされており、さらには浮動小数点数オペランドや演算結果がその型の値ではない場合もあるという規定もあります。このような規定は実際に、浮動小数点数演算を含む式を数式として解釈して最適化を行うことで、計算性能や精度を向上させることの根拠となってもいます。

一方で、well-definedな浮動小数点数演算を厳密に(コードの通りに)適用することは、一部の数値アルゴリズムにとって極めて重要な場合があります。

現在のC++のこのような規定はCから受け継いだものであり、CがFLT_EVAL_METHOD等のマクロによってこの自由度から実装が何を選択しているのかを表明する方法を提供しているように、C++においてもC++特有の事情を考慮したうえでこの自由度をより一貫して解釈する方法についてを模索する試みが始まっています。しかし、それはまだ成果を伴うものではありません。

この提案は、そのようなセマンティクスの一案を提示するものです。ここで提案されているのは次の事です

  • トリビアルコピー可能な型の定義の変更
    • 浮動小数点数型はある一つの値表現に対して複数の値を持つことを許可する
    • そのような値集合のうち一つは標準的な値(canonical)である
  • 浮動小数点数型がある値表現を取得すると、そのオブジェクトは対応する標準的な値を持つ
  • より高い精度と範囲の規定([expr.pre]/6)を、丸め処理の文脈依存性に関する注記に置き換える
  • 単項+演算子static_castは、浮動小数点数値を対応する標準的な値に丸める

これは、浮動小数点数型の値の集合を、その型がメモリ上で占有する領域に格納可能な値の範囲を超えて拡張するというアプローチを採用した場合の仕様の提案です。このセマンティクスによって、浮動小数点数値は浮動小数点数型の値表現に対して多対一で対応関係を持つものの、あらゆる浮動小数点数演算および浮動小数点数型オブジェクトは依然として単一の値を持つ、事になります。

これにより、a * b + cという式(全てdouble値とする)の部分式の結果はdoubleの表現可能な値の範囲外の値を取ることができ、この式の結果の値の選択はFMA命令によって実装されている丸めによって行われます。

このようなセマンティクスによって、FMAのような浮動小数点数演算を数式として解釈する最適化は依然として許可されるものの、その結果は実装定義なものではなくなり、最適化の境界を越えて伝播する値はその型によって制限され、標準的な値という単一の値を取るようになります。この提案では仮想的な値を取っている浮動小数点数値を明示的に標準的な値に丸めるための方法としてstatic_cast(と単項+)を採用しています。

単なる代入操作は標準的な値への丸めを実行するポイントではなく、これは浮動小数点数型のラッパ型においても同様のセマンティクスを適用するためです。

P3566R0 You shall not pass char* - Safety concerns working with unbounded null-terminated strings

その長さが静的に既知ではない文字列(const char*)を受け取るstd::string/std::string_viewのコンストラクタを置き換える提案。

std::string/std::string_viewはどちらにも、const char*を受け取るコンストラクタが存在します。これは、実行時に受け取った文字列ポインタから文字列を取得する関数ですが、その終端は渡されるまで未知のため構築時に\0を探して終端を決定します。しかしこの動作は、間違ったポインタが渡された場合などに無制限にメモリ空間を探索してしまう(当然UB)という問題があります。

auto f(const char* ntbs) {
  std::string str{ntbs};        // ntbsがnull終端文字列でない場合UB
  std::string_view strv{ntbs};  // 同上
}

この提案は、C++の安全性向上の一環として、そのようなコンストラクタをより安全な動作をするものと明示的なもので置き換えようとする提案です。

提案では、std::stringstd::string_viewconst char*を取る現在のコンストラクタを非推奨にしたうえで、const char(&)[N](配列参照)を取るコンストラクタを追加し、const char*から構築するコンストラクタをstd::unsafe_length_tタグを明示的に取るコンストラクタとして追加します。

string_viewで単純に例示すると次のようになります

// 現在のコンストラクタ
constexpr string_view(const char *p) noexcept
  : _data(p), _size(Traits::length(p)) {...}
// この提案による変更
[[deprecated]]
constexpr string_view(const char *p) noexcept
  : _data(p), _size(Traits::length(p)) {...} 

template<size_t N> 
string_view(const char (&p)[N]) noexcept 
  : _data(p), _size(Traits::length_s(p, N)) noexcept {...} 

explicit constexpr string_view(unsafe_length_t, const char *p) noexcept
  : _data(p), _size(Traits::length(p)) {...} 

同様に、引数にconst char*を渡させるインターフェースのうちで安全ではないもの(無制限の探索を行いうるもの)を非推奨とするとともにコンストラクタと同様の引数を取る新しいオーバーロードを追加します。

  • 両方
    • find_first_of, find_last_of, find_first_not_of, find_last_not_of
  • std::stringのみ

そして、std::char_traitslength_sを追加して最大文字数を指定しながら文字列長の計算(文字列終端の探索)を行えるようにします

template<size_t N> 
size_t length_s(const char_type (&s)[N]) {...} 

size_t length_s(const char_type* s, size_t N) {...} 

これはstrnlen_s()と同様に動作し、長さN内でnull文字が見つかる場合はその前の文字数を返し、見つからない場合はNを返します。

ここではstd::stringstd::string_viewで例示しましたが、これらの変更はstd::basic_stringおよびstd::basic_string_viewに対して行われます。

auto example1(const char* ntbs) {
  std::string str1{ntbs};        // コンストラクタは非推奨
  std::string_view strv1{ntbs};  // コンストラクタは非推奨

  std::string str2{std::unsafe_length, ntbs};        // ok
  std::string_view strv2{std::unsafe_length, ntbs};  // ok
}

template<std::size_t N>
auto example1(const char(&ntbs)[N]) {
  std::string str{ntbs};        // ok、最大でもN文字の範囲内で終端チェックを行う
  std::string_view strv{ntbs};  // ok、最大でもN文字の範囲内で終端チェックを行う
}

P3567R0 flat_meow Fixes

flat_xxxな連想コンテナに対するバグフィックス提案。

この提案はlibc++での実装経験をベースとしたstd::flat_mapをはじめとするflat_xxxなコンテナに対する修正提案です。

同様の提案は以前にP2762R2でも提出されていましたが、そちらはより影響の大きい設計変更を含んでいたため合意に至ることができませんでした。libc++におけるstd::flat_map等連想コンテナのリリースが完了したことに伴って、改めてその経験から得られたより純粋な問題点についてここでは報告されています。

修正は次のものです

  • flat_map::insert_range()で挿入する範囲の要素型がpairであることを明確にする
    • 効果を指定する式において要素型をautoで受けており、これだとtuple等も使用可能だったものの、明らかにstd::pairだけを対象にしていた
    • 要素型を明確にvalue_typestd::pair<Key, Value>)で取るとともに、他のコンテナとの一貫性向上のためにranges::for_eachを使用するように修正する
  • swapの条件付きnoexcept指定
    • flatなコンテナのswapはすべて無条件noexcept指定されているが、使用するコンテナのswapが例外を送出した場合に不変条件を保護しながら取れることは次の2つしかなかった
      • 例外を握り潰し、不変条件を復元する(内部コンテナをクリアする)
      • std::terminate()
    • 不変条件を復元しながら例外を伝播させることを許可するために、swap()を条件付きnoexceptにする
  • insert_range(sorted_unique, rg)の欠如
    • flatなコンテナはいずれもinsert_range()によって任意の範囲を挿入できるが、複数の要素を一度に挿入できるその他の方法(イテレータペア/初期化子リスト)ではサポートされているソート済み範囲からの挿入がサポートされていない
    • flat_map/flat_multimapinsert_range()にはsorted_uniqueを取るオーバーロードを追加し、flat_set/flat_multisetinsert_range()にはsorted_equivalentを取るものを追加する
  • flat_set::insert_range()におけるコピーの回避
    • flat_set::insert_range()では入力範囲の要素型がKey型(に変換可能な型)であればいいため、可能ならムーブすることができるが、現在の規定はそれを妨げている
    • 効果を指定する式においてauto&&で受けて完全転送するように指定する
  • 特殊メンバ関数が規定されていない
    • flatなコンテナはいずれも特殊メンバ関数(コピー/ムーブ コンストラクタ/代入演算子)が明示的に指定されていない
      • = default定義してしまうと、ムーブやコピー時に例外が送出された場合に不変条件が保護されなくなる
    • 特殊メンバ関数の宣言とその動作を明確に指定することで、その実行中に例外が送出された場合でも不変条件が保たれる事を規定する

なお、flatな連想コンテナの不変条件とは、内部コンテナがソート済みであることと、mapの場合は2つのコンテナの要素数が一致し、なおかつKeyのコンテナとValueのコンテナで同じインデックス位置にあるものはKey-Valueペアの対応付けされた要素であること、です。

P3568R0 break label; and continue label;

名前付きループにより、ネストしたループからの脱出の簡易な構文をサポートする提案。

これはほとんど、C23で採用された名前付きループの機能をC++に持ってきたものです。label: forlabel: switchのように制御文にラベルによって名前を付けて、その文の内側でbreak label;continue label;のように制御文の名前を指定してbreak/continueする機能です。

outer: for (auto x : xs) {
  for (auto y : ys) {
    if (/* ... */) {
      continue outer; // OK, continueはouterのループに対して適用される(xについてのループがcontinueする
      break outer;    // OK, breakはouterのループに対して適用され、2重ループを終了させる
    }
  }
}

switch_label: switch (/* ... */) {
  default: while (true) {
    if (/* ...*/) {
      break switch_label; // OK, breakはswitchに対して適用され、switch文を抜ける
    }
  }
}

break outer;       // error: ループの外からbreakできない
goto outer;        // OK, 以前から可能だったので影響を与えないようにする

このようなネストしたループから簡単に脱出する方法は強く需要があったものの言語機能としてはサポートされておらず、既存の代替案はどれも問題がありました。C++に対しても提案されていたものの採用に至ることは無く、そうしてる間に他言語(Java/Javascript/Rust/Kotlinなど)でこのようなラベル付き文がネストした文の制御のためのポピュラーな方法となり、C23に向けても採択されました(N3355)。C23での採用によって、元々合った要望や他言語での例に加えて、Cとの互換性を取るというモチベーションも生まれています。

さらに、C++におけるconstexprの多機能化によってネストループを定数式で記述する機会が増加しており、定数式ではgotoによる簡易な代替機能が利用できないことによって、このような機能の要求が増大していました。

これらの流れを受けて、この提案はC23に向けて採用されたN3355をベースとした名前付きループの機能をC++にも導入しようとするものです。

基本的な機能はC23に採用されたものを踏襲しているのですが、1点だけ調整されている点があります。それは、ラベルの再利用が可能とされている点です。

outer: while (true) {
  inner: while (true) {
    break outer;  // outerループをbreakする
  }
}

outer: while (true) { // OK, ラベルの再利用が許可される
  inner: while (true) {
    break outer; // outerループをbreakする(1つ目のouterではなく2つ目のouterをbreakする
  }
}

goto outer; // error: ジャンプ先が曖昧となるためエラー

gotoは名前付き文のラベルに対してもジャンプできますが(これは後方互換性のため)、ラベルが再利用されている場合はill-formedとなります。

また定数式の対応については、この新しい名前付きループとbreak/continueをそのまま(定数式で禁止するという条項を付けることなく)導入することによってgotoを許可せずとも自然に定数式で使用できるようになります。

P3569R0 Split define_aggregate from Reflection

define_aggregate()をP2996のリフレクション提案から外す提案。

define_aggregate()はその名の通り集成体型を定義するためのリフレクションメタ関数です。例えば次のように使用できます

// タプル型を実装する例
template<typename... Ts>
struct Tuple {
  struct storage;
  consteval {
    define_aggregate(^^storage, {data_member_spec(^^Ts)...})
  }
  storage data;

  Tuple(): data{} {}
  Tuple(Ts const& ...vs): data{ vs... } {}
};

1つ目に不完全クラス型(もしくは共用体型)を受け取り、2つ目の引数で受け取ったデータメンバのリフレクションの列を受け取って、そのクラス型に対して指定されたデータメンバを集成体型として実装する形の定義のリフレクションを返す関数です。

このdefine_aggregate()は非常に便利な機能ではあるもののその設計に関してはこれまで二転三転してきており、P2996の他の部分の安定性と比較すると明確に不安定かつリスクのある機能になっています。

その理由の1つはcomplete-class context(完全クラスコンテキスト)という概念についてのものです。完全クラスコンテキストの概念はP2996R9を見ても明確ではないですが、どうやら定数評価中にdefine_aggregate()で定義することで完全型となったクラス型について、その完全性が観測可能になる場所とタイミングについてを規定しようとしているもののようです。

もう一つは、到達可能性(reachability)の新しい形式を導入しようとしていることです。到達可能性はモジュールの採択時に導入されたあるコンテキストからの宣言の利用可能性についての規格用語ですが、define_aggregate()によって生成された定義がいつどこで到達可能になるかを規定するために到達可能性の定義を拡張しようとしています。

どちらの問題も設計や問題解消方法がまだ固まっておらず、この提案の提出時点でC++26のタイムフレーム内で解決できるか見通しが立っていない状況です。

それを受けてこの提案では、C++26にリフレクション機能を確実に間に合わせるために、P2996からこのdefine_aggregate()を取り除くことを提案しています。

リフレクション機能のほとんどの部分はコンパイラが内部に持っているASTの状態をクエリする事で成り立っており、これは現在の型特性機能が行っていることでもあるため実装はそれを認識し、専用のインターフェースを用意しています。一方でdefine_aggregate()は定数評価中にASTを書き換えることができる機能であるため、実装にとっても標準にとっても新たな試みであり、多くの課題があります。

このようなコード生成に関する機能は最終的にはリフレクション機能の一部として必要であるものの、現時点の他の部分のリフレクション機能(クエリ関連機能)だけでも十分に有用性があり、なおかつCWG内でもほとんど異論がありません。

このため、define_aggregate()を切り離すことでリフレクション機能全体がC++26に間に合わなくなるリスクを大きく低減することができるとともに、define_aggregate()の実装と規定に関しての探求作業を集中的に行うことができるようになります。

どうやら、この提案はEWGにてコンセンサスを得られなかったようです。

P3572R0 Pattern matching

C++26にむけてパターンマッチング機能の選択を促す提案。

現在C++のパターンマッチング提案はP1371(match式)とP2392(is/as)の2つが並立しています。これら2つは設計が異なっているため標準に採択するにはどちらかを選ぶ必要がありますが、今のところどちらかを選択することに合意が取れていません。

この提案は、C++のパターンマッチングとしてP2392(is/as)の設計を支持し、これを簡素化したバージョンをC++26のパターンマッチングとして採択すべきとするものです。

この理由としては次のようなものを挙げています

  • is/asフレームワークであり、将来的に新しい構文を追加せずに新しい選択形式を追加できる
  • 簡単に使用できる
  • 効率的に実装可能であり、既に実装がある
  • シンプルかつ汎用的な構文を持つ
  • match式が多様な新しい専用の記法を多数導入しようとしているのに対して、is/asではデリファレンス*が必要なのみ
  • 各パターンはis/asというプレフィックスパターンで始まるため、可読性とレイアウト性が向上する

なお、結局この提案を受けても選択はできなかったようで、パターンマッチング機能はC++29以降になります。

P3573R0 Contract concerns

P2900のContracts機能に対する懸念を表明する文書。

これは、P2900R13で提案中のContracts機能に対して懸念を持つ人々が集まり、その懸念点を列挙して報告する文書です。

P3573R0に列挙されているP2900のContractsに対する懸念点は以下の通りです。

  • const
    • 変更を防ぐのための方法としてconst化は役に立たない
    • これは実装者、ユーザー、教育者にとって大きなコストを伴う別の不規則性
    • 警告によってほとんどの問題は対処可能
  • 例外の補足
    • 契約述語からの例外送出は契約違反ではなく、契約が開始されなかったことを意味する
    • 例外を送出する述語をContracts内の特別なケースとして処理されるべきではない
  • 過度に複雑な階層型契約モデル
    • このモデルは未検証であり、全員に強制すべきではない
    • 仮想関数での入り口と出口で4つ以上の契約が評価されるモデルは、問題の一部の解決策に過ぎず、不完全であり、正しいと見做されていない
    • 契約の継承を必要とする設計は数多く存在する
  • 全体的な設計の不安定さ
    • 2024年にP2900に対して、10以上の変更提案、1000以上のリフレクターメッセージ、数十回の変更が加えられており、議論を追うのが困難
    • 実際に何が提案されているか知っているのは少数の人々だけであり、これでは国際標準の確固たる基盤とはいえない
  • 実装定義が多すぎる
    • 平均的なユーザーから専門家まで、何がチェックされ、失敗の結果どうなるかを理解するのは困難
    • 実装定義とされている項目が多岐にわたるため、ポータブルな契約を書くのは難しく、初心者に対して契約が何をするかを説明・教育することは実質的に不可能
      • assert()であれば、初心者に約1分で説明できる
  • 契約のグループ化機能が提供されていない
    • 契約の有効化/無効化はグローバルでONかOFFかのみであり、契約を個別に有効化・無効化できるグループに分類する方法がない
  • デフォルト設定
    • P2900は実装定義なセマンティクス等について、デフォルトの最適な設定があるとしているが、それは具体的に示されていない
  • 未検証
    • 実装経験はあるものの、以下の点が未検証
      • 事前条件と事後条件は大規模に検証されていない
        • 特に、提案されている仮想関数メカニズムを使用した事前条件と事後条件はほぼ完全に未検証
    • 契約違反を例外にマッピングする使用経験がない
    • 様々なセマンティクスでコンパイルされたコードをリンクする機能の仕様が不十分で未検証
  • 実行時チェックと静的チェック
    • 実行時チェックと静的解析における契約述語の使用との関係性は十分に検討されていない
    • 例えば、実行できない述語をどのように指定するのか?
  • 関数ポインタ
    • C言語スタイルのプログラミングでは重要な機能であるため、省略すべきではない
  • 安全性
    • 契約が安全性とどのように相互作用するかが不明
    • 安全性にはコードベース全体またはその一部に対する保証を伴う必要がある
      • 例えば、SG23が推進するプロファイル機能では一般的な保証が提供されている
    • 契約は主に、使用されている箇所での正確性のチェックを提供し、どちらも違反ハンドラーを必要とし、その違反ハンドリングが同じであることが理想的
    • これらの問題が解決される前に、安全性に影響を与える重要な設計基準を固定すべきではない
      • これは上の「デフォルト設定」に関連している
  • 未定義動作
    • 未定義動作は契約設計で直接対処されるべきか、それとも他の提案で一般的に対処されるべきか?
  • 翻訳単位の構成
    • 異なる契約設定を持つ翻訳単位をリンクした場合の影響が明確に規定されていない
    • 特に、あるテンプレートが異なる契約設定を持つ2つの翻訳単位でインスタンス化された場合、それらは異なる設定になるのか?リンカーがそれを防ぐべきなのか?そうでない場合取得される設定は何によって決定されるのか?
    • インライン関数、constexpr関数、consteval関数、およびコンセプトについても同様の疑問がある
  • 契約とモジュール
    • 翻訳単位と同様の疑問がある
    • 契約設計はモジュールでテストされているのか?
  • 契約と静的リフレクション
    • 翻訳単位と同様の疑問がある
  • 将来の改善の互換性
    • これらの問題が現在の設計の枠組み内で将来対処できると言えれば良いが(これはMVPの基本的な目標だが)、いくつかの問題の解決には互換性のない変更が必要となるように思える
  • 提案が大きすぎる
    • 契約は(他言語のように)シンプルな機能であるべきだが、仕様は巨大で、実装は困難であり、一部の機能は学習曲線が急峻になっている
  • スケーラビリティ
    • コンパイラとリンカがバイナリに多くの情報を取り込む必要があるため、提案はスケーラブルではない可能性がある
    • これは複数のツールチェーンの変更を意味する

そのうえで、これらの問題に対処する必要があり、なおかつその解決策はSG21の常任のメンバーよりもはるかに多くの人に理解されていなければならず、標準化の前にしばらく安定していなければならないものの、C++26サイクル中でそれが実現できるとは思えないため、国際標準の提案としては不完全で未検証である、と主張しています。

ただし、ここではこれら懸念点を列挙するのみで、具体的な解決策やそれについての提案が行われているわけではありません。合意に達することのできる単純な解決策の設計は見当たらないとしています。

P3574R0 Constexpr Callables

std::copyable_functionstd::move_only_functionconstexpr対応する提案。

std::copyable_functionC++26で、std::move_only_functionC++23で追加されたクラスです。これらは、std::functionの問題点を改善した新しい型消去呼び出し可能ラッパクラスです。これは当然、まだconstexpr対応されておらず定数式で使用できません。

同じくC++26では、定数式におけるvoid*への/からのキャストが可能になっているため、これらのクラスをconstexpr対応させる障壁は無くなっています。

通常のプログラミングで有用なものは定数式でも有用であるため、この2つのクラスをconstexpr対応させようとする提案です。

P3575R0 SG14: Low Latency/Games/Embedded/Financial Trading virtual Meeting Minutes 2024/11/13

2024年11月13日に行われた、SG14のオンラインミーティングの議事録。

どのようなことを議論したのかが簡単に記載されています。

P3576R0 SG19: Machine Learning virtual Meeting Minutes to 2024/11/14-2024/12/12

2024年11月14日と12月12日に行われた、SG19のオンラインミーティングの議事録。

どのようなことを議論したのかが簡単に記載されています。

P3577R0 Require a non-throwing default contract-violation handler

デフォルトの契約違反ハンドラが例外を送出しないようにする提案。

P2900R13で提案中のContracts機能では、契約違反が起きた時に違反ハンドラが呼ばれそれを処理します。違反ハンドラはデフォルトのものがまず用意されますが、ユーザーが置換して任意のハンドラを使用できるようになっています。この違反ハンドラは例外を送出することによって契約違反を処理するようにすることができるように例外を送出しうることが考慮されています。そしてそれは、デフォルトの違反ハンドラにおいても同様でした。

C++29以降の拡張として、言語の未定義動作に対して暗黙の契約を付加することによって契約プログラミング機能の枠組みでUBに対処しようとする方向性があります。この場合、UBが起きる場合に代わりに違反ハンドラが呼ばれるようになりますが、その違反ハンドラが例外を送出する場合、UBの代わりに例外送出という別のコードパスが追加されてしまうことになり、これについて懸念を持つ人が少なくなかったようです。

この提案は、少なくともデフォルトの違反ハンドラは例外送出しないことを規定することによって、それらの懸念を払拭しようとするものです。

ただ依然として、ユーザー定義の違反ハンドラは例外送出によって終了することができます。あくまで、デフォルトの違反ハンドラの動作(実装定義とされている)について例外送出という選択肢を無くそうとするものです。

この提案はSG21では好まれたものの、EWGでのコンセンサスを得ることができなかったようです。

P3578R0 Language Safety and Grandma Safety

安全性(Safety)という言葉の定義を明確にし、その使用方法に注意を払う事を推奨する提案。

色々ある安全性の中でも、言語安全性と機能安全性(ここではおばあちゃんの安全性(Grandma Safety)と呼ばれている)を区別し定義し

  • 言語安全性: 言語設計者とコンパイラが強制できる全てのものの集合
    • (型|メモリ|ライフタイム|スレッド|etc)安全性が含まれる
  • おばあちゃんの安全性(機能安全性): プログラムがおばあちゃんに危害を加えないことを保証するものの集合

そのうえで次の事を推奨、あるいは説明しています

  1. ハイフン無しで“safe”/“safety”という言葉を使用しない
    • 意図する内容を正確に表す業界で認められた完全な正しい用語を使用する
  2. 言語安全性とは何か、機能安全性とどう違うのかを理解する
  3. 言語安全性と機能安全性はどちらも必須だが、同じ機能で満たす必要はない
  4. 契約チェックは機能安全性に必須だが、言語安全性にも役立つ
  5. 言語安全性だけに重きを置き機能安全性を気にしない場合、それに注力し続けたうえで機能安全性を向上させようとしている機能を阻害しない

これは主に、C++ Contracts機能がプログラムの正確性の向上を介して機能安全性を向上させるためのものであり、言語安全性の向上のための機能ではなく、言語安全性の向上が果たされないからと言ってContractsが不要というわけではないことを説明するための文書の様です。

P3579R0 Fix matching of non-type template parameters when matching template template parameters

テンプレートテンプレートの特殊化のマッチング時に、NTTPの縮小変換を許可しないようにする提案。

この提案はP0522R0(C++17)で修正された部分特殊化のマッチングルールの悪影響を緩和しようとするP3310R5の提案をフォローアップするものです。その問題とはNTTPを含むテンプレートテンプレートパラメータに対して、テンプレートをマッチングしようとする際に、NTTPの縮小変換が起きてしまうというものです。

template<template<short> class>
struct A {};

template<short>
struct B;

template struct A<B>; // OK, exact match

template<int>
struct C;

template struct A<C>; // #1: OK, all 'short' values are valid 'int' values

template<char>
struct D;
template struct A<D>; // #2: error, not all 'short' values are valid 'char' values

P0522R0では#1を許可することを意図していたものの、意図せずに#2も許可してしまっていたようです。この提案はこの#2を拒否しようとする(サンプルコードコメントの通りの動作になるようにする)ものです。

P0522R0で導入されたルールでは、このようなマッチングをテンプレートパラメータから変換した関数テンプレートのオーバーロード解決によって指定していました。しかし、そのように生成された関数テンプレートではNTTPの変換時にその値依存性(具体的な値によって変換可能性が決まる)により縮小変換のチェックが行われないようになっていたため、NTTPの縮小変換が許可される形でマッチングが成功してしまっていたようです。

このマッチングはテンプレートテンプレート仮引数Pがテンプレートテンプレート実引数Aと「P is at least as specialized as A」という関係にあるかどうかを指定するところで行われており、この規定の意味は「テンプレートテンプレート仮引数に指定可能なテンプレート引数とは、テンプレートテンプレート実引数に対しても適用可能なものである」ということだったようですが(すなわち、仮引数と実引数を逆にしても指定可能なはず、ということ)、それを関数テンプレートのオーバーロード解決に委譲してしまった事によってその意図が正しく表現されていなかったようです。

この提案の修正では、上記関数テンプレート生成時にNTTPの縮小変換について明確に規定することで問題を解消しようとしています。

P3580R0 The Naming of Things

C++の新機能(主にライブラリ機能)の名前付けについてのガイドラインを提供使用とする文書。

この文章では、名前付けは設計の重要な要素であるとして命名の重要性を説いています。

  • 良い名前はソフトウェア設計の重要な要素
    • 設計は名前を中心に構築される
  • 良い名前を見つけるのに苦労する場合、問題は設計にある可能性が高い
  • 名前の選定はBikesheddingではない

そのうえで、次の事を推奨しています

  • 提案の著者は、明確かつ強固な、設計を反映した命名について提案すること
    • 命名が不明瞭である場合や、命名について論争がある場合、提案にはそのことを含める
  • 提案の著者は、全てのフィードバックを検討し、最終的な推奨事項をまとめる
    • 著者が名前について確信が持てず、委員会からのフィードバックや提案リストを必要とする場合、積極的に尋ねるべき
    • 提案の読者は命名について疑問を持つ場合、他の主要な設計要因と同様に、提案で提示される論理的な議論によって対処する
  • 名前の投票という慣行をやめるべき
    • この方法を選択する場合、本当にBikesheddingに陥っている可能性が高い

おそらくこれはLEWG/EWGのポリシーに関連する提案に該当すると思われます。

P3581R0 No, inplace_vector shouldn't have an Allocator

inplace_vectorにアロケータサポートを追加することに反対の立場を示す文書。

P3160R2ではinplace_vectorにアロケータサポート(要素へのアロケータの伝播)を追加することを提案しています。この提案はEWGではコンセンサスを得られていませんが、P3160の著者の方は粘り強く推進しているようです。

この文書ではSG14で反対意見が無かったことは議論の方向性を示しておらず、P3160R0が否決された時から新しい情報(動機やユースケースなど)は示されていないとして、反対意見を表明しています。

どうしてもしたい場合、basic_inplace_vector<T,N,A>の様な別の型として追加すべきとしています。

P3582R0 Observed a contract violation? Skip subsequent assertions!

Observeセマンティクスにおいて、契約違反発生後は後続の契約アサーションを評価しないようにする提案。

例えば次のような関数があるとき

Tool* selectTool(Tool* ta, Tool* tb)
  pre (ta != nullptr)
  pre (ta->is_configured())
  pre (tb != nullptr)
  pre (tb->is_configured())
  post (r: r != nullptr)
  post (r: r->is_configured())
{
  if (preferFirst) return ta;
  else             return tb;
}

ある時点において、この関数は契約アサーション無しで運用されており、この関数にはnullptrが渡されている可能性があります。この関数の事後条件(戻り値はnullptrではない)はどうやら有効活用されていなかったことで問題は表面化していませんでした。

その後、この関数に契約アサーションを導入することにして上記例のように契約を付加して、Observeセマンティクスによって様子を見ることにします。すると、契約アサーション内でnullptrチェックの後ポインタのデリファレンスを行っていることによって、この関数は未定義動作を伴うようになります。

この場合のUBは、Ignore/Enforceセマンティクスでは発生せず、Observeセマンティクスでのみ発生します。

この提案は、このようなUBを回避するために、ある契約シーケンスの中でObserveセマンティクスの下で契約違反が起きた場合、そのシーケンス内で後続の契約アサーションのチェックをスキップする(あるいはIgnoreセマンティクスで評価する)ようにしようとするものです。

契約シーケンスとは、ある関数の事前条件の全体、事後条件の全体、あるいは本体内のアサーションの全体、などの小さなまとまりの事です。

この提案はSG21のレビューにおいて否決されています。

契約条件が正しいとすると、契約違反が起きている状態というのはプログラムが正しくない状態であるため、Observeセマンティクスでそれが検出された後にUBになるのは想定された動作です。この場合、少なくとも契約違反に気づくことはできるため、関数の利用側を修正するべきです。また、契約条件が正しくない(指定された条件は事前条件と言えるものではないなど)場合、それは関数内で明示的にifなどでガードするべきです。いずれにせよ、Observeセマンティクスの動作を弄ることによって表面的なUBを回避する方向性が好まれなかったものと思われます。

P3583R0 Contracts, Types & Functions

関数型に契約注釈を持たせるようにすることで、関数ポインタに対する契約注釈の指定を可能にする提案。

P2900R12でC++26に向けて提案中のContracts機能においては、関数ポインタに対する契約注釈の指定をサポートしていません。これは関数ポインタに対する契約注釈の扱いについて解決すべき様々な問題があることから、C++29以降の機能として議論していくことにしているためです。

関数ポインタに対する契約注釈についての検討はP3327R0で詳細に検討されており、そこでは関数ポインタに対する契約注釈を型の一部にする、値の一部にする、変数のプロパティとする、の3つの方向性が挙げられています。

この提案はその3つのいずれでもなく、関数型に契約注釈を埋め込むアプローチについて検討しているものです。

関数型に対する契約を可能にするために解決する必要がある問題点は次の3つです

  1. 2つの契約の同一性を判定する方法
  2. 名前マングル処理の実装方法
  3. 関数型の契約を使用するようになった場合に、使用していない既存のコードに悪影響を及ぼさないこと

現在の関数型は構造的な型(structural type、NTTPの文脈の用語とは若干異なる意味)であり、関数の構造からその型が決定されます。例えば、異なる場所で定義された関数であってもそのシグネチャが同一であれば同じ型を持ちます。この関数型には名前がなく、同じ型に対しては名前マングリングによって一意の識別子が生成される必要があります。

関数型に契約注釈を追加しようとする場合、コンパイラは2つの関数が同一の型を持つかどうかを判断できる必要があり、このためには契約注釈を決定的な形で正規化できる必要があります。名前マングリング時にも同じ事が求められます。

この提案では、契約注釈付きの関数型については構造的な型としないことによってこれらの問題(特に1と2)を解決しようとしています。このアプローチはすでにクラス型において運用されています。クラス型の型は、定義時の名前によって区別され、クラス構造が完全に同一であっても名前が異なれば異なる型として扱われます(このような型をnominal type、公称型と呼ぶ)。

すなわち、契約注釈付きの関数型はその宣言時にユニークな名前を与えられ、それによって型の区別が行われます。したがって、同じシグネチャと同じ契約注釈をもつ契約注釈付きの関数型であっても、その名前が異なれば異なる型として扱われます。これによって、契約注釈の正規化の問題を回避することができます。

また、このような型付けの導入は専用の構文を用いて非侵入的に行うことを提案しています。すなわち、契約注釈の無い関数は従来どおり構造的型付けが行われ、シグネチャが同じなら同じ型を持ちます。しかし、契約注釈が付加された時点で記名的型付けによって公称型となります。

型に付与された契約注釈はオーバーロード解決に寄与せず、異なる型のポインタ(契約注釈付き関数ポインタ)の間には暗黙変換が定義されません。また、P2900R12が提案している現状(契約注釈を持つ関数のアドレスを関数ポインタに入れられる)を保護し、なおかつ型に対する契約注釈情報の伝播を行わないようにするために、関数は型の一部とは別に実装契約を持つことができるようにします(これは、P2900R12において関数ポインタ経由の呼び出し時に契約チェックを行うための実装方法であるサンクのような仕組みをサポートする意図)。これらのことによって、後方互換性の問題を回避しています(上記の3)。

この新しい、契約注釈付きの関数型を宣言する構文としては、function classという文を提案しています。

// 通常の関数型
function class Fa = int(int,int);
function class Fb = int(int,int);
static_assert(is_same_v<Fa, Fb>);

// 契約注釈付き関数型
function class Fc = int(int a,int) pre(a < 4);
function class Fd = int(int a,int) pre(a < 4);
static_assert(not is_same_v<Fc, Fd>);

異なる関数が同じ契約注釈を持つと判定されてほしい場合、同じ型宣言を使用して作成する必要があります。しかし、C++には定義済みの型から(クラスのオブジェクトを構築するように)関数を定義する方法はありません。この提案ではそのための構文としてキャプチャレスラムダの構文を借用します。

// 契約注釈付き関数型
function class Fa = int(int a,int) pre(a < 4);

// 関数型による関数定義
Fa fa = [](auto a,auto b) { return a+b; }

// 有効な定義例
Fa fa = [](auto a, auto b) { return a+b; } // Ok
Fa fa = [](int a, int b) { return a+b; } // Ok
Fa fa = [](auto... a) { return a+...; } // Ok
Fa fa = [](a, b) { return a+b; } // Ok?
Fa fa = [](float a, float b) { return a+b; } // Error

これはキャプチャレスラムダの関数ポインタへの変換のようにも見えますが、この提案では直接ラムダを宣言する形のみを許可することを意図しており、任意のラムダ式から変換する形で定義するものではありません。

この関数インスタンスの定義においては引数型はすでに関数型としてわかっているため、厳密に一致させるかautoにするか名前のみを指定するかのいずれかが許可されます。

関数ポインタは通常の型に対するポインタのように構築できます

function class Fa = int(int a, int) pre(a < 4);

// 関数型による関数定義
Fa fa = [](a, b) { return a+b; }

// 契約注釈付き関数ポインタ定義
Fa* pa = &fa;

// 通常の関数型
function class Fb = int(int a, int);
Fb* pb = &fa; // Error, 関数型が異なる

pbの例は、異なるユーザー定義型のポインタ型の間で暗黙変換が用意されないのと同様に拒否されます。

型に対する契約は安全かつ適切であるものの、契約が変更されるとこの型を使用するコードを変更する必要がある場合があります。これを軽減するために、関数の実装(インスタンス)に対しても契約を指定することができます。

function class Fa = int(int a, int) pre(a < 4);
Fa fa = [](auto a, auto b) post(b<13) { return a+b; }
Fa* pa = &fa;

このfaは関数型に指定された事前条件と、関数実装に指定された事後条件の2つの契約を持っています。変数faを直接使用して呼び出す場合は両方の契約が可視になりますが、関数ポインタpaを使用する場合は必ずしもそうではありません。

この提案では、この場合の実装に指定された契約注釈を実装契約(Implementation contracts)と呼び、実装契約を持つ関数(契約注釈付き関数型のインスタンス)のアドレスを取得する時、この実装契約がチェックされる処理を持つ、同じ型の関数ポインタが作成されます。そのような関数ポインタ経由の呼び出しは、

型の事前条件 -> 実装契約の事前条件 -> 関数本体 -> 実装契約の事後条件 -> 型の事後条件

の順で評価されます。

通常の関数に対する契約注釈とそのアドレス取得時にもこの延長で処理することを提案しています。すなわち、通常の関数の関数型はそこからfunction classによる関数型(ただし契約注釈はない)を求めて、その型のインスタンスとして定義される、と解釈します。これによって、通常の関数に指定された契約注釈は実装契約として扱うことでP2900R12の動作を維持できます。

// 通常の関数宣言
int fc(int a, int b) pre(b<13) { return a+b; }

// ↑はこのような関数型とそのインスタンスとして解釈される
function class __FC = auto (int, int)-> int;
__FC fc = [](a, b) pre(b<13) { return a+b; }

// 契約注釈付き関数型と同じルールにより関数ポインタが作成される
int (pc*)(int, int) = fc;

このような__FCは同じシグネチャを持つ通常の関数宣言による関数型の間で共有されます。

関数テンプレートの場合もクラステンプレートと同じような扱いによって処理できます。

// テンプレート化された関数型
template<typename E>
function class Fe = int (int, E e) pre(e < 4.14);

// 関数テンプレートのインスタンス(関数テンプレートの定義
template<typename E>
Fe<E> fb = [](a, b) { returna+b;};

// 呼び出しによる型推論とインスタンス化
fb(A{} ,3.14); // 型は `Fe<double>`

// 関数テンプレートの明示的特殊化による定義
Fe<int> fi = fb;  // fiという名前でintによりインスタンス化

// ジェネリックラムダでの使用
auto fg = []<typename T>Fe<T>(a, b) { return a+b; };

この関数型とそのインスタンスによる関数定義における前方宣言は、引数を参照するインスタンスプロパティに名前を付ける必要性から、これまでとは少し異なった形になります。

// 関数型の定義
function class Fa = int(float, int);

// 前方宣言
Fa fa;
int fa(float, int);
int fa(float, int b) pre(b == 4);

// 関数定義
Fa fa = [](float a, int b) pre(b == 4);
Fa fa = [](auto a, auto b) pre(b == 4);

メンバ関数の場合、契約注釈内でクラスメンバを参照するために、明示的オブジェクト引数(deducing this)を持つメンバ関数のみを ただし、メンバ関数の場合は少し複雑になってしまいます

struct X {
  function class Xf = int (int);              // 関数型
  function class Zf = int (this X, int, int); // メンバ関数型

  function class Xg = int (int) pre (x == 3);       // Error、メンバアクセスできない
  function class Xg = int (this X ,int) pre (x==3); // Ok

  int x;

  // 関数型インスタンス
  static Xf my_static_function;
  Xf my_member_function;
};

// メンバ関数ポインタ
X::Zf X::* pc;
pc = &X::my_member_function;

// 
X::Xf* ps;
ps = &X::my_static_function;

X::Xfは文脈に応じてフリー関数型としてもメンバ関数型としても使用できるようにすることを提案しています。しかし、このような性質はその契約注釈からメンバが参照されたときに問題となるため、契約注釈からメンバを参照可能な関数型はthisを明示的に取る形式に限定することを提案しています。このために、関数型を拡張してメンバ関数型の表現のために明示的なthis引数型を許可するようにします。

契約注釈付き関数型によって宣言された仮想関数においては、P2900R12で提案されているのと互換性を持つように

  1. 基底型の契約の事前条件
  2. 基底型の実装契約の事前条件
  3. 派生型の実装契約の事前条件
  4. 派生型の実装契約の事後条件
  5. 基底型の実装契約の事後条件
  6. 基底型の契約の事後条件

の順序でチェックすることを提案しています。

P3584R0 Enrich Facade Creation Facilities for the Pointer-Semantics-Based Polymorphism Library - Proxy

P3086R3からfacade_builderというクラステンプレートだけを導入する提案。

P3086では、通常のインターフェースと継承のような動的な多態性ではなく、静的な多態性を実現するためのライブラリ機能について提案しています。P3086については以前の記事を参照

この提案は、その中の一つの機能であるfacade_builderbasic_facade_builder)だけを導入しようとする提案です。

basic_facade_builderはP3086R3の機能のベースとなっているProxyライブラリにおいて、std::proxyにインターフェース型を登録する際に、その性質を細かく指定してファサード型を導出するためのクラステンプレートです。これは、Proxyライブラリを使用するにあたって複雑な設定を使いやすくするための入り口となる機能です。

basic_facade_builderのメンバはすべて何らかのエイリアステンプレートでありfacade_builderはその設定済みのエイリアスになっています。

template <class Cs, class Rs, proxiable_ptr_constraints C> 
struct basic_facade_builder { 
  template <class D, class... Os> requires(see below) 
  using add_indirect_convention = see below; 

  template <class D, class... Os> requires(see below) 
  using add_direct_convention = see below; 

  template <class D, class... Os> requires(see below) 
  using add_convention = see below; 

  template <class R> 
  using add_indirect_reflection = see below; 

  template <class R> 
  using add_direct_reflection = see below; 

  template <class R> 
  using add_reflection = add_indirect_reflection<R>; 

  template <facade F, bool WithUpwardConversion = false> 
  using add_facade = see below; 

  template <size_t PtrSize, size_t PtrAlign = see below> requires(see below) 
  using restrict_layout = see below; 

  template <constraint_level CL> 
  using support_copy = see below; 

  template <constraint_level CL> 
  using support_relocation = see below; 

  template <constraint_level CL> 
  using support_destruction = see below; 

  // 導出結果を得る型エイリアス定義  
  using build = see below; 

  basic_facade_builder() = delete; 
};

using facade_builder = basic_facade_builder<tuple<>, tuple<>, 
  proxiable_ptr_constraints{ 
    .max_size = default-size, 
    .max_align = default-size, 
    .copyability = default-cl, 
    .relocatability = default-cl, 
    .destructibility = default-cl
  }>;

facade_builder::buildあるいは各種設定を指定したbasic_facade_builder<...>::buildからstd::proxyで使用可能なファサード型を得ることができます。見ての通りbasic_facade_builderは複雑であり設定の指定方法が難しいので、その単純なインターフェースとしてfacade_builderが用意される形になります。

P3585R0 allocator_traits::is_internally_relocatable

std::allocator_traitsをリロケーション対応させる提案。

P2786R11ではC++26に向けてオブジェクトのリロケーションという操作を言語レベルで許可しようとしています。これが導入されると、コンテナにおいて内部で要素移動を行う必要がある場合(std::vectorのキャパシティ伸長時や.erase()時)のオブジェクト移動コストを大きく下げることができます。特に、トリビアルリロケーションが可能であれば、クラス型であってもその移動(リロケーション)はmemcpyにまで単純化することができるようになります。

しかし、実際の標準コンテナの動作はアロケータに依存するところが大きく、コンテナ要件ではコンテナ要素のオブジェクトの構築と破棄はstd::allocator_traits<Alloc>::construct()std::allocator_traits<Alloc>::destruct()を使用して行わなければならないと指定されています。この要件は標準コンテナにおけるリロケーションの使用を妨げることになり、また、トリビアルリロケーションを不可能にします。

この提案は、std::allocator_traitsにアロケータからコンテナへのリロケーション許可権限を通知するためのインターフェースを追加することで、アロケータおよび標準コンテナをリロケーション対応させようとするものです。

std::allocator_traitsに追加されるインターフェースは次の2つです

  1. std::allocator_traits<Alloc>::internally_relocate()
    • std::relocate()(P2786R11)と同じインターフェースをもつ
    • Allocinternally_relocate()メンバ関数を提供する場合それを呼び出し、そうでない場合はstd::relocate()を呼び出す
    • リロケーション操作の前後にアロケータからの処理を挟みたい場合に使用できる
  2. std::allocator_traits<Alloc>::is_internally_relocatable<T>
    • と対応するstd::allocator_traits<Alloc>::is_internally_relocatable_v<T>
    • コンテナがT型の要素をconstrcut()/destroy()を介さずにリロケーションすることを許可する場合、trueを返す
    • アロケータがis_internally_relocatable<T>メンバ(メタ関数)を提供する場合、それに委譲
    • そうでない場合次のいずれかの場合にtrue
      • Tis_nothrow_relocatable_v<T>を満たしていて、Allocconstrcut()/destroy()を提供していない
      • Allocis_internally_relocatableメンバを提供する

コンテナでは、std::allocator_traits<Alloc>::is_internally_relocatable_v<T>の値によってリロケーションを行うかどうかを決定し、リロケーション操作はstd::allocator_traits<Alloc>::internally_relocate()を使用して行います。このようにすることで、アロケータによるカスタマイズ性を維持しながら、リロケーションの恩恵を受けられるようになります。

std::vector<T, A>::erase(first, last)における利用例

この提案がない場合 この提案
using AT = allocator_traits<A>;
if constexpr (is_nothrow_relocatable_v<T> &&
              ! requires(A& a, T* p) {
                  a.construct(p, std::move(*p)); } &&
              ! requires(A &a, T* p) { a.destroy(p); })
{
  AT::destroy(m_alloc, first);
  m_end = relocate(last, m_end, first);
}
using AT = allocator_traits<A>;
if constexpr (AT::template is_internally_relocatable_v<T>) {
  AT::destroy(m_alloc, first);
  m_end = AT::internally_relocate(m_alloc,
                                  last, m_end, first);
}

また、std::vector.erase()でリロケーションを許可するために、その要件を緩和する文言変更も同時に提案しています。

P3586R0 The Plethora of Problems With Profiles

議論中のプロファイル機能についての問題点を指摘する文書。

指摘は多岐にわたり、安全性向上への取り組みが必要であることに同意しつつも、プロファイル機能をほとんど全否定しています。

ここでは問題点の詳細を転記しませんが、例えば既存の実行時検査メカニズムの利活用、構文の無視可能性、構文の冗長さ、長期的に運用した場合の後方互換性についての考慮不足、危険とされる構文をどこまで禁止すべきかの考慮不足(あるいはその困難性)、標準で警告を制御しようとすることの有害性、安全性とスタイル上の懸念の混同、言語のサブセット化メカニズムとしての貧弱さ、などが指摘されています。

この文書は何かを提案するものではないものの、C++の言語安全性の向上には次のような多角的なアプローチが必要としています

  • 危険な構文を削除/非推奨化し、より明確な代替手段を提供する
  • 危険な構文の削除が困難な場合には、エポック(P1881R1)のような仕組みを検討する
  • P3100などで提案されているように、実行時に言語のUBを検出しハンドリングするのに契約機能を使用する
  • 標準ライブラリ実装との連携を継続し、実行時に事前条件チェックの強制適用を徹底する
  • 制御フロー解析ツールが十分に成熟し、どの部分を標準化すべきか判断できる段階にいたるまで、実装/ツールベンダとの協力関係を維持する
  • 長期的な視点から、メモリ安全性についての言語レベルの解決策を検討する
  • 委員会が、CHERI/MTE/pointer認証技術の普及支援にどの程度貢献できるか検討する
  • メモリ安全な言語との互換性向上/インターフェースの改善について検討する
    • このような言語は一部の業界で主要な開発ツールとして採用されつつあるため
  • 時期尚早に扉を閉ざさない
    • たとえそれが一見困難だったり、野心的なアイデアだったりしたとしても、多くの有用な提案は慎重に検討する価値がある

C++の安全性向上策としては、標準仕様の変更とベンダ提供ソリューションを組み合わせた多角的なアプローチを採用し、核問題に対して適切なツールを選択するようにすることを推奨しています。

また、言語安全性の向上はC++の将来にとって重要な問題点であるものの切迫感に駆られて判断を誤るべきではなく、規制当局からの圧力は非常に複雑で多面的なこの問題を数週間で解決しようとする理由にならない、としています。

P3587R0 Reconsider reflection access for C++26

リフレクション機能において、クラスのプライベートデータへのアクセスを禁止する提案。

C++26に向けて静的リフレクション機能の導入がP2996R8で議論されています。リフレクションは、これまで手作業で行われてきたボイラープレートコードの自動化などを目指しており、プログラマの生産性向上に大きく貢献することが期待されています。

しかし、リフレクション機能の中でも特に、プライベートメンバへのアクセス制御の扱いに関しての議論が紛糾しています。P2996のリフレクションでは、任意のクラスのプライベートメンバに無制限でアクセスすることができますが、リフレクション機能が既存のC++のルールに反してクラスのアクセス制御を簡単に無視できてしまうことについて反対する声が大きくなっています。

この提案もリフレクションがアクセス制御をバイパスできてしまう現状に反対を表明するものです。

この提案では、クラスのプライベートメタデータ(型のサイズやアライメント、メンバ変数の型とオフセット、など)とプライベートデータ(メンバ変数オブジェクトに格納される値、メンバ関数のアドレス値)へのアクセスを区別して考えることを指摘しています。

プライベートメタデータへのアクセスはすでに型特性メタ関数によってアクセス可能になっていますが、そのためには部分特殊化などのボイラープレートコードを書く必要があり、これはリフレクション機能による自動化対象のタスクです。

  • カスタム型特性は有用であることが知られている
  • メタデータへのアクセスはコンパイル時にしか起こらないため安全
  • ユーザーは既にカスタム型特性を手で記述することができる

これらの理由により、この提案は任意のプライベートメタデータへのアクセスには反対していません。

プライベートデータへのアクセスもその必要性と有用性は認めつつも、許可を得てアクセスすることと無制限かつ無秩序にアクセスできることは異なるとして、特に後者について反対しています。

クラスにはその型固有の不変条件が明示的にも暗黙的にも存在しており、言語が提供するアクセス制御によってそれは通常保護されています。不変条件に関してよく理解しないコンテキストからプライベートデータにアクセスすることは、不変条件を容易に棄損してしまうことになります。プライベートデータの変更を伴わない読み取りのみだったとしても、そのプライベートデータが複数のスレッドからアクセスされるものだったとすると読み取りさえも安全ではありません。

結局、P2996R8の時点でのリフレクション機能は、プライベートメンバは外部アクセスから安全であるという言語が提供する非常に古く根本的な前提を覆そうとしています。

提案には、これらの事を踏まえたうえで既知のアクセス制御をバイパスする機能への賛成意見に対してそれがその機能を正当化しないことを指摘しています(シリアライズライブラリでは型に侵入的なアノテーションが必要なのでオプトインのアクセス許可は苦にならないはずである、など)。

ここでの提案としては、C++26で任意のプライベートデータにアクセスする機能を提供すべきではないとしています。ただし前述のように、任意のプライベートメタデータと許可されたプライベートへのアクセスは許容されます。ただし、この機能を26に間に合わせようとはしておらず、26ではアクセス制御をバイパスする様な機能を一切入れるべきではないとしています。

C++29以降で、任意のプライベートメタデータと許可されたプライベートへのアクセスを許容する方向性としては、いくつかのオプションを提示しています

  1. スプライシングはアクセス制御を尊重する(P3473R0)
    • P3473R0ではスプライシング[: refr :])式によるアクセスの際に、通常のアクセス制御を適用することが提案されている
  2. P3547R0のaccess_context::unchecked()を削除したバージョン
    • P3547R0では、型のメンバへのアクセス権限を表現するためのstd::meta::access_contextが提案されている
    • そこでは、無制限アクセス権限を得るためのaccess_context::unchecked()が提案されているが、これを削除したバージョンを受け入れる
      • ただし、プライベートメンバにアクセスする方法が無くなる

この提案の筆者の方は、1の方法を推奨しています。これは現在のアクセス制御ルールに従ったうえで、許可された場合にのみプライベートデータへのアクセスを許可するものです。これがC++26に含まれたとしても、C++29でプライベートアクセスの方法が変更された場合には簡単に制限を解除することができます。

C++26にアクセス制御をバイパスする機能が導入されて発行された場合、後からそれを削除することは不可能になります。一方で、26に含めずに発行しても29などで後からそれを追加することは容易なことです。そして、アクセス制御をバイパスする機能が無かったとしても、P2996のリフレクションはC++にとってかなり良いインパクトのある機能追加であり、プログラマの生産性を大きく向上させることは間違ない、としています。

SG7の議論と投票においては、おおむねオプション2の方向性が採択されたようです。

P3588R0 Allow static data members in local and unnamed classes

ローカルクラスでstaticメンバ変数を宣言できるようにする提案。

ローカルクラス(関数内で定義されたクラス)と無名クラスは、その制限としてstaticメンバ変数を持つことができません。これはその定義を提供する方法がないことから課されていた制限のようです。しかしC++17以降、クラスの静的メンバ変数はinline定義を持つことができるようになっているため、定義を提供する方法は現在存在しています。

また、N2657の採択によってC++11以降、ローカルクラス/無名クラスはどちらもテンプレートパラメータに渡すことができるようになっています。これはローカルクラス/無名クラスに対して適切な名前マングルの方法が確立されていることを示しており、さらにはローカルクラス/無名クラスのメンバ関数のアドレスを取得できていることから、それらの関数は弱い外部シンボルを持つ必要があるため、やはり関数定義を提供することがすでに可能になっています。

このように、定義を提供できないという実装上の問題点は解消されており、かつ非ローカルクラス(通常のクラス)で静的メンバ変数が有用であるようにローカルクラスでも有用性があるとして、この提案ではローカルクラスおよび無名クラスのこれらの制限を取り払い、静的メンバ変数を宣言できるようにしようとしています。

定義の方法に関しては前述のようにstatic inlinestatic constexpr)変数と同じ方法でinline定義を提供するようにする方向性のようです。この場合、非inlineな静的メンバ変数は依然として定義を提供する方法がないわけですが、評価されない文脈での使用などで有用であるため、(定義を必要としない使用法に限られるものの)許可するようにすることを提案しています。

一方リンケージ目的のtypedef名を持つクラスでは許可しないようにしています。

typedef struct {
  inline static int x = 0;
} S;

これは、このようなクラスに対してはすでに、C-likeな性質を達成するために様々な制約が課されているためです。Cには静的メンバ変数というものは存在しないため、これを許可する理由はないようです。

なお、このように許可される変数の初期化順序は、その変数が非ローカルクラスで宣言されている場合と同じ規則に従います。

inline int x = ...;

void f() {
  struct S {
    static inline int y = ...;
  };
}

struct T {
  static inline int z = ...;
};

この場合、ローカルクラスの静的メンバ変数yの初期化は、xの後、zの前に順序付けられます。この時、f()がテンプレートであるならば、その初期化は任意のタイミング(おそらくテンプレートがインスタンス化されるとき)に行われます。

この提案の内容は、GCCでは-fpermissiveの場合にすでに利用できるようです(ただし、明示的なinlineが未実装)。clangに対しては筆者の方がパッチを書いたようで、実装に当たっては特に問題ないようです。

P3589R0 C++ Profiles: The Framework

フレームワークとしてのプロファイル機能の概説文書。

C++29以降を目指してプロファイルという機能が検討されています。これは主にC++のコードに特定の保証を追加するためのもので、これによってC++コードの安全性やセキュリティを高めることを目的としています。

この文書は、プロファイル機能のフレームワークとしての部分に着目して大まかに意義や目的等を説明しているものです。特に、個別のプロファイルの詳細についての議論にプロファイル機能そのもの(プロファイルを指定する構文やその意味論)が巻き込まれることで議論が停滞したり発散したりするのを回避するために、具体的な特定のプロファイルから分離されたプロファイル機能そのものに焦点を当てています。

提案文言も含まれていますが、これも標準の変更についてを例示するもので、文言自体はP3081から借りてきているようです。

P3590R0 Constexpr Coroutines Burdens

コルーチンのconstexrp対応を遅らせる提案。

P3367ではコルーチンのconstexpr対応が提案されていますが、その実装可能性についての懸念が多く上がっているようです。

この提案は、コルーチンのconstexpr対応の実装についての懸念を簡単に説明したうえで、その採択を当分遅らせることを提案しています。

提案によれば、現在の主要な実装はおそらくすべて、式とconstexpr関数の解析から構築されたASTをその頂点ノードから再帰的に評価することで定数式実行を実装しているようです。当然ながら、このような定数式の実行方法はパフォーマンスやスタック消費においてかなりのデメリットを抱えています。

また、一部のC++フロントエンドは自身あるいはほかのツールから使用されるコンポーネントとして提供されています(例えばclang)。このため、ASTの表現を外部ツールが解析可能であるように保つ必要があり(それによりconstexprコルーチン実行に適した形に変更できない)、またコンポーネントの実行のためにその導入する環境に対して過度の負荷をかけないように配慮されています(追加のライブラリへの依存の忌避や、自身を記述するコードのC++バージョンの制限など)。

これらの理由により、P3367で挙げられているような実装方法は採用されない可能性が高いとしています。

そのうえで、現在のC++の実装では定数式の実行基盤を上記のようなASTの直接評価から、仮想マシンベースの評価機(バイトコードインタプリタのようなもの)へ移行しようとする動きがあるようです。これは、静的リフレクションの導入によって定数式への依存度がかなり高まっていくことが予想されるためです。

ただし、このような移行は少なくとも今後10年の単位での話であり、C++26や29にはとても間に合うものではありません(clangはすでにこの作業を開始しているようですが、既に数年間が経過しています)。

これらの理由から、コルーチンのconstexpr化はC++26に対しては時期尚早であり、仮に導入したとしても既存の実装は(上記のような以降が完了する)今後10年の大半の間実装することはなく、それによって標準への準拠を放棄するだろうと予想されます。

このため、この提案では定数式の実装が上記のようなVMライクな実装に移項するまでの間、コルーチンのconstexpr対応を標準化するのは待つべきとしています。

この提案提出後のP3367R3に対するEWGでの投票では、C++26に導入することにコンセンサスは得られなかったものの、C++29導入を目指すことについてはコンセンサスが得られたようです。

おわり

この記事のMarkdownソース