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

文書の一覧

全部で125本あります。

もくじ

N4966 St. Louis Meeting Invitation and Information

2024年6月24〜29日(米国時間)にかけてアメリカのセントルイスで行われるWG21全体会議の案内。

N4967 WG21 2023-10 Admin telecon minutes

2023年10月27日に行われたWG21管理者ミーティングの議事録

前回(6月)のミーティング以降の各SGの進捗や作業の報告や、11月のKona会議におけるミーティングの予定などが報告されています。

N4970 WG21 2023-11 Kona Minutes of Meeting

2023年11月にハワイのKonaで行われたWG21全体会議の議事録

N4971 Working Draft, Programming Languages -- C++

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

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

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

P0447R24 Introduction of std::hive to the standard library

P0447R25 Introduction of std::hive to the standard library

P0447R26 Introduction of std::hive to the standard library

要素が削除されない限りそのメモリ位置が安定かつメモリ局所性の高いコンテナであるstd::hive(旧名std::colony)の提案。

以前の記事を参照

R24での変更は

  • 代替実装詳細のappendixにある、オーバーアライメントなしで小さい型をサポートする方法についての見直し
  • shrink_to_fitの文言を、std::vectorのものに近づける様に変更

R25での変更は

  • spliceが終端イテレータを無効化することを明確化
  • block_capacity_limits()constexprを付加
  • reshape(), shrink_to_fit()で要素の並べ替えが発生する可能性があるタイミングを明確化するとともに、削減
  • sort()list::sort()と調和する様に変更
  • 標準の言葉の表現と一貫するように、"shall be"を"is"に、"into hive"を"into *this"に修正
  • reshape()がキャパシティを変更する可能性があることを追記
  • 他の部分でカバーされているため、sort()の例外に関する記述を削除

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

  • is_active()を削除
  • get_iterator()に事前条件が追加され、効果の指定を削除
  • googleグループのリンク更新
  • 代替実装ガイドラインを代替実装のappendixへ移動
  • よくある質問と、委員会からの一部の質問への回答をappendixにまとめた
  • 最初のリファレンス実装と現在の実装に関するメモとベンチマークをappendixにまとめた
  • 委員会への質問セクションを削除し、簡単な定義セクションに置き換え
  • 負の値を処理できるdistance()への参照を削除
  • size()を持たないことについての情報を、Design DecisionsからFAQへ移動
  • 時間計算量のappendixを削除し、個々の関数のDesign Decisionsセクションへ移動
  • 正確さの向上のため、オーバーアライメントをストレージを人工的に広げる、に変更
  • 代替実装appendixのビットフィールド+ジャンプカウントをより小さな型をカバーできる様に変更
  • 非常に小さな型をサポートするための3つのより良いアプローチについて代替実装appendixに追記
  • FAQにslot mapとの比較に関するセクションを追加

などです。

この提案は、LEWGでのレビューと投票を終えて、LWGに転送されています。

P0609R2 Attributes for Structured Bindings

構造化束縛の個々の名前に属性を指定できるようにする提案。

構造化束縛宣言には属性を指定することができますが、その属性が作用するのは構造化束縛宣言の裏に隠れている分解対象のオブジェクトであり、導入されている個々の名前ではありません。

auto f() -> std::tuple<int, double, char>;

int main() {
  // この属性指定はf()の戻り値(見えていない)に対するもの
  [[maybe_unused]]
  auto [a, b, c] = f();
}

構造化束縛対象そのものに対してアライメントを指定するなど、この指定にはユースケースがあります。

しかし一方で、構造化束縛宣言の個々の名前に対して属性を指定する方法はありません。標準属性で使用可能かつ意味があるのは[[maybe_unused]]のみですが、コンパイラベンダなどが提供する多くのアノテーション属性などを考慮すると、それを行いたい動機付けは大きくなる可能性があります。

この提案は、構造化束縛宣言内のそれぞれの名前に対して直接属性指定を行えるように文法を拡張しようとするものです。

提案されている構文は次のようなものです

auto f() -> std::tuple<int, double, char>;

int main() {
  // cにのみ[[maybe_unused]]を指定
  auto [a, b, [[maybe_unused]] c] = f();
}

構造化束縛宣言の[]の中で名前に対して直接属性を指定できるようにしています。最初の名前に指定する場合[[[のように角括弧が連続する可能性はありますが、構文は他の場所での属性指定構文と一貫しています。

P0952R2 A new specification for std::generate_canonical

std::generate_canonicalの仕様を改善する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言に、アルゴリズム指定の式中のRについての注記を追加したことです。

この提案は2023年11月のKona会議で採択され、C++26ドラフトに取り込まれています。

P1028R6 SG14 status_code and standard error object

現在の<sysytem_error>にあるものを置き換える、エラーコード/ステータス伝搬のためのライブラリ機能の提案。

以前の記事を参照

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

  • errc::successerrc::invalidに変更
  • status_code_domainstring_viewから構築するconstexprコンストラクタを追加
  • status_code_domaindo_*()protectedメンバ関数_を削除
  • status_code_domain<=>を追加
  • status_code_domainトリビアルコピー可能ではなくなった
  • 型消去されたステータスコードに対しては、通常のコピーコンストラクタではなくstatus_code(in_place_t, const status_code<void> & v)を使用する様に変更

などです。

P1061R6 Structured Bindings can introduce a Pack

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の変更とより複雑な例の追加などです。

この提案は、現在CWGのレビュー受けています。

P1068R10 Vector API for random number generation

<random>にある既存の分布生成器にイテレータ範囲を乱数で初期化するAPIを追加する提案。

以前の記事を参照

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

  • ADLの代わりに非静的メンバ関数を探索するようにCPOの動作を変更
  • std::ranges::generate_randomが一時オブジェクトなエンジンと分布生成器をサポートする様に変更(フォーワーディングリファレンスの使用による)

などです。

この提案はLEWGのレビューを通過し、LWGへ転送されています。

P1673R13 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更は多岐に渡りますが、LWGのレビューを受けての文言の細かい調整がメインです。

この提案は、2023年11月のKona会議で全体投票を通過し、C++26ドラフトに取り込まれています。

P1708R8 Basic Statistics

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

以前の記事を参照

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

  • コンストラクタは基本的にexplicitではなくした
  • accumulator object.value()メンバ関数を単純化
  • 名前に使用されているstatsstatisticsに変更

などです。

P1709R4 Graph Library

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案。

以前の記事を参照

このリビジョンでは、4年分の経験や検討を反映した大規模な再設計が行われています。その変更は

  • 考慮すべきアルゴリズムの再確認
  • 外向きエッジを持つ隣接リスト、エッジリスト、remove/mutableインターフェースに焦点を絞って、提案の範囲を縮小
  • directed/undirectedコンセプトをグラフ型に対する順序なしエッジのオーバーロード可能な型、に置き換え
  • グラフコンテナ型と関数を単純化
    • 特に、const/非constの変種を1つの定義に統合し、必要に応じて両方の場合を扱える様にした
  • 全てのグラフコンテナインターフェースはカスタマイズポイントとなった
  • NWGraphライブラリの設計からインスピレーションを得たviewを導入し、グラフをトラバースするためのよりシンプルでクリーンなインターフェースを実現し、コンテナインターフェースの設計を簡素化
  • 二部グラフと多部グラフのサポート追加
  • 2つのコンテナ実装を、高性能グラフ処理でよく使用されているデータ構造であるCompressed Sparse Rowに基づく圧縮グラフに置き換え

などです。

このリビジョンでは特に、Boost.Graphの経験を踏まえてC++20で作成されたNWGraphライブラリにおける経験を取り込んでいます。NWGraphでは、範囲の範囲としてのグラフを定義し、その抽象の下でいくつかのグラフアルゴリズムを実装しています。このリビジョンでは、その設計の利点やアルゴリズム実装を取り込むことで以前のAPIを進化させています。

一方、NWGraphには既に使用されている任意のグラフデータ構造を使用可能にするためのAPIが欠けており、この提案では以前のリビジョンからのものを発展させてその部分を補っています。

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

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

以前の記事を参照

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

  • reduce_min_index/reduce_max_indexの戻り値としてstd::optionalを返すAPIの検討
  • CV修飾されていない算術型、をより明確な型のリストに置き換え
  • その他文章と文言の修正

などです。

P1967R12 #embed - a simple, scannable preprocessor-based resource acquisition method

コンパイル時(プリプロセス時)にバイナリデータをインクルードするためのプリプロセッシングディレクティブ#embedの提案。

以前の記事を参照

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

  • embed-element-widthを削除し、適切なCHAR_BITに置き換え
  • マクロ展開の方法を変更することでマクロ展開の問題の解消を図る

などです。

この提案は現在CWGのレビュー中です。

P2022R3 Rangified version of lexicographical_compare_three_way

std::lexicographical_compare_three_wayのRange版を追加する提案。

以前の記事を参照

このリビジョンでの変更は、same_as_any_ofコンセプトを<concepts>に移動したこと、インターフェースを再考したことなどです。

P2264R6 Make assert() macro user friendly for C and C++

P2264R7 Make assert() macro user friendly for C and C++

assertマクロをC++の構文に馴染むように置き換える提案。

R6での変更は、条件式のboolへの変換に際して、スコープ付き列挙型の値の変換を抑制する巧妙なトリックの採用によって<type_traits>への依存等の懸念を解消したことです。

このリビジョンでの変更は、LWGのレビューを受けての文言の修正です。

R5では、assertマクロのオペランドを明示的bool変換することで渡された条件式の評価結果をbool値として取得していました。ただし、そうしてしまうとスコープ付き列挙型の値が渡された場合にもbool値に変換できてしまうためこれを防止するためのトリックが必要だったのですが、そのトリックのためには<type_traits>への依存関係や追加のラップ関数等が必要となり、それが懸念されていました。

この問題に対して、スコープ付き列挙値の変換を防ぎつつ他のものはboolに変換する次のようなトリックがフィードバックとして寄せられました

#define assert(...) ((__VA_ARGS__) ? (void)sizeof(bool(__VA_ARGS__)) : (void)__assert_fail(#__VA_ARGS__, __FILE__, __LINE__))

条件演算子の第一オペランドで、文脈的bool変換によって渡された条件式の結果をbool値に変換しています。文脈的bool変換はifオペランドで行われるのと等価の変換で、暗黙変換であるためスコープ付き列挙値をbool値に変換することができません。

R6ではこのトリックの採用を前提として、明示的bool変換の代わりに文脈的bool変換によって渡された条件式の結果を取得するように文言を修正しています。

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

P2267R1 Library Evolution Policies

C++標準ライブラリ設計のためのポリシーについて検討する提案。

以前の記事を参照

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

  • 以前に行った作業のリストに提案を追加
  • LEWGからのフィードバック(ポリシーを採用することで一貫性が向上し時間が節約される根拠)を追加

などです。

P2308R1 Template parameter initialization

非型テンプレートパラメータの初期化に関しての規定を充実させる提案。

以前の記事を参照

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

  • NTTPの模範となる値の宣言にconstexprを付加
  • テンプレートパラメータオブジェクトの制約と選択を明確化し、模範(exemplar)という用語を削除

などです。

このリビジョンでは、以前に使用されていた模範(exemplar)という用語は削除され、代わりに初期化子候補(candidate initializer)という用語が導入されています(その意味するところは若干異なっていますが)。

P2414R2 Pointer lifetime-end zap proposed solutions

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

以前の記事を参照

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

  • 在野のLIFOプッシュアルゴリズムの調査に基づく更新
    • LIFOプッシュライブラリが次のスタックノードへのポインタに直接アクセスできないという事実について
  • 選択されていないオプションを削除して、特定のソリューションに焦点を当てる
  • ソースコード内の特定の明確にマークされたポインタのみが、実装がポインタの無効性を再検討できる様にするアプローチのみに焦点を当てている

などです。

P2447R6 std::span over an initializer list

std::spaninitializer_listを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、コンストラクタからnoexceptを削除したことです。

この提案は既に、2023年11月のKona会議で全体投票を通過し、C++26WDに取り込まれています。

P2481R2 Forwarding reference to specific type/template

テンプレートパラメータ指定時にconstと値カテゴリを推論可能にする構文の必要性について説明した文書。

このリビジョンでの変更は、EWGにおける投票結果を追記したことと、提案する対象を絞ったことなどです。

EWGにおける投票では、この問題の解決の手段として新しい転送参照(forwarding reference)を求める意見が明確化された様です。

これを受けて、この提案は新しい転送参照として提案する対象を2つに絞っています。

1つは以前から提示されていたT auto&&形式

void f(std::string auto&& a);

template <typename... Ts>
void g(std::tuple<Ts...> auto&& b);

template <typename T>
void h(T auto&& c);

template <typename T>
void i(T auto& d);

もう一つはこの提案で提示されたforward Tの形式

void f(forward std::string a);

template <typename... Ts>
void g(forward std::tuple<Ts...> b);

template <typename T>
void h(forward T c);

template <typename T>
void i(forward T& d);

forward Tの形式T auto&&とほぼ同じように使用でき同じ利点がありますが、concept autoと構文が異なることで異なる動作をすることが明確になる点が改善されています。ただし、他の欠点(decltype()を使用しないと型を取得できない)はそのままであり、またforwardというキーワードが一般的すぎる点などが欠点として追加されます。

P2542R7 views::concat

同じ要素型を持つ異なる型の範囲を連結するRangeファクトリ、views::concatの提案。

以前の記事を参照

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

  • !common_range && random_access_range && sized_rangeのような範囲をサポートしないことを文言に適用
  • const変換コンストラクタを修正
  • 文言の修正

などです。

この提案はLEWGのレビューを終えて、LWGに転送されています。

P2573R1 = delete("should have a reason");

関数のdelete指定にメッセージを付加できるようにする提案。

以前の記事を参照

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

  • C++26をターゲットとした
  • ベースとなるドラフトの更新と、関連する提案の追記
  • 以前の同様の提案であるN4186の投票結果を追記

などです。

P2642R5 Padded mdspan layouts

std::mdspanpadding strideをサポートするためのレイアウト指定クラスを追加する提案。

以前の記事を参照

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

  • 機能テストマクロを削除して、__cpp_lib_submdspanバンプアップするように変更
  • P2630R3(submdspan)の内容を適用
  • 実装経験などを更新

などです。

この提案はLEWGのレビューを終えて、LWGへ転送されています。

P2662R3 Pack Indexing

パラメータパックにインデックスアクセスできるようにする提案。

以前の記事を参照

このリビジョンでの変更はCWGレビューを受けての文言の改善です。

この提案はすでに2023年11月の全体会議で採択され、C++26に取り込まれています。

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

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

以前の記事を参照

このリビジョンでの変更は、std::complex浮動小数点数型の特殊化のみが許可されることを明確にしたこと、real/imagセッターのフリー関数を考慮するオプションを削除しメンバ関数のみを考慮するようにしたことです。

この提案はLEWGのレビューを終えて、LWGへ転送されています。

P2664R5 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

  • gatherscatterをそれぞれgather_fromscatter_toに変更
  • gather_fromscatter_toにマスキングオーバーロードを追加
  • メモリの動作を制御するために、gather_fromscatter_toにフラグを追加
  • gather_fromscatter_toのメリットについて追記
  • simd_splitsimd_catに新しい名前を使用するよう切り替え

などです。

P2717R4 Tool Introspection

P2717R5 Tool Introspection

C++周辺ツールが、Ecosystem ISにどれほど準拠しているのかを互いに通信する手段を標準化する提案。

以前の記事を参照

R4での変更は、ツールにとっての後方互換性の意味についての説明を追加したことです。

このリビジョンでの変更は、機能名の区切り文字(コマンドオプションではなく、返すJSONのキー名)をドッド(.)に変更し、文言をEcosystem ISドラフトにマージしたことなどです。

P2747R1 constexpr placement new

定数式において、placement newの使用を許可する提案。

以前の記事を参照

このリビジョンでは、以前に提案していた3つのことのうち1つ(placement newの定数式での許可)にのみ提案を絞ったことです。

以前のこの提案では

  1. void*からの適切なポインタキャストを定数式で許可する
  2. 定数式でplacement newを許可する
  3. 未初期化オブジェクトの配列の取り扱いの改善

の3つを提案していました。1はP2738R1の採択によって解決され、3は本質的に別の問題であるため分離され他のところで対処されようとしています(P3074R0など)。

そのため、この提案は2の解決にのみ対象を絞っています。

定数式でのplacement newを行う関数としては、std::construct_atが既に存在しています。しかし、この関数による初期化方法は非常に限定されています。

初期化方法 placement new construct_at()
値初期化 new (p) T(args...) std::construct_at(p, args...)
デフォルト初期化 new (p) T できない
リスト初期化 new (p) T{a, b} できない
指示付き初期化 new (p) T{ .a=a, .b=b } できない

また、ライブラリ関数であるためにコピー省略を妨げる問題もあります。

auto get_object() -> T;

void construct_into(T* p) {
  // get_object()の結果をムーブして構築
  std::construct_at(p, get_object());

  // get_object()の結果から直接構築
  :::new (p) T(get_object());
}

placement newが定数式で禁止されていたのはポインタをvoid*で受け取るためほぼなんでもできてしまうためで、それを回避するためにstd::construct_atという限定された機能を持つ関数が導入されていました。

しかし、定数式ではポインタの正しい型を追跡することができ、placement newができることを制限することができます。この能力を使用することでvoid*からのキャスト(static_cast<T*>(static_cast<void*>(p)))も許可されており、定数式でならplacement newを安全に使用することができます。

P2758R1 Emitting messages at compile time

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

以前の記事を参照

このリビジョンでの変更は、P2741R3と重複する内容を削除したことです。

以前のリビジョンでは、static_assertの第二引数に文字列範囲を渡せるようにすることとコンパイル時メッセージ出力のためのライブラリ関数の2つの事を提案していました。前者はP2741がC++26に採択されたことで不要になったため、このリビジョンでは後者のライブラリ機能だけに的を絞っています。

P2760R1 A Plan for C++26 Ranges

C++26に向けての、<ranges>ライブラリ関連作業の予定表。

以前の記事を参照

このリビジョンでの変更は、output_iteratorの改善と並行アルゴリズムサポートについてを優先度1に追加した事です。

output_iteratorの改善とは、T*のように入力イテレータでもある出力イテレータに対して、back_insert_iteratorのように出力だけしかできない出力イテレータを区別することです。それを行うことで、出力イテレータの定義を簡略化するとともに出力動作を効率化する機会を提供できるためです。

出力イテレータは典型的には次のように使用されます

template <typename InputIt, typename OutputIt>
void copy(InputIt first, InputIt last, OutputIt out) {
  for (; first != last; ++first) {
    *out++ = *first;
  }
}

outが出力イテレータですが、これは入力イテレータよりも明らかに必要な操作が少ないことが分かります。これを反映して、back_insert_iteratorを例えば次のように実装できるかもしれません。

template <typename C>
class back_inserter {
  C* cont_;

public:
  explicit back_inserter(C& c) : cont_(&c) { }

  // these do nothing
  auto operator*() -> back_inserter& { return *this; }
  auto operator++() -> back_inserter& { return *this; }
  auto operator++(int) -> back_inserter { return *this; }

  // this one does something
  auto operator=(typename C::value_type const& val) -> back_inserter& {
      cont_->push_back(val);
      return *this;
  }

  // same
  auto operator=(typename C::value_type&& val) -> back_inserter& {
    ...
  }
};

この実装は有効ではありますが、冗長な関数をいくつも記述しなければならないなど面倒な部分が多数あります(本来=のみでいいはず)。

そして何より、出力イテレータの出力が要素を一個づつ出力していくことしか考慮していないことが、出力パフォーマンスを低下させています。入力のサイズが分かっている場合、reserve()したりC++23の範囲挿入関数を使用するなどしてより出力操作を効率化できる可能性があります。しかし、現在の出力イテレータの定義はその機会を提供していません。

出力専用のイテレータを定義することで、出力イテレータの実装簡易化と効率的な出力経路の提供が同時に達成できます。

P2761R0 Slides: If structured binding (P0963R1 presentation)

P0963R1の紹介スライド。

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

EWGIのメンバに向けて、P0963R1で提案されている機能のモチベーションや動作について説明するものです。

P2767R2 flat_map/flat_set omnibus

flat_map/flat_setの仕様にあるいくつかの問題点とその解決策について報告する提案。

以前の記事を参照

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

  • LWGを対象とするものとLEWGを対象とするものに分割し、並べ替えた
  • 1つの提案で一度に解決されるサブセクションを統合
  • 多くの例を前後比較するテーブルに置き換えた
  • コンテナのゼロ初期化についてを追加
    • 内部コンテナを値初期化していることで一部のコンテナでは非効率になる可能性がある

などです。

P2795R4 Erroneous behaviour for uninitialized reads

未初期化変数の読み取りに関して、Erroneous Behaviourという振る舞いの規定を追加する提案。

以前の記事を参照

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

  • 再び、対象をすべての自動変数(一時オブジェクトを含む)に戻した
  • std::bit_castに誤った値(EBとして読み取られた値)を処理するための文言を追加
  • オブジェクト表現がその型に対して有効ではない場合、誤った動作の後で未定義動作が発生する可能性がある事を明確化
  • 関数パラメータに対するオプトアウト属性指定は、関数の最初の宣言に指定する必要があることを明確化
  • 誤った動作が発生する状況を定義するために使用される標準のフレーズを確立するために文言を更新

などです。

この提案は現在CWGのレビュー中です。

P2806R2 do expressions

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

以前の記事を参照

このリビジョンでの変更は、do式からのreturndo return ...からdo_return ...へ変更した(曖昧さを解消するため)ことと生存期間に関するセクションを追加したことなどです。

P2810R2 is_debugger_present is_replaceable

P2810R3 is_debugger_present is_replaceable

P2546で提案されているis_debugger_present()をユーザーが置換可能にする提案。

以前の記事を参照

R2での変更は、2023年9月のLEWGにおける投票結果を追記し、それを反映してユーザー置換関数に事前条件を付加しないように修正した事です。

このリビジョンでの変更は、提案する文言から問題のあった注記を削除した事です。

この提案はLEWGのレビューを終えてLWGに転送されています。

P2819R2 Add tuple protocol to complex

std::complextupleインターフェースを追加する提案。

以前の記事を参照

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

  • 専用の機能テストマクロを削除
  • Hidden friendなget()はtuple-likeでは動作しなかったため、フリー関数に戻した
  • Annex Cの文言を削除

などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2821R5 span.at()

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

以前の記事を参照

このリビジョンでの変更は、余分なフリースタンディングコメントを削除したことです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2826R1 Replacement functions

ある関数を指定したシグネチャでそのオーバーロード集合に追加できるようにする、一種の関数エイリアスの提案。

以前の記事を参照

このリビジョンでの変更はよく分かりませんが、文書の構成を整理していくつか例を追加しているようです。

P2827R1 Floating-point overflow and underflow in from_chars (LWG 3081)

std::from_chars浮動小数点数を変換する際にアンダーフローを検出できるようにする提案。

以前の記事を参照

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

P2830R1 constexpr type comparison

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

以前の記事を参照

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

  • std::type_info::beforeの変更を行わないオプションを追加
  • 匿名名前空間を空にすることができないことを明確化
  • FAQセクションを追加
  • 実用的なサンプルコードを追加
  • 提案する構文を追加
  • appendixを追加

などです。

この提案ではオプションとして次の5つを提示しています

  1. std::type_info::beforeconstexprを付加
    • 利点
      • 新しいトークンが必要ない
      • 発見されやすい
      • 組み込みのtype-erasureであること
    • 欠点
      • ABI破壊
      • 多くの場所で禁止されている<typeinfo>をインクルードする必要がある
      • NTTPに対して使用できない
  2. std::strong_ordertype_info比較のためのオーバーロード追加
    • 利点
    • 欠点
      • <compare><typeinfo>のインクルードが必要になる
      • NTTPに対して使用できない
  3. std::entity_ordering変数テンプレートの導入
    • 利点
      • 新しい名前であり、明確にコンパイル時に利用可能
      • <typeinfo>のインクルードが必要ない
    • 欠点
      • 新しい名前であること
      • 発見されづらい
  4. std::type_identityoperator<=>を追加
    • 利点
      • 若干やることが明白
    • 欠点
      • <type_traits><compare>に依存するようになる
      • NTTPに対して使用できない
  5. ユニバーサルテンプレートパラメータを用いたstd::__lift::operator<=>を導入
    • 利点
      • 強いて言うなら、そのうちこの__liftのようなものが必要になるかもしれない
    • 欠点
      • 何をするか明確ではない
      • 新しい名前であること
      • 発見されづらい
      • 本質的に別のtype_infoを追加している

3つ目のstd::entity_orderingとは次のような変数テンプレートです。

template <universal template T, universal template U>
inline constexpr std::strong_ordering entity_ordering = ORDER(T, U);

すなわち、std::entity_ordering<T, U> < 0のようにしてT, Uの順序付比較が行えます。

P2845R5 Formatting of std::filesystem::path

std::filesystem::pathstd::format()でフォーマット可能にする提案。

以前の記事を参照

このリビジョンでの変更は、「無効なコード単位」をより具体的な「不適格な部分列の最大部分」に置き換えたこと、LEWGでの投票結果を追記したことです。

P2863R3 Review Annex D for C++26

現在非推奨とマークされている機能について、C++26で削除/復帰を検討する提案。

以前の記事を参照

このリビジョンでの変更は、関連提案のステータス更新や、その表記法の変更などです。

P2864R2 Remove Deprecated Arithmetic Conversion on Enumerations From C++26

C++20の一貫比較仕様に伴って非推奨とされた、列挙値から算術型への暗黙変換を削除する提案。

以前の記事を参照

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

  • Kona会議でのEWGのレビューの記録を追加
  • いくつかのセクションの並べ替え
  • 非推奨警告についての調査を更新
  • ベースとなるドラフトの更新
  • Annex Cのテキスト修正

などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2865R4 Remove Deprecated Array Comparisons from C++26

C++20の一貫比較仕様に伴って非推奨とされた、配列間の比較を削除する提案。

以前の記事を参照

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

  • 最新のWDに追随
  • 委員会での進捗を追記
  • C互換性について追記
  • 非推奨警告の調査を更新

などです。

P2868R3 Remove Deprecated std::allocator Typedef From C++26

std::allocatorにある非推奨化された入れ子型定義を削除する提案。

以前の記事を参照

このリビジョンでの変更は、最新のWDに追随したことと、Annex Cの修正などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2869R3 Remove Deprecated shared_ptr Atomic Access APIs From C++26

C++20で非推奨とされた、std::shared_ptrのアトミックフリー関数を削除する提案。

以前の記事を参照

このリビジョンでの変更は全体的な修正などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2870R3 Remove basic_string::reserve() From C++26

C++20で非推奨とされたstd::string::reserve()C++26に向けて削除する提案。

以前の記事を参照

このリビジョンでの変更は、最新のWDに追随したことと、Annex Cの修正などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2871R3 Remove Deprecated Unicode Conversion Facets From C++26

C++17で非推奨とされた<codecvt>ヘッダをC++26で削除する提案。

以前の記事を参照

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

  • 最新のWDに追随した
  • 互換性の文言を更新し、ヘッダユニットのインポートについて言及
  • C++03との差分にある<codecvt>ヘッダ周りの参照を削除

などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2878R6 Reference checking

プログラマが明示的に関数の戻り値に関するライフタイム注釈を行えるようにする提案。

以前の記事を参照

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

などです。

P2890R1 Contracts on lambdas

P2890R2 Contracts on lambdas

ラムダ式に対する契約条件指定ができるようにする提案。

以前の記事を参照

R1での変更は、キャプチャの問題を解決するための別のオプションを追加したことです。

このリビジョンでの変更は、無意味な名前探索ルールを修正したことです。

ラムダ式で契約注釈を使用可能にする際に最も問題となるのは、デフォルトキャプチャ使用時に契約注釈内部だけで使用されている外部エンティティを暗黙的にキャプチャするかどうかです。

constexpr auto f(int i) {
  return sizeof( [=] [[pre: i > 0]] {});  // 戻り値はどうなる?
}

constexpr auto g(int i) {
  return sizeof( [=] { [[assume (i > 0)]]; } ); // 戻り値はsizeof(int)
}

R0では契約注釈だけで使用されているラムダ式外部の変数はデフォルトキャプチャによるキャプチャの対象とすることを提案していました。これについて、R1では全部で7つのオプションが提示されています

  1. 契約述語はキャプチャをトリガーできる
    • assume属性における同様の議論では、そのようなキャプチャをill-formedにするために言語の複雑化を正当化するには実際のコードで問題が起きる可能性が低い(エッジケースである)として、キャプチャをトリガーすることを維持した
    • 契約注釈にも同じことが言える
  2. 契約述語はキャプチャをトリガーしない
    • P2932R2で提案されている
    • 単にキャプチャをトリガーしない、とするだけだと契約注釈で間違った変数を使用してしまう可能性がある(ローカル変数と同名のグローバル変数を暗黙的に使ってしまう)
  3. 契約述語がキャプチャをトリガーする場合ill-formed
    • オプション2の問題をカバーしたもの
    • 言語が複雑化し、ユーザーにとって自明ではないかもしれない
    • assume属性と矛盾する
  4. 契約述語がキャプチャをトリガーする場合警告する
    • 契約注釈からキャプチャをトリガーするコードは常に疑わしいコードであり、警告するならill-formed(オプション3)の方が望ましい
  5. 契約述語内で他の方法でodr-usedされないエンティティをodr-usedすることはill-formed
  6. デフォルトキャプチャを使用したラムダ式で契約注釈指定を許可しない
    • ユーザにとってあまりに多くの価値を奪うものであり、思い切りが良すぎる
  7. ラムダ式で契約注釈の使用を許可しない
    • 6と同様

この提案では、この中で実行可能な選択肢は1か3のどちらかであるとしています。なお、現在のContracts MVP仕様としては2が採用されています(別の提案によるもの)。

P2894R1 Constant evaluation of Contracts

定数式においても契約チェックを有効化する提案。

以前の記事を参照

R0では、コンパイル時に契約チェックするかどうか(及び定数式で実行できない契約条件の扱いをどうするか)について、グローバルなスイッチによって一律的に制御するようなことを提案していましたが、翻訳単位によってその設定が異なる可能性があることを指摘されました。それによって再検討が行われ、このリビジョンでは結局実行時と同様に、全ての契約注釈のセマンティクスは実装によって定義され、注釈ごと、その評価ごとに変化しうる、というモデルを採用することになりました。

以前はコンパイル時にセマンティクスを区別する必要はない、としていましたが、実装が定数評価中にそのセマンティクスを選択する以上3種類のセマンティクスが定数評価中にどのように動作するかを指定する必要があります。

この提案での検討対象はR0と同様に次の場合のみです

  1. 契約条件式はコア定数式ではない
  2. 契約条件式はコア定数式だが、falseに評価された(契約が破られた) trueにもfalseにも評価されない場合は全て、コア定数式ではない

まず1の場合、契約注釈を持つ関数が定数式で呼ばれるまではその契約条件式の定数実行可能性を気にしないことはR0と同様です。定数実行できない契約注釈を持つ関数が定数式で呼ばれてしまった時が問題になります。

その場合、R0では実装定義(コンパイラオプション等で指定)としていました。このリビジョンでは、契約違反が起きたものとして扱うことを提案しています。すなわち、コンパイル時に呼び出すことのできない事前・事後条件(及びアサーション)はコンパイル時には決して満たされることはないと言うことであり、それは契約違反となります。契約違反時の動作は契約注釈のセマンティクスによって指定されます。

2の場合とはつまり契約違反が起きたと言うことであり、その動作はセマンティクスごとに指定され、次のようになります

  • ignore, observe : 診断を発行(エラーにならない)
  • enforce : 診断を発行しill-formed(エラーになる)

そして、定数式における契約注釈のセマンティクスは契約注釈及びその評価ごとに実装定義(オプション等によって指定)となります。

また、定数初期化されうる変数の初期化式などの定数式で評価されるかどうかが決定的でない文脈においての契約注釈の評価については、上記のようなセマンティクス適用以前に、その契約注釈を含む式が定数評価可能かを契約注釈を無視してテストして、定数評価可能ならば再度契約セマンティクスを有効化した上で初期化式の評価を行うようにすることを提案しています。

P2900R2 Contracts for C++

P2900R3 Contracts for C++

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

以前の記事を参照

R2での変更は

  • 構文としてnatural syntaxを採用
    • P2961R2を採択
  • default関数に対する事前/事後条件の指定はill-formed
    • P2932R2を採択

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

  • delete関数に対する事前/事後条件の指定はill-formed
  • ラムダ式に対する事前/事後条件の指定を許可
  • 契約注釈は暗黙のラムダキャプチャをトリガーしない
    • P2932R2を採択
  • std::contracts::invoke_default_contract_violation_handler()(デフォルトの違反ハンドラを呼び出す関数)を追加
  • 契約条件式から参照されるローカルエンティティは暗黙的にconstとする
    • P3071R1を採択
  • 事後条件における戻り値名のセマンティクスを明確化
    • P3007R0を採択し、戻り値名は暗黙的にconstとする
  • Overviewセクションを追加
  • Recursive contract violationsセクションを追加

などです。

P2909R3 Fix formatting of code units as integers (Dude, where's my char?)

P2909R4 Fix formatting of code units as integers (Dude, where's my char?)

std::format()charを整数値としてフォーマットする際の挙動を改善する提案。

以前の記事を参照

R3での変更は

  • LEWGでの投票結果を追加

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

  • 機能テストマクロ__cpp_lib_format__cpp_lib_format_ucharに置き換え
  • wchar_tへの対応を改善するために文言を調整

などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2918R2 Runtime format strings II

std::format()の実行時フォーマット文字列のためのAPIを追加する提案。

以前の記事を参照

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

  • 機能テストマクロ__cpp_lib_formatバンプするようにした
  • runtime-format-stringに説明専用メンバstrを追加
  • runtime-format-stringにコンストラクタを追加
  • この提案で追加される関数にはnoexceptを付加する
  • runtime-format-stringを固定化

などです。

この提案は、2023年11月のKona会議で採択され、C++26WDにマージされています。

P2932R2 A Principled Approach to Open Design Questions for Contracts

契約機能に関する未解決の問題についての設計原則に基づく解決策の提案。

以前の記事を参照

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

  • セクションをいくつか追加
  • 定数式の処理セクション、observeセマンティクスを持つ契約注釈の実行時以外の評価についてのセクションを明確化
  • 提案1.Bをトリビアルな特殊メンバ関数に適用するように修正し、1.Cを追加
  • natural syntaxを使用するように書き換え

などです。

提案1.Cはトリビアルでないものも含めてdefaultで定義された関数に対して契約注釈を行えるのかどうかについてのもので、次のようなものです

  • default関数が、その最初の宣言で事前条件/事後条件を持つ場合、ill-formed

これは、defaultとはインターフェースなのか実装なのか、それに関して契約注釈はどうあるべきかなどについての議論を行うために、C++26に対してはとりあえず禁止しておくことを意図したものです。C++26 Contracts仕様にはこれが採用されています。

P2933R1 std::simd overloads for <bit> header

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

以前の記事を参照

このリビジョンでの変更は、文言も含めた全体のテキストの修正などです。

P2935R4 An Attribute-Like Syntax for Contracts

C++契約プログラミングのための構文として属性構文を推奨する提案。

以前の記事を参照

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

  • [[assert : expr]]を式にした
  • 事後条件における戻り値命名の代替案の追加

などです。

2023年11月のKona会議において、C++契約プログラミングのための構文としてはこちらではなくP2961R2のnatural syntaxを採用することが決定されました。

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

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

以前の記事を参照

このリビジョンでの変更は、void戻り値型の指定とそのトリビアル性(この提案では許可しない)についての議論を追加したことです。

P2961R2 A natural syntax for Contracts

契約プログラミング機能のための構文の提案。

以前の記事を参照

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

  • アサートのキーワードとしてcontract_assertを採用
  • assertが契約におけるアサートのキーワードとしてふさわしくない理由を追記
  • 実装経験について追記
  • SG22での議論をもとに、Cでの実現可能性について追記

などです。

この提案は2023年11月のKona会議でC++26の契約プログラミング機能のための構文として採用されました。

P2968R1 Make std::ignore a first-class object

P2968R2 Make std::ignore a first-class object

std::ignoreを戻り値破棄のために合法的に使用できるようにする提案。

以前の記事を参照

R1での変更は

  • ビットフィールドについて言及し、それがstd::ignoreの代入にとって有効な右辺オペランドではないことを説明
  • 右辺オペランドとしてnon-volatileビットフィールドがサポートされるべきかの質問を追加
  • 既存の主要な実装の分析を含むフィードバック提案を組み込み
  • volatileビットフィールドによる副作用を防ぐために、ビットフィールドを右辺オペランドとして指定しないoperator=(auto&&)を使用してコードとして仕様を推奨

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

  • 個別のヘッダにしないことを決定
  • std::ignoreの代入演算子シグネチャとしてoperator=(auto const &) const noexceptを使用する
    • auto&& -> const auto&に変更し、左辺値参照修飾を削除

などです。

std::ignoreの代入演算子はあらゆる値を受けることができ、ほとんどの場合に元の値の読み取り(すなわちコピー)は発生しません。しかし、その例外がビットフィールドであり、ビットフィールドを渡そうとすると対応する整数型の一時オブジェクトを生成して渡す形になり、ビットフィールドの値へのアクセスが発生します。これが特に問題となるのはそのビットフィールドがvolatileである場合で、volatileの場合はその値へのアクセスを消し去ることができず、std::ignoreの代入演算子が何もしないという効果を達成できなくなります。

std::ignoreの代入演算子の規定としてどんな値でも受け取り何もしないと言う風に指定する代わりに、コードとして代入演算子シグネチャを指定し引数型を制限することでこの問題は解決できます。その際に、引数型をauto&&(ビットフィールド禁止)にするのかcosnt auto&volatileビットフィールドを受けられない)にするのかによってビットフィールドを許可するか、あるいは非volatileビットフィールドを許可するかが分岐します。

このリビジョンでは、cosnt auto&にすることでstd::ignoreではビットフィールドを許可するもののvolatileビットフィールドはサポートしないことにしています。

この提案はすでにLEWGのレビューを終えてLWGに転送されています。

P2969R0 Contract annotations are potentially-throwing

contract_assert式に対するnoexcept演算子の振る舞いについての解決策を探る提案。

C++26 Contractsのセマンティクスとして、特に例外に関する部分についてSG21では次のことが合意されています

  • 契約条件式の評価に伴って例外がスローされる場合は契約違反として扱われ、関数の例外仕様とは無関係に違反ハンドラを呼び出す
  • 違反ハンドラからの例外送出は、通常の例外と同じ扱いとなる
    • スタック巻き戻しが発生し、巻き戻しがnoexcept関数呼び出しに及んだ場合はstd::terminate()によってプログラムは終了する
  • 契約注釈がチェックされているかどうかによって、noexcept演算子の結果や契約注釈が付加されている関数の型が変化することはない
  • 事前条件・事後条件・アサーションは全て、述語または違反ハンドラからの例外送出に関して同じように動作する

これによって、チェックされると常に例外を送出する、Well-definedな動作をもつWell-formedなC++プログラムが存在し得ます

#include <contracts>
using namespace std::contracts;

void handle_contract_violation(const contract_violation&) {
  throw 666;
}

int main() {
  contract_assert(false); // チェックされると必ず例外を送出する
}

しかもこのことは、契約違反ハンドラがnoexceptであることがわかっている場合にのみコンパイル時にわかります(違反ハンドラの置換は一般的にリンク時に行われるため、コンパイル時には分からないでしょう)。従って、違反ハンドラがどの様にカスタマイズされているか(あるいはされていないか)によらず、ユーザーから見た契約注釈は常に例外を投げうるものとして写ります。

現在のC++にはプログラムのコンパイル時のセマンティクスが特定の式が例外送出するかどうかに依存する場合があります。それは、noexcept演算子の結果を決定する時、最初の宣言がdefaultである特殊メンバ関数の例外仕様を決定する場合、の2つの場合です。

2023年11月のKona会議において、クラスの特殊メンバ関数の最初の宣言がdefaultである場合、契約注釈を付加できないことが決定されました。しかし、それでもなお、この2つの場合のそれぞれについて契約注釈の有無によって結果が変化しうる場合が存在し、現在のMVP仕様ではその状況について何も指定していません。

1つは、契約注釈そのものにnoexcept演算子を適用した場合で、これはcontract_assert()でのみ起こります。

noexcept(contract_assert(false)); // これはtrueになる?falseになる?

2つめは、クラスの特殊メンバ関数の例外仕様が推定される場合で、通常この場合はその関数が例外を投げうるものを含んでいなければnoexcept(true)と推定されます。現在その様な場所に直接契約注釈を付加することはできませんが、間接的に現れる可能性があります

struct B {
  int i = (contract_assert(false), 42);
};

struct S : B {
  // これらのコンストラクタのnoexcept性はどうなる?
  S() = default;
  S(S&&) = default;
};

この様なコンテキストは現在この2つだけのはずですが、将来の機能(例えば、noexcept(auto))を考慮するとさらに増加する可能性があります。

この提案は、C++26に契約機能を導入する前にこれらの仕様の空白を埋めるために可能な解決策を探るものです。

この提案で上げられている解決策は次の7つです

  1. 契約注釈を例外を送出しうるものとして扱う
    • 上記どちらも、noexcept(false)となる
    • ゼロオーバーヘッド原則に反する(契約注釈の追加によりムーブコンストラクタの例外仕様が変化するなど)。これは契約機能の採用の阻害要因となりうる
  2. 契約注釈を例外を送出しないものとして扱う
    • 上記どちらも、noexcept(true)となる
    • 契約注釈が存在する場合にnoexcept演算子が嘘をつくことなる
    • 例外中立なライブラリフレームワーク(P2300の様なもの)の記述が困難となる
  3. 1と2の両方の状況を許可する
    • 契約注釈のメタアノテーションコンパイラオプションによって契約注釈が例外を投げうるかを指定する
    • ユーザーが契約注釈にnoexceptメタアノテーションを付加する場合、単純に関数全体にnoexceptを付加するだけで良い
    • ビルドモードを導入するとC++の方言が生まれる
  4. 謝って送出された例外が推定されたnoexcept(true)例外仕様を回避できる様にする
    • 契約注釈は言語内で例外を投げないものとして扱われるが、違反ハンドラからスローされた例外のみnoexcept(true)な関数のスタック巻き戻しを行うことが許可される
    • 呼び出しが例外を投げないことを前提とする処理でこれが起こると、結局プログラムの実行は安全ではなくなる
    • 例外仕様のメンタルモデルが複雑化し言語内に矛盾が生じ、仕様と実装が複雑化する
  5. これらの場合が現れない様にする
    • 次の3つの方法が考えられる
      1. contract_assertを式ではなく文にする
      2. noexcept演算子contract_assertまたはcontract_assertを部分式として含む式に適用するとill-formed
        • さらに、例外仕様に影響を与えうる箇所にcontract_assertが出現するとill-formed
      3. noexcept演算子contract_assertまたはcontract_assertを部分式として含む式に適用するとill-formed
        • ただし、例外仕様に影響を与えうる箇所にあるcontract_assertがill-formedとなるのはその存在が例外仕様の推定に影響を与える場合のみ
    • 仕様や実装が複雑になり、ユーザーにより多くの苦労を強いることになるが、上記問題を回避し全ての場合にゼロオーバーヘッド原則を満たす
  6. ガイドラインと診断に頼る
    • ユーザーにその様なコードを書かないほうがいいと警告するならば、単にill-formedとしたほうがいい
  7. 違反ハンドラからの例外送出サポートを禁止する
    • 違反ハンドラからの例外送出を使用するのは実際には非常に難しいため、これを削除し例外を使用しない方法で同等の契約違反後の処理継続を可能にする方法を模索する
    • それを決定するとこの提案で挙げられている問題は全て解決されるが、後から違反ハンドラからの例外送出を許可するのは困難になる
    • 現時点では例外に代わる同等のメカニズムは存在していない

この提案では、それぞれの解決策による影響や実現可能性について考察していますが、どれかを推しているわけではありません。ただし、3,4の案は実現可能ではないと評されており、1,2,6の案は問題があり、7の案も困難が伴うとして、一番ポジティブな評価を受けているのは5(の2,3)の案です。

この提案でどの様な決定がなされたとしても事前・事後条件における違反ハンドラからの例外送出について同様の問題がある様に見えますが、それはすでに合意済みのことです。契約注釈の存在は例外仕様等のコンパイル時セマンティクスに影響を与えず、Lakos Ruleに基づいて契約注釈がなされている限り(無条件noexcept指定と契約注釈の両立は誤っている)それは問題になりません。例えば、あるクラスのムーブコンストラクタがnoexceptとなる場合、そのムーブコンストラクタは広い契約を持ちいかなる事前・事後条件も持たないはずで、そのクラスのメンバのムーブコンストラクタそれぞれについても同様の仮定が成立するはずです。この過程が成り立っていない場合、そのムーブコンストラクタはnoexcept(false)とするのが適切であり、その場合にのみ契約注釈によって事前・事後条件をチェックすることができます。

P2977R0 Module commands database format

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

ビルドシステムのようなC++プロジェクトのビルドを管理するツールは、それをコンパイルしたコンパイラとは異なるコンパイラを使用するプロジェクトにおいてもモジュールを管理しなければなりません。しかし、一般的にビルド済みモジュールはコンパイラ間や同じコンパイラのバージョン間、あるいはモジュールを分析する必要のあるツール(静的解析ツールなど)等それぞれの間で互換性がありません。これらのツールはメインのビルドの設定に対応する形で、それぞれ独自の形式によってモジュール(のインターフェース)をビルドできる必要があり、メインのビルドと(形式等は除いて)同等のコンパイル済みモジュールを生成するために必要なフラグなどを知る必要があります。

この提案は、そのような情報をファイルに保存してツール間でやり取りすることを目的として、どのような情報が必要かやその保存の方法などを特定しようとするものです。

メインビルドと同等のコンパイル済みモジュールを作成するためには、ツールは少なくとも次の情報を知っている必要があります

  • モジュールインターフェースのソース
  • 生成されるモジュールのモジュール名
    • そのモジュールが使用される場所を特定するために必要
  • 必要なモジュールの名前(依存モジュール名)
    • モジュールの依存関係を解決するために必要
  • 必要なモジュールを提供するソースコード
  • ソース内でのモジュールの可視性
    • モジュール実装単位/実装パーティションのように、プライベートなモジュールが存在する
    • そのようなモジュールに属するシンボルは外部からアクセスできない場合があり、より的確な診断のためにモジュール名を使用できる
  • ソースを処理するビルド中に使用されるローカルプリプロセッサ引数
    • メインビルドの成果物に対応するものを生成するために使用する必要がある
    • ツールは、メインビルドに使用されるコンパイラのフラグを自身の利用のために変換する必要がある可能性がある
  • コンパイルのための作業ディレクト

これらの情報をビルドツリー内で表現する方法としては次の3つが挙げられています

  1. スタンドアロン
    • 全ての情報を何らかのファイルに保存する
    • (おそらくソースファイルごとに)別々のファイルとして存在している
    • この利点は、情報をビルドだけでなくインストールにも使用できる点
  2. コンパイルコマンドデータベースとの相互参照
    • Compile Commands Databaseと手元の情報とを組み合わせて使用する
    • 重複を減らすことができるが、必要な情報を全て取得するには2つのデータベースを手動でマージするツールが必要になる
    • この欠点は、あるソースが異なるフラグでビルドグラフ上に複数回現れうる点
  3. コンパイルコマンドデータベースとの共有
    • 手元の情報をコンパイルコマンドデータベースと共有できる部分とそうでない部分に分割し、共有できる部分はコンパイルコマンドデータベースのものを取得する

このようなモジュールコンパイルデータベースは、一般的にビルド中に作成される必要があります。なぜなら、モジュール名はビルドが完了するまでわからないからです。しかし、上記コンパイルコマンドデータベースがビルドの一部が完了するまで存在しない生成されるソースのコンパイルを参照することができ、新しい問題というわけではありません。

メインビルドに関わらない他のツールがこれらの情報を簡易に取得するために、ビルドシステムはモジュールコンパイルデータベースをよく知られた場所(おそらく、pkgconfigなどのようなものがある場所)にあるファイルにまとめる仕組みを提供する必要があります。これによって、他のツールが関連ファイルを探索する手間が軽減され、ファイル全体にわたって情報が一貫していることが保証されます。

この提案はこのような要件を列挙したもののようで、まだ何かを提案するには至っていないようです。

P2980R1 A motivation, scope, and plan for a quantities and units library

物理量と単位を扱うライブラリ機能の導入について説明する提案。

以前の記事を参照

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

  • ガーディアン紙の華氏の取り扱いに関する問題について追加
  • ScopeとPlan for standardizationを更新
  • タイトルからPhysicalを取り除いた

などです。

P2981R1 Improving our safety with a physical quantities and units library

↑の物理量と単位を扱うライブラリ機能について、コンパイル時の安全性に関する側面を解説する文書。

以前の記事を参照

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

  • ガーディアン紙の華氏の取り扱いに関する問題について追加
  • 燃料消費の例を拡張
  • いくつかの参考リンクを追加
  • 型を保持する型特製についてLack of safe numeric typesで説明
  • Non-negative quantitiesを書き直し
  • Limitations of systems of quantitiesセクションを追加
  • Temperaturesセクションを追加

などです。

P2982R1 std::quantity as a numeric type

↑の物理量と単位を扱うライブラリ機能について、数値と計算の側面に関する設計の説明をする文書。

以前の記事を参照

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

  • 燃料消費の例を拡張
  • Dimension is not enough to describe a quantityセクションを拡張
  • Lack of convertibility from fundamental typesセクションを追加
  • Terms and definitionsセクションにmp-unitsの用語集へのリンクを追加
  • Equivalenceセクションのサンプルコードを修正

などです。

P2984R1 Reconsider Redeclaring static constexpr Data Members

static constexprメンバ変数のクラス外定義の非推奨の扱いを検討する提案。

以前の記事を参照

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

  • コンパイラの警告についての状況を追加
  • 2023年11月のKona会議におけるレビュー結果について追加
  • conclusionセクションを追加

などです。

EWGのレビューにおいては、static constexprメンバ変数のクラス外定義を削除することが好まれた様でしたが、一方で現在十分に非推奨動作であることが警告されていない状況でいきなり削除してしまう事は好まれなかった様です。そのため、C++26に対してはこの点に関して変更を加えず、コンパイラベンダにはこの問題についての警告をよりユーザーに見える様にすることを推奨する、という方向性が支持されました。

P2996R1 Reflection for C++26

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

以前の記事を参照

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

  • 全てのサンプルにCompiler Explorerでの動作例リンクを付加
  • synth_structdefine_classに再指定
  • いくつかのメタ関数を関数テンプレートの代わりに関数として再指定
  • エラー処理メカニズムと例外の優先設定に関するセクションを追加
  • チケットカウンタとバリエーションの例を追加
  • entity_refpointer_to_membervalue_ofで表現する様に置き換え

などです。

静的リフレクションにおけるエラーとは例えば、std::meta::template_of(^int)のような間違ったコード(template_of(^T)は型Tのテンプレートの鏡像を取得する。intはテンプレートの特殊化ではないのでエラー)が何を返すか?という問題です。

この候補としては現時点で次のものが考えられます

  1. 静的リフレクション過程のエラーは定数式ではない(コンパイルエラー)
  2. 無効値(エラーであること、およびソースの場所とその他有用な情報)を保持する鏡像を返す
    • P1240の推奨アプローチ
  3. std::expected<std::meta::info, E>を返す。Eはソースの場所とその他有用な情報を提供する
  4. Eを例外としてスローする
    • 定数式で例外を許可する必要がある。おそらく、定数式中でキャッチされない例外のみコンパイルエラーとなる様にする

2つ目の方法の欠点は、メタ関数が範囲を返す場合(template_arguments_of(^int)の様な場合)に何を返すかという点です。この場合に単一の無効な鏡像値を返すと正常なものが帰る場合との一貫性がなくなり、使いづらくなります。かといって、空の範囲を返してしまうとエラー情報が失われてしまいます。結局、何を返したとしても、expectedもしくは例外のアプローチの方がより一貫性があり簡単なエラー処理を提供します。

例外にはいくつかの懸念がありますが、ここでの文脈は定数式であるため、実行時における例外の問題点はここでは問題になりません。その上で次のようなよく使用されると思われる様な例を考慮すると

template <typename T>
  requires (template_of(^T) == ^std::optional)
void foo();
  • template_of(^T)expected<info, E>を返す場合、foo<int>は置換失敗
    • expected<info, E>infoは等値比較可能であり、結果はfalseとなり制約を満たさなくなる
  • template_of(^T)が例外を送出する場合、foo<int>から送出された例外がキャッチされない場合はコンパイルエラー
    • これは置換失敗ではなく、制約が定数式ではなくなることによるエラー
    • 置換失敗にする場合、まず制約でTがテンプレートであることを確認するか、制約が定数式であることを要求する言語の規定を変更する

これらのことを考慮し、例外の懸念を考慮しても、この提案では静的リフレクションにおけるエラー伝搬にはexpectedよりも例外の方がユーザーフレンドリーであるとしています。

P2999R1 Sender Algorithm Customization

P2999R2 Sender Algorithm Customization

P2999R3 Sender Algorithm Customization

P2300のsenderアルゴリズムがカスタマイズを見つける手段を修正する提案。

以前の記事を参照

R1での変更は、提案する文言を追加したこと、比較テーブルを追加したことなどです。

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

  • 早期のカスタマイズと後からのカスタマイズの例を追加
  • 早期のカスタマイズを残す理由について追記
  • connect時のドメイン計算を修正
  • senderの要件に完了schedulerが全てドメインを共有するという要件を追加
  • transform_senderは必要に応じて再帰することを明確化
  • 不要になっていた説明専用のmake-transformer-fnを削除

などです。

P3006R0 Launder less

バイト配列上に構築した別の型のオブジェクトへのポインタをより直感的に取得できる様にする提案。

バイト配列を確保して、その配列の要素型とは異なる型のオブジェクトをその配列上に構築する場合、そのオブジェクトへの正しいアクセス方法は非常に限定されています。

// Tのオブジェクトを配置するバイト配列
alignas(T) std::byte storage[sizeof(T)];

// Tのオブジェクト構築、戻り値のポインタをそのアクセスに使用する
T* ptr1 = ::new (&storage) T(); // ok

// storageのアドレスから直接Tのオブジェクトへのアドレスを得る
T* ptr2 = reinterpret_cast<T*>(&storage);  // UB
T* ptr3 = std::launder(reinterpret_cast<T*>(&storage));  // ok

この場合、配置newによってstorage上に構築されているオブジェクトへのアクセスを行えるのはptr1ptr3のみであり、storageのアドレスからreinterpret_cast<T*>しただけのptr2からのアクセスは未定義動作となります。

これはポインタとその領域で生存期間内にあるオブジェクトとが結びつかないためで、配置newの戻り値を使用しない場合はptr3のようにstd::launderによってその領域上で生存期間内にあるオブジェクトへのポインタを取得しなければなりません。

とは言えこのことはかなり意味論的なことで、多くのコンパイラptr2の様にstd::launderを使用しない場合でもstd::launderを使用する場合と同等のコードを出力する様です。

この提案は、その様な既存の慣行を標準化し、このようなユーザーの期待に沿わないUBを取り除こうとするものです。

これによるメリットは、現在UBを回避するために配置newの戻り値を保存している様なコード(例えば、Boost.Optionalなど)において、その様なポインタを保存しなくてもよくなることでストレージサイズを削減でき、なおかつstd::launderというよく分からないものを使用せずとも直感的なコードによってUBを回避して意図した動作を実現できる点です。

P3007R0 Return object semantics in postconditions

事後条件の契約注釈で戻り値を参照する場合の、その戻り値のセマンティクスに関する提案。

事後条件注釈では、その関数の戻り値を使用するために戻り値の名前を指定してそれを参照することができます。

int f()
  post (r: r > 0);  // 戻り値は正の値

現在のMVP仕様では、まだこの戻り値に関するセマンティクスが正確に指定されていません。

C++20時点の仕様及びそれを受け継いだMVP仕様においては、「事後条件では、その関数のglvalueもしくはprvalueの結果オブジェクトを表す識別子を導入できる」とだけ言及されていて、その値カテゴリは何か、それは参照なのか、その型は何か、アドレスを取れるのか、変更可能なのか、などについての規定はありません。

この提案は、事後条件の戻り値のセマンティクスについての現在の不明な点について考察し、そのセマンティクスを決定しようとするものです。ここで提案されていることは次のような事です

  • 事後条件における戻り値を表す変数名は、本物の戻り値オブジェクトを参照する左辺値である、とする
    • これは構造化束縛によって導入される名前のセマンティクスからの借用
    • その値カテゴリは左辺値(lvalue)
    • 言語参照(T&)ではないが、戻り値を参照している
    • 従って、事後条件から戻り値を参照する場合にはコピーもムーブも行われず、RVOを妨げない
  • 戻り値名rに対するdecltype(r)の結果は、関数の戻り値型
  • 戻り値型がtrivially copyableである場合、戻り値名のアドレスと実際の戻り値のアドレスは異なる可能性がある
    • これは、trivially copyableオブジェクトをレジスタに配置して返す挙動を変化させない(ABI破壊を回避する)ため
    • その場合、戻り値名は戻り値を保持する一時オブジェクトを参照している
  • 呼び出し側で戻り値を受ける変数がconstである場合、関数の戻り値型がconstでなければ、事後条件における戻り値の変更は未定義動作とならない、とする
    • 変数に対するconstは初期化が完了するまで有効ではなく、事後条件は戻り値を受ける変数の初期化よりも前に呼び出される

この提案では、事後条件における戻り値名が暗黙constであるかは著者の間で合意できなかったことから提案していません。それには利点と欠点がありますがどちらを採用するべきかは明確ではないため、その方向性の決定は委員会に委ねています。

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

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

以前の記事を参照

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

などです。

P3019R1 Vocabulary Types for Composite Class Design

P3019R2 Vocabulary Types for Composite Class Design

P3019R3 Vocabulary Types for Composite Class Design

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

以前の記事を参照

R1での変更は

  • 機能テストマクロの追加
  • std::indirectstd::formatサポートを追加
  • 使用前後の比較サンプルをまとめたAppendix Bを追加
  • 型が値を持つことを事前条件として追加
  • constexprサポートを追加
  • std::polymorphicのQoIとしてsmall buffer optimizationを許可
  • アロケータサポートのために文言を追加
  • 不完全型のサポートを有効化
  • pointer入れ子型はallocator_traits::pointerを使用する様に変更
  • std::uses_allocator特殊化を削除
  • std::indirectコンストラクタのinplace_tを削除
  • sizeofエラーを削除

R2での変更は

  • std::indirect比較演算子の戻り値型がautoであることについての議論を追加
  • emplace()の議論をappendixに追加
  • allocator awarenessサポートのために文言調整

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

  • コンストラクタにexplicitを追加
  • indirect(U&& u, Us&&... us)コンストラクオーバーロードと制約を追加
  • polymorphic(allocator_arg_t, const Allocator& alloc)コンストラクオーバーロードを追加
  • std::variantとの類似/相違点についての議論を追加
  • 破壊的変更とそうではない変更の表を追加
  • 不足している比較演算子を追加し、それらが条件付きnoexceptであることを確認
  • std::indirectの推論補助を修正
  • 複雑な例におけるstd::indirectの間違った使用例を修正
  • swap()noexceptに関する文言を修正
  • std::indirectの比較演算子の制約に関する文言についての問題を解決
  • コピーコンストラクタはallocator_traits::select_on_container_copy_constructionを使用する様にした
  • 自己swapと自己代入が問題とならないことを確認
  • std::optional特殊化を削除
  • erroneousの使用をundefined behaviourで置き換え
  • コピー代入における強い例外保証を追加
  • コンストラクタにおいて、Tのuses-allocator構築を行う様に指定
  • 文言の見直し

などです。

P3022R1 A Boring Thread Attributes Interface

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

以前の記事を参照

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

  • P2019R4の変更に追随
  • 文言と例を追加
  • Conclusionセクションを追加

などです。

この提案の方向性はLEWGでの指示が得られなかったため、追及は停止されています。

P3023R1 C++ Should Be C++

C++標準化委員会の目標・見通しについて問い直す文章。

以前の記事を参照

R0が主張をリストアップしたものだったのに対して、こちらはそれを文章にまとめた形になっている完成版です。

P3024R0 Interface Directions for std::simd

C++26に向けて提案中のstd::simdのインターフェースやその設計動機などについて紹介するスライド。

P3025R0 SG14: Low Latency/Games/Embedded/Financial trading/Simulation virtual Minutes to 2023/09/12

SG14の2023年9月12日に行われたオンラインミーティングの議事録。

P3026R0 SG19: Machine Learning virtual Meeting Minutes to 2023/07/13

SG19の2023年7月13日に行われたオンラインミーティングの議事録。

P3027R0 UFCS is a breaking change, of the absolutely worst kind

P3021のUFCS(Unified function call syntax)提案に異議を唱える提案。

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

P3021では、メンバ関数呼び出しを拡張する形でUFCSを有効化することを提案しており、メンバ関数呼び出し(x.f(a,b)x->f(a, b))を非メンバ関数の呼び出し(f(x, a, b)f(*x, a, b))にフォールバックすることでUFCSを可能にするものです。

P3021のUFCSは確かに既存のコードを壊しません(有効なコードがill-formedにならない)が、現在のメンバ関数呼び出しに関してユーザーが依存している保証を壊している、というのがこの提案の主張です。

その保証とは、メンバ関数呼び出しはADLとは無関係に行われるというものであり、別の言い方をするとメンバ関数呼び出しではADLは行われないという保証です。このため、確かに現在ある既存のコードが壊れることはありませんが、将来のコードは壊れる可能性があります。

例えばリファクタリングにおいて誰もが経験すると思われる、メンバ関数の名前を変更する場合を考えます。

struct Foo {
  // この関数名を変えたい
  void snap();
};

// この.snap()は例えばこのように呼び出されている
template <class T>
void do_snap(T&& f) {
  f.snap(); 
}

このFoo::snap()の名前をslap()に変更しようとする場合、これを宣言・定義しているファイルを編集し名前を変更してから使用されているところを修正していけば良いでしょう。修正を忘れたり、呼び出しを間違えればコンパイラコンパイルエラーとして修正が完全ではないことを教えてくれます。これは静的型付け言語の基本中の基本振る舞いであり、コードベースが大きくなったとしてもそれは変化しません。

しかしP3021が有効化された場合そのような保証はもはや期待できず、snap()slap()に変更してなにも修正しなかったとしても一切エラーが出ない可能性があります。なぜなら、ADLによってslap()を探しに行き、たまたま呼び出せる候補を見つけてしまうかもしれないからです。P3021が導入される以前はメンバ関数呼び出しからADLが発動されることはなく、ADLによって呼び出せてしまうようなslap()を定義していたとしても安全でした。しかし、P3021導入後はそのような関数がメンバ関数呼び出しからのADLによって呼び出されることを意図していたかどうかは分からなくなります(P3021以前の保証の上に立っているのか、P3021の機能の上に立っているのかが分からなくなるため)。

これはまた、メンバ関数を削除する場合や、関数の引数型を変えるなどのリファクタリングにおいても同様です。

本来であればメンバ関数呼び出しではそのようなADLの複雑さを考えなくても良いはずで、P2855のようにそれを有効に利用しようとする向きもあります。しかし、P3021の機能が導入されるとそのような保証はなくなり、メンバ関数呼び出しは常に意図しない関数呼び出しによってバグを静かに埋め込んでしまう可能性を抱えることになります。

意図的かどうかはともかく、現在の多くのC++ユーザーはメンバ関数呼び出しを使用することでクラスのメンバ関数のみが呼び出され、ADLの複雑さについて頭を悩ませることを回避しています。メンバ関数はクラスという単位でのカプセル化によって、そのアクセス範囲及び呼び出される方法はかなり明確です。

メンバ関数呼び出し構文がそのようなカプセルの外側に及ぶようにするというのは、その単純さと分かりやすさを壊しています。それはユーザーへのサービスではなく、ユーザーに対して破滅的な不利益を与えるものです。

このような理由によりこの提案はP3021のUFCSに反対し、UFCSが必要ならば専用の構文を導入して行うべきであり、既存の関数呼び出し構文を拡張する形でそれを行えば以前にはその複雑さが無かった場所に新しい複雑さを導入してしまい、UFCSによる利点を複雑さの増大による欠点が上回るとしています。

P3028R0 An Overview of Syntax Choices for Contracts

2つの契約構文候補を比較する提案。

C++26契約プログラミング機能に向けて議論が進んでおり、残す大きな問題は構文の決定のみとなっています。現在契約構文としてはC++20の契約機能由来の属性様構文(P2935)と新しい文脈依存キーワードによる自然な構文(P2961R2)の2つが候補として残っています。

P2695で示されたロードマップに従って、2023年11月のKona会議ではこのどちらの構文を採用するのかを決定する予定です。

この提案はその議論のために、2つの構文候補を比較することでそれぞれの特性を明らかにしようとするものです。

この提案では次の2つの観点から候補を比較しています

  1. 提案されている全ての構文候補を、契約のある機能についてそれぞれ対応する構文で記述したものを横に並べて比較
  2. P2885で挙げられている原則とともに、各構文候補がその原則をどの様に満たすのか(あるいは満たさないのか)を比較

事前条件構文の比較

// P2935
void f() [[ pre : true ]];

// P2961
void f() pre( true );

事後条件構文の比較

// P2935
int f() [[ post r : true ]];
int f() [[ post (r) : true ]];
// P2935R4の代替提案
int f() [[ post : r : true ]];

// P2961
int f() post( r : true );

アサーション構文の比較

// P2935
void f() {
  [[ assert : true ]];
}

// P2961
void f() {
  contract_assert( true );
}

かかる文字数の比較

P2935 P2961
事前条件 8 5
事後条件 9〜10 6〜7
アサーション 11 17

これ以外にも様々な観点からの比較が行われています。

2023年11月に行われたKona会議では、P2961の自然な構文をC++契約プログラミング機能のための構文として採用することで合意されました。

P3029R0 Better mdspan's CTAD

std::span/std::mdspanコンパイル時定数によってインデックス指定を受ける場合のCTADを改善する提案。

たとえば、mdspanでよく使用されることになると思われる配列ポインタとそれを参照する多次元インデックスの指定を受けるコンストラクタに対応する推論補助は次の様に定義されています

template<class ElementType, class... Integrals>
  requires((is_convertible_v<Integrals, size_t> && ...) &&
            sizeof...(Integrals) > 0)
explicit mdspan(ElementType*, Integrals...)
  -> mdspan<ElementType, dextents<size_t, sizeof...(Integrals)>>;

このため、このコンストラクタ(CTAD)が使用される場合にはインデックスにコンパイル時定数を渡していてもmdspanのエクステントはdextent(実行時エクステント)が使用されます。

mdspan ms (p, 3, 4, 5); // mdspan<int, extents<size_t, dynamic_extent, dynamic_extent, dynamic_extent>>
mdspan ms2(p, 3, integral_constant<size_t, 4>{}, 5);                              // 同上
mdspan ms3(p, integral_constant<size_t, 3>{}, 4, integral_constant<size_t, 5>{}); // 同上

後2つについてはコンパイル時定数を渡しているため、エクステントのその部分は静的になってほしいものがあります。現在これを叶えるためには、ユーザーは次の様に記述する必要があります

mdspan ms2(p, extents<size_t, dynamic_extent, 4, dynamic_extent>(3, 5)); // mdspan<int, extents<size_t, dynamic_extent, 4, dynamic_extent>>
mdspan ms3(p, extents<size_t, 3, dynamic_extent, 5>(4));                 // mdspan<int, extents<size_t, 3, dynamic_extent, 5>>

最初の例の後ろ2つが自動的にこれと同等になることが望ましいでしょう。また、std::spanにも同様の問題があります。

この提案は、std::mdspan及びstd::spanの推論補助を修正して、動的インデックス指定がコンパイル時定数(std::integral_constantのような型の値)によって指定されている場合にそれを静的な指定としてエクステントに反映する様にしようとするものです。

これによる利点は次の様なものが挙げられています

  • 動的・静的エクステントを同等な形式によって指定できるため、エクステント型の反映が直感的になる
    • 現在、動的エクステントは数値で直接コンストラクタに指定できるのに対して、静的エクステントはstd::extents<I, idx...>{...}の様に指定する必要がある
  • 正しい数の引数を渡すためにdynamic_extentsの数を計算する必要がなくなり、エラーが起こりにくくなる
    • extents<size_t, dynamic_extent, 4, dynamic_extent>(3, 5)の様に、静的エクステント中に動的エクステントが混ざっている場合にextentsのコンストラクタでその要素数を指定しなければならない
  • P2781のstd::constexpr_vを使用すると、エクステントが混在するmdspanmdspan(c_<3>, 4, c_<5>)の様に記述できる様になる。

この様なことはstd::submdspanC++26に導入済)では既に行われており、その仕組みを再利用することで実装可能です。

まず次の様な検出ユーティリティを用意して(integral-constant-likestd::submdspanとともに導入済)

// std::integral_constantと同等の型を検出する
template<class T>
concept integral-constant-like =        // exposition only
  is_integral_v<decltype(T::value)> &&
  !is_same_v<bool, remove_const_t<decltype(T::value)>> &&
  convertible_to<T, decltype(T::value)> &&
  equality_comparable_with<T, decltype(T::value)> &&
  bool_constant<T() == T::value>::value &&
  bool_constant<static_cast<decltype(T::value)>(T()) == T::value>::value;


template<class T>
constexpr size_t maybe-static-ext = dynamic_extent;        // exposition only

template<integral-constant-like T>
constexpr size_t maybe-static-ext<T> = static_cast<size_t>(T::value);

これを用いてstd::mdspan及びstd::spanの既存の動的エクステント指定に対応する推論補助を修正します

// 変更前
template<class It, class EndOrSize>
span(It, EndOrSize) -> span<remove_reference_t<iter_reference_t<It>>>;

// 変更後
template<class It, class EndOrSize>
span(It, EndOrSize) -> span<remove_reference_t<iter_reference_t<It>>, maybe-static-ext<EndOrSize>>;
//                                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


// 変更前
template<class ElementType, class... Integrals>
  requires((is_convertible_v<Integrals, size_t> && ...) && sizeof...(Integrals) > 0)
explicit mdspan(ElementType*, Integrals...)
  -> mdspan<ElementType, dextents<size_t, sizeof...(Integrals)>>;

// 変更後
template<class ElementType, class... Integrals>
  requires((is_convertible_v<Integrals, size_t> && ...) && sizeof...(Integrals) > 0)
explicit mdspan(ElementType*, Integrals...)
  -> mdspan<ElementType, dextents<size_t, maybe-static-ext<Integrals>...>;
//                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

P3031R0 Resolve CWG2561: conversion function for lambda with explicit object parameter

ジェネリックthisパラメータを持つラムダ式の関数ポインタへの変換の規定が曖昧なのを解決する提案。

C++23のDeducing thisはクラスのメンバ関数thisパラメータを明示的に書くことができる機能です(この引数の事を明示的オブジェクトパラメータと呼びます)。これはラムダ式においても使用することができますが、ラムダ式の場合はそのクロージャ型名を知ることができないので、通常this autoのように書かれます。

また、ラムダ式はキャプチャしていない場合にのみ関数呼び出し演算子シグネチャと互換性のある関数ポインタへ変換することができます。

このとき、明示的オブジェクトパラメータを持つラムダ式が対応する関数ポインタへ変換可能であるかについて実装間で挙動に差があります。

int main() {
  using T1 = decltype([](int x) { return x + 1; });  // ok、ラムダ式のクロージャ型
  int(*fp1)(int) = +T1{};  // ok、関数ポインタへの変換

  using T2 = decltype([](this auto, int x) { return x + 1; });  // ok、ラムダ式のクロージャ型
  int(*fp2)(int) = +T2{};  // Clangはng、MSVCはok
}

1つ目の例は通常のラムダ式から関数ポインタへ変換する例です。これはC++11以来のもので今回特に問題はありません。

2つ目の例がこの提案の主要な問題であり、ジェネリックな明示的オブジェクトパラメータを持つラムダ式が対応する関数ポインタへ変換できるかどうかが、現時点でこの機能を実装しているClangとMSVCの間で異なっているようです。

また、ラムダの明示的オブジェクトパラメータがジェネリックではなかったとしてもClangは拒否するようで、どうやら規格ではこの場合に生成されるクロージャ型のメンバがどうなるかについて曖昧なようです。

struct Any { Any(auto) { puts("called"); } };

int main() {
  auto a1 = [](int x) { return x+1; };
  auto a2 = [](this Any self, int x) { return x+1; }; // 明示的オブジェクトパラメータはジェネリックではない
  
  int(*fp)(int) = +a2;  // Clangはng、MSVCはok
}

このa1, a2それぞれのクロージャ型は例えば次のようになります

// 型変換演算子で複雑な型を使用するためのエイリアス
template<class T>
using Just = T;

// a1のクロージャ型の例
struct A1 {
  int operator()(int x) const { return x+1; }
  operator Just<int(*)(int)>() const { return +[](int x) { return A1()(x); }; }
};

// MSVCにおけるa2のクロージャ型の例
struct A2_MSVC {
  int operator()(this Any self, int x) { return x+1; }
  operator Just<int(*)(int)>() const { return +[](int x) { return A2_MSVC()(x); }; }
};

// Clangにおけるa2のクロージャ型の例
struct A2_Clang {
  int operator()(this Any self, int x) { return x+1; }
  operator Just<int(*)(Any, int)>() const { return &A2_Clang::operator(); }
};

Clangにおいては明示的オブジェクトパラメータの部分の引数についても関数ポインタ型へ現れるようです。一方、MSVCは明示的オブジェクトパラメータは関数ポインタに現れません。

MSVCの挙動は、従来の暗黙的オブジェクトパラメータを持つラムダを明示的オブジェクトパラメータを持つものにそのままリファクタリングでき(その逆も可能)たり、自身の型を関与させない形で再帰ラムダを定義できたりと、よりユーザーフレンドリーであると思われます。

// Error on Clang, OK on MSVC
auto fib = [](this int (*fib)(int), int x) {
  return x < 2 ? x : fib(x-1) + fib(x-2);
};
int i = fib(5);

この例は、Clangの場合はthisパラメータの型に自身の型が再帰的に現れるのを回避することができませんが、MSVCはこのように関数ポインタによってその再帰を断ち切ることができます。これは再帰ラムダを非ジェネリックに定義できるため、コンパイル時間で有利になるかもしれません。

ラムダ式の関数ポインタへの変換演算子に関して、MSVCはラムダ式における明示的オブジェクトパラメータをかなり特別扱いしており、その引数は常にそのラムダ式自身と同等とみなせると強く仮定しています。ラムダ式が関数ポインタへ変換可能であるのはキャプチャしていない場合のみなので、これは実際にはあまり問題にならないかもしれません。

一方で、this autoを用いずにテンプレート構文によって明示的オブジェクトパラメータを記述するとMSVCでもClangと同様なクロージャ型を生成するようです。

auto a = [](this auto) {}; // MSVCは非ジェネリックとして扱う
auto b = []<class T>(this T) {}; // ジェネリックラムダ

auto pa = +a; // OK on MSVC
auto pb = +b; // error on MSVC
void (*qa)() = a; // OK on MSVC
void (*qb)() = b; // error on MSVC

この非一貫性は非自明ではあります。

この問題はCWG Issue 2561で捕捉され、当初はClangのアプローチを標準化する方向性でした。この提案はそれに対してMSVCのアプローチの方を推すものでしたが、それには文言についてさらなる検討が必要になるということで、明示的オブジェクトパラメータを持つ関数の関数ポインタへの変換をとりあえず禁止しておくことを提案するものです(このR0が出る前に6つのリビジョンがあった様子)。

この提案には、明示的オブジェクトパラメータを持つラムダ式の関数ポインタへの変換を禁止する、Clangのアプローチを採用、MSVCのアプローチを採用、の3つのオプションが含まれており、EWGは1つ目のアプローチを採用したようです。ただし、これはMSVCのアプローチを将来的に採用することを妨げるものではありません。

P3033R0 Should we import function bodies to get the better optimizations?

モジュールのインターフェースにある関数定義をそのモジュールをインポートした翻訳単位にインポートしないようにする提案。

Clangの最適化においては、あるモジュールからインポートされた関数について、その関数の定義をインライン化するような最適化をリンク前に行っているようです。

// a.cppm
export module a;
export int a() { return 43; }

// use.cpp
import a;
int use() { return a(); }

たとえば、最適化を有効にするとuse.cppuse()int use() { return 43; }であるかのようにコンパイルされます。

これはゼローバーヘッド原則に則っており、一見合理的であるように見えます。しかし、use.cppコンパイルする時にオプティマイザはモジュール内のa()に対しても作用してしまうため、プロジェクトの規模が大きくなるとコンパイル時間に跳ね返ってきます。

とはいえそれでも、この問題は単に実装の問題であり規格が口を出す話ではないように思えます。しかし、実際にはABI依存関係と潜在的なODR違反に関連しています。

例えば上記コード群が次のようなコンパイル結果を生成していた場合

a.o       // モジュールaのオブジェクトファイル
a.pcm     // モジュールaのBMI
use.o     // use.cppのオブジェクトファイル
libuse.so // use.cppを含む共有ライブラリ

この時にモジュールa内のa()44を返すように変更して再コンパイルした場合、再コンパイルが発生して再生成されるのはどのファイルでしょうか?これには2つのオプションがあります

rebuild a.o
rebuild a.pcm
rebuild use.o
link libuse.so

もしくは

rebuild a.o
rebuild a.pcm
link libuse.so

この2つの違いはuse.cppが再コンパイルされるかどうかだけです。モジュールの位置づけを考えた場合は再コンパイルされないのが正しい振る舞いにも思えますが、その場合インライン化されているa()の定義の変更が反映されません。 再コンパイルする場合は実装の一貫性は保たれますが、コンパイル時間が増大します。あるいは、最適化を有効にしている場合にのみABI依存関係(現在一般的ではない)に基づいて再コンパイルを行うべきでしょうか?

この提案ではこの問題への対処として、モジュール本文内の非inline関数本体を変更する場合、対応するモジュールインターフェースのBMIを変更するべきではない、とすることを提案するものです。

すなわち、ユーザーがモジュールインターフェースユニット(の本文)内の非インライン関数の本体のみを変更したプロジェクトを再コンパイルする場合、再コンパイルされるのは変更されたモジュールインターフェースのみであり(そのBMIすらも再コンパイルするべきではなく)、他のすべてのコンパイルは行われるべきではありません。ただし、リンクを除きます。

提案では、これによりユーザーエクスペリエンスが向上するはずとしています。

ビルドシステムのレベルでは、ビルドシステムが再コンパイルが必要かを決定する依存関係はモジュールのソースではなくモジュールのBMIに依存するように実装することでこれが実現できます。そして、コンパイラBMIにはモジュールからエクスポートされているインターフェースのみを保存しておき、その定義を保存しないようにする必要があります。GCC/MSVCは現在そのようにしていますが、Clangは2フェーズコンパイルモデルを実行する都合上そのようになっていないようです。

実行時のパフォーマンスについても、ヘッダファイルベースのライブラリをモジュールに移行することを考えた場合、ヘッダファイル内の関数はほぼインライン関数であるためこの提案の制約に接触せず、パフォーマンスの低下が発生する場合は限定されるとしています。また、LTOを使用することで翻訳単位を跨いだ定義のインライン化のような最適化が可能となるため、問題とされている最適化が全く利用できなくなるわけではありません。

P3034R0 Module Declarations Shouldn't be Macros

名前付きモジュールのモジュール宣言において、モジュール名のマクロ展開を禁止する提案。

モジュールのソースファイル形式は他のものと区別されておらず、あるファイルがモジュールソースであるかは、そのファイルの先頭にモジュール宣言があるかどうかによって決まります。モジュール宣言およびグローバルモジュールフラグメントの宣言はプリプロセッサによって導入することができませんが、モジュール名はマクロの展開によって指定することができます。

例えば、次の様なコードはC++20時点で有効です

// version.h
#ifndef VERSION_H
#define VERSION_H

#define VERSION libv5

#endif


// lib.cppm
module;
#include "version.h"
export module VERSION;

あるソースファイルがモジュールファイルであり、かつそのモジュール名が何であるかを知ることは、ソースの依存関係を知るために必要な作業です。従来のヘッダインクルードであれば、その依存関係を知らなくてもビルドを行うことができますが、モジュールの場合はビルドにあたってその依存関係を把握しソースのビルド順を決定する必要があります。これを行うのはコンパイラではなくビルドシステムの仕事であるため、ビルドシステムはソースファイルを読み込みそれがモジュールであるか、モジュールの場合はその名前は何かを読み取る必要があるかもしれません(これを行わなくてもいい方法がいくつか考案されていますが、まだ標準化等されてはいません)。

モジュール宣言はプリプロセッサによって導入されないものの、モジュール名はマクロの展開を完了させた上で読み取らなければなりません。そのためには、上記例のようにグローバルモジュールフラグメント内のインクルードやマクロ定義を読み込んだ上でマクロの展開を行わなければなりません。その作業は実装も処理も簡単なものとは言えず、ビルドシステムの実装および処理時間にかなりの負担になります。

この様な理由から、この提案はモジュール名もマクロによって導入できない様にする提案です。

その目的はモジュールファイルのパースを簡単にすることでビルドシステムがモジュール名パースを実装しやすくすることにあります。また、ビルドの前にモジュールの依存関係解決フェーズを行う場合に、パース処理が単純化されることで依存関係解決フェーズの遅延時間を短くすることもできます。

ただし、この変更はC++20への破壊的変更になります。提案では、モジュールの実装はまだ出揃っておらず使用例も稀であるため、影響は最小限である、としています。

この提案はSG15でもEWGでもほぼ反対なく支持されたようで、EWGではこの提案をC++20へのDRとするつもりの様です。

P3037R0 constexpr std::shared_ptr

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

C++20で定数式における動的メモリ確保が可能になり、C++23でstd::unique_ptrconstexpr対応され定数式で使用できる様になりました。

スマートポインタは実行時と同様に定数式においてもメモリ管理を自動化することができます。しかし、std::shared_ptrはその実装に必要な言語機能の一部が定数式で使用可能ではなかったためすぐにconstexpr対応することができませんでした。

C++23におけるP2738R1(void*からの正しいポインタキャストの許可)とP2448R2(定数式で実行不可能なものは評価されるまではエラーにならない)の採択によりその障害は取り除かれており、この提案はそれを受けてC++26に向けてstd::shared_ptrconstexpr対応しようとするものです。

筆者の方はlibstdc++のstd::shared_ptr実装をベースとして実装を試みており、アトミック操作の使用を回避の必要性やstd::make_shared()などの行う1回のメモリ確保による初期化の問題などを報告していますが、いずれも回避は可能であり実装可能であるとしています。

また、この提案ではさらに、C++23ではconstexprで実装できなかったため外されていたstd::unique_ptrの比較演算子に対してもconstexprを付加することも提案しています。

P3038R0 Concrete suggestions for initial Profiles

既存のC++コードの一部により強力な保証を付加するためのプロファイルについての提案。

この提案は、P2687で提案されていたアイデアについて、より具体的な最初の機能について説明するものです。

プロファイルはC++コード上でユーザーによって指定されるもので、スコープもしくはモジュールに対して付加することができます。

// モジュール宣言にmemory_safetyプロファイルを適用
export module DataType.Array [[enforce(memory_safety)]];

// 名前空間宣言にプロファイルを適用
namespace N [[enforce(p)]] {
  ...
}

[[enforce(p)]]はプロファイルpをそのスコープに対して適用するもので、そのスコープの内側にあるコードに対してプロファイルpの保証が強制されます。モジュールの場合の適用範囲は、そのモジュール本文の全体です。

// モジュールMに対してプロファイルPを適用
import M [[enable(P)]];

// モジュールoldでtype_safetyプロファイルを無効化
import old [[suppress(type_safety)]];

プロファイルは既存コードに付加して保証を強化するものであり、[[enable(P)]]によって特にプロファイルを使用していないモジュールのインポート時にプロファイルを適用することができます。また、プロファイルはスコープに対して指定されある程度広い領域でその保証が強制されるため、[[suppress(P)]]によって部分的にプロファイルを無効化することもできます。

想定されるプロファイルにはいくつかの種類が考えられますが、この提案では実装負担の軽減のために最初の小さいものとしてtype_safetyプロファイルに焦点を当てています。与えられる保証は例えば

  • 変数初期化の強制
    • [[uninitilize]]とマークされない変数には初期化が必要
  • ポインタの利用の制限
    • ポインタは単一要素を指すか、nullptrであるかのどちらか
      • ポインタによる範囲のランダムアクセス禁止、その用途にはspanvectorを使用する
    • ownerとマークされていない限り、ポインタは所有権を持たない
      • ownerはポインタの先のオブジェクトを破棄する責任を負う
      • owner以外のポインタに対してnew/deleteできない
    • nullptrチェックなしのポインタアクセスの禁止
  • ダングリングポインタ(参照)の抑止
    • ポインタ(参照)はオブジェクトを指すか、nullptrのどちらか
    • ownerではないポインタはdeleteできない
    • 生存期間が不明なポインタを外側のスコープに漏出できない
    • returnできるポインタを制限する
  • ポインタ(参照)の無効化の防止
    • const参照によってコンテナを取得する関数では参照の無効化が発生する可能性があり、const参照によってコンテナを取得する関数では参照の無効化が発生しないと仮定
    • const参照によってコンテナを取得するがコンテナを変更しない関数では[[not_invalidating]]アノテーションによってそれを表明する
      • 間違った[[not_invalidating]]の利用は検出できるはずで、エラーにする

提案文書より、例

void f1() {
  int n;  // error

  [[uninitialized]]
  int m;  // ok
}

void f2(int* p1, owner<int*> p2) {
  delete p1; // error、ownerでは無いポインタをdeleteいている
  delete p2; // p2はdeleteしないとエラー
}

void f3(int* p1, owner<int*> p2) {
  p1=p2; // OK、p1はownerではないが、p2と同じオブジェクトを指す
  p2=p1; // error、p2は上書きされる前にdeleteされなければならない
}

int* glob = nullptr;
void f4(int* p) {
  glob = p; // error、不明な生存期間のポインタを保存しようとしている
}

int glob2 = 0;
int* f5(int* p) {
  int x = 4;
  return &x;          // error: ローカルオブジェクトへのポインタを返そうとしている
  return p;           // OK: pは関数呼び出し時に有効であり、無効化されていない
  return new int{7};  // error, ownerポインタを非ownerで返そうとしている
  return &glob2;       // OK 静的オブジェクトへのポインタ
  throw p;            // error: pを*pのスコープ外に漏出しうる
}

void f6(vector<int>& vi) {
  vi.push_back(9); // 要素の再配置が発生しうる
}

void f7() {
  vector<int> vi { 1,2 };
  auto p = vi.begin(); // viの最初の要素を指すイテレータ
  f6(vi); // 参照を無効化しうる関数呼び出し
  *p = 7; // error、参照が無効化されている可能性がある
}

提案では、このtype_safetyプロファイルに加えて、vector等の範囲に対するアクセスの境界チェックを行う実行時検査を伴うプロファイルであるrangesプロファイルや、組み込み数値演算の安全性向上(オーバーフロー防止、縮小変換・符号変換の禁止など)のためのプロファイルであるarithmeticプロファルなどを初期のプロファイルの候補として挙げています。

このようなプロファイルに基づく保証の提供はC++ Core Guidelineおよびそのチェッカー実装とガインドラインサポートライブラリの経験から来ています。それはあくまで静的解析としてC++コンパイルとは別でチェックされることでしたが、プロファイルとしてその保証をC++のコードに対して取り込むことで、既存のC++コードの上に被せる形でC++コードの安全性を高めることができ、プロファイルの指定は小さななスコープから始めることができます。

この提案のプロファイルとその静的な検査については、コアガイドラインチェッカーにて現在利用できるものであり、実装可能であることが確かめられています。また、この提案による安全性の静的検査は、コンパイラに強力なフロー解析などを強いるものではなく、危険を招く可能性のある操作を制限することで抑止するとともに、コンパイラの静的解析にいくつかの仮定を与えることで解析を補助する事を目指す物です。

P3039R0 Automatically Generate operator->

<=>演算子の様な書き換えによってoperator->を導出する提案。

この提案では、->->*演算子オーバーロード解決時に書き換えて実行することでこの2つの演算子を自動で導出できる様にすることを提案しています。それぞれ次の様になります

  • lhs->rhs(*lhs).rhsに書き換えて実行
  • lhs->*rhs(*lhs).*rhsに書き換えて実行

ライブラリソリューション(Boost.Operatorsのような)でこれと同じことを行おうとする場合、*lhsがprvalueを返す場合(例えばプロクシイテレータなど)に一時オブジェクトの寿命が->の定義内で尽きてしまうことによって未定義動作が発生する問題が回避できません。しかし、言語機能による演算子の書き換えはその様な問題を回避することができます(その場でインラインに置換される形になるので、->の呼び出しコンテキストと書き換え後の*lhsの生存コンテキストは一致する)。

また、比較演算子の場合は逆順の演算子や細かいコーナーケースを処理するためにその書き換えルールが複雑になっていますが、->->*はどちらも逆順を考慮する必要がなく、->はクラス内でのみ定義でき右辺のオペランドオーバーロード解決とは無関係となるため、書き換えに伴う仕様はかなりシンプルになります。

どちらの演算子でも、まずは->/->*として定義されたもの(delete含む)を優先して選択し、それが見つからずoperator*が利用可能である場合にのみ書き換えた候補を使用します。->を定義したいクラス側でdefault宣言しておく必要はなく、書き換えによって導出されたくない場合はdelete宣言をしておくことで書き換えを抑止できます。

これによるメリットは、主にイテレータ定義時の->に関する記述をほぼ完全に削除することができる点です。

提案では、<=>にならってこの提案が採択された場合に既存の->定義を削除するオプションについて検討されており、そこではC++20時点の標準ライブラリで->を持つクラスにおける定義のされ方を調査しています。それによれば、スマートポインタやstd::optional、コンテナのイテレータ型や一部のRangeアダプタのイテレータ型など、多数のクラス型において->定義を削除することができることが示されています。

ただし、std::iterator_traitspointerメンバ型の定義や、std::to_addressstd::pointer_traitsなどその動作について->演算子の存在に依存している部分があるライブラリ機能について、この提案の影響を回避する様にしなければなりません。それについてはいくつか方法が提示されているものの未解決です。

P3040R0 C++ Standard Library Ready Issues to be moved in Kona, Nov. 2023

11月に行われたKona会議でWDに適用されたライブラリに対するIssue報告の一覧

P3041R0 Transitioning from "#include" World to Modules

ヘッダファイルによるライブラリをモジュールベース変換する際の実装戦略についての報告書。

標準ライブラリモジュールstdと標準ヘッダファイルは同時にインポート/インクルードしたとしてもODR違反等を起こさず一貫して使用可能であることが規定されています。

#include <vector>
import std;

int main() {
  // vectorおよびそのメンバ関数実体は曖昧にならない
  std::vector<int> vee { 1, 2, 3, 4, 5 };
  return vee.size();
}

これは、通常のユーザーが定義できる名前付きモジュールでは得られない保証です。

ヘッダインクルードによるエンティティはグローバルモジュールに属しており、モジュールのエンティティは名前付きモジュールという翻訳単位に属しています。この2つのものは例え同じ名前で定義されていたとしても異なるものとして扱われるため、上記の様なコードをユーザー定義ライブラリでやると意図通りになるか曖昧になるかは実装次第となります。しかし標準ライブラリモジュールとヘッダファイルに関しては、これが確実に動作する(ヘッダファイルとstdモジュールとの対応する名前は同じ1つのエンティティを参照する)ことが規定され、要求されています。

これは、標準ヘッダと標準モジュールの両方を適用する必要がある現状においても両方を自然に同居させるための要求ですが、もしこの様な保証をユーザー定義の名前付きモジュールに対しても与えることができれば、ヘッダファイルとモジュールを同時に提供するライブラリの実装が可能になり、ヘッダからモジュールへの移行を促進することができます。

また、上記の様な標準ライブラリの保証を実現する実装戦略は、グローバルモジュール(ヘッダファイル)のエンティティに対してstdモジュールのエンティティを対応づけるような形になる様ですが、これは名前付きモジュールのいくつかの利点を犠牲にしています。

この報告書は、ヘッダとモジュールを同時に提供しながら名前付きモジュールの利点を余すところなく享受し、なおかつそれを任意のC++ライブラリで利用可能にする実装戦略について説明するものです。

この戦略は、ビルド定義とコンパイラが連携してBMIマッピング#include変換を組み合わせることで、現在のstdモジュールの保証を実現するものです。標準ライブラリヘッダに関しては、ビルドは次の様に行われます

  1. 標準ライブラリヘッダのインクルードをヘッダユニットのインポートへ変換
  2. 全ての標準ヘッダユニットに対して、stdモジュールのBMIを使用する様にコンパイラへ指示する
  3. 標準マクロを強制的にインクルードする

マクロに関してはこの方法では導入できないため、別途(コマンドライン等から)インクルードする必要があります。この文書では、C互換ではない標準ヘッダが提供する必要のあるマクロをまとめたヘッダファイルを用意しそれをインクルードする事を推奨しています。

この実装戦略は標準モジュールに対してのものですが、より一般のC++ライブラリに対しても適用可能です。ただし、そのためには次のようなものが必要です

  1. あるヘッダが名前付きモジュール(または別のヘッダ)に含まれていることを記述する機能
  2. ヘッダのインクルードをヘッダユニットのインポートへ変換する機能
  3. 2のヘッダユニットのBMIを包含モジュールのBMIマッピングする機能
  4. ヘッダファイルで導入されるはずのマクロを強制的にインクルードする機能

現在のところこれらの機能のいずれも非標準ライブラリに対しては提供されていません。

P3042R0 Vocabulary Types for Composite Class Design

P3019の紹介スライド。

std::indirectstd::polymorphicのモチベーションや設計要求などについて丁寧に説明されています。おそらく、提案を見るよりも分かりやすそうです。

P3043R0 Slides: Using variable template template without meta programming

変数テンプレートテンプレートの動機付けについて説明する文書。

lldにあるコードを簡略したものを整理することを例にとって、変数テンプレートテンプレート(変数テンプレートを受け取るテンプレートパラメータ)の必要性を説明しています。ただし、これ自体は何かを提案しているわけではありません。

これを可能とする提案としては例えばP2989があります。

P3046R0 Core Language Working Group "ready" Issues for the November, 2023 meeting

11月に行われたKona会議でWDに適用されたコア言語に対するIssue報告の一覧。

P3050R0 Optimize linalg::conjugated for noncomplex value types

std::linalg::conjugated()を非複素数型に対してアクセサの変更をしないようにする提案。

std::linalg::conjugated()複素数配列を参照するmdspanの各要素を、その複素共役となるように変換する関数です。ただし、戻り値もmdspanで返され、変換はmdspanのアクセサポリシーを変更することで行われます。従って実際の要素は変更されず、mdspanからの要素参照時に引き当てられた要素に対して複素共役への変換を行うことで配列全体の変換を行います。

namespace std::linalg {

  // conjugated()の宣言例
  template<class ElementType,
           class Extents,
           class Layout,
           class Accessor>
  constexpr auto conjugated(mdspan<ElementType, Extents, Layout, Accessor> a);
}

std::linalg::conjugated()の現在の動作は次のようになっています

  • 入力mdspan<T, E, L, A>のアクセサ型Aconjugated_accessor<NestedAccessor>NestedAccessorは任意の他のアクセサ型)である場合、mdspan<NestedAccessor::element_type, E, L, NestedAccessor>を戻り値型として入力aの領域とレイアウトをそのまま渡して返す
  • それ以外の場合、mdspan<T, E, L, conjugated_accessor<A>>を戻り値型として入力aの領域とレイアウトをそのまま渡して返す
    • 複素共役を行うアクセサポリシーconjugated_accessorで元のアクセサをラップする

conjugated_accessor<A>はアクセサポリシーAをラップして、Aで定義されたアクセス結果に対して複素共役変換を適用して返すアクセサポリシー型です。

conjugated_accessor<A>による変換は、conj-if-needed()という説明専用の関数によって行われ、conj-if-needed(c)cに対するADLによって非メンバconj()が使用可能であればそれを使用して複素共役を取得し、それが見つからない場合はcをそのまま返します。これによって、cおよびmdspanの要素型が複素数型ではない場合はこの変換は最適化によってスキップされることが期待できます。

しかし、その呼び出し階層を削除することができたとしても、conjugated_accessor型の存在を削除することはできません。

mdspanを扱う多くのユーザーは関数等でmdspanを受け取る場合デフォルトのポリシーを使用して型を記述し、特にアクセサ型を変更する形で記述されることは稀だと思われます(すなわち、要素型Tに対してdefault_accessor<T>が専ら使用されるはず)。

そのようなユーザーは<linalg>の主機能であるBLASラッパを使用しないとしても、std::linalg::conjugated()などのユーティリティは使用することになるでしょう。そして、自身の持つmdspanstd::linalg::conjugated()に通すと、その要素型がなんであれアクセサポリシーが変更されたmdspanが得られ、デフォルトのアクセサを使用したmdspanを受け取るように定義された関数に対してそれを渡すとコンパイルエラーに遭遇するでしょう。

std::linalg::conjugated()複素数要素に対して作用するためこれは回避不可能なものであるといえるかもしれません。しかし、BLASそのものやMatlab等では、転置と共役転置(随伴)の操作は同じものとして統合されており、<linalg>でもconjugate_transposed()が用意されている他、conjugated(transposed(x))のように書かれることもあるでしょう。これらの関数に対する入力はその要素型が浮動小数点数型か複素数型かを意識せずに渡されるはずで、この場合に非複素数要素型のmdspanに対してアクセサ型の変更が行われることは驚きを伴う可能性があります。

これに対応するにはアクセサポリシー型をジェネリックにしなければならず、それはコンパイル時間の増大を招くとともに、デフォルトアクセサを仮定する最適化を行えなくなることを意味します。

LWGにおけるP1673のレビュー中にこの問題が指摘され、そのままでも致命的な問題ではなかったためP1673はそのままレビューされC++26 WDにマージされました。この提案は、改めてこの問題を解決するために提出されました。

この提案による変更は、std::linalg::conjugated()がアクセサポリシーをconjugated_accessorに変更しようとする場合(共役の共役とならない場合)にその要素型が複素数型ではないならば入力のmdspanをそのまま返すようにします。これによって、次の2点の変更は観測可能となります

  • std::linalg::conjugated()の戻り値型は必ずしもconjugated_accessor<A>ではなくなる
  • 複素数型の要素型のmdspanに対して、戻り値型のmdspanconst element_typeを持たなくなる

これは、conjugated()の呼び出しは常にconst element_typeを持つわけではないことや、結果を他の関数に渡しているコードの呼び出しが壊れるわけではないことなどから許容されるとしています。

P3051R0 Structured Response Files

ツールが他のツールにコマンドラインオプションをファイルで引き渡す方法についての提案。

現在、いくつかのコンパイラはそのオプションをファイルにまとめて指定する方法を提供しています。それはよく似た方法で行われていますが、コンパイラ間で互換性はなく、相互運用が可能なものではありません。例えばファイルを渡すオプション名が異なり、ファイル形式もバラバラです。

そのような方法を標準化することで、ツールが他のツールへ(例えばビルドシステムからコンパイラへ)そのコマンドラインオプションを渡すことが容易になり、ツール間の相互運用性が向上します。また、そのような一貫した方法/フォーマットはツールに対する一貫した共通オプションのようなものを定義するための下地にもなります。

この提案は、コマンドラインオプションをまとめたファイルのフォーマットとそれを受け渡す方法について提案するものです。

提案ではファイルの形式としてJSONファイルとすることを提案しています。

そして、そのファイルにツールのオプションを記録する方法として、引数とオプションの2つのスタイルを提案しています。引数はコマンドラインオプション文字列をそのまま記録するもので、オプションは実際のコマンドラインオプションに対応するより概念的な指定となるものです。

提案より、それぞれの表現例

// 引数の例
{
  "arguments": [
    "-fPIC",
    "-O0",
    "-fno-inline",
    "-Wall",
    "-Werror",
    "-g",
    "-I\"util/include\"",
    "-c"
  ]
}

// オプションの例
{
  "options": [
    "fPIC",
    { "O": "0" },
    "fno-inline",
    { "W": [ "all", "error" ] },
    "g",
    { "I": [ "util/include" ] },
    "c"
  ]
}

引数のスタイルは既存のコマンドラインオプション構文に直接対応しており移行しやすいものですが、ツール依存になります。オプションは既存のコマンドラインオプション構文をより抽象化したもので、ツール間のオプション構文の差異を吸収できる可能性があります。また、これら2つのスタイルは1つのファイル内に同居することができます。

それぞれの利点/欠点

  • 引数
    • 利点
      • 既存のJSON compilation databaseをベースとしており、これをパースする実装は既に存在している
      • ツールのコマンドラインオプションとの直接のマッピングがあり、サポートに労力がかからない
    • 欠点
      • オプションとその値を取得するにはパースが必要
      • 通常のコマンドラインオプションと同じ制限を受ける
  • オプション
    • 利点
      • オプション名はオプションのプリフィックス- -- /など)を省略しているため、ツールに依存しない共通名を使用できる
      • オプション値に配列やオブジェクトを利用できることで論理的なグループ化が可能になり、コマンドライン引数のパースで発生するような追加の引数マージ処理のようなものが不用になる
    • 欠点
      • 既存ツールはこの新しいオプション構文を読み取る実装が無い
      • JSON compilation databaseでもこの形式が採用される場合、さらに追加の作業が発生する

そして、このファイルを指定するコマンドラインオプションとしてstd-rspを提案しています。

tool --std-rsp=file
tool -std-rsp:file

実際のファイル全体は例えば次のようになります

{
  "$schema": "https://raw.githubusercontent.com/cplusplus/ecosystem-is/release/schema/std_rsp-1.0.0.json",
  "version": "1",
  "arguments": ["-fPIC", "-O0", "-fno-inline", "-Wall", "-Werror", "-g", "-I\"util/include\"", "-c" ]
}

提案では、オプションスタイルの場合の各オプション名について、実際のコマンドライン引数名に対応させるか、より抽象的な名前にするかについて未解決としています(例えば、W, o, Iwarning, output, include)。これにも利点欠点があるため、どちらを選択するかやこの提案でそれを追求するかについてはSG15の決定に委ねています。

P3052R0 view_interface::at()

view_interfaceat()メンバ関数を追加する提案。

C++26でP2821が採択されたことでstd::spanでもインデックスアクセスにat()が使用できる様になり、既存の標準コンテナ等とのインターフェースの一貫性が向上しています。これにより、標準にある2つのview型(spanstring_view)でat()が使用できる様になったため、これをより汎用的なview型でも一貫させることの根拠が生まれました。

残りのview型とは<ranges>の各種view型(subrangeやRangeアダプタのview型)のことで、これらの型はその共通インターフェースをview_interfaceというCRTPベースクラスを継承することで提供しています。

この提案は、インデックスアクセスの安全性とインターフェースの一貫性を向上させるために、view_interfaceat()メンバ関数を追加しようとするものです。

namespace std::ranges {
  template<class D>
    requires is_class_v<D> && same_as<D, remove_cv_t<D>>
  class view_interface {
    ...
  public:
    ...
    
    // 現在の添字演算子オーバーロード
    template<random_access_range R = D>
    constexpr decltype(auto) operator[](range_difference_t<R> n) {
      return ranges::begin(derived())[n];
    }

    template<random_access_range R = const D>
    constexpr decltype(auto) operator[](range_difference_t<R> n) const {
      return ranges::begin(derived())[n];
    }

    // 提案するat()
    template<random_access_range R = D>
      requires sized_range<R>
    constexpr decltype(auto) at(range_difference_t<R> n);

    template<random_access_range R = const D>
      requires sized_range<R>
    constexpr decltype(auto) at(range_difference_t<R> n) const;
  };
}

このat()はコンテナ等のそれと同様に動作し、指定されたインデックスが範囲外参照となる場合にout_of_range例外を送出するものです。view_interfaceで提供されることで、<ranges>のほぼ全てのview型で使用可能になります。

P3053R0 2023-12 Library Evolution Polls

2023年12月に行われる予定のLEWGの全体投票の予定。

次の19個の提案が投票にかけられます

後ろの2つを除いて、残りのものはC++26に向けてLWGに転送するための投票です。

P3055R0 Relax wording to permit relocation optimizations in the STL

リロケーション操作による最適化を標準ライブラリのコンテナ等で許可するために、標準の規定を緩和する提案。

C++26に向けてリロケーション(relocation)操作を言語に導入する議論が進んでいます。リロケーションは意味的にはムーブ+破棄に相当し、ムーブした直後にムーブ元オブジェクトを破棄する操作をひとまとめにしたものです。中でも、トリビアルリロケーションはmemcpyによってオブジェクト表現(ビット列)をコピーするだけで行うことができます。

リロケーション後の元のオブジェクトはコード上で使用不可になるという性質から、一部のムーブを伴う操作はリロケーションによって効率化できる可能性があります。特に、標準コンテナの操作やアルゴリズムに関わる操作などにおいて最適化を促進することが期待されています。

仮にリロケーション操作が言語に入った時に問題となるのは、それらコンテナやアルゴリズムの規定、特に計算量の規定がリロケーションではなくムーブを前提として指定されていることです。たとえば、std::vector::erase()をリロケーションによって書き換えると次の様な実装になるでしょう

void erase(iterator it) {
  if constexpr (std::is_trivially_relocatable_v<value_type>) {
    std::destroy_at(std::to_address(it));
    std::uninitialized_relocate(it + 1, end_, it);
  } else {
    std::ranges::move(it + 1, end_, it); // operator=
    std::destroy_at(std::to_address(end_ - 1));
  }
  
  --end_;
}

しかし、std::vector::erase()の計算量の指定は、「要素型Tのデストラクタは消去された要素の数と等しい回数呼ばれ、Tの代入演算子は消去された要素の後にある要素の数と等しい回数呼ばれる」と規定されています。トリビアルリロケーションの場合、要素のムーブはそのオブジェクト表現のコピーのみで元オブジェクトの破棄は行われない(オブジェクトの配置場所が変わるだけでオブジェクトそのものは何ら変化しない)ため、std::vector::erase()ではリロケーション操作が利用可能になったとしても標準の範囲内でそれを利用することができません。

このような規定がなされているものがコンテナの操作やアルゴリズムに関して存在しており、これがある限り言語にリロケーションが導入されても標準ライブラリはそれを活かすことができません。しかし、これを取り除いておけばリロケーションの到来と関係なく、標準ライブラリはトリビアルリロケーション可能であると現在わかっている型(std::deque<int>など)についてそのような最適化を行うことができます。

この提案は、その様な過剰な制限を強いてしまっている現在の規定をリストアップし、それをトリビアルリロケーションをサポート可能なように緩和しようとするものです。

提案する変更は、全て現在の制限を若干緩めるものなので、既存の実装がこれを受けて何か変更する必要があるものではありません。たとえば、先ほどのstd::vector::erase()の計算量の規定の場合、「元のvector上で削除された一番先頭にある要素の後にある要素の数について線形」の様に変更しています。これによって、特定の操作に対して計算量を指定する事を回避しています。

P3056R0 what ostream exception

現在例外を投げる際に避けることのできない動的メモリ確保を回避する提案。

標準ライブラリにあるstd::exception派生クラスは.what()メンバ関数からエラーメッセージを返すためにそのコンストラクタで文字列を受け取りますが、動的に構成した文字列をstd::stringに保持している状態で渡そうとする場合、文字列をコピーして受け取る以外の選択肢がありません。

void f(int n) {
  std::string err_msg = std::to_string(n);
  std::runtime_error err{err_msg};  // コピーされる

  throw err;
}

これは、std::exception派生クラスのstd::stringを受け取るコンストラクタはconst std::string&を受け取るものしかないためです。

また、このように例外が発生するコンテキストでエラーメッセージを動的に構成する場合、その作業そのものに伴って動的メモリ確保が発生しています。例えば例外の.what()が呼ばれない場合、このコストは余分なものとなります。

この提案は、std::exception派生クラスおよびstd::exceptionに2種類のメンバ関数を追加することによって、この2つの動的メモリ確保を回避もしくは遅延させ、ライブラリ実装者およびそのユーザーが動的メモリ確保を制御できる様にしようとするものです。

追加するのは次の2つです

  1. std::exception派生クラスのコンストラクタにstd::string&&を受け取るコンストラクタを追加する
  2. std::exception.what()オーバーロードとして、std::ostream&を受け取りエラーメッセージの構築と出力まで行うオーバーロードを追加する

1つ目の変更によって、std::exception派生クラスにエラーメッセージのstd::stringをムーブ渡しできる様になり、コピーに伴う動的メモリ確保を回避することができます。

void f(int n) {
  std::string err_msg = std::to_string(n);
  std::runtime_error err{std::move(err_msg)};  // ムーブされる

  throw err;
}

2つ目の変更ではさらに、例外オブジェクト内部に必要な情報を保持しておき、エラーメッセージが必要になったタイミングでエラーメッセージをオンデマンドに構成することが可能になります。

class runtime_error_v2 : virtual public runtime_error_v2 {
private:
    const int m;
    const std::source_location location;
public:
  runtime_error_v2(int n, const std::source_location location)
    : m{n}
    , location{location}
  {}

  virtual std::ostream& what(std::ostream& os) const noexcept override {
    // 呼ばれてからメッセージを構成する
    return os << "file: "
        << location.file_name() << '('
        << location.line() << ':'
        << location.column() << ") `"
        << location.function_name() << "`: "
        << "value: " << n << '\n';
  }
};

void f(int n) {
  std::runtime_error err{n, std::source_location::current()};
  throw err;
}

P3057R0 Two finer-grained compilation model for named modules

名前付きモジュールの依存関係管理について、より細かい単位で依存関係を管理する方法についての報告書。

名前付きモジュールでは、ヘッダファイルとは異なり個々のモジュールが1つの翻訳単位を成しているため、プログラム全体をビルドするためにはその依存関係を把握した上で依存関係の根本から順番にビルドしていく必要があります。そのため、インクリメンタルビルド等においては、ある1つのファイルの変更がより多くのモジュールや翻訳単位のリビルドを引き起こす可能性があります。

この文書は、この問題を軽減するために、より細かい単位で依存関係管理を行うコンパイルモデルを説明するものです。

この文書で挙げられているモデルは2つあります。

  1. 使用したファイルベースのソリューション
    • あるソースファイルのコンパイル中に使用されたソースファイルを記録しておき、2回目以降のビルドでは自身及び使用したファイルが変更されていなければ再コンパイルを省略する
    • この場合の使用されたかされていないかは、ファイルのインポートやインクルードではなく、その中身の宣言が使用されているかによって判定される
    • ここでのファイルの変更は、ファイルシステムにおける変更によって判定する
  2. 宣言のハッシュによるソリューション
    • あるソースファイルのコンパイル中に、そこで使用されている宣言のハッシュを記録しておき、2回目以降のビルドでは記録した宣言ハッシュを比較して変更がなければ再コンパイルを省略する
    • ハッシュの計算と比較のコストやビルドシステム側での対応など課題がある

この2つの方法はClangのプラグインを通して既に試すことができるようで、文書中でも実際のデモの様子が報告されています。

P3059R0 Making user-defined constructors of view iterators/sentinels private

<ranges>の内部イテレータ型のコンストラクタを非公開にする提案。

<ranges>にある各種のview型は、その動作の実装のほとんどの部分をイテレータによって行なっています。その様なイテレータは構築時に親のviewを受け取りそのポインタを保存しますが、そのコンストラクタは親のview型からアクセス可能であれば良いはずで、他のところからアクセスできる必要はありません。

現在のところ、標準のRangeアダプタのview型のイテレータのその様なコンストラクタは、ものによってアクセスできたりできなかったりします。

int main() {
  auto base = std::views::iota(0);
  auto filter = base | std::views::filter([](int) { return true; });

  // 内部イテレータ型のコンストラクタが呼べる(場合もある)
  auto begin = decltype(filter.begin())(filter, base.begin()); // ok
  auto end   = decltype(filter.end()  )(filter);               // ok
}

この提案は、この様なコードはエラーとなるべきで、標準のRangeアダプタのview型のイテレータのコンストラクタは一部を除いて公開されるべきではない、とするものです。

上記のコードは実はGCCにおいてはエラーになります。それは、GCCfilter_viewイテレータの実装が親のfilter_viewを参照ではなくポインタで受け取る様になっているためです。実装の観点からは、これによってfilter_viewbegin()内では構築時にthisを渡すだけですみ、イテレータ側もaddresof()の呼び出しを適用する必要がなくなります。

現在の規定に照らせばGCCのこの実装は間違っていますが、これはviewの実装詳細の部分であり、本来公開されるべきではないものが公開されていることによる副作用と見なすことができます。また、このGCCfilter_viewにおけるイテレータの実装は、chunk_viewイテレータにおいては規格でそのように指定されており一貫していません。このことからも、これらのコンストラクタは公開されないのをデフォルトにするのが望ましいと言えます。

この提案の対象はあくまで実装のために使用されるコンストラクタを非公開化するもので、デフォルトコンストラクタやムーブコンストラクタ、変換コンストラクタなどを非公開にしようとするものではありません。

P3060R0 Add std::ranges::upto(n)

0から指定した数の整数シーケンスを生成するRangeアダプタ、views::uptoの提案。

この提案のviews::upto(n)views::iota(0, n)と同じシーケンスを生成します

import std;

int main() {
  // iota(0, n)
  for (int i : std::views::iota(0, 10)) {
    std::println("{} ", i);
  }

  std::println("");

  // upto(n)
  for (int i : std::views::upto(10)) {
    std::println("{} ", i);
  }
}

どちらも0 1 2 3 4 5 6 7 8 9が出力されます。

このため実装はごく簡単に行うことができます

namespace std::ranges {
  // ranges::upto 実装例
  inline constexpr auto upto = [] <std::integral I> (I n) {
    return std::views::iota(I{}, n);
  };

  namespace views {
    using ::std::ranges::upto;
  }
}

これだけだとiotaで十分にしか見えませんが、uptoの意義は符号なし整数型で同じことをする場合の微妙な使用感の悪さを改善することにあります。

void f(const std::vector<int>& vec) {
  auto seq1 = std::views::iota(0, vec.size());  // ng
  auto seq2 = std::views::upto(vec.size());     // ok
}

整数値a, ba < bとして)によってviews::iota(a, b)の様にする場合、a, bの型は異なっていても構いませんが少なくとも符号有無は一致している必要があります(これは、iota_viewの推論補助の制約によって要求されます)。この様な制約は、符号有無が混在した整数型の比較が暗黙変換の結果として意図通りにならなくなる場合があり、それを防止するためのものです。

そのため、上記例のように符号有無が混在した整数値によって指定するとコンパイルエラーとなります。正しくはviews::iota(0u, vec.size())とすべきですが、出力されるエラーメッセージも難しくこの原因を推察するのは容易ではありません。

uptoはシーケンス終端の整数値を1つ指定するだけで、先頭の値はその整数型をデフォルト構築して(0が)補われるため、この問題を回避することができます。

また、同じシーケンスを生成する際にはわずかではありますがviews::iotaよりも短く書くことができ、その意図も明確になります。

P3061R0 WG21 2023-11 Kona Record of Discussion

2023年11月に行われたKona会議の全体会議における議事録。

会議期間中の各SGの作業報告や、LWG/CWGを通過した提案の投票の様子が記載されています。

P3062R0 C++ Should Be C++ - Presentation

P3023の紹介スライド。

EWG/LEWGのメンバに向けてP3023の主張を紹介したものです。

プレゼンテーション用のスライドなので、文章よりも行間が補われている部分があり、主張が分かりやすくなっています。

P3066R0 Allow repeating contract annotations on non-first declarations

関数の最初の宣言にのみ契約注釈を行えるという制限を撤廃する提案。

現在C++26に向けて議論が進められている契約プログラミング機能においては、関数に対する事前条件・事後条件は関数の最初の宣言でのみ行うことができ、たとえ内容が同じだったとしても再宣言で契約注釈を指定する(あるいは再宣言のみで契約を行う)ことはできません。

// 最初の宣言(ヘッダ内など)
int f(const int n)
  pre(n < 100)
  post(r: r == n);

int g();


// 再宣言、f()の定義(翻訳単位内)
int f(const int n)
  pre(n < 100)      // ng
  post(r: r == n)   // ng
{
  return n;
}

int g()
  post(r: -10 < r)  // ng
{
  return 20;
}

これは、同じ関数に対して異なる翻訳単位で異なる契約注釈が行われてしまうことを防止するための制限です。

関数の宣言と定義がヘッダファイルと実装ファイルに分割されている場合、多くのユーザーはヘッダに書いた宣言をコピペして実装ファイルにおける定義を書き始めますが、その関数に契約がなされている場合契約注釈を削除しないとコンパイルエラーになることになります。これは驚くべき動作かもしれません。

クラスのメンバ関数の定義など、関数の宣言と定義が離れていてそこで使用されるエンティティが直接的に見えていない場合、契約注釈がそれを表示しなおかつそれが繰り返されることでコードの可読性を向上させられる可能性があります。

// Widget.h
struct Widget {
  int f() const noexcept
    pre(i > 0);
  
  ...
  
  // much further below:
private:
  int i;
};


// Widget.cpp
int Widget::f() const noexcept
  pre(i > 0)
{
  return i * i; // using i here!
}

元々、C++20で一旦導入されていた契約プログラミング機能では、契約注釈のリストが同じであるという制約の下で再宣言でも契約注釈を行うことができました。初期のMVPにもこれは受け継がれていましたが、後ほど削除されました。なぜなら、当初の仕様では契約注釈のリストについての同一性の定義がなく、どのように同一であるとするのかが不明だったためです。GCCの契約機能の実験実装(C++20の機能ベース)では契約条件式のODRベースの同一性を判定して実装されている様です。

しかし後で、異なる翻訳単位で同じ関数の最初の宣言が複数含まれるプログラムがwell-formeddであるかを指定する必要が出てきたことで、結局契約注釈の同一性の定義を行わなければならなくなった様です。そのため、これは解決すべき問題としてリストアップ(P2896R0)されており、その解決はP2932R2で提案されています。

この提案は、P2932R2で提案されている契約注釈の同一性の定義を採用することで、最初の宣言にある契約注釈を後の宣言で繰り返すことができる様にしようとするものです。

P2932R2で提案されている契約注釈の同一性の定義は次の様なものです

関数宣言d1の契約注釈c1と関数宣言d2上の契約注釈c2は、仮引数名・戻り値名・テンプレートパラメータ名が異なることを除いて、 その述語(契約条件式)p1, p2がそれぞれ宣言d1, d2に対応する関数定義上に置かれた場合にODRを満たしているならば、 c1c2は同じ契約注釈である

この提案はこの定義を採用した上で、C++20時点の仕様だった、関数の後の宣言は最初の宣言と同じ契約注釈を指定するか契約注釈を省略するかのどちらかを行う、というものを復活させることを提案しています

この提案ではあくまでこのことだけを提案していて、最初の宣言で契約注釈を省略して後の宣言でのみ指定する、ことを可能にすることは提案していません。

P3070R0 Formatting enums

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

C++23時点のstd::format()std::print)で自前の列挙型を出力しようとする時、主に2つの方法があります。

namespace kevin_namespacy {

  // フォーマットしたい列挙型
  enum class film {
    house_of_cards, 
    american_beauty,
    se7en = 7
  };
}

// 1. フォーマッター特殊化を定義
template <>
struct std::formatter<kevin_namespacy::film> : formatter<int> {
  auto format(kevin_namespacy::film f, format_context& ctx) {
    return formatter<int>::format(std::to_underlying(f), ctx);
  }
};

int main() {
  using kevin_namespacy::film;

  film f = film::se7en;

  // 2. 基底の整数値を出力
  auto s = std::format("{}", std::to_underlying(f));
}

1つはその列挙型のためにstd::formatterを特殊化してフォーマット方法を定義することです。ただし、この例のように整数型のフォーマッタを再利用したとしてもそれなりの量のボイラープレートコードの記述が必要となります。また、フォーマット方法の定義を同じ名前空間で行うことができず、列挙型とそのフォーマッタの定義が空間的に別れてしまいます。

もう1つはstd::to_underlying()によって列挙値に対応する整数値を取得してそれを出力する方法です。これはフォーマット時に常にstd::to_underlying()の呼び出しが必要となります。

この提案は、これらの方法の欠点を改善したフォーマットのカスタマイズ方法を提案するものです。

この提案では、std::formatに対してformat_as()というカスタマイズポイントを導入することを提案しています。

namespace kevin_namespacy {
  // フォーマットしたい列挙型
  enum class film {
    ...
  };

  // filmのためのカスタムフォーマット定義
  auto format_as(film f) {
    return std::to_underlying(f);
  }
}

format_as()std::format()呼び出し内からADLによって発見される関数であり、フォーマット対象の値(ここでは列挙値)を受け取ってそれを既にフォーマット可能な他の型の値(整数値や文字列など)に変換して返すようにしておく必要があります。

この方法のメリット・目的は次の様なものです

  • 列挙型のためのフォーマットカスタマイズ方法の単純化
  • 列挙型のフォーマット効率を向上
    • 既にフォーマッタ特殊化が存在する場合、フォーマッタを経由しないことでフォーマットのパフォーマンスを向上させられる
  • 後方互換性を確保し、std::formatへの移行を促進する

提案より、他の例

enum class color {
  red,
  green,
  blue
};

auto format_as(color c) -> std::string_view {
  switch (c) {
    case color::red:   return "red";
    case color::green: return "green";
    case color::blue:  return "blue";
  }
}

auto s = std::format("{}", color::red); // s == "red"

この提案ではこれを列挙型に限って有効化することを提案していますが、この仕組みはより一般の型に対して拡張可能です。実際に、{fmt}ライブラリではこの仕組みが列挙型に限らず一般の型に対して有効化された上で出荷されています。

P3071R0 Protection against modifications in contracts

P3071R1 Protection against modifications in contracts

契約注釈内から参照されるローカル変数と関数引数は暗黙的にconstとして扱われるようにする提案。

現在のContracts MVP仕様では、契約注釈内での意図しないプログラム状態の変更に対する保護が欠けているため、それを追加しようとする提案です。次のようなことを提案しています

  • contract context(契約コンテキスト)は契約注釈内の条件式
    • その文法はP2961R2で提案されているnatural syntaxのもの
  • 契約コンテキストの部分式であり、オブジェクト型Tの自動変数、または自動変数に対するT型の構造化束縛を指名するid式は、const T型の左辺値(lvalue
  • 契約コンテキストの部分式であり、自動変数であるTの参照を指名するid式は、const T型の左辺値(lvalue
  • 契約コンテキストの部分式であるラムダ式がコピーによって非関数エンティティをキャプチャする場合、暗黙に宣言された(クロージャオブジェクトの)メンバ型はTだが、ラムダの本体内でそのようなメンバを指名するとラムダがmutableでない限りconst左辺値が返される(これは通常通り)
    • ラムダ式が参照によってそのようなエンティティをキャプチャする場合、その参照を指名するid式は、const T型の左辺値(lvalue
  • 契約コンテキストの部分式で現れるthis式はcv Xへのポインタを示すprvalue
    • cvconstと囲むメンバ関数のCV修飾(存在する場合)との組み合わせ
    • この場合の契約コンテキストの部分式には、非静的メンバ関数の本体内での暗黙変換の結果を含む
  • 契約コンテキストの部分式であるラムダ式Tへのポインタであるthisをキャプチャする場合、暗黙に宣言された(クロージャオブジェクトの)メンバ型はconst Tへのポインタ

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

int global = 0;

int f(int x, int y, char *p, int& ref)
  pre((x = 0) == 0)            // proposal: ill-formed、const左辺値への代入
  pre((*p = 5))                // OK
  pre((ref = 5))               // proposal: ill-formed、const左辺値への代入
  pre(std::same_as_v<decltype(ref), int&>)  // OK; 結果はtrue
  pre((global = 2))            // OK
  pre([x] { return x = 2; }())           // error: xはconst
  pre([x] mutable { return x = 2; }())   // OK, 関数引数xのコピーを変更する
  pre([&x] { return x = 2; }())          // proposal: ill-formed、const左辺値への代入
  pre([&x] mutable { return x = 2; }())  // proposal: ill-formed、const左辺値への代入
  post(r: y = r)               // error: yはconstではないので事後条件で使用できない
{
  contract_assert((x = 0));    // proposal: ill-formed、const左辺値への代入
  int var = 42;
  contract_assert((var = 42)); // proposal: ill-formed、const左辺値への代入

  static int svar = 1;
  contract_assert((svar = 1)); // OK
  return y;
}

struct S {
  int dm;

  void mf() /* not const */
    pre((dm = 1))                         // proposal: ill-formed、const左辺値への代入
    pre([this]{ dm = 1; }())              // proposal: ill-formed、const左辺値への代入
    pre([this] () mutable { dm = 1; }())  // proposal: ill-formed、const左辺値への代入
    pre([*this]{ dm = 1; }())             // error: ill-formed、const左辺値への代入
    pre([*this] () mutable { dm = 1; }()) // OK, *thisのコピーを変更
  {}
};

proposalとコメントされているところがこの提案によって動作が変更されるところです。メンバ関数の契約注釈からthisを使用する場合、それはconstメンバ関数内であるかのように扱われます。

この提案は既にSG21においてMVPに採用することに合意されているようです。

P3072R0 Hassle-free thread attributes

スレッドへの属性指定APIについて、集成体と指示付初期化によるAPIの提案。

P2019ではstd::thread/jthreadに対してスレッド属性(のうちスレッド名とスタックサイズ)を指定できるようにすることを提案しています。そのAPIについては揺れているようで、P2019のリビジョン毎に変化している他、P3022では既存のライブラリに倣った異なるAPIが提案されています。

この提案は、P2019とP3022とも異なるAPIを提案するものです。

P2019R4では、属性ごとに異なる型を用意して、スレッドのコンストラクタ先頭でそれを受け渡します。P3022では、1つのスレッド属性クラスに全てのスレッド属性をまとめて、それをコンストラクタ先頭で受け渡します。

この提案は、P3022のアプローチに近いものですが、P3022とは異なりスレッド属性クラスを集成体として、それをコンストラクタ先頭で渡します。

// P2019R4
std::jthread thr(std::thread_name("worker"),
                 std::thread_stack_size_hint(16384),
                 [] { std::puts("standard"); });

// P3022R0
std::jthread::attributes attrs;
attrs.set_name("worker");
attrs.set_stack_size_hint(16384);

std::jthread thr(attrs, [] { std::puts("standard"); });

// この提案
std::jthread thr({.name = "worker", .stack_size_hint = 16384},
                 [] { std::puts("standard"); });

この提案のAPIは、P3022の利点(属性を1つにまとめられる、既存の慣行に従っている)という点を受け継ぎながら、よりユーザーにとって使いやすい構文で属性を指定することができます。

この実装はまず、std::thread内部にスレッド属性クラスを定義したうえで

class thread {
  ...

public:

  // スレッド属性集成体
  struct attributes {
      std::string const &name = {};
      std::size_t stack_size_hint = 0;
  };

  ...
};

これを受け取るコンストラクタをstd::threadstd::jthreadに追加します

class thread {
  ...

  // 追加するコンストラクタ
  template<class Attrs = attributes, class F, class... Args>
    requires std::is_invocable_v<F, Args...>
  explicit thread(Attrs, F &&, Args &&...);

  ...
};

class jthread {
  ...

  // 追加するコンストラクタ
  template<class Attrs = thread::attributes, class F, class... Args>
    requires std::is_invocable_v<F, Args...>
  explicit jthread(Attrs, F &&, Args &&...);

  ...
};

このようにすることで、先程のようにコンストラクタ引数内での指示付初期化による属性指定が可能になります。さらに、ベンダ定義の独自属性指定を使用することもできます。

std::thread t5(__gnu_cxx::posix_thread_attributes{.schedpolicy = SCHED_FIFO},
               std::puts, "vendor extension");

また、将来属性が増えた場合は新しい属性クラスを用意して、これらのコンストラクタのデフォルトテンプレートパラメータを差し替えることでAPI/ABIの互換性を保ったまま拡張することができます。

P3074R0 constexpr union lifetime

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

この提案の動機は、現在議論中のstd::inplace_vectorを定数式でも利用できるようにしようとするもので、そこでの問題は次のようなものです

template <typename T, size_t N>
struct FixedVector {

  // 単一要素union、ストレージ領域のオブジェクトを構築しない
  union U {
    // storageの各要素の生存期間は外側のFixedVectorが管理する
    constexpr U() { }
    constexpr ~U() { }

    T storage[N]; // 要素が挿入されるまでは初期化したくない
  };

  U u;
  size_t size = 0;

  // note: we are *not* constructing storage
  constexpr FixedVector() = default;

  constexpr ~FixedVector() {
    std::destroy(u.storage, u.storage+size);
  }

  constexpr auto push_back(T const& v) -> void {
    std::construct_at(u.storage + size, v); // ng、u.storageはアクティブメンバではない
    ++size;
  }
};

constexpr auto silly_test() -> size_t {
  FixedVector<std::string, 3> v;
  v.push_back("some sufficiently longer string");
  return v.size;
}
static_assert(silly_test() == 1);

このコードは合法的に動作しないようです。例えば、MSVC/EDG/GCC13.21までは動作しますが、clangや最新のGCCはこれを拒否します。

問題は、共用体のコンストラクタがメンバを初期化していないため、その唯一のメンバstorageオブジェクトの生存期間が開始されていないため、その領域(の一部)を遅延初期化しようとすると非アクティブメンバに対するアクセスになってしまい、これが定数式で許可されていないためにエラーとなることです。

とはいえ、共用体のコンストラクタでstorageを初期化してしまうと、storageの全ての要素の初期化が必要になってしまいます。要素型がデフォルトコンストラクタを持たない場合はこれはエラーになります。また、共用体を利用せずにこのような遅延初期化を行うことはできません(aligned_storageのようなものはTではない別の型の領域を再利用する形になる)。

この提案は、このような共用体使用時の非アクティブメンバアクセスを定数式でも行えるようにしようとするもので、3つのアプローチを紹介しています。

  1. 上記のようなストレージのための特別扱いされたライブラリ型std::uninitialized<T>を提供する
    • まさに上記のU::storageを提供するための汎用型、要素型がimplicit-lifetime typeでないならば各要素の生存期間は自動的に開始されない
    • その初期化と破棄の管理は完全に利用者の責任
    • コンパイラの特別扱いなどにより、上記の問題を回避する
  2. 共用体の最初のメンバがimplicit-lifetime typeならば、共用体の生存期間開始時に暗黙的にそのメンバの生存期間を開始する
    • 上記例の場合、配列オブジェクトstorageの生存期間は開始されるが、各要素の生存期間は開始されず初期化もされない
  3. 初期化を伴うことなく、メンバの生存期間だけをユーザーが明示的に開始する方法を提供する
    • 2の方法の問題点を回避する

2の方法の問題は、共用体が複数のimplicit-lifetime typeメンバを持っていてコンテキストに応じて使い分けたい場合に先頭以外のメンバの生存期間(だけ)を開始する方法がないことです。

union U {
  T x[N];
  U y[M];
} u;

例えばこのような共用体の場合、2の仕様を有効化したとするとU::xだけは定数式で初期化せずに使用できますが、U::yは最初の問題と同じことに悩まされます。そのため、3の方法では暗黙的ではなく明示的に、共用体の特定のメンバの生存期間だけを開始するライブラリ関数を提供することでこれを解決します。

現在の標準ライブラリにはそれに近いことを行ってくれる関数std::start_lifetime_as()がすでに存在しています

template<class T>
T* start_lifetime_as(void* p) noexcept;

しかし、これが行うことは今回解決したい問題の解消とは少し異なっており、いくつか問題があります

  • constexpr指定されていない
  • 実行時に使用されないようにif constevalで囲う必要がある
  • 型名を指定しなければならない
  • 戻り値を使用しなければならない
    • 実装によって[[nodiscard]]が付加される場合警告されてしまう
template <typename T, size_t N>
struct FixedVector {
  union U { constexpr U() { } constexpr ~U() { } T storage[N]; };
  U u;
  size_t size = 0;

  // note: we are *not* constructing storage
  constexpr FixedVector() {
    if consteval {
      std::start_lifetime_as<T[N]>(&u.storage);
    }
  }
};

そこで、この提案では共用体の特定メンバの生存期間を開始することに特化した関数を改めて追加することを提案しています

template<class T>
constexpr void start_lifetime(T*);

ただし、この関数で生存期間を明示的に開始できるのはTimplicit-lifetime typeの場合に限ります。

これを用いると、先ほどの例は次のようになります

template <typename T, size_t N>
struct FixedVector {
  union U { constexpr U() { } constexpr ~U() { } T storage[N]; };
  U u;
  size_t size = 0;

  // note: we are *not* constructing storage
  constexpr FixedVector() {
    std::start_lifetime(&u.storage);
  }
};

また、この関数を利用すると1の方法を簡単に実装することができるようになります。

この提案では、この3番目の方法をメインとして提案しています。

P3075R0 Adding an Undefined Behavior and IFNDR Annex

C++の規格書の付属としてコア言語の未定義動作のリストを追加する手続きについての提案。

P1705R0にてコア言語の未定義動作のリストを規格書に添付することが提案されています。そこでは、未定義動作を少なくとも1つのサンプルコードと共に例示しておくことで、言語の未定義動作を明確にするとともにそのリストをC++コミュニティに対して提供し、各未定義動作についての追跡を容易にすることを目的としていました。また、追加で診断不用のill-formed(IFNDR)のリストも同様に提供しようとしています。

この提案はその具体的なプロセスについてのもので、序文およびリストの各要素がどのような情報を伴うかについて提案するものです。

提案する文書構造としては、現在の規格書のコア言語のグループに準じた形でグループ化したうえで、その下にそのグループに属する未定義動作/IFNDRを要素として配置するものです。

各要素は次のようなレイアウトになります

  • 問題を簡潔にまとめたタイトル
  • メインの規格の関連する部分へのクロスリファレンス
  • 標準のnote形式の問題の概要テキスト
  • 問題が起こる場合を示すサンプルコード

転記はしませんが、UB/IFNDRのAnnexの序文も提案されています。

おわり

この記事のMarkdownソース

[C++] コンパイル時std::stringを実行時に持ち越す方法

コンパイル時動的メモリ確保の制約

C++20でコンパイル時の動的メモリ確保が可能になり、それに伴ってstd::vectorstd::stringが完全に定数式で使用可能になりました。ただしそれには制限があり、コンパイル時に確保したメモリはコンパイル時に返却しなければならないため、定数式で構築したstd::vectorstd::stringのオブジェクトを実行時に持ち越すことはできません(少なくともC++23時点では)。

#include <vector>
#include <string>

int main() {
  constexpr std::vector vec = {1, 2, 3, 4, 5};  // ng
  constexpr std::string str = "non transient constexpr allocation"; // ng
}

これはどちらも、コンパイルエラーとなります。

実行時からも参照可能な様にコンパイル時の定数が残るということはそのデストラクタは呼ばれていないということで、std::vector/std::stringの場合はどちらもデストラクタで確保したメモリを解放するため、このようにconstexpr変数として受けてしまうと定数式で確保されたメモリを解放していないことになり、その様なメモリ確保は定数式で許可されないためコンパイルエラーとなります。

したがって、コンパイル時のstd::vectorstd::stringの可能な使用法はコンパイル時に呼ばれる関数内に閉じた形で利用するなど、あくまで定数式の中だけで参照可能な様に使用することです。

#include <concepts>
#include <ranges>
#include <algorithm>
#include <vector>
#include <array>

using namespace std::ranges;

// 単純な範囲の結合処理
template<input_range R1, input_range R2>
  requires std::same_as<range_value_t<R1>, range_value_t<R2>>
constexpr auto concat_to_vector(const R1& r1, const R2& r2) -> std::vector<range_value_t<R1>> {
  std::vector<range_value_t<R1>> vec{};

  for (const auto& v : r1) {
    vec.push_back(v);
  }

  for (const auto& v : r2) {
    vec.push_back(v);
  }

  return vec;
}

// ok
static_assert(equal(concat_to_vector(std::array{1, 2, 3}, views::iota(4, 7)), std::vector{1, 2, 3, 4, 5, 6}));
#include <concepts>
#include <ranges>
#include <algorithm>
#include <string>

using namespace std::ranges;

// 単純な文字列フォーマット処理
constexpr auto simple_format(std::string_view fmt, std::string_view str) -> std::string {
  // 文字列fmt中の%をstrで置換する
  auto replace = [str](const char& c) -> std::string_view {
    if (c != '%') {
      return std::string_view{&c, 1};
    } else {
      return str;
    }
  };

  std::string result;

  for (char c : fmt | views::transform(replace)
                    | views::join)
  {
    result += c;
  }

  return result;
}

// ok
static_assert(simple_format("Hello % !!", "world") == "Hello world !!");

このことはまた、C++23のstd::unique_ptrに対しても同じことが当てはまります。

コンパイル時SSO!

実は、std::stringに限ってはこの制限を回避することが可能です。

この制限はコンパイル時に確保したメモリをコンパイル時に解放しなければならないことから来ているため、そもそもメモリを確保しなければその様な制限に引っかからないわけです。そして、std::stringはSSO(small string optimization)という短い文字列に対して動的メモリ確保を回避してそのオブジェクト内に文字列を保持する最適化が行われています。

コンパイル時のstd::stringでもSSOが実装されている場合、短い文字列に対してはコンパイル時にもメモリ確保が行われないため、SSOが行われているstd::stringオブジェクトは実行時に持ち越すことができます。

#include <string>
#include <concepts>
#include <ranges>
#include <print>
#include <string_view>

using namespace std::literals;

constexpr auto make_string(char init, std::unsigned_integral auto N) -> std::string {
  std::string str;
  
  for (auto i : std::views::iota(init, init + char(N))) {
    str += i;
  }

  return str;
}

constexpr std::string cstr = make_string('a', 10u);

int main() {
  static_assert(cstr == "abcdefghij"sv);  // なぜかgccのみng
  std::println("{:s}", cstr);
}

現時点で最新のコンパイラバージョンが必要ですが、これはC++の主要3実装で利用可能です。

最大文字数

SSOはstd::string内部に文字列を保持するため、SSOが行われる文字列の最大長はstd::stringのサイズ以下になります。実装の制限などが加わることでその長さは減り、主要3実装ではコンパイル時SSOが可能な文字列長(charの場合かつ\0は除く)は次の様になります

  • Clang libc++ : 22
  • GCC libstdc++ : 15
  • MSVC STL : 15
#include <string>
#include <ranges>
#include <print>

constexpr auto make_string(char init, std::unsigned_integral auto N) -> std::string {
  std::string str;
  
  for (auto i : std::views::iota(init, init + char(N))) {
    str += i;
  }

  return str;
}

// 各実装における最大容量
#ifdef __clang__
  constexpr std::size_t length = 22;
#elif defined(__GNUC__)
  constexpr std::size_t length = 15;
#elif defined(_MSC_VER)
  constexpr std::size_t length = 15;
#else
  #error not covered
#endif

constexpr std::string cstr = make_string('a', length);

int main() {
  std::println("{:s}", cstr);
}

ただしこのことは、定数式におけるstd:stringの使用時にその実装詳細が露呈しコードの可搬性に影響を与えるとして、バグではないかとする向きもあります。

// clangのみok、gcc/msvcはng
constinit std::string contant_init_str = make_string('a', 16);

どういう文字列で初期化するかはともかくとして、この様なコードは普通に書かれる可能性があります。その場合に、SSOの制限によってエラーになったりならなかったりするのは非自明なことかもしれません。

実装状況

std::stringのSSOはほぼ全ての実装で行われていますが必須ではなく、コンパイル時のstd::stringに対しても求められていません。そのため、ほとんどの実装ではstd::stringconstexpr対応よりも遅れてコンパイル時SSOへの対応が行われています。また、これは実装品質の問題であるため個別の提案による機能に比べて扱いが小さく、どのバージョンから導入されたのか分かりづらいものがあります。

ここに、主要3実装においてstd::stringコンパイル時SSOが利用可能になる最小のバージョンについてメモしておきます

  • Clang libc++ : Clang 18.0
  • GCC libstdc++ : GCC 14.1
  • MSVC STL : VS2022 17.4

ただし、GCC(libstdc++)の現時点の実装では、実装の都合上ローカルconstexpr変数で使用できません。

// clang/gcc/msvc全てでok
constexpr std::string global_str = "global";

int main() {
  // gccのみng
  constexpr std::string local_str = "local";
}

実装について

主要3実装におけるコンパイル時SSO実装はどれも、実行時のSSO実装をコンパイル時にも利用できるように調整したもので、その仕組みは実行時のものと変わりありません。したがって、SSO可能な最大文字数も実行時とコンパイル時で共通しています。

実装についてはこちらの記事を参照

主要3実装はそれぞれ異なる実装を取っているわけですが、GCCの実装はstd::stringオブジェクト内部にSSO文字列へのポインタを保持する様な実装になっています。これは実行時に条件分岐を削減するためのものと思われますが、このことがローカルconstexpr変数としてSSOstd::stringを保持することを妨げています。

GCCstd::stringのSSO実装のエッセンスを抽出すると、次の様になっています

// GCCのstd::string SSOの簡易実装例(libstdc+++の実装を改変)
strucy string {
  ...

  // EBOによりアロケータサイズを圧縮
  struct _Alloc_hider : allocator_type {
    ...

    char* ptr;  // 文字列領域へのポインタ
  };

  _Alloc_hider dataplus;      // アロケータと文字列ポインタ
  std::size_t string_length;  // 文字列長

  // SSO最大長の定数
  enum { local_capacity = 15 / sizeof(char) };

  // ローカルバッファとキャパシティをオーバーラップさせる
  union {
    char local_buf[local_capacity + 1]; // SSOの場合のバッファ
    std::size_t allocated_capacity;     // 非SSOの場合のキャパシティ
  };

  ...

  // 文字列リテラルを受け取るコンストラクタ
  constexpr basic_string(const char* str, const allocator_type& alloc = _Alloc())
    : dataplus(+local_buf, alloc) // SSOバッファのポインタを保存
  {
    const std::size_t len = std::char_traits<char>::length(str);

    if (len > local_capacity) {
      // SSO最大長を超える場合、動的メモリ確保を行う
      auto [new_ptr, new_capacity] = allocate_new_memory(len);  // allocate_new_memory()はメモリ確保と初期化を行う架空の関数

      dataplus.ptr = new_ptr; // 確保した領域へのポインタを保存
      allocated_capacity = new_capacity;
    }

    // 文字列コピーなど残りの処理
    ...
  }

  ...

  constexpr const char* data() const {
    return dataplus.ptr;  // 分岐が必要ない
  }
};

これはGCCの現在のstd::string実装の一部を抜き出して見やすく整えたものです。

_Alloc_hider::ptrはSSOされているかに関わらず文字列領域の先頭を指すポインタであり、SSOが行われている場合はstd::string内部にあるlocal_bufへのポインタとなります。

std::stringがローカル変数である場合、local_bufはローカル変数内のストレージであり、_Alloc_hider::ptrの指すポインタはスタック領域を指すポインタとなります。

実行時の場合、スタック領域へのポインタ(ローカル変数の配置場所)は関数実行のたびに変化します。それが定数式でも同様でしょうが、定数式におけるその様なポインタがconstexpr変数に保存されて実行時に参照される場合それはどうなるでしょうか?スタック領域へのポインタであるため実行のたびに変化しますが、一方でそれはconstexpr変数でありコンパイル時に決定される値であるはずです。

その様な矛盾を表面化させないために、現在のC++はこのような定数式におけるローカルなポインタが実行時に残るような変換や式の実行を禁止しています。これにより、GCCstd::stringのSSO実装はローカルconstexpr変数の場合に定数式で実行できずにコンパイルエラーとなります。

これがstatic変数あるいはグローバル変数であればこの様な問題は起こらないため、静的ストレージに配置されるconstexprstd::stringではSSOが利用可能です。

ちなみに余談ですが、これと同様のことが構造化束縛のconstexpr宣言を許可する議論の過程で問題となっており、そこでは定数式におけるポインタ/参照の実装戦略として記号的なアドレス指定(Symbolic addressing)と呼ばれるアドレス値の使用を回避した新しい実装方法が検討されています。

これが導入されると、この問題も解消されるかもしれません。

std::vectorの場合

std::vectorは基本的にSSO(SOO)の様なことが行われることはありません。ただし、std::vectorにも構築後にメモリ確保を全く行わない状態があり、その場合は実行時に持ち越すことができそうです。

#include <vector>

int main() {
  constexpr std::vector<int> cvec{};
}

std::vectorのデフォルトコンストラクタはせいぜいそのメンバを初期化するくらいのことしか行わないため、動的メモリ確保が走ることはありません。主要3実装はこれを許可する様です。

一見これは無意味に見えますが、これができるということはこの初期化は定数式で完了しているということでもあり、次の様にすると用途がありそうに見えてきます

// 初期化のみをコンパイル時に終わらせる
constinit std::vector<int> constant_init_vec{};

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で80本あります。

もくじ

N4961 2024-03 Tokyo meeting information

2024年3月末に東京で行われるWG21全体会議の案内。

N4962 WG21 agenda: 6-11 November 2023, Kona, HI

2023年11月にハワイのコナで行われるWG21全体会議のアジェンダ

N4963 2023 WG21 admin telecon meetings, rev. 2

11月の全体会議に先立って行われる、WG21管理者ミーティングの案内。

N4964 Working Draft, Programming Languages -- C++

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

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

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

新規に採択された提案はなく、編集上の変更のみです。また、C++23 DISに対するNBコメントの対応についても記されています。

P0447R23 Introduction of std::hive to the standard library

要素が削除されない限りそのメモリ位置が安定かつメモリ局所性の高いコンテナであるstd::hive(旧名std::colony)の提案。

以前の記事を参照

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

  • Apendixのconstexprの使用法に関するセクションの更新
  • 「Design Decisions」セクションのbit_castreinterpret_castに置き換え
  • 「Erased-element location recording mechanism」セクションにどのブロックが消去されているかを追跡するためのさまざまなメカニズムに関するセクションを追加
  • 「Collection of element memory blocks + metadata」セクションの修正
  • 提案の最初の方にstd::hiveの構造に関するインフォグラフィックを追加
  • <=>を定義しないことについてのメモを削除
  • 標準の他の成長因子の記述と一致させるために、概要から、1より大きい成長因子を削除し、整数である必要はないを追加
  • 概要の'poem'を削除
  • LWGのフィードバックに基づいて、時間計算量を現在の実装の最大値で指定した(後から調整可能だが、ABI破壊が懸念される)
  • 小さな型をオーバーアライメントすることなく全ての型に対してO(1)で消去可能であるため、消去処理の時間計算量要件を削除
  • その他個人からのフィードバックの適用
  • 時間計算量に関するその他の修正
  • 代替実装方法の詳細を更新
  • 他の部分でカバーされていたため、clear()の説明を削除。同様に消去処理の説明も削除

などです。

P0876R14 fiber_context - fibers without scheduler

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

以前の記事を参照

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

  • uncaught_exceptions()current_exception()への変更を撤回し、結果が現在のスレッドで実行されている他のファイバーの例外を反映する可能性があることを明確化
  • “Cooperative Threads”セクションのタイトルから“User-Mode”を削除し、説明段落を削除
  • スタック領域を指定するコンストラクタからexplicitを削除
  • スタック領域を指定しないコンストラクタの例外条件にsystem_error: resource_unavailable_try_againを追加
  • スタック領域を指定するコンストラクタの例外条件にsystem_error: resource_unavailable_try_againを追加
  • ムーブ操作がムーブ元のfiber_contextの状態を空にすることを規定
  • 代入演算子からempty()事前条件を追加し、デストラクタと同様に! empty()効果(事後状態)を追加
  • “execution context.”へのresume_with()からの参照を削除
  • resume_with()のReturnsとThrowsの節を削除
  • can_resume()による複数スレッドからの呼び出しに関するRemarksを削除
  • 説明専用のメンバの型をvoid*へ変更
  • メンバ名等の調整
  • 機能テストマクロのセクションを移動
  • ヘッダ概要をクリーンアップ
  • 前方参照を持つクラスメンバをグループ化
  • std::swapの特殊化を追加
  • 段落番号を追加
  • 単一項目のダッシュリストを合理化
  • EnsuresをPostconditionsに変更
  • テンプレートパラメータの宣言のtypenameclassに変更
  • コンストラクタ規定の調整
  • entry_copy, stack_copy, deleter_copyfiber_constextのメンバになることを意図していないことを明確化
  • 説明専用オブジェクトの初期化の合理化
  • “Instantiates a fiber_context”を“Initializes state”へ言い換え
  • “empty() returns true”を“empty() is true”へ言い換え
  • スタック領域を指定するコンストラクタから、スタックサイズとアライメントについての事前条件を削除し、例外条件を例外種別ごとに記述
  • ムーブコンストラクタのEffectsを調整

などです。

P0952R1 A new specification for std::generate_canonical

std::generate_canonicalの仕様を改善する提案。

std::generate_canonicalの現在の規定は、浮動小数点数の仕様を無視しているため誤って制約されています。それによって実質的に正しい実装が不可能になっています。この提案では主に2つの問題について指摘しています。

この関数の動作は次の3つの要件に従うはずです

  1. 結果は[0, 1)に入らなければならない
  2. アルゴリズムは正確に指定されており、使用するURBGは指定された引数に対して特定の固定回数だけ呼び出される必要がある
  3. 結果は一様分布となる

1つ目の問題は、これらの要件を満たしながらの実装が不可能である点です。現在の規定は、要件2を満たすように正確に数式によって指定されており、結果であるS/Rkは数学的には1未満の値を返します。しかし、浮動小数点数によってこの式が実行される場合、丸めによってこの結果は正確に1になる場合があります。このことは、generate_canonicalから取得したxに対して(1.0 - x)で除算している場合にバグを引き起こします。

すなわち、現在の規定では要件2を満たそうとすると、要件1に違反します。結果が1になってしまった場合に結果を修正しようとすれば要件3に違反します。要件1と3を満たすようにすると、結果が1となる場合にアルゴリズムを再実行する必要があり、それは要件2に違反します。

2つ目の問題は、除算と浮動小数点丸め(最近接偶数丸め)が組み合わさることで出力が均一とならない場合があることです。例えば、0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5という値に対して丸めを行うことを考える場合、最下位ビットを切り捨てれば0, 0, 1, 1, 2, 2, 3, 3となりこれは一様ですが、浮動小数点数において一般的な最近接偶数丸めを行うと0, 0, 1, 2, 2, 2, 3, 4となり偶数と奇数のバランスに偏りが生じる他、0にもバイアスが生じています。

これと同じことがstd::generate_canonicalの計算の過程で発生し、最下位ビットあるいはその周辺の数ビットにおいて結果のバイアスが生じることで最終的な結果の均一性が失われます(この辺りの説明はこの記事を書いている人が理解できていないので、提案の方を参照されるといいと思います・・・)。

この提案は、この2つの問題をstd::generate_canonicalのインターフェースを変更することなく解決するために、アルゴリズムや計算量についての規定を変更することで改善しようとするものです。

この提案は一部の引数の解釈やアルゴリズムそのものを変更することによって、non-trivial divisionを使用せず再近接偶数丸めの問題を回避しようとしています。そのため、副作用や計算量が変化し、また、同じ入力から生成される乱数列も変更前と異なることになります。そのため、それらに依存する既存のコードの動作は実行時に壊れます。

ただし、この提案ではstd::generate_canonicalの結果が区間[0, 1)内でRealTypeの表現可能な全ての値が含むようにすることは、定義が困難かつ実装が複雑化するとして行っていません。

P1144R9 std::is_trivially_relocatable

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

以前の記事を参照

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

  • 機能テストマクロ(__cpp_impl_trivially_relocatable__cpp_lib_trivially_relocatable)を追加
  • §3.1 "Design non-goals"セクションを追加し、P2785R3との比較を行う
  • Appendix C "Examples of non-trivially relocatable class types"を短い要約で置き換え
  • Appendix E "Open questions"は§5.2 "Confusing interactions with std::pmr"と重複しているため削除

などです。

P1729R3 Text Parsing

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

以前の記事を参照

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

  • イテレータではなくsubrangescanから返すようにした(scanの正常値のscan_resultsubrangeを内包するようになった)
  • formatterとの一貫性のため、scannerCharTテンプレートパラメータのデフォルトをcharにした
  • 数字文字列の3桁区切り(千毎の区切り)のパースフラグに関するオプションの設計議論を追加
  • 追加のエラー情報に関する設計の議論を追加
  • § 4.3.4 Width and precisionに幅と精度に関する説明を追加
  • § 2 Introductionの最後にこの機能の目的について追記
  • § 3.5 Alternative error handlingのエラー処理例を修正し明確化
  • § 4.2 Format stringsにwhitespaceの定義を追加し、whitespace以外のリテラル文字の一致を明確化
  • テキストエンコーディングに関するセクション§ 4.11 Encodingを追加し、読み取りコードユニットの処理に関するセクション§ 4.3.8 Type specifiers: CharTを追加
  • § 4.10 Localesにロケールの使用例を追加
  • 将来の拡張に関して§ 6.3 Reading code pointsを追加

などです。

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

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

以前の記事を参照

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

  • mask reductionの事前条件を削除。ただし、その決定の取り消しをLEWGに要求する予定
  • basic_simd_maskの単項演算子の戻り値型を修正
  • simd-select-implboolオーバーロードを修正
  • simd_splitにおける不必要な実装の自由度を削除
  • テンプレートパラメータの宣言時はtypenameではなくclassを使用する
  • ブロードキャストコンストラクタのconstexpr-wrapper-like引数の値に関するSFINAEの実装
  • basic_simd_maskに関係演算子を追加
  • size_tintの使用法に関するセクションを追加
  • 未解決の設計に関するセクションをすべて削除し、LWG向けの文言に関する質問のみを残すようにした
  • 実装ノートにLWGへの質問を追加
  • オーバーロード候補を削減するために二項演算子に制約を追加

などです。

P2019R4 Thread attributes

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

このリビジョンでの変更は、R3で提案されたmake_with_attributes()のアプローチがLEWGに好まれず、既存のstd::threadコンストラクタ呼び出しの前に属性を追加できるようにする方向性が好まれ、また、型がスレッド属性であることを検出する実装方法を提供すべきではないとされたためmake_with_attributes()およびthread_attribute基本クラスが削除されたことです。

このリビジョンでは、当初のように専用のクラスの値としてスレッド属性を渡しますが、その位置はスレッドで実行する処理の前になりました。

namespace std {
  
  // スレッド名を指定するクラス
  class thread_name {
  public:
    constexpr thread_name(std::string_view name) noexcept;
    constexpr thread_name(std::u8string_view name) noexcept;
  private:
    implementation-defined __name[implementation-defined]; // 説明専用
  };

  // スタックサイズを指定するクラス
  class thread_stack_size {
  public:
    constexpr thread_stack_size(std::size_t size) noexcept;
  private:
    constexpr std::size_t __size; // 説明専用
  };

  class thread {
    ...

    // デフォルトコンストラクタ(元からある
    thread() noexcept;

    // 処理とその引数を受け取るコンストラクタ(元からある
    // template<class F, class... Args>
    // explicit thread(F&& f, Args&&... args);

    // 属性、処理、その引数、を受け取るコンストラクタ
    template<class... Args>
    explicit thread(Args&&... args);

    ...
  
  private:
    template <class F, class... Attrs>
    thread(attribute-tag, F && f, Attrs&&... attrs); // 説明専用
  };

  // jthreadも同様
}
void f();

int main() {
  // スレッド名とスタックサイズの指定
  auto thread = std::jthread::(std::thread_name("Worker"),
                               std::thread_stack_size(512*1024),
                               f);
}

処理に渡す引数がある場合は、今まで通りこのスレッド処理の後ろに指定します。

P2075R3 Philox as an extension of the C++ RNG engines

<random>にPhiloxというアルゴリズムベースの新しい疑似乱数生成エンジンを追加する提案。

以前の記事を参照

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

  • 純化のためにいくつかのエイリアスを削除
  • 渡される値の数を制御するためにstd::counterAPIを変更
  • set_counter, get_counterの機能とAPIについて説明する設計上の考慮事項に関するセクションを拡張
  • コードの移植性を維持するために、set_counterに渡されるWord値の解釈についてリトルエンディアンであるという指定を削除
  • counter_sizeの計算ロジックの曖昧さを回避するために、pseudo_random_functionコンセプトをcounter_sizeパラメータを使用して拡張

などです。

P2267R0 Library Evolution Policies

C++標準ライブラリ設計のためのポリシーについて検討する提案。

ここでのポリシーとは、C++標準ライブラリの将来のユーティリティの提案者が従うべき技術ルールもしくは技術ガイドラインのことです。

この提案では、ポリシーの利点と欠点とを勘案しながら、LEWGのレビューを通過した提案全てに適用されるものとしてのポリシーを設定し、それを常設文書(SD-9)として設定することを提案しています。ただし、提案しているのはポリシーの確立と文書化のプロセスのみで、ポリシーそのものは例示するに留めています。

ポリシーを設定することの利点と欠点としては次のようなものが挙げられています

  • 利点
    1. ポリシーによって、標準ライブラリの様々な部分の動作におけるユーザーの期待に対して統一性を持たすことができる
    2. ポリシーによって、提案の著者とレビューする委員会の両方の時間が節約される
    3. ポリシーは共有された知識ベースから作成される必要がある(それによって、それら知識の断片化や解釈の不一致を最小限に抑えることができる)
    4. ポリシーによって、標準化プロセスを新規参入者にとって優しいものにすることができる
  • 欠点
    1. ポリシーは、委員会内で少数派領域の代表の意見を押し退けてしまう可能性がある
    2. 提案された一部のユーティリティに対しては、ポリシーによって誤った技術的解決策を強制してしまう可能性がある

欠点でも触れられているように、単一の原則が全てに適合することはなく、あるポリシーについてライブラリ全体の合意に達することには困難が伴います。この提案では、設定されたポリシーは常に強制されるものではなく、ポリシーに違反していることを明示しその理由について説明されていれば、ポリシーを常に守る必要はない、という運用にすることを提案しています。

小さなポリシー1つをとってもライブラリ全体での合意を得るには時間がかかりますが、そのような議論を提案ごとではなくポリシーを確立する1度だけ行っておくことで、長期的には時間の節約になり他の利点を得ることもできる、としています。

P2447R5 std::span over an initializer list

std::spaninitializer_listを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを(再)追加したこと、Annex Cのための文言を追加したことなどです。

この提案は2023年11月のKona会議において採択され、C++26 WDにマージされています。

P2500R2 C++ parallel algorithms and P2300

P2300で提案されている実行コンテキストの指定を、C++17の並列アルゴリズムにも適用できるようにすることを目指す提案。

以前の記事を参照(番号が間違って公開されたとのことで提案番号が変更されています)

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

  • 逐次実行をデフォルトとするようにexecute_onの実装を変更
  • コンパイルエラーまたは実行時エラーを起こすexecute_onのカスタマイズを許可
  • アルゴリズムexecute_onのカスタマイズを行う例を追加
  • 提案しているアプローチの拡張性に関するサブセクションを追加
  • 遅延for_eachアルゴリズムの可能なシグネチャを追加

などです。

P2542R6 views::concat

同じ要素型を持つ異なる型の範囲を連結するRangeファクトリ、views::concatの提案。

このリビジョンでの変更は、!common_range && random_access_range && sized_rangeに関するセクションを追加したことです。

!common_range && random_access_range && sized_rangeという条件は、これを入力範囲の最後のrangeが満たしている場合にconcat_viewcommon_rangeになるべきではない、という条件でした。これを反映してconcat_viewcommon_rangeとなるかどうかは入力の最後のrangecommon_rangeであるかによっていました。

しかし、LWGのレビューにおいてconcat_viewbidirectional_rangeとしての動作はcommon_rangeの動作と一貫する必要がある事が示されました。これはすなわち、!common_range && random_access_range && sized_rangeを満たすような範囲をサポートしないことを推奨するものです。

これによるメリットは、concat_viewの実装および標準文言が簡素化されることがあります。また、random_acces_rangeとなる場合についても同様の要求をすることでさらに実装を簡素化することができます。

LEWGではその方向性が支持され、次のリビジョンにおいて適用されるようです。

!common_range && random_access_range && sized_rangeであるような範囲をconcatしたい場合でも、一度views::commonに通しておけばconcatすることができます。

P2642R4 Padded mdspan layouts

std::mdspanpadding strideをサポートするためのレイアウト指定クラスを追加する提案。

以前の記事を参照

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

  • 標準ドラフトのレイアウトに従うように修正
  • submdspanの文言を書き直し

などです。

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

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

以前の記事を参照

このリビジョンでの変更は、P1928R6でstd::simdに加えられた変更を適用したことです。

P2664R4 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

  • P1928R3に合わせて、simd/simd_maskbasic_simd/basic_simd_maskに変更
  • 推定されたマスクをネストされたtypedefに置き換え
    • basic_simd_mask<T, MaskABI>の代わりにbasic_simd<T, ABI>::mask_typeを使う
  • fixed_size型を対応する新しい型に置き換え
    • 例えば、fixed_size_simd<T, 5>simd<T, 5>になる
  • 圧縮及び展開で異なるサイズのsimd値を取る事の出来る設計オプションを削除
  • マスクパラメータの位置を並べ替え
    • マスクを最初に指定するようになり、simd_selectcopy_toなどと一貫するようになった

などです。

P2717R3 Tool Introspection

C++周辺ツールが、Ecosystem ISにどれほど準拠しているのかを互いに通信する手段を標準化する提案。

以前の記事を参照

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

  • 2023年6月の会議における投票結果を追記
  • イントロスペクション情報を取得するためのプログラム実行が望ましくない場合などでもその情報を提供するために、イントロスペクションファイルを使用できるようにした
  • 2種類のコマンドラインオプション構文のサポートを追加して、オプション構文解析を変更せずにこの提案の内容をサポートできるようにした
  • バージョンについて、セマンティックバージョニングを採用

などです。

P2762R2 Sender/Receiver Interface For Networking

現在のNetworking TSにP2300のsender/receieverサポートを入れ込む提案。

以前の記事を参照

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

  • Obtaining the Schedulerセクションにソケットコンテキストの利用について追記
  • schedulerインターフェースが非標準の操作に関して拡張可能である必要があるという要件について追記
  • コルーチンをより分かりやすくするために、awaitablesenderに関する説明を追加

などです。

P2774R1 Concurrent object pool (was: Scoped thread-local storage)

並行アルゴリズムにおけるキャッシュとして使用可能な並行オブジェクトプール機能の提案。

以前の記事を参照

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

  • SG1のレビューを受けて再設計
  • 設計をTLSから並行オブジェクトプールに向けたものに変更
  • 設計はstd::atomicで表現できる範囲に制限されなくなった

などです。

以前はTLSの観点から機能を定義し説明していましたが、このリビジョンでは並行アルゴリズムから使用されるオブジェクトプールとして機能を再定義しています。それに伴って、クラス名がstd::object_pool<T>へ変更されるとともにインターフェースも変更されています。

とはいえ、基本的な役割やモチベーションには変化がありません。

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

// 入力データ
std::span<Triangle> input = …;
double max_area = …;

// 並行オブジェクトプール
std::object_pool<std::vector<Triangle>> tmp;

// メインの並行処理
std::for_each(std::execution::par, input.begin(), input.end(),
  [&](const auto & tria) {
    // スレッド固有の領域を初期化
    auto handle{tmp.lease()};

    // スレッドローカルvectorを取得
    auto& object = *handle;

    // 結果(複数)をスレッドローカルな領域へ出力
    for (const auto & t : split(tria, max_area)) {
      object.emplace_back(t);
    }

    // handleのデストラクタによってobjectが再利用可能になる
  }
);

// 後処理、シングルスレッド
for(const auto & tria : tmp.lease_all() | std::views::join) {
  process(tria);
}

std::object_poolの実装例

template<default_initializable T, typename Allocator = allocator<T>>
class object_pool {
  mutable mutex mutex;
  mutable intrusive_list<T, Allocator> storage;

  class handle; // see below
  class snapshot; // see below

public:
  object_pool(Allocator allocator = Allocator{}) noexcept;
  object_pool(const object_pool &) =delete;

  auto operator=(const object_pool &) -> object_pool & =delete;
  ~object_pool() noexcept;
  
  // Tの単一オブジェクトの所有権を取得、プールが空なら新しいオブジェクトを割り当てる
  [[nodiscard]]
  auto lease() const -> handle; 
  
  // 現在のプールで利用可能なすべてのTオブジェクトの所有権を取得
  [[nodiscard]]
  auto lease_all() const noexcept -> snapshot;
};

std::object_pool::handleはRAIIによってプール内の単一オブジェクトへのアクセスを管理するクラスです。

実装例

template<default_initializable T, typename Allocator>
class object_pool<T, Allocator>::handle {
  object_pool & owner;
  typename decltype(storage)::node_type object;
public:
  handle() =delete;
  handle(const handle &) =delete;

  auto operator=(const handle &) -> handle & =delete;
  ~handle() noexcept;
  
  // プール上のある1つのTオブジェクトにアクセス
  auto operator*() const noexcept -> T &;
  auto operator->() const noexcept -> T *;
  auto get() const noexcept -> T *;
};

同様に、std::object_pool::handleはプール内の複数(全て)のオブジェクトへのアクセスを管理します。上の例のように、これは並行処理の後でプールにためたデータを消費する後処理において使用することを意図しています。

template<default_initializable T, typename Allocator>
class object_pool<T, Allocator>::snapshot {
  vector<handle> handles;
public:
  snapshot() =delete;
  snapshot(const snapshot &) =delete;

  auto operator=(const snapshot &) -> snapshot & =delete;
  ~snapshot() noexcept;
  
  class iterator { … };
  static_assert(forward_iterator<iterator>);
  
  auto begin() noexcept -> iterator;
  auto end() noexcept -> iterator;
};

P2786R3 Trivial Relocatability For C++26

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

以前の記事を参照

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

  • EWGをターゲットとし、LEWGをフォローアップ
  • 主要提案としてタイトルを変更
  • ライブラリ拡張機能をP2959R0とP2967R0へ移動
  • 文書レイアウト等の調整
  • モチベーションとなる使用例を追加
  • 前のリビジョンでの変更を考慮して例を修正
  • 提案する文言を完成させた
  • 未解決の質問セクションを追加
  • trivially_relocate()関数からconstexprを削除

などです。

P2800R0 Dependency flag soup needs some fiber

依存ライブラリとしてモジュールを使用する際に、必要となる情報について説明する文書。

この文書では、あらゆるプロジェクトにおいて共通する、そのプロジェクトを有意義に活用するために必要な一連の手順があり、その手順のために必要な各種情報(コンパイラ/リンカオプション、動的ローダーの環境変数プラグインローディング環境変数などを通じて伝達される傾向にあるあらゆるもの)のことをプロジェクトの使用要件(usage requirement)と呼んでいます。この使用要件を発見することはビルドシステムの仕事の中核をなしています。

また、その使用要件を収集するための(ツールチェーンに依存する)方法のことをフラグスープ(flag soup)と呼んでいます。

この文書は、現在のフラグスープに代わるより構造化された使用要件の伝達手段の必要性を説くものです。

文書の意図は、モジュールを使用するクライアントはクライアントによって異なる使用要件を持ち、同じプロジェクト内でさえも同じモジュールに対して翻訳単位ごとに異なるBMIを取得する可能性があり、現在のフラグスープではその用途のために十分でないことを示すことにあります。また、そのような使用要件をより完全に伝達することができるようにすることで、BMIを再利用しやすくなるメリットもあります。

P2809R2 Trivial infinite loops are not Undefined Behavior

自明な無限ループを未定義動作ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、寄せられたフィードバックの適用などです。

EWGにおける投票では、提案する2つのうちオプション2を採用する方に合意され、CWGに転送されています。

P2841R1 Concept Template Parameters

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

以前の記事を参照

このリビジョンでの変更はよくわかりませんが、構文等に変更はないようです。

P2845R3 Formatting of std::filesystem::path

P2845R3 Formatting of std::filesystem::path

std::filesystem::pathstd::format()でフォーマット可能にする提案。

以前の記事を参照

R3での変更は

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

P2862R1 text_encoding::name() should never return null values

std::text_encoding の提案(P1885R12)に含まれる text_encoding::name() の仕様を変更する提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなるドラフトをP1885R12がマージされたN4958に更新した事です。

P2863R2 Review Annex D for C++26

現在非推奨とマークされている機能について、C++26で削除/復帰を検討する提案。

以前の記事を参照

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

  • この文書の変更履歴と文書内の派生提案の参照時にリビジョン番号を付加するようにした
  • 派生提案のステータスを更新

などです。

P2885R3 Requirements for a Contracts syntax

C++契約プログラミング機能の構文の選択のための評価基準や要件をまとめる文書。

以前の記事を参照

このリビジョンでの変更は、この提案の採択の投票後に寄せられた構文要件をAppendix Aに追加したことです。

追加された構文要件は次の3つです

  1. 契約注釈に対する属性の指定
  2. 契約ラベルの事前宣言
    • ラベルやメタアノテーションを後の宣言にのみ付加する場合に、その存在を最初の宣言でほのめかす構文が必要となる
  3. 関数型に対する契約注釈
    • 関数型に対して契約注釈を行う方法を提供する必要がある
    • 関数そのものと関数型に対するものとの間で、構文上の曖昧さがあってはならない

 

P2893R1 Variadic Friends

friend宣言でのパック展開を許可する提案。

以前の記事を参照

このリビジョンでの変更は、clangのフォークでこの提案を実装したうえでCompiler Explorerから利用可能になったことへの謝辞を追加したことです。

P2897R1 aligned_accessor: An mdspan accessor expressing pointer overalignment

mdspanのアクセサポリシークラスに、参照する領域ポインタにstd::assume_alignedを適用してアクセスするaligned_accessorの提案。

以前の記事を参照

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

  • gcd変換コンストラクタのConstraintをMandateに変更
  • is_sufficiently_alignedを利用してポインタのオーバーアライメントの事前条件をチェックする例を追加
  • aligned_allocを使用してオーバーアライメントされたメモリ確保を行う例を追加
  • aligned_mdspan y{x.data_handle(), x.mapping()}の代わりにaligned_mdspan y{x}のように使用できるように、default_accessorからのexplicitコンストラクタを追加
  • aligned_accessorのMandateにbyte_alignment >= alignof(ElementType)trueであることを追加。これにより、無効なaligned_accessorオブジェクトの構築が防止される
  • aligned_mdspanエイリアスを含めない理由の説明
  • aligned_accessor構築の安全性について説明

などです。

P2900R0 Contracts for C++

P2900R1 Contracts for C++

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

C++20で一度標準入りしてから撤回され、MVPという最小仕様を確立すべくSG21で議論を重ねられてきました。この提案はC++26に向けて現時点でのContracts MVP仕様をまとめ、それを正式な契約プログラミング機能としてC++26に導入するためのものです。

この提案は大きく設計と文言のセクションに分かれており、設計セクションではMVPの契約機能の構文と意味論について説明され、文言セクションでは設計セクションで説明された機能をC++言語で有効化するための標準のための文言の変更が記載されています(現時点では文言は未整備ですが)。設計セクションを読むことでC++26契約プログラミング機能がどういうものかを把握することができるでしょう。

ただし、現在のところ構文がまだ決まっておらず、いくつか小さめの未解決の問題が残されているため、まだ完全なものではありません。

P2911R1 Python Bindings with Value-Based Reflection

提案中の値ベースリフレクションによってPythonバインディングの作成を簡素化できることを示す文書。

以前の記事を参照

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

  • この提案が対象としない事項について明確化
  • 関数引数名のリフレクションとそれによって無効なリフレクションエラーが発生する場合についての議論の追加
  • デフォルトのバインディングによって引き起こされる危険な動作のよりよい例を追加
  • リフレクションされたエンティティの範囲を拡張して、その名前の範囲を生成するためのmeta::name_of/meta::names_of関数の詳細な説明
  • オーバーロードされた演算子バインディングに関するセクションを追加
  • Classdesc frameworkと値ベースリフレクション、および将来の作業とを比較するセクションの追加

などです。

P2927R0 Observing exceptions stored in exception_ptr

std::exception_ptrを再スローせずに例外オブジェクトの取得を試みる関数の提案。

std::exception_ptrは例外オブジェクトを型消去して可搬にすることができるラッパー型であり、例外オブジェクトを取り扱いやすくすることに貢献します。しかし、std::exception_ptrはかなり制限されたAPIしか持たず、現在参照している例外オブジェクトの型の情報などを取得することができません。

それによって、他のエラー伝達手段と比較してエラーを処理するための方法が制限されます。例えば、std::optional/std::expectedのようなモナディックインターフェースを実装しようと思うと、std::exception_ptrの参照する例外オブジェクトを一旦再スローし、ハンドルされない場合はキャッチして再びstd::exception_ptrに格納しなおすようなことをしなければならず、これはとても重い処理になります。

この提案はそのために、再スローをすることなくstd::exception_ptrの例外オブジェクトを取得するためのAPIを追加しようとするものです。

この提案では、try_cast<T>()という関数を提案しています。これは、std::any_caststd::get_ifなどとよく似たAPIで、std::exception_ptrを引数で渡してTに想定する型を指定し、std::exception_ptrの参照する例外オブジェクトの型がT(もしくはその曖昧でない基底クラスがT)である場合に例外オブジェクトへのポインタを返し、そうでない場合はnullptrを返します。

template <typename T>
const std::remove_cvref_t<T>* 
  try_cast(const std::exception_ptr& e) noexcept;

提案文書より、サンプル

struct Foo {
  virtual ~Foo() {}
  int i = 1;
};

struct Bar : Foo, std::logic_error {
  Bar() : std::logic_error("This is Bar exception") {}
  int j = 2;
};

struct Baz : Bar {};


int main() {
  const auto exp = std::make_exception_ptr(Baz());

  if (auto* x = try_cast<Baz>(exp)) {
    printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j); 
  }
  if (auto* x = try_cast<Bar>(exp)) {
    printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j);
  }
  if (auto* x = try_cast<Foo>(exp)) {
    printf("got '%s' i: %d\n", typeid(*x).name(), x->i);
  }
}

この出力は、例えば次のようになります

got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' i: 1

try_cast<T>(exptr)は、exptrの中身を再スローした時にcatch節に記述してマッチングする型がTに指定された場合に例外オブジェクトへのポインタをconst T*として返し、マッチングしない場合はnullptrを返します。

このような機能の実装のためには例外機構に手を加える必要がありそうですが、GCC/MSVC(libstdc++/MSVC STL)は非公開ながらそのような機能を持っており、それぞれの実装者も実装可能であると言っているようです。さらに、metaのfollyというライブラリではこれとよく似た機能がポータブルに実装されており、metaの様々なサービス内部で使用されているようです。

このような機能はまた、将来的にパターンマッチング機能においてstd::exception_ptrのパターンマッチングを可能にすることもできます。

P2932R1 A Principled Approach to Open Design Questions for Contracts

契約機能に関する未解決の問題についての設計原則に基づく解決策の提案。

以前の記事を参照

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

  • ラムダキャプチャの回避策を明確化
  • 信頼性(reliability)、直交性(orthogonality)、その他議論の原則を追加
  • コンパイル時評価を処理するアプローチの明確化
  • 提案1について代替案を追加

などです。

提案1はトリビアルメンバ関数に対する契約注釈に関するものでした。この提案ではR0での提案を提案1.Aとして、新しい解決案を提案1.Bとして追加しています

  1. トリビアルな特殊メンバ関数と契約
    • 提案A
      • 契約注釈は関数のトリビアル性に影響を与えない
      • そのような契約注釈は評価されない可能性がある
    • 提案B

2023年11月のKona会議において、このリビジョンで追加された提案1.BをC++26の契約プログラミング機能における制約として採用したようです。

P2935R3 An Attribute-Like Syntax for Contracts

C++契約プログラミングのための構文として属性構文を推奨する提案。

以前の記事を参照

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

  • 事前・事後条件指定位置をpure-specifier= 0)の前に移動
  • 契約注釈そのものに対する属性指定位置を明確化
  • [[assert : expr]]が式とする具体的な提案の追加

などです。

P2959R0 Container Relocation

ブロックベースコンテナ(特にstd::vector)の再配置時の動作を修正する提案。

標準ライブラリのコンテナは主にノードベースコンテナ(std::list, std::mapなど)とブロックベースコンテナ(std::vector, std::dequeなど)の2つに大別することができます。

コンテナが管理する個別のオブジェクトの事を要素(element)と呼び、その要素の状態の事を値(value)と呼ぶとするとき、要素と値の区別は理論的なもので、この違いが実際のコードで現れることはほとんどありません。

しかし、ノードベースコンテナとブロックベースコンテナの間でこの違いが顕在化する場合があります。

using element = std::tuple<int &>;

static_assert(std::is_move_assignable_v<int &>);
static_assert(std::is_move_assignable_v<element>);

template<typename C>
void test() {
  int a = 1;
  int b = 2;
  int c = 3;

  C x;
  x.emplace_back(a);
  x.emplace_back(b);
  x.emplace_back(c);

  std::cout << "a:\t" << a << "\n"                            // 1
            << "b:\t" << b << "\n"                            // 2
            << "c:\t" << c << "\n";                           // 3


  auto const mid = std::next(x.begin());

  std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"    // 1
            << "x[1]:\t" << std::get<0>(*mid)      << "\n"    // 2
            << "x[2]:\t" << std::get<0>(x.back())  << "\n";   // 3

  x.erase(mid);

  std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"    // 1
            << "x[1]:\t" << std::get<0>(x.back())  << "\n";   // 3

  b = 4;
  c = 5;

  std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"    // 1
            << "x[1]:\t" << std::get<0>(x.back())  << "\n";   // 5 or 4
}

int main() {
  test<std::vector<element>>();
  std::cout << "----------------\n";
  test<std::list<element>>();
}

この実行結果は次のようになります

a:   1
b:  2
c:  3
x[0]:   1
x[1]:   2
x[2]:   3
x[0]:   1
x[1]:   3
x[0]:   1
x[1]:   4
----------------
a:  1
b:  2
c:  3
x[0]:   1
x[1]:   2
x[2]:   3
x[0]:   1
x[1]:   3
x[0]:   1
x[1]:   5

一番最後のx[1]の出力結果だけが異なっています。

std::tuple<int&>の構築時は、メンバの参照がコンストラクタ引数のオブジェクトへ束縛されますが、代入時はメンバの参照の切り替えではなくメンバ参照の参照先のオブジェクトへの代入が行われます。

std::vectorの要素が削除される時、特に中間位置にある要素が削除されるとき、現在のブロック全体を確保しなおすのではなく、削除された位置よりも後ろの要素を前にずらすようにして再配置が行われます。この時、各要素が破棄されてから再構築されるのではなく、ムーブ代入によって要素の移動が起こります。そのため、x.erase(mid)ではbへの参照をメンバに持つtuple要素を削除しますが要素の破棄はせず、そこにはすぐ後ろのcへの参照をメンバに持つtupleが代入されます。それによって、b = cのような値の移動が起こり、vectorから削除されるのはbの参照ではなくcの参照です。そのため、その後b = 4をするとvectorの2番目の要素からもそれが観測できます。

std::list(他ノードベースコンテナ)の場合は単に1つのノードが削除されリンクが修正されるだけなので、このような要素の再利用は起きず、結果は意図通りになります。

このことはまた、std::vectorそのものの状態によっても挙動に差異が生まれる場合があります。

int main() {
  int a = 1;
  int b = 2;

  using element = std::tuple<int &>;
  std::vector<element> v;
  v.reserve(4);
  assert(4 == v.capacity());

  // aの参照で埋める
  auto fill = [&](int & i ) {
    v.clear();
    for (int j = 0; j != 4; ++j) {
      v.emplace_back(i);
    }
  };

  // 真ん中にbの参照を挿入
  auto inject = [&](int & i) { v.emplace(v.begin() + 2, i); };

  // 値と参照先変数名を出力
  auto report = [&] {
    for (auto& j : v) {
      std::cout << std::get<0>(j);
    }

    for (auto& j : v) {
      if (&a == &std::get<0>(j)) {
        std::cout << 'a';
      }
      else if (&b == &std::get<0>(j)) {
        std::cout << 'b';
      }
      else {
        std::cout << '?';
      }
    }

    std::cout << '\n';
  };

  for (int dummy : {1, 2}) {
    fill(a);
    inject(b);
    report();
  }
}
11211aabaa
22222aaaaa

std::vectorのキャパシティの状態によってこの挙動の差異が生じており、キャパシティが丁度4つ分の場合はinject()においてブロック再確保が発生し全ての要素は再構築されますが、キャパシティが充足している場合は再確保が発生せず、挿入は再代入によって行われます。

これらの振る舞いは現在の規格の規定に則ったもので、現時点でもこの仕様によって引き起こされる懸念がいくつかありますが、トリビアルリロケーションを考慮するとそれらの懸念はより大きいものになり得ます。そして、この懸念はコンテナの要素型とコンテナのアロケータ型の2つの異なる原因から生じています。

  1. ムーブ構築ではなくムーブ代入による要素の置換
    • 要素型のムーブ代入によって、破棄とムーブ構築とは異なる状態が生成される場合、ブロックベースコンテナはノードベースコンテナとは異なる振る舞いをする
  2. アロケータは要素の同一性を考慮する必要がある
    • ムーブ代入によって内部再配置が行われる場合に、構築された要素のID(アドレス)が重要である場合、アロケータの.destroy()の期待に反する可能性がある
  3. 強い例外安全保障が破られる可能性がある
    • アロケータのconstruct()がカスタマイズされている場合、構築時に要素型のコンストラクタ以外から例外が投げられないことを仮定できない
    • 現在のブロックベースコンテナに対する強い例外安全保障は要素型のムーブコンストラクタのみを考慮しており、アロケータを考慮していない
  4. トリビアルリロケーションのサポートにおいて問題が起こりうる
    • アロケータがconstruct()を提供する場合、(3と同様の理由により)要素型のトリビアルリロケーション可能性の情報を利用できない
  5. 再配置は標準ライブラリの未初期化アルゴリズムと同様にオブジェクトを作成する
    • <memory>に現在のuninitialized_*アルゴリズムに対応する、ムーブ構築を基本とし利用可能な場合はリロケーションを利用して最適化する汎用の関数を追加する必要がある

この提案はこれらの懸念に対処するために次のような変更を標準ライブラリに加えることを提案しています

  1. ムーブ構築/代入が一貫しない振る舞いをする型のために、新しい型特性を追加する
  2. アロケータが独自の実装を提供せず1の特性がfalseとなる場合に、現在の動作をデフォルトとする内部再配置(置換)をサポートするための非静的メンバ関数std::allocator_traitsに追加する

1つめの新しい型特性はstd::container_replace_with_assignment<T>というもので、これはTのムーブ代入と破棄+ムーブ構築が異なる振る舞いをする(上記のstd::tuple<int&>のように)ことを通知するものです。

namespace std {
  template <class T>
  struct container_replace_with_assignment : is_move_assignable<T>::type {};

  template <class T>
  constexpr bool container_replace_with_assignment_v = container_replace_with_assignment{};
}

下位互換性のために、ムーブ代入が可能な型ではtrueとなるのがデフォルトとされます。意図的にfalseとするには部分特殊化を定義します

namespace std {
  template <class ...TYPES>
  struct container_replace_with_assignment<tuple<TYPES...>>
    : conjunction<container_replace_with_assignment<tuple<TYPES>>...>::type
  {};
}

2つめの関数はstd::allocator_traits::relocate()という関数で、これは主にコンテナ内で再配置が起こる場合に再配置の方法をカスタマイズするものです。次のような動作をします

  1. アロケータ型がrelocate()メンバ関数を定義する場合、それを呼び出す
  2. アロケータ型が少なくとも1つのconstruct()/destroy()を定義している場合、再配置対象の右辺値を渡してそれらを利用する
  3. 要素型がトリビアルコピー可能ならば、memmoveを利用する
    • トリビアルなリロケーションが導入された場合、それで置き換えられる
  4. それ以外の場合、ムーブ代入/構築によって再配置
    • std::container_replace_with_assignment<T>trueならばムーブ代入によって再配置(現在の動作)
    • std::container_replace_with_assignment<T>falseならば破棄とムーブ構築によって再配置

コードで書くと、次のようになります

if constexpr (requires requires{ allocator::relocate(...); }) {
  // forward to allocator::relocate
} else if constexpr(requires requires{ allocator::construct(...); }) {
  // `allocator_traits::destroy(); allocator_traits::construct(rvalue)` each element
} else if constexpr (is_trivially_relocatable_v<T>) {
  // trivially relocate
} else if constexpr (container_replace_with_assignment_v<T>) {
  // move-assign elements
} else {
  // destroy-move-construct elements
}

現在動作しているコードは基本的に引き続いて4番目の動作を選択することで動作を維持します。3番目の動作はさらに、トリビアルコピー可能な型に対しての最適化を組み込んでいます。また、4番目の動作では同時に提案しているstd::container_replace_with_assignment<T>を使用してムーブ代入の使用が適切かどうかを判定し、不適切な場合は再配置先要素の破棄の後そこにムーブ構築することで再配置を行います。

これらの変更によって、後方互換性を維持しつつ現在の振る舞いを修正するとともに、将来的なトリビアルリロケーションを適切に有効化することができます。ただし、この提案は現在の実行時の動作を静かに変更する可能性があります。

P2961R1 A natural syntax for Contracts

契約プログラミング機能のための構文の提案。

以前の記事を参照

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

  • default/delete指定と一貫させるために、pre-or-postcondition(事前/事後条件指定)がpure-specifier= 0)の前に来るように調整
  • 事後条件における戻り値を指す名前の指定が必須ではないことを明確にした
  • assertキーワードの代替案に関する説明の追加
    • 代替案の候補としてcontract_assertassertexprを追加
  • Cにおける実現可能性についての議論を追加
  • C++26でこの構文によって契約機能を追加した後の、クラス不変条件の構文に関する議論を追加
  • P2885R3からの新しい3つの構文要件に関する議論の追加

などです。

この提案のR2が2023年11月のKona会議でレビューされ、アサートのキーワードとしてはcontract_assertを採用することに決定したようです。同時に、C++26の契約プログラミング機能のための構文としてこの提案のnatural syntaxが採用されることになったようです。

P2962R0 Communicating the Baseline Compile Command for C++ Modules support

Baseline Compile Commandの説明と、それを伝達する方法についての提案。

ある翻訳単位におけるヘッダユニットのインポートは、その翻訳単位のプリプロセッサ状態の影響を受けず、ヘッダユニットはマクロをエクスポートするためヘッダユニットがインポートされるとその翻訳単位のプリプロセッサ状態は更新されます。一方で、ヘッダユニットはその翻訳単位に指定されているコンパイラオプション(コンパイラコマンド)は適用されなければなりません。

ただし、ヘッダユニットのインポートにおいてはその翻訳単位のプリプロセッサ状態を含めたローカルプリプロセッサ引数を適用しないようにする必要があります。この制約は、同じヘッダユニットを異なる翻訳単位でインポートした時でも、双方の翻訳単位において同じようにインポート可能なヘッダをパースするためのものです。特に、推移的なインポートが起こる場合に同じヘッダユニットの内容が異ならないようにするために求められることです。そのため、ローカルプリプロセッサ引数とはある翻訳単位に固有な、コンパイル中のプリプロセッサ状態に影響を与えうる引数のことです。

また、SG15における合意ではビルドシステム以外のものがコンパイルコマンドの構成を行うべきではないというものがあるため、ローカルプリプロセッサ引数の区別を行うのはビルドシステムであるとして、依存関係スキャンプロセス(これを行うのはコンパイラや静的解析ツールなど)にはその翻訳単位自身のコマンドライン引数と、インポートされた全てのヘッダユニットをコンパイルするために使用されるBaseline Compile Commandの2つの入力(コマンドライン引数)が必要となります。

この提案におけるBaseline Compile Commandは、次のような情報のことです

  1. どのファイルがコンパイルされるか
  2. どのような出力が生成されるべきか
  3. ローカルプリプロセッサ引数を含まないコンパイルオプションの一部

ビルドシステムは、各翻訳単位をビルドするために必要なコンパイルコマンドを構成し、そこからBaseline Compile Commandを区別する役割を担います。したがって、ある翻訳単位におけるBaseline Compile Commandはそのコンパイルにおいて使用されるコンパイルオプションの一部分であり、それを区別する方法にはいくつか問題があります。

この提案では、コンパイルオプションからBaseline Compile Commandを独立させてファイルに保存し、依存関係スキャンプロセスへの入力にはコンパイルオプション及びBaseline Compile Commandを記録したファイルパスを渡すようにすることを提案しています。

提案より、LLVMJSON Compilation Database(いわゆるcompile_commands.json)をBaseline Compile Commandを含むように拡張する例

{
    "directory": "/path/to/build/dir",
    "file": "/path/to/source/main_translation_unit.cpp",
    "arguments": [ "g++", "-o" ,"main_translation_unit.o",
                   "-DFOO=1", "-DBAR=2", "-I/one/path",
                   "-I/other/path" ],
    "output": "main_translation_unit.o",
    "baseline-arguments": ["g++", "-DFOO=1", "-I/one/path" ]
}

一番最後のbaseline-argumentsフィールドがBaseline Compile Commandです。

P2967R0 Relocation Is A Library Interface

リロケーションサポートのためのライブラリ機能の提案。

この提案はP2786で提案されているトリビアルリロケーションをサポートするための、追加のライブラリ機能を提案するものです。P2786では主にコア言語にトリビアルなリロケーション可能性の概念を提案し、それを検出して活用するために必要な最小限のライブラリ機能のみが提案されています。この提案は、標準ライブラリ全体でトリビアルリロケーションを活用するための機能、特にユーザーが自身のコードでリロケーションを活用するときに必要となる機能についてを提案するものです。

ここで提案されているライブラリ機能は2つだけで、まず1つはstd::relocate()です

namespace std {
  template <class T>
    requires (is_trivially_relocatable_v<T> || is_nothrow_move_constructible_v) && !is_const_v<T>
  constexpr T* relocate(T* begin, T* end, T* new_location);
}

これは[begin, end)の領域にあるTのオブジェクトをnew_locationの領域へリロケーションするものです。その際、トリビアルではないリロケーションも含めてあらゆる手段でリロケーションを行おうとします。

この関数は次のようなコードと等価な振る舞いをします

if constexpr (is_trivially_relocatable_v<T>) {
  // Tがトリビアルリロケーション可能ならそうする
  return std::trivially_relocate(begin, end, new_location);
} else if (less{}(end, new_location) || less{}(new_location + begin - end, begin)) {
  // 未初期化アルゴリズムを使用して、ムーブ&破棄によってリロケーション(領域にオーバーラップがない場合
  std::uninitialized_move(begin, end, new_location);
  std::destroy(begin, end);

  return new_location;
} else if (std::less{}(begin, new_location)) {
  // ↓こんな感じの領域の重なり方をしている場合の処理
  // |begin ...       ... end|
  //                  |nee_location...        ...|

  // 要素ごとにムーブ&破棄によってリロケーション(後から
  while (T* dest = new_location + begin - end; dest != new_location) {
    ::new (--dest) T(std::move(*--end));
    std::destroy_at(end);
  }

  return dest;
} else {
  // ↓こんな感じの領域の重なり方をしている場合の処理
  //                |begin ...       ... end|
  // |nee_location...        ...|

  // 要素ごとにムーブ&破棄によってリロケーション(前から
  while (begin != end) {
    ::new (new_location++) T(std::move(*begin++));
    std::destroy_at(begin);
  }

  return new_location;
}

複雑な分岐はほとんど、リロケーション元と宛先の領域がオーバーラップしている場合にも正しく動作させるためのものです。

この関数は効率的なリロケーションのためにTのムーブコンストラクタが例外を投げないことを求めています。それを満たさない型での使用やイテレータ範囲によって同等のことを行うために、2つ目の機能であるstd::uninitialized_move_and_destroy()が用意されています。

namespace std::ranges {
  // イテレータ版
  template<forward_iterator I, sentinel_for<I> S1,
           nothrow-forward-iterator O, nothrow-sentinel-for <O> S2>
    requires constructible_from<iter_value_t<O>, iter_rvalue_reference_t<I>>
  uninitialized_move_and_destroy_result<I, O>
    uninitialized_move_and_destroy(I ifirst, S1 ilast, O ofirst, S2 olast);

  // range版
  template<forward_range IR, nothrow-forward-range OR>
    requires constructible_from<range_value_t<OR>, range_rvalue_reference_t<IR>>
  uninitialized_move_and_destroy_result<borrowed_iterator_t<IR>, borrowed_iterator_t<OR>>
    uninitialized_move_and_destroy(IR&& in_range, OR&& out_range); 
}

ここでは代表としてRangeアルゴリズムのものを抜粋しましたが、非Rangeのものや並行アルゴリズム_n付きのものも提案されています。

これらのアルゴリズムは未初期化領域[ofirst, olast)out_range)に対して[ifirst, ilast)in_range)の領域のオブジェクトを、ムーブ&破棄によってリロケーションするものです。

これも含めた未初期化メモリに対するアルゴリズム全体の指定として、例外がスローされた場合は出力領域は未初期化状態にリセットされます。また、既存の未初期化メモリに対するアルゴリズムと同様に、事前条件で入出力領域がオーバーラップしていないことを求めており、std::relocate()とは異なりオーバーラップ領域には使用できません。

P2971R1 Implication for C++

P2978R0 A New Approach For Compiling C++

ビルドシステムとコンパイラが相互にやり取りをするためのAPIの提案。

現在のC++コンパイルは、人間かビルドシステムがコンパイラの実行ファイルを呼び出すことで行われています。提案によれば、ビルドシステムがコンパイラの機能を実行ファイルではなく共有ライブラリ経由で使用するようにすることで25~40%のコンパイル速度向上が見込めるとのことです。

速度が向上する理由は次の2点です

  1. ビルドシステムはAPIを利用してコンパイラが読み込んだファイルを知ることができる。その情報を利用すれば、複数のファイルのコンパイルで同じキャッシュファイルを使用可能になる
  2. モジュールのビルドにおいては、本ビルドの前にモジュール間の依存関係を調べる必要がある。その役割は基本的にコンパイラが担っているが、APIを使用することでビルドシステムがその解決を行うことができ、依存関係スキャンを行う必要がなくなる

ビルドシステムがコンパイラの持つ情報を得ようとすると一々コンパイラを呼び出す必要があり、それはオーバーヘッドが大きいため現在は避けられているか時間がかかっています。API経由でコンパイラの個別機能を使用することでビルドシステムの任意のタイミングでその情報を得られるようになり、そのコストはおそらくコンパイラを呼び出すオーバーヘッドよりもかなり小さくなると思われます。

この提案ではWindows11上のMSVCを使用したSFLMおよびLLVMコンパイル時間の分析を行うことでその効果を見積もっており、25~40%のコンパイル速度向上が見込めるとしています。主に、翻訳単位それぞれでの依存関係スキャンをスキップできることと、コンパイル全体で読み取る必要のあるファイル数を削減すること、立ち上げるべきプロセス数の削減によって高速化されるようです。

提案しているAPIは次のようなものです。これはおそらくC++標準ではなくSG15(Tooling Study Group)で議論中のEcosystem International Standardに対して適用されるものだと思われます。

namespace buildsystem {

  struct string {
    const char *ptr;
    unsigned long size;
  };

  // Those char pointers that are pointing to the path are platform dependent i.e. whcar_t* in-case of Windows
  struct compile_output {
    // completed == false : コンパイラ状態へのポインタ
    // それ以外の場合 : nullptr
    void *compiler_state;

    // completed == true : コンパイラの出力メッセージ
    // それ以外の場合 : nullptr
    string stdout;
    string stderr;

    // completed == true : コンパイラが待機しているモジュール名orヘッダユニットのビルド済モジュールorヘッダユニット名(すなわち依存関係の情報)
    // それ以外の場合 : エクスポートされたモジュールがある場合その論理名
    string wait_string_or_logical_name;

    // インクルードされているヘッダの数と名前(パス)
    unsigned long header_includes_count;
    string *header_includes;

    // コンパイル成果物のファイル名、パス、数
    string *output_files;
    string *output_files_paths;
    unsigned short output_files_count;

    // コンパイラがモジュールの入力を待っている場合true
    bool waiting_on_module;

    // コンパイルが完了していれば(エラーでも)true
    bool completed;

    // completed == trueの場合、コンパイルエラーが起きていればtrue、成功していればfalse
    bool error_occurred;
  };

  compile_output new_compile(string compile_command, string (*get_file_contents)(string file_path));
  compile_output resume_compile(void *compiler_state, string bmi_file);
  string get_object_file(string bmi_file);

} // namespace buildsystem

このようなAPIコンパイラ共有ライブラリが提供し、ビルドシステムはこのAPIを介してコンパイラコンパイルを制御します。次のように利用することを意図しています。

最初に、new_compile()コンパイラオプションを引数として渡してコンパイルを開始します(このオプションには依存関係の情報は含まれていません)。その後、コンパイル実行中にモジュールのインポートに遭遇した場合、戻り値のwait_string_or_logical_nameにその依存関係の名前を指定しwaiting_on_moduletrueに設定してこの関数はリターンします。また、コンパイル実行中にヘッダユニットのインポートに遭遇した場合、wait_string_or_logical_nameにそのヘッダユニットのパスを指定しwaiting_on_modulefalseに設定してリターンします。

ビルドシステムは一連のビルドの間で得られたcompiler_stateを保存しておき、必要なファイルが既にビルドされているか、ビルドする必要があるか、ビルド中であるかを管理します。あるビルドが待機しているファイルが利用可能になると、そのcompiler_stateと新たに利用可能になったモジュール(ヘッダユニット)のBMIを渡してresume_compile()を呼び出すことでコンパイルを再開します。

ビルドが完了すると、コンパイラcompile_outputオブジェクトのcompletedtrueに設定し、output_filesoutput_files_pathsに成果物の名前とパスを設定してリターンします。

あるcompiler_stateに対して依存関係が解消されコンパイルが完了するまでresume_compile()は繰り返し呼び出され、それらのビルドは複数並行で行われます。

このAPIの実行モデルでは、コンパイラは依存関係スキャンを行わず依存関係(モジュール関連)にぶつかるとそこでコンパイルを一時停止しビルドシステムに制御を返します。ビルドシステムはプロジェクト内の全てのモジュール/ヘッダユニットについて並行的にnew_compile()を走らせ、その成果物をもってresume_compile()を呼び出すことで依存関係を自動的に解決しながらビルドを完了します。

この提案は、API定義を通してそのようなモジュールのビルドモデルを定義しようとしてもいます。

P2979R0 The Need for Design Policies in WG21

LEWG/EWGでの機能設計のための設計ポリシーを整備する提案。

WG21に参加する人々は、C++標準を可能な限り最高のものにする目標を共有していますが、誰もが同じ原則に基づいて設計を選択するわけではありません。そのため、関連するすべての原則が先に合意されない限り設計に関する議論を行き詰まってしまう可能性があります。一見の一致を見た場合でも、投票の後で1つ以上の重要な設計原則について十分な情報が共有されていなかったことが後で反目する場合があります。

この提案では、原則に基づいた設計を採用し、WG21の議論プロセスにおいて、現在のように初めに関連する問題について議論した後に解決策の一つに投票するのではなく、原則に基づいた設計のプロトコルに従ってまず関連する設計原則を明確にした上で優先順位を付けるようにすることを提案しています。

また、そのような設計原則に基づいた決定を文書化して共有することで、別の議論における同様の決定の際に再検討を避け将来の議論を合理化できます。

現在では、そのような設計原則やそれに基づく過去の決定などの情報は属人化しており、その人がたまたまその議論において欠けていることで以前の議論や決定が継承されず、設計に矛盾が生じることが少なからずあったようです。提案では、クラスのデフォルト特殊メンバ関数に対するnoexcept指定のバージョン間での振る舞いの不一致や、LEWGにおけるラコスルールの軽視の例をあげています。

この提案はそのようなポリシーの概要を説明するもので、具体的な提案は個別の2つの提案で行おうとしています

  1. 議論のある設計の決定を仲裁するための原則に基づいたアプローチ (P3004)
  2. 確立された設計ポリシーを文書化してアクセスするための体系的なメカニズム(P3005)

なお、これらの提案はまだ公開されていません。

P2980R0 A motivation, scope, and plan for a physical quantities and units library

物理量と単位を扱うライブラリ機能の導入について説明する提案。

この提案では、物理量と単位を扱うライブラリを導入する理由やモチベーションを説明し、設計目標や現時点での例を示すものです。

モチベーションとしては次のようなものが挙げられています

  • 安全性
    • 単位を間違えた計算がコンパイルエラーとなる
    • 物理量とその単位を扱うライブラリは多くの機械の制御コードの安全性を向上させる
  • 語彙型となること
    • ベンダーがそれぞれ独自実装をしなくてよくなる
  • 認証されたライブラリ
    • MISRA等の安全性基準に従ったソフトウェア開発においてはOSSを使用できない場合が多い
    • 標準ライブラリとして提供されていることでより安全なコードを記述できるようになる
  • 独自実装は複雑で難しい
  • 拡張性
    • 非SI単位などに対応するために単位の追加を容易にする
  • 幅広いドメインで使用可能(されている)

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

// mp-unitsというライブラリで試験実装が行われている
#include <mp-units/systems/si/si.h>

using namespace mp_units;
using namespace mp_units::si::unit_symbols;

// 単純な数値計算
static_assert(10 * km / 2 == 5 * km);

// 単位変換
static_assert(1 * h == 3600 * s);
static_assert(1 * km + 1 * m == 1001 * m);

// 組立量
static_assert(1 * km / (1 * s) == 1000 * m / s);
static_assert(2 * km / h * (2 * h) == 4 * km);
static_assert(2 * km / (2 * km / h) == 1 * h);

static_assert(2 * m * (3 * m) == 6 * m2);

static_assert(10 * km / (5 * km) == 2 * one);

static_assert(1000 / (1 * s) == 1 * kHz);
#include <mp-units/format.h>
#include <mp-units/ostream.h>
#include <mp-units/systems/international/international.h>
#include <mp-units/systems/isq/isq.h>
#include <mp-units/systems/si/si.h>
#include <iostream>

using namespace mp_units;

constexpr QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto d,
                                                QuantityOf<isq::time> auto t)
{
  return d / t;
}

int main()
{
  using namespace mp_units::si::unit_symbols;
  using namespace mp_units::international::unit_symbols;

  constexpr quantity v1 = 110 * km / h;
  constexpr quantity v2 = 70 * mph;
  constexpr quantity v3 = avg_speed(220. * km, 2 * h);
  constexpr quantity v4 = avg_speed(isq::distance(140. * mi), 2 * isq::duration[h]);
  constexpr quantity v5 = v3.in(m / s);
  constexpr quantity v6 = value_cast<m / s>(v4);
  constexpr quantity v7 = value_cast<int>(v6);

  std::cout << v1 << '\n';                // 110 km/h
  std::cout << v2 << '\n';                // 70 mi/h
  std::println("{}", v3);                 // 110 km/h
  std::println("{:*^14}", v4);            // ***70 mi/h****
  std::println("{:%Q in %q}", v5);        // 30.5556 in m/s
  std::println("{0:%Q} in {0:%q}", v6);   // 31.2928 in m/s
  std::println("{:%Q}", v7);              // 31
}

この提案及び将来的に標準に導入しようとしているライブラリ機能は、mp-unitsというライブラリで試験実装が進められています。

提案ではこの機能をC++29に導入することを目指しており、そのための機能ごとのカテゴライズやその中での優先順位付けを行っています。掲載されている予定表によれば、C++26サイクル中にLEWGのレビューを完了し、C++29サイクル中でLWGの承認を取り付け、標準に導入するような予定を組んでいるようです(詳細な予定表が提案にはあります)。ただし、まだ確度の高いものではなく、計画をLEWGに承認してもらってスムーズに進めようとしています。

P2981R0 Improving our safety with a physical quantities and units library

↑の物理量と単位を扱うライブラリ機能について、コンパイル時の安全性に関する側面を解説する文書。

この文書では主に、P2980のライブラリ(以下単位ライブラリと呼びます)がC++コードの安全性向上に役立つことを解説しており、単位ライブラリの必要性や使用可能な産業領域等を説き、単位付きの量の取り扱いを間違ったことで起きた事故を紹介し、現在よく見られる単位付きの量を使用している危ういコード例について紹介したうえで、単位ライブラリがそれらの問題をどう解決できるかを示しています。

単位ライブラリによる安全性は主に、算術演算コードにおいてその値の単位をコンパイル時にチェックすることで間違った計算を防止するものです。それをベースに、ダングリング参照に対する配慮や、同じ単位を持つ異なる種類の量のサポートなどの安全性への配慮が行われています。

P2982R0 std::quantity as a numeric type

↑の物理量と単位を扱うライブラリ機能について、数値と計算の側面に関する設計の説明をする文書。

あまりに長いので詳しくは見ませんが

  • 量体系
    • 同じ次元を持つ量でもその意味が異なる場合があり、それを考慮しなければならない
    • 単位系だけではなく量体系もモデリングする必要がある
    • 国際量体系(International System of Quantities)に基づいてそれを行う
  • 単位系
    • 単位の種類・組み合わせ・スケール・単位を指す記号などについて
    • 単位は量の種類に関連づけられ、特定の量に対して制約される
  • 単位・次元・量を表す型の操作
    • 単位・次元・量を表す型に対して定義された演算や変換などについて
    • 全ての単位・次元・量を表す型はそれぞれ一意な型を持つ
    • 演算の結果は、量と単位の型の階層や関係性に基づいて決定される
  • 量(std::quantity
    • quantityの概念や表現、構築や演算などについて
    • quantityは数値と単位のペアであり、単位の変換や算術演算が可能
    • quantityはこのライブラリの中心クラス型であり、std::chrono::durationの一般化ではあるが直接の互換性はない
  • ジェネリックインターフェース
    • quantityをテンプレートで受け取る関数や変数を定義する方法について
    • 関数のインターフェースや戻り値の受け取り、クラスメンバへの保存時などには単位の選択が必要となる
    • quantityを制約するためにコンセプトを使用できる
  • 数値を表現する型のカスタマイズ
    • quantityの数値表現型として任意の型を使用する方法について
    • quantityの数値表現型はその値の範囲や精度に影響する
  • アフィン空間
    • アフィン空間の概念とこのライブラリによってそれを表現する方法について
    • アフィン空間の中心の2つの概念(点とベクトル)はそれぞれ、PointOriginコンセプト+quantity_point型、quantity型によって表現される

のようなことが書かれています。

P2984R0 Reconsider Redeclaring static constexpr Data Members

static constexprメンバ変数のクラス外定義の非推奨の扱いを検討する提案。

C++11でstatic constexprメンバ変数が宣言できるようになりましたが、その定義が必要となる場合は通常の静的メンバ変数と同様にクラス外に定義を置く必要がありました。これはC++17のインライン変数の導入時にstatic constexprメンバ変数は暗黙inlineとされるようになったことでクラス外での定義が不要になりました。それとともに、クラス外の定義は不要な再宣言となり非推奨とされました。

struct A {
  static constexpr int n = 5;  // 定義(C++14までは宣言)
};

constexpr int A::n;            // 冗長な再宣言(C++14までは定義)

__cpp_inline_variables機能テストマクロを用いてクラス外定義の存在を切り替えることでこのようなコードはバージョン間でポータブルにすることができます。とはいえ、C++14以前にこの書き方をされている既存のコードはおそらくたくさんあり、必ずしも全てがそのような対応を行えるわけではないかもしれません。

現在の主要なコンパイラ(フロントエンド)は、C++17以降のモードにおいてもこれらの冗長な再宣言(以前の定義)に対して非推奨である旨の警告を発しません。

C++26に向けて現在非推奨とされているものを整理し可能なら削除しようとする取り組み(P2863)のEWGにおけるレビューにおいてこの問題も議論され、そこでは現状維持(非推奨のまま)とする方向性のようですが、さらにこれの非推奨化を解除する方向性について提案が望まれたようで、この提案はそれを受けてのものです。

この提案では、その歴史的経緯や現状を説明するとともに、現状維持・非推奨解除・削除のいずれかを選択することを促しています。

その後のEWGのレビューでは、C++26サイクルでは現状維持とすることになったようです。

P2985R0 A type trait for detecting virtual base classes

あるクラスが他のクラスの仮想基底になっているかを判定する型特性の提案。

このような判定はクラス型のポインタの変換を行う場所において必要となり、特にスマートポインタの変換コンストラクタにおいて必要となります。

ポインタderived* dbase*に変換するには、basederivedの仮想基底クラスであるかどうかによって実装が分岐します

  1. basederivedの仮想基底クラスではない場合
    • dnullptrかをチェックする
      • nullptrではない場合、dコンパイル時にわかっている定数オフセットを加算する
      • nullptrの場合、nullptrを返す
  2. basederivedの仮想基底クラスである場合
    • dnullptrかをチェックする
      • nullptrではない場合、dの参照する領域の仮想テーブルを検査して適切なポインタ値を得る
        • 実際の仕組みはABIに依存する

この処理はユーザーが実装するものではなく、コンパイラが挿入する変換処理です。

2でdnullptrではない場合に問題なのは、仮想テーブルにアクセスしてポインタ値を得るのが1の場合に比べてコストがかかることと、dの参照先が既に破棄されている場合に未定義動作となることです。

weak_ptrの変換コンストラクタを実装することを例として考えてみます。

weak_ptrのクラス構造は簡単には次のようになっています

template <typename T>
class weak_ptr {
  control_block *m_cb;
  T *m_data;
};

この場合にweak_ptr<Y*>からweak_ptr<T*>へムーブしつつ変換するコンストラクタを考えると、単純には次のようになります

template <typename Y>
  requires std::is_convertible_v<Y*, T*>
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(std::exchange(other.m_cb, nullptr)),
    m_data(std::exchange(other.m_data, nullptr)) // <-- danger
  {}

この実装には

  • TYの仮想基底であり
  • other.m_dataの参照するオブジェクトが既に破棄されている場合

に前述の理由により、dの領域の仮想テーブルにアクセスしようとして未定義動作となります。

正しい実装は、きちんとotherの領域が有効であるかを調べる必要があります

template <typename Y> 
  requires std::is_convertible_v<Y*, T*>
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(other.m_cb),
    m_data(other.lock().get())
  {
     other.m_cb = nullptr; other.m_data = nullptr;
  }

ただ、これは今度はTYの仮想基底ではないほとんどのケースで非効率となります。

この実装を正しくかつ効率的に行うにはTYの仮想基底であるかどうかによって実装を分岐する必要があります。

// 非仮想基底の場合の単純な実装
template <typename Y> 
  requires (std::is_convertible_v<Y*, T*> && !std::is_virtual_base_of_v<T, Y>)
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(std::exchange(other.m_cb, nullptr)),
    m_data(std::exchange(other.m_data, nullptr)) 
    {}
    
// 仮想基底の場合の安全だが重い実装
template <typename Y> 
  requires (std::is_convertible_v<Y*, T*> && std::is_virtual_base_of_v<T, Y>)
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(other.m_cb),
    m_data(other.lock().get())
  {
     other.m_cb = nullptr; other.m_data = nullptr;
  }

同様の問題はobserver_ptrというスマートポインタ(標準にはない)の変換コンストラクタにおいても発生し得ます。

この提案は、主にスマートポインタの安全な実装のために、この例のis_virtual_base_of型特性を標準ライブラリに導入しようとするものです。

namespace std {

  // 追加するis_virtual_base_ofの宣言例
  template<class Base, class Derived>
  struct is_virtual_base_of;

  template<class Base, class Derived>
  constexpr bool is_virtual_base_of_v = is_virtual_base_of<Base, Derived>::value;
}

P2986R0 Generic Function Pointer

定数式でも使用可能な関数ポインタ専用の型消去型を追加する提案。

現在利用可能な関数ポインタの型消去機能(std::function等)は全て、定数式で使用可能ではありません。これはstd::function等の実装の都合上、構築も呼び出しも定数式で行えないためです。

例えば、C++26で導入される予定のstd::function_refのCallableを保存するストレージの実装を見てみると

struct _function_ref_base {
  union storage {
    void *p_ = nullptr;
    void const *cp_;
    void (*fp_)();

    constexpr storage() noexcept = default;

    // 関数オブジェクトを受け取るコンストラクタ
    template<class T>
      requires std::is_object_v<T>
    constexpr explicit storage(T *p) noexcept
      : p_(p)
    {}

    // 関数オブジェクトをconstで受け取るコンストラクタ
    template<class T>
      requires std::is_object_v<T>
    constexpr explicit storage(T const *p) noexcept
      : cp_(p)
    {}

    // 関数ポインタを受け取るコンストラクタ
    template<class T>
      requires std::is_function_v<T>
    constexpr explicit storage(T *p) noexcept
      : fp_(reinterpret_cast<decltype(fp_)>(p)) // 👈 reinterpret_castは定数式で実行不可
    {}
  };

  template<class T>
  constexpr static auto get(storage obj) {
    if constexpr (std::is_const_v<T>) {
      // C++26ではvoid*からのキャストは定数式で可能(本来の型が合っていれば)
      return static_cast<T*>(obj.cp_);
    } else if constexpr (std::is_object_v<T>) {
      // C++26ではvoid*からのキャストは定数式で可能(本来の型が合っていれば)
      return static_cast<T*>(obj.p_);
    } else {
      return reinterpret_cast<T*>(obj.fp_); // 👈 reinterpret_castは定数式で実行不可
    }
  }
};

std::function_refは構築後に保持するCallableを切り替える必要がないためそのストレージの実装はかなり単純になります。そのため、constexpr対応も可能なように思えます。

しかし実際には、関数ポインタがvoid*に変換できないため関数ポインタの保存においては特別扱いが必要となります。すると、void(*)()というポインタ型で型消去することになりますが、関数ポインタのこのキャストにはreinterpret_castが必要となり、それは定数式で実行できません。これに引っ張られる形で、std::function_refは構築も呼び出しも定数式では行えません。

C++26時点でも、これを解決するソリューションは存在していません。この提案はこの解決のために、定数式で使用可能な関数ポインタ型専用の型消去ポインタ型を用意しようとするものです。

提案されているのはstd::function_ptr_tという名前のものです。これは言語組み込みの型で、この型の値は任意の関数ポインタ型を代入することができます。そして、この型の操作は全て定数式で行うことができます。

constexpr int f() {
  return 42;
}

int main() {
  // nullptrを代入可能
  constexpr std::function_ptr_t fp1 = nullptr;

  // 任意の関数ポインタをそのまま受け入れられる
  constexpr std::function_ptr_t fp2 = f;

  // static_castを用いて関数ポインタを復帰できる
  // これは型が合っていれば定数式で行える
  constexpr auto p_f = static_cast<int(*)()>(fp2);
  static_assert( p_f == f );
  static_assert( p_f() == 42 );

  // 直接の呼び出しやデリファレンスはできない
  fp_f(); // ng
  *fp_f;  // ng
}

例えばstd::function_refにおいては、このstd::function_ptr_tを使用して関数ポインタを保存する部分を書き換えることでreinterpret_castを使用する必要がなくなり、構築も呼び出しも定数式で行えます。

EWGIによるレビューでは、この問題はキャストの仕様調整によって解決することが望ましいという方向性のようで、この提案の方向性は支持を得られていないようです。

P2988R0 std::optional<T&>

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

現在、std::optional<T>Tには参照型を指定することができません。当初の提案では検討されていましたが、ポインタとの意味的な差異が不明瞭であることや非参照のTに対するメインのstd::optional<T>の導入を優先するために途中で提案から削除されました。

std::optional<T&>は単純には無効値を取れる参照です。それは一見すると確かにポインタと意味的な違いが無いようにも思えます。

std::optionalC++17に導入されてからおよそ7年の現在において、コンテナ等から特定の要素を見つけたいがその所有権を引き取りたくはないような場合など、std::optionalに参照を保持させたい場合は稀によくあります。

要素を検索する関数で戻り値型として生ポインタを使用する場合との比較

ポインタ この提案
Cat* cat = find_cat("Fido");
if (cat!=nullptr) { return doit(*cat); }
std::optional<Cat&> cat = find_cat("Fido");
return cat.and_then(doit);

要素を検索する関数で戻り値型としてスマートポインタを使用する場合との比較

ポインタ この提案
// use a smart pointer instead of optional<T&>
std::shared_ptr<Cat> cat = find_cat("Fido");
if (cat != nullptr) {...
std::optional<Cat&> cat = find_cat("Fido");
cat.and_then([](Cat& thecat){...

このような用途にスマートポインタを使用する場合はobserver_ptr<T>のようなものを使用するべきですが、それはまだ標準に存在していません。また、スマートポインタを使用してもnullptrチェックの必要性からは逃れられません。

要素を検索する関数で戻り値型としてイテレータを使用する場合との比較

ポインタ この提案
std::map<std::string, Cat>::iterator cat
    = find_cat("Fido");
if (cat != theunderlyingmap.end()){ ...
std::optional<Cat&> cat = find_cat("Fido");
cat.and_then([](Cat& thecat){...

このような用途にイテレータを使用するのは標準アルゴリズムでも見られるパターンですが、イテレータ型をとおして元のコンテナ型が漏洩し、さらには見つかったかどうかをチェックするのに元のコンテナ(の終端イテレータ)が必要になります。

生ポインタに比べたstd::optional<T&>には次のような利点があります

  • 所有権を引き取っていないことが明確
  • アドレス演算ができない
  • 変なポインタキャストできない
  • それを引数に取る関数へ渡す際に素直に渡せる(&等が必要ない)
  • optionalの便利なメンバ関数群を使用できる(nullチェックを明示的に行う必要が無い)

特に最後の利点はC++23で追加されたモナディックインターフェースによるところが大きく、それがこの提案を大きく後押ししてもいます。

この提案はこれらの利点や利便性から、std::optional<T&>を可能にすることを提案するものです。

std::optional<T&>の実装としてはポインタの薄いラッパとして実装することで無効値のための領域を節約するとともに、通常のstd::optionalのような複雑な実装を回避できることが知られており、この提案ではstd::optional<T>の部分特殊化としてstd::optional<T&>を追加することでそのような実装を取るようにすることを提案しています。

また、std::optional<T&>に対する代入のセマンティクスは参照の再バインドであり、参照先に対する代入ではありません。そのため、代入時には変換不可能な代入が拒否されます。

import std;

int main() {
  int n = 10;
  
  std::optional<int&> ro = n; // nの参照を保持
  ro = 15;  // ng、int&&はint&で束縛できない

  int m = 20;

  ro = m;   // ok、roはmの参照を保持する
}

これはstd::optional<T&>の再代入を内部参照に対してスルーするようにすると利点よりもむしろバグの発生源となるためです(詳しくはP1683R0を参照)。

さらに、この提案ではstd::optional<T&>に対するconstは浅いconstとなるようにすることを提案しています。これはポインタやstd::spanと同じであり、参照先に対するconstが必要な場合はstd::optional<const T&>のようにする必要があります。

import std;

int main() {
  int n = 10;
  int m = 20;
  
  const std::optional<int&> ro = n; // nの参照を保持

  ro = m;   // ng、roはconst
  *ro = 15; // ok、nの値が変更される

  std::optional<const int&> cro = n; // nの参照を保持

  *ro = 25; // ng、const int&に代入できない
  ro = m;   // ok、roはmの参照を保持する
}

P2989R0 A Simple Approach to Universal Template Parameters

より限定されたユニバーサルテンプレートパラメータの提案。

ユニバーサルテンプレートパラメータは型・非型・テンプレートを統一的に受けることのできるテンプレートパラメータのことで、P1985で提案されました。P1985の提案は言語のあらゆるコンテキストでユニバーサルテンプレートパラメータを使用可能にしようとするもので、仕様と実装に追加する複雑さが大きくなりメリットが相対的に小さくなっていました。

この提案はそれを改善しつつほぼ同じ機能を追加しようとするもので、のP1985との違いは、ユニバーサルテンプレートパラメータの構文としてuniversal templateを選択し、その導入を純粋にテンプレートパラメータとしての使用のみに限定したことです。

  • ユニバーサルテンプレートパラメータは関数・クラス・変数テンプレートのテンプレートヘッド(template<...>の中)でのみ宣言できる
  • ユニバサールテンプレートパラメータ名はテンプレート引数としてのみ使用できる
  • ユニバーサルテンプレートパラメータはパックを取れる
  • ユニバーサルテンプレートパラメータのデフォルト引数は設定できない
  • ユニバーサルテンプレートパラメータを処理するには、別のテンプレートに転送するか、部分特殊化によって行う

提案しているユニバーサルテンプレートパラメータ(UTP)によるライブラリ機能とその実装例

// UTPが型名かを判定
template <universal template T>
inline constexpr bool is_typename_v = false;

template <typename U>
inline constexpr bool is_typename_v<U> = true;

// UTPがNTTPかを判定
template <universal template U>
inline constexpr bool is_nttp_v = false;

template <auto U>
inline constexpr bool is_nttp_v<U> = true;

// UTPがテンプレートかを判定
template <universal template U>
inline constexpr bool is_template_v = false;

template <template<universal template....> universal template U>
constexpr bool is_template_v<U> = true;

// UTPが型テンプレートかを判定
template <universal template U>
inline constexpr bool is_type_template_v = false;

template <template<universal template....> typename U>
inline constexpr bool is_type_template_v<U> = true;

// UTPが変数テンプレートかを判定
template <universal template U>
inline constexpr bool is_var_template_v = false;

template <template<universal template....> auto U>
inline constexpr bool is_var_template_v<U> = true;

// UTPがコンセプトかを判定
template <universal template U>
inline constexpr bool is_concept_v = false;

template <template<universal template....> concept U>
inline constexpr bool is_concept_v<U> = true;

これを利用したis_specialization_ofの実装例

template<universal template T, universal template Primary>
  requires is_var_template_v<Primary> || requires is_type_template_v<Primary>
inline constexpr bool is_specialization_of_v = false;

// 型についてのテンプレート特殊化を検出
template<
  template<universal template...> typename Primary,
  universal template... Args
>
inline constexpr bool is_specialization_of_v<Primary<Args...>, Primary> = true;

// NTTPについてのテンプレート特殊化を検出
template<
  template<universal template...> auto Primary,
  universal template... Args
>
inline constexpr bool is_specialization_of_v<Primary<Args...>, Primary> = true;

P2098R1で提案されていたもの(汎用性が低いとしてリジェクト)とは異なり、この実装の場合はクラステンプレートと変数テンプレートの特殊化をチェックすることができ、さらに特殊化しているものが型でない場合についてもチェックすることができます。

この提案のユニバーサルテンプレートパラメータは他のテンプレートに渡す以外は何もできないため、このように最終的には部分特殊化によって処理することになるでしょう。そのため、P1985R3の例のいくつかは実装に工夫が必要となります。

// メタ関数FにArgsを適用したい、P1985の実装
template<universal template F, universal template... Args>
using apply = F<Args...>; // これはできない、エイリアスでUTPを使用できない

// メタ関数FにArgsを適用したい、この提案における実装
template<template <universal template...> typename F, universal template... Args>
struct apply {
  using type = F<Args...>;  // ここではFは型テンプレートパラメータなので普通に使える
};

ただし、この提案ではコンセプトのテンプレートパラメータでUTPを使用可能にすることは提案していないため(コンセプトは部分特殊化できないため)、コンセプトでは使用できません。

// これはできない・・・
template <typename R, template<universal template....> concept C>
concept range_of =
  ranges::input_range<R> &&
  C<remove_cvref_t<ranges::range_reference_t<R>>>;

P2990R0 C++ Modules Roadmap

モジュールのエコシステムのために必要な作業についての提案。

この提案は、モジュールを実際に利用可能にするために必要なツールのサポートのために現在欠けているものを特定し、SG15(tooling study group)においてそのために必要な作業とその優先度を提案するものです。

提案では、現状のエコシステム(コンパイラやビルドシステム、静的解析ツールなど)におけるモジュールのサポート状況を紹介し、それらツールがモジュールを相互に運用するために欠けているものについて説明したうえで、それを解消するために必要な作業について優先度を付けて提示しています。

提案されているのは次のようなロードマップです

  1. 単一のプロジェクトにおいてモジュールを利用可能にする
    1. モジュールインターフェースの複数回のコンパイル
      • 1つのモジュールインターフェースが異なる翻訳単位において異なるビルドオプションを用いてコンパイルされることがよくある
      • あるモジュールインターフェースが何回コンパイルされるのかを特定することに関して、ビルドシステムとコンパイラ間の相互運用性の疑問に答える必要がある
    2. 静的解析ツールのビルドシステム外部におけるサポート
      • IDEにおけるコード補完など、静的解析ツールはビルドシステムと深く統合することなく動作することが求められる
      • ビルドシステムはそれらツールが動作するのに十分な情報を提供する必要がある
  2. 事前ビルドライブラリにおけるモジュールの利用
    1. 事前ビルドライブラリでのモジュールのメタデータ
    2. 標準ライブラリモジュールのメタデータ
  3. インポート可能なヘッダ
    1. インポート可能ヘッダの識別
      • ビルドシステムやパッケージマネージャーがプロジェクト内でインポート可能なヘッダを見つける時の問題について、ツールの実装者との協力が必要
    2. 依存関係スキャンとプリプロセッサ状態
      • 依存関係スキャン実行時のインポートメカニズムのエミュレーションの要件について未解決の問題が残っている

この提案は、SG15における行動喚起を促す事と同時に、ツール開発者がモジュールサポートのための投資を行っても安全であることを示す目的があります。

SG15ではこのロードマップに沿った作業を行っていくことに合意が取れており、Githubのcplusplus/modules-ecosystem-trで作業を行っていくことにしたようです。

P2991R0 Stop Forcing std::move to Pessimize

std::move()がNRVOを阻害しないようにする提案。

C++の学習においては、新しいオブジェクトを構築するときにそのオブジェクトが別の左辺値オブジェクトから構築され、構築元のオブジェクトが以降使用されないような場合にstd::move()を使用して新しいオブジェクトを構築するように教えられます。しかし、return文においてローカル変数を返そうとするときには逆にstd::move()を使用すべきではないとも教わります。その理由はreturn文におけるstd::move()がNRVOを妨げるためですが、このことは一貫しておらず、return文における例外の理由についても非常に複雑なものがあります。

std::vector<std::string> readStrings(int numStrings) {
  std::vector<std::string> result;
  std::string string;

  while (numStrings--) {
    std::cin >> string;
    // ここではmove()がいる
    result.push_back(std::move(string));
  }

  // ここではmove()がいらない
  return result;
}

例えばこのコードにおいては、ローカルのstring及びresultは他のオブジェクトの初期化に使われた後で再び参照されることがなくコピーが重いクラスであるため、他のオブジェクトの初期化時にはムーブすることが適切です。しかし、プログラマが明示的にstd::move()によってムーブする必要があるのはループ中のstringに対してのみです。

全てのC++バージョンにおいてNRVOは許可されており(必須ではない)、NRVOが行われる場合ムーブすらも省略されresultは最初から呼び出し側の変数であったかのように使用されます。NRVOが行われない場合でも、暗黙ムーブによってreturn文における変数名を指定する式は値カテゴリがxvalueとなるため、resultは自動でムーブされます。

return std::move(result)と書くことはむしろ有害であり、NRVOを確実に行われなくします。これはNRVOの対象となるものがローカル変数の変数名を指定する式のみであるためであり、std::move()を追加すると変数名を指定する式ではなくなるためNRVOの対象でもなくなるためです。

このことによって、C++初学者にはstd::move()を使用する時のルールと、std::move()を使用してはならない時のルールの2つを教えなければならなくなります。しかも、後者のルールに違反するとパフォーマンス上のペナルティとして帰ってきます。

この提案は、return文におけるstd::move()を特別扱いすることでstd::move()がNRVOを妨げることがないようにして、std::move()に関してはstd::move()を使用する時のルールのみを教えれば済むようにしようとするものです。

そのために現在の規定で、return文のオペランドとしてNRVO eligibleとされている式Eについて、次の形式に当てはまる場合の式もNRVO eligibleであるというように判定を行うことを提案しています

  • Fが名前解決の後でstd::move()になる場合の式F(E)
  • Tが戻り値型への右辺値参照となる場合の式static_cast<T>(E)
  • Tが戻り値型への右辺値参照となる場合の式(T)E
  • Tが戻り値型への右辺値参照となる場合の式T(E)

さらに、コンパイラが以前の言語バージョンでもこの動作を実行できるように、このことを以前の言語バージョンに対するDRとすることも提案しています。

P2992R0 Attribute [[discard]] and attributes on expressions

式の結果を破棄することを明示する[[discard]]属性の提案。

[[nodiscard]]属性を付加された関数の戻り値を消費しない場合、警告が発せられます。これは基本的にはとても便利なのですが、場合によっては[[nodiscard]]な関数の戻り値を使用せずに破棄したい場合もあります。例えば

  • テスト
    • スモークテストにおいて広い契約を持つ関数がクラッシュしないことを確かめたい場合など
    • この場合に戻り値に興味はなく、警告は必要ない
  • 部分的なドメイン
    • 例えば、エラーコードを返す[[nodiscard]]な関数がある特定の引数を渡された場合に決して失敗しないことがわかっている場合にユーザーがそれを確認して呼び出してる時
    • この場合、戻り値を消費する必要はなく、安全に破棄できる
  • 古い関数
    • 当初は成否を戻り値で返していた関数が後のバージョンで決して失敗しないようになったものの、API/ABI保護のために戻り値型を維持し続けている場合
    • この場合、戻り値は意味がないため安全に破棄できる

などの場合があります。

そのような場合に警告を抑制しつつ戻り値を破棄するのに使用可能な方法は主に次の2つがあります

  1. voidキャスト
  2. std::ignore
[[nodiscard]]
int f();

int main() {
  f();  // 警告される

  // 1. voidキャスト
  (void)f();  // 警告されない
  void(f());  // 警告されない

  // 2. ignore
  std::ignore = f();  // 警告されない
}

しかしこれらの方法には欠点があります

  1. voidキャスト
    • キャスト式の濫用であり、警告を抑制するために言語の難解なルールを使用しているだけ
    • 初心者への教育が困難
    • コードベースで検索(grepなど)できない
    • voidキャストを[[maybe_unused]]として使用している場合がある
    • 破棄の根拠はコメントとしてのみ提供できるため、コードベースにそのような根拠を義務付けるルールを強制するのが困難
  2. std::ignore
    • 右辺がvoid式の場合にコンパイルエラーになるため、ジェネリックコードで使用しづらい
    • C互換性がない
    • std::ignoreはライブラリソリューションであり、言語の問題の解決には言語によるソリューションの方が適している
    • voidキャストと比べて冗長
    • 破棄の根拠はコメントとしてのみ提供できる

この提案は、これらの欠点を解決する言語機能による戻り値の明示的破棄のソリューションとして、[[discard]]属性を提案するものです。

[[nodiscard]]
int f();

int main() {
  [[discard]] f();  // 警告されない
  [[discard("just testing")]] f();  // 警告されない
}

これには次のような利点があります

  • [[discard]]は使用法と構文において[[nodiscard]]と対称になっている
    • [[nodiscard]]は呼び出し元において無視して欲しくない関数/型宣言に置かれ、[[discard]]は呼び出し側において結果を明示的に破棄したい式に置かれる
  • 理由を書いておくことができる
    • これはコンパイラからは使用されないが、ユーザーや周辺ツールにとって有用となる
  • void式でも使用可能
  • 警告抑制のために標準ライブラリのものを持って来なくてもいい
  • Cとの互換性を図ることができる
  • voidキャストと比べて適度に冗長

この提案ではこの属性を式に対する属性指定として提案しています。現在のC++では式に対する属性指定を行うことができず、現在の文法も文に対する属性指定は可能でも式に対する属性指定には問題があります。

現在の式(expression)の文法定義は次のようになっています

expression:
    assignment-expression
    expression , assignment-expression

ここに属性指定を単純に追加すると次のようになるでしょう

expression:
    attribute-specifier-seq(opt) assignment-expression
    expression , assignment-expression

しかし、これは既存の文(statement)の文法と衝突します

statement:
    attribute-specifier-seq(opt) expression-statement

expression-statement:
    expression(opt) ;

従って、現在の文法のもとでは提案している式に対する属性は実際には文に対するものになっています。

[[discard]] f();  // 文に対する属性指定

ただし、このような単純な関数呼び出し式のみを含む文に対する[[discard]]属性の適用はこの属性の最も一般的な使い方であり、式文(expression-statement)に対して属性適用が行われていればこの提案の目的には十分です。

その場合に問題となるのは、組み込みカンマ演算子を使用した場合です。

[[discard]] a(), b(), c();  // 全ての関数の戻り値破棄警告が抑制されてしまう

個別の式に対して属性指定が必要となるのはこのように組み込みカンマ演算子を使用した場合のみであり、これを追求するのは完全性を追求する二次的な目標ではあります。

この提案ではそれでもあえて式に対する属性指定を提案しており、そのアプローチとして2つのものを提案しています。

1つ目のアプローチは式の右側に属性を指定するものです。

expression:
    assignment-expression attribute-specifier-seq(opt)
    expression , assignment-expression
[[discard]] f();  // 警告されない(文に対する属性)
f() [[discard]];  // 警告されない(式に対する属性)

[[discard]] a(), b();  // 警告されない(文に対する属性)
a(), b() [[discard]];  // 警告されない(式全体に対する属性)
a() [[discard]], b();  // a()は警告されないが、b()は警告されうる

int x = (a() [[discard]], b()); // 警告されない、a()は抑制され、b()は破棄されていない

struct S {
  S(int i) 
    : m_i((check(i) [[discard]], i))   // 警告されない
  {}

  int m_i;
};

このアプローチにはいくつか問題があります

  • 配列のnew式との競合
    • auto ptr = (new T[123] [[someattribute]]);が現在合法なコード
  • 変換関数を指定する式における競合
    • struct S { operator int() const; };がある時
    • auto ptr = (&S::operator int [[attribute]]);が現在合法なコード

このアプローチを採用する場合、この既存のコードとの衝突の影響を評価した上でどうするかを決定する必要があります。

2つ目のアプローチはかっこで括った上で式の左側に属性を指定するものです。

primary-expression:
    literal
    this
    ( attribute-specifier-seqopt expression )
    id-expression
    lambda-expression
    fold-expression
    requires-expression
[[discard]] f();   // 警告されない(文に対する属性)
([[discard]] f()); // 警告されない(式に対する属性)
f() [[discard]];   // ill-formed

[[discard]] a(), b();     // 警告されない(文に対する属性)
([[discard]] a(), b());   // 警告されない(式全体に対する属性)
([[discard]] a()), b();   // a()は警告されないが、b()は警告されうる

int x = ([[discard]] a(), b()); // 警告されない、a()は抑制され、b()は破棄されていない
int y = ([[discard]] a()), b(); // 警告されない、a()は抑制され、b()は破棄されていない

struct S {
  S(int i) 
    : m_i(([[discard]] check(i)), i)  // 警告されない
  {}

  int m_i;
};

こちらのアプローチでは対象の式を一々かっこで括る必要があるものの、式の左側という自然な位置に属性を導入でき、かっこで括ることによってどの式に属性を指定しているのかが明確になります。

この提案ではこれらのアプローチをのどちらを選択するかの決定をEWG/EWGIに委ねています。

EWGIの投票では、この提案の[[discard]]属性については好まれたものの、個別の式に対する属性指定にはコンセンサスが得られませんでした。おそらく、式文に対する属性としての[[discard]]としてEWGに転送されています。

P2994R0 On the Naming of Packs

パラメータパックそのものを指定する構文を検討する提案。

C++11で導入された可変長テンプレートとパラメータパックはとても便利な機能ですが、基本的にパックそのものにできることは展開のみです。C++17で畳み込み式が追加されましたが、パックそのものに対する操作はまだ導入されておらず、いくつかの提案が進行中です。

そのような機能の難しい点は、パック自体に操作を適用する構文とパックを展開してその要素に操作を適用する構文を区別するようにしなければならない点です。例えばインデックスアクセスの場合、パックの最初の要素にアクセスするのにpack[0]のような構文を選択できません。なぜなら、f(pack + pack[0]...)は現在有効な式であり、これはパック最初の要素をパック内の全ての要素に加算するという意味にはならないためです。

このため、パックそのものに操作を適用する機能についての提案は、それぞれの機能のために個別の提案を選択します。

機能 単一要素 パック
インデックスアクセス elem[0] pack...[0]
展開ステートメント template for(auto x : elem) 次のうちのどれか
template for(auto x : {pack...})
template for...(auto x : pack)
for...(auto x : pack)
リフレクション ^elem なし
スプライス [: elem :] ... [: pack :] ...

elemは何か単一の値(非パック)、packは関数引数パックです。

パラメータパックに対する構文は単一要素に対するものと異なっているだけでなく、パラメータパックそのものに対する操作の間でも異なっています。ここには直交性がなく、パックに対して操作を適用したい場合に...をどこに置くのかは場合により変化します。

ここで問題にしているのは個別の機能そのものについてではなく、それらの間でパックそのものを指定する構文に一貫性がないことです。

この提案は、パラメータパックそのものを指定する構文をまず考案し各操作ではそれをベースとした構文を採用するようにすることで、パックそのものに対する操作ごとに個別の構文を導入するのを回避し、直交性と一貫性を回復しようとするものです。

ただし、パックそのものを指定する構文は捻り出す必要もなく現在すでに存在しています。それは、パラメータパックの(関数引数パックの)宣言やラムダ式の初期化キャプチャで現れる...packという構文です。この提案はこれをそのまま採用し、各機能に展開していくことを提案しています。先ほどの表に当てはめると次のようになります

機能 単一要素 パック この提案
インデックスアクセス elem[0] pack...[0] ...pack[0]
展開ステートメント template for(auto x : elem) 次のうちのどれか
template for(auto x : {pack...})
template for...(auto x : pack)
for...(auto x : pack)
template for(auto x : ...pack)
リフレクション ^elem なし ^...pack
スプライス [: elem :] ... [: pack :] ... [: ...pack :] ...

この提案による構文では、単一要素の構文においてelem...packで置き換えた形になっており、パックに対する各種操作の間でも一貫しています。

個別の機能の個別の提案を見ると、一番左の列の構文よりも中列の構文を好む人はいるかもしれませんが、パック操作の全体を俯瞰したときにはこの構文による一貫性と直交性がその小さな好みを上回るだろうとしています。

この提案による構文には1つ空白地帯があります。

// この提案が採用されたとして
template <typename... Ts>
void foo(Ts... pack) {
  // パックの最初の要素を取得
  auto first = ...pack[0];

  // パックの各要素をイテレートする
  template for (auto elem : ... pack) { }

  // では、これはどういう意味?
  auto wat = ...pack;
}

ここを突き詰めると言語タプルのような用法が開かれる可能性もありますが、この提案ではそれは将来の発明に期待するとしてとりあえずは禁止(ill-formed)としておくことを提案しています。

P2995R0 SG16: Unicode meeting summaries 2023-05-24 through 2023-09-27

SG16(Unicode Study Group)の2023年5月~9月にかけてのミーティングの議事録。

7回のミーティングにおいての提案等のレビュー時の様子が簡単に記録されています。

P2996R0 Reflection for C++26

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

この提案は以前に提案されていたP1240R2のサブセットであり、主に次のものからなります

  • 定数式におけるプログラム要素を表現する鏡像値(reflection value)は、不透明型std::meta::infoの値となる
    • 鏡像値は略して、単に鏡像(reflection)と呼ぶ
  • 与えられたオペランドの鏡像を生成する反射演算子reflection operator^
  • 鏡像に対して作用する(別の鏡像を導出する事を含む)多くのconstevalメタ関数
  • 鏡像から文法要素を生成するスプライサー(splicer
    • [: refl :]のような構文
  • 追加のマイナーな変更

この提案はP1240R2の中から有用なコアな部分を抽出したもので、静的リフレクション及びそれを使用したコンパイルメタプログラミングに関する最後の提案ではありません。P1240R2の残りの部分や更なる機能はこの提案のコア機能をベースとして後から成長させていく事を意図しています。

鏡像を表現するのにstd::meta::infoという単一の型を使用しているのは、言語エンティティに対応する個別の型を用意してしまうとその存在が言語に制約を与えてしまうためです。例えば、C++03では変数と呼ばれるものには参照が含まれていませんでしたが、C++11では変数は参照を含むようになりました。もしC++03にstd::meta::variableのようなものが存在していた場合、変数のカテゴライズの変更がそれを使用しているコードに影響を与えるため不可能だったでしょう。この提案では、将来の言語に不当な制約を課さないためにあえて鏡像を保持する型を単一のstd::meta::infoに限定しています。

リフレクション領域と構文領域を接続する例

constexpr auto r = ^int;  // int型の鏡像を取得

// 鏡像から型名の情報を取り出して構文要素にする
typename[:r:] x = 42;       // 次と等価 : int x = 42;
typename[:^char:] c = '*';  // 次と等価 : char c = '*';

クラスメンバをインデックスアクセスする例

struct S {
  unsigned i:2, j:6; 
};

// 指定された数値に対応するメンバの鏡像を取得する
consteval auto member_number(int n) {
  if (n == 0) {
    return ^S::i;
  } else if (n == 1) {
    return ^S::j;
  } else {
    return std::meta::invalid_reflection("Only field numbers 0 and 1 permitted");
  }
}

int main() {
  S s{0, 0};

  // member_number()の返すメンバ変数の鏡像をメンバ名にする
  s.[:member_number(1):] = 42;  // 次と等価 : s.j = 42;
  s.[:member_number(5):] = 0;   // Error (likely with "Only field numbers 0 and 1 permitted" in text).
}

型のリストから型のサイズのリストを作成する例

// 型の鏡像のリスト
constexpr std::array types = {^int, ^float, ^double};

// typesの各要素の型のサイズのリスト
constexpr std::array sizes = []{
  std::array<std::size_t, types.size()> r;
  std::ranges::transform(types, r.begin(), std::meta::size_of);
  return r;
}();
// 次と等価
// std::array<std::size_t, 3> sizes = {sizeof(int), sizeof(float), sizeof(double)}:

std::make_integer_sequenceを実装する例

#include <utility>
#include <vector>

template<typename T>
consteval std::meta::info make_integer_seq_refl(T N) {
  // 整数型Tの鏡像を先頭に入れておく
  std::vector args{^T};

  for (T k = 0; k < N; ++k) {
    // 各kをその定数値の鏡像へ変換しvectorへ詰める
    args.push_back(std::meta::reflect_value(k));
  }

  // argsの各鏡像をテンプレート引数要素へ変換しinteger_sequenceのテンプレート引数として埋めてその鏡像を返す
  return std::meta::substitute(^std::integer_sequence, args);
}

// std::integer_sequence<T, 0, ..., N - 1>となる
template<typename T, T N>
  using make_integer_sequence = [:make_integer_seq_refl<T>(N):];

std::meta::substitute()はテンプレートの鏡像とテンプレート引数の鏡像のリストを受けて、1つ目の引数のテンプレート引数としてリストの各要素の鏡像の実体で埋めた型の鏡像を返すものです。

namespace std::meta {
  consteval auto substitute(info templ, span<info const> args) -> info;
}

値ベースリフレクションの強い点として、これらの例のようにstd::vectorstd::spanなどを使用しながら通常の定数式としてリフレクション処理を記述できる点があります。

列挙値の文字列化

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  // 列挙型Eの列挙値毎にif文を生成する
  template for (constexpr auto e : std::meta::members_of(^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::name_of(e));
    }
  }

  return "<unnamed>";
}

enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

ここでは、template for(拡張ステートメント)を使用していますが、これはこの提案の一部ではなく別に提案(P1306R1)されているものです。template forは通常のfor文のように評価される時にループするのではなく、それ(を含む関数)が実体化されたときにループし、ループとともに本体内のコードをその場に順番にコピペしていくような動作をします。すなわち、鏡像オブジェクトのリストからのコード生成をサポートするものです。

プログラムオプションをパースする例

using std::meta;

template<typename Opts>
auto parse_options(std::span<std::string_view const> args) -> Opts {
  Opts opts;

  // オプション定義型Optsの非静的メンバ毎にループ
  template for (constexpr auto dm : nonstatic_data_members_of(^Opts)) {
    auto it = std::ranges::find_if(args,
      [](std::string_view arg){
        return args.starts_with("--") && args.substr(2) == name_of(dm);
      });

    if (it == args.end()) {
      // no option provided, use default
      continue;
    } else if (it + 1 == args.end()) {
      std::print(stderr, "Option {} is missing a value\n", *it);
      std::exit(EXIT_FAILURE);
    }

    using T = typename[:type_of(dm):];
    auto iss = std::ispanstream(it[1]);
    if (iss >> opts.[:dm:]; !iss) {
      std::print(stderr, "Failed to parse option {} into a {}\n", *it, display_name_of(^T));
      std::exit(EXIT_FAILURE);
    }
  }

  return opts;
}

struct MyOpts {
  string file_name = "input.txt";  // Option "--file_name <string>"
  int    count = 1;                // Option "--count <int>"
};

int main(int argc, char *argv[]) {
  MyOpts opts = parse_options<MyOpts>(std::vector<std::string_view>(argv+1, argv+argc));
  // ...
}

この例ではparse_options()自体は実行時に実行される関数ですが、その内部で使用されている拡張ステートメントやスプライサーなどはコンパイル時に(parse_options()が実体化したときに)処理されます。

シンプルなtuple実装例

#include <meta>

template<typename... Ts>
struct Tuple {
  // Tsの各型を非静的メンバとして保持する構造体を生成
  using storage = typename[:std::meta::synth_struct({nsdm_description(^T)...}):];
  storage data;

  Tuple(): data{} {}
  Tuple(Ts const& ...vs): data{ vs... } {}
};

template<typename... Ts>
  struct std::tuple_size<Tuple<Ts...>>: public integral_constant<size_t, sizeof...(Ts)> {};

template<typename I, typename... Ts>
  struct std::tuple_element<I, Tuple<Ts...>> {
    using type = [: template_arguments_of(^Tuple<Ts...>)[I] :];
  };

template<typename I, typename... Ts>
  constexpr auto get(Tuple<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {
    // Tuple::storageのメンバ名をインデックスから求めてアクセス
    return t.data.[:nonstatic_data_members_of(^decltype(t.data))[I]:];
  }

// Similarly for other value categories...

nsdm_description()は型の鏡像をその型の非静的メンバの鏡像となるものに変換するメタ関数です(nsdm=non static data member)。そして、synth_struct()は非静的メンバの鏡像のリストを受け取って、その非静的メンバを持つ構造体型の鏡像を返すメタ関数です。nsdm_description()は特に指定しなければ結果の非静的メンバ変数名は未規定となりますが、この例のget<I>()実装ではNTTPインデックスを用いてそのインデックスに対応するメンバ名を直接取得しており、そのメンバ名を知る必要がないようになっています。

そのほかにも様々な例が提案には記載されています。

なお、この記事における訳語は筆者の独断によるもので、何かしらの合意を得たものではありません。

P2997R0 Removing the common reference requirement from the indirectly invocable concepts

イテレータを介した間接呼び出し系のコンセプトから、common_reference要件を取り除く提案。

問題の説明のために、次のようなrange型を仮設します。

struct C {
  auto f() -> void;
};

struct Iterator {
  using value_type = C;
  using difference_type = std::ptrdiff_t;
  using iterator_category = std::input_iterator_tag;

  auto operator*() const -> C&&;
  auto operator++() -> Iterator&;
  auto operator++(int) -> void;
  auto operator==(Iterator const&) const -> bool;
};

static_assert(std::input_iterator<Iterator>);
static_assert(std::same_as<std::iter_value_t<Iterator>, C>);
static_assert(std::same_as<std::iter_reference_t<Iterator>, C&&>);

struct R {
  auto begin() -> Iterator;
  auto end() -> Iterator;
};

static_assert(std::ranges::range<R>);
static_assert(std::same_as<std::ranges::range_reference_t<R>, C&&>);

ここで重要なことは、このRの要素の参照型がC&&である点であり、他の部分はイテレータrangeを整えるための部分でしかありません。そしてこのRstd::generator<C>と同じ性質を備えています。

これを範囲forranges::for_eachで普通にループを回してみると異なった振る舞いが得られます

void test(R r) {

  // これはok
  for (auto&& c : r) {
    c.f();
  }
  
  // こっちはng
  std::ranges::for_each(r, [](auto&& c){
    c.f();  // const修飾がマッチしないらしい
  });
}

エラーメッセージを見てみると、const C&cc.f()を呼び出そうとしているけれどC::f()const修飾がないため呼び出すことができずエラーになっているようです。

この例のコードにはどこにも、Rの要素をconst化するコードはありません。const C&は一体どこから来たのでしょうか?

ranges::for_eachでしか起こらないことからranges::for_eachそのものに問題があると考えられます。その宣言は次のようになっています

template<input_range R, class Proj = identity,
         indirectly_unary_invocable<projected<iterator_t<R>, Proj>> Fun>
constexpr ranges::for_each_result<borrowed_iterator_t<R>, Fun>
  ranges::for_each(R&& r, Fun f, Proj proj = {});

呼び出しに関して制約してるのはindirectly_unary_invocableコンセプトで、これは次のように定義されています

template<class F, class I>
concept indirectly_unary_invocable =
  indirectly_readable<I> &&
  copy_constructible<F> &&
  invocable<F&, indirect-value-t<I>> &&
  invocable<F&, iter_reference_t<I>> &&
  invocable<F&, iter_common_reference_t<I>> &&
  common_reference_with<
    invoke_result_t<F&, indirect-value-t<I>>,
    invoke_result_t<F&, iter_reference_t<I>>>;

ranges::for_eachの場合、このIにはprojected<iterator_t<R>, Proj>>が指定されており、Franges::for_eachに渡した各要素に対する処理を記述した呼び出し可能オブジェクト(例ではラムダ式)です。

プロジェクションがデフォルトのままなので、Projstd::identityであり、projected<iterator_t<R>, identity>>Iと同じ性質を備えたイテレータっぽい型になります。したがって、ここではprojectedを無視できます。

Fが適切にRの要素型に対して呼び出し可能として定義されているとして(範囲forでは同等の処理でコンパイルが通るのでその仮定は満たされているはず)、indirectly_unary_invocable<F, Iterator>の各制約を検討します。

Iinput_iteratorであることは予め確認しており、Fは今ラムダ式クロージャ型でありムーブオンリー型をキャプチャしない限りはコピー構築可能になります。したがって最初の2つの制約はパスしています。

残りのイテレータ経由の呼び出しに関わるコンセプトを調べる前に、イテレータIに関する関連型をまず明らかにしておきます。

関連型
indirect-value-t C&
iter_reference_t C&&
iter_value_t C
iter_common_reference_t const C&

iter_common_reference_t<I>Iiter_value_titer_reference_tの間のcommon_referenceを求めるもので、common_reference_t<C&&, C>は通常const C&になります。

これらの型を使って残りのコンセプトの妥当性をチェックしていくと次のようになります

制約 結果
invocable<F&, indirect-value-t<I>>
invocable<F&, iter_reference_t<I>>
invocable<F&, iter_common_reference_t<I>>
common_reference_with<...>

したがって、冒頭の例のエラーの原因はindirectly_unary_invocableコンセプトの中のinvocable<F&, iter_common_reference_t<I>>が満たされないことによるもので、const C&iter_common_reference_t<I>で発生していることがわかりました。

この制約が満たされないのはまさに、[](auto&& c){ c.f(); }というF&const C&を渡して呼び出そうとしているためです。

イテレータの共通参照型(iter_common_reference_t)の要件は(上記ranges::for_eachのような)アルゴリズムに呼び出し可能オブジェクト(以下callable)を渡す際に、そのcallableの引数型をジェネリックにする必要をなくすためのものです。それによって、ユーザーはイテレータ型の性質に踏み込まなくても自然にそのようなアルゴリズムを使用できます。

ところがそのような共通参照型は実際にアルゴリズムの実装で使用されるわけではありません。現在の標準ライブラリ内で実際に共通参照型を使用しているのは次の場所のどちらかのみです

  • 明示的にマージを行っているため、実際に複数の範囲の共通参照が必要な場合
    • views::join_withや提案中のviews::concatなど
  • 新しい参照型を生成しようとしている時
    • views::as_constなど

つまり、アルゴリズムの実装においては共通参照型を使用することは無く、実際にiter_common_reference_tが使用されるべきなのはユーザーがアルゴリズムに渡すcallableオブジェクトの引数型においてであり、そのようなcallableアルゴリズム中で使用可能かどうかの制約は上2つの値型と参照型による制約でチェックされています。

共通参照型が存在するのは、複数の範囲をマージできるようにするためと、一般的ではない呼び出し可能オブジェクトを書く方法をユーザーに提供するためですが、一般的なアルゴリズムの実装にとって有益な要件ではありません。この呼び出し(invocable<F&, iter_common_reference_t<I>>のような)を要求する一方で、使用されていないこの呼び出しが実際にアルゴリズム中で使用されている呼び出し方法と互換性があるかどうか(最後のcommon_reference_with<...>のような)はチェックされていません。したがって、このチェックは有効なコードを拒否するだけで何ら価値を提供していません。

この提案は、これらの理由により全てのイテレータを介した間接呼び出し系のコンセプトからこのようなイテレータの共通参照型に関する要件を削除することを提案するものです。

対象は次の6つのコンセプトです

  • indirectly_unary_invocable
  • indirectly_regular_unary_invocable
  • indirect_unary_predicate
  • indirect_binary_predicate
  • indirect_equivalence_relation
  • indirect_strict_weak_order

現在これらのコンセプト内に存在するinvocable<F&, iter_common_reference_t<I>>のようなiter_common_reference_tによって呼び出し可能であることをチェックする制約のみを単純に削除することを提案しています。

P2999R0 Sender Algorithm Customization

P2300のsenderアルゴリズムがカスタマイズを見つける手段を修正する提案。

P2300で提案されているsenderアルゴリズムexecution::thenexecution::justなど)は全てカスタマイゼーションポイントオブジェクトであり、tag_invokeによるディスパッチによってユーザーによってカスタマイズされた実装を検出するとそれを優先して使用します。そのようなカスタマイズが見つからない場合にのみデフォルトの実装が使用されます。senderアルゴリズムのカスタマイズは実行コンテキストに対して行われるものであり、P2300では実行コンテキストの表現はschedulerが担っています。

実行コンテキストは処理が実行される場所を指す抽象であり現在の環境ではCPU/GPU/FPGAなどが該当しますが、将来的に全く性質の異なるアクセラレータが登場する可能性があります。そうした現時点で姿形のない将来の実行コンテキストにおいてもsenderアルゴリズムを効率的に実行できるようにするために、senderアルゴリズムのカスタマイズはscheduler(実行コンテキスト)に対して行われます。

したがって、senderアルゴリズムがそのカスタマイズを検出するにはsenderアルゴリズムが構成される時点においてそのアルゴリズムが完了する実行コンテキスト(completion scheduler、完了scheduler)を知っていなければなりません。それがわからない場合はデフォルトの実装が使用され、開始時などに後から与えられたschedulerの実行コンテキストで処理を行い完了します。

現在のthenアルゴリズムの実装例

// アルゴリズムのカスタマイズを検出するヘルパコンセプト
template <class AlgoTag, class SetTag, class Sender, class... Args>
concept has-customization =
  requires (Sender sndr, Args... args) {
    tag_invoke(AlgoTag(),
               get_completion_scheduler<SetTag>(get_env(sndr)),
               std::forward<Sender>(sndr),
               std::forward<Args>(args)...);
  };

// `then`アルゴリズムの実装型かつタグ型
struct then_t {
  template <sender Sender, class Fun>
    requires /* requirements here */
  auto operator()(Sender&& sndr, Fun fun) const
  {
    // sndrは先行する処理を表すsender
    // funはthenによって継続する処理

    // 先行senderから完了schedulerを取得でき
    // かつカスタムthen実装が見つかる場合はそちらにディスパッチする
    if constexpr (has-customization<then_t, set_value_t, Sender, Fun>)
    {
      auto&& env = get_env(sndr);
      return tag_invoke(*this,
                        get_completion_scheduler<set_value_t>(env), // 完了schedulerの取得
                        std::forward<Sender>(sndr),
                        std::move(fun));
    }
    else
    {
      // デフォルト実装を使用する
      return then-sender<Sender, Fun>(std::forward<Sender>(sndr), std::move(fun));
    }
  }
};

inline constexpr then_t then {};

これはたとえば、just(42) | then([](int) { ... })のように使用してsenderアルゴリズムによって処理グラフを構成しますが、その実装のカスタマイズが検出されるのはthen_toperator()が呼ばれてsenderを取得した時点であることがわかります。schedulerそのものはこのグラフの上から(予め)でも下から(後から)でも指定することができますが、senderアルゴリズムのカスタマイズ実装が検出されるのは上からschedulerが指定されている場合のみです。

現在のこのような仕組みにはいくつかの欠点があります

  1. just(42)(値を投入するだけのsenderアルゴリズム)のような単純なsenderはその完了schedulerを知らない
    • その処理が開始された実行コンテキストで完了するが、これはsenderが構築された時点ではわからない
  2. on(sched, then(just(), fun))(実行コンテキストの指定/切り替え)のようなsenderの場合、ネストしたthen(just(), fun)の部分のsenderschedulerを知らない状態で構築される
    • ここでschedに対してカスタマイズされたthen実装を使用させるにはどうすればいい?
  3. when_all(sndr1, sndr2)(指定された全ての処理の完了待機)のような複合senderは一般的にはその完了schedulerを知ることができない
    • sndr1, sndr2の両方が完了schedulerを知っていたとして、それぞれをsched1, sched2としてもsndr1, sndr2のどちらが先に完了するかによってwhen_allsenderの完了schedulersched1, sched2のどちらかになる
    • これは実行時の動的な性質であり、アルゴリズムのカスタマイズを検出するのに適していない

1と2の場合、正しいアルゴリズム実装を見つけるために必要な情報がそれを探索する時点で利用できないという問題であり、3の場合はアルゴリズムのセマンティクスによってアルゴリズムのどのカスタマイズを使用すべきかを静的に決定できないという問題です。

2の問題は特にプログラマの意図しない動作につながる可能性があるため深刻です

正しい 間違い
my::thread_pool_scheduler sch = /*...*/;

// スレッドプール上でのバルク処理の記述
auto work =
  ex::transfer_just(sch, data)
| ex::bulk(data.size(),
           [](int i, auto& data) {
             ++data[i];
           });

// 処理の実行
std::this_thread::sync_wait(std::move(work));
my::thread_pool_scheduler sch = /*...*/;

// バルク処理の記述
auto work =
  ex::just(data)
| ex::bulk(data.size(),
           [](int i, auto& data) {
             ++data[i];
           });

// 処理をスレッドプール上で実行する
std::this_thread::sync_wait(ex::on(sch, std::move(work)));

この2つの例の違いは処理を実行する場所であるschedulerを先に与えるか後に与えるかの違いのみです。当然、この2つの例は同じ動作をすることが期待されます。

しかしここで、my::thread_pool_schedulerの提供者がそのスレッドプールのためにカスタマイズしたbulkアルゴリズムを提供していたとすると

namespace my {
  // thread_pool_schedulerのためのbulkアルゴリズムのカスタマイズ
  template <ex::sender Sender, std::integral Shape, class Function>
  auto tag_invoke(ex::bulk_t,
                  thread_pool_scheduler sched,
                  Sender&& sndr,
                  Shape shape,
                  Function fun) {
    /*...*/
  }
}

左側のコードはこのカスタマイズを検出し(transfer_justによって処理グラフの最初のsenderに完了schedulerが与えられているため)意図通りにスレッドプールのスレッドをフルに使用してバルク処理を実行します。しかし、右側のコードではこのカスタマイズを検出できないため(just(data)には完了schedulerが与えられていないため、後続のbulkthread_pool_schedulerに対するカスタマイズを見つけられない)、デフォルト実装が使用された結果としてバルク操作はスレッドプールの1つのスレッドだけを使用して実行されます。

このことは明らかに間違った動作であり、修正が必要となります。この提案はその修正を行おうとするものです。

この提案による変更は次のようなものです

  1. 他に決定可能なドメインがない場合に使用するdefault_domain型を追加
  2. 新しいget_domain(env) -> domain-tag転送クエリを追加
  3. カスタマイズ不可能なtransform_sender(domain, sender [, env]) -> senderを追加
    • これは初期のカスタマイズと後からのカスタマイズの両方に使用される
    • 初期のカスタマイズ(Early customization
      • senderアルゴリズムのカスタマイゼーションポイントオブジェクト内から呼ばれる
      • 完了schedulerをタグとして使用してカスタマイズを検出する現在の仕組みを置換する
      • 環境変数なしで呼び出される
      • 次のいずれかによってsenderからドメインを取得する
        • get_domain(get_env(sender))
        • get_domain(get_completion_scheduler<completion-tag>(get_env(sender)))
        • default_domain()
    • 後からのカスタマイズ(Late customization
      • connectカスタマイゼーションポイントオブジェクト内から、connect_tのカスタマイズ検出の前に呼び出される
      • receiver環境変数によって呼び出される
      • 次のいずれかによってreceiverからドメインを取得する
        • get_domain(get_env(receiver))
        • get_domain(get_scheduler(get_env(receiver)))
        • default_domain()
    • transform_sender(domain, sender [, env])は次のいずれか有効なものを返す
      • domain.transform_sender(sender [, env])
      • default_domain().transform_sender(sender [, env])
      • sender
  4. 標準の遅延sender型は構造化束縛によって[tag, data, …children]に分解可能なsender型を返す
  5. 全ての引数のドメイン型が同じにならない限り、when_allアルゴリズムの呼び出しはill-formed
    • when_allの返すsenderはその環境を介してそのドメインを公開する
  6. on(sch, sndr)アルゴリズムtransferアルゴリズムの後からのカスタマイズを拾い上げるためにtransferに対して指定されるべき
  7. senderファクトリjust, just_error, just_stoppedでは、タグ型を指定する必要がある
  8. let_value(sndr, fun)アルゴリズムでは、先行するsndrsenderオブジェクト)がset_valueの完了schedulerを持っている場合、二次接続側(後続のsender)に接続されるrecieverはそのschedulerを現在の環境のschedulerとして公開する
    • 言い換えると、先行するsndrが値vs...で完了する場合、fun(vs...)の結果はget_scheduler(get_env(r))get_completion_scheduler<set_value_t>(get_env(sndr))と等しくなるようなrecieverオブジェクトrに接続される
    • let_errorも同様に、先行senderの完了schedulerをクエリする時にset_error_tを使用する
  9. schedule_from(sched, sndr)アルゴリズムget_domain(get_env(s))get_domain(sched)と同じになるようなsenderオブジェクトsを返す必要がある
  10. 次のアルゴリズムについては、デフォルトの実装では結果のsenderを返す前に作業を行う必要があり、それはdefault_domain::transform_senderオーバーロードで行われる
    • split
    • ensure_started
  11. 次のアルゴリズムはデフォルトの実装が他のより原始的な操作を利用して指定されているが、それらをdefault_domain::transform_senderオーバーロードによって置き換えた形になる
    • transfer
    • transfer_just
    • transfer_when_all
    • transfer_when_all_with_variant
    • when_all_with_variant
  12. let_value(snd, fun)アルゴリズムでは、入力関数funが返す可能性のあるsenderの型は全て同じドメインを持っていなければならない
    • そうでなければ、let_valueの呼び出しはill-fomred
    • let_valuesenderはそれを自身のドメインとして報告する
    • let_errorlet_stoppedでも同様
  13. sender消費アルゴリズムstart_detachedsync_waitでは、タグディスパッチのためにアルゴリズムタグと入力senderドメインをタグとして使用する

この提案では、senderアルゴリズムは指定された完了schedulerではなくその処理グラフのドメインからアルゴリズムのカスタマイズ実装を発見するようにしています。コード上ではドメインドメインタグ型として表現され、when_allのようなアルゴリズムでは入力のsenderドメインが全て一致(すなわち、ドメイン型が一致)している場合にのみ使用可能となります。そして、senderアルゴリズムによる処理グラフの構成時に加えて、全ての情報が揃った時(senderreceiverconnectされた時)に再度カスタマイズ実装を探索する後からのカスタマイズ(Late customization)を有効化でしています。

初期のカスタマイズ/後からのカスタマイズ双方共にその処理はデフォルトのsenderドメイン及び環境に基づいて変換する作業になり、そのための関数としてtransform_sender()を追加しています。transform_sender()ドメインsender及びオプションで環境オブジェクトを受け取って、それらの情報を使用してそのsenderを現在のドメインと環境によって発見されるカスタマイズを適用したものに変換します。後からのカスタマイズではこれをexecution::connectの呼び出し内で行うことで処理グラフの構成後に全ての情報が揃った状態でsenderアルゴリズムのカスタマイズが発見できるようになります。

transform_sender()自体は指定されたドメインtransform_sender()senderと環境を渡して返すか、それができない場合はデフォルトのドメインを使用して同じことを行うか、それもできなければ元のsenderをそのまま返します。ドメインtransform_sender()ではそのドメインに沿った形でsender型のカスタマイズを発見する処理を行いますが、デフォルトのドメインでは単にsender型に対してそれを行い、標準のsender型の多くはtransform_sender()を用意しておりそのデフォルトの動作を行うsenderを返します。

P3001R0 std::hive and containers like it are not a good fit for the standard library

std::hiveは標準ライブラリのコンテナとしてふさわしくないとする提案。

まず、標準ライブラリの要素は理想的には次のいずれかに該当するものです

  • コンパイラサポートが必要な型や関数
    • 標準ライブラリはコンパイラと一緒に出荷されるため、コンパイラサポートの必要な物を配置できる唯一の場所
  • コアな語彙型
    • std::optional, std::span, std::string_viewなどの語彙型は汎用性と表現力に優れ、C++プログラミングの基礎部品となる
    • 語彙型を標準化しない場合、同じ目的に対応する独自実装がライブラリ毎に定義され、それの間の相互変換のために余計なオーバーヘッドがかかる
  • クロスプラットフォームの抽象
    • 標準ライブラリはプラットフォームの専門家によって実装されており、ほとんどのプラットフォームはI/Oやメモリ割り当て、スレッドなどの機能を提供する
    • これを標準化することで、ユーザーはクロスプラットフォームで統一的にそれを利用でき、実装者はプラットフォームの専門知識を活かしてそれを実装できる
  • 基礎的なアルゴリズムとデータ構造
    • コンテナ(動的配列やキュー、スタックなど)とアルゴリズム(ソートや検索など)はほとんどすべてのプログラミングタスクの基礎であり、必須の機能である
    • 頻繁に必要となるこれらのものを標準化することで、ユーザーはそれを再実装する必要がなくなる
    • またこれらの機能は、広く理解できるセマンティクスと安定した実装を備えてもいる
    • これらのエンティティは、エンティティを再発明することなく作業を完了するために重要であるという点で、語彙型とは異なる
      • 語彙型は規則を確立するために重要であり、異なるコード間の相互運用に利用される

これに該当しないものが標準ライブラリとしてふさわしくないわけではありませんが、該当しないものを標準化するにはそれなりの根拠が必要となります。

賛否はあれど、C++標準ライブラリは安定したABIとAPIを維持しており、そこからの逸脱はユーザーに大きな混乱をもたらします。std::vector<bool>のように明らかに失敗とみなされる機能であっても非推奨や削除されることは稀であり、残り続けます。そのため、標準化委員会はインターフェースが確立されていない限りライブラリ機能を標準化することはできず、一度標準化されるとライブラリのAPIとABIは事実上凍結されます。それによって、実装も変更できなくなる場合があります。

標準化された機能はすべてのプラットフォーム間で移植可能である必要があり、その実装や品質はプラットフォームによって異なります。そのため、すべてのプラットフォームで利用できるわけではないAPIや、パフォーマンスなどの特定の実装特性に依存したAPIの標準化には注意が必要です。

そして、標準化委員会の時間は限られており、ある機能の議論に時間をかけるということは別の提案の議論の時間が取られるということを意味しています。

std::hiveは高性能コンテナに該当するタイプのコンテナであり、実行時の動作やメモリ使用量などの点で既存の標準コンテナに対して優位性を持つコンテナです。このようなコンテナには次のような特徴があります

  • 時間・空間計算量で他の実装よりも優れている
  • 関連するベンチマークで他の実装よりも明確に優れた実行時の速度やメモリ使用量が計測される
  • 語彙型である必要はない
    • APIの特定の部分にカスタムの特化型を使用することはパフォーマンス上のメリットがある
  • 積極的にメンテナンスされている
    • CPUは進化し続けており、より優れたアルゴリズムがすぐに利用可能になる
    • 高性能コンテナがそうあり続けるためには、これらの変化に適用し改善し続けなければならない

この性質はその実装が安定しておらず、プログラミングにおいて必須というわけでもないことを示しています。さらに、高性能コンテナはコンパイラのサポートやOSのAPIのサポートを必要とせず、語彙型でもないため、最初に挙げた標準ライブラリ要素のカテゴリのいずれにも該当しないことになります。

むしろ、高性能コンテナを標準化した場合の欠点を上げることができます

  • 安定性の要件は高性能コンテナの進化を妨げる
    • メンバ変数の追加などはABI破壊であり、内部実装のほとんどはAPIの要件を介して公開されている
  • 標準化のためには委員会の多大な時間を必要とする
    • 標準化に時間を食っている間に想定する実装が時代遅れになっていたとしても、ABIの問題から更新できない
  • 標準ライブラリはインターフェースのみを標準化しており、実装は標準化していない
    • 実装は複数の標準ライブラリベンダによって行われ、そのパフォーマンスはプラットフォームごとに異なる可能性がある
    • 標準ライブラリ機能を使用する場合、パフォーマンスの保証はない

それでも高性能コンテナを実装することの利点はせいぜい、外部ライブラリに依存せずにそのコンテナを利用できるようになるくらいのものです。

ここまでの高性能コンテナの批判は一般的な話ですが、std::hiveは高性能コンテナなのですべて該当します。

提案者によって提供されているリファレンス実装は堅牢であるようで、有用性は疑うべくもありません。しかし、委員会が標準化するのはリファレンス実装ではなくインターフェースであり、それは標準ライブラリ実装者が独自のトレードオフを行うのに十分な余地を残すのと同時に、後の最適化によって重大な変更が発生する可能性があるほど具体的です。わざわざstd::hiveを使用するほど性能にこだわるのに、標準ライブラリの実装品質や外部ライブラリの保証に無頓着であるということは考えられません。

これらのマイナス面を無視したとして、std::hiveを導入するメリットを考えてみます。

前述のように、最初に挙げた要素にはいずれも該当していません。残るは標準ライブラリに載せることでサードパーティライブラリを入手するためのメカニズムをセットアップする必要がなくなるため使いやすくなるという利便性の向上です。しかし、現在使用したい人はリファレンス実装ないし同等の特性を持つ代替実装をなんとかして使用していると考えられ、現在それを使用していないプロジェクトで使用されるようになるかは疑問があります。

この提案は、ここまで述べたように、std::hiveを標準化するメリットはほぼなくstd::hiveそのものの利点も保証されない可能性があるため、C++26の限られた時間サイクルを割いてまでstd::hiveを標準化するべきではない、とするものです。

この提案を受けてのLEWGにおける投票では、std::hiveの標準化作業を続けることに合意されています。

P3002R0 Guidelines for allocators in new library classes

標準ライブラリ機能がアロケータを使用する際のポリシーの提案。

この提案は、上の方のP2267やP2979で提言されていたポリシー整備に関する具体的なものの一つです。ここではLEWGに向けて、新しいライブラリ機能がアロケータを使用するべきかや使用する際のガイドラインとして2つのポリシーを提案しています。

  1. クラスはどのような場合にアロケータを使用する必要があるか?
    • 動的にメモリを確保するクラスは、構築時にアロケータを受け入れ、確保と解放のためにそのアロケータのコピーを保持する必要がある
    • アロケータを使用するサブオブジェクト(基底クラス/非静的メンバ)を含むクラスは、それらのサブオブジェクトに転送するためにアロケータを構築時に受け入れる必要がある
  2. 型がstd::pmr名前空間エイリアスを持つ必要があるのはどのような場合か?
    • デフォルトでstd::allocatorの特殊化に設定されたアロケータパラメータをもつクラステンプレートには、std::pmr名前空間polymorphic_allocatorについてのエイリアスが必要
    • アロケータパラメータをstd::allocatorの特殊化に設定するもエイリアスを持つクラステンプレートは、std::pmr名前空間polymorphic_allocatorについてのエイリアスが必要

これらのポリシーは、現在の標準ライブラリのアロケータサポートをベースとしたものです。

P3003R0 The design of a library of number concepts

数値コンセプトの設計についての文書。

この文書のいう数値コンセプトとは、数学的な意味での数というものをC++コンセプトで定式化しようとするものです。主に、<numeric>にあるもののコンセプト対応や将来の物理量と単位を扱うライブラリにおいて使用することを意図しています。

まず全ての数値コンセプトはオプトインが必要で、そのためのいくつかの型特性が用意されます

  • numberコンセプトを有効化するためのenable_numberenable_complex_number
  • 特定の数値を示すnumber_zeronumber_one
    • 例えば、number_zero_v<T>のようにしてnumberTの零元を取得する
  • 数値に関連した型を取得するnumber_difference_tvector_scalar_t
    • number_difference_t<T>Tの差の結果型
    • vector_scalar_t<T>はベクトル空間Tのスカラ型

これによって、一番基本的なnumberコンセプトが次のように定義されます

template<typename T>
concept number = enable_number_v<T> && std::regular<T>;

template<typename T, typename U>
concept common_number_with =
  number<T> && number<U> && std::common_with<T, U> && number<std::common_type_t<T, U>>;

common_number_withは2つのnumber型を関連づけるためのコンセプトです。

他の数値コンセプトはこのnumberコンセプトをベースとして組み立てられます。例えば

// 順序付け可能な数
template<typename T>
concept ordered_number = number<T> && std::totally_ordered<T>;

// 数直線
template<class T>
concept number_line =
  ordered_number<T> &&
  requires(T& v) {
    number_one_v<number_difference_t<T>>;
    { ++v } -> std::same_as<T&>;
    { --v } -> std::same_as<T&>;
    { v++ } -> std::same_as<T>;
    { v-- } -> std::same_as<T>;
  };

また、基本的な数値演算に関するコンセプトも用意されています。例えば加算の場合

template<class T, class U> concept addition-with =
  number<T> &&
  number<U> &&
  requires(const T& c, const U& d) {
    { c + d } -> common_number_with<T>;
    { d + c } -> common_number_with<T>;
  };

template<class T, class U> concept compound-addition-with =
  addition-with<T, U> &&
  requires(T& l, const U& d) {
    { l += d } -> std::same_as<T&>;
  };

これらは今の所説明専用として定義されています。

最後に、これらによって代数的構造を表すコンセプトが定義されます。例えば

template<typename T, typename U>
concept point_space_for =
  subtraction-with<T, U> && // TとUの値t, uに対して、t - uが可能
  negative<U> &&            // Uは負の数を含む
  common_number_with<number_difference_t<T>, U>;

template<typename T, typename U>
concept compound_point_space_for = point_space_for<T, U> && compound-subtraction-with<T, U>;

// 点付き空間
template<typename T>
concept point_space = compound_point_space_for<T, number_difference_t<T>>;

// ベクトル空間
template<typename T>
concept vector_space = 
  point_space<T> && 
  compound-scales-with<T, vector_scalar_t<T>>;  // vector_scalar_t<T>はベクトル空間Tにおいてスカラ型としての性質を満たすこと

この文書による数値コンセプトの設計はまだ完全ではなく経験も不足しているため、この文書は現状報告であり、将来の標準数値コンセプトの実現のための1つの足がかりとして提出されたものです。

P3008R0 Atomic floating-point min/max

浮動小数点数型のstd::atomicにおけるfetch_max()/fetch_min()の問題を解消する提案。

P0493ではstd::atomicに対して指定した値との大小比較を条件として値の入れ替えを行うfetch_max()/fetch_min()を提案しており、これはC++26導入目前まで進んでいます。しかし、2023年6月の全体会議において、浮動小数点数型の場合の動作について問題が提起されたことで足踏みをしています。

fetch_max()/fetch_min()はその大小比較についてstd::min/std::maxをベースとしていますが、そのstd::min/std::max浮動小数点数型の特定の値の比較に対して望ましい結果(IEEE754に定義され、多くのハードウェア実装やCのライブラリ関数が返す結果)を返さないという問題がありました。

  • 符号付の0
    • -0 < +0と判断するのか否か
  • quiet NaN(qNaN)
    • 引数の片方がqNaNの場合、それを欠落したデータ(Missing Data)として扱うのか、エラーを伝播させるのか
C C++ signed 0 qNaN
std::min/std::max 同値 UB
fmin/fmax 同値、QoIとして-0 < +0 Missing Data
fminimum/fmaximum -0 < +0 error
fminimum_num/fmaximum_num -0 < +0 Missing Data

C++std::min/std::maxの場合qNaNは事前条件違反で未定義動作となり、その場合全ての実装で第一引数を返すようです。

// std::min/std::maxの動作例
min(qNaN, 2.f); // UB: qNaN
max(qNaN, 2.f); // UB: qNaN
min(2.f, qNaN); // UB: 2
max(2.f, qNaN); // UB: 2
min(-0.f, +0.f); // -0
max(-0.f, +0.f); // -0
min(+0.f, -0.f); // +0
max(+0.f, -0.f); // +0

この振る舞いは並行プログラミングにおいては問題となる可能性があり、異なるスレッドからの値の出力を待機してこのような比較を行う処理がある場合に、その処理結果はデータの到着順によって変化し、実行ごとに異なった結果になる(特に符号が異なる)可能性があります。

Cのfmin/fmaxの場合、符号付0の扱いはstd::min/std::maxと同じですが、QoI(実装品質)として-0 < +0とすることが許可されています。また、aNaNは欠落したデータとしてもう片方の値を返します。

// Cのfmin/fmaxの動作例
fmin(qNaN, 2.f); // 2
fmax(qNaN, 2.f); // 2
fmin(2.f, qNaN); // 2
fmax(2.f, qNaN); // 2
fmin(-0.f, +0.f); // -0 or +0
fmax(-0.f, +0.f); // -0 or +0
fmin(+0.f, -0.f); // -0 or +0
fmax(+0.f, -0.f); // -0 or +0

// Cのfminimum/fmaximumの動作例
fminimum(qNaN, 2.f); // qNaN
fmaximum(qNaN, 2.f); // qNaN
fminimum(2.f, qNaN); // qNaN
fmaximum(2.f, qNaN); // qNaN
fminimum(-0.f, +0.f); // -0
fmaximum(-0.f, +0.f); // +0
fminimum(+0.f, -0.f); // -0
fmaximum(+0.f, -0.f); // +0

// Cのfminimum_num/fmaximum_numの動作例
fminimum_num(qNaN, 2.f); // 2
fmaximum_num(qNaN, 2.f); // 2
fminimum_num(2.f, qNaN); // 2
fmaximum_num(2.f, qNaN); // 2
fminimum_num(-0.f, +0.f); // -0
fmaximum_num(-0.f, +0.f); // +0
fminimum_num(+0.f, -0.f); // -0
fmaximum_num(+0.f, -0.f); // +0

fminimum/fmaximumおよびfminimum_num/fmaximum_numはIEE754にある同名操作(頭のfを省いたもの)に対応する関数で、C23で追加されたものです。どちらの関数も-0 < +0となり、fminimum/fmaximumはqNaN入力に対してエラー伝播としてqNaN引数を返し、fminimum_num/fmaximum_numはqNaNを欠落したデータとして扱いもう片方の値を返します。

Cのこれらの関数はIEEE754の規定によく従った振る舞いとなります。

また、std::min/std::maxは現在のGPUのISAにおける浮動小数点数比較命令の結果とも一貫していません

ベンダ ISA 命令 対応 signed 0 aNaN
AMD CDNA2+ MIN/MAX minimum_num/maximum_num -0 < +0 Mssing Data
intel Xe ISA AOP_FMIN/AOP_FMAX minimum_num/maximum_num -0 < +0 Mssing Data
NVIDIA PTX atom red minimum_num/maximum_num -0 < +0 Mssing Data
SPIR V OpAtomicFMinEXT/OpAtomicFMaxEXT C fmin/fmax 同値、QoIとして-0 < +0 Mssing Data

この結果を受けて、C++における浮動小数点数型のstd::atomicfetch_max()/fetch_min()の設計指針は2つあり、std::min/std::maxと一貫させるかどうかです。両選択肢の比較は次のようになります

カテゴリ 一貫させる 一貫させない
x.fetch_min(y);
x.fetch_fminimum_num(y);
x.fetch_min(y, std::less{});
x.fetch_min(y);
利点 セマンティクスの一致
一貫性
安全なセマンティクスがデフォルト
ハードウェア命令のデフォルト
欠点 minという一般的な名前がポータブルではない振舞いをする
一般的な名前の処理がパフォーマンス的に不利になる
同名関数との非一貫性
atomicに移行する際の微妙な挙動の違い
教育の必要性 間違った使用とパフォーマンス 浮動小数点数型の微妙な動作変更
デフォルト 安定性 正しさとパフォーマンス
オプトイン 正しさとパフォーマンス 安定性

この提案では、浮動小数点数型のstd::atomicfetch_max()/fetch_min()std::min/std::maxとは異なるセマンティクスを提供することを提案しています。また、既存のAPIとの非一貫性を和らげるために、C23のfminimum/fmaximumおよびfminimum_num/fmaximum_num<cmath>に追加しstd名前空間で利用できるようにすることも提案しています。

P3009R0 Injected class name in the base specifier list

クラステンプレート内での特殊化名を指すクラス名を基底クラスリストでも使用可能にする提案。

クラステンプレート内部において、そのクラスの名前は現在のクラステンプレートの特殊化名を指しています。これは、クラスの内部でのみ使用可能であり、特に基底クラス指定の場所では使用できません。

template<typename D>
class crtp_base {...};

template<typename T, typename U, typename V = int>
class sample
  : crtp_base<sample>       // ng、sampleはテンプレート名
  , crtp_base<sample<T, U>> // ok、これでクラス名、ただしVはデフォルト
  , crtp_base<sample<T, U, V>> // ok、全てのテンプレートパラメータの指定を反映している
{
  // このクラス内部でのsampleの名前は、sample<T, U>としたのと同じになる
  sample* p;  // ok

  void f() {
    const sample& r = *this;  // ok
  }

  // ここでも使用可能
  friend bool operator==(sample, sample) = default; // ok
};

このような名前のことを規格用語ではinjected-class-name(注入されたクラス名)と言います。

基底クラスで注入されたクラス名が使用できないことにより、主にCRTPパターンの記述時にその記述が冗長かつ複雑になります。この例のように短いテンプレートパラメータ名が小数だけならさほど変化はありませんが、標準のコンテナ型のように多様なテンプレートパラメータを取る場合に問題は大きくなります。

また、注入されたクラス名が使用できない場所でそのつもりでクラス名を使用しても、テンプレート名でしかないことから通常エラーになりますが、テンプレートパラメータにデフォルトパラメータが指定されている場合はそのパラメータについての指定を忘れていたとしてもエラーにはなりません。これはともすれば見つけづらいバグの元になる可能性があります。

Deducing thisのおかげでCRTPを記述する必要性は大きく減少していますが、C++20以前の環境でも使用されるコードなどにおいて依然としてCRTPを使用したいケースは残っています。そのため、この提案はクラス定義における基底クラスリスト内部でも注入されたクラス名を使用可能にしようとするものです。

ただし、クラステンプレートの基底クラスリストにおけるそのクラスの名前は、テンプレートテンプレート名としては有効であるため、基底クラスがテンプレートテンプレートに対して部分特殊化していたり、テンプレートテンプレートに対して動作が変わるような記述をしていると、現在有効なコードがコンパイルエラーとなるようになります。

struct WasType {};
struct WasTemplate {};

template <typename Type>
auto foo() -> WasType; // overload 1

template <template <typename...> class Template>
auto foo() -> WasTemplate; // overload 2

template <typename Type>
struct CurrentlyUnambiguousBase
  : decltype(foo<CurrentlyUnambiguousBase>()) // 現在はok、overload 2が選択される、この提案ではエラー
{
  // 現在でもエラー、オーバーロード解決に失敗
  using InsideBody = decltype(foo<CurrentlyUnambiguousBase>());
};

// 現在の動作
static_assert(std::is_base_of_v<WasTemplate, CurrentlyUnambiguousBase<void>>);

さらに巧妙なコードを考えると、エラーにせずに動作を変更することもできます。

struct WasType {};
struct WasTemplate {};

template <typename Type>
auto bar(int) -> WasType; // overload 1

template <template <typename...> class>
auto bar(long) -> WasTemplate; // overload 2

template <typename Type>
struct DifferentBehavior
  : decltype(bar<DifferentBehavior>(0)) // overload 2が選択される、この提案ではoverload 1が選択される
{
  using InsideBody = decltype(bar<DifferentBehavior>(0)); // overload 1が選択される
};

// 現在の動作
static_assert(std::is_base_of_v<WasTemplate, DifferentBehavior<void>>);
static_assert(std::is_same_v<WasType, DifferentBehavior<void>::InsideBody>);

先ほどの例はクラス名が注入されたクラス名としても扱われるようになることで2つの関数の間でオーバーロード解決が失敗していましたが、この例では追加の引数の一致によって順序がつく(0longよりもintによりマッチする)ことによってエラーにならずに選択される関数が変化します。

これらのコードは標準仕様としては曖昧ではなく明確であったとしてもコードの読者にとっては既に曖昧であり、2つ目の例などは1行場所が異なるだけで異なることをしているのはほとんどの読者が気づかないものであるとして、このような例は考慮しないことを提案しています。

EWGのレビューにおいては消極的な推進の合意が得られており、実装経験を求めています。

P3010R0 Using Reflection to Replace a Metalanguage for Generating JS Bindings

P2320の値ベースリフレクションを利用して、Javascriptバインディングを記述した経験の報告書。

この文書では、Bloomberg社内で使用されているROBというC++プリプロセッサ言語フレームワークをP2320で提案されている値ベースリフレクションを用いて書き換え、その際に得られた経験を報告するもので、主に値ベースリフレクションの使用経験や改善案を報告することを目的とするものです。

ROBはBloomberg社内でCで記述されたアプリケーションサーバの処理のために使用されており、.robファイルに書かれたC++クラスに似た記述によるクラス定義をパースしてそこから実行時にJavascriptのクラスにマーシャリングを行うために必要な情報を抽出し、C++のクラス定義や関連するボイラープレートコードとともに生成して.cppと.hファイルに保存します。生成された.cpp/.hファイルをコンパイルすることで、実行時にその型のオブジェクトとJavascriptのクラスオブジェクトの間で相互に変換する処理が利用可能になります。

つまりは、C++コードとしてコンパイルする前に.robファイルをコンパイルするプログラムが必要となります。この文書では、現在使用されているROBをベースにrob2というフレームワークを試作し、そこで値ベースリフレクションによってそのような前処理を純粋なC++コードで記述したものです。

その使用経験をもとに、meta::infoが関数引数で渡した時に定数式の文脈で使用不可能になること(constexpr引数の必要性)やあらゆるリフレクション情報がmeta::infoに畳まれてしまうことによってある時点のmeta::infoオブジェクトが何のどのような情報を保持しているのかわからなくなる点などの使いにくさや、ユーザー定義属性の必要性などについて報告しています。

P3011R0 Supporting document for Hive proposal #1: outreach for evidence of container-style use in industry

std::hive(元plf::colony)の標準への導入のために、コンテナの実使用実体を探るためのアンケートとその結果を記載した報告書。

LEWG/SG14の一部のメンバはstd::hiveのようなコンテナが本当に実使用されている、あるいは必要とされているのかに疑問を持っている人がいるようで、彼らを納得させるためとstd::hiveの標準導入へのサポートのために、C++を使用している企業やコミュニティに向けてアンケートを取りました。この文書は、その結果を報告するものです。

次のようなアンケートを、企業へメール送信、reddit/discordでポスト、その他ゲーム開発系Webコミュニティへポスト、の形でアンケートを募りました

  1. 業務において次のようなタイプのコンテナを使用していますか?
    • 複数のメモリブロックまたは単一のメモリブロック内のシーケンシャルストレージ
    • 要素は削除時に何かしらのマーキングがなされ、マーキングされた要素はイテレーション中にスキップされる
  2. 1がyesの場合、そのコンテナは複数のメモリブロックからなるのか、単一であるか?
    • また、可能であれば何に使用していますか?
  3. plf::colonyもしくは提案中のstd::hiveを知っていますか?
  4. このタイプのコンテナが標準化されることで、メリットがあると思いますか?
  5. その他質問やコメントがあれば

このアンケートに対して、企業メールは8社から、discordは4人、redditは11人、その他Webコミュニティ(TIGsource)は1人の回答が得られました。

1の回答

  • email: 5/7 yes
  • discord: 2/4 yes
  • reddit: 6/11 yes
  • TIGsource: 1/1 yes

2の回答

  • Email: 3 multiple, 2 singular, 1 both.
  • Discord: no responses
  • Reddit: 1 multiple, 3 singular, 1 both.
  • TIGsource: 1 singular.

3の回答

  • email: 4 yes, 3 no.
  • Discord: 省略
  • Reddit: 省略
  • TIGsource: 投稿漏れ

4の回答

  • email: 6 yes, 1 yes but in the future.
  • Discord: 省略?
  • Reddit: 省略?
  • TIGsource: 1 no opinion

得られた回答数はあまり多くないですが、筆者の方(std::hive提案者)の経験や実感と一致しているとのことです。

文書には、得られたコメントなどが詳細に記録されています。

P3012R0 Supporting document for Hive proposal #2: use of std::list in open source codebases

std::hive(元plf::colony)の標準への導入のために、std::listとの比較やstd::listの使用実態を報告する文書。

std::listは要素の挿入・削除に伴って他の要素の移動が発生せず安定しており、それによってポインタやイテレータの安定性を保証しているコンテナです。ただし、std::vectorstd::dequeと異なり、要素はメモリ上でバラバラに配置されており、要素の挿入時は個別のリストノードが割り当てられ、削除時はそのノードごと削除されます。これによって、std::vector/std::dequeに比べてイテレーションと要素追加のコストが高くなります。

std::hiveは両者のいいとこどりをするような性質を持つコンテナで、要素はブロック(大きな配列)内に割り当てられ、削除時は破棄された後でそのブロック内位置が削除済みとしてマーキングされるだけです。これによって、要素はメモリ上でかなり隣接(std::vector程ではないにせよ)することになりイテレーションパフォーマンスが向上し、要素ごとにノードを確保・削除しないため要素の挿入時のコストが低下しコンテナ全体でのメモリ使用量が削減できます。

std::hiveは標準コンテナに準じたインターフェースを持っているため、現在std::listを使用しているところをstd::hiveで置き換えることでそのようなコードのパフォーマンスが向上しメモリ使用量が低下するはずです。

この文書は、std::listとの比較によってその効果を示すとともに、大規模なオープンソースコードベースでどれくらいstd::listが使用されているか(std::hiveによってどれだけパフォーマンス改善が見込めるか)を報告するものです。

10から初めて1000000まで1.1倍しながら5種類のデータ(1, 2, 4, 40, 490 [byte])を挿入・削除・イテレーションした時のパフォーマンスの差は次のようになったようです

  • 挿入(単一要素)のパフォーマンスはstd::listよりも最大520%高速
    • 型のサイズが小さいほど差がついた
  • 消去のパフォーマンスは、最大71%高速
    • 型のサイズが大きいほど差がついた
  • イテレーション(25%の要素を削除した後)のパフォーマンスは、大きな型と大きな要素数(~1000)で最大50%高速
    • CPUのキャッシュに収まりきらなくなると(要素数1000越えくらい)最大218%高速

そのうえで、Githubのトレンド等から合計13件の大規模なオープンソースC++コードベースを調査したところ、13のうち7プロジェクトでstd::listが使用されていました。1件(libreoffice)を除いて要素安定を目的として使用しているかどうかは不明ですが、中にはspliceを使用してリストの結合や移動を行っているものもあったようです。

これらのプロジェクトでは標準にあるからstd::listを使用しているものと思われるため、std::hiveを標準に導入することで切り替えることができるようになり、先に示したようなパフォーマンスの向上が見込めます。

P3014R0 Customizing std::expected's exception

std::expected<T, E>::value()が投げる例外をカスタマイズする方法を提供する提案。

P0260R7で提案されている並行キューではそのAPIとしてstd::filesystemと同様のerror_codeによるAPIを用意することで無例外インターフェースを提供しています。

P2921R0ではerror_codeによるAPIの代わりにstd::expectedを使用したAPIが検討されましたが、それが現在のAPIに比べて明確に優れているとは言えないと報告されています。

// P0260の現在のAPI
void push(const T&);  // 失敗したら例外を投げる
bool push(const T&, error_code& ec);  // 無例外

// std::expectedによるAPI
auto push(const T&) -> expected<void, conqueue_errc>;

無例外APIとの比較

P0260R7 `std::expected`
std::error_code ec;
if (q.push(5, ec)) {
  return;
}
println("got {}", ec);
if (auto result = q.push(5)) {
  return;
} else {
  println("got {}", result.error());
}

例外を投げるAPIとの比較

P0260R7 `std::expected`
q.push(5);
  ...
catch(const conqueue_error& e)
// Awkward use.
q.push(5).or_else([](auto code) {
  throw conqueue_error(code);
});
  ...
catch(const conqueue_error& e)

// Awkward exception type.
q.push(5).value();
...
catch(const bad_expected_access<conqueue_errc>& e)

この提案は、std::expected.value()が投げる例外をカスタマイズできるようにすることで、std::expectedを使用するAPIの使用感を改善するものです。

P0260R7 この提案
q.push(5);
  ...
catch(const conqueue_error& e)
q.push(5).value();
...
catch(const conqueue_error& e)

この提案ではstd::expected_traits<E>というクラステンプレートを追加して、.value()の動作をこれによって変更することを提案しています

namespace std {

  // デフォルト実装、bad_expected_accessを投げる
  template <typename E>
  struct expected_traits {

    [[noreturn]]
    static void throw_error(E e) {
      throw std::bad_expected_access<E>(std::move(e));
    }

  };
}

現在のstd::expected.value()は有効値を保持していない場合にstd::bad_expected_access(error())throwして終了しますが、この提案ではその代わりにstd::expected_traits<E>::throw_error(error())を実行するようにすることを提案しています。

std::expected<T, E>Eについてこのstd::expected_traitsを特殊化し、.throw_error()をカスタマイズしておくことで、エラー型Eによって投げられる例外を変更することができます。

P3015R0 Rebuttal to Additional format specifiers for time_point

P2945R0で提案されている%Sの変更に反対する提案。

P2945R0に関しては以前の記事を参照

P2945R0の提案2では、time_point値に対するフォーマット指定の一つである%S指定の動作を秒数を2桁整数で出力するものに変更しようとしています。現在は秒単位未満の値を全て10進少数として一緒に出力する動作のため、これは破壊的変更となります。

この提案は、既存のコードの実行時の動作を変更するとバージョンアップに伴って既存のコードが実行時エラーやセキュリティリスクに見舞われる可能性があり、C++の標準は過去のバージョンに対して下位互換性を提供するという重要な約束も失われるとして、P2945R0の提案に反対するものです。

ただし、反対しているのは%S指定の動作を変更するというその部分だけで他の提案に関しては反対していません。

またこの提案では、<chrono>ライブラリが精度に中立に設計されていることから現在の%Sの動作はそれと一貫しているとも指摘しています。

提供される値の精度を決めるのはOSで、その値を使用するときの精度を決めるのはクライアントであり、<chrono>は両者の仲介を行うAPIに過ぎ無いため<chrono>ライブラリの中で勝手に精度を変更してしまうような機能はそもそもの<chrono>の設計趣旨に反しています。

%Sの指定はフォーマット時は対象のtime_point型からその精度が提供され、パース時は入力時刻文字列からその精度が提供されるという対称性があります。フォーマット時のみその動作を変えることはこの対称性も破壊することになります。

P2945R0で疑問が提起されていた時・分単位の出力(%H %M)が秒単位を考慮しないのはなぜか?という問いに対しては、時・分・秒という単位はそれぞれ同時に確立されたわけではなく、紀元前1500年ごろからの長い歴史の中で、人類の科学技術の発展に伴って段階的に導入されたものであり、時・分の単位が下位単位の値を気にしないのは歴史的経緯でありライブラリ設計の問題ではない、としています。

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

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

まず、std::valarrayに対してstd::beginstd::cbeginを呼んだ場合の挙動に違いがあります(libstdc++以外)。

#include <iterator>
#include <valarray>

int main() {
  std::valarray<int> v = {1,2,3};
  std::begin(v);  // OK
  std::cbegin(v); // Error
}

std::valarrayは非メンバのstd::beginオーバーロードを備えており、それが呼び出されています。

一方、std::cbeginにはstd::valarray用のオーバーロードが無いためプライマリのstd::cbeginが使用され、そこではADLによってconststd::valarrayに対してbegin()を探索しますが、std::cbegin<iterator>ヘッダで定義されているためそのコンテキストからはstd::valarrayの定義が見つからずにエラーになります。

<valarray>ヘッダが<iterator>をインクルードしているため、ユーザー側でインクルード順を入れ替えてもこの問題は解決しません。

次に、ほぼ同様の問題が初期化子リストに対してもあります。

#include <iterator>

int main() {
  std::begin({1,2,3});  // OK
  std::cbegin({1,2,3}); // Error
}

初期化子リスト({...})そのものには型が付かないため、std::cbeginの呼び出しでconst C&に束縛することができずにエラーになります。しかし、初期化子リストからinitializer_list<E>に変換が可能であるため、std::beginの場合はinitializer_listオーバーロードが選択されることで呼び出し可能になっています(initializer_listには非メンバstd::beginはあるがstd::cbeginはない)。

とはいえ、std::begin({1,2,3})std::end({1,2,3})は背後にある配列が異なる可能性があるため必ずしも範囲を形成せず、単にダングリングイテレータを返しいるため、このコードは意味がないどころか危険です。

この提案はこのstd::beginstd::cbeginに対する一貫しない動作について、std::valarrayの方は機能するように、初期化子リストの方は機能しないように修正するものです。

この提案では、std::valarray及びstd::initializer_listの双方からフリーのstd::begin/std::endオーバーロードを削除し、std::valarrayに対してはメンバのbegin/endconstオーバーロードも含めて追加します。また、std::initializer_listに対してメンバの.data().empty()を追加し、<iterator>ヘッダに定義されているinitializer_listオーバーロードを削除しています。

これらの変更によるコードの動作の変更は次のようになります

現在 この提案
#include <initializer_list>


void f(std::initializer_list<int> il) {
  auto it = std::begin(il);
}

struct S {

  S(std::initializer_list<int> il) :
    S(il.begin(), il.size()) {}

};

auto dangle = std::begin({1,2,3});  // ok
#include <initializer_list>
#include <iterator> // for std::begin

void f(std::initializer_list<int> il) {
  auto it = std::begin(il);
}

struct S {

  S(std::initializer_list<int> il) :
    S(il.data(), il.size()) {}

};

auto dangle = std::begin({1,2,3});  // ng
現在 この提案
#include <valarray>
#include <utility>

std::valarray<int> va;
auto it = std::begin(std::as_const(va));
#include <valarray>
#include <iterator>

std::valarray<int> va;
auto it = std::cbegin(va);

initializer_listにフリー関数のstd::begin/std::endオーバーロードが用意されているのは、当初の範囲for文がフリー関数版のbegin/endしか考慮しなかったためのようです。しかし、C++11の最終仕様ではメンバ関数の方を優先して呼び出すようになったためC++11時点で不要になっていたようです。

P3018R0 Low-Level Integer Arithmetic

低レベルかつ扱いやすい整数演算ライブラリ機能の提案

C++組み込みの整数演算はC言語から受けつがれたもので非常に長い歴史がありほとんど変更されることなく今日に至っています。C言語誕生当時やC++誕生当時の整数演算の要件は現在とは大きく異なっており、さまざまな実装における整数演算および整数の表現について共通の抽象化を見出すことにが主な課題でした。

一方、現在の整数演算の環境はかなり均質化されており、C++20でもそれを反映して符号付き整数型の表現が2の補数であることが規定されました。ただし、これを反映して組み込みの整数演算のセマンティクスを変更するの下位互換性の問題から困難です。

最も問題なのは、C++言語内で整数演算を実行する方法がそのような組み込みの整数演算しかないことです。これによって、基底のハードウェアによって提供される整数演算と一致するセマンティクスによって整数演算を行う必要のあるライブラリ作成が困難になっています。

例えば、ほぼ全ての整数演算がオーバーフロー時に未定義動作となるため、C++でオーバーフロー対応整数演算を記述する方が同等のコードをアセンブラで直接記述するよりも複雑になリます。その結果得られるC++コードは人間にとってもコンパイラにとっても理解するのが難しいコードになります。

#include <limits>

struct Result {
  int64_t sum;
  bool overflow;
};

Result safe_add(int64_t a, int64_t b) {
  using lim = std::numeric_limits<int64_t>;

  const auto max = lim::max();
  const auto min = lim::min();
  
  bool overflow = false;

  if (a >= 0) {
    if (max - a < b) {
      overflow = true;
    }
  } else {
    if (b < min - a) {
      overflow = true;
    }
  }

  return overflow ?
    Result{ .sum = 0, .overflow = true } :
    Result{ .sum = a+b, .overflow = false };
}

例えばこのコードのコンパイル結果(x86-64)は次のようになります

safe_add(long, long):
  test rdi, rdi
  js .L2
  movabs rax, 9223372036854775807
  sub rax, rdi
  cmp rax, rsi
  jl .L6
.L4:
  xor ecx, ecx
  lea rax, [rdi+rsi]
  movzx edx, cl
  ret
.L2:
  movabs rax, -9223372036854775808
  sub rax, rdi
  cmp rax, rsi
  jle .L4
.L6:
  mov ecx, 1
  xor eax, eax
  movzx edx, cl
  ret

整数加算はハードウェア(x86-64に限らず)でハンドリングされ、オーバーフローチェックも後から簡単に行うことができます。整数演算のオーバーフローは現代の多くのハードウェアでは未定義動作ではなく予期される現象であり、ハードウェアはそれをハンドリングすることができます。しかしC++の整数演算セマンティクスはそれを反映し活用することができません。また、現在C++コンパイラはこの例のようなC++コード上での整数演算のオーバーフローチェックパターンを認識することができず、それをハードウェアの命令に直接的にマップすることができません。

この提案は、現在の整数演算を取り巻く環境を反映した整数演算のセマンティクスを持つ整数演算APIをライブラリ機能として追加することで、整数演算に大きく依存する処理やライブラリの記述のための基礎部品として提供することを目指す物です。特に、提案する整数演算機能は未定義動作を起こしません。

提案するAPIを用いると先ほどの例は次のように書き直されます

#include <integer>

struct Result {
  int sum;
  bool overflow;
};

Result safe_add(int64_t a, int64_t b) {
  auto const [sum, overflow] = std::integer_i64_add_overflow(a, b);

  return Result{ .sum = sum, .overflow = overflow };
}

そしてこのコンパイル結果(x86-64)は例えば次のようになります

safe_add(long, long):
  mov rax, rdi
  mov edx, 0
  add rax, rsi
  seto dl
  ret

この提案のAPIはハードウェアが持つ整数演算の能力を直接的に活用することができ、その結果はほぼハードウェアの持つ整数演算命令と対応します。

この提案では、Rust/Swiftの持つ同様のライブラリAPIおよびC++コンパイラの組み込み関数を参考にし、さらにx86-64/ARM64/RISC-Vの対応する命令の振る舞いについても検討しています。それによって必要と思われるAPI機能を特定し、またそれらに優先順位をつけてリストアップしています。

  1. 基礎的な操作であり、全てのハードウェアでサポートされているため実装が簡単なもの
    • add_overflow
    • sub_overflow
    • mul_overflow
  2. 実装が簡単だがそれほど有用ではない可能性があるもの
    • div_overflow
    • rem_overflow
    • neg_overflow
  3. 一部のユースケースで有用だが、実装が難しく実装経験やフィードバックが必要なもの
    • mul_wide
    • div_wide
    • div_rem_overflow
    • add_with_carry
    • sub_with_borrow
    • mul_with_carry

これらの名前をベースとして、この提案では関数テンプレート(どの整数型でその操作が使用可能かが分かりづらい)やオーバーロードオーバーロード解決ルールを回避して実装するのが難しい)を避けて、integer_プリフィックスとして整数型に対応する略称(i8_, u8_, i16_, u16_, i32_, u32_, i64_, u64_のいずれか)を付加した個別の関数とすることを提案しています。

例えば

  • std::integer_i64_add_overflow()
  • std::integer_i8_neg_overflow()
  • std::integer_u32_mul_with_carry()

などになります。

これらの関数の戻り値型はその関数名に_resultを付加した集成体型となり、計算結果の値と操作ごとの追加情報をまとめて返します。

例えば次のようになります

// <integer>で定義
namespace std {

  struct integer_i32_add_overflow_result {
    int32_t sum;
    bool does_overflow;
  };

  integer_i32_add_overflow_result integer_i32_add_overflow(int32_t a, int32_t b);
}

提案する整数演算ライブラリは例えば、安全な整数演算ライブラリや標準のものよりも大きな幅の整数型ライブラリなどの実装の基盤として使用することができ、また、将来的にそのような機能が標準ライブラリに導入されるときの基盤となることができます。前述のように、このようなライブラリを現在のC++環境で書こうとすると純粋なC++コードでは困難となるため、環境ごとの組み込み関数やアセンブラを直接記述することになります。

SG6のレビューではこの提案の目指す方向性については全会一致で合意されており、先んじてC++26に導入されている類似機能である飽和演算ライブラリ機能(P0543)とAPIを一貫させた上でLEWGに提出することが決定されています。

P3019R0 Vocabulary Types for Composite Class Design

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

クラスの実装の一部がそのクラスの内側にない場合(つまりヒープメモリ等他の場所に配置されているオブジェクトを保持する場合)、多くの場合ポインタによって間接的に保持することになります。スマートポインタを使用すればその際の領域解放忘れを防ぐことができますが、それを保持するクラスに対して幾つかの問題をもたらします。

  • 特殊メンバ関数の実装伝播の阻害
  • 深いコピーの実装
    • デフォルトのコピーは禁止か浅いコピーになるため、コピーコンストラクタの実装などを通して手動で実装する必要がある
  • const伝播の阻害
    • ポインタのconst性はその参照先のconst性とは無関係
    • クラスのメンバ関数const性がポインタの先まで伝播しない

すなわち、ポインタはスマートポインタも含めて参照セマンティクスを持つため、クラスのメンバとして保持する場合に使いづらい側面があります。

この提案は、ポインタに代わってヒープ領域上に構築されたオブジェクトを扱うための値のセマンティクスを持つ2つのクラス型、indirect<T>polymorphic<T>を提案するものです。

indirect<T>はヒープ領域上のTのオブジェクトを表現するクラス型で、polymorphic<T>はヒープ領域上のTの派生クラス型オブジェクトを表現するためのクラス型です。どちらも次のような性質を持ちます

  • 特殊メンバ関数Tのものになるべく従う
    • デフォルトコンストラクタ : Tがデフォルト構築可能なら利用可能
    • コピーコンストラクタ/代入演算子
      • indirect<T> : Tがコピー可能なら利用可能
      • polymorphic<T> : 無条件で利用可能
    • ムーブコンストラクタ/代入演算子 : 無条件で利用可能
    • デストラクタ : 保持するオブジェクトを破棄する
  • 値のセマンティクスを持つ
    • コピー可能な場合、コピーはディープコピーとなる
      • 新しい領域を確保し、その領域に元のオブジェクトをコピーする
    • constメンバ関数const Tオブジェクトにアクセスする
    • indirect<T>は比較演算子ハッシュ関数Tで利用可能なら利用可能
  • 動的に確保した領域の所有権を保有する
    • 標準コンテナ同様のアロケータインターフェースによって確保戦略のカスタマイズが可能

宣言の例

namespace std {

  template <class T, class Allocator = std::allocator<T>>
  class indirect {
    T* p_; // exposition only
    Allocator allocator_;  // exposition only
  public:
    ...
  };

  template <class T, class Allocator = std::allocator<T>>
  class polymorphic {
    control_block* control_block_; // exposition only
    Allocator allocator_; // exposition only
  public:
    ...
  };
}

polymorphic<T>の場合はコンストラクタでTの派生オブジェクトを受け取って、型消去機構を通して保持します。

どちらのクラス型もムーブ後にnullに相当する状態になります。これはどちらもムーブの実装を簡易かつ効率的にするためです。ただし、この状態をユーザーが通常観測することは意図していないためoperator boolのようなものは提供されておらず、valueless_after_move()という専用のメンバ関数でそれをチェックできるようにしています。また、この性質により、両クラス型に対してstd::optionalの特殊化が提供され、余分な領域を使用しないようにしています。

これらのクラス型はそれぞれ以前に個別の提案(P1950P0201)に分かれて提案されていましたが、このようにその設計が非常に似通っているためこの提案でまとめて提案されることになりました。

P3020R0 2023-09 Library Evolution Poll Outcomes

2023年9月に行われたLEWGの投票の結果を報告する文書

投票にかけられた提案は次のものです

全てC++26に向けてLWGに転送されています。文書では、投票の際に寄せられたコメントが記載されています。

P3021R0 Unified function call syntax (UFCS)

メンバ関数呼び出しを拡張するUFCSの提案。

C++には大きく分けるとメンバ関数呼び出し(x.f(), x->f())と非メンバ関数呼び出し(f(x))の2つの形の関数呼び出し構文があります。これら2つの間に互換性はなく、どちらかの呼び出しがもう片方にフォールバックすることはありません。そのため、コードを書く際に呼び出したい関数がメンバ関数なのかそうでないのかを認識しながら書かなければなりません。

それによって、ジェネリックコードで関数呼び出しが必要となる場合に何かしらの分岐が必要となり、現在のC++では真にジェネリックなコードを記述することができなくなっています。

この問題は以前から認識されており、解決のために一様関数呼び出し構文(Unified function call syntax : UFCS)の提案がいくつも提出されてきました。提案された機能は非メンバ関数呼び出しを拡張してメンバ関数を呼び出せるようにするものでした。

この提案はそれら既存の提案とは逆方向に、メンバ関数呼び出し構文(x.f(a,b)x->f(a, b))を一般化して、非メンバ関数の呼び出し(f(x, a, b)f(*x, a, b))にフォールバックすることを提案するものです。

この提案は現在認識されている次のような問題を解決します

  1. ジェネリックコードにおける呼び出し構文の汎用化を達成する
    • 単一の構文によってメンバ/非メンバ関数を呼び出すことができる
  2. ライブラリの回避策やそれにしか使用できない言語機能を用意することなくメソッドチェーンを可能にする
    • <ranges>|演算子やそれを改善することを目指す|>等は必要なく、.によってチェーンできる
  3. それにしか使用できない言語機能を用意することなく拡張メソッドを可能にする
    • この提案の.拡張は自然に拡張メソッドを可能にする
    • 既存のコードに手を入れることなく有効化され、この提案の機能は拡張メソッドのためだけのものではない(むしろその効果はおまけ)

さらに次のような恩恵があります。

  1. 一貫性・単純さ・教えやすさの向上
    • 関数呼び出し構文が1つになるため言語がシンプルになり教えやすさが向上する
    • メンバ関数呼び出しも引き続き有効だが次善の機能となる(引数なしの非メンバ関数呼び出しを除く)
  2. 既存のコードの発見しやすさと使いやすさを向上させる
    • この提案は既存のC++ライブラリコードを呼び出すとすぐに(何の変更も必要とせず)機能する
    • C標準ライブラリを含むCライブラリについても同様
    • これによって、既存のライブラリを学習して使用するための強力な方法となる
  3. ツールサポートの改善
    • C++での作業をより便利にする強力な新しいツール機能の作成を直接支援する
    • 既存のツール、特にコードのオートコンプリート機能(VSのIntelliSenseなど)を強化する

2に関しては例えば、Cのライブラリ機能を使用する既存のコードを次のように改善します

// 現在のC code
void f( FILE* file ) {
  fseek(file, 9, SEEK_SET); // 引数は関数名の後にあるため、コード補完は機能しない
}

// 提案によるC++コード
void f( FILE* file ) {
  file.fseek(9, SEEK_SET); // .を入力するとコード補完が候補を表示する
}

この時、エディタのオートコンプリート機能が効いていれば、使用したいライブラリの関数名を知らない/覚えていない状態からでもオートコンプリート機能がサジェストする候補から使用する関数を選択し、また他に使用できる機能を探すことができます。C++のコードにおいてはそれは現在メンバ関数に限定されており、Cのコードに対しては機能していません。

このような例はC++にも存在しており、一番目立つのは配列からのイテレータ取得時でしょう。

template<typename T, std::size_t N>
void f(T(&array)[N]) {
  using namespace std::ranges;
  // 現在
  auto it = begin(array);

  // この提案
  auto it = array.begin();
}

メソッドチェーンはその記述と処理の流れが一致することで視覚的に見やすくなり、また上記のようなコード補完による恩恵を受けやすいなどのメリットからC++言語内だけではなく他言語でも好まれる傾向にあります。

first().second().third().fourth();  // 評価と実行の順序と一致する
fourth(third(second(first())));     // 評価と実行順序と逆になる

<ranges>ではこれを解決するために|演算子オーバーロードして使用しています。これはこの提案の.演算子オーバーロードによってシミュレートしているものであるため、この提案の.によって置き換えることで同様に機能します。

// 現在
std::ranges::for_each( in | std::views::transform(rot13), show );
std::ranges::for_each( in | transform(rot13), show ); // with ‘using std::views’

// この提案
std::ranges::for_each( in.std::views::transform(rot13), show );
std::ranges::for_each( in.transform(rot13), show );   // with ‘using std::views’

また、この|演算子オーバーロードの複雑性解消のための言語サポートを行うためのUFCSとして、|>演算子の提案(P2672R0)が以前提出されていました。

using namespace std::ranges;

// HTMLタグの中身を抽出する
auto filter_out_html_tags(std::string_view sv) -> std::string {
  return sv
    |> transform(%, [](char c){ return c == '<' or c == '>'; })
    |> zip_transform(std::logical_or{}, %, % |> rv::scan_left(%, std::not_equal_to{}))
    |> zip(%, sv)
    |> filter(%, [](auto t){ return not std::get<0>(t); })
    |> values(%)
    |> to<std::string>(%);
}

これはプレースホルダがあるため少し手直しが必要ですが、ほぼそのままこの提案で書き直せます。

auto filter_out_html_tags(std::string_view sv) -> std::string {
  auto call = []<typename T, typename F>(T&& o, F&& f) {
    return std::forward<F>(f)(std::forward<T>(o));
  };

  return sv
    .transform([](char c){ return c == '<' or c == '>'; })
    .call([](auto const& x) { return zip_transform(std::logical_or{}, x, x.scan_left(true, std::not_equal_to{})); })
    .zip(sv)
    .filter([](auto t){ return not std::get<0>(t); })
    .values()
    .to<std::string>();
}

この提案の機能はここまで説明しているように|演算子オーバーロードを置き換えるためだけのものではなくより一般的なものであり、.でチェーンされていることによって視覚的にも優れています。

この提案の内容はcppfront(筆者の方のプロジェクト)で実装されているようです。

P3022R0 A Boring Thread Attributes Interface

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

OSの提供するスレッド作成APIではいくつもの追加のオプションがあり(例えばpthreadの場合は12以上)、現在のC++スレッドAPIはそのほとんど全てを指定することができません。そのため、将来的に指定可能にしたいスレッドのオプションはスタックサイズとスレッド名以外にも多数に及ぶ可能性があり、将来のバージョンで少しづつ増やしていった場合に関数インターフェースを複雑にせずになおかつABI破壊を避けるようにAPIを設計しなければなりません。

現在のところ、同様の機能はP2019で提案されています。

P2019R3では、ファクトリ関数によってスタックサイズとスレッド名を指定できるようにするAPIを提案しています。

void f(int);

int main() {
  // P2019R3のスレッドオプション指定例
  auto thread = std::jthread::make_with_attributes(
    []{ f(3); },
    std::thread_name("Worker"),
    std::thread_stack_size(512 * 1024)
  );
}

これに対して、この提案は20年以上に渡って実使用されてきたBoost.Threadの設計をベースとしたAPIを提案するものです。

void f(int);

int main() {
  // この提案のスレッドオプション指定例
  std::jthread::attributes attrs;
  attrs.set_name("Worker");
  attrs.set_stack_size_hint(512 * 1024);
  
  auto thread = std::jthread(attrs, f, 3);
}

このAPIに比べるとP2019R3のAPIには次のような使いづらい点があります

  • コンストラクタの代わりにファクトリ関数が使用される
  • スレッドの追加引数を取れないため、引数を渡す場合はラムダでラップする必要がある
  • スレッドオプションは型によってディスパッチされる

このAPIはBoost.Threadの設計をベースにしており、Boost.Threadが20年前に登場して以降に現れた多くのスレッドライブラリにおいても同様のAPIによってスレッドオプションを指定するようになっています。そのため、多くのプログラマはこの設計に慣れており、P2019R3の込み入ったAPIと比べてシンプルで使いやすいと主張しています。

std::jthreadにおける変更は次のようになります

namespace std {

  class jthread {
  public:
    // オプションを保持する内部クラス型を追加
    class attributes;
    
    // デフォルトコンストラクタ
    jthread() noexcept;

    // 追加 : オプション指定のみを受け取るコンストラクタ
    jthread(attributes&& attrs) noexcept;

    // 実行する関数とその引数を受け取るコンストラクタ
    template<class F, class... Args>
    explicit jthread(F&& f, Args&&... args);

    // 追加 : 実行する関数とその引数に加えてオプション指定を受け取るコンストラクタ
    template<class F, class... Args>
    explicit jthread(attributes&& attrs, F&& f, Args&&... args);

    ...
  
    // オプションを取得する関数を追加
    [[nodiscard]] std::string_view get_name() const noexcept;
    [[nodiscard]] std::size get_stack_size() const noexcept;

    ...
  };

  // オプションを保持するクラス型                
  class jthread::attributes {
    attributes() noexcept;                                
    ~attributes() = default;

  public:
    // set thread name
    void set_name(const char* name) noexcept;
    void set_name(std::string_view name) noexcept;
    void set_name(std::string&& name) noexcept;
    // set thread stack size hint
    void set_stack_size_hint(std::size_t size) noexcept;
  };
}

将来的にスレッドオプションを増やしたくなった場合は、同様のオプションクラス(jthread::attributes2など)とそれを受け取るコンストラクタを追加することで、ABIの破壊を回避しながら機能拡張ができます。

また、この提案ではstd::jthreadへの移行を促すためにもこのAPIstd::threadに対しては追加しないようにしています。

2023年11月のKonaで行われたLEWGのレビュー投票においては、この提案のAPIよりもP2019のAPIの方が好まれたため、この提案の追求は停止したようです。

P3023R0 C++ Should Be C++

C++標準化委員会の目標・見通しについて問い直す文章。

昨今のC++を取り巻く状況や個人個人の標準化参画意図などから独立して、標準化委員会が目指すべき方向性や委員会に参加する際に持つべき心得みたいなものについて記述されています。

これは未完成版であり、R1が既に出ているのでそちらを見た方が良いでしょう。

おわり

この記事のMarkdownソース

[C++]契約プログラミング機能のこれまでとこれから

この記事はC++アドベントカレンダー2023 25日目の記事です。ご参加の皆様お疲れ様でした!

C++ Contracts

ContractとはContract programmingの略称で、C++ ContractsとはC++における契約プログラミング機能を指す言葉です(sは付いたり付かなかったりします)。

C++ Contractsとは、契約プログラミングという考え方に基づいた設計(契約による設計)をより自然に行えるようにするための言語機能です。契約プログラミングについてはあまり深入りしないので各自ググってください。

契約による設計の中心概念には、関数の事前条件・事後条件とクラスの不変条件という3つのものがあります。C++ Contractsがまず導入しようとしているのは、前者の関数の事前条件・事後条件をコードとして記述しそれを実行時にチェックできるようにする機能です。

C++ Contractsは単に契約プログラミングの実現のためだけのものという訳ではなく、それを含めてさまざまなユースケースが想定されています。例えば

  • 契約プログラミング(契約による設計)を言語機能によって実現する
  • 現在ドキュメントあるいはコメントに記載するしかない関数に対する契約の一部(事前条件・事後条件)をコードで記述する
    • そして、実行時にそれを検査する
  • より高機能な実行時検査のための言語機能
    • 特に、関数呼び出しの境界においての実行時検査方法の提供
    • デバッグ時はもちろんとして、リリースバイナリにおいても有用な実行時検査方法の提供
  • 実行時検査の違反時のより効果的なハンドリング
    • 実行時のプログラム状態チェックが失敗した場合に、より高度なログ出力や安全なシャットダウンなどを可能にする
  • ユニットテストのための基盤機能としての利用
  • ツールに対するプログラマの知識の伝達手段
    • IDE(入力支援など)が契約注釈に基づいたコード補完やサジェストを行う
    • 静的解析ツールが契約注釈を読み取り解析に役立てる
    • サニタイザーが契約注釈を読み取り実行時検査に使用する
    • 形式検証を可能にするようなアノテーション拡張(将来?)

などです。もちろん、ここに記載されていないものや、実際に利用可能になってから開けるユースケースもあるでしょう。いずれにせよ、C++ ContractsはC++コードをさらに安全に記述する事に確実に貢献する機能です。

これらのユースケースの実現には単に事前条件・事後条件を記述できるようにするだけでは足りず、その周辺の諸機能が必要となることが想像できると思います。それらを含めたC++ Contractsは単純な言語機能ではなく、実装され実使用される場合の事を考慮したうえでC++の機能として相応しく設計されなければなりません。そのため、その議論はかなり難しく時間を要するものになっています。

C++20 Contracts

C++ ContractsはC++20に対してP0542で提案され、2018年6月に一旦C++20ドラフトにマージされました。

この提案自体もどうやら2013年頃から継続された議論の一つの集大成であったようです。あまり踏み込まないので詳しくは当時書かれた別の記事をご覧ください

cpprefjpのサンプルコードを拝借すると、構文的には次のようなものでした

#include <iostream>
#include <cmath>

double sqrt_checked(double x)
  [[expects: x > 0]]   // 引数に対する事前条件
  [[ensures r: r > 0]] // 戻り値に対する事後条件
{
  return std::sqrt(x);
}

int main()
{
  double x;
  std::cin >> x;

  [[assert: x > 0]]; // アサーション

  double y = sqrt_checked(x);

  std::cout << y << '\n';
}

事前・事後条件および中間のアサーションのためにはこのような属性様構文によって注釈を行い、これがチェックされるかどうかはビルドレベルによって制御します。

ビルドレベルは全ての契約条件がチェックされるモード、一部だけがチェックされるモード、全てがチェックされないモードの3つがありコンパイルオプションで指定します。

契約条件がチェックされている場合に契約条件が破られる(falseとなる)と、違反ハンドラと呼ばれる関数が呼ばれてプログラムは終了します。違反ハンドラはユーザーがカスタマイズできるものではなくその内容については実装定義でした。また、契約が破られた場合に実行を継続するモードも用意されており、これもコンパイルオプションで指定します。

基本的な機能は抑えておりこれでも実際に使用できるようになれば有用だったのでしょうが、色々議論が紛糾した結果、2019年7月の全体会議においてC++20から削除することが決定され削除されてしまいました。

問題点とMVP(Minimum Viable Product)

C++20 Contractsは一旦はドラフト入りしたものの、1年後の2019年7月に削除することが合意されC++20から削除されてしまいました。

直接の提案(P1823R0)や標準化会議参加者のレポ等を要約すると、ドラフトへのマージ後に設計変更が相次ぎ、しかもそれに統一的な合意を得ることができず、実装経験不足を含めた問題が提起され、それによって現在の設計が出荷に値するものではないことが分かったため、のようです。

例えば、2019年に提出されたContracts関連の提案はリビジョン改定含めて47本に及び(2019年の提案一覧をcontractというワードでページ検索)、そのほぼ全てが2019年7月ごろまでの間に提出されています。なお、前年は19本、翌年は8本でした。

なんとなく納得はできますがあまり具体的ではありません。具体的に何が問題だったのでしょうか?47本の提案のタイトルを眺めていると議論が紛糾している様はわかりますが個別に見ていくのは辛いものがあります。幸いなことに、後に争点をまとめた文書があります。

これによると

  • 契約違反後の継続
    • 契約違反後に継続できるべきかどうか
    • 継続できるようにするとして、それはグローバルなスイッチ(コンパイラオプション)とローカルなスイッチ(個別の契約条件ごとの指定)のどちらによって制御されるべきか?
  • ビルドレベル
    • 全ての契約注釈のセマンティクスはビルドレベルによって一律に制御されるが、それは可能なのか?
    • また、ある契約注釈がチェックされるかどうかを決定するのがグローバルなビルドレベルだけであるべきか?
    • よりきめ細かい制御の方法を望む向きがあり、それらを並立させた場合にデフォルトはなんであるべきか、あるいは契約注釈のセマンティクスを制御できる方法が複数提供されないほうが良いか?
  • グローバルスイッチの存在
    • 全ての契約注釈のセマンティクス及び有効/無効は全てグローバル制御になっているが、ローカルな制御を望む向きもあった
    • グローバルな契約機能の有効化/無効化は全ての契約注釈に影響を与えるのか、一部の注釈だけがそのグローバル設定を上書きできるのか
    • どのレベルで契約注釈の制御が行われるか、あるいはどのレベルでそれを制御できるかはContractsがどのように使用されるかに大きな影響を与えうる
  • ソース内コントロール
    • それぞれの契約注釈ごとにそのセマンティクスを設定し、それがグローバル設定の影響を受けないようにする提案があった
    • そのようなアイデアは十分に成熟しているか?
    • 契約注釈のセマンティクスの制御をどの程度細かく行うべきか?
  • コードの仮定に使用すること
    • チェックされていない契約条件をコードの仮定とすることが許可されていたが、これが当初の設計意図に合致しているかどうか?
      • すなわち、契約注釈が[[assume(expr)]]の機能を含むことは妥当なのか?
    • チェックされない契約条件をコードの仮定とすることが合理的かどうか、またその制御とデフォルトはどうあるべきか?

これらの(相互に絡み合う)問題について合意を得ることができず、またその見通しも立たなかったためにC++20 Contractsは一旦取り下げられたわけです。最後のものはチェックされないはずの契約が破られた場合にプログラムが未定義動作に陥るため、特に紛糾したポイントだったようです。残りの上4つはよく似た事を言っており、これらの問題はざっと2つにまとめられます

  1. C++ Contractsの単純さと応用可能性の問題
  2. C++ Contractsの安全性の問題

この2つの問題はどちらかが相対的に影響が小さいものではなく、それぞれ単独でC++20のContractsを行き詰まらせるだけの影響があったようです。

とはいえこのような議論の紛糾はContractsそのものに対する反対ではなくその設計に反対するもので、Contractsそのものの有用性は認められていたため、Contractsの再設計を行う専門のStudy Group(SG21)を設立し、将来機能として引き続き議論していくことになりました。

SG21では上記のP2076も含めてC++ Contractsの機能や設計についての再調査を行い、C++23に向けたC++ Contractsとして上記のような紛糾した点の影響を受けない最小の範囲の機能をC++ ContractsのMVP(Minimum Viable Product)としてまとめ、まずこのMVPを最初のC++ Contractsとして導入することを目指すことにしました。上記の紛糾ポイントを含めた意見の相違がある機能や設計については時間をかけて議論してた上で将来的に導入していくことを目指します。

このMVPの考え方と方針は現在のC++ Contractsに関連する作業や議論の基礎となっています。

ただ、P2182R1の部分でさえもSG21での合意に至ることはできなかったようで、さらに機能を絞り込んだものが最初のMVPとして確立されました。

P2388による最初のMVPは主に次のようなものです

  1. 事前条件 : [[pre: condition]]
  2. 事後条件 : [[post: condition]]
  3. アサーション : [[assert: condition]]
  4. 2つのビルドモード
    • No eval : コンパイラは契約の式の有効性だけを検証し、実行時に影響を与えない
    • Eval and abort : 全ての契約を実行時にチェックする。違反した場合契約違反ハンドラが呼び出され、その後std::abort()する
  5. カスタマイズ不可能でstd::abort()を呼ぶだけの違反ハンドラ

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

int select(int i, int j)   // 最初の宣言
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]];
  
int select(int i, int j);  // 再宣言では、同じ契約を指定するか、何も指定しない
                          
int select(int i, int j)   // 定義
{
  [[assert: _state >= 0]];
  
  if (_state == 0) {
    return i;
  } else {
    return j;
  }
} 

ここではビルドモードは契約条件をチェックするかしないかの2つしかなく(継続モードはなく)、それはビルドモードによってグローバルにのみ制御可能で、チェックされない契約条件をプログラムの仮定として最適化に利用することは明示的に禁止されています。この段階では前述の争点を何も解決していませんが、後からそれを拡張することを意図しており、それが可能なようになっています。

逆にいうと、C++20のContracts仕様からこのP2388によるMVPを引いた部分が議論が紛糾したポイントであるといえます。P2388は本当に最小のもので、3種類の契約注釈の指定方法とそれがチェックされるかされないかのグローバルフラグのみからなります。

最初のMVP仕様

P2388がMVPとしての地位を確立した後、P2521で改めてMVPの仕様がまとめられました。

このMVPの仕様においては契約注釈の構文は未決定なためプレースホルダの構文を使用しています。ただし、この記事では以降属性構文を基本として使用することにします。

このMVP仕様に対しては、後述のロードマップ策定までの間にいくつかの未解決の問題の解決が図られています。その中でも大き目なものをいくつかピックアップします。

関数の再宣言と契約注釈

C++における関数宣言は定義が一つだけである必要があるものの、単なる宣言は何度でも行うことができます。その際問題となるのは、宣言ごとに契約注釈が異なっている場合にどの契約注釈が最終的なその関数に対する契約注釈となるのか?という事です。

/// header.hpp

int select(int i, int j)  // 最初の宣言
  [[pre: i >= 0]];

/// implemetation.cpp

int select(int i, int j)  // 再宣言であり定義
  [[pre: j >= 0]]
  [[post r: r >= 0]]; 

変な例を考えれば、いくつも再宣言を行いそれぞれに異なる契約注釈を与えるような例なども容易に想像ができます。

MVP仕様においては、契約注釈は関数の最初の宣言にのみ行うことができ、再宣言に対してなされている場合はill-formed(コンパイルエラー)とされています。

/// header.hpp

int select(int i, int j)    // ok、最初の宣言
  [[pre: i >= 0]];


int select2(int i, int j);  // ok、最初の宣言

/// implemetation.cpp

int select(int i, int j)  // ng、再宣言に契約注釈を行えない
  [[pre: j >= 0]]
  [[post r: r >= 0]];

int select2(int i, int j)   // ng、再宣言に契約注釈を行えない
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]];

正しい記述は次のようになります

/// header.hpp

int select(int i, int j)  // ok、最初の宣言
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]];

/// implemetation.cpp

int select(int i, int j); // ok、再宣言であり定義

これは、ヘッダファイルに関数の宣言だけを記述し別のソースファイルでその定義を提供するプログラムやライブラリでの使用時に最も自然な形になります。すなわち、関数契約をコード化した契約注釈は常に利用者側に公開されなければならないということを反映しています。

引数の事後条件からの参照

事後条件においては関数の戻り値だけではなく関数引数を参照することができます。その際、関数引数のどの時点の状態を取得するかによって条件の意味が変わってしまう場合があります。

// ユーザーが見る宣言
int generate(int lo, int hi)
  [[pre lo <= hi]]
  [[post r: lo <= r && r <= hi]];

// 開発者が見る定義
int generate(int lo, int hi) {
  int result = lo;
  while (++lo <= hi) // loが更新される
  {
    if (further())
      ++result;      // loよりもゆっくりとインクリメントされる
  }
  return result;
}

このgenerate()関数では、事後条件としてlo <= rrは戻り値)であることが表示されています。しかし、その定義を見てみるとloは関数中でその値が変更されています。このため、確かにgenerate()の戻り値の値はgenerate()を呼び出した時点のlo以上になるのですが、一方で関数終了時点のloよりは小さくなります。

このように、関数引数がその実装内部で変更できることによってユーザーが見る条件と実装者が見る条件の解釈が異なる場合があり得てしまいます。

また、暗黙ムーブによって事後条件がムーブ後オブジェクトを参照する可能性もあります。

// ユーザーが見る宣言
string forward(string str)
  [[post r: r == str]];

// 開発者が見る定義
string forward(string str) {
  // ...
  return str; // 暗黙ムーブが行われる
}             // UB、事後条件はムーブ後オブジェクトを読み取る

契約注釈は実装者からユーザーに向けての関数契約をコードであらわしたものであり、それを読み取るのはユーザーです。ユーザーは関数宣言の契約注釈を見た場合、そこで使用されている仮引数名の値は自分が渡した値に一致すると思って見るのが一般的な見方だと思われます。そしておそらく、契約注釈を読み取るツールも同様の見方をするでしょう。そのため契約注釈の条件の意味はユーザー目線で自然であることがあるべき姿です。

また、これらの問題は関数の引数が非参照(参照型変数は変更されうることが明確かつ暗黙ムーブ対象外)かつ非constconst引数であれば変更されないことが明確)の場合にのみ問題となります。そのため、MVPでは事後条件で関数引数を使用する場合、その引数が参照型であるかconstでなければならず、そうでない場合はill-formed(コンパイルエラー)としています。

// これはダメ
int generate(int lo, int hi)
  [[pre lo <= hi]]
  [[post r: lo <= r && r <= hi]]; // ng、loもhiもconstでも参照でもない

// こう書く
int generate(const int lo, const int hi)
  [[pre lo <= hi]]
  [[post r: lo <= r && r <= hi]]; // ok、loもhiもconst

契約条件式の副作用

C++のほとんどの式はあらゆる副作用を含む可能性があり、それは契約注釈の条件式も例外ではありません。また、関数が副作用を含んでいるかどうかを判定することは困難であるため、副作用を含む関数の排除を徹底することもできません。

C++ Contractsは安全なコードの記述を促進するための機能であるため、契約条件式からの副作用というものは望ましいものではありません。そのため、当初の論調では契約注釈内の副作用は禁止する雰囲気でしたが、それは困難なため何とか制限しようと試みられていました。

MVP仕様では、契約条件式の副作用を禁止していません。ただし、契約条件式はビルドモード等によって0回以上評価される可能性がある(評価回数が不定)とすることで、プログラマが契約条件式の副作用に依存することを回避しようとしています。

C++26に向けたロードマップ

MVPに対する改善や設計検討はC++23サイクル中も行われていましたが、あまり活発ではありませんでした。そのためMVPの標準への導入はC++23には間に合いませんでした。

その後C++23サイクルの終盤の2022年11月、SG21よりC++26に向けたContracts MVP仕様の標準への導入を目指すロードマップが示されました。

これによると、次のようなスケジュールになっていました

タイムライン マイルストーン やるべきこと
2022年11月(Kona) 契約注釈の副作用に関する議論
2023年2月(Issaquah) C++26サイクルの開始 契約注釈の副作用に関する設計の決定
2023年6月(Varna) 違反ハンドラに関する設計の決定
2023年11月(Kona) 契約構文に関する設計の決定
2024年3月(Tokyo) 残った問題への対処。MVPを完成させEWGおよびLEWGへ転送する
2024年6月(St. Louis) EWGにおけるMVPのレビュー
2024年11月? C++26のための最後のEWGレビュー MVPをEWGでの合意の下でCWGへ転送する
2025年2月? C++26のための最後のCWGレビュー CWGにおけるレビューを完了し、C++26WPヘマージ
2025年6月? C++26 CD完了 未解決の設計/文言の問題解決
2025年11月? NBコメントへの対応
2026年2月? C++26 DIS完了 NBコメントへの対応

このロードマップはSG21で合意され(僅かながら反対票がありましたが)、SG21はこのロードマップに沿ってC++26 Contractsの作業を進めていくことになりました。この作業にあたっては、P2521(当時はR2~R3)でまとめられていたMVP仕様が作業ベースとなります。

そして、これを受けて2023年は非常に活発にContractsに関する議論が行われており、2023年中の関連提案がリビジョン改訂含めて74本も提出されています。これはC++20サイクルの終盤で議論が紛糾していた時よりもさらに多い数です。

契約注釈のセマンティクス

契約注釈のセマンティクスとは契約注釈がC++コード上で持つ意味のことで、特に契約条件がどう評価されるのか、評価された時何が起こるのかなどを指定するものです。条件式の副作用の扱いもセマンティクスの一部であり、MVPにおける契約注釈のセマンティクスのほとんどの部分はビルドモードによって一律的に制御されていました。

とはいえ細かいところまですべてが決まっていたわけではなく、例えば契約条件の評価に伴って例外がスローされたときにどうなるのか、あるいは未定義動作が発生したらどうなるのか、などが未決定でした。

そのためまず、ビルドモードによって大まかに指定される契約注釈のセマンティクスの細かい部分についてP2751が提案するセマンティクスが採用されました

これは次のようなものです

  1. 契約条件式は文脈的にbool変換可能な式であり、評価する際は式の評価に関するC++の通常のルールに従う
  2. 契約条件式の評価が完了し、その結果がtrueでは無い時、契約違反が発生している
    1. 契約条件式の評価結果がtrueとなる場合は契約違反を意味せず、それ以上のアクションは発生しない
    2. 契約条件式の評価結果がfalseとなる場合は契約違反が発生している
    3. 契約条件式の評価時に例外が投げられる場合は契約違反が発生している
      • 契約条件式評価時に例外が伝播することを許可すると、noexceptが付加されている関数が契約注釈をもつ場合、その関数評価を含むような式を渡したnoexcept演算子は何を返すべきか?
    4. 契約条件式がUBを含む場合、欠陥が発生する
      • UBによるタイムトラベル(制御フローパスの破棄)が契約条件内で発生する場合、違反ハンドラの呼び出しに置き換えることを意図している
    5. 正常に評価されないその他の契約条件式は通常通りに動作する
      • [[noreturn]]関数からのreturnlongjmp等、病的と思われるものであっても式の評価に関するC++の通常のルールに従う
  3. 契約条件が評価される回数は未規定。違反の検出のために0回以上、違反の処理のために1回以上評価されうる
    1. 契約条件を1回だけ評価することは許可される
    2. 契約条件を0回だけ評価することは許可される
      • 条件式に副作用がない場合、その評価をスキップすることが許可される
      • コンパイラが同じ違反を検出する同等の式を識別できる場合、その代替式の評価で条件式を置き換えることができる(as-ifルールに基づく)
    3. 契約条件を複数回評価することは許可される
    4. 複数の契約条件が連続して評価される場合、それらは相互に並べ替えて評価される可能性がある
      • 並べ替えられた契約条件の評価は、0ではない回数評価される可能性がある

当初のMVP同様に、契約条件式内での副作用を許容しつつ(副作用を禁止するようなサブセットを生み出さず)条件式の評価回数を不定とすることで副作用に依存できないようにしています。この提案で特に明確にされたのは2つ目の契約条件式が評価された場合の振る舞いについてです。

ただしこのセマンティクスにも未決定な部分があり、それは2-3の条件式から例外が送出される場合についてです。この契約注釈からの例外送出に関してはセマンティクスのより根本の部分にかかわる問題があり、契約違反発生時に例外を送出するモードの必要性が望まれたことでその議論が本格化しました。

問題というのはまず、noexcept(true)指定がなされている関数に対して契約注釈を行われているとき、その関数のnoexcept性がどうなるのかについてです。

// noexceptだが、事前条件違反が起こる
void fun() noexcept [[pre: false]];

constexpr bool mystery = noexcept(fun());  // この値は何になる?

using nothrow_callback_t = void(*)() noexcept;
nothrow_callback_t p = fun;                // コンパイルが通る?

void overload(void(*)());                  // #1
void overload(void(*)() noexcept);         // #2

overload(&fun);                            // どちらのオーバーロードが呼ばれる?

Eval and throwモードでは契約違反に伴って例外が送出されるため、その関数の呼び出し時のプログラム状態次第でnoexcept関数から例外が送出されることになります。とすると、ビルドモードがEval and throwの場合は契約注釈が行われている関数は全て例外が送出されうるため。そのような関数のnoexcept演算子の結果はfalseとなるべきでしょうか?

Eval and throwモードでなくとも、契約条件式にはC++の任意の式が指定可能であるため任意の例外が送出される可能性があります。それを考慮すると、契約注釈が行われている全ての関数について契約条件がチェックされる場合に同様の問題があります。

void my_func(int i) [[pre: i >= 0]];
void your_func(int i) noexcept [[pre: i >= 0]];

int x; // Value is not used.
static_assert( false == noexcept(my_func(x)) );   // 常に成り立つ
static_assert( true == noexcept(your_func(x)) );  // 常に成り立つ?

この例のyour_func()は、呼び出し側から見ると契約条件が評価される場合は常に例外を投げる可能性があります。従って、noexcept演算子は契約条件が評価されるビルドモードにおいて契約注釈が行われている関数に対して常にfalseを返さなければなりません。

static_assert( true == noexcept(your_func(x)) );  // 契約条件をチェックしないビルドモードでは成り立つ
static_assert( false == noexcept(your_func(x)) ); // 契約条件をチェックするビルドモードでは成り立つ

noexcept演算子の振る舞いは契約のビルドモードによって変化してしまうわけです。しかし、これは現在noexcept演算子によってその動作を切り替えているコード(std::vectorのムーブコンストラクタのようなコード)の動作をそのビルドモードに応じて静かに変更させることになりますが、それはおそらくバグというべき振る舞いでしょう。

これらの問題について、その解決や整理を試みる提案がいくつか提出され、C++26 Contractsに向けては次の提案によるセマンティクスが採用されました。

これらによる契約注釈のセマンティクス変更は次のようになります

  1. 契約注釈が評価される場合、その評価はignoreobserveenforceのいずれかのセマンティクスを持つ
    • ignore : 契約注釈は各契約条件を評価しないため、契約違反を起こさない
      • 無視される契約注釈のオーバーヘッドはゼロであり、ignoreセマンティクスを持つ契約注釈はODRを除いてコメントアウトされたのと同等になる
    • observe : 契約注釈は各契約条件を評価し、そのいずれかがtrueを返さない場合は契約違反処理プロセスが発生する
    • enforce : 契約注釈は各契約条件を評価し、そのいずれかがtrueを返さない場合は契約違反処理プロセスが発生する。契約違反処理プロセスの終了後、プログラムは実装定義の方法で終了する
  2. 契約注釈の個々の評価において、それがどのようなセマンティクスを持つかは実装定義とする
    • プログラム内の全ての注釈が同じセマンティクスを持つように強制される場合がある
      • MVPの2つのビルドモードに対応する
    • 異なる評価で異なるセマンティクスを持つことにより、同じ関数の異なるインライン版で異なるセマンティクスをコンパイル時に選択することができ、それはODR違反ではなくなる
    • 実装は、その選択をどのように行うかを指定する仕組みがユーザーに公開されていれば、契約注釈のセマンティクスをコンパイル時・リンク時・実行時のいずれかのタイミングで選択できる
  3. noexcept演算子をはじめとするコンパイル時のクエリについて、契約注釈以外のC++コンパイル時セマンティクスが契約のビルドモードによって変化することはない
    • 契約がなされている関数においてその契約条件がコンパイルされwell-formedだったならば、noexcept演算子は全てのビルドモードで(すなわち契約が評価されるかどうかに関わらず)同じ動作をする

このセマンティクスの指定は契約機能全体のC++言語機能内での直交性を向上させており、ビルドモードが元々備えていたセマンティクスにobserveを1つ追加するとともに、契約注釈の評価のセマンティクスを契約注釈全体からその個々のプロパティとすることで、契約注釈を持つ関数のコンパイル時プロパティが契約注釈のセマンティクスに依存しない(できない)ようにしています。

これによって、契約注釈がビルドモードに関わらず他の言語機能に影響を与えないようになり、翻訳単位間で契約注釈のセマンティクスが混合していることも許可されます。すなわち、関数のnoexcept指定は契約注釈とは無関係に今まで通りの振る舞いとなり、契約注釈のセマンティクスは翻訳単位で異なっていることも、翻訳単位内の関数呼び出しごとに異なっていることも許可されます。ビルドモードはあくまで実装によって提供される契約注釈のセマンティクスを一律的に制御する方法(コンパイルオプション)にすぎなくなり、ビルドモードはこの3種類のセマンティクスの上に構築されるものであるため、あるビルドモードの性質が後から追加される別のビルドモードの性質に影響を与えないようになります。

P2877の提案単体では、実装はMVPの2つのビルドモードだけをサポートすることを選択でき、また、コンパイラはリンク時やコンパイル時、実行時で契約注釈のセマンティクスを選択できるようなビルドオプションを提供することもできるようにすることを意図していましたが、これに関しては違反ハンドラの役割の変更に伴ってさらに更新されます(後述)。

このセマンティクスの下では、noexcept指定されている関数の呼び出し時に、Eval and throwモードにおいて契約が破られるか単に契約条件から例外がスローされた場合の振る舞いは、従来のnoexcept指定された関数から例外が送出された場合と同じになり、std::terminate()によってプログラムが終了されます。これは仕様の矛盾ではなく、Lakos Ruleに背いて契約がなされてしまっていることにより発生する結果です。

Lakos Ruleに基づけば

  • noexcept指定された関数の例外仕様と狭い契約は、本質的に互換性がなく、矛盾している
  • つまり、何かしらの契約がなされている関数は狭い契約を持つ(引数等プログラム状態に関して事前条件を持つ)ため、noexcept指定されるべきではない

となり、noexcept指定された関数の契約注釈からの例外送出を特別扱いする必要は無いことが分かります。ただし、これを言語機能として強制すること(noexcept指定された関数への契約注釈はコンパイルエラーとすること)は、有効なユースケース(ネガティブテストやプログラマの仮定に基づくnoexcept指定など)があるため回避されています。

結局例外仕様と契約注釈に関しては、noexcept指定されている関数に契約を付与することができ、noexcept関数ではその契約は評価及び破られた時にも例外を投げないとみなされます。もしその仮定が裏切られ、その契約が評価中に例外を投げるか、契約が満たされなかった時に例外を投げた場合、現在のnoexcept関数から例外を投げた時と同様にstd::terminate()を呼び出してプログラムを終了させます。

違反ハンドラ

C++ Contractsの方向性としては、契約違反時の振る舞いを違反ハンドラ(violation handler)によってユーザーがカスタマイズできるようにすることを最終的な方向性としています。しかし、C++20でもMVPでも、違反ハンドラは実装が定義する固定的なものでその動作は最小限なものでした(C++20では厳密には実装定義の方法で可能とされていたようです)。

違反ハンドラの想定されるユースケースは次のようなものでした

  • 契約違反時の報告方法や形式のカスタマイズ
  • 契約違反時のセマンティクス管理
    • 即終了する、例外を投げる、longjmpによる終了、無視して継続、など

カスタム違反ハンドラは特に、2つ目の契約違反時のセマンティクス制御をビルドモードとは無関係に行う方法でもあります。

MVPの違反ハンドラはシステムデフォルトのものから置き換えることはできず、その動作も特にログ出力などはせずにプログラムを終了させるだけです。とはいえこれは違反ハンドラのデフォルトの動作を最小限にすることで将来的なカスタマイズなどを妨げないようにするものであり、C++20でも議論の的になった部分について何かを規定することを避けるものです。

しかし、MVPの契約違反時の動作は終了するか契約条件を全くチェックしないかの2つしかなく、最低限のログ出力やEval and throwモードの動機のように安全なシャットダウン等のために違反後の継続を行いたい需要がやはり存在しており、それを可能にしようとする議論が行われました。

議論の結果、C++26 Contractsに対してはP2811(とP2838)を採用する形で違反ハンドラのユーザーによるカスタマイズを許可することになりました。

採用されたP2811の違反ハンドラ周りは次のように標準ライブラリの一部として指定されます

// <contract> ヘッダで定義
namespace std::contracts {

  enum class detection_mode : int {
    predicate_false = 1,
    evaluation_exception  = 2,
    evaluation_undefined_behavior = 3
    // 将来の標準によって追加されうる
    // また、実装定義の値を許可する
  };

  enum class contract_semantic : /int {
    enforce = ...
    observe,
    // 将来の標準によって追加されうる、例えば以下のもの
    // assume
    // ignore
    // また、実装定義の値を許可する
  };

  enum class contract_kind : int {
    pre = 1,
    post = 2,
    assert = 3
  };

  class contract_violation {
  public:
    // 仮想関数かどうかは実装定義
    /*virtual*/ ~contract_violation();

    // コピー(及びムーブ)禁止
    contract_violation(const contract_violation&) = delete;
    contract_violation& operator=(const contract_violation&) = delete;

    // 破られた契約の条件式のテキスト表現 
    const char* comment() const noexcept;

    // 契約違反の起こり方
    detection_mode detection_mode() const noexcept;

    // 違反ハンドラが正常にリターンした時、その直後の評価を継続することが期待されているかを返す
    // 現在はfalseを返す(違反後継続モードはまだ組み込まれていない)
    bool will_continue() const noexcept;

    // 破られた契約の種別
    contract_kind kind() const noexcept;

    // 違反を起こした場所の情報
    source_location location() const noexcept;

    // ビルドモードに関する情報
    contract_semantic semantic() const noexcept;
  };

  // デフォルトの違反ハンドラ
  // 受け取ったcontract_violationオブジェクトのプロパティを出力する
  void invoke_default_contract_violation_handler(const contract_violation&);
}

// 置換可能、noxeceptや[[noreturn]]であってもいい
void handle_contract_violation(const std::contracts::contract_violation&);

そして、契約条件式のチェックに伴って例外がスローされた場合の振る舞いを次のように変更します

  • 事前条件/事後条件の評価中に発生した例外は関数本体内で発生したものとして扱われる
  • 契約条件式の評価から送出される例外は契約違反ハンドラを呼び出す
    • この例外を呼び出し元に伝播したい場合、それを行うカスタムハンドラを定義できる
  • 例外は、契約違反ハンドラの呼び出し中にスローされる可能性がある
    • このような例外はすべて、対応する契約条件式の評価中にスローされる例外と同じようにスタック巻き戻しを実行する

すなわち、関数に対する契約注釈は全て、その関数が呼び出す他の関数の評価に伴うものと同様に、現在のC++の例外伝播とnoexceptルールに従います。これは、無条件noexcept指定を行う関数に対する基準であるLakos Ruleに従うものでもあります。

契約注釈のセマンティクスは実装定義かつ契約注釈ごとに異なる可能性があり、契約違反が発生した場合のセマンティクスは違反ハンドラによって指定されます。したがって、契約違反時にどういう振る舞いをするのかについては違反ハンドラのカスタマイズによって制御されるため、それを指定するビルドモードは不要になり、契約条件式を一切チェックしないビルドモードも標準として区別する必要はなくなります(契約注釈がignoreセマンティクスを持つかどうかは実装定義であるため)。

結果、前節のセマンティクスの変更と合わせて標準としてはビルドモードという概念が不必要となったため、MVP仕様からはビルドモードという概念は削除されました。ただし、ユーザー目線でビルドモードというものが無くなったわけではなく、おそらくそれはコンパイラオプションという形で現れることになるでしょう。

たとえば、Eval and abortモードは次のように実装でき

[[noreturn]]
void ::handle_contract_violation(const std::contracts::contract_violation&) {
  std::abort();
}

Eval and throwモードの実装はたとえば次のようになります

[[noreturn]]
void ::handle_contract_violation(const std::contracts::contract_violation& violation) noexcept(false) {
  if (violation.detection_mode() == detection_mode::evaluation_exception) {
    throw; // 例外を伝播させる
  } else {
    throw std::runtime_error{"error message"};
  }
}

No evalモードに関しては違反ハンドラのカスタマイズでエミュレーションはできてもignoreセマンティクスを達成できないため、実装定義の方法(コンパイルオプション)によって一律的に契約注釈のセマンティクスをignoreに設定することで行われるはずです。

そして、契約注釈のセマンティクスは実装定義かつ実行時に指定可能となり、違反ハンドラはリンク時に差し替えるものとなったため、契約注釈のセマンティクスの殆どの部分は必ずしもビルド時点で固定されなくなりました。これによって、ビルド済バイナリによるライブラリ配布時などに、そのライブラリ内部の契約注釈に関してセマンティクスの指定や違反ハンドラのカスタマイズなどを利用者側で事後的に行えるようになります(どのように行うのかは実装定義)。

構文

C++20 Contractsでは契約注釈のための構文として属性(っぽい)構文を採用していました。C++20で議論が紛糾したポイントにこの構文は含まれていなかったのですが、MVP仕様では構文に関しても未決定としてプレースホルダなものを当てていました。しかし、C++20以降のContracts関連の提案では主として属性構文が使用されていました。

// 属性like構文の例
int select(int i, int j)
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]]
{
  [[assert: _state >= 0]];

  if (_state == 0)
    return i;
  else
    return j;
}

int pre;    // ok
int assert; // ok
int post;   // ok

MVPに対する構文の提案としては属性構文の他にも、ラムダ式のような構文を提案するもの

// ラムダ式like構文の例
int select(int i, int j)
  pre{i >= 0}
  pre{j >= 0}
  post(r){r >= 0}
{
  assert{_state >= 0};

  if (_state == 0)
    return i;
  else
    return j;
}

int pre;    // ok
int assert; // ???
int post;   // ok

新しいキーワードとともに専用の領域を導入する条件中心と呼ばれるもの

// 条件中心構文の例
int select(int i, int j)
  precond(i >= 0)
  precond(j >= 0)
  postcond(result >= 0)
{
  incond(_state >= 0);

  if (_state == 0)
    return i;
  else
    return j;
}

int precond;  // ng
int incond;   // ng
int postcond; // ng

文脈依存のキーワードによる専用の領域を導入する自然な構文と呼ばれるもの

// 自然な構文の例
int select(int i, int j)
  pre(i >= 0)
  pre(j >= 0)
  post(result : result >= 0)
{
  contract_assert(_state >= 0);

  if (_state == 0)
    return i;
  else
    return j;
}

int pre;  // ok
int post;  // ok
int contract_assert; // ng

の4つの構文候補が提案されていました。

ロードマップに従って2023年11月の全体会議で構文を決定することになりましたが、その時点でP2737の条件中心構文は新しいキーワードの導入などが忌避されかつ更新が無く、P2461のラムダlike構文はP2961の自然な構文をその後継として認めていたため取り下げられており、実質的に当初の属性構文と自然な構文との二者択一となりました。

構文選択のためにP2885R3にて契約注釈のための構文に求められる要件がまとめれその比較基準が示されました

より正確には、P2961の自然な構文は最後発の構文であり、属性構文の欠点を解消しラムダlikeと条件中心構文の利点を取り入れたうえで、この要件をなるべく満たすように考慮して設計され提案された構文案でした。

P2885に基づいて2つの構文が比較検討された結果、2023年11月のKona会議においてC++ Contractsの構文としてはP2961の自然な構文が採用されました。

議論の過程などは公開されていないためどういう判断基準で決定されたかはわかりませんが、2つの構文の詳細な比較検討はP3028に見ることができます。

P2961では属性構文の欠点として次のような点が挙げられていました

  1. 契約注釈の区切りのトーク[[ ... ]]が構文として重い
    • 一部のユーザーからは醜いと認識されている
  2. 契約構文は属性と同様の記法を利用するが属性ではないため、混乱が生じる
    • 契約構文は違反ハンドラを通じるなどして、関数から新しいコードパスを作成できるが、標準属性はこのようなことを行うように設計されていない
  3. 契約注釈を置ける構文上の位置は関数宣言の自然な読み取り順序に反している
    • 属性の置ける位置を再利用するため、後置戻り値の前(overriderequires節の前)に事前条件と事後条件がくる
  4. assertは式ではないため、Cのassertの完全な代替となり得ない
  5. 3と4を属性構文のまま解決しようとすると、属性構文の利点が失われる
    • 現在それらが可能なように標準属性はできていない、そのため実装経験もない
  6. 属性構文では、その内部の述語の前に:がくる場合に、それより前の内容に区切りを導入しない
    • 視覚的な情報の区別(契約種別や戻り値の名前、ラベルなどの見分け)が難しくなり、将来的に構文解析の曖昧さを生じさせる
  7. 契約注釈自体に属性を付加する場合、属性内の属性という文法を導入させなければならない

そして、P2961は既存の構文が満たしていない部分を満足することを設計ゴールとし、次のような目標を掲げていました

  • 構文は既存のC++に自然に馴染む。
    • 契約機能に慣れていないユーザーでも混乱を招くことなく直感的に理解できるものである必要がある
  • 契約注釈構文は、属性やラムダ式など既存のC++の構成要素に似ていてはならない
    • ぱっと見で認識可能な独自の設計空間に置かれているべき
  • 構文はエレガントかつ軽量である
    • 必要以上にトークンや文字を使用するべきではない
  • 読みやすくするために、一次情報とニ次情報を構文的に分離する
    • 一次情報(条件種別、契約条件式、戻り値名、キャプチャなど)をそれ以外のニ次情報(ラベルなど)よりも視覚的に強調する

P3028R0より、P2961の自然な構文による契約注釈の例

// フリー関数に対する契約注釈
template <typename T>
auto f() noexcept -> int requires something<T> pre( true );

// 配列ポインタを返す関数に対する契約注釈
int (*g(char i))[17] pre( true );

// 事後条件における戻り値参照と戻り値型
auto f() -> int post( r : r > 0 );

// 事後条件と属性
auto f()
  [[ function_type_attribute1 ]]
  [[ function_type_attribute2 ]]
  -> int
  [[ return_type_attribute1 ]]
  pre( 1 || true )
  pre( 2 || true )


// メンバ関数に対する契約注釈
struct S {

  // デフォルト宣言への注釈(C++29以降)
  bool operator=(const S&) pre( true ) = default;

  template <typename T>
  auto f() const&& -> int requires something<T>
    pre( true )
    post( r : true )
  {
    return 17; 
  }
};

// メンバ初期化子でのアサーション
struct S {
  int d_x;
  
  S : d_x( contract_assert( true ) , 17 ) {}
};

// ラムダ式
auto x = [] (int a) -> int pre( true ) { return 17; };
auto y = [] -> int pre( true ) { return 17; };

// ラムダ式 + requires節
auto x = [](auto a) requires something<decltype(a)>
  pre( true ) { ... };

C++26に向けて、残りの問題

契約注釈のセマンティクスと契約構文というとても大きな問題がP2695R1のロードマップ通りのスケジュールで解決を見たことで、C++26 ContractsのSG21における作業完了は俄然現実味を帯びてきました。

とはいえまだC++26に間に合わせるにあたってのすべての問題が解決したわけではありません。細かいながらも決定を必要とする問題がいくつか残っています。

P2896でまとめて提示されている残りの設計上の問題は次のものです

  1. 異なる翻訳単位の最初の宣言における契約について
    • 現在のMVPでは、fが異なる翻訳単位で宣言されている場合、その契約は同一(identical)でなけれならない(そうでない場合診断不用のill-formed)とされているが、同一(identical)の意味が定義されていない。この定義が必要
    • 選択肢
      1. 同一(identical)の意味を定義し、これが実装可能であることを確認する
      2. 異なる翻訳単位の同じ関数の2つの宣言について、両方に契約がなされている場合をill-formed(診断不用)として問題を回避する。ただしこれは実用的ではない
  2. オーバーライドする関数とされる関数の契約について
    • 現在のMVPでは、オーバーライドする関数はされる関数の契約を継承し、追加の契約を行えない。これについて異論があり、どうするかを選択する必要がある。
    • 選択肢
      1. なにもしない(現在のMVPのまま)
      2. MVPの制限を強め、オーバーライドする関数もされる関数も契約を行えないようにする
      3. 継承された契約をオーバーライドする機能などの仮想関数に対するより柔軟なソリューションを考案し、MVPを緩和する
  3. ラムダ式に対する契約と暗黙キャプチャについて
    • 契約機能はラムダ式においても機能しなければならない。その際、ラムダの本体で使用されていないが契約指定で使用されている名前はキャプチャされるかどうか(契約に表れている名前がODR-usedであるかどうか)が未解決
    • 選択肢
      1. ラムダにおける契約は他の所と同じルールに従う。すなわち、契約条件式でのみ使用されている名前はキャプチャされる
        • P2890R0が提案している
      2. ill-formedとする。ラムダにおける契約条件式は、他の方法でキャプチャされない名前をキャプチャできない
        • P2834R1が提案している
      3. ラムダ式における契約機能を無効にする
  4. コルーチンにおける契約
    • コルーチンに対する契約は通常の関数と同様に動作するのかが未解決
    • 選択肢
      1. コルーチンで事前・事後条件とアサーションを許可し、事前条件と事後条件のセマンティクスを指定する
        • P2957R0が提案し、セマンティクスについても提供している
      2. コルーチンではアサーションのみ許可する
      3. コルーチンでは契約機能は無効とする
  5. 定数式における契約について
    • 定数評価中に契約条件は評価されるのか、どういうセマンティクスを持つのかが未解決。特に、契約条件式はコア定数式ではない場合と、契約条件式はコア定数式だがfalseに評価された場合にどうなるのかが問題。
    • 選択肢(2つの場合のどちらについても)
      1. ill-formed
      2. ill-formed、ただし診断不要
      3. 契約条件式は無視される(定数評価中のみ)
      4. 契約条件式は無視するが、警告を発することを推奨する
      5. いずれの場合も定数式における契約注釈のセマンティクスは実装定義とする
  6. トリビアルな特殊メンバ関数に対する契約について
    • トリビアルな関数に契約がなされている場合、そのトリビアル性に影響するかどうかが未解決
    • 選択肢
      1. 契約が指定されていてもトリビアルのまま
      2. 契約が指定されていてもトリビアルのままだが、その結果として事前・事後条件がチェックされない可能性がある
        • P2834R1が提案している

これら問題に対する設計についてはそれぞれ既に提案が提出されています。

ただし、このうち一部のものについては2023年11月の会議で解決が採択されています。

4と6についてはC++26に向けた設計が決定されており、それぞれ次のようになりました

  • コルーチンにおける契約
    • コルーチンに対しては事前条件と事後条件を指定できない(コンパイルエラー)
    • アサーションcontract_assert)は使用可能
  • トリビアルな特殊メンバ関数に対する契約について
    • 最初の宣言でdefault指定されている関数には契約注釈(事前条件/事後条件)を行えない(コンパイルエラー)

これらはどちらも契約注釈の存在をコンパイルエラーとしており、これが最終的に確定した仕様というわけではありません。MVPの当初の理念に基づいて、この問題の詳細な設計の確定は将来のバージョンで行い、C++26 Contractsに向けてはエラーとしておくことで将来の拡張を妨げないようにしています。

残りの問題については、来春3月末の東京における会議でその設計が決定される予定です。

2023年末時点でのMVP

これらの議論をすべて反映した現時点でのMVP仕様、すなわちC++26 Contracts仕様候補はP2900にまとめられています。この記事を執筆している時点ではR3が最新版です。

ここで改めて、C++20で議論が紛糾したポイントについて振り返ってみます。

  • 契約違反後の継続
    • 契約違反後に継続できるべきかどうか
    • 継続できるようにするとして、それはグローバルなスイッチ(コンパイラオプション)とローカルなスイッチ(個別の契約条件ごとの指定)のどちらによって制御されるべきか?
  • ビルドレベル
    • 全ての契約注釈のセマンティクスはビルドレベルによって一律に制御されるが、それは可能なのか?
    • また、ある契約注釈がチェックされるかどうかを決定するのがグローバルなビルドレベルだけであるべきか?
    • よりきめ細かい制御の方法を望む向きがあり、それらを並立させた場合にデフォルトはなんであるべきか、あるいは契約注釈のセマンティクスを制御できる方法が複数提供されないほうが良いか?
  • グローバルスイッチの存在
    • 全ての契約注釈のセマンティクス及び有効/無効は全てグローバル制御になっているが、ローカルな制御を望む向きもあった
    • グローバルな契約機能の有効化/無効化は全ての契約注釈に影響を与えるのか、一部の注釈だけがそのグローバル設定を上書きできるのか
    • どのレベルで契約注釈の制御が行われるか、あるいはどのレベルでそれを制御できるかはContractsがどのように使用されるかに大きな影響を与えうる
  • ソース内コントロール
    • それぞれの契約注釈ごとにそのセマンティクスを設定し、それがグローバル設定の影響を受けないようにする提案があった
    • そのようなアイデアは十分に成熟しているか?
    • 契約注釈のセマンティクスの制御をどの程度細かく行うべきか?
  • コードの仮定に使用すること
    • チェックされていない契約条件をコードの仮定とすることが許可されていたが、これが当初の設計意図に合致しているかどうか?
      • すなわち、契約注釈が[[assume(expr)]]の機能を含むことは妥当なのか?
    • チェックされない契約条件をコードの仮定とすることが合理的かどうか、またその制御とデフォルトはどうあるべきか?

現時点でのMVPはこれらの問題に次のような回答を示しています

  • 契約違反後の継続
    • 違反後のセマンティクスは違反ハンドラを通してカスタマイズされる
    • 違反ハンドラからreturnすることで元の制御フローを継続することもできる
      • ただしその場合は未定義動作に陥る可能性がある
  • ビルドレベル
    • ビルドレベルの概念は仕様からは削除された
    • 契約注釈のセマンティクスは個々で異なる可能性があり、それは実装定義
      • 契約注釈毎のセマンティクス指定は将来の機能
  • グローバルスイッチの存在
    • 契約注釈のセマンティクスは個々で異なる可能性があり、それは実装定義
      • 全ての契約注釈に一律なセマンティクスを与えるオプションがおそらく提供される
      • 契約注釈毎のセマンティクス指定は将来の機能
    • 契約違反後のセマンティクスは違反ハンドラを通して制御される
  • ソース内コントロール
    • 契約注釈のセマンティクスは個々で異なる可能性があり、それは実装定義
      • 契約注釈毎のセマンティクス指定は将来の機能
    • 契約違反後のセマンティクスは違反ハンドラを通して制御される
  • コードの仮定に使用すること
    • 禁止
    • その用途には[[assume(expr)]]を使うべきで、現時点では契約注釈にその能力を持たせることは検討されていない

C++ Contractsの単純さと応用可能性の問題については、セマンティクスを3つだけ定義しておきその決定を実装定義とすることで将来のより粒度の細かいセマンティクス制御への道を開いており、C++ Contractsの安全性の問題に関しては[[assume(expr)]]の機能を分離することで解決されています。

順当にいけば、P2900の将来のリビジョンがC++26 Contractsの提案文書としてC++26のワーキングドラフトにマージされることになるでしょう。ロードマップ通りならそれは2025年2月ごろになるはずで、来年の今頃にはC++26 Contracts仕様がほぼ固まりEWGのレビューを終えているはずです。

ただ1つ不安な点があるとすれば、契約注釈からの例外送出に伴うセマンティクスの問題で、noexcept(contract_assert(...))の結果がどうなるべきか?など一筋縄ではいきそうにない問題が提起されていることです。

この問題はどう決定しても禍根を残しそうで、このあたりの問題に関してはまた変更があるかもしれません。

参考文献

[C++]UBとEB

この記事はC++アドベントカレンダー2023 11日目の記事です。

C++26に対して現在、一部のUBを置き換える概念としてEBというものが議論されています。

EBとは

EBはErroneous Behaviourの略称であり、EBはUB同様に規格に準拠したC++プログラムの動作状態を指定するもので、未定義動作(UB)に対して誤った動作という言葉通りの意味です。

EBは誤った動作ではあるものの、その動作は未定義ではなく定義されたものであり、EBを含むプログラムはUBを含むものと異なり何が起こるか分からないという状態にはありません。

例えば、いわゆる自動変数というカテゴリにある非クラス型の変数の初期化子を省略するとその変数は初期化されずその値は不定となります。それそのものは問題ないのですが、その値を読み取ろうとするとUBとなります。

void f(int);

int main() {
  // 未初期化変数、値は不定
  int x; // ✅

  // 未初期化変数の読み取り、UB
  f(x); // 💀

  // 初期化してからならUBではない
  x = 0;
  f(x); // ✅
}

この場合のUBは未初期化の変数を読み取ることですが、これをEBとして規定しなおし特定の値(例えば0)を読み取るようにすることができます。

void f(int);

int main() {
  // 未初期化変数、0に初期化される
  int x; // ✅

  // EB、0を読み取る
  f(x); // ✅
}

この場合、依然として未初期化変数を読み取るコードは間違っている(erroneous)コードではありますが、その動作として何が起こるか分からない(UB)ではなく特定の値(この例では0)を読み取るように規定するのがEBです。また、安全な動作をするようになるとはいえ間違っているコードであり続けているため、実装は依然としてこれに警告を発するでしょう。

このように、EBは一部のUBを置き換えることを意図して導入されようとしています。この例の未初期化変数読み取りがその急先鋒にあり、順調にいけばC++26で未初期化変数読み取りをしているコードはUBではなくなります。そして同様に、UBがEBに置き換えられることによって、現在UBのコードは何もしなくても言語バージョンをアップデートするだけで安全なコードになります。

規定

EBはまだ標準に導入されていませんが、その議論の最前線であるP2795R3(未初期化変数読み取りのEB化)には現時点の標準文言案が記されており、EBは次のように規定されています

erroneous behaviour

実装が診断することが推奨されるwell-definedな動作(実装定義・未規定の動作を含む)

[Note: Erroneous behaviourは常に正しくないプログラムコードの結果として生じる。実装はそれを診断することが許可されているが、必須ではない。定数式の評価においては明示的にerroneousと指定された動作を示すことはない。]

また、C++の実装に対する要件としてUBと並べられて次のように指定されています

この文書(規格書のこと)は、未定義の動作をする構成要素を評価するプログラムに関する実装を要件としていない
erroneous behaviourをする構成要素を評価した後の動作は実装定義

推奨プラクティス : 実装はerroneous behaviourの実行を、診断無しで実行するか、診断を発行して継続するか、診断を発行して終了、のいずれかで行う必要がある

[Note: 実装は、プログラムの動作に関する実装固有の仮定の下でerroneous behaviourに到達可能であると判断する場合、診断を発行することができる(これにより、誤検知が発生する可能性がある)。]

UBプログラムに対する要件が無いというのは以前から同じような規定が存在しており、UBが鼻から悪魔と呼ばれる根拠の一部でもあります。それと比較すると、EBの動作は(実装定義を介するとはいえ)しっかりと規定されており、UBのように最適化に利用することは明確に禁止されています。

注意点として、EBに関して現れる実装定義(または未規定)の動作とはEBそのものではなくEBに対してどういう動作をするかについてのもので、その詳細は個別のEBによって変化するものです。例えば未初期化変数読み取りでは、未初期化変数読み取りそのものがEBであり、どういう値を読み取るかが実装定義となり、それはコンパイラオプションによって指定することを意図するものです。

すなわち、EBが内包する実装定義の意図としてはコンパイラオプション等によって動作を調整するもので、実装が好き勝手な動作をできるというような意味ではありません。

例えば、未初期化変数読み取りに関してGCC/Clangは-ftrivial-auto-var-init=...というコンパイラオプションを(GCCは12から、clangは16から)利用可能であり、...zeroを指定すると未初期化変数は0に初期化され、patternを指定すると特定パターンで初期化され、オプションそのものを指定しないあるいはuninitializedを指定すると今まで通りの動作(未初期化のまま)となります。

これはEBの実装経験の1つであり、EBはこの動作を追認するとともにそれに明確な規格上の意味を与えるものです。

現在のC++プログラムでill-formedではないものは、well-definedかつUBを含まずコードの記述通りに正しく動作するものと、UBを含むために動作が規格の範囲外にあるものの2つに大別されます。誤った動作をしながら標準仕様によってその動作が制限される、というものは存在していません。

EBはそのギャップを埋めるもので、well-definedかつ間違っている(erroneousである)ものの、間違っていることが認識されているため実装はその診断を提供でき、同時にその動作について仕様の制約を与えるものです。

経緯

EBという動作状態の発明は、ここまで例示に使用してきた未初期化変数読み取りのUBをUBではなくそうとする議論に端を発しています。

2022年11月に公開されたP2723R0は、C++プログラムの安全性向上のために未初期化変数を強制的にゼロ初期化するように変更することを提案するものでした。これはそこそこの反響があり、概ね好意的に受け止められていたように思えます。

この提案はSG23のレビューでも大きな反対はなくEWGに転送されました。

この提案に対してはいくつものフィードバックやアイデアが寄せられたようで、P2754R0でそれらをまとめて代替ソリューションとの比較検討がなされました

そこでは、7つの解決策候補が提示されました

  1. 常にゼロ初期化する(P2723の提案)
    • 非クラス型の自動変数が初期化されない場合、常にゼロ初期化される
  2. ゼロ初期化もしくは診断する
    • 無条件に不定値を読む場合は診断(コンパイルエラー)
    • 条件次第で不定値を読む可能性がある場合はゼロ初期化
  3. ソースでの初期化を強制する
    • 非クラス型の未初期化変数はill-formed
  4. 後から初期化されることを考慮しつっつ、ソースでの初期化を強制する
    • 注釈なしの非クラス型の未初期化変数はill-formed
    • 未初期化変数は明示する
  5. 実装定義の値で初期化するものの、書き込み前の読み取りは未定義動作
  6. 実装定義の値で初期化するものの、書き込み前の読み取りは誤った動作
    • 書き込み前の値の読み取りは誤っているものの、UBではない
    • コンパイラフラグなどによって、テストのために検出しやすい値で初期化したり、実運用のために安全な値で初期化したりする
    • あるいは、誤った動作を未定義動作として扱うこともできる
  7. 値初期化に一本化
    • 仕様からデフォルト初期化を削除する
    • これによって初期化は常に値初期化となり、仕様が単純化され、未初期化を含む初期化周りの問題が解決される

そして、これらの解決策候補を実現可能性、下位互換性、表現可能性の3つの観点から比較します

  • 実現可能性 : その解決策が既存のC++標準に対して一貫しているかどうか。つまりは、C++標準に適用可能であるかどうか
    • 実現可能
    • 実現不可能
    • 不透明 : 現時点では判断できない
  • 下位互換性 : その解決策が採用された場合に、既存のコードを壊すことが無いかどうか
    • 互換性がある : 以前にコンパイル可能なコードは引き続きコンパイル可能であり、UBの場合のみ動作が変更される
    • 正しいコードと互換性がある : 以前にコンパイル可能でUBを含まないものは引き続きコンパイル可能だが、UBを含むコードはコンパイルエラーとなる場合がある
    • 互換性がない : 以前に正しいコードもコンパイルが通らなくなる
    • 不透明 : 現時点では判断できない
  • 表現可能性 : その解決策が採用された場合に、既存コードの意味が変更されるかどうか
    • 良い : 初期化を遅らせる意図を明示、あるいはロジックエラー(初期化忘れ)を修正するためにコードを更新する必要がある
    • 悪い : 意図的な初期化遅延とロジックエラー以外の可能性が発生することで、現在よりも状況が悪くなる
    • 変わらない : 意図的な初期化遅延もしくはロジックエラーを含むような(未初期化変数を含む)既存コードが曖昧ではなくなる
    • 不透明 : 現時点では判断できない

比較結果は次のようになりました

解決策 実現可能性 下位互換性 表現可能性
1. 常にゼロ初期化 実現可能 互換性がある 悪い
2. ゼロ初期化/診断 不透明 正しいコードと互換性がある 変わらない
3. 初期化の強制 実現可能 互換性がない 良い
4. 遅延初期化を考慮した初期化の強制 実現可能 互換性がない 良い
5. 実装定義の値で初期化+その読み取りは未定義動作 実現不可能 互換性がある 変わらない
6. 実装定義の値で初期化+その読み取りは誤った動作 実現可能 互換性がある 変わらない
7. 値初期化に一本化 不透明 不透明 不透明

3つの評価軸から最適と思われる組み合わせは、実現可能であり下位互換性があり表現可能性が良いものです。しかし、それに該当する解決策は存在しないようです。評価軸の最初の2つについては自由度がほぼありませんが、表現可能性については、そのうえで最悪変化が無ければ良い解決策であるといえます。そして、それに該当するのは6の解決策だけです。

1の解決策(P2723の提案)は表現可能性が悪化すると評価されています。これは、その解決策の適用後に初期化子が無い変数宣言がバグなのかどうかわからなくなるためです。

void f(int);

// P2723が適用されているとすると
int main() {
  // この宣言はint x = 0;のつもりで書いたのか
  // 本当にint x;と書いてしまったのかわからない
  int x;

  f(x);
}

現在のC++は非クラス型ローカル変数は初期化しなければその値は初期化されないのがデフォルトであり、現在のコードベースで出現するそのような変数宣言は初期化を忘れたか意図的に初期化していないかのどちらかになります。

意図的に初期化をしない場合については属性などのオプトアウトするメカニズムによってそれを明示することができます。しかし、P2723の無条件でゼロ初期化を行う世界では、初期化子を持たない変数宣言は意図的にゼロ初期化をしているのか初期化を忘れたのかの区別がつかなくなります。

つまり、int x = 0;のつもりでint x;と書いてしまっている現在の誤ったコードは、P2723適用後に正しいコードになってしまいます。これは基本的にはいいことに思えますが、たとえばint x = 10;のつもりでint x;と書いてしまっている現在のコードだとどうでしょうか?それはP2723適用後も誤ったコードであり続けますが、コード上からはそれを読み取ることができなくなり、0という分かりやすい値で初期化されていることで実行も上手くいってしまう可能性が高いでしょう。コンパイラ等のツールも、int x = 0;int x;の違いを認識できなくなることで、初期化忘れというバグを警告できなくなります。

6の解決策は、意図しない未初期化変数はバグであるという現在の基本をベースとしており、int x = 0;int x;は異なる意味を持つ宣言です。そして、あくまで未初期化の値を読み取る場合をUBではなく特定の値を読み取るようにすることで問題の解決を図っています。さらに、初期化する値を指定可能(実装定義)とすることで実行時のデバッグに役立てたり、本番環境で安全な値にしておくことができます。意図して未初期化のままにしておくためのオプトアウトメカニズムが用意されれば、残ったすべての未初期化変数宣言はバグであると人間もツールも認識することができ、なおかつその動作を安全なものに変更することができるわけです。

もうお察しの通り、この6番目の解決策がErroneous Behaviourとよばれる動作状態です。

前述のように、このような振る舞いは-ftrivial-auto-var-initとして既に利用可能であったため、あとはC++標準仕様でErroneous Behaviourというものを規定し、未初期化変数読み取りがEBであるというように変更するだけでした。

これらの議論を経て、EBの規定追加と未初期化変数読み取りのEB化のための標準文言を追加するためにP2795が提出され、P2723が解決しようとした問題はEBという解決策によってこちらの提案に引き継がれています。

P2795では、意図的な末初期化を行うためのオプトアウトメカニズムとして、[[indeterminate]]という属性を提案しています。これを使用すると、初期化忘れ・正しい初期化・意図的な未初期化の3つの意図がコード上で明白になります。

void f(int);

int main() {
  // 初期化忘れ、バグ
  int x;

  // 初期化済み
  int y = 0;

  // 意図的な初期化省略
  int z [[indeterminate]];

  f(x); // EB、特定の値が読み取られ、コンパイラは警告を発する
  f(y); // OK
  f(z); // UB、初期化しなければならない
}

P2795はこの記事執筆時点(2023年12月初旬)でEWGのレビューを通過しCWGでレビューされています。全ての自動変数(関数引数や一時オブジェクトを含む)に対してこのEBを適用するように改訂が必要とされてはいますが、今のところ順調に進行しています。

他のEB候補について

P2795R3には、現在UBとなっているものをEBに切り替える際の判断基準となる原則が提供されています。

  1. 既存のコードを破壊するため、現在well-definedな動作は一般的にEBにすべきではない
    • 特定の動作が常に間違っていることが判明した場合、例外的にこれを考慮することができる
  2. 現在未定義とされている動作で、有害なコンパイル結果となる可能性が低いもの(例えばCVEなどによって報告された脆弱性を悪用することで明示されるもの)はUBのままにしておくべき
    • これは規格の複雑さを最小にするため
  3. 現在UBであるもののうち、有害な障害モードを露呈するもので妥当なwell-definedな動作が見つかるものは、EBへの変更を検討すべき
    • 注記 : ある種のUBは、たとえ有害であったとしても全ての場合に合理的に検出できるわけではないため、その動作を定義しようとすることは不可能
    • 注意 : EBはUBよりもプログラマが故意に依存する可能性が若干高いため、その動作を定義することの明確な利点が無い限りUBを保持する判断をすべき

そのうえで、現在標準にあるUBのうちEBに転換できうるものをリストアップしています

  • 符号付整数型のオーバーフロー
  • 表現できない数値変換
  • 不正なビットシフト
  • ゼロ除算
  • void戻り値型の関数終端からの流出(return文忘れ)
  • [[noreturn]]関数からのreturn
  • 抽象クラス型のコンストラクタ/デストラクタからの純粋仮想関数呼び出し

このうち、非void戻り値型の関数終端からの流出のより限定的な状況である、コピー/ムーブ代入演算子定義におけるreturn *this;忘れについて、その場合に暗黙にreturn *this;する動作をEBとしてUBから転換する提案がすでに提出されています。

ただし、これに関してはEBよりもill-formed(コンパイルエラー)とすることを好む向きもあります。

参考文献

この記事のMarkdownソース

[C++]名前を必要としない変数のための変数名

C++26より、使用しない値に対する共通した変数名として_(U+005F、アンダースコア/アンダーバー)を言語サポート付きで使用できるようになります。

[[nodiscard]]
auto f() -> int;

auto g() -> std::tuple<int, double, std::string>;

int main() {
  auto _ = f(); // ok、警告なし
  auto [n, _, str] = g();   // ok

  std::cout << _;   // ng
}

概要

ローカル変数でその変数名が_であるものは、暗黙的に[[maybe_unused]]が指定されたように振る舞います。

[[nodiscard]]
auto f() -> int;

int main() {
  // この宣言は
  auto _ = f();
  
  // このように宣言されているのと同等
  [[maybe_unused]]
  auto _ = f();
}

これによって、この変数_は以降使用されていなくてもコンパイラは警告しません。また、[[nodiscard]]指定された関数では、戻り値を捨てている警告も抑制されます。

変数名_はまた、ローカルの構造化束縛の変数名としても使用でき、同じことが適用されます。

auto g() -> std::tuple<