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

文書の一覧

全部で40本あります。

P0493R3 Atomic maximum/minimum

std::atomicに対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、read-modify-writeのセマンティクスをSG1のフィードバックに基づいて修正、replace_xxx関数を削除、機能テストマクロの追加、提案する文言の調整、サンプルの追加、実装例とそのベンチマークの追加、などです。

この提案はC++23に向けてLWGに転送されています。

P1467R8 Extended floating-point types and standard names

C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。

以前の記事を参照

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

  • 浮動小数点数型のCV修飾を正しくハンドルできるように文言を修正
  • 暗黙変換と定数式、暗黙変換と縮小変換の間の相互作用に関するセクションを追加
  • オーバーロード解決の例を追加
  • 実装経験の欠如に関するセクションを追加

などです。

P1673R6 A free function linear algebra interface based on the BLAS

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

以前の記事を参照

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

  • P0009とP2128の参照を更新した事
  • 2より大きなランクのmdspanへの参照を削除
  • symmetric_matrix_rank_k_update, hermitian_matrix_rank_k_updatealphaを取らないオーバーロードを削除
  • conjugated_scalar+. *と比較演算子を追加

などです。

P1774R4 Portable assumptions

コンパイラにコードの内容についての仮定を伝えて最適化を促進するための[[assume(expr)]]の提案。

以前の記事を参照

このリビジョンでの変更は、文言の改善(機能テストマクロ削除、重複属性指定の許可など)、EWGのフィードバックに基づく文章の改善、例の追加などです。

P2093R11 Formatted output

std::formatによるフォーマットを使用しながら出力できる新I/Oライブラリstd::printの提案。

以前の記事を参照

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

  • LEWGのフィードバックに基づいて、vprint_unicodeの規定にあった「ネイティブのユニコードAPIの呼び出しで変換が必要となる場合、実装は無効なコードユニットを(ユニコード標準に従って)U+FFFDに置換すべき」という文章をRecommended Practiceに移動
  • ストリームを受け取らないprintlnオーバーロードを追加
  • 文言の改善
  • ユニコード標準の参照を更新

などです。

P2152R1 Querying the alignment of an object

alignofを型だけではなくオブジェクトに対しても使用出来るようにする提案。

以前の記事を参照

このリビジョンでの変更は、フィードバックに基づいて提案する文言を改善したこと、以前に問題と定義していたものを削除した事、文言と代替案のセクションを分割したことなどです。

このリビジョンでの提案内容は式(オブジェクト)に対するalignofを許可することだけに絞られています。alignof(expr)に対するその結果は、exprの結果となるオブジェクトtについて、tの型Talignas(x)が指定されている場合はx、そうでない場合はalignof(decltype(t))となるように提案されています。

以前の提案に含まれていたCとのalignasの非互換やalignofalignasの値が異なる場合のハンドルなどは、分離されてはいないものの解決のための文言を提案してはいません。

P2173R1 Attributes on Lambda-Expressions

ラムダ式の関数呼び出し演算子に対して属性指定を出来るようにする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューを提案する文言に反映したこと、最新のドラフトをベースとするように更新した(P1102の変更を反映した)ことです。

この提案はすでにCWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2198R4 Freestanding Feature-Test Macros and Implementation-Defined Extensions

フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGのフィードバックを提案する文言に反映したこと、既存のマクロの値を更新する代わりに新しいマクロを追加することにしたこと、フリースタンディングでも使用できる機能についての機能テストマクロの必要性を追記したことなどです。

この提案はLEWGで議論中ですが、C++26に向けて作業されることになったようです。

P2248R3 Enabling list-initialization for algorithms

値を指定するタイプの標準アルゴリズムにおいて、その際の型指定を省略できるようにする提案。

以前の記事を参照

このリビジョンでの変更はreplace_copy_ifrange版のデフォルト引数型を修正したこと、projectionがある場合のデフォルト引数型に関する議論の追加などです。

P2283R2 constexpr for specialized memory algorithms

<memory>にある未初期化領域に対する操作を行う各関数をconstexprにする提案。

以前の記事を参照

このリビジョンでの変更は、default_construct_atuninitialized_default_constructを削除したことです。これは、この2つの追加に伴ってコア言語の変更が必要になるためです。

P2286R4 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更は範囲の要素のpair/tupleのフォーマットについてを削除したこと(複雑すぎるため)、動的・静的なデリミタに関するセクションを追加しstd::format_joinを削除したこと、format_as_debugset_debug_formatに変更したこと、filesystem::pathについての議論を追記(コンセプトの制約の再帰が発生する)したことです。

P2300R3 std::execution

P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。

以前の記事を参照

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

  • いくつかのバグ修正
  • run_loop実行コンテキスト(処理をFIFOかつシングルスレッドで実行する)の追加
  • receiver_adaptorreceiverを簡単に書けるようにする)の追加
  • schedulersendersender_ofを要求することで、completion schedulerを提供するようにする
  • when_allのキャンセル機会について規定
  • as_awaitableCPO(senderなどのオブジェクトをコルーチンで待機可能なものに変換する)の追加
  • as_awaitableによるawaitableを考慮するようにconnectを変更
  • エイリアステンプレートvalue_types_of_t(そのsenderreceiverset_valueに渡す引数列)とerror_types_of_t(そのsenderreceiverset_errorに渡す引数列)を追加
  • stop_token_type_tstop_token_of_tに変更
  • possibly eager algorithmを削除する根拠を追記
  • 実装経験について追記

などです。

P2302R2 std::ranges::contains

新しいアルゴリズムとしてstd::ranges::containsを追加する提案。

このリビジョンでの変更は、範囲が含まれているかを調べるオーバーロードcontains_subrangeに変更したこと、実装経験について追記したことです。

P2329R0 Move, Copy, and Locality at Scale

どのような条件でムーブ代入を使用すればコピー代入よりもパフォーマンスが向上するのか、を実験した報告書

この文書では、単純化したシステムによる実験によってムーブ代入がコピー代入よりも有利な場合、あるいはコピー代入の方が有利な場合を明らかにしようとしています。

実験におけるシステムは複数のサブシステムから構成され、各サブシステムは複数のデータエレメントを保持し、それぞれのエレメントが一定サイズのメモリ領域を使用します。各エレメントはサブシステム内で頻繁にアクセス(読み書き)され、サブシステム間で転送もされると想定し、この転送にムーブ代入orコピー代入を使用します。

実験のパラメータは次の7つです。

  1. システムの全体サイズ(systemSize
    • systemSize = numSubsystems * elemsPerSubsys * elemSize
  2. システム内のサブシステムの数(numSubsystems
  3. サブシステム内のエレメント数(elemsPerSubsys
  4. 各エレメントのサイズ(elemSize
  5. エレメントがサブシステム間でシャッフルされた回数(churnCount
  6. 各サブシステム内のエレメントのアクセス回数(accessCount
  7. シャッフル/アクセスのサイクル全体が繰り返される回数(repCount

これらのパラメータを変更しながら実行時間を計測し、コピーとムーブの実行時間を比較します。パラメータ数は7つですが、一部実行に数時間かかるものもあったため、今回の実験では全部はカバーしきれなかったようで、実行したテストは次の3つです。

Test1

L1キャッシュサイズ未満からL3サイズの数倍までのシステムサイズでテスト

パラメータ 変数?
systemSize 変数 213~225 [byte]
elemsPerSubsys 変数 4~systemSize / elemSize / 2
elemSize 定数 128 [byte](キャッシュラインサイズの2倍)
churnCount 定数 1
accessCount 定数 4
repCount 変数 min(1/systemSize, 32)

Test2

Test1をベースに、エレメントサイズがキャッシュラインサイズの倍数では無いようにした

パラメータ 変数?
systemSize 変数 213~225 [byte]
elemsPerSubsys 変数 4~systemSize / elemSize / 2
elemSize 定数 96 [byte](キャッシュラインサイズの1.5倍)
churnCount 定数 1
accessCount 定数 4
repCount 変数 min(1/systemSize, 32)

Test3

エレメントアクセス回数がシャッフル回数を大幅に超える場合を、物理メモリを超える非常に大きなシステムサイズでテスト

パラメータ 変数?
systemSize 変数 232~235 [byte]
elemsPerSubsys 変数 8~systemSize / elemSize / 16
elemSize 定数 64 [byte](キャッシュラインサイズ)
churnCount 定数 1
accessCount 定数 8
repCount 定数 5

テスト環境

  • Model: MacBook Pro 2018 (Model ID: MacBookPro15,1)
  • CPU: 6-core Intel Core i7, 2.2 GHz
  • L1 Data Cache: 32KiB per core
  • L1 Instruction Cache: 32KiB per core
  • L2 Cache 256KiB per core
  • L3 Cache 9MiB shared
  • RAM: 16GiB
  • Disk: 512GB SSD

結果

提案では結果の包括的な分析をしていませんが、とりあえず観察された結果を報告しています。詳細な結果は筆者のリポジトリcsvで置いてあります。

  • エレメントサイズ128[byte]、システムサイズ32[MiB]では、ムーブ代入により小さいサブシステムが多い場合は2倍まで高速化(50%の実行時間)
    • 大きいサブシステムが少ない場合は2倍まで低速化(実行時間189%)
  • エレメントサイズ64[byte]、システムサイズ4/8[GiB]では、ムーブ代入はコピー代入よりも悪い結果(最大7倍悪い)となった
    • この場合でも、多数の小さなサブシステムではムーブ代入はコピー代入より大幅に高速だった
  • 結果はややノイジーで、わずかなパラメータの変更で連続実行すると大きく結果が振動するケースが多く見られた
    • 一説では、キャッシュライン上の要素のアライメントが実行の度に異なっていたと言われる
    • それでも、ページのスラッシングによって引き起こされる大幅な速度低下など、一定のパターンが浮かび上がった

この報告はこれで終わりではなく、さらなる改善実験を予定しているようです。

P2363R2 Extending associative containers with the remaining heterogeneous overloads

連想コンテナの透過的操作を、さらに広げる提案。

以前の記事を参照

このリビジョンでの変更は、LEWGからのフィードバックを適用してDesign decisionsの項を拡張したこと、この提案によるパフォーマンス改善を測定し追記したこと、提案する文言を改善したことなどです。

P2374R2 views::cartesian_product

P2374R3 views::cartesian_product

任意個数のシーケンスの直積を取って、その元のシーケンスを生成するcartesian_product_viewの提案。

以前の記事を参照

R2での変更は

  • 提案する文言の修正
  • コンストラクタをexplicitにした
  • cartesian_product_viewborrowed_rangeにしないことについての説明を追記
  • 機能テストマクロの追加
  • input_rangeを入力できるようにした
  • common_rangeとなるように要件を緩和
  • イテレータとセンチネルに-(距離を計算する)を追加

R3での変更は、sizedifference_typeの型を実装定義にしてそれらの要件のみを指定するようにしたことです。

P2387R3 Pipe support for user-defined range adaptors

ユーザー定義のRangeアダプタに対して、パイプライン演算子|)サポートを簡単に行えるユーティリティを提供する提案。

以前の記事を参照

このリビジョンでの変更は、文言のアップデートのみです。

この提案はLWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2416R1 Presentation of requirements in the standard library

現在の規格書の、要件(requirement)の記述方法を変更する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映したことと文言の修正です。

P2438R1 std::string::substr() &&

右辺値std::stringからのsubstr()を効率化する提案。

以前の記事を参照

このリビジョンでの変更は、実装経験を追加したこと、P1787R6を考慮したconstオーバーロードについての説明を追記、ユーザー提供のアロケータを持つ場合のsubstrについての説明の拡張などです。

P2440R1 ranges::iota, ranges::shift_left, and ranges::shift_right

Rangeアルゴリズムranges::iota, ranges::shift_left, ranges::shift_rightの提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映したことです。

この提案はLWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2442R1 Windowing range adaptors: views::chunk and views::slide

元のシーケンスの各要素を指定した要素数のウィンドウによって参照するようなviewを生成する、views::chunk/views::slideアダプタの提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを2つに分割したこととLWGのフィードバックを反映したことです。

この提案はLWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2447R1 std::span and the missing constructor

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

以前の記事を参照

このリビジョンでの変更は、破壊的変更についての説明を追記したことなどです。

この提案はLEWGのメーリングリストレビューにおいて少し紛糾しているようです。特に、この提案の内容が破壊的かつ危険なものである点が問題視されていて、より詳細なレビューが行われるようです。

P2455R0 2021 November Library Evolution Poll Outcomes

2021年11月に行われたLEWGにおける全体投票の結果。

次の13の提案が投票にかけられ、否決されたものはありません(全てLWGへ転送されています)。また、投票に当たって寄せられた賛否のコメントが記載されています。

P2456R0 2021 December Library Evolution Polls

2021年の12月に予定されている、LEWGでの全体投票の予定表。

以下の12の提案が投票にかけられる予定です。

11と12を除いて、C++23に導入するためにLEWGでの作業を終えてLWGへ転送するための投票です。

P2468R1 The Equality Operator You Are Looking For

==から導出される!=演算子とユーザー定義!=演算子が衝突してしまう問題を解消する提案。

以前の記事を参照

このリビジョンでの変更は、更新されたルールに従って2つ目の解決策と提案する文言を更新したことです。

以前は「生成された演算子候補について、同じスコープで宣言された同じシグネチャを持つ別の候補が存在し、その候補が生成されていない(ユーザー定義である)候補である場合、生成された演算子オーバーロード候補から除外される」というルールを提案していましたが、このリビジョンでは「生成された候補について、その生成元がoperator==であり、それにマッチするoperator!=が宣言されている場合(つまり、そのoperator==の名前をoperator!=に書き換えた時に何かの再宣言となる場合)、生成された候補を取り下げる」というルールを提案しています。これによって、C++20のほかの部分(演算子)のルールに影響を与えることなく問題となる== !=の問題を改善することができます。

これによって、operator==に対応するoperator!=が宣言されていると、そのoperator==からの演算子生成はすべて(逆順の==も)行われなくなります。

struct A {};

template<typename T> 
bool operator==(A, T);  // #1

bool a1 = 0 == A();  // OK、#1から生成された逆順の候補が呼ばれる

template<typename T> 
bool operator!=(A, T);

bool a2 = 0 == A();  // error、#1に対応する!=が宣言されているため、#1からの生成は行われない


struct B {
  bool operator==(const B&);  // #2 (const忘れ)
};

struct C : B {
  C();
  C(B);

  bool operator!=(const B&);  // #3 (const忘れ)
};

bool c1 = B() == C();  // OK、#2が呼ばれる。#2に対応する!=(#3)がCに関して見つかるため、#2から逆順の候補は生成されない
bool c2 = C() == B();  // error, Cに関してみつかった#2とBに関してみつかった#2の逆順候補との間で曖昧となる 

最後のc1, c2のケースは少し理解か難しいです。B() == C()では#2およびその逆順の候補を考慮しますが、2つ目の引数C()のスコープで(つまりクラスCについて)#3が見つかるため、それによって#2からの演算子生成を抑制します。

C() == B()でも#2およびその逆順の候補を考慮しますが、2つ目の引数B()のスコープで(つまりクラスBについて)対応する!=は見つからないため、逆順の候補が生成されます。その結果、#2const修飾がないため、#2とそれを用いた逆順の候補の間のオーバーロード解決は曖昧となり、コンパイルエラーとなります。

このoperator==に対するoperator!=のチェックは、演算子を使用した時に生成された候補に対して使用されるoperator==とその1つ目の引数について行われます。つまり、x == yに対してはy == xx !== yに対してはx == y(選択されると最終的に!(x == y)となる)のように候補が生成され、この時に使用されている==についてその1つ目の引数のスコープで対応する!=を検索するわけです。したがって、x == yではyのスコープでのoperator==について、x != yではxのスコープのoperator==について、それぞれoperator!=が探索され、見つかれば生成された候補は取り下げられ、見つからなければ生成された候補が考慮されます。

これはかなりパッチ感のあるルールですが、この意図は主にconst修飾を忘れているメンバopertor==/!=定義に対する一貫比較仕様の影響を緩和しようとするものです。このルールによって、C++20一貫比較のほかの部分に影響を与えることなくこれらの問題に対する破壊的な影響を抑えることができています(提案では、110のプロジェクトに対してコンパイルエラーが起こるのは8プロジェクト)。

この提案のEWGにおけるレビューと投票では、この提案をC++20へのDRとすることに合意が取れているようです。

P2473R1 Distributing C++ Module Libraries

C++モジュールのビルド済み成果物の配布を容易にするための、ビルドツール・コンパイラ・静的解析ツール間の相互運用性のための共通フォーマットの提案。

以前の記事を参照

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

  • meta-ixx-infoファイルが必ずしもixxファイルと同じ場所になく、相対パスでの検索の対象となることを明確にした
  • 変数置換の責任をパッケージマネージャに移動
  • 同じインターフェースファイルの代替解析オプションについてのセクションを追加。さらに、BMIファイル名に使用された解析オプションを識別するためのものを挿入するようにした
  • 「Search order」のセクションを明確にした
  • meta-ixx-infoファイルに基づくBMiファイルの生成について概念実証を追記

などです。

P2474R0 views::repeat

指定された値の繰り返しによる範囲を生成するRangeファクトリである、views::repeatの提案。

int main() {
  for (int i : std::views::repeat(17, 4)) {
    std::cout << i << ' ';  // 17 17 17 17 
  }
}

repeat_viewiota_viewを参考にして設計されており、views::repeat(v)repeat_view{v})のように呼ぶとvの値の無限列を、views::repeat(v, n)repeat_view{v, n})のように呼ぶとvの値のサイズnの範囲を生成します。指定された値はrepeat_view内部にコピーorムーブして保持されており、repeat_viewイテレータは値をコピーせずrepeat_view本体に保存されている値へのポインタを持っています。repeat_viewイテレータの間接参照は値をコピーして返すのではなく、そのconst参照を返します。

この提案ではrepeat_viewに指定する値の型にはcopy_constructibleであることが要求されていますが、上記性質を考えるとmove_constructibleで十分なはずです。ただ、それは現在の他のviewsingletransformなど)と一貫せず、何かを保持する必要のあるview型の要求をコピーからムーブに変更するのは別の提案(P2494R0)で行なっています。

また、iota_view同様に、views::take, views::dropに対してrepeat_viewのための特殊対応も追加されます。

int main() {
  auto tr1 = std::views::repeat(17, 4) | std::views::take(2);  // repeat_view{17, 2}が返る
  auto dr1 = std::views::repeat(17, 4) | std::views::drop(2);  // repeat_view{17, 2}が返る

  auto tr2 = std::views::repeat(17) | std::views::take(2);  // repeat_view{17, 2}が返る
  auto dr2 = std::views::repeat(17) | std::views::drop(2);  // repeat_view{17}が返る
}

iota_viewと異なるのは、終端が指定されない場合でもこのような特殊対応がなされることです。iota_viewの場合、iota_view{a, b}[a, b)の範囲を示し、この場合のa, bの型が等しくないとiota_viewsized_rangeではなくなります。すると、有限かつrandom_access_rangeだけどサイズを求められないiota_viewが出来上がり、bが範囲に含まれない範囲終端であることから、その場合に範囲をオーバーランする危険性があるため、iota_viewsized_rangeであるときだけviews::take, views::dropで特殊対応されます。repeat_viewの場合は、2つ目の引数は範囲のサイズそのものであり、iota_viewのような状況は発生し得ないため常に特殊対応が可能となります。

P2486R1 Structured naming for function object and CPO values

CPOやniebloidなどの関数オブジェクトに対しての命名ルールに関する提案。

以前の記事を参照

このリビジョンでの変更は、フィードバックを反映したこと、実装経験について追記したことです。

この提案はSG9とLEWGの投票においてコンセンサスを得ることができず、どうやらこれ以上の継続はなさそうです。

P2494R0 Relaxing range adaptors to allow for move only types

何かを保持する必要があるタイプのview型について、保持するものの型に対する要件をcopy_constructibleからmove_constructibleに弱める提案。

このような要件はviewコンセプトの当初の要求に基づいたものでしたが、その後viewコンセプトの要件は緩和され(P1456R1, P2325R3)、その変更が反映されていなかったようです。この提案では、その変更は<ranges>view全体に反映するものです。影響を受けるのは次のものです

  • single_view
  • transform_view
  • zip_transform_view
  • adjacent_transform_view
  • repeat_view(P2474)

同じことはP2483でも提案されていましたが、そちらはsingle_viewだけを対象としていたのと、copy_constructibleだけどcopy_assignableではない型をハンドルしていませんでした。この提案は対象をview全体に広げるとともにそのようなケースをハンドルしたものです。

この提案のSG9におけるレビューではC++20へのDRとすることが推奨されています(まだ未定ですが)。

P2498R0 Forward compatibility of text_encoding with additional encoding registries

std::text_encodingP1855)がIANA以外のレジストリに対応できるようにしておく提案。

現在のstd::text_encodingの文字エンコーディングはIANAレジストリに登録されているものだけを扱っており、他のものを考慮していません。しかし、IANAのリストは不完全であり、C++標準の参照先として適さない所があるため、今から将来的にIANA以外の文字エンコーディング方式のレジストリなどを参照できるようにしておこうとする提案です。

この提案では、std::text_encoding::idstd::text_encoding::iana_idに、std::text_encoding::mib()std::text_encoding::iana_mib()へ変更するとともに、クラスレイアウトについて将来的な拡張に備えるようにしておくことを推奨事項(recommended practice)として追記しておくことを提案しています。

P2499R0 string_view range constructor should be explicit

std::string_viewrangeコンストラクタをexplicitにする提案。

文字列はchar(あるいは他の文字型)の範囲として扱うことができますが、逆は必ずしも常に正しくありません。そのため、文字型の範囲からstd::string_viewへの変換が暗黙変換となっていると問題を起こす可能性があります。

char const t[] = "text";
std::string_view s1(t); // s1.size() == 4;

std::span<char const> tv(t);
std::string_view s2(tv); // s2.size() == 5;

このs1, s2はどちらも同じ範囲tから構築されていますが、結果として得られるstring_viewオブジェクトの意味するところは異なっています。s1const char*から構築するコンストラクタが選択され、文字列終端(\0)は含まれませんが、s2rangeコンストラクタによってspanから構築され、文字列終端も含めた範囲全体を参照します。

別の例として

extern void get_string(std::span<char> buffer);
extern void use_string(std::string_view str);

char buf[200];
get_string(buf);
use_string(buf);

このようなコードは良く書かれており、std::string_viewが配列の参照をとるコンストラクタを持たない理由でもあります。このコードが次のように書き換えられた時

extern void get_string(std::span<char> buffer);
extern void use_string(std::string_view str);

std::array<char, 200> buf;
get_string(buf);
use_string(buf); // oops

このコードはコンパイルされ実行可能で特に未定義動作も起こしませんが、実行結果が異なります。use_string(buf)の呼び出しではstd::arrayからstd::string_viewへの変換がrangeコンストラクタによって行われ、buf全体を参照する文字列となります。これはconst char*からの変換時とは異なっており、おそらくプログラマの意図とも異なります。std::string_viewrangeコンストラクタがexplicitであれば、この変換をコンパイルエラーとすることができます。

これらの理由から、この提案はstd::string_viewrangeコンストラクタにexplicitを追加することを提案しています。

P2501R0 Undo the rename of views::move and views::as_const

提案中のviews::moveviews::as_constの名前をリネームしないでおく提案。

views::moveviews::as_conststd::move, std::as_constと同じ名前をしており、使用した時に引数のviewそのものに作用するのかその要素に作用するのかが分かりづらくなるとして、views::all_moveviews::all_constに変更されました。

viewは常に怠惰な(lazyアルゴリズムであり、その効果は入力のrangeそのものではなく常にその要素に対して作用します。views::joinviews::join_with)はrangeに対して作用しているように見えるかもしれませんが、views::joinは入力の各要素(内側range)をフラット化して見ることができるviewを作成するだけです。範囲をjoinするのか要素をjoinするのかはAPIレベルのことではなく実装詳細ですが、joinの実装詳細もまた要素を結合することです。

views::filterは範囲をフィルタリングするviewであるという意見もあるようですが、filterは要素をフィルタリングすることで範囲のフィルタリングを行います。範囲とは要素の容器のようなもので、フィルタリングアルゴリズムは要素に適用されます。あるいは、範囲は説明のための実装詳細としてのみ存在し、述語は要素に適用されます。

これらのことは、views::moveviews::as_constにも当てはまっています。views::moveは各要素をmoveするviewであり、views::as_constは各要素をas_constするviewです。従って、views::moveviews::as_constはこのようなviewに対する正しい名前であり、それがなにをするかを端的に表しています。views::moveviews::as_constという名前は意図的なもので、怠惰なアルゴリズムの名前はそれがどのような操作を入力範囲の各要素に適用されるかをユーザーに伝えるために意図的に名前付けられています。

views::all_moveviews::all_constという名前は、このような原則に従っておらず、他のviewとの一貫性がありません。allは冗長であり、その名前は要素に何をするかを表現していません。このような名前はRangeライブラリのview設計原則にそぐわず一貫性のない名前付けになっています。

これらの理由から、この提案は名前を元に戻す(views::moveviews::as_constのままにする)ことを提案しています。

P2502R0 std::generator: Synchronous Coroutine Generator for Ranges

Rangeライブラリと連携可能なT型の要素列を生成するコルーチンジェネレータstd::generator<T>の提案。

この提案は、P1268をベースとして、その設計は継続しています。そのため、基本的なところは以前の記事を参照ください

この提案での変更は次の2点です

  1. テンプレートパラメータの並べ替え
  2. O(1)で破棄する必要についての懸念の解消

std::generatorは新旧どちらも3つのテンプレート引数をとります

// P1268
template<class Ref, common_reference_with<Ref> Value = remove_cvref_t<Ref>, class Allocator = void>
class generator;

// この提案
template<typename T, typename Allocator = void, typename U = void>
class generator {

  // generatorの範囲/イテレータの値型(value_type)
  using Value = conditional_t<is_void_v<U>, remove_cvref_t<T>, U>;

  // generatorの範囲/イテレータの参照型(reference_type)
  using Reference = conditional_t<is_void_v<U>,
                                  conditional_t<is_reference_v<T>, T, const T&>,
                                  T>;

  // generatorのpromise_type::yield_value()の引数型
  using Yielded = conditional_t<is_reference_v<Reference>, Reference, const Reference&>;
};

RefT)は返す値の型(iter_reference_t)、ValueU)は要素の型(iter_value_t)を表しており、ValueU)は指定されなければRefT)のremove_cvrefした型が要素型となります。

この提案では2番目と3番目のパラメータの位置が入れ替わった形で、それは要素型をTと異なる型にしたい場合よりもアロケータをカスタムしたいユースケースの方が多いと考えられるためです。また、そこからイテレータの値型/参照型を導出する部分も改善されており、これによってTに真の参照型を指定できるようになっています。

この提案のLEWGにおける最初のレビューでは、generator::Referenceの型が問題となっています。この提案ではgenerator<T>についてTが参照型ではない場合の型としてconst T&を提案していますが、follyやcppcoroではT&、range-v3ではTだったりしており、それをどうするか議論が紛糾しています。一応まだC++23を目指して作業されています。

P2504R0 Computations as a global solution to concurrency

P2300のsender/receiverモデルが、並行処理に関して大域的な解決策を構成できることを証明する文書

C++標準に並行性に関するモデルを追加することは重要な作業であり、低レベルな部分(構成可能性、エラー処理、効率、ユーザビリティなど)だけでなく高レベルでもセマンティクスの正しさを確保する必要があります(提案されたモデルの一般性)。低レベルな部分はLEWG/SG9の議論で十分にカバーされているため、この提案は高レベルセマンティクスである提案されたモデルの一般性の検証を行うものです。

この文書は次のような疑問に答えるものです。

  • sender/receiverはあらゆる種類の並行処理に関する問題に対処できるか?
  • sender/receiverはロックを使用する古典的なアプローチを排除できるか?
  • sender/receiverプログラムの並行性の側面を設計する際に基礎的な要素として使用できるか?
  • sender/receiverが並行処理に関する一般的な解決策となるように提案(P2300)に追加するものはあるか?

この文書では、低レベルな部分を抽象化した上で、高レベルなセマンティクスについて論理的な証明を与えることで、P2300の並行モデルの表現力を示すとともにこれらの疑問に答えています。

その上で、この文書は次のような推奨事項を述べています

  • P2300で示されているモデルをできるだけ早く採用するように努めること
  • senderの名前をasync computation(短縮系 computation)、receiverの名前をasync notification handlerに変更する
  • 計算の上により多くの並行性に関する抽象化を提供するための作業を開始する
    • リアライザ、パイプライン、タスクグラフ、リアクティブプログラミングなど

P2505R0 Monadic Functions for std::expected

std::optionalのMonadic interfaceをstd::expectedにも導入する提案。

P0798R8の採択によって、std::optionaltransform, and_then, or_elseの3つのモナド的操作のためのメンバ関数が追加されました。

std::expectedstd::optionalを発展させたものであり、よく似た役割と意味論を持つことから、意識的に共通のインターフェースとなるように設計されています。しかし現在のところ、std::expectedにはP0798の3つのモナド的操作は含まれていません。モナド的操作はstd::optionalと同様にstd::expectedに対しても恩恵があり、std::optionalとの一貫性向上のためにもtransform, and_then, or_elsestd::expectedにも追加しようとする提案です。

using time_expected = expected<boost::posix_time::ptime, std::string>;

time_expected from_iso_str( std::string time ) {
  try {
    ptime t  = boost::posix_time::from_iso_string( time );
    return t;
  }
  catch( std::exception& e ) {
    return unexpected( e.what() + " str was: "s + time);
  }
}

// for use with transform
ptime next_day( boost::posix_time::ptime t ) {
  return t + boost::gregorian::days(1);
}

// for use with or_else
void print_error( std::string error ) {
  cout << error << endl;
}

// valid iso string
const std::string ts( "20210726T000000" );


void before() {
  time_expected d = from_iso_str( ts );

  if (d) {
    ptime t = next_day( *d );
  } else {
    print_error( d.error() );
  }
}

void after() {
  auto d = from_iso_str( ts )
               .or_else( print_error )
               .transform( next_day ) ;
}

std::optionalと異なる点として、これらの関数に指定する処理はvoidを返すことができます。また、std::expectedstd::expected<void, E>が認められており、その場合はtransformが使用できなくなります。

using void_expected = expected<void, std::string>;

// for use with or_else
void print_error( std::string error ) {
  cout << error << endl;
}

void_expected doit(int x) {
    if (x < 5) return {}; // void return
    return unexpected(std::format("X must be less than 5 passed {}", x));
}

// chained in and_then
void_expected doit2() {
    return {};
}

int main() {
  auto res = doit(1).or_else( print_error ); // res.has_value() == true
  res = doit(5).or_else( print_error );      // call print_error
  res = doit(1).and_then( doit2 );           // call doit2

  res = doit(5).transform(doit(5)); // compile error
  res = doit(5).value_or({});       // compile error
}

std::expectedC++23に導入すべくLWGのレビュー中で、この提案もC++23入りを目指しています。

P2507R0 Only [[assume]] conditional-expressions

P1774で提案されている[[assume(expr)]]について、exprの部分に条件式(conditional-expression)だけを書けるようにする提案。

P1774は[[assume(expr)]]と言う構文で、コードの仮定(事前条件)をコンパイラに伝えることで最適化を促進するものです。現在の提案ではexprの部分にはassignment-expressionと言う構文を置くことができるようになっています。これは条件式(conditional-expression)だけでなく次の4つの構文を包含しています。

  1. 条件式(conditional-expression
  2. yield式(yield-expression
  3. throw式(throw-expression
  4. logical-or-expression assignment-operator initializer-clause

[[assume(expr)]]exprの結果がtrueとなる式を渡すことを想定しているため、明らかに2と3は不適格です。

4つ目の構文は簡単にはx = 0のような代入式です。logical-or-expressionは2項演算子を受理可能な構文で、assignment-operatorは複合代入演算子を含み、initializer-clause{}など任意の初期化式を置けます。これの有効性は判断しづらいですが、元の提案ではこのユースケースが示されておらず例やモチベーションの説明でも条件式が想定されていることと([[assume(expr)]]は仮定を伝えるものであるため)条件式が仮定を表現するのに最適であることから、4番目のような構文もやはり不適格であると思われます。これらのことから、exprの部分に1番目の条件式(conditional-expression)だけを書けるようにしようとする提案です。

現在の[[assume(expr)]]assignment-expressionを取るのは、既存実装(__builtin_assume(expr)など)とif/switchの慣行に従った結果のようです。

また、この提案の変更によって[[assume(x == 42)]][[assume(x = 42)]]のように間違えた場合をコンパイルエラーとして弾くことができるようになります。

P2508R0 Exposing std::basic-format-string

説明専用のstd::basic-format-string<charT, Args...>をユーザーが利用できるようにする提案。

std::basic-format-string<charT, Args...>はP2216で追加されたもので、フォーマット文字列のコンパイル時チェックを行うための型です。これは説明専用(exposition only)として追加されており、利用可能なのはstd::formatをはじめとする標準ライブラリの機能だけです。しかし、これをユーザーも利用したい場合があります。

template <typename... Args>
void log(std::format_string<Args...> s, Args&&... args) {
  if (logging_enabled) {
    log_raw(std::format(s, std::forward<Args>(args)...));
  }
}

これはlogging_enabledと言うフラグによってロギングするかを切り替えたい実装で、その判定と出力をlog()にまとめる事でstd::formatと同様の使用感でそれを行おうとするコードです。このような場合にstd::basic-format-string(上記コード中のstd::format_string)をフォーマット文字列を受け取る引数として使用したくなりますが、現在それはユーザーに解放されていません。実装を再利用すれば同様のものを整える事はできるでしょうが、標準ライブラリの実装詳細に依存することになるので移植性の担保が面倒です。

この提案は、std::basic-format-string<charT, Args...>およびその特殊化を説明専用ではなくすことで、フォーマット文字列のコンパイルチェックを単体の機能としてユーザーが任意に利用できるようにしようとするものです。

std::basic-format-string<charT, Args...>が説明専用になっているのは、P2216がC++20へのDRだったためその影響範囲を制限しようとしていたことと、将来的により良いコンパイル時フォーマット文字列チェック方法が実装可能となった場合に変更可能であること、の2つを意識したためのようです。後者は特に、constexpr引数(P1045R1)や衛生的なマクロ(P1221R0)などなど将来的な言語機能を見越したもののようです。ただ、C++23にこのようなものはなく、上記の問題についての現在可能なソリューションはstd::basic-format-string<charT, Args...>を利用可能とすることだけです。

P2509R0 A proposal for a type trait to detect value-preserving conversions

算術型について、その値を保持する変換を検出するための型特性を追加する提案。

例えば次のように、算術型をテンプレートパラメータで受け取ってメンバとして保持して何かするクラスを考えます。

template <class Rep> // Repは算術型あるいはそれと同等の型を想定
class Quantity {
  Rep value;

  ...
};

このような型はstd::complex, std::chrono::durationや在野のQAngleなどに見ることができます。このような型は異なる特殊化の間で相互に変換可能としたい場合が多く、そのために変換コンストラクタが用意されます。

template <class Rep>
class Quantity {
  Rep value;

public:

  template<typename Rep2>
  // explicit(...) or requires ...
  Quantity(const Quantity<Rep2>& other);
};

このような変換コンストラクタは、情報の損失や未定義動作を防止するために何らかの形で制約されるはずです。例えば

  1. std::complex<From> -> std::complex<To>の変換はFrom -> Toの変換が縮小変換となるとき、explicit指定される
  2. QAngle<From> -> QAngle<To>の変換はToFromの値を正確に表すことができないとき、explicit指定される
  3. std::chrono::durationは、時間の刻み間隔の処理やtreat_as_floating_pointを利用して変換コンストラクタをオーバーロード候補から外す

これらのアプローチにはそれぞれ次のようなメリットとデメリットがあります。

  1. 「縮小変換」を制約することでそのセマンティクスを定義についての問題を回避できる(標準の規定を参照すればいい)
    • 一方、直感に反する場合がある。例えばint -> long doubleの変換は常に縮小変換となる
  2. 暗黙変換によって情報が欠落させないと言う考え方に沿って、変換によって値が保持されることを制約すれば、より期待に一致する結果を得られる
    • 一方、これはプラットフォーム間の非互換性によって移植性を損ねる場合がある。例えば、MSVC ABIではlong double -> doubleの変換は情報を失わない(実質同じ型)が、Itanium ABIではそうではない
  3. アドホックなアプローチでは最大限の柔軟さを得られる
    • 一方、正しく使用するには面倒な場合がある。例えば、カスタム浮動小数点数型を使用する場合にtreat_as_floating_pointの特殊化を忘れるなど。

現在の標準ライブラリはケース1,2の実装を支援するための機能を提供していません(3はアドホックであるため一般実装を提供することは困難)。1はP0870R4で提案され作業中です。この提案は2の実装を支援する機能、すなわちFrom -> Toの変換によって情報の欠落が発生することを検出するためのユーティリティ(型特性)を追加しようとするものです。

template <class Rep>
class Quantity {
  Rep value;

public:

  template<typename Rep2>
  explicit(std::is_value_preserving_convertible_v<Rep, Rep2> == false)  // Rep -> Rep2の変換で値が保持されないならばexplicit
  Quantity(const Quantity<Rep2>& other);
};

提案されているのはstd::is_value_preserving_convertible<From, To>というもので、From -> Toへの変換が可能かつFromが表現可能なすべての値がToでも正確に表現される場合にtrueとなります。正確に表現される(exactly represented)のような言葉は規格用語であり、すでに浮動小数点数型の変換の文脈で使用されているものです。この表現では操作的な定義を定めるのではなく、変換における意味論だけを定義しています。

このような値が正確に保持される変換は縮小変換とは異なるものです。例えばlong double -> doubleへの変換は常に縮小変換ですが、MSVC ABIではlong double, doubleは全く同じ表現を持っているため実際には情報の欠落は発生しません。標準が定義する縮小変換は型の意味論と関係性によって定義されていますが、この提案の言う変換は実際の(実装固有の)量を考慮するものです。

P2510R0 Formatting pointers

std::formatについて、ポインタ型のフォーマットを充実させる提案。

std::formatは現在でもpを用いる事でポインタ値を出力することはできます。

int i = 0;
std::format("{:p}", i);  // 0x00007ffe0325c4e4
std::format("{}", &i);   // NG

しかしこれ以上のフォーマット指定を行うことはできず、ポインタ型を直接指定したり、整数型に用意されているようなフォーマットを利用したければreinterpret_castする必要があります。

int i = 0;
std::format("{:#018x}", reinterpret_cast<uintptr_t>(&i));  // 0x00007ffe0325c4e4
std::format("{:#018X}", reinterpret_cast<uintptr_t>(&i));  // 0X00007FFE0325C4E4
std::format("{:#Lx}", reinterpret_cast<uintptr_t>(&i));    // 0x7ffe_0325_c4e4

この提案は、std::formatがポインタ型を直接受け取れるようにすると共に、0(0パディング)、type(型に応じたデータ表示方法の指定)、Lロケール指定フォーマット)をポインタ型のフォーマットでサポートするものです。

現在 この提案
format("{:018}", ptr);  // 不明瞭(LWG3644の後ではNG)
format("{:P}", ptr);    // NG
format("{:L}", ptr);    // NG

format("{:-}", ptr);    // 不明瞭(LWG3644の後ではNG)
format("{:#}", ptr);    // 不明瞭(LWG3644の後ではNG)
format("{:018}", ptr);  // 0x00007ffe0325c4e4
format("{:P}", ptr);    // 0X00007FFE0325C4E4
format("{:L}", ptr);    // 0x7ffe_0325_c4e4

format("{:-}", ptr);    // 不明瞭(LWG3644の後ではNG)
format("{:#}", ptr);    // 不明瞭(LWG3644の後ではNG) 

type指定は大文字のPでこれを指定するとポインタ値の16進出力を大文字で出力します。省略すると従来のp相当で小文字で出力されます。

おわり

この記事のMarkdownソース

[C++] ExecutorとNetworking TSで起きていたこと

この記事はC++ Advent Calendar 2021の21日目の記事です。

ExecutorとNetworking TS

Executor

Executorライブラリは、非同期並行処理の制御のための基盤となる機能を提供しようとするものです。

標準インターフェースを介して様々なハードウェアリソースへアクセスする方法と非同期処理のグラフを表現する方法を提供し、処理がいつどこで行われるのかをプログラマが制御可能とするためのライブラリです。

Networking TS

Networking TSは、標準ライブラリに基礎的なソケット通信の機能を追加しようとするものです。

Networking TSはBoost.Asioをベースとして設計されており、一部変更されている部分はあるもののBoost.Asioほぼそのままの形で利用することができるようになっています。

Networking TS(Boost.Asio)は、非同期I/O操作のために非同期処理のサポート機構、すなわちExecutorに相当するものを内包しています。そのため、ExecutorライブラリはNetworking TSの一部であり、ベースとなるものとされます。

現在のC++標準化委員会では、Executorを導入してからNetworking TSをそれをベースとするように修正し導入する、といった感じで作業が進められています(した)。

ExecutorもNetworkingも、C++20策定時点ではC++23に導入される見込みであるとされており、実際設計も固まっていたのでほぼ内定だろうと思われていました・・・

Executorの道程

AsioにおけるExecutorの発見

AsioにおけるExecutorは開発の早い段階で見出され、2006年ごろにはasio_handler_dispatch()(後にasio_handler_invoke())と言う名前のADLカスタマイズポイントとして実装されました。

namespace mine {
  template <typename Handler>
  class counting_handler {
    void operator()(...) {
      // invoke handler
    }

    Handler handler_;
  };

  // counting_handlerに対するカスタマイズasio_handler_invoke
  template <typename Function>
  void asio_handler_invoke(Function function, counting_handler* context) {
    // increment counter
    // invoke function
  }
}


int main() {
  // ...

  mine::counting_handler handler(&handle_read);

  // 非同期Read
  boost::asio::async_read(socket, buffer, handler);
}

Asioの実装内で、asio_handler_invokeは利用されます。

namespace boost::asio {

  // デフォルトのasio_handler_invoke()
  template <class Function>
  void asio_handler_invoke(Function& function, ...) {
    function(); // デフォルトは現在のスレッド
  }
  
  // async_readの実装とする
  auto async_read(const Socket& socket, Buffer& buffer, Handler handler) {
    // I/O操作・・・

    // ADLによるasio_handler_invokeを介したhandlerの呼び出し
    using asio_handler_invoke;
    asio_handler_invoke(handler, ...);
  }
}

(細部を端折っている雑な例であることをお許しください)
これによって、Asioの提供する非同期操作に与える完了ハンドラによって、そのハンドラが呼び出される場所(実行コンテキスト)とその方法を制御することが可能となります。これは例えば、非同期処理の完了ハンドラでさらに非同期処理をネストさせて呼び出すときに全てのハンドラを1つのスレッドで実行させたい場合などに使用できます。

これはC++11にムーブセマンティクスが導入された際にムーブオンリーなハンドラに対応できない問題が発覚し(この詳細はよくわかんないです・・・)、それに対処するためにassociated_executorに変更され、その際にカスタマイズポイント自体(associated_executor)から実行コンテキストの表現(上記例のcontext)が分離されました(これはどうやら、Networking TSに移植する際に変更されたようです)。

namespace boost::asio {

  // 完了ハンドラからexecutorを取得するためのtraitsクラス
  template<class T, class Executor = system_executor>
  struct associated_executor {
    using type = T::executor_type;
    // T::executor_typeがなければ
    using type = Executor;

    static type get(const T& t, const Executor& e = Executor()) noexcept {
      return t.get_executor();
      // T::executor_typeがなければ
      return e;
    }
  };

  // async_readの実装とする
  auto async_read(const Socket& socket, Buffer& buffer, Handler&& handler) {
    // I/O操作・・・

    // 完了ハンドラに関連づけられたexecutorを取得する
    auto ex = associated_executor<Handler>::get(handler);
    // exの場所でexによって指定された方法で完了ハンドラを実行
    dispatch(ex, std::forward<Handler>(handler), ...);
  }
}

ここで出てくるexこそが今日Executorと呼ばれているものです。associated_executorは非同期操作の完了ハンドラ型に対して部分特殊化しておくことでカスタムすることができ、その::get()から得られるExecutorによって完了ハンドラが実行される場所を指定します。何もカスタムしなければデフォルトのsystem_executorが使用され、これは別のスレッド(スレッドプール内の任意のスレッド)で完了ハンドラを実行します。

WG21 SG1におけるUnified Executorの追求

Asioでの動きを受けてかそれとは別かはわかりませんが、2012〜2015にかけていくつかの異なるExecutorの提案がSG1(Study Group1 : Concurrency Study Group)に提出されました。

1つはgoogleからのもの

class executor {
public:
  virtual void add(function<void()> closure) = 0;

  virtual void add_at(time_point abs_time,
                      function<void()> closure) = 0;

  virtual void add_after(duration rel_time,
                         function<void()> closure) = 0;
  ...
};

class thread_pool : public executor { ... };

1つはAsioからのもの

class my_executor {
public:
  template<class Function, class Allocator>
  void dispatch(Function&& f, const Allocator& alloc);
  
  template<class Function, class Allocator>
  void post(Function&& f, const Allocator& alloc);

  template<class Function, class Allocator>
  void defer(Function&& f, const Allocator& alloc);

  execution_context& context() noexcept;
  
  ...
};

1つはNVIDIAからのもの

template<class Executor>
struct executor_traits {
  template<class Function>
  static future<auto> async_execute(Executor& ex, Function f);

  template<class Function>
  static future<auto> async_execute(Executor& ex, Function f, shape_type shape);
  
  template<class Function>
  static auto execute(Executor& ex, Function f);
 
  template<class Function>
  static auto execute(Executor& ex, Function f, shape_type shape);
  ...
};

Googleのものは任意のスレッドプールの共通インターフェースとして設計されており、AsioはExecutorとExecution Contextが分かれていたり、NVIDIAのものはおそらくGPUでの実行を意識してバルク実行インターフェースを備えていたりと、それぞれが必要とするユースケースによって提案されたExecutorの設計は微妙に異なっていました。

これを受けてSG1では、これらのニーズとユースケースを満たしつつそれぞれの提案の問題点を解決するような統一的なExecutorモデルが模索され、2016年10月に最初のUnified ExecutorであるP0443R0 A Unified Executors Proposal for C++が提案されました。

P0443R0はかなりNetworking TSのExecutorの影響が見られるものでしたが、その後議論を経て洗練されていきコンセプトとCPOベースの非常にジェネリックなライブラリとして進化していきました。

2019年7月のSG1の全体投票においてP0443の設計をExecutorライブラリとして承認し、C++20標準への導入を目指してLEWGでのレビューが開始されました。また、先立つ2019年5月には、SG1の投票において将来的にP0443が標準化された時に、Networking TSをP0443ベースに書き換えることに全会一致で合意が取れていました。

C++標準化において、ライブラリ提案は各SG->LEWG->LWGの3段階のレビュープロセスを踏みます。また、各SGでは方向性やコンセプトの決定と最初の設計を行い、LEWGでは設計を集中的にレビューし、LWGでは標準に提案する文書についてのレビューを主に行います。

なかなか巨大なこともあってC++20には間に合わず、その後COVID-19パンデミックが直撃したこともありレビューは遅れ、2020年夏頃からLEWGのリソースを全力投入する形で、P0443をいくつかの部分に分割して個別のグループによってレビューされました。提案が巨大で歴史のあるものであったこともありレビューは困難を極めたようで、Executorの設計とその歴史についてLEWGのメンバに周知を図る必要性が提起されたりしましたが、レビューは為されそこそこの数の問題が報告されました。とはいえP0443を根本的にボツにする致命的なものはなかったようです。

2020年8月ごろには、AsioはP0443のExecutorを実装してリリースされました。これはP0443の実装経験を得るためでもあるようです。

その後2020年12月のLEWGのレビューで、P0443からいくつかの部分が別の提案に分離されることが決定された他はほぼ設計が固まったように見え、一部の人から実装経験について心配する声が上がっていましたが、C++23に向けて作業されていました。

2021年5月ごろにはP0958R3(Networking TSをP0443ベースに書き換える提案)とP1322R3(Networking TSの各APIが任意のExecutorを受け取れるようにする提案)がSG4(Networking Study Group)を全会一致で通過し、LEWGに転送されました。この2つの提案はNetworking TSをP0443に対応させるためのものです。

P2300 std::execution

P0443そのものは半年ほどは目立った(外部から観測できる)動きはなかったのですが、2021年6月、P0443を置き換える新しいExecutorライブラリであるP2300R0 std::executionが提案されました。P2300はP0443をベースとしていくつかの問題を解決した上で、分離して提案されていた非同期アルゴリズムライブラリを含むさらに大きなライブラリで、C++における非同期処理に関する大統一モデルの構築を目指しているものです。

P2300はすぐR1に改稿されLEWGで議論を重ねていたのですが、2021年7月ごろのレビューでP2300のsender/receiverによる非同期モデルとNetworking TS(Asio)の非同期モデルが異なっていることが指摘されました。P2300を大統一非同期モデルとして追求していくなら、Networking TSのExecutor部分に対して大きな変更が必要となります。一部の人は、P2300によるそのような大統一モデルの必要性と有効性に疑問を呈すとともに、Networking TSをP2300から分離すべきだと主張し始めたようです。

2021年8月初旬のLEWGレビューにおける方向性を探る投票においては、大統一非同期モデルの必要性についてはコンセンサスが得られなかったものの、C++標準ライブラリのstructured concurrency保証のソリューションとしてP0443の代わりにP2300を追求する方向性には強いコンセンサスが得られました。

2021年8月中旬のLEWGレビューではP2300の第一ピリオドとして次の改稿のためのまとめが行われ、P2300のモデルを説明するための例の充実やコルーチンとの関係性や既存の標準並列アルゴリズムとどう統合されるかなど、主に説明や解説を追記していくことが指示されました。一方で、大統一非同期モデルについては単一のモデルでカバーするには領域が広すぎるという意見や、P2300はstructured concurrencyと並行性という狭いスコープに焦点を当てて標準化し、標準ライブラリの他の部分(Networking TSなど)は別の非同期モデルを選択できるようにした方が良いのではないかという意見があったようです。

この時点で、Executorの議論は完全にP2300に移り、P0443と関連する提案の追求はストップされました。

2021年9月の1回目のLEWGレビューにおいて、7月ごろに指摘されたNetworking TSとの間の問題について、LEWGのメンバに向けて解説が行われたようです(P2444R0P2463R0)。これらの提案・スライドの作成および説明は、Asioの作者自らによって行われています。これを受けて方向性を探る投票が行われ、Networking TSを今のまま(P2300に対応せず)C++23に導入するコンセンサスは得られませんでしたが、P2300をC++23に導入するコンセンサスも得られませんでした。これは方個性を決定する投票ではないためP2300とNetworking TSがこれで終わるわけではありませんでしたが、ExecutorとNetworkingのC++23入りについて暗雲が立ち込め始めます。

2021年9月の2回目のLEWGレビューではNetworking TSとP2300をどう進めていくかについて本格的に議論されることになり、Networking TSとP2300を切り離した場合に後からP2300とNetworking TSを相互運用可能なようにラッパーを書くことができるか、あるいは(Asioが必要とする)プロパティ指定方法なしでP2300をC++23に導入した場合に後でそれを追加できるか?などが議論されたようです。
ここでも方向性を探るための投票が行われ、大統一非同期モデルの必要性についてはコンセンサスが得られず、Networking TSとP2300のC++23入りについても前回同様の結果となりました。

2021年10月公開の提案文書では、このP2300とNetworkingTSの問題に関連する提案(報告書)がいくつも提出されました。

これを受けて、10月のLEWGレビューでもP2464とP2469について議論されたようです。双方の支持者は、互いが互いの上に構築できる(P2300の上にNetworking TSを構築できるし、Networking TSの上にP2300を構築できる)と主張していて、議論の主題はエラー処理に関することでした。Networking TSのExecutorモデル(およびP0443のexeecutor)はエラー通知に関するチャネルを持たず、特に処理が実行コンテキストに投入されてから実行されるまでの間に起きるスケジューリングエラーをハンドルする方法を提供しないことが懸念されたようです。もう一つの論点はパフォーマンスに関することで、P2300のモデルではsender/receiverオブジェクトの構築に伴う受け入れがたいオーバーヘッドが発生すると考えている人が多かったようです。

これらの問題についてP2300/Networking TSをどう進めていくかの方向性を決定するために、LEWGおよびSG1のメンバで次の項目について投票を行いました

  1. Networking TS/Asioの非同期モデルは、ネットワーキング・並列処理・GPUなどのほとんどの非同期ユースケースの優れた基盤である
    • 弱い否決
  2. P2300の非同期モデルは、ネットワーキング・並列処理・GPUなどのほとんどの非同期ユースケースの優れた基盤である
    • コンセンサス
  3. C++の標準ネットワークライブラリとして、Networking TSを追求するのをやめる
    • 否決
  4. C++標準ネットワークライブラリは、P2300をベースとすべき
    • 弱いコンセンサス
  5. C++標準ネットワークライブラリを、TLS/DTLSサポートなしで出荷することを容認する
    • 否決

2番目の投票は反対9に対して賛成40と大差がついています。3番目の投票は反対16に対して賛成26でしたが、賛成票を投じた人の多くはNetworking TSを白紙に戻したいのではなく、P2300の上に構築したいという意図のようです。

これによって、LEWG/SG1およびSG4の方向性は

  1. Networking TS/Asioの非同期モデルをベースとしてP2300を構築しない
  2. Networking TS/Asioの非同期モデルはネットワーキングのためのモデルとして追及したほうがいい(するなら
  3. LEWGはC++23に向けてP2300の作業を続行する
  4. Networking TSは死んでいない
  5. Networking TSを標準に導入するためには、非同期モデルとセキュリティについての作業が必要となる
    • この作業は重く、C++23に間に合う可能性は低い
  6. Networking TSがP2300ではない非同期モデルを採用するなら、説得力のある提案が必要となる

一応はこれで決着を見たはずです、火種は残ってる気がしますが・・・

この結果に従って、12月まではP2300がLEWGにて引き続きレビューされていますがLWGに進めてはいません。今のところ、Networking TS絡みで大きな問題はないようです。

C++23 Feature Complete

まだ2023年までは時間がありますが、あらかじめ決められているスケジュール(P1000R4)として、C++23に導入する機能の設計は2022年2月7日までに完了していることが求められています。

LEWGの予定表(P2489R0)によれば、P2300のLEWGにおけるレビューの機会はあと一回(2022年1月11日)で、LWGへ進めるための投票の機会もあと一度(2022年1月14日~)です。

前述のように、Networking TSは実質的に作業がストップしており、非同期モデルとセキュリティという2つの重い課題を抱えているので、C++23には間に合わないでしょう。Executorが間に合うかは来年2週目のLEWGのテレコンレビューで設計を完了できるかで決まります。

各ライブラリの雰囲気

経緯はわかりましたがそれを眺めただけではなぜ紛糾していたのかがよくわからないでしょう。ここではP0443, P2300, Asio(Networking TS)の設計の雰囲気を見ていくことで、P2300 vs Asioの背景を探ります。

P0443

P0443におけるExecutorとは次のように使用される何かです。

// P0443のAPIはstd::execution名前空間の下に定義される
using namespace std::execution;

// 任意の場所(たとえばスレッドプール)で処理を実行するexecutorを取得する
std::static_thread_pool pool(16);
executor auto ex = pool.executor(); // この記法はコンセプトによる変数の制約

// 高レベルのライブラリによる処理がどこで実行されるかを制御するためにexecutorを使用する
perform_business_logic(ex);

// あるいは、P0443によるよりプリミティブなAPIを直接使用することもできる

// スレッドプールに処理を投げ、すぐ実行する
execute(ex, []{ std::cout << "Hello world from the thread pool!"; });

// スレッドプールに処理を投げすぐ実行し、完了まで現在のスレッドをブロックする
execute(std::require(ex, blocking.always), foo);

// 依存性のある一連の処理を記述し、後で実行する
sender auto begin    = schedule(ex);
sender auto hi_again = then(begin, []{ std::cout << "Hi again! Have an int."; return 13; });
sender auto work     = then(hi_again, [](int arg) { return arg + 42; });

// 処理の最終結果を標準出力へ出力する
receiver auto print_result = as_receiver([](int arg) { std::cout << "Received " << std::endl; });

// 先ほど定義したworkによる処理をreceiverと組み合わせてスレッドプールで実行する
submit(work, print_result);

ここで、exという変数で示されているものがP0443のExecutorであり、それはexecutorコンセプトを満たす任意の型のオブジェクトです。executorコンセプトは単純には次のようなものです。

template<typename E>
concept executor =
  requires(const E& e) {
    {execution::execute(e, [] -> void {})} -> void;
  };

execution::executeはCPOであり、execution::execute(ex, f);のようにして、Executorexに任意の処理f(引数なし、戻り値なし)を投入します。他にも細かい指定があるのですが、一番重要な性質はこのexecuteCPOによって指定されるものです。

executeCPOによる処理の投入は単純な処理の投入のみをサポートしており、その実行に際しては追加の引数を取らず戻り値を返しません。そのためexecutorは処理の戻り値の取得やエラーハンドル、処理のスケジューリングとそのキャンセルなどの方法を提供せず、その能力を持ちません。

executorはこのようにインタフェースを制限することで、処理が実行されうる任意のハードウェア(CPU/GPU/SIMDユニット/スレッド...)を抽象化しており、それらのハードウェアリソース(まとめて実行コンテキストと呼ばれる)の軽量なハンドルとして統一的なアクセスを提供します。executorに実際に処理がどのように投入されるのか、実行がいつ行われるのか、どのように実行されるのか、はexecutorという抽象の中に覆われます。

// 単純なexecutorの実装例

struct inline_executor {

  template<class F>
  void execute(F&& f) const noexcept {
    // 現在のスレッドで即時実行
    std::invoke(std::forward<F>(f));
  }

  // 実行コンテキストの比較
  auto operator<=>(const inline_executor&) const = default;
};

struct thread_executor {

  template<class F>
  void execute(F&& f) const noexcept {
    // 別のスレッドを起動してそこで実行
    std::thread th{std::forward<F>(f)};
    th.detach();
  }

  auto operator<=>(const thread_executor&) const = default;
};

この2つのexecutor実装を眺めると、それぞれには異なる性質(プロパティ)がある事に気付きます。つまり、execute(ex, f)した時にブロッキングするかしないか、あるいはexecute(ex, f)が戻った時に処理が完了しているかどうかです。このようなexecutorのプロパティは他にもいくつも考えることができて、時にはそのプロパティを制御したくなるでしょう。このようなプロパティはexecutorの最小の契約の範囲外ですが、それを制御するために、require/preferによるプロパティ指定が用意されます。

// 何かしらのexecutorを取得する
executor auto ex = ...;

// 実行にはブロッキングが必要という要求(require
executor auto blocking_ex = std::require(ex, execution::blocking.always);

// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(blocking_ex, execution::priority(p));

// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);

requireによるプロパティの指定は強い要求で、不可能な場合コンパイルエラーとなります。一方、preferは弱い要求であり、不可能な場合でも無視されコンパイルエラーにはなりません。require/preferによるプロパティの指定は、executorをそのプロパティを持つexecutorに変換するものであり、プロパティ指定後得られたものもまたexecutorとして扱うことができます。

executorは処理を投入した後からその処理について何かをハンドルする能力を持ちません。これは基盤的な共通のAPIとしては良いものかもしれませんが、非同期処理グラフを構成し、その実行を制御するための基盤としては表現力が足りません。P0443はゴールとしてそのような非同期アルゴリズムAPIを整えることを目指しており、そのためにexecutorよりも強い保証を持つものと、非同期処理及びそのコールバックに関する共通の基盤が必要となります。

P0443ではそのために、Scheduler、Sender/Receiverという3つの抽象を定義します。

Schedulerとはexecutor同様にschedulerコンセプトを満たす任意の型のことです。

template<class S>
concept scheduler =
  requires(S&& s) {
    {execution::schedule((S&&)s)} -> sender;
  };

(これは簡略化したものですが)定義もexecutor同様に、execution::scheduleCPOによってその実行コンテキストでスケジューリングされるsenderオブジェクトを取得可能な任意の型を指定します。schedulerexecutor同様に実行コンテキストへのハンドルとしての役割を持ちますが、executorとは異なり処理の実行を遅延して実行コンテキストへ投入することができ、またその間のキャンセル操作に対応(sender/receiver経由で)しています。

senderは任意の非同期処理(特に、まだスケジュールされていない処理)を表現するもので、senderコンセプト(この定義はほぼ何も言わないので省略)によって指定されます。scheduleCPOによって得られるsenderというのは、そのscheduler上で実行される何もしない処理を表現しており、それに対して非同期アルゴリズムを適用することで非同期処理グラフを構成していくことができます。

// P0443の目指した世界の一例
sender auto s = just(3) |                                  // 即座に`3`を生成
                via(scheduler1) |                          // 実行コンテキストを遷移(変更)
                then([](int a){return a+1;}) |             // 継続処理をチェーン
                then([](int a){return a*2;}) |             // さらにもう一つ継続処理をチェーン
                via(scheduler2) |                          // 実行コンテキストを遷移(変更)
                handle_error([](auto e){return just(3);}); // エラーハンドル、デフォルト値を返すようにする

int r = sync_wait(s);                                      // 一連の処理を実行し、結果を待機

ただし、これらの非同期アルゴリズムAPIはP0443の一部ではなく別の提案(P1341R0)で提案されているもので、P0443はExecuotrのコアな部分を整備しようとする提案です(した)。

これらの非同期アルゴリズムとは単純には次のように、senderを受け取りsenderを返すものです。

template<typename A>
concept async_algorithm = 
  requires (A alg, sender s) {
    alg(s) -> sender;
  };

パイプライン演算子|)の場合、s | alg -> senderのようになります。つまりは各非同期アルゴリズムは、senderという抽象にのみ入出力を依存することで、前段及び後段の処理が何をするか、あるいは実行コンテキストがどこであるのかということを意識せずに実装できるようになっています。この性質から、非同期アルゴリズムはSenderアルゴリズムとも呼ばれます。

senderコンセプトは構文的に何か特別なことを求めておらず、senderだけでは処理結果の取得すら行うことができません。senderの表現する処理から結果を取得(またはエラーハンドリング、キャンセル)するには、receiverを用います。

template<class T, class E = exception_ptr>
concept receiver =
  requires(remove_cvref_t<T>&& t, E&& e) {
    { execution::set_done(std::move(t)) } noexcept;
    { execution::set_error(std::move(t), (E&&) e) } noexcept;
  };

template<class T, class... An>
concept receiver_of =
  receiver<T> &&
  requires(remove_cvref_t<T>&& t, An&&... an) {
    execution::set_value(std::move(t), (An&&) an...);
  };

receiverコンセプトは2つの部分に分かれていますが、set_done/set_error/set_valueという3つのCPOで使用可能であることを求めています。これらのCPOはそれぞれ、完了(キャンセル)・エラー・処理結果、を通知するチャネルであり、receiverとは3つの通知チャネルを持つコールバックです。これら3つのチャネルは、receiverオブジェクトが破棄される前に必ずどれか1つ呼ばれなければならず、1つが呼ばれたら残りの2つは呼ばれないと仮定することが許されており、この要件をreceiver contractと呼びます。

receiversenderに接続(connectCPO)することでそのsenderの処理結果をハンドルすることができて、receiverを接続されたsenderreceiver contractを履行する義務を負います。senderの処理を実行するには、どこかの実行コンテキストに投入する必要があり、それはstart, sync_waitなどのCPOによって行えます。

sender auto work = just(3) | then([](int n) { return 2*n; });
// この時点では実行されていない

// 処理を実行し完了を待機、receiverは用意される
// schedulerを使用していないので処理は現在のスレッドで、即座に実行される
int n = sync_wait(work);
// 任意のschedulerとreceiver
scheduler auto sc = ...;
receiver auto rec = ...;

sender auto work = schedule(sc) | just(3) | then([](int n) { return 2*n; });
// この時点では実行されていない

// senderとreceiverの接続
operation_state auto state = execution::connect(work, rec);

// 後から実行、scの実行コンテキストで実行される
execution::start(state);
// ブロッキング有無や実行タイミングはsender(work)とscheduler(sc)による
// 結果はrecが(ユーザーが指定した方法で)持っている

// 例えば
int n = rec.value();
// あるいは
if (bool(rec)) {
  int m = *rec;
} else {
  std::cout << rec.err_msg();
}
// などなど・・・

sender/receiverschedulerはお互いにコンセプトという抽象を介してしかお互いのことを知りません。そのため、この例のような処理の構成・実行は特定の型に依存せず、sender/receiverは(コンセプトを満たすように)自由に定義できますし、schedulerも任意のハードウェアのハンドルとなることができます。

多くの汎用非同期アルゴリズムは、同じ実行コンテキストに対して複数の実行状態を持ち得るため、特定の実行コンテキストで完了するsenderを非同期アルゴリズムで受け取って使用するだけでは不十分です。むしろ、非同期アルゴリズムの方をそのようなsender(特定の実行コンテキストで完了するsender)のファクトリに渡すほうが理にかなっており、そのようなファクトリがscheduler(とscheduleCPO)です。

非同期アルゴリズムを複数チェーンする場合、パイプライン演算子|)はsendersenderに対して作用しており、その結果もsenderとなります。多段パイプを適用した時でも得られるものはsenderであり、そこにreceiverを接続するとパイプラインを遡るように各段でそれぞれの非同期アルゴリズムが用意するreceiverが接続されていきます。つまり、パイプラインによる接続部でも後段のsenderは前段のsenderの結果やエラーハンドリングをreceiverを介して行っています。これは複雑ではありますが、<ranges>viewのパイプラインとやっていることは同じで、最終的なsenderオブジェクトおよびそれとreceiverをパッケージングするoperation_stateオブジェクト内部でマトリョーシカのように多段ラッピングされています(thenアルゴリズムの実装例)。

1つ注意点として、sender/receiverおよびoperation_stateなどは関数オブジェクト的なものではないため、そのままexecutorで実行可能ではありません。これらの非同期アルゴリズムによる処理の構成はexecutorではサポートできません。executorのインターフェースでは、結果を受け取ることもエラーをハンドルすることも、処理をキャンセルすることもできないからです。schedulerscheduleを介してその実行コンテキストでの実行と完了を保証するsenderを返すことで任意のreceiverを利用可能となり、それによって3つの通知チャネルを獲得しています。

P2300

P2300はP0443にパッチを当てたものではなく、sender/receiverとSenderアルゴリズムを中心として、それが正しく働くようにP0443を進化・改善したものです。

using namespace std::execution;

scheduler auto sch = thread_pool.scheduler();                     // schedulerの取得

sender auto begin = schedule(sch);                                // schの実行コンテキストで完了するsenderを取得
sender auto hi = then(begin, []{                                  // thenアルゴリズムによる処理のチェーン
    std::cout << "Hello world! Have an int.";
    return 13;
});
sender auto add_42 = then(hi, [](int arg) { return arg + 42; });  // thenアルゴリズムによる処理のチェーン

auto [i] = this_thread::sync_wait(add_42).value();                // 構成した処理を実行コンテキストに投入し、完了を待機する

一見すると先ほど見たP0443のsender/receiverから大きな変化はないように思えます。

P2300のP0443との大きな違いは次のような点です

  • executorコンセプトの削除
    • 処理を投入するだけでは表現力不足であり、sender/receiverと調和しない
    • schedulersenderをベースとするように全体を書き換え
  • プロパティ指定の削除
    • プロパティはscheduler(実行コンテキスト)が備えている性質であり、変更可能ではない
    • 一部のプロパティについては問い合わせが可能
    • 将来的にrequire/preferベースのプロパティ指定を導入することはできる(より便利になれば)
  • Senderアルゴリズムの取り込み
  • senderの保証の強化
    • 特定のsenderが特定の実行コンテキストで完了する保証を追加
      • 実行コンテキストに処理をどう投入するか?という部分の抽象化がexecutorからsenderに移された
      • schedulerは実行コンテキストのハンドルとしてその実行コンテキストのためのsenderを生成する、senderファクトリの1つに過ぎない
    • sender型のconnect()オーバーロードconnectCPOの呼び出し先)を右辺値と左辺値で分けることで実行可能回数を表現する

executorの担っていた役割はschedulersenderへ移管されると共に、sender/receiverとSenderアルゴリズムがExecutorライブラリの中心に据えられた形です。とはいえ、sender/receiverとSenderアルゴリズムの雰囲気はP0443の頃から大きく変わってはいません。

P2300R3には、さらに多くのサンプルコードが掲載されています。眺めるとsender/receiverとSenderアルゴリズムの雰囲気をよりつかめるかもしれません。

Asio(Networking TS)

Networking TSはAsioをベースとしていますが、AsioはP0443を取り込む形で進化している一方でNetworking TSは2018年以降更新がないため、設計が少し変わっている部分があります。ここでは、現在のAsioを見てみます。

Asioにおける非同期処理は通常、I/O操作(主にネットワーク処理)の完了後にその結果を受けるコールバックとして起動されます。それらの機構をまとめて非同期操作(Asynchronous operation)と呼び、Asioにおける非同期操作は開始関数(Initiating function)と完了ハンドラ(Completion handler)からなります。非同期操作はAsioにおける非同期モデルの基本単位となります。

開始関数とは非同期I/O関数(soket.async_read_some()とかasio::async_write()など)のことで、完了ハンドラはそこに渡されるコールバック関数です。

// 呼び出しはすぐにリターンする
socket.async_read_some(buffer,
  [](error_code e, size_t) {
    // bufferにsocketからのデータを読み込んだ後、呼び出される
    ...
  }
);

Asioの非同期操作は、このような開始関数の行うI/O処理と完了ハンドラの呼び出しが、同期的に(シングルスレッドで順番に)実行された場合と同じように呼び出されることを保証します。それによって、開始関数(非同期I/O)の実行とコールバックの実行ではリソースの使用がオーバーラップする事がありません。そのため、完了ハンドラではそれを開始した非同期I/Oそのものを再帰的に呼び出した時でも、解放されないリソースが蓄積してリソース使用量が増大する事はありません。

// async_read_someの簡易実装例
// async_read_someはこのような実装を別スレッドで呼び出してリターンする
void sokcet::async_read_some_impl(Buffer buffer, CompletionHandler handler) {
  // ソケットからのデータ読み出し処理
  // ブロッキングと、リソースの確保など
  error_code ec{};
  std::size_t len = this->io_read(buffer, ec);  // 説明のための読み込み操作

  // データの読み出しとbufferへの格納の完了後、完了ハンドラを呼び出す
  async(std::move(handler), std::move(ec), len);
}

すなわち、完了ハンドラは開始関数の末尾で(おそらく非同期的に)呼び出され、完了ハンドラの実行が始まった時には開始関数は完了しておりそこで確保されたリソースは解放されています。すなわち、完了ハンドラが呼び出された時には開始関数は終了していることが保証され、完了ハンドラはI/O操作の詳細とほぼ無関係に実行されます。

完了ハンドラはさらに継続して別の非同期操作を実行することができます。それら非同期操作のチェーン全体およびそれらチェーンの進行を保証する仕組みのことを非同期エージェント(Asynchronous agent)と呼びます。非同期エージェントは概念的な存在というか、何か特定のライブラリ実体に結びつくものではなく、Asioの非同期モデルに組み込まれたある種のシステムです。

socket.async_read_some(buffer,
  [&socket, &buffer](error_code e, size_t) {
    // ...
    
    socket.async_read_some(buffer, [&socket, &buffer](error_code e2, size_t) {
      // ...

      socket.async_read_some(buffer, [&socket, &buffer](error_code e2, size_t) {
        // ここまで(およびこの先も)きちんと実行されることを保証する機構が実行エージェント

        // ...
      }
    };
  }
);

実行エージェントは複数の非同期操作からなり、実行エージェントのそれぞれの非同期操作内においての各完了ハンドラは、スケジューリング可能な作業の最小単位を表します。

非同期エージェントには、その一部となっている非同期操作がどのように動作するべきかを指定する次の3つの特性が関連づけられています

  • Allocator
    • 非同期操作がどのようにメモリリソースを取得するかを指定する
  • Cancellation slot
    • 非同期操作がどのようにキャンセル操作をサポートするかを指定する
  • Executor
    • 各完了ハンドラがどこで、どのように実行されるかを指定する

非同期エージェント内の各非同期操作が実行される時、その実装はこれらの関連づけられた特性を照会しそれらの指定する要件を満たすために使用します。非同期操作内では、完了ハンドラにassociator traitsを適用することでそのようなクエリを実行します。

// async_read_someの簡易実装例
// async_read_someはこのような実装を別スレッドで呼び出してリターンする
void sokcet::async_read_some_impl(Buffer buffer, CompletionHandler handler) {
  // 関連づけられたアロケータの取得
  auto alloc = associated_allocator<CompletionHandler>::get(handler);
  // 関連づけられたcancellation slotの取得
  auto cancel = associated_cancellation_slot<CompletionHandler>::get(handler);

  // ソケットからのデータ読み出し処理
  error_code ec{};
  std::size_t len = this->io_read(buffer, ec, alloc, cancel);

  // 関連づけられたExecutorの取得
  auto ex = associated_executor<CompletionHandler>::get(handler);
  // exの場所でexによって指定された方法で完了ハンドラを実行
  dispatch(ex, std::move(handler), std::move(ec), len);
}

associator traitsassociated_xxx)は完了ハンドラ型(CompletionHandler)について特殊化することでユーザーがカスタマイズすることができます。

全ての非同期エージェントには関連づけられたExecutorがあり(カスタマイズしない場合でもデフォルトのものが関連づけられる)、非同期エージェントのExecutorは非同期エージェントの完了ハンドラがどのようにキューイングされ、どのように実行されるかを決定します。非同期エージェント内の非同期操作は、非同期エージェントに関連づけられたExecutorを使用して次のようなことを行います

  • 非同期操作の実行中、その非同期操作を表す作業の存在を追跡する
  • 非同期操作の完了時、完了ハンドラを実行するためにキューイングする
  • 完了ハンドラがリエントラントに実行可能ではないことを保証する
    • こうしない場合、完了ハンドラの2回以上の呼び出しによって意図しない再起やスタックオーバーフローに陥る可能性がある

このように、非同期エージェントに関連づけられたExecutorは、その非同期エージェントがいつ、どこで、どのように実行されるべきかというポリシーを表し、それを非同期エージェント全体にわたって横断的に指定します。

AsioにおけるExecutorの使用例としては

  • 共有データ構造上で実行される非同期エージェントのグループを調整し、各エージェントの完了ハンドラが同時に実行されないようにする
  • データまたはイベントソース(NICなど)に近い特定の実行リソース(CPU)上で非同期エージェントを実行する
  • 関連のある非同期エージェントのグループを示し、スレッドプールがよりスマートなスケジューリングを行えるようにする(実行リソース間でエージェントを移動するなど)
  • GUIアプリケーションのメインスレッドで実行される完了ハンドラを1つのキューに入れていくことで、UIを安全に更新する
  • ロギング、ユーザー認証、例外処理など、完了ハンドラの実行前後に何か処理を実行する
  • 非同期エージェントとその完了ハンドラの実行優先度を指定する
  • デフォルトのExecutorを使用することで、非同期操作完了のトリガーとなったイベントから最速のタイミングで完了ハンドラを起動する

などがあります。ただし、AsioにおいてユーザーがExecutorをカスタムすることは稀であり、基本的にはExecutorの存在やそのカスタムを意識する必要はありません。

AsioにおけるExecutorは開始関数(非同期I/O操作)の末尾で、完了ハンドラの実行をカスタマイズするポイントであることから、tail callのカスタマイゼーションポイントと呼ばれます。

tail callの通り、完了ハンドラの実行時点で呼び出した開始関数は完了しているため、呼び出しに伴うエラーを処理したり完了ハンドラの結果を受けたりするのは後続の処理の責任となり、Executorを使用して呼び出した側(開始関数)はそれ以降の完了ハンドラで起こることに責任を持つ必要がなく、完了ハンドラを実行コンテキストへ送信する役割だけを負います。これは明らかにP0443のexecutorと親和性があり、実際追加のプロパティ指定を必要とするものの、Asioが元々持っていたExecutorはP0443のexecutorによって表現可能です。一方で、P2300のschedulerexecutorの役割をsenderと分割して請け負っているなど、AsioのExecutorと互換性がありません。

tail callのカスタマイゼーションポイントはC#(.Net)ではSynchronizationContextとして知られており、細部は異なるものの他のところ(SwiftのExecutorJava Netty libraryのEventExecutor)でも同様のものが見られます。このことは、AsioのExecutorモデルが決して的外れなものではないことを示しているといえます。

ここまでのことは、Networking TSとAsioで大きく変わらない事です。
C++11のstd::futureC++20のコルーチン、P0443のSenderアルゴリズムなど、C++の進化に伴ってAsioはコールバック以外の非同期の継続メカニズムに対応する必要が出てきました。既存のコードに対する互換性を維持しつつAsioの非同期モデルに種々の非同期継続メカニズムを親和させるための仕組みが完了トークン(Completion token)です。

// コールバック
socket.async_read_some(buffer, [](error_code e, size_t) {
  // 継続作業...
});

// コルーチン
awaitable<void> foo() {
  size_t n = co_await socket.async_read_some(buffer, use_awaitable);
  // 継続作業...
}

// future
future<size_t> f = socket.async_read_some(buffer, use_future);
// 継続作業...
size_t n = f.get();

// fiber
void foo() {
  size_t n = socket.async_read_some(buffer, fibers::yield);
  // 継続作業...
}

完了トークンは開始関数(async_read_some)の最後の引数で受け取っているもの(ラムダ、use_awaitableuse_futurefibers::yield)です。つまり、完了トークンとは完了ハンドラを一般化したものです。トークンの名の通り必ずしも呼び出し可能なものでなくてもよく、そのトークンに応じて非同期操作(完了ハンドラを含む)の実行方法を柔軟にカスタマイズすることができます。

完了トークンを受け取る非同期操作では完了ハンドラの関数型(completion signature)を用意しておき、非同期操作の開始関数はcompletion signature、完了トークン、自信の内部実装をasync_result型(traitクラス)に渡します。async_resultはこれらの情報から具体的な完了ハンドラの作成と非同期操作の起動を行うカスタマイゼーションポイントです。

// 完了トークンを用いるasync_read_some実装例
template <class CompletionToken>
auto async_read_some(tcp::socket& s, const mutable_buffer& b, CompletionToken&& token)
{
  // 非同期操作を開始するための関数オブジェクトの定義
  auto init = [](auto completion_handler,   // async_resultから渡される完了ハンドラ
                 tcp::socket* s,            // async_resultから渡される追加の引数
                 const mutable_buffer& b)   // async_resultから渡される追加の引数
  {
    // 切り離されたスレッドで非同期操作を実行する
    std::thread(
      [](auto completion_handler,
         tcp::socket* s,
         const mutable_buffer& b)
      {
        error_code ec;
        size_t n = s->read_some(b, ec);

        std::move(completion_handler)(ec, n); // 完了ハンドラ呼び出し
      },
      std::move(completion_handler),
      s,
      b
    ).detach();
  };

  // 非同期操作の起動と適切な戻り値の返却
  return async_result<decay_t<CompletionToken>, // 完了トークンの型
                      void(error_code, size_t)  // completion signature
                     >::initiate(init,
                                 std::forward<CompletionToken>(token),  // 完了トークンはasync_result内で適切な完了ハンドラに変換される
                                 &s,  // 非同期操作に必要な追加の引数
                                 b);  // 非同期操作に必要な追加の引数
}

async_resultはAsioに予め用意されているクラスです。完了トークンとしてラムダなど呼び出し可能なものが渡された場合、それが完了ハンドラとしての要件を満たしていればデフォルトの実装が使用され、それは従来(上で説明したこと)と同じ意味や保証を持つ非同期操作の実行と完了ハンドラの呼び出しを行います。

// 完了ハンドラ用のデフォルト実装、引数を単純に転送するだけ
template <class CompletionToken, completion_signature... Signatures>
struct async_result {

  template<class Initiation,
           completion_handler_for<Signatures...> CompletionHandler,
           class... Args>
  static void initiate(Initiation&& initiation,
                       CompletionHandler&& completion_handler,
                       Args&&... args)
  {
    std::forward<Initiation>(initiation)(std::forward<CompletionHandler>(completion_handler),
                                         std::forward<Args>(args)...);
  }
};

完了ハンドラは初期化関数の完了と共に即座に実行され、初期化関数そのものも即座に開始されるため、ここでは全ての引数のコピーが回避されます。しかし、遅延完了トークン(use_awaitableなど)では初期化関数の遅延起動のために引数をキャプチャしておく必要があります。例えば、単純なdeferredトークン(deferred_t deferred{};、操作をパッケージングするだけ)に対するasync_resultの特殊化は次のようになります。

template <completion_signature... Signatures>
struct async_result<deferred_t, Signatures...> {

  // initiate()の戻り値=非同期操作の直接の戻り値は、完了トークン1つを受けて非同期操作を遅延実行する関数オブジェクト
  template <class Initiation, class... Args>
  static auto initiate(Initiation initiation, deferred_t, Args... args) {
    return [initiation = std::move(initiation),             // 開始関数の実装詳細
            arg_pack = std::make_tuple(std::move(args)...)  // 非同期操作の追加引数をtupleで固めてキャプチャ
           ](auto&& token) mutable
    {
      return std::apply(
        [&](auto&&... args) {
          // 非同期操作を起動する
          return async_result<decay_t<decltype(token)>, Signatures...>::initiate(
            std::move(initiation),
            std::forward<decltype(token)>(token),
            std::forward<decltype(args)>(args)...
          );
        },
        std::move(arg_pack)
      );
    };
  }
};

// 例えば次のように使って
auto def = socket.async_read_some(buffer, deferred);
// 後から実行、その際にもう一度完了トークンを指定できる
size_t n = co_await def(use_awaitable);

完了トークンは任意にユーザーが定義することができて、完了トークン型と期待する完了ハンドラのシグネチャasync_resultを特殊化し、そのinitiate()内でそのトークンの振る舞いを記述することでAsioの非同期操作の実行と完了ハンドラの呼び出しを制御することができます。

このような完了トークンはAsioに対してBoost1.54(2013年ごろ)で導入され、今の形になったのはBoost1.70(2019年5月)からのようです。

完了トークンを用いるとSenderアルゴリズムlikeな非同期アルゴリズムを構成することができます。

auto f = socket.async_read_some(buffer(data), use_then)
  .then([&](error_code e, size_t n) {
    return async_write(socket, buffer(data, n), use_then);
  })
  .then([&](error_code e, size_t n) {
    return async_write(socket, buffer("\r\n", 2), use_then);
  })
  .then(use_future);

これはuse_thenトークンに対してasync_resultを特殊化するという同様の手順で実装できます(P2463の後ろの方に実装例があります)。しかも、完了トークンを用いることでコルーチンなど他の継続スタイルと自然に同居することができています。

次の図はAsioの非同期モデルをまとめてその関係性を表したものです。

現在のAsioはすでにこのような非同期モデルに基づいて構築されており、部分にもよりますが数年〜10年以上の実装経験を持っています。

Networking TS(Asio)にとって問題だったこと

  • Asioの要求するExecutorはtail callのカスタマイゼーションポイントであり、schedulerと異なる
  • AsioのExecutorに対する操作(dispatch/post/defer/)には少なくともExecutorのブロッキングプロパティを制御できる必要があるが、P2300で削除されている
  • Asioには10年以上の、P0443の部分も1年の実装経験があるが、P2300には実装経験がない
    • 従って、P2300をベースとするようにAsio(Networking TS)を書き換える事は実装経験の不足から好ましくない
    • libunifexではP2300にあるSenderアルゴリズムの一部が未実装
  • 他の継続スタイル(コルーチンやfutureなど)のサポートがない
  • メモリ割り当て戦略についての考慮が不足している
    • Asioには何十年にもわたって研究されてきた割り当て戦略がある

一部は実際には正しくないものもありますが、実装経験とAsioのExecutorとP2300のschedulerのミスマッチはかなり大きな問題です。

とはいえ経緯のところで見たように、LEWGとしてはP2300をC++23に向けて詰めていく方向性は確定しており、これらの問題が考慮されることはあっても、これらの問題によってP2300が止まることはなさそうです(多分executorが復活することもないでしょう)。一方で、Netowrking TSはExecutorの部分についてP2300ベースとするか今のまま行くのか(あるいは別のアプローチを取るのか)決定しなければならず、どちらにしても困難が待ち構えていそうです・・・

参考文献

この記事のMarkdownソース

[C++]std::stringをキーとする(非順序)連想コンテナでHeterogeneous Overloadを有効化する

この記事はC++ Advent Calendar 2021の17日目の記事です。

Heterogeneous Overload?

Heterogeneous Overloadというのは、(非順序)連想コンテナ(C<Key, ...>)の操作においてKeyと異なる型のオブジェクトをとることのできるオーバーロードのことです。

例えばstd::map::find()C++14から次の2つのオーバーロードを持っています(constは無視します)

iterator find(const key_type& x);             // (1)

template <class K>
iterator find(const K& x);                    // (2) C++14

ここでkey_typeとはstd::map<Key, Value>Key型であり、2つ目のオーバーロードKeyと異なる型の任意のオブジェクトを受け取るようになっています。これがHeterogeneous Overloadであり、keyと比較可能な別の型のオブジェクトを直接用いて値を検索することができ、使いやすさが向上するだけでなく、Keyの一時オブジェクトを作る必要がないため効率的です。

ところが、Heterogeneous Overloadを使用するためにはKeyと比較可能な型の値を渡すだけではだめで、連想コンテナならば比較クラスCompareに、非順序連想コンテナならばそれに加えてハッシュクラスHashに、is_transparentメンバ型が定義されていて、Keyと異なる型との直接比較および一貫したハッシュ生成をサポートしている必要があります。。

悲しいことに、互換性のためにデフォルトの比較クラス(std::less<Key>, std::equal_to<Key>)は比較がテンプレートではないため異なる型との直接比較ができず、ハッシュクラス(std::hash<Key>)にはis_transparentメンバ型が定義されていないため、デフォルトではHeterogeneous Overloadは有効になりません。そのため、Heterogeneous Overloadを有効化するためには、自分で要件が満たされるように一工夫しなければなりません・・・

ここでは、一番需要が高くてよく出会いそうなstd::stringをキーとする(非順序)連想コンテナについて見ていきます。

std::map<std::string, T>

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>

using namespace std::literals;

int main() {
  std::cout << std::boolalpha;

  std::map<std::string, int> map = { {"1", 1}, {"2", 2}, {"3", 3}};

  std::cout << (map.find("1"sv) != map.end()) << std::endl; // エラー
  std::cout << (map.find("4") != map.end()) << std::endl;   // OK、ただし一時オブジェクトが作成されている
}

std::string_viewをそのまま受け取れないのは、std::string_viewから変換するstd::stringのコンストラクタにexplicitが付加されているためです。静かなパフォーマンス低下を長文コンパイルエラーで教えてくれるのでとても親切だといえるでしょう・・・

std::unordered_map<std::string, T>

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>

using namespace std::literals;

int main() {
  std::cout << std::boolalpha;

  std::map<std::string, int> map = { {"1", 1}, {"2", 2}, {"3", 3}};

  std::cout << (map.find("1"sv) != map.end()) << std::endl; // エラー
  std::cout << (map.find("4") != map.end()) << std::endl;   // OK、ただし一時オブジェクトが作成されている
}

これもエラーが起きてるのはmapと同じ理由です。

連想コンテナ

連想コンテナ(std::set, std::mapとそのmultiバージョン)の場合、問題となるのはデフォルトの比較クラスがテンプレートパラメータに指定された型の比較しかできずis_transparentを持っていないことだけです。デフォルトの比較クラスstd::less等はなぜかvoidに対する特殊化に対してのみis_transparentが定義される(比較がテンプレートになる)のでそれを使ってもいいですし、C++20からならstd::rangesにある比較クラスを使ってもいいでしょう。

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
#include <functional> // ranges::lessとかはここにある

using namespace std::literals;

int main() {
  std::cout << std::boolalpha;

  std::map<std::string, int, std::ranges::less> map = { {"1", 1}, {"2", 2}, {"3", 3} };

  std::cout << (map.find("1"sv) != map.end()) << std::endl; // OK
  std::cout << (map.find("4") != map.end()) << std::endl;   // OK
}

これだけで連想コンテナはHeterogeneous Overloadを有効化できます。当然どちらのケースでもstd::stringの一時オブジェクトは生成されておらず、直接比較されています(はずです)。std::ranges::lessの代わりにstd::less<void>を使用しても大丈夫です、お好みでどうぞ。

std::ranges::mapみたいな感じでこれを標準で用意してほしいものです。

非順序連想コンテナ

非順序連想コンテナ(std::unorderd_map, std::unorderd_setとそのmultiバージョン)の場合、比較クラスに加えてハッシュクラスも問題となります。残念ながらハッシュクラスに関しては自分で定義をするしかありません・・・

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
#include <functional>

using namespace std::literals;

struct string_hash {
  using hash_type = std::hash<std::string_view>;
  using is_transparent = void;

  std::size_t operator()(std::string_view str) const {
    return hash_type{}(str);
  }
};

int main() {
  std::cout << std::boolalpha;

  std::unordered_map<std::string, int, string_hash, std::ranges::equal_to> map = { {"1", 1}, {"2", 2}, {"3", 3} };

  std::cout << (map.find("1"sv) != map.end()) << std::endl; // OK
  std::cout << (map.find("4") != map.end()) << std::endl;   // OK
}

少し面倒ですが、std::hashをそのまま利用してやればハッシュを独自実装する必要はありません。ハッシュクラス(string_hash)のoperator()の引数型をstd::string_viewにしておけば、const char*, std::string, std::string_viewの3つを受け取ることができます。

std::string系の文字列クラスならこうすればいいのですが、他の型の場合はハッシュ共通化が難しい場合もあるかもしれません。そういう場合は自分でハッシュ実装をする必要がありそうです。

const char*, std::string, std::string_viewのハッシュ一貫性保証

ところで、上記のようにconst char*, std::stringハッシュ値std::string_viewから計算することに問題はないのでしょうか?

これは保証されていて、[basic.string.hash]/1によれば

If S is one of these string types, SV is the corresponding string view type, and s is an object of type S, then hash<S>()(s) == hash<SV>()(SV(s)).

とあります。

要は、事前定義されている各種std::basic_stringハッシュ値と、それに対応するstring_viewハッシュ値は一致すると言っています。

また、[string.view.hash]/1にも

The hash value of a string view object is equal to the hash value of the corresponding string object ([basic.string.hash]).

と、同じことを逆方向から確認しています。const char*の文字列もstd::string/std::string_viewを経由して考えれば同じ結論となるので、各種文字列のハッシュ値はその型によらず一致することが分かります。

どうやらこれはC++17から規定されたもののようですが、この規定によって変更が必要になるとすればそれは破壊的となるはずなので、おそらく実装がそうなっていたことを規格に反映しただけで、以前から同様の保証は暗黙的に存在していたと思われます。

(非順序)連想コンテナのHeterogeneous Overload

Heterogeneous Overloadは、連想コンテナの検索系の関数(find(), count(), lower_bound(), upper_bound(), equal_range())に対してC++14で追加され(N3657)、続いてC++20にて非順序連想コンテナの検索系関数に対しても追加されました(P0919)。

C++23ではさらに、全ての連想コンテナの削除(erase())とノードハンドルの取り出し(extract())に対してもHeterogeneous Overloadが追加されました(P2077)。

さらに、挿入系の操作(insert(), insert_or_assign(), try_emplace(), bucket())についてもHeterogeneous Overloadを追加する提案が進行中で(P2363)、まだ入ってはいませんがC++23を目指しています。これが入るとKeyを指定するタイプの操作のほぼ全てでHeterogeneous Overloadを使用可能となるため、それにアダプトする方法およびstd::stringstd::string_view使用時の問題について頭の隅にとどめておく重要性が高まるでしょう。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で43本あります。

P0009R14 MDSPAN

多次元配列に対するstd::spanである、mdspanの提案。

以前の記事を参照

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

  • std::spanと調和するように変更
  • レイアウトマッピングクラスの変換コンストラクタを、暗黙変換できないextentによってexplicitになるように修正
  • mdspanの変換コンストラクタを、保持するメンバとテンプレートパラメータが明示的に変換可能であるかによってexplicitになるように修正
  • submdspanの文言改善
  • レイアウトについての文言の一貫性改善
  • アクセサクラスとマッピングクラスからのデフォルト構築可能性を削除
  • layout_strideの変換コンストラクタを修正
  • extents/mdspanの整数値からの推論補助にexplicitを追加
  • 要素型とextentを含まないようにmdspanの制約を調整
  • 機能テストマクロの追加
  • submdspanのsubslice引数をpairの代わりにtupleを取るように変更
  • mdspan::unique_sizeを削除
  • 整数型のstride配列に対して柔軟になるように、layout_strideのコンストラクタを修正
  • extent/mdspanのコンストラクタがrank_dynamicまたは整数rank指定値(あるいはそのサイズの配列)のいずれかを受けられるように変更
  • mdspantrivially default constructibleであるという指定を削除
  • mdspanがnon-owningであるという単語を削除(例えば、shared_ptrをポインタとして使用する事ができる)
  • 1次元layout_leftからlayout_rightの相互変換を追加
  • ランク0のlayout_left, layout_right, layout_strideそれぞれの間の暗黙変換を許可

などです。

この提案はC++23入りを目指して、LEWGからLWGへ転送する採決を取るために12月のLEWG投票待ちをしています。

P0323R11 std::expected

エラーハンドリングを戻り値で行うための型、std::expected<T, E>の提案。

以前の記事を参照

このリビジョンでの変更は、std::unexpectedの変換コンストラクタを削除した事、制約と効果を全体的に合理化した事、expected<void, E>のために部分特殊化を定義した事、などです。

この提案は、C++23入りを目指してLWGでのビュー中です。

P0447R17 Introduction of std::hive to the standard library

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

以前の記事を参照

このリビジョンでの変更は、使用経験とconstexprの検討について追記した事です。

P0533R9 constexpr for cmath and cstdlib

<cmath><cstdlib>の一部の関数をconstexpr対応する提案。

以前の記事を参照

このリビジョンでの変更は、「non-constant library call」という用語を導入して、Cライブラリ関数がnon-constant library callとなる条件とnon-constant library callがコア定数式ではないことを追記する形で、提案する文言を改善したことです。

non-constant library callはFE_INEXACT以外の浮動小数点例外を発生させるようなC標準ライブラリ関数の呼び出しとして規定され、コア定数式となる(non-constant library callではない)C標準ライブラリ関数の呼び出しセマンティクスは、その呼び出しの引数型の浮動小数点数型に対して適用可能なC標準のAnnex Fで規定されたもの、として指定されています。また、math_errhandling & MATH_ERRNO == trueとなる場合のC標準ライブラリ関数の呼び出しもまたnon-constant library callとなります。

すなわち、浮動小数点環境に影響を与えるorの影響を受けるような<cmath><cstdlib>)の関数の呼び出しは、定数式で実行不可となるようになります。

P1413R3 Deprecate std::aligned_storage and std::aligned_union

std::aligned_storagestd::aligned_unionを非推奨とする提案。

std::aligned_storagestd::aligned_unionを含めた在野の類似のものは、次のような問題があるため本質的に危険だと思われます

  1. その使用は未定義動作を引き起こす
    • これらの型はストレージを提供するのではなく、指定されたサイズをもつ型を提供する
  2. 保証が間違っている
    • 規格では型が指定されたサイズ以上であることを指定しているだけで、サイズ上限が無い
  3. APIが適切ではない
  4. APIが適切ではないため、利用にあたって同じような事前作業が繰り返される

API選択の間違い

std::aligned_storagestd::aligned_unionは共通して次のような問題があります。

  1. その領域上に構築された値のアクセスにはreinterpret_castする必要がある
    • constexpr化できない
    • 未定義動作を容易に引き起こす
  2. ::typeが自動解決されない
  3. ::typeのサイズに上限が無い

2つ目の問題は意味が分かりづらいですが、std::aligned_storagestd::aligned_union)は::typeとして指定されたサイズとアライメントをもつ型を提供するものです。従って、std::aligned_storagestd::aligned_union)のオブジェクトを作成しても意味はなく、さらに間違ってそれを使用してしまうと悲惨なことになります。これは、std::aligned_storage_tstd::aligned_union_t)を使用すれば防止できますが、それが提供されていてもstd::aligned_storagestd::aligned_union)を直接使うという間違いを阻止する手段がありません。ここに1つ目の問題が重なり、間違って使用されても気づけない可能性があります。

3つ目は単に標準の規定の欠陥です。どちらも規定では::typeは少なくとも要求されたサイズ以上であることを指定しており、その上限は指定されていません。それによって、必要以上のメモリが想定外に使用される可能性があります(特にstd::aligned_storagestd::aligned_union)を配列の要素にした場合に影響が大きくなる)。

std::aligned_storageの問題

さらに、std::aligned_storageに固有の次のような問題があります

  1. テンプレート引数として構築したい型Tを直接取らない
  2. 第二引数(アライメント指定)にデフォルト引数が指定されている

std::aligned_storageはテンプレート引数として2つのstd::size_t値を取ります。1つ目は領域の最小サイズ指定、2つ名は領域のアライメント指定です。しかし、2つ目のアライメント指定が1つ目のサイズ指定と無関係に指定されることはまれであり、std::aligned_storageの用法を考えればむしろ構築したい型Tは固定で、std::aligned_storage<sizeof(T), alingof(T)>と指定するのが適切なはずです。

このように、現在のAPIは本来必要な構築したい型Tを取らないだけでなく、アライメント指定にはデフォルト値が指定されています。オーバーアラインされた型をサポートする必要はなく、デフォルト値が有効なのはそれが適正であることをたまたま利用者が知っている場合だけです。

Folly/Boost/Abseilの3つのライブラリにおけるaligned_storagestd::aligned_storagelikeなものも含む)の使用を調査したところ、95例のうち69例でaligned_storage<sizeof(T), alingof(T)>のように使用されていたようです。他にもインターネット上で検索可能なところでも同様の用法が支配的であることが確認できます。

std::aligned_unionの問題

std::aligned_unionにも固有の次のような問題があります

  1. 第一引数(サイズ指定)は無意味
  2. サイズとアライメントの推論がstd::aligned_storageと一貫していない

std::aligned_unionはサイズパラメータと可変長の型のリストを取り、それらの型の中の最大のサイズとアライメントを使用したストレージ型を用意します。第一引数のサイズ指定はstd::aligned_unionの最小サイズ指定であり、引数リストの全ての型のサイズがその値よりも小さい時でも、std::aligned_unionの提供する型のサイズはそれ(第一引数)よりも小さくなりません。

しかし、この最小サイズが必要になるのは非常にまれであり、ほとんどの場合はstd::aligned_union<0, Ts...>のように使用されます。この0の指定の意味はstd::aligned_unionを使い慣れていない場合には分かりづらく、その意図がサイズ0の型が欲しいのか単にAPIを満足するためだけに指定されているのか解読するのは困難です。

そして、std::aligned_unionが領域サイズとアライメントを勝手に計算してくれるのはいいことではありますが、そのことがstd::aligned_storageAPIと逆になっています。これはstd::aligned_union<0, T>の様な使用法(サイズとアライメントを自動で求めてほしい)につながり、このコードを書いた人以外の人が見た時に、std::aligned_storage<sizeof(T), alingof(T)>の代わり使用しているのか、将来型を追加することを見越しているのか、APIの矛盾という前提によってその意図を把握することは困難となります。

これらの問題から、この提案ではstd::aligned_storagestd::aligned_unionを非推奨にしようとしています。

また、可能であればstd::aligned_storageは次のように置き換えることを推奨しています。

namespace std2 {
  template <typename T>
  using aligned_storage = alignas(T) std::byte[sizeof(T)];
}

ただし、alignas(T)の指定はusing宣言では意味がなく正しく機能しないため、この提案ではこのような代替を導入することは提案していません。代わりに、ユーザーに対して現在std::aligned_storagestd::aligned_unionを使用しているところを次のように置換することを推奨しています

template <typename T>
class MyContainer {
  // ...
  
private:
  //std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
  alignas(T) std::byte t_buff[sizeof(T)];
  
  // ...
};
template <typename T>
class MyContainer {
  // ...
  
private:
  //std::aligned_union_t<0, Ts...> t_buff;
  alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
  
  // ...
};

こうしたときでもreinterpret_castの使用は避けられませんが、既にそれが必要とされるところで引き続き必要になるだけなので、新規に導入するよりも悪影響は少ないはずです。

この提案はすでにLWGのレビューを終え、次の全体会議で投票にかけられることが決まっています。

P1467R6 Extended floating-point types and standard names

P1467R7 Extended floating-point types and standard names

C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。

以前の記事を参照

R6での変更は、

  • SG22/EWGでの議論に基づいて、通常の算術変換(usual arithmetic conversion)のルールをC23の動作と一致するように変更
  • オーバーロード解決のセクションを大幅に変更し、提案する方向性を「最小の安全な変換を優先」から「同じ変換順位を優先」に変更
  • std::is_extended_floating_pointなどの型特性を削除
  • 拡張浮動小数点数型のエイリアスなどを配置するヘッダを<stdfloat>に変更
  • C23の_FloatN_t名に関する説明を追記
  • エイリアスとそのリテラルサフィックスについて予備的な文言を追加

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

  • 拡張浮動小数点数型のためのリテラルサフィックスを言語機能として定義するように変更
  • 機能テストマクロを追加
  • C23の_FloatN_t名がC++でも使用可能であることを要求しないことを決定
  • IEEE/IEC浮動小数点数標準を参考文献(bibliography)から参照規格(normative reference)へ移動
  • タイプエイリアスの順番を論理的な順序になるように並び替えた

などです。

P1494R2 Partial program correctness

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

現代のコンパイラは未定義動作を活用して(未定義動作が現れないことを前提として)積極的な最適化を行うことがあります。それが起こると、未定義動作を回避するためのチェックやテストのコードをコンパイラが排除することがあります。

#include <cstdio>
#include <cstdlib>

static void bad(const char *msg) {
  std::fputs(msg, stderr);
#ifdef DIE
  std::abort();
#endif
}

void inc(int *p) {
  if(!p) bad("Null!\n");  // #1
  ++*p;
}

このコードでは、DIEマクロを事前定義していない場合に#1の行を削除する最適化が可能です(ただし、現在これを行うコンパイラはないようです)。なぜなら、#1の分岐はtrue/falseどちらのパスを通ったとしても次の行の++*p;に結局は到達し、pnullptrである場合のデリファレンスは未定義動作であるため、コンパイラpnullptrとならないと仮定することができ、遡って#1if(!p)は常にfalseとなるためtrueの分岐は実行されない、と導くことが可能だからです。「pnullptrである場合のデリファレンスは未定義動作であるため、コンパイラpnullptrとならないと仮定することができ」のような無茶な導出を支えているのは、未定義動作を含むプログラムは全体が未定義動作になるという規定([intro.abstract]/5)により、それによって未定義動作が起こりうる時にそれが起こらないとみなしてプログラムを書き換える最適化が許可されます。

なお、DIEマクロが定義されていればbad()の実行は戻ることがなく、従ってif(!p)trueとなる分岐はそこで実行が終了するため先ほどのような推論はできなくなります。

このような最適化あるいは未定義動作は、C++20で導入されかけていた契約プログラミングサポートを取り下げさせた原因の一つとなりました

void f(int *p) [[expects: p]] [[expects: *p<5]];

C++20契約プログラミングでは、契約条件が破られている時でも実行を継続する継続モードという実行モードが規定されており、その場合には1つ目の契約条件の実行後に2つ目の契約条件がチェックされることになり、先程と同様に未定義動作を起こらないものと仮定して1つ目の契約条件は常にtrueとみなしてしまうことが可能となります。

C++20契約プログラミングには契約違反時の動作をカスタムするための違反ハンドラーというものが規定されており、違反ハンドラを最適化に対してブラックボックス扱い(すなわち、違反ハンドラは戻ってくるとは限らない)とすることでこのような問題に対処することが模索されていたようです(結局はその議論も含めて紛糾したためC++20から取り下げられました)。

一番最初の例のコードは、volatile変数を用いた次のようなテクニックによって最適化から保護することが可能となります。

inline void log(const char *msg) {
  std::fputs(msg, stderr);    // always returns
}

bool on_fire() {
  static volatile bool fire;  // always false
  return fire;
}

void f(int *p) {
  if (p == nullptr) log("bad thing 1");
  if (on_fire()) std::abort();  // #1
  if (*p >= 5) log("bad thing 2");
}

volatile変数の読み取りはC++仮想機械(実装が模倣すべき振る舞いをする仮想適正実装)が規定する観測可能な振る舞い(observable behavior)の一部であり、観測可能な振る舞いは最適化の後でも必ず実行される必要があります。#1ifの条件では関数呼び出しを介してvolatile変数の読み取りが行われており、その読み取りは最適化の対象となりません。そのため、trueパスのstd::abort()は到達しないことがわかっていてもon_fire()の実行およびif (on_fire())文を最適化によって除去することはできず、次の行の*pに全てのパスで到達すると仮定できないことから、先ほどのような最適化が抑止されます。

ただし、コンパイラpnullptrである場合にon_fire()trueを返さない限り未定義動作となることを推察することができ、その場合on_fire()よりも前にstd::abort()を持ってくることができます(未定義動作は起こらないのだから、on_fire()trueを返すと仮定してもよい + プログラムが未定義となる場合にはそのプログラムは観測可能な振る舞いを実行しなくても良い)。その場合は、未定義動作を実行することなくそれを検出することができます。

とはいえこのような分析に実装が従う必要はなく、このテクニックには保証がありません。

この提案はこのテクニックを一般化し、最適化抑止の保証を与えたstd::observable()を導入することで、これら因果関係を逆転するような最適化をコントロールできるようにしようとするものです。

namespace std {
  void observable() noexcept;
}

std::observable()の呼び出しは最適化における一種のブロックとして動作して、std::observable()によるある1つのブロックが未定義動作を含まずに完了した場合、そのブロックはブロック内に含まれる観測可能な振る舞いを示すことを要求します。ブロックが未定義動作を含む時に未定義となるのはそのブロック内に留まり、コード上の因果関係を遡って未定義化が波及することはありません。より正確には、std::observable()(およびプログラムの終了)は1つの観測可能なチェックポイント(observable checkpoint)として規定され、そのようなチェックポイントの後方に未定義動作がある場合でもチェックポイント前方の観測可能な振る舞いを実行しなければならない、のように規定されます。

先ほどのC++20契約プログラミングの例では次のように使用して、いかなる場合でも1つ目の契約条件が評価されることを保証できます。

void f(int *p) [[expects: p]] [[expects: (std::observable(), *p<5)]];

他にstd::observable()を適用可能な明らかな場所は、その成否にかかわらずリターンするI/O操作の後、エラーをハンドルするコードの中です。そのような場所では未定義動作が発生する可能性が高いはずです。

この提案は、EWGのレビューを通過しており、LEWG/SG1/SG22での確認を待ってCWGに転送される予定で、今のところC++23を目指しているようです。

P1774R4 Portable assumptions

コンパイラにコードの内容についての仮定を伝えて最適化を促進するための[[assume(expr)]]の提案。

プログラマーはあるコードについて特定の仮定が成立する事を知っている場合があり、そのような情報をコンパイラに伝えることができれば、コンパイラの最適化の一助となる可能性があります。そして、全ての主要なC++処理系はその手段を提供しています。

  • clang : __builtin_assume(expr)
  • MSVC/ICC : __assume(expr)
  • GCC : if (expr) {} else { __builtin_unreachable(); }
int divide_by_32(int x) {
  __builtin_assume(x >= 0); // 仮定を伝える
  return x/32;
}

この例では、コンパイラは通常符号付整数の可能な全ての入力で正しく動作するコードを出力しますが、__builtin_assume(x >= 0)によってxが負およびゼロのケースを考慮しなくても良いことがわかるため、コンパイラは正の場合のみで正しく動作するコード(5ビット右シフト)を出力します。

このように高い有効性が期待できますが、各実装の独自拡張でありその意味論や振る舞いも微妙に異なっているなどポータブルではありません。この提案はこの既存の慣行となっている機能を標準化するとともに、既存実装とC++標準の両方にうまく適合するように統一された構文と意味論を与えようとするものです。

構文は__builtin_assume(expr)をベースとした属性構文[[assume(expr)]]を提案しています。

int divide_by_32(int x) {
  [[assume(x >= 0)]];
  return x/32;
}

この属性はどこにでも書けるわけではなく、空のステートメントに対して([[fallthrough]]と同様)のみ指定でき、かつ関数内部でのみ使用できます。[[assume(expr)]]exprは評価されないオペランドであり、副作用を持つ式を指定することもできますが、決して実行されません。そして当然ですが、expr == falseとなるような入力に対しては未定義動作となるため、この仮定が満たされるようにするのはプログラマの責任となります。

このような仮定は、契約プログラミングにおける事前条件とよく似たものに思えます。しかし、契約の目的は事前条件と事後条件をコード上で記述できるようにするとともに、実行時にチェックすることでバグを発見するものであり、インターフェースなどAPIの境界の部分で使用されるものです。この機能(仮定の伝達)の目的はコードの特定の部分における事前条件(不変条件)をコンパイラに伝えるもので、特定の実装の詳細として使用されます。また、誰もが広く使用するものではなく、パフォーマンスが必要となるところで専門家だけが使用するものです。

また、契約の事前条件を仮定とみなすことでパフォーマンスが向上するということを示した調査はなく、むしろ低下させるか全く変化がないことを示した調査は存在しています。そのため、事前条件のアサーションと仮定を同じ言語機能で表現すべきではなく、提案中の契約プログラミングの構文とは異なったものをここでは提案しています。また、将来的に契約プログラミングに仮定の能力を与える場合でも、この機能をベースとしてそれを指定することができます。

P1854R2 Conversion to literal encoding should not lead to loss of meaning

文字列リテラルエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。

以前の記事を参照

このリビジョンでの変更は、マルチキャラクリテラルの各要素が基本文字集合のメンバに限定されるのではなく、1つのコード単位として表現可能なように文言を修正したこと、マルチキャラクリテラル関連の変更が視覚的な曖昧さを避ける為だけのものであることを強調するように文章を変更したことなどです。

P1899R1 stride_view

範囲を等間隔の要素からなる範囲に変換するRangeアダプタstride_viewの提案。

stride_viewのような機能はSTLに存在しておらず、C++20のRangeライブラリにもこれを簡単に合成する方法はありません。それによって次のような処理のforループからアルゴリズムへの移行が妨げられています。

// 2つ飛ばしの代入
for (auto i = 0; i < std::ssize(v); i += 2) {
  v[i] = 42; // fill
}

// 3つ飛ばしの変換
for (auto i = 0; i < std::ssize(v); i += 3) {
  v[i] = f(v[i]); // transform
}

// 3つ飛ばしの選択ソート
for (auto i = 0; i < std::ssize(v); i += 3) {
  for (auto j = i; j < std::ssize(v); i += 3) {
    if (v[j] < v[i]) {
      std::ranges::swap(v[i], v[j]);
    }
  }
}

stride_viewによってこれらの処理は次のように書くことができるようになります。

// 2つ飛ばしの代入
std::ranges::fill(v | std::views::stride(2), 42);

// 3つ飛ばしの変換
auto strided_v = v | std::views::stride(3);
std::ranges::transform(strided_v, std::ranges::begin(strided_v), f);

// 3つ飛ばしの選択ソート
stdr::stable_sort(strided_v);

C++23にstride_viewがない場合、必要とするユーザーはそれを得ようとして自作を試み、filter_viewが最適だと思うかもしれません。

auto bad_stride = [](auto const step) {
  return views::filter([n = 0, step](auto&&) mutable {
    return n++ % step == 0;
  });
};

この実装は少なくとも次の2つの問題があり、間違っています

  • filter_viewに渡す述語はstd::predicateのモデルでなければならず、副作用は認められない。
  • このラムダは後方への移動を考慮しておらずbidirectional_rangeの入力rangeに対して動作しない。
    • ラムダがstd::predicateのモデルとなっておらず、それによって出力rangebidirectional_rangeのモデルにもならないため、これは診断不要の未定義動作となる。

stride_viewは利便性が高いく、欠けていればこのように誤った実装をされる可能性が高いため、<ranges>追加しなければならないということで、C++23に向けて追加しようとする提案です。

提案されているstride_viewは、入力範囲のrandom_access_rangeを継承するようになっています。その際問題となるのは、指定された数で割り切れない長さを持つ範囲に対するstride_viewの後退時で、ナイーブな実装(指定された数飛ばしてイテレータを進行/後退する実装)だと終端に到達した時に正しく後退することができません。

// ここでのviews::strideはナイーブ実装のものとする

// 3で割り切れる長さの入力範囲
auto x = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

// prints 0 3 6 9
std::ranges::copy(std::views::stride(x, 3), std::ostream_iterator<int>(std::cout, " "));

// prints 9 6 3 0
std::ranges::copy(std::views::stride(x, 3) | std::views::reverse, std::ostream_iterator<int>(std::cout, " "));

// 3で割り切れない長さの入力範囲
auto y = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// prints 0 3 6 9
std::ranges::copy(std::views::stride(y, 3), std::ostream_iterator<int>(std::cout, " "));

// prints 8 5 2、reverseすると異なる範囲になる
std::ranges::copy(std::views::stride(y, 3) | std::views::reverse, std::ostream_iterator<int>(std::cout, " "));

ナイーブな実装だと、stride_view(stride)reverseした時には終端イテレータからのstride飛ばしの後退をすることになりますが、入力範囲がstrideで割り切れない長さの場合先頭から進行した時と異なる要素をイテレートすることになります。

これを防ぐためには、ステップ数を記憶しておき、それを利用して正しい位置を求めるようにします。

// 次のものはstride_view::iteratorのメンバ変数
// n : 進める距離
// stride_ : stride_view(n)のn
// step_ : 

iterator& advance(difference_type n) {
  if (0 < n) {
    step_ = ranges::advance(current_, n * stride_, ranges::end(underlying_range_));
    return *this;
  }
  //...
}

この形式のranges::advanceは指定された距離(n * stride_)に対して進めなかった距離を返します。すなわち、入力範囲の終端以外のところではstep_はゼロです。入力範囲の終端かつ入力範囲長がnで割り切れない場合のみstep_は非ゼロ(正)になります。進行時はこのadvance(n)を使用して、最後に進めなかった距離をstep_に記録しておきます。

iterator& advance(difference_type n) {
  //...

  if (n < 0) {
    auto stride = step_ == 0 ? n * stride_
                             : (n + 1) * stride_ - step_;
    step_ = ranges::advance(current_, -stride, ranges::begin(underlying_range_));
  }
}

後退時はadvance(-n)のように使用して、step_がゼロであれば端点の考慮は必要なたいめn * stride_分(これは負になる)入力範囲のイテレータcurrent_)を現在位置から後退させます。step_が非ゼロなら元の範囲の終端を超えた位置まで進行しようとしていたことがわかるので、(n + 1) * stride_ - step_のようにして終端位置から後退する距離を調整します。

これらの工夫によって、stride_viewは入力範囲のrandom_access_rangeを継承できるようになります。

P2071R1 Named universal character escapes

ユニバーサル文字名として、16進エスケープシーケンスの代わりにユニコードの規定する文字名を使用できるようにする提案。

C++11から、基本文字集合に含まれない任意のユニコード文字をポータブルに表すためにユニバーサル文字名を使用できます。

// UTF-32 character literal with U+0100 {LATIN CAPITAL LETTER A WITH MACRON}
char32_t c = U'\u0100';
// UTF-8 string literal with U+0100 {LATIN CAPITAL LETTER A WITH MACRON} U+0300 {COMBINING GRAVE ACCENT}
char8_t c = u8"\u0100\u0300";

とはいえ、16(8)進エスケープシーケンスではそれがどの文字を指すのか直感的ではありません。この提案は、16進数値列の代わりにユニコードの規定する文字の名前を使用してユニバーサル文字名を構成できるようにしようとするものです。

char32_t c = U'\N{LATIN CAPITAL LETTER A WITH MACRON}'; // Equivalent to U'\u0100'
char8_t c = u8"\N{LATIN CAPITAL LETTER A WITH MACRON}\N{COMBINING GRAVE ACCENT}"; // Equivalent to u8"\u0100\u0300"

16(8)進エスケープシーケンスによるユニバーサル文字名では、その内容を表示するために同時にコメントを追記する場合、時間経過とともにコードとコメントの同期が取れなくなりがちですが、このように文字の名前(エイリアス)で指定する事でそのようなコミュニケーション手段を取る必要がなくなります。

この機能は名前付文字エスケープ(Named character escape)と呼ばれます。構文は、文字/文字列リテラル中で\N{...}の中に文字の名前を指定する形です。

使用可能な文字名はユニコード規格によって提供され、ユニコード規格に追随しており安定性が保証されている次のものを参照します。

ユニコードが将来追加しうる名前との衝突を回避するために、この提案では使用可能な文字名を実装が拡張することを許可しないようにしています。

名前指定のマッチングについてはUAX44-LM2を参照しており、これによって大文字と小文字を区別しない、ハイフンの省略、アンダースコアのスペースへの置換など、柔軟な指定が可能となっています。例えば、次の名前は全てU+200B {ZERO WIDTH SPACE}を示すものとして扱われます

ZERO WIDTH SPACE
ZERO-WIDTH SPACE
zero-width space
ZERO width S P_A_C E

この提案はSG16およびEWGのレビューを通過し、C++23導入を目指してCWGへ転送されるための投票待ちをしています。

P2093R10 Formatted output

std::formatによるフォーマットを使用しながら出力できる新I/Oライブラリstd::printの提案。

以前の記事を参照

このリビジョンでの変更は、ベースとするドラフトを最新のものに更新し、それに伴ってP2418の変更を適用したことです。

この提案はLEWGのレビューを通過し、LWGに送られるための投票待ちをしています。

P2198R3 Freestanding Feature-Test Macros and Implementation-Defined Extensions

フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。

以前の記事を参照

このリビジョンでの変更は、(この提案がC++23で入るとして)フリースタンディングと示される機能テストマクロの値を更新するものをC++20以前の機能のみに限定したこと(その機能の導入・更新とフリースタンディング化を区別するため)、C++23のライブラリ機能でフリースタンディング指定可能なものに対応する機能テストマクロを追加したこと(__cpp_lib_byteswap, __cpp_lib_constexpr_typeinfo, __cpp_lib_invoke_r, __cpp_lib_is_scoped_enum, __cpp_lib_ranges_zip, __cpp_lib_to_underlying)などです。

P2249R3 Mixed comparisons for smart pointers

スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGのレビューを受けて設計上の決定を明確にするために説明を改善したこと、std::unique_ptr/std::shared_ptrから派生したクラスを除外するための制約を追加したことです。

P2273R3 Making std::unique_ptr constexpr

std::unique_ptrを全面的にconstexpr対応する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けて提案する文言のフォーマットを調整したこと、unique_ptr同士の比較演算子を対象に追加したことなどです。

この提案はすでにLEWG/LWGのレビューと投票を完了し、次の全体会議で投票にかけられることが決定しています。

P2278R2 cbegin should always return a constant iterator

std::ranges::cbegin/cendを拡張して、常にconst_iteratorを返すようにする提案。

以前の記事を参照

このリビジョンでの変更は、views::as_constviews::all_constに名前を変更したこと、エイリアステンプレート(std::ranges::const_iterator_t/std::ranges::range_const_reference_t)と機能テストマクロを追加したことなどです。

この提案はこのリビジョンを持ってLEWGでのレビューを完了し、LWGに送るためのLEWGの投票待ちをしています。

P2286R3 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更はP2418の変更を適用したこと、文字列を適切に引用符で括る方法や連想コンテナをフォーマットする方法について議論と機能を拡充したこと、あらゆるフォーマット指定子の紹介とそれをより広く機能させるための議論の追加、設計に集中するために提案文言を一旦削除、などです。

P2302R1 std::ranges::contains

新しいアルゴリズムとしてstd::ranges::containsを追加する提案。

このリビジョンでの変更は、std::basic_string_view/std::basic_string.contains()メンバ関数削除を提案しなくしたこと、命名についての説明を追加したことなどです。

P2338R2 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

以前の記事を参照

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

  • strtokC++のフリースタンディングライブラリとして追加
  • <ratio>への誤った言及を削除
  • <cinttypes>の提案からの削除完了
  • その他C向けの変更

などです。

P2361R4 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなる規格ドラフトを更新したことです。

P2388R4 Minimum Contract Support: either No_eval or Eval_and_abort

契約が破られた時に継続しないコントラクトサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、不適切な契約によって契約チェックが無限ループに陥る問題について追記したこと、関数の戻り値型がvoidである場合に事後条件に名前(戻り値のキャプチャ)を導入できないように文言を修正したことなどです。

この提案による契約サポートは事前条件と事後条件のみを対象としていて、クラス不変条件についてのサポートは欠けています。その場合、クラス不変条件とはすべてのパブリックメンバ関数の事前条件である、と考える人がいるかもしれません。すると、例えば次のようなコードが生まれる可能性があります

class Container {
  // ...
public:
  bool invariant() const { return (size() == 0) == empty(); }

  int size() const [[pre: invariant()]];
  bool empty() const [[pre: invariant()]];
};

このクラスのsize()/empty()はどちらの関数を呼んだとしても、自身及び片方が事前条件invariant()のチェックで呼び出されるため、無限ループに陥ります。この問題の考えられる解決策は、これが危険であることを周知・教育しこのようなコードが書かれないことを信頼する、あるいは契約のチェック時には再帰的に契約チェックを行わないようにする、の二つが考えられます。

2つ目のアプローチでは、事前条件の中でそれ自体が事前条件をなしている関数を呼び出すことはできず、すべての事前条件が広い契約(wide contract)を持たなければならないことを示しています。

2つ目のアプローチはBoost.Contracts、1つ目のアプローチはD言語でそれぞれ実装されています。

P2407R1 Freestanding Library: Partial Classes

一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。

このリビジョンでの変更は、機能テストマクロを追加したこと、std::optionalmonadic oeprationstd::string_view::containsについても同様にフリースタンディング化することにしたことなどです。

P2408R3 Ranges iterators as inputs to non-Ranges algorithms

P2408R4 Ranges iterators as inputs to non-Ranges algorithms

非Rangeアルゴリズムイテレータに対する名前付き要件を、イテレータコンセプトで置き換える提案。

以前の記事を参照

R3での変更は、プロクシイテレータを正しくハンドルできるように変更したことです。

これによって、Cpp17XXXIterator要件からイテレータコンセプトを使用してイテレータチェックを行うように変更するのは、constant iteratorに対してのみになります(以前はすべてのイテレータ)。プロクシイテレータでは、*it = vがユーザーの期待通りに振舞うように設計されていますが、*it = std::move(v)swap(*it1, *it2)などの変異操作は予期しない振る舞いをする可能性があります。なぜなら、通常のイテレータdecltype(*it)T&であるのに対して、プロクシイテレータのそれは別のもの(おそらくprvalue)となるためです。例えば、zip_view::iteratorの場合はdecltype(*it)は参照型のstd::pair/std::tupleであってstd::pair/std::tupleの参照ではありません。

C++20 Rangeアルゴリズムはこのようなプロクシイテレータを正しく扱うことができるように設計されています。例えばムーブ/swapiter_moveiter_swapCPOを使用することで、プロクシイテレータによってカスタムされたムーブ/swap操作を呼び出します。非Rangeアルゴリズム(つまり従来のアルゴリズム)ではその考慮はされておらず、イテレータデリファレンス結果に直接ムーブ/swap操作を呼び出します。

ただし、プロクシイテレータconstant iterator(要素アクセスのみ可能で書き換えができないイテレータ)としては正しく動作します。問題となるのはプロクシイテレータように設計されておらず、かつmutable iterator(要素の書き換えができるイテレータ)を必要とするアルゴリズムだけです。したがって、Cpp17XXXIterator要件を非Rangeアルゴリズムに渡されるmutable iteratorのために維持すれば、そこでプロクシイテレータを使用することは未定義動作となります。

R3ではこれに対処して、ForwardIterator要件が要求されるところでは、mutable iteratorが必要となる場合はCpp17ForwardIterator要件を満たす、そうでない場合はforward_iteratorのモデルとなる、というように変更しています。

R4(このリビジョン)での変更は、実装経験を収集し追記したこと、それに基づいてこの提案の既存実装への影響セクションを更新したことです。

筆者の方はこの提案の内容をGCC(libstdc++)、MSVC(MSVC STL)に適用したうえでテストを行い、その結果いくつかの問題は見つかったもののこの提案の内容を実装可能であると報告しています。

P2441R1 views::join_with

パターンによってrangerangeとなっているようなシーケンスを接合して平坦化するRangeアダプタ、views::join_withの提案。

以前の記事を参照

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

P2443R1 views::chunk_by

指定された述語によって元のシーケンスの可変個の要素を組にした要素からなるシーケンスを生成する、views::chunk_byアダプタの提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映したことです。

この提案は、すでにLEWG/LWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2446R1 views::all_move

入力シーケンスの各要素をstd::moveするようなviewである、views::moveの提案

以前の記事を参照

このリビジョンでの変更は、名前をviews::all_moveへ変更したこと、機能テストマクロを追加したことです。

P2454R0 2021 November Library Evolution Polls

2021年の11月に予定されている、LEWGでの全体投票の予定表。

以下の13の提案が投票にかけられる予定です。

LEWGでの作業を完了してLWG(CWG)へ転送することを確認するための投票です。

P2461R1 Closure-based Syntax for Contracts

属性likeな構文に代わるコントラクト構文の提案。

以前の記事を参照

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

  • タイポ修正と単語や文書の改善
  • 文法の変更
    • preconditionspostconditionsassertionsに分割し明確化
    • postconditionsの戻り値パラメータをオプションにした
  • []の指定が採用されない場合、メンバ関数におけるキャプチャデフォルトを[&, this]となるように変更
  • 値によるキャプチャでは、対応するラムダ本体は常にmutableであることを明確にした
  • サンプルの拡張
  • 文書から「lambda」を削除し、「closure」に統一した
    • 提案する文法はラムダクロージャの構文に依存しているが、ラムダ本体の構文には依存していないため
  • 「future extensions」のセクションを副節に分割
  • 副作用消去の異なるモデルを紹介し、このテインのモデルよりも緩いものを採用すると何が失われるかを説明
  • 戻り値の破棄の例を意味のあるものに変更
  • attribute-specifier-seqの場所を変更して、契約指定そのものに対する属性指定を許可
  • コントラクト仕様そのものに対するテストとファジングについてのセクションを追加
  • 省略形ラムダとの関連を追記
  • 「capture design space」セクションを追加
  • 検討、否定されたアイデアに関するセクションを追加
  • 契約指定がチェックされないモードでもODR-usedであることを明確化

などです。

P2467R0 Support exclusive mode for fstreams

fstreamに排他モードでファイルオープンするフラグ、std::ios_base::noreplaceを追加する提案。

C11ではfopenで書き込みモードでファイルを開く際のフラグにxを追加できるようになりました。これによって、ファイルが排他モードでオープンされ、既存のファイルが存在する場合はオープンに失敗するようになります。

FILE *fp = fopen("foo.txt", "wx"); // w+x,wbxなど、書き込みモードのみxが有効

これはいわゆるTime of Check, Time of Use(TOCTOU)という問題に対処するためのものです。

FILE *fp = fopen("foo.txt","r"); 
if(!fp) { 
  // file does not exist
  fp = fopen("foo.txt", "w");

  //...

  fclose(fp); 
} else { 
  // file exists
  fclose(fp); 
} 

このようなコードにおいて、1-2行目のファイルの存在チェックから3行目の書き込み用ファイル作成(オープン)までの間にその名前のファイル(あるいはシンボリックリンクなど)が作成されてしまうと、4行目以降の処理において意図しないところに書き込みを行ってしまう可能性があります。xを追加した排他モードの書き込みファイルオープンでは、fopenにおいてファイルの存在チェックとファイル作成を同時に行うことでTOCTOUに対処し、既存ファイルが存在する場合に上書きを行わないようになります。

C++はC11を参照しているのでfopenに対するxフラグはすでにサポートされていますが、std::fstreamで同じことをする標準的な手段はなく、TOCTOUを回避しようとする場合に使用することができません。xフラグはglibcで早期からサポートされており、時期POSIX標準でも導入される予定です。また、C++の初期(標準化以前)のstd::ofstreamではnoreplaceフラグがサポートされていました(これはおそらくPOSIXO_EXCLから来ており、C90との互換のために標準化されませんでした)。また、MSVCではios_base::_Noreplaceとしてサポートされています。

これらの理由から、C++std::ofstream)でも排他モードの書き込みファイルオープンをサポートすべき、という提案です。

排他モードフラグはstd::ios_base::noreplaceとして追加されます。

int main() {
  // 書き込みモードかつ排他モード(ファイルが無い場合のみファイル作成)でオープン
  std::ofstream ofs("file.txt", std::ios_base::out | std::ios_base::noreplace); 

  if (!ofs) {
    // file.txtが存在する場合失敗する
    std::cout << "file exist\n";
    return -1;
  }
}

P2477R1 Allow programmer to control and detect coroutine elision by static constexpr bool must_elide() and

P2477R2 Allow programmer to control and detect coroutine elision

コルーチンの動的メモリ確保を避ける最適化を制御し、起こったことを検出するAPIを追加する提案。

以前の記事を参照

R1での変更は

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

  • must_elide()の戻り値型をboolから3要素のenumへ変更
  • 背景をさらに追記
  • must_elide()を非constexprとすることについて議論を追加

must_elide()で動的メモリ確保省略を強制できないケース(コルーチン本体が見えていない時)をどうするかなどの問題について議論が必要であるため、この提案はまだ方向性を決定していません。

P2483R0 Support Non-copyable Types for single_view

single_viewがコピー不可なクラスのオブジェクトでも保持できるようにする提案。

現在のsingle_viewstd::copy_constructibleコンセプトによって要素型を制約しているため、コピー構築不可能な型を要素とすることができません。

// fooはムーブオンリーな型とする
foo make_foo();

std::views::single(make_foo()) // error

この制約はどうやら、最初期のview定義(copyableかつmovable)を満たすための制約のようですが、途中でviewの定義は変更され現在はmovableであればよくcopyableは必ずしも求められていません。したがって、single_viewのこの要件はstd::move_constructibleまで弱める事ができるはず、という提案です。

これによって冒頭のサンプルのような、ムーブオンリーな型を要素とするsingle_viewが作成可能となります。

P2484R0 Extending class types as non-type template parameters

クラス型を非型テンプレートパラメータ(NTTP)の条件にアダプトするための新しい構文operator template()の提案。

C++20からNTTPの制限が緩和され、一部のクラス型をNTTPとして取れるようになりました。新たな制限はNTTPとなれる型をstructural typeとして制限しており、それは次のいずれかに該当するものです

  1. スカラ型
  2. 左辺値参照型
  3. 次の条件を満たすリテラル
    • 全ての基底クラス及び全ての非静的メンバ変数はpublicかつmutableではない
    • 全ての基底クラス型及び全ての非静的メンバ型はstructural typeであるか、配列型である

クラス型のNTTPは3つ目の条件を満たすものに限られています。これによってstd::pairstd::arrayなどの型をNTTPとする事ができるようになりますが、std::tuple, std::optional, std::variantstd::string, std::vectorなどの型をNTTPとすることはできず、これらの型をこの条件にアダプトさせることも困難です。

特に問題となるのは全メンバがpublicであるという制約です。この制約はテンプレートの等価性判定のための制約で、あるクラス型のNTTPのテンプレートとしての等価性判定をメンバ毎の比較で行える型を指定するものです。型として追加の意味論を持つためにそのような比較が適切ではない型では、そのメンバ変数はprivateとされる事が一般的です。

そういう意味論の下では、std::tuple, std::optional, std::variantなどはそのメンバの比較によるテンプレート等価性の判定が適切ではありますが、これらの型はそのメンバを全てpublicとして実装されません。これらの型をNTTPとして扱うために必要なことは、C++20で導入されたメンバ毎比較によるテンプレート等価性判定にアダプトするための仕組みだけです。

将来的にはstd::string, std::vectorなどの型もNTTPとして活用できると便利ですが、std::tuple, std::optional, std::variantと同じアプローチではこれは達成できません。例えば、std::stringはポインタ3つ(あるいはポインタ2つとサイズ2つ)をメンバとして実装される事が多いですが、単にそれらメンバの比較によってテンプレート等価性判定をしてしまうと次のような問題があります

// テンプレート等価性をメンバのポインタ値の比較によって可能としたとすると
template <std::string S>
struct X {
  bool operator==(X) const = default;
};

// この2つは同じ型となっていて欲しいが
X<"hello"> a;
X<"hello"> b;

a = b;  // error、型が合わない

異なるstd::stringオブジェクトのメンバのポインタ値は異なる領域を指すため、このa bは異なる型を持ちます。これはstd:string及びテンプレート等価性の意味論にそぐわないため、std::string, std::vectorなどの型をNTTPとして扱うためには別のアプローチが必要そうです。

この提案のアプローチはoperator template()という演算子を追加する事で、ある型のNTTPのテンプレート等価性を別のstructural typeに移譲するものです。

class A {
private:
  int m_i;

  struct Repr {
    int i;
  };

  constexpr A(Repr r) : m_i(r.i) {}

  constexpr auto operator template() const -> Repr { 
    return {m_i};
  }

public:
  constexpr A(int i) : m_i(i) { }
};

template <A a>
struct X { };

T::operator template()structural typeな型Rを返さなければならず、RTの表現として機能する必要があり、TRから構築可能である必要があります。この例では、A::operator template()の返す型A::RepeによってA::Repr{1} == A::Repr{1}となるため、A{1} == A{1}となります。

この例は説明的なもので、実際には次のようにより簡易化できます。

class A {
private:
  int m_i;

  constexpr auto operator template() const -> int { 
    return m_i;
  }

public:
  constexpr A(int i) : m_i(i) { }
};

template <A a>
struct X { };

intはすでにstructural typeなので、それをラップする型は必要ありません。

これをtuplelikeな型に対して書くのは非常に面倒な作業となるので、operator template()default実装可能です。

class A3 {
private:
  int i;
  int j;

  constexpr auto operator template() const = default;
public:
  constexpr A3(int i, int j) : i(i), j(j) { }
};

template <A3 a> struct X { };

default定義のoperator template()を持つ型は、C++20の集成体をNTTPとして使用した時と同様に、全ての基底クラス及び非静的メンバ変数についての比較によってテンプレートの等価性判定が可能であることを表明し、それによってテンプレート等価性判定が行われます。唯一の違いは、基底クラス及び非静的メンバ変数がprivateであっても構わない点です。ただし、これは再帰的ではなく、全ての基底クラス及び非静的メンバ変数がstructural typeである事が求められます。

// structural typeではない
class B {
  int i;  // プライベートメンバ
};

// structural typeとしたい
class D : B {
  int j;
  constexpr auto operator template() const = default;
};

template <D d> // error、BがstructuralではないためDもstructuralではない
struct Y { };

operator template()は関数のように見えますが、あくまでコンパイラがテンプレートの等価性判断(及びマングリング方法)をどうするかを指定する注釈にすぎません。したがって、実際にこれが呼び出される事はなく、呼び出された時の振る舞いなどは規定されず、defaultoperator template()の戻り値型を気にする必要はありません。ユーザー定義型Cをマングリングに参加させる(Cによってテンプレート等価性を判定する)には単にCを直接使用すればokです。

class A {
private:
  C c;  // ユーザー定義型(C++20のstructural typeかもしれないし、operator template()を持つかもしれない)
  D d;  // なんらかの理由によりマングリングに関与しないユーザー定義型

  struct Repr { C c; };
  constexpr auto operator template() const { return Repr{c}; }
  explicit constexpr A(Repr);
};

Cstructural typeであればその性質に到達する方法にかかわらず(Cintエイリアスであったりoperator template()を持っていたりにかかわらず)、A::Reprstructural typeでありCstructural性を正しく反映します。ここでも、operator template()を呼び出す必要はありません。

std::tuple, std::optional, std::variantはこのoperator template()を使用してメンバごとの比較によってテンプレート等価性を判定できるようになり、簡単にNTTPにアダプトする事ができます。一方、std::string, std::vectorは現在定数式での動的メモリ確保が一時的(実行時に持ち越せない)なため、operator template()を正しく定義したとしてもNTTPとして使用する事ができません。それを解決する提案は進行中ですがまだ採択されていないため、この提案ではこの2つの型に対しては何もしません。

定数式での非一時的な動的メモリ確保が許可されていないことから非defaultoperator template()を急ぐ必要はないため、この提案ではC++23に向けてクラス型でdefaultoperator template()を定義可能にし、そのクラス型は全ての基底クラス及び非静的メンバがstructural typeであれば自身もstructural typeとなるようにすることを提案しています。また、それをstd::tuple, std::optional, std::variantにも定義して、要素型が全てstructural typeであればこの3つの型もstructural typeとなるようにすることも提案しています。

ここまでの説明のように、この提案では非defaultoperator template()を使用してstd::string, std::vectorなどの型をNTTPとして扱えるようにする方向性が示されていますが、ここではそれは提案されません。将来的に定数式での非一時的な動的メモリ確保が許可された後で解禁する予定です。

P2485R0 Do not add value_exists and value_or to C++23

std::numeric_traitsに代わる数値特性クエリAPIとして提案されているP1841R1から、value_existsvalue_orを取り除く提案。

P1841R1に関しては以前の記事を参照

P1841R1に提案されているvalue_existsは数値特性TraitTについて利用可能かどうかを調べるもので、次のような定義になります。

template <template<class> class Trait, class T>
constexpr bool value_exists = requires { Trait<T>::value; };

これは例えば、value_exists<finite_max, int>のように使用しますが、LWGにおける議論の過程でTraitTを別々に受け取る設計について疑問が提起されたようです。

template <class T>
constexpr bool value_exists = requires { T::value };

// このようなAPIではないのはなぜ?
static_assert(value_exists<finite_max<int>>);

つまりこのように、Trait<T>の形で受けた上で静的メンバ::valueの存在チェックをする形の方が理解しやすく使いやすいのでは?という事です。

value_or()は数値特性TraitTについて利用可能でない場合に、指定された値へフォールバックするためのものです。

template <template <class> class Trait, class T, class R = T>
inline constexpr R value_or(R def = R()) noexcept;

これは例えば、value_or<finite_max, int>(100)のように使用します。この問題は、value_or()の戻り値型は引数として渡した値の型Rであるため、Tと異なる可能性がある事です。例えば、value_or<finite_min, double>(1)int型の結果となり、これは想定される振る舞いではないでしょう。

これらの理由により、P1841からvalue_existsvalue_orを取り除き、他の数値特性のみを採用することを提案しています。これらのユーティリティが必要になったら、また後で議論をすれば良いとのことです。

P2486R0 Structured naming for function object and CPO values

CPOやniebloidなどの関数オブジェクトに対しての命名ルールに関する提案。

P2322R5 ranges::foldの議論の過程で、その命名に関する議論が起こりました。名前付についての議論は、名前が主観的になるとともにその名前の技術的な側面が主観的な側面によって曖昧になってしまうため、とても厄介な議論です。この提案は、そのような議論をなるべく回避するために、主として関数オブジェクトに対する命名についての標準的な方法を提案するものです。

物事のある集合に名前をつける際は、ネスト構造を追加することで簡単になります。この提案の言うStructured naming(構造化された名前付)とはそのようなネスト構造を適切に反映した命名のことであり、例えば次のようなものです。

構造化された名前 構造化されていない名前
std::vector<T>
std::vector<T>::value_type
std::list<T>
std::list<T>::const_iterator
std::chrono
std::chrono::steady_clock
std::chrono::steady_clock::time_point
std_vector<T>
std_vector_value_type<T>
std_list<T>
std_list_const_iterator<T>
std_chrono
std_chrono_steady_clock
std_chrono_steady_clock_time_point

この構造化されていない例の命名は多くの人が適切ではないと考えると思われますが、それは私たちが無意識下で想定している普遍的で適切な命名構造への期待に反しているからこその反応だと思われます。

現在LEWGで合意されたP2322R5の関数群は次のような命名となっています。

fold_left()                   // 左畳み込み
fold_left_first()             // 最初の要素を初期値とする左畳み込み
fold_right()                  // 右畳み込み
fold_right_last()             // 最初の要素を初期値とする右畳み込み
fold_left_with_iter()         // イテレータを返す左畳み込み
fold_left_first_with_iter()   // イテレータを返す最初の要素を初期値とする左畳み込み

ここに載っていないものも含めて、fold系操作はさらに増加する可能性があります。その際、このように構造化されていない命名は組合せ爆発とともに複雑化します。とはいえ将来的に追加されるものも含めて、それらの変種が別々のオーバーロードとして提供されるのは妥当なことであり、問題となるのはその命名のみです。

プレーンな関数名では構造化されていない命名を避けることは困難でしたが、このfoldは関数オブジェクトとして実装されることが示唆されており、他の提案でも関数オブジェクトやCPOの命名について構造化されていないものがあります。関数オブジェクトであれば、メンバとして関数オブジェクトをネストさせることができるはずです。それによって、非構造化名を使用せざるを得なかった関数に対しても構造化された命名をすることができます。

構造化された名前 構造化されていない名前
fold.left()
fold.left.with_iter()
fold.left.first()
fold.left.first.with_iter()
fold.right()
fold.right.last()
fold_left()
fold_left_with_iter()
fold_left_first()
fold_left_first_with_iter()
fold_right()
fold_right_last()

他のところでは、P2300のCPOに対してもこれを適用できそうです。

構造化された名前 構造化されていない名前
std::execution::receiver.set_value()
std::execution::receiver.set_error()
std::execution::receiver.set_done()
std::execution::sender.connect()
std::execution::set_value()
std::execution::set_error()
std::execution::set_done()
std::execution::connect()

ただし、senderrecieverは同じ名前空間でコンセプトとして提供されているため、実際にはこのような命名は行えません。この提案の構造化された命名の問題点は、コンセプト定義とのこのような衝突を回避すること(コンセプトの構造化された命名)ができなければコンセプトとCPOの命名についてLEWGの時間を無駄に消費してしまう点です。

この提案はP2322R5をブロックし命名を変更しようとするものではないですが、この方向性が受け入れられるならば事後的にP2322R5の命名を構造化されたものに変更することを目指しているようです。

P2487R0 Attribute-like syntax for contract annotations

契約プログラミングの構文について、属性likeな構文は契約の指定に適しているかを考察する文書。

現在、契約プログラミングのサポートの議論は「P2388R4 Minimum Contract Support: either No_eval or Eval_and_abort」にて行われており、そこではC++20の契約プログラミングの時からの属性に似た構文を採用しています。

int f(int i)
  [[pre: i >= 0]]
  [[post r: r >= 0]];

一方、それに対してラムダ式に似た構文の提案(「P2461R1 Closure-based Syntax for Contracts」)も出ています。

int f(int i)
  pre{i >= 0}
  post(r){r >= 0};

また、例えば次のような構文を容易に思いつくことができます(提案はされていません)

int f(int i)
  pre(i >= 0)
  post(r: r >= 0);

この提案は、現在の属性likeな契約構文が契約の指定にとって適しているのかを吟味するものです。主に以下のように分析しています。

  • 無視できる
    • 現在のC++の属性についての規定では、「無視する」の意味が曖昧
    • 属性の無視について規定しなおすことを提案している
  • 宣言的or命令的
    • 契約が数学的な意味での述語である(宣言的)なら属性構文は適している
    • 契約がチェックされる(命令的)なら、属性構文は直観に反する
  • 並べ替え可能
    • 1つの属性中の2つの属性([[A, B]])は並べ替え可能だが、2つの属性([[A]] [[B]])は並べ替えられない(意味が変わる)
    • これは契約指定と互換性がある(ショートサーキットされるかが変わるため契約は並べ替えられない)
  • 順序
    • 属性構文では、他の属性と契約の順序についての問題が発生する
int f1(int i)               // correct declaration?
[[pre: i > 0]]
[[using gnu: fastcall]]
[[post r: r > 0]]; 

int f2(int i)               // correct declaration?
[[using gnu: fastcall]]
[[pre: i > 0]]
[[post r: r > 0]]; 

int f3(int i)               // correct declaration?
[[pre: i > 0]]
[[post r: r > 0]]
[[using gnu: fastcall]]; 
  • コンテナとしての[[]]
    • 人々が抱いている(可能性のある)直感は、[[]]が0か1以上の属性をカンマ区切りで指定できるコンテナ(リスト)であるというもの
  • 型と効果の分析
    • 属性を使用して、型に対する効果の注釈を行うEffect systemをいくらでも考えられる
    • そのような型と効果の静的分析という観点からは、属性構文は自然に見える
  • 関数型に現れるか
    • 属性が関数型に影響を与えるのかを明確にする必要がある
  • 契約チェックとUB
    • 契約指定に違反したときはある種の未規定の動作となるが、その未規定の動作は実際にはコンパイラオプションによって制御されている。契約指定に違反しないプログラムのセマンティクスには影響を与えず、それは属性の無視可能な側面に合致する。
  • メタ注釈
    • 属性構文を採用しない場合、契約そのものに属性指定できる
  • リフレクションでの検出
    • 属性指定されたものをリフレクションで検出可能とするかどうか
  • コロンの使用
    • コロンだけでは契約なのか属性なのかを判別できない

総合的には属性は不利なのでは?と思わせる内容ですが、この文書はどちらを提案しているわけでもありません。

P2489R0 Library Evolution Plan for Completing C++23

C++23の設計完了に向けたLEWGの作業予定や進捗を示す文書。

C++23は2023年発行予定ですが、そこに向けた提案は2022年2月7日までに採択されなければなりません。LEWGのリソースは限られており、それをC++23に入る可能性のある提案に集中させる必要があり、この文書はそのような提案をリストアップしたものです。

C++23に向けて取り組む必要のある提案

優先度を高くする必要はないが、サイズが小さめで労力がかからなそうな提案

LEWGとしてはおそらく、これ以外の提案に(一時的に)リソースを割かなくなるため、これ以外の提案がC++23に入る可能性はほぼありません。なおこれは、ライブラリについての提案のみなので、コア言語に関してはまた別の話です。

P2490R0 Zero-overhead exception stacktraces

例外からのスタックトレースの取得の提案について、問題点とその解決策についての提案。

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

P2370R0では、任意の例外オブジェクトからスタックトレースを取得可能とすると多大な実行時コストがかかることを認めた上で、std::this_thread::capture_stacktraces_at_throw()によってその使用をスイッチできるようにすることを提案しています。しかし、そのアプローチはスレッドローカル変数へのアクセスを伴っており、スレッドローカル変数へのアクセスには実行時コストがかかります(参考)。現在の例外機構では例外そのもののコストに比べればスレッドローカル変数へのアクセスコストは無視できるものであるため問題とされないことが多いようですが、将来的に「P0709 Zero-overhead deterministic exceptions: Throwing values」が採用されるとそれが問題となることが予想されます。それを考慮しなくても、P2370の主張する最小の実行時コストはゼロではありません。

また、P2370の方法ではそれを有効化するのに再コンパイルとリンクが必要となりますが、独自の例外発生メカニズムを持つサードパティのライブラリの場合、再コンパイルとリンクされて再出荷されるのに年単位の時間がかかる可能性があります。さらに、例外を内部で使用しているライブラリではユーザーがこの機能を有効化した時に、ライブラリ内部で完全に捕捉されている例外であってもオーバーヘッドがかかることになるため忌避される可能性があります。その場合はライブラリから漏れる例外に対処する必要がないとすれば、APIのエントリポイントで無効化しておく、という方法がとられる可能性があります。

C++の例外処理は通常、言語に依存しない低レベルの機能の上に構築されており、それはWindowsでは構造化例外、Itanium ABIではLevel I Base ABIです。これらの低レベルの機能では通常、例外処理は2段階の過程を経て行われています。1段目は「検索」フェーズで、例外が投げられた地点から適切なハンドラを見つけるためにスタックを調べます。2段目は「巻き戻し」フェーズで、例外が投げられた地点から選択されたハンドラまでクリーンアップ(デストラクタ呼び出し)しながら戻ります。重要なのは次の2点です。

  • 検索フェーズではスタックの内容を変更しない
  • ハンドラの識別は動的であり、コンパイラ/ライブラリによって見つかった関数が呼ばれる

これらの点から、次のような代替メカニズムを考案できます

  1. ユーザーコードでは、特別な関数または新しい構文を利用して、特定のcatchブロックについてスタックトレースが必要である事をマークする
  2. コンパイラはそのようにマークされたcatchブロックを認識すると、そのcatchブロックが例外ハンドラとして選択された時にその選択の直前(検索フェーズ)でスタックトレースを取得するための適切なコード/データを発行できる
  3. ユーザーコードでは、巻き戻しフェーズの後の例外処理において、保存されたスタックトレースを取得することができる

このアプローチの利点は次の2つです

  1. 透明性
    • 例外を投げるコードを修正したり再コンパイル・リンクする必要がない
    • このメカニズムは例外をキャッチする側の変更のみに依存している
  2. ゼロコスト
    • 例外スロー時の検索フェーズでマークされたハンドラに到達しなければ、動作に影響がない

これを実現するための構文として次のいずれかを提案しています

  1. std::stacktrace::from_current_exception()の特別扱い
    • 欠点 : from_current_exception()の呼び出しがcatchブロックの外に意図せず移動すると機能しなくなる。
  2. catchブロックのデフォルトパラメータ
    • 欠点 : 新しい構文である事、キャッチする複数の型を指定するものとして勘違いされる可能性がある
  3. catch-with-init
    • 欠点 : 新しい構文である事、検索フェーズで一般的なユーザーコードを実行するのは危険
  4. 検索フェーズの露出
    • 欠点 : 新しい構文であり悪用される可能性がある、検索フェーズで一般的なユーザーコードを実行するのは危険
void f() noexcept(false);

int main() {
  // 1
  try {
    f();
  } catch (const std::exception& ex) {
    std::cout << ex.what() << "\n" << std::stacktrace::from_current_exception() << std::endl;
  }

  // 2
  try {
    f();
  } catch (const std::exception& ex, std::stacktrace st = std::stacktrace::from_current_exception()) {
    std::cout << ex.what() << "\n" << st << std::endl;
  }

  // 3
  try {
    f();
  } catch (auto st = std::stacktrace::current(); const std::exception& ex) {
    std::cout << ex.what() << "\n" << st << std::endl;
  }

  // 4
  try {
    f();
  } catch (const std::exception& ex) if (auto st = std::stacktrace::current(); true)  {
    std::cout << ex.what() << "\n" << st << std::endl;
  }
}

これは少なくともWindowsのABIとItanium ABIで実装可能である事を確かめているようですが、その他のABI(プラットフォーム)で実装可能かどうかは不明であり情報を求めています。

P2491R0 Text encodings follow-up

システムの文字エンコーディングを取得可能とする提案(P1885)に対して、設計の欠陥を指摘する提案。

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

要約すると、以下の3点を問題としてあげています

  1. "UTF-16"というエンコーディング名を別の目的で再利用しているため、混乱が生じている
  2. Windowsで以前に使用されていたUCS-2ワイドエンコーディングを適切に表現できない
  3. オブジェクト表現を指定しようとしており、C++抽象化を壊している

これに対して、次のような改善を提案しています。

  1. IANAレジストリに登録されているエンコーディング方式(encoding scheme)のオクテット(バイト)が、std::text_encodingにおけるコードユニットとみなされる事を規定する
  2. 現在または将来のIANAのエンコーディング割り当てとの衝突を避けるために負の列挙値を持つ追加のエンコーディングWIDE.UTF16, WIDE.UTF32, WIDE.UCS2, WIDE.UCS4)を規定する
  3. std::text_encoding::(wide_)literal()からは、sizeof(char_type) == 1でないとIANAのエンコーディング方式を返せない、という注意を追記
  4. CHAR_BIT == 8またはsizeof(wchar_t) > 1に関する制限の削除

  5. P2491 進行状況

P2492R0 Attending C++ Standards Committee Meetings During a Pandemic

WG21ミーティングへCOVID19パンデミックが与えた影響についての報告書。

筆者の方の個人的な経験を元にして書かれていて、WG21ミーティングがオンラインへ移行したことによって参加のためのコストが低下し、準備に時間を取れるようになったことで積極的に議論に参加できるようになったとのことです。筆者の方にとってはオンラインミーティングにはメリットしかなく、再び対面ミーティングに移行してしまうと参加のハードルが上がることから傍観者に追いやられてしまうことを危惧しているようです。

P2493R0 Missing feature test macros for C++20 core papers

コア言語機能について、忘れられていた機能テストマクロの更新を行う提案。

忘れられていたのは、P0848R3 Conditionally Trivial Special Member FunctionsP1330R0 Changing the active member of a union inside constexprの2つで、どちらもC++20のコア言語機能です。

P2231R1 Missing constexpr in std::optional and std::variantの実装にあたってこれらの機能が使用可能である必要があり、実装はそれを検知できる必要がありますが、その手段が提供されていない事がわかりました。

この提案では、__cpp_concepts__cpp_constexprの値を202002LC++20規格完成の年月)に指定します。C++20から時間が経ってしまっていますが、__cpp_conceptsの値は更新されておらず、__cpp_constexprの値は更新されている(P2242R3)ものの、GCCのみがこれを実装済みでかつP1330R0も実装済みであるので問題ないようです。

おわり

[C++]owning_viewによるパイプライン安全性

この記事はC++ Advent Calendar 2021の7日目の記事です。

owning_view

owning_viewについては、ちょうど別に書いたので以下もご参照ください。

owning_viewは右辺値の範囲から構築され、それを所有することで右辺値範囲の寿命を延長するものです。定義は簡単なのでコピペしておくと次のようになっています

namespace std::ranges {
  template<range R>
    requires movable<R> && (!is-initializer-list<R>) // see [range.refinements]
  class owning_view : public view_interface<owning_view<R>> {
  private:
    R r_ = R(); // exposition only
  public:
    owning_view() requires default_initializable<R> = default;

    // 専ら使用するコンストラクタ
    constexpr owning_view(R&& t) : r_(std<200b>::<200b>move(t)) {}

    // ムーブコンストラクタ/代入演算子
    owning_view(owning_view&&) = default;
    owning_view& operator=(owning_view&&) = default;

    // 保持するRのオブジェクトを取得する
    constexpr R& base() & noexcept { return r_; }
    constexpr const R& base() const& noexcept { return r_; }
    constexpr R&& base() && noexcept { return std::move(r_); }
    constexpr const R&& base() const&& noexcept { return std::move(r_); }

    // Rのイテレータをそのまま使用
    constexpr iterator_t<R> begin() { return ranges::begin(r_); }
    constexpr sentinel_t<R> end() { return ranges::end(r_); }

    // Rがconst-iterableならそうなる
    constexpr auto begin() const requires range<const R>
    { return ranges::begin(r_); }
    constexpr auto end() const requires range<const R>
    { return ranges::end(r_); }

    constexpr bool empty() requires requires { ranges::empty(r_); }
    { return ranges::empty(r_); }
    constexpr bool empty() const requires requires { ranges::empty(r_); }
    { return ranges::empty(r_); }

    // Rがsized_rangeならそうなる
    constexpr auto size() requires sized_range<R>
    { return ranges::size(r_); }
    constexpr auto size() const requires sized_range<const R>
    { return ranges::size(r_); }

    // Rがcontiguous_rangeならそうなる
    constexpr auto data() requires contiguous_range<R>
    { return ranges::data(r_); }
    constexpr auto data() const requires contiguous_range<const R>
    { return ranges::data(r_); }
  };
}

ムーブコンストラクタを除くとコンストラクタは一つしかなく、そこではRrangeかつmovable)の右辺値(これはフォワーディングリファレンスではありません)を受け取り、それをメンバ変数r_にムーブして保持します。このようにして入力の右辺値範囲の寿命を延長しており、それ以外の部分は見てわかるように元のRの薄いラッパです。

views::allviews::all_t

owning_viewを生成するためのRangeアダプタとしてviews::allが用意されていますが、views::allowning_viewだけでなくref_viewも返します。

Rのオブジェクトrに対して、views::all(r)のように呼ばれた時の効果は

  1. Rviewのモデルであるなら、rをdecay-copyして返す
    • decay-copyはrをコピーorムーブしてその型の新しいオブジェクトを作ってそれを返すこと
  2. rが左辺値ならref_view(r)
  3. rが右辺値ならowning_view(std::move(r))

このように、views::allrangeを入力としてviewを返すもので、別の言い方をするとrangeviewに変換するものです。views::allを主体としてみれば、ref_viewとかowning_viewの区別は重要ではないため、この2つをまとめて(あるいは、views::allによるviewを)All viewと呼びます。

<ranges>のRangeアダプタと呼ばれるviewは、任意のviewを入力として何か操作を適用したviewを返すものです。そのため、Rangeアダプタ(の実態のview型)にrangeを渡すためには一度viewに変換する必要があり、views::allはその変換を担うRangeアダプタとして標準に追加されています。とはいえ、ユーザーがRangeアダプタを使用する際に一々views::allを使用しなければならないのかといえばそうではなく、この適用はAll viewを除く全てのRangeアダプタにおいて自動で行われます。そのため通常は、ユーザーがviews::allおよびref_viewowning_viewを直接使う機会は稀なはずです。

views::allの自動適用は推論補助をうまく利用して行われています。簡易な実装を書いてみると

using namespace std::ranges;

// 任意のview
template<view V>
class xxx_view {
  V base_;

public:
  // 入力viewを受け取るコンストラクタ
  xxx_view(V v) : base_(std::move(v)) {}

};

// この推論補助が重要!
template<range R>
xxx_view(R&&) -> xxx_view<views::all_t<R>>;

views::all_tviews::allの戻り値型を求めるもので、次のように定義されます。

namespace std::ranges::views {

  template<viewable_range R>
  using all_t = decltype(all(declval<R>()));
}

このxxx_viewxxx_view{r}のように使用した時、クラステンプレートの実引数推定が起こることによって1つだけ定義されている推論補助が使用され、rの型Rviews::all_t<R>のように通して、views::all(r)の戻り値型をxxx_viewのテンプレートパラメータVとして取得します。views::allの戻り値型は、rviewならそのview型(prvalueとしての素の型)、rが左辺値ならref_view{r}rが右辺値ならowning_view{r}を返します。つまり、views::all_t<R>は常にRを変換したviewのCV修飾なし参照なしの素の型(prvalue)を得ます。

そうして得られた型をVとすると、xxx_view{r}xxx_view<V>{r}のような初期化式になります。xxx_view(および標準Rangeアダプタのview)のviewを受け取るコンストラクタはexplicitがなく、テンプレートパラメータに指定されたview型(V、これは実引数rの型Rに対してviews::all_t<R>の型)を値として受けるものであるため、そのコンストラクタ引数ではR -> Vの暗黙変換によってviews::all(r)を通したのと同じことが起こり、ここでviews::allの自動適用が行われます。

これと同じことが、All viewを除く全てのRangeアダプタのview型で実装されており、これによって、Rangeアダプタはviews::allを自動適用してviewを受け取っています。これはxxx_viewに対してviews::xxxの名前のRangeアダプタを使用した時でも同様です(その効果では結局、何かしらのview型を適用することになるため)。

#include <ranges>
#include <vector>

auto f() -> std::vector<int>&;
auto g() -> std::vector<int>;

using namespace std::ranges;

int main() {
  auto tv = take_view{f(), 5};
  // decltype(tv) == take_view<ref_view<std::vector<int>>>

  auto dv = drop_view{g()}, 2;
  // decltype(dv) == drop_view<owning_view<std::vector<int>>>

  auto dtv = drop_view{tv, 2};
  // decltype(dtv) == drop_view<take_view<ref_view<std::vector<int>>>>

  auto ddv = dv | views::drop(2);
  // decltype(ddv) == drop_view<drop_view<owning_view<std::vector<int>>>>

  auto ddv2 = drop_view{dv, 2};
  // decltype(ddv2) == drop_view<drop_view<owning_view<std::vector<int>>>>
}

パイプラインで起こること

個別のview型で起こることはわかったかもしれませんが、実際に使用した時に起こることはイメージしづらいものがあります。

#include <ranges>
#include <vector>

auto f() -> std::vector<int>;

auto even = [](int n) { return 0 < n; };
auto sq = [](int n) { return n * n; };

using namespace std::views;

int main() {
  // pipesの型は?構造は??
  auto pipes = f() | drop(2)
                   | filter(even)
                   | transform(sq)
                   | take(5);

  // 安全、f()の戻り値はowning_viewによって寿命延長されている
  for (int m : pipes) {
    std::cout << n << ',';
  }
}

例えばこのようなRangeアダプタによるパイプラインの結果として得られたpipesは、どんな型を持ちどんな構造になっているのでしょうか?また、f()の結果(右辺値)はowning_viewによって安全に取り回されているはずですが、pipesのどこにそれは保持されているのでしょうか?

先程のviews::all/views::all_tの標準Rangeアダプタでの使われ方を思い出すと、pipesの型はわかりそうです。

1行目のf() | drop(2)ではdrop_viewviews::drop(2)による)の構築が行われ、f()の戻り値をrとするとdrop_view{r, 2}が構築されます。前述の通り、そこではviews::allが自動適用され、rは右辺値std::vector<int>なのでその結果はowning_view{r}が帰ります。したがって、この行で生成されるオブジェクトの型はdrop_view<owning_view<std::vector<int>>>となります。

その結果をv1として、次の行v1 | filter(even)ではfilter_viewが、filter_view{v1, even}のように構築されます。ここでもviews::allが自動適用されていますが、views::all(v1)v1が既にviewであるため、それがそのまま(decay-copyされて)帰ります。したがって、この行で生成されるオブジェクトの型はfilter_view<drop_view<owning_view<std::vector<int>>>, even_t>となります(述語evenクロージャ型をeven_tとしています)。

パイプラインの2段目以降ではviews::allの適用はほぼ恒等変換となるため、views::all_tの型を気にする必要があるのはパイプラインの一番最初だけです。後の行およびその他のRangeアダプタの適用時に起きることも同じようになるため、この2行目で起きている事がわかれば後は簡単です。ただし、Rangeアダプタオブジェクトの返す型に注意が必要ではあります。

auto pipes = f() | drop(2)        // V1 = drop_view<owning_view<std::vector<int>>>
                 | filter(even)   // V2 = filter_view<V1, even_t>
                 | transform(sq)  // V3 = transform_view<V2, sq_t>
                 | take(5);       // V4 = take_view<V3>

略さずに書くとdecltype(pipes) == take_view<transform_view<filter_view<drop_view<owning_view<std::vector<int>>>, even_t>, sq_t>>となります。標準view型は入力のviewをテンプレートの1つ目の引数として取るので、パイプライン前段のview型が、次の段のview型の第一テンプレート引数としてはまっていきます。

型がわかれば、そのオブジェクト構造がなんとなく見えてきます。しかし、標準view型の個々のクラス構造がわからないとこのパイプライン全体の構造も推し量る事ができません。

標準view型(主にRangeアダプタ)の型としての構造(第一テンプレート引数に入力viewをとる、推論補助によってviews::all_tを自動適用する)がある程度一貫していたように、そのクラス構造もまたある程度の一貫性があります。そこでは、入力のviewオブジェクトをコンストラクタで値として受け取って、メンバ変数にムーブして保持しています。

using namespace std::ranges;

// 任意のview
template<view V, ...>
class xxx_view {
  // 入力viewをメンバとして保持
  V base_ = V();

public:
  // 入力view(と追加の引数)を受け取るコンストラクタ
  xxx_view(V v, ...) : base_(std::move(v)) {}

};

viewコンセプトの定義するviewとは、ムーブ構築がO(1)で行えて、ムーブされた回数Nと要素数Mから(ムーブ後viewを含む)N個のオブジェクトの破棄がO(N+M)で行えて、ムーブ代入の計算量は構築と破棄を超えない程度、であるような型です。owning_viewのような例外を除けば、これは範囲を所有せずにrangeとなるような型を指定しており、ムーブ構築のコストは範囲の要素数と無関係に行える事を示しています(ここではviewのコピーについては触れないことにします)。
owning_viewは範囲を所有しますが、ムーブオンリーであるためviewコンセプトの要件を満たすことができる、少し特殊なview型です。

views::all_t<R>Rviewである時にRの素の型(prvalueとしての型)を返します。それは右辺値R&&と左辺値R&およびconst Rに対して、Rとなる型です。このようなCV修飾なし参照なしの型がview型の入力Vとなるため、Vのオブジェクトrv(これはパイプライン内では右辺値)はコンストラクタ引数vに対してまずムーブされ、メンバbase_として保持するためにもう一度ムーブされます。Vref_viewをはじめとする範囲を所有しないタイプのviewである時、その参照を含むviewオブジェクトごとムーブ(コピー)されメンバとして保存されます。Vowning_viewのように範囲を所有するviewの場合、その所有権ごとviewオブジェクトをムーブしてメンバとして保存します。その後、そうして構築されたviewオブジェクトは、パイプラインの次の段で同様に次のviewオブジェクト内部にムーブして保持されます。

パイプラインの格段でこのような一時viewオブジェクトのムーブが起きているため、最初に構築されたref_view or owning_viewオブジェクトは最後まで捨てられることなく、パイプラインの一番最後に作成されたオブジェクト内に保持されます。そして、パイプラインの段が重なるごとに、それを包むようにRangeアダプタのviewの層が積み重なっていきます。

イメージとしてはマトリョーシカとか玉ねぎとかそんな感じで、一番中心にパイプラインの起点となった入力rangeを参照or所有するviewオブジェクトが居て、それは通常ref_viewowning_viewのどちらかとなります。

#include <ranges>
#include <vector>

auto f() -> std::vector<int>;

auto even = [](int n) { return 0 < n; };
auto sq = [](int n) { return n * n; };

using namespace std::views;

int main() {
  // f()の戻り値はpipesの奥深くにしまわれている・・・
  auto pipes = f() | drop(2)
                   | filter(even)
                   | transform(sq)
                   | take(5);

  // 安全、f()の戻り値は生存期間内
  for (int m : pipes) {
    std::cout << n << ',';
  }
}

構造を簡単に書いてみると次のようになっています

  • pipes : take_view
    • base_ : transform_view
      • base_ : filter_view
        • base_ : drop_view
          • base_ : owning_view
            • r_ : std::vector<int>
        • pred_ : even_t
      • fun_ : sq_t

(変数名は規格書のものを参考にしていますが、この名前で存在するわけではありません)

このようにして、f()の戻り値である右辺値のstd::vectorオブジェクトの寿命は、パイプラインを通しても延長されています。views::filterが受け取る述語オブジェクトなども対応する層(viewオブジェクト内部)に保存されており、同様に安全に取り回し、使用する事ができます。

ref_viewの場合

先ほどの例のf()が左辺値を返している場合、パイプライン最初のdrop_view構築時のviews::all適用時には、ref_viewが適用されます。

#include <ranges>
#include <vector>

auto f() -> std::vector<int>&;

auto even = [](int n) { return 0 < n; };
auto sq = [](int n) { return n * n; };

using namespace std::views;

int main() {
  // f()の戻り値は参照されている
  auto pipes = f() | drop(2)
                   | filter(even)
                   | transform(sq)
                   | take(5);

  // f()で返されるvectorの元が生きていれば安全
  for (int m : pipes) {
    std::cout << n << ',';
  }
}

この時のpipesの型は先ほどowning_view<std::vector<int>>だったところがref_view<std::vector<int>>に代わるだけで、他起こることは同じです。

ref_viewは次のように定義されています。

namespace std::ranges {
  // コンストラクタ制約の説明専用の関数
  void FUN(R&);
  void FUN(R&&) = delete;

  template<range R>
    requires is_object_v<R>
  class ref_view : public view_interface<ref_view<R>> {
  private:
    // 参照はポインタで保持する
    R* r_;  // exposition only

  public:

    // 左辺値を受け取るコンストラクタ
    template<different-from<ref_view> T>
      requires convertible_to<T, R&> &&         // T(右辺値or左辺値参照)がR&(左辺値参照)へ変換可能であること
               requires { FUN(declval<T>()); }  // tが右辺値ならFUN(R&&)が選択され制約を満たさない
    constexpr ref_view(T&& t)
      : r_(addressof(static_cast<R&>(std<200b>::<200b>forward<T>(t))))
    {}

    constexpr R& base() const { return *r_; }

    constexpr iterator_t<R> begin() const { return ranges::begin(*r_); }
    constexpr sentinel_t<R> end() const { return ranges::end(*r_); }

    constexpr bool empty() const
      requires requires { ranges::empty(*r_); }
    { return ranges::empty(*r_); }

    constexpr auto size() const requires sized_range<R>
    { return ranges::size(*r_); }

    constexpr auto data() const requires contiguous_range<R>
    { return ranges::data(*r_); }
  };

  // 推論補助、左辺値参照からしか推論できない
  template<class R>
  ref_view(R&) -> ref_view<R>;
}

コンストラクタはかなりややこしいですが、推論補助と組み合わさって、確実に左辺値のオブジェクトだけを受け取るようになっています。そして、ref_viewは参照する範囲へのポインタを保持してラップすることでrangeRviewへと変換します。また、あえて定義されてはいませんが、ref_viewのコピー/ムーブ・コンストラクタ/代入演算子は暗黙定義されています。

パイプラインへの入力が左辺値である場合、パイプラインによって生成されたマトリョーシカの中心にはref_viewがおり、そこからは元の範囲をポインタによって参照しているわけです。

ref_viewはデフォルト構築不可能であるので、メンバのポインタr_nullptrとなることを考慮する必要はないですが、参照先のrangeオブジェクトが先に寿命を迎えれば容易にダングリングとなります。また、ポインタの関節参照のコストも(おそらく最適化で除去可能であるとはいえ)かかることになります。owning_viewが好まれたのは、これらの問題と無縁であることも理由の一つです。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で74本あり、SG22(C/C++相互互換性に関する研究グループ)のCの提案を除くと73本になります。

採択された文書

ここにあるのは10月の全体会議でワーキングドラフト入りが承認された提案です。ただし、今月提案文書の改定がないものもあるのでこれで全部ではありません。

P0798R8 Monadic operations for std::optional

std::optionalmonadic interfaceのサポートを追加する提案。

以前の記事を参照

R7(未公開)の変更は

  • 機能テストマクロの変更
  • 各関数の制約の調整

R8の変更は

  • transformの受け取る関数の戻り値型にremove_cvを適用
  • or_elseから事前条件を削除
  • transformがコピー省略行えるように調整

などです。

P1147R1 Printing volatile Pointers

標準ストリームにおいて、volatileポインタの<<による出力をできるようにする提案。

標準出力ストリームを使用してvolatileポインタを出力すると、予期しない値が出力されます。

int main() {
           int* p0 = reinterpret_cast<         int*>(0xdeadbeef);
  volatile int* p1 = reinterpret_cast<volatile int*>(0xdeadbeef);

  std::cout << p0 << std::endl; // 0xdeadbeef
  std::cout << p1 << std::endl; // 1
}

標準出力ストリームに対するストリーム出力演算子<<)では、const void*(非volatileポインタ)の出力を行うオーバーロードはあり、普通のポインタはこれを使用してアドレスが出力されます。しかし、volatileポインタはこのオーバーロードを使用できず(CV修飾が合わないため)、ポインタ->boolの変換を介してbool値として出力されます。

この提案は、operator<<(const volatile void*)オーバーロードを追加することによってこの意図しない変換を防止し、volatileポインタを適切に出力できるようにしようとするものです。

P1272R4 Byteswapping for fun&&nuf

バイトスワップ(バイトオーダー変換)のための関数std::byteswap()の提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正のみです。

P2077R3 Heterogeneous erasure overloads for associative containers

連想コンテナに対して透過的な要素の削除と取り出し方法を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックに基づいて提案する文言を修正したことです。

P2314R4 Character sets and encodings

規格文書中の ~ character setという言葉を明確に定義し直す提案。

以前の記事を参照

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

  • P1949の変更を適用する形で更新
  • CWGなどからのフィードバックによる提案する文言の改善

などです。

P2415R2 What is a view?

viewコンセプトの要件を緩和する提案。

以前の記事を参照

このリビジョンでの変更は提案する文言の修正のみです(多分)。

P2418R2 Add support for std::generator-like types to std::format

std::generator-likeな型に対する<format>のサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善(主にFormatter要件を要求するところの調整)です。

P2432R1 Fix istream_view, Rev 1

std::views::istream_view<T>()の他のviewとの非一貫性を正す提案。

以前の記事を参照

このリビジョンでの変更は例の修正と、提案する文言の修正です。

P2450R0 C++ Standard Library Issues to be moved in Virtual Plenary, Oct. 2021

今回の会議で採択された標準ライブラリについてのIssue報告とその解決。

  1. 2191. Incorrect specification of match_results(match_results&&)
  2. 2381. Inconsistency in parsing floating point numbers
  3. 2762. unique_ptr operator*() should be noexcept
  4. 3121. tuple constructor constraints for UTypes&&... overloads
  5. 3123. duration constructor from representation shouldn't be effectively non-throwing
  6. 3146. Excessive unwrapping in std::ref/cref
  7. 3152. common_type and common_reference have flaws in common
  8. 3293. move_iterator operator+() has incorrect constraints
  9. 3361. safe_range<SomeRange&> case
  10. 3392. ranges::distance() cannot be used on a move-only iterator with a sized sentinel
  11. 3407. Some problems with the wording changes of P1739R4
  12. 3422. Issues of seed_seq's constructors
  13. 3470. convertible-to-non-slicing seems to reject valid case
  14. 3480. directory_iterator and recursive_directory_iterator are not C++20 ranges
  15. 3498. Inconsistent noexcept-specifiers for basic_syncbuf
  16. 3535. join_view::iterator::iterator_category and ::iterator_concept lie
  17. 3554. chrono::parse needs const charT* and basic_string_view<charT> overloads
  18. 3557. The static_cast expression in convertible_to has the wrong operand
  19. 3559. Semantic requirements of sized_range is circular
  20. 3560. ranges::equal and ranges::is_permutation should short-circuit for sized_ranges
  21. 3561. Issue with internal counter in discard_block_engine
  22. 3563. keys_view example is broken
  23. 3566. Constraint recursion for operator<=>(optional<T>, U)
  24. 3567. Formatting move-only iterators take two
  25. 3568. basic_istream_view needs to initialize value_
  26. 3570. basic_osyncstream::emit should be an unformatted output function
  27. 3571. flush_emit should set badbit if the emit call fails
  28. 3572. copyable-box should be fully constexpr
  29. 3573. Missing Throws element for basic_string_view(It begin, End end)
  30. 3574. common_iterator should be completely constexpr-able
  31. 3580. iota_view's iterator's binary operator+ should be improved
  32. 3581. The range constructor makes basic_string_view not trivially move constructible
  33. 3585. Variant converting assignment with immovable alternative
  34. 3589. The const lvalue reference overload of get for subrange does not constrain I to be copyable when N == 0
  35. 3590. split_view::base() const & is overconstrained
  36. 3592. lazy_split_view needs to check the simpleness of Pattern
  37. 3593. Several iterators' base() const & and lazy_split_view::outer-iterator::value_type::end() missing noexcept
  38. 3595. Exposition-only classes proxy and postfix-proxy for common_iterator should be fully constexpr

P2462R0 Core Language Working Group “ready” issues for the October, 2021 meeting

今回の会議で採択されたコア言語についてのIssue報告とその解決。

  1. 1249. Cv-qualification of nested lambda capture
  2. 1724. Unclear rules for deduction failure
  3. 1726. Declarator operators and conversion function
  4. 1733. Return type and value for operator= with ref-qualifier
  5. 2484. char8_t and char16_t in integral promotions
  6. 2486. Call to noexcept function via noexcept(false) pointer/lvalue
  7. 2490. Restrictions on destruction in constant expressions
  8. 2491. Export of typedef after its first declaration
  9. 2496. ref-qualifiers and virtual overriding

その他の文書

N4896 PL22.16/WG21 agenda: 4 October 2021, Virtual Meeting

2021年10月4日 08:00 (北米時間)に行われたWG21本会議のアジェンダです。

C++23のための3回目の全体会議です。

N4897 WG21 admin telecon meeting: September 2021

WG21の各作業部会の管理者ミーティング。

前回から今回の会議の間のアクティビティの報告がされています。

N4898 WG21 2021-10 Virtual Meeting Minutes of Meeting

2021年10月4日(北米時間)に行われた、WG21全体会議の議事録。

CWG/LWG/LEWGの投票の様子などが記載されています。

N4899 WG21 admin telecon meetings: 2022

次回以降のWG21の各作業部会の管理者ミーティング。

次は2022年1月24日に予定されています。

N4900 WG21 virtual plenary meeting(s): 2022

次回以降のWG21全体会議の予定表。

次は2月7日に予定されています。

N4901 Working Draft, Standard for Programming Language C++

C++23ワーキングドラフト第5弾。

N4902 Editors' Report - Programming Languages - C++

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

10月の会議で採択された提案とコア言語/ライブラリのIssue解決が適用されています。

P0009R13 MDSPAN

多次元配列に対するstd::spanである、mdspanの提案。

以前の記事を参照

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

  • P2299R3の内容の適用
  • 提案する文言の修正
  • LEWGのガイダンスに従って、設計に関する議論を追記
  • ランク0mdpsnarequired_span_sizeのレイアウト修正
  • 少なくとも1つの要素数指定が0であるmdspanlayout_stride::required_span_sizeを修正
  • operator[]を多次元配列アクセスに使用
  • spanへの文言の参照を解消
  • layout_strideストライドとユニークレイアウトのための変換コンストラクタを追加
  • mdspanにポインタとエクステントからのコンストラクタを追加
  • layout policy mappingnothrow move constructible/assignableの要件を追加
  • accessor policynothrow move constructible/assignableの要件を追加
  • accessor policyaccessor policy pointerの要件を追加
  • mdspan/submdspanが何も例外を投げないという指定を削除

などです。

P0627R6 Function to mark unreachable code

到達不可能なコード領域であることを示す、std::unreachable()の提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の機能テストマクロ表現を修正したことです。

この提案はすでにLWGのレビューを終えており、次の全体会議で投票にかけられる予定です。

P1169R3 static operator()

関数呼び出し演算子operator())を、静的メンバ関数として定義できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したこと、LWG Issue 3617の変更を前提とした文言の追加です。

P1467R5 Extended floating-point types and standard names

C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。

以前の記事を参照

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

  • 文言と設計に関する文書を個別のセクションに分割
  • C言語との互換性に関するドキュメントの追記
    • 2つの言語での同じ浮動小数点数型を指す型名について
    • 2つの言語の間の拡張浮動小数型の変換(usual arithmetic conversion)の違いについて
  • 可変引数関数に渡した時のdoubleへの昇格を削除
  • <format>周りの文言に関する説明を追記(不要だった)
  • I/Oストリームに、long double以下の幅を持つ拡張浮動小数点数型のサポート追加
  • <charconv>/<cmath>の文言についての背景の追記
  • よく知られている浮動小数点数に対応する型エイリアスの名前を、std::floatN_tとする事を決定

などです。

P1642R7 Freestanding Library: Easy [utilities], [ranges], and [iterators]

[utility]<ranges><iterator>から一部のものをフリースタンディングライブラリに追加する提案。

前回の記事を参照

このリビジョンでの変更は、<atomic>がフリースタンディングであり続けるように文言を調整したこと、std::unreachableの追加に関する議論を追記したことです。

この提案では、<utility>全体をフリースタンディングとしていますが、std::unreachable<utility>に追加される予定のため衝突しています。std::unreachableをフリースタンディングとすることに対する投票が行われたようですが、反対する人はいなかったようです。

P1673R5 A free function linear algebra interface based on the BLAS

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

このリビジョンでの変更は、mdspan()に代わって[]を使用するようになったため、その変更を反映したことです。

どうやらこの提案はC++26を目指すことになったようです。

P1854R1 Conversion to literal encoding should not lead to loss of meaning

文字列リテラルエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。

文字列リテラルエンコーディングソースコードエンコーディング)と実行時エンコーディングが異なるケースは比較的よく発生し、その時の振る舞いは実装定義とされています。

#include <cstdio>

int main() {
  puts("こんにちは");
}

このコードをUTF-8で保存し、実行時エンコーディングをAsciiとしてコンパイルすると、MSVCでは警告のみでコンパイルが通るのに対してGCCではエラーとなり、その際MSVCはAscii内の代替文字(?)で文字を置換しています。

この事はC++プログラムの移植性を損ねており、文字列は意味や目的を持つ文書であるので実装がその意味を変えるべきではなく、このようなエンコーディングの縮小変換が起こる場合をill-formedと規定しようとする提案です。

この提案ではまた、複数の文字によって1文字を構成するユニコード文字(é🇯🇵など)がマルチキャラクリテラルとして読み取られて意図しない1文字になる場合をエラーとする事も同時に提案しています。

int main() {
  [[maybe_unused]]
  char c = '🇯🇵';
}

マルチキャラクリテラルに含まれる文字は基本リテラル文字集合の要素のみ、と規定する事でこのような文字がコンパイルエラーととなるようにします。これによって、結合文字などの不可視文字が排除されるため見た目の曖昧さが解消され、マルチキャラクリテラルの結果がintに収まるようになります。

P1885R8 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

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

などです。

P2012R2 Fix the range-based for loop, Rev2

現在のrange-based forに存在している、イテレーション対象オブジェクトの生存期間にまつわる罠を修正する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善などです。

P2066R10 Suggested draft TS for C++ Extensions for Minimal Transactional Memory

現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。

以前の記事を参照

このリビジョンの変更点は、LWGのフィードバックに対応して文言を修正した事です。

この提案は全体会議で承認され、「Minimal Transactional Memory TS」として発行されることになります。

P2248R2 Enabling list-initialization for algorithms

値を指定するタイプの標準アルゴリズムにおいて、その際の型指定を省略できるようにする提案。

以前の記事を参照

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

  • 理論的根拠について詳しく追記
  • 出力イテレータを使用するアルゴリズムのデフォルトvalue_typeについての説明の追記
  • テスト実装のリンク追加
  • *_scan系数値アルゴリズムについてのデフォルトを修正

などです。

P2249R2 Mixed comparisons for smart pointers

スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正です。

P2255R2 A type trait to detect reference binding to temporary

一時オブジェクトが参照に束縛されたことを検出する型特性を追加し、それを用いて一部の標準ライブラリの構築時の要件を変更する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロの追加、std::make_from_tupleをこの提案に沿って修正、提案する文言の変更などです。

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

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

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

R1での変更は、ローカルなテスト実装を追記したこと、CとC++の提案する文言を分離したこと、フィードバックを受けた議論を追加したことです。

R2での変更は、さらに議論を追記したことなどです。

この提案はCとC++の両方に対して提出されていますが、Cの方の投票では一旦否決されているようです。

P2291R3 Add Constexpr Modifiers to Functions to_chars and from_chars for Integral Types in Header

std::to_chars, std::from_charsを整数変換に関してconstexprにする提案。

以前の記事を参照

このリビジョンでの変更は、ヘッダの肥大化防止やコンパイル時間削減のためコンパイラ組み込み命令で実装されうることについて追記したことです。

P2300R2 std::execution

P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。

以前の記事を参照

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

  • 即時実行senderアルゴリズムの削除
  • connectカスタマイゼーションポイントとsender_traits<>を拡張して、awaitable(コルーチンPromise型)をtyped_senderとして扱えるようにした
  • as_awaitable(), with_awaitable_senders<>を追加して、コルーチン型がコルーチン内でsenderを待機可能にした
  • sender/awaitableの相互作用について説明を追記
  • sender/recieverのキャンセルサポートの設計についての説明を追記
  • 単純なsenderアダプタアルゴリズムの例を示すセクションを追加
  • 単純なschedulerの例を示すセクションを追加
  • 数独ソルバ、並行再帰ファイルコピー、エコーサーバー、の例を追記
  • bulkアルゴリズムforward progress保証を改善
  • 様々なsenderを使用して非同期シーケンスを表現する方法を説明するセクションの追加
  • senderを使用して、部分的に成功を表す方法を説明するセクションの追加
  • senderファクトリjust_error, just_doneを追加
  • senderアダプタdone_as_optional, done_as_error
  • sender/recieverの製品

などです。

今月の文書にもやりあっているのが見られますが、NetworkingTSのベースとなるExecutorとしてこの提案のschedulerがふさわしくないことから、議論が紛糾しているようで、進捗が芳しくなさそうです・・・。

P2322R5 ranges::fold

rangeアルゴリズムであるranges::foldの提案。

以前の記事を参照

このリビジョンでの変更は、プロジェクションを削除したこと、foldl_while系の操作を削除したこと、全てのオーバーロードがプロジェクションを取らないようになったこと、各関数の名前をfold_left, fold_left_first, fold_right, fold_right_lastに暫定的に変更したことなどです。

プロジェクションは、初期値を最初の要素から取得するタイプ(fold_left_first/fold_right_last)の場合に、プロクシオブジェクトを返すイテレータ(例えばzip)を適切に扱うには、余分なコピーが避けられない問題を回避するために削除されました。

このリビジョンの提出後、名前はfold_left, fold_left_first, fold_right, fold_right_lastで正式に決定し、LEWGの投票で合意が取れればLWGに転送される予定です。

P2324R1 Labels at the end of compound statements (C compatibility)

複合ステートメントcompound statement)の末尾にラベルを置けるようにする提案。

このリビジョンでの変更は、3つ提案していた文言を1つに絞ったこと、SG22での投票結果を記載したこと、実装経験について追記したこと、Cと同様の暗黙のnullステートメントを追加できるようにしたこと、などです。

P2327R1 De-deprecating volatile compound operations

非推奨となったvolatile値に対する複合代入演算子を再考する提案。

このリビジョンでの変更は、bitwise演算子の複合代入演算子|= &= ^=)の非推奨解除のみを提案するようにしたことです。

この提案はEWGの議論を通過し、CWGに送られるためのEWGの投票待ちです。なお、C++20へのDRとなるようです。

P2347R2 Argument type deduction for non-trailing parameter packs

関数テンプレートの実引数型推論時に、引数リスト末尾にないパラメータパックの型を推論できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、テスト実装を使用してより広いコンテキスト(クラス、関数テンプレートのアドレス取得、エイリアステンプレート、部分特殊化)でこの推論が上手くいくかの調査の結果を追記したことです。調査では、特に問題は見つからなかったようです。

P2348R2 Whitespaces Wording Revamp

ユニコードの仕様に従う形で、改行と空白を明確に定義する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正です。

この提案はSG16とEWGのレビューを通過し、CWGへ送るためのEWGの投票待ちをしています。

P2350R2 constexpr class

constexpr対応クラスの簡易構文の提案。

以前の記事を参照

このリビジョンでの変更は、例を追加したこと、EWGでの投票の結果を記載したこと、constexprクラスのstaticメンバ変数はconstexprとなることを明確にしたこと、constexpr(false)が必要な理由と不要な理由を記載したこと、などです。

メンバ関数の一部がCのAPIに依存していたりするとその関数はconstexpr化不可能ですが、そのクラスをconstexprクラスにしてしまうとそのようなメンバ関数が1つあるだけでエラーとなってしまいます。そのため、選択的に非cosntexprにするためにconstexpr(false)が必要だ、というフィードバックが寄せられたようです。ただし、その場合でもクラス外で定義することで回避可能であったり、P2448R0が採択されればその問題を回避できるとして、この提案ではconstexpr(false)を提案していません。

P2361R3 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

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

  • プリプロセッシングトークンはコンテキストに依存してはならないことからunevaluated-stringプリプロセッシングトークンとしないように文言を改善した
  • 文字列リテラルの評価中にその文字列をnull終端することで、unevaluated-stringがnull終端されていないことを明確化
  • literal-operator-idの文言にunevaluated-stringを適用
  • externの文言にunevaluated-stringを適用し、リンケージ指定がユニコード文字を示すことを明確化
  • asmステートメントでの数値エスケープシーケンスの許可

などです。

P2384R1 2021 Spring Library Evolution Poll Outcomes

2021年の春(4月から6月にかけて)に行われた、LEWGの全体投票の結果。

以前の記事を参照。

このリビジョンの変更は引用する文書が間違っていたのを修正しただけです。

P2387R2 Pipe support for user-defined range adaptors

ユーザー定義のRangeアダプタに対して、パイプライン演算子|)サポートを簡単に行えるユーティリティを提供する提案。

以前の記事を参照

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

この提案はSG9とLEWGのレビューを終え、LWGに送られるための投票待ちをしています。C++23入りを目指しています。

P2388R3 Minimum Contract Support: either No_eval or Eval_and_abort

契約が破られた時に継続しないコントラクトサポートを追加する提案。

以前の記事を参照

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

  • 構文について、属性との混同、Cとの潜在的な非互換性について追記
  • 契約モード名をNo_eval、Eval_and_abort*に変更
  • "function parameter"とすべきところを"function argument"としていたところを修正
  • 事後条件で非const非参照仮引数を使用できるようにする解決策の案として、暗黙const扱いにする例を追加
  • セキュリティの懸念に対処するため、violation handlerが標準出力にメッセージを出力することを推奨する文言を削除
  • 事後条件で参照されている関数引数をconst_castした場合に未定義動作となる説明を追記
  • 契約条件部分式ごとの副作用削除についての議論の追記
  • 実装可能性についてのセクションを追加
  • 契約違反時にstd::terminate()ではなく、std::abort()を呼び出す理由について追記
  • 契約条件がimmediate contextにないことを明確にした
  • 構文の選択、副作用の説明、プログラミングモデルの有効性、に関する未解決の問題の追加

などです。

P2400R2 Library Evolution Report: 2021-06-01 to 2021-09-20

2021年6月から9月にかけての、LEWGでのミーティングについてのまとめ。

どれくらいミーティングを行ったのか、おおまかな機能単位についての進行状況、レビューを行った提案についての議論の状況などが記載されています。

P2408R2 Ranges iterators as inputs to non-Ranges algorithms

非Rangeアルゴリズムイテレータに対する名前付き要件を、イテレータコンセプトで置き換える提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更、input iteratoroutput iteratorの要件についてイテレータコンセプトを使用するようにしていた部分の削除、mutable iteratorに関する議論の追加、zip_viewイテレータなどについての説明の追記。

この提案はSG9のレビューを通過し、LEWGへ転送されました。

P2417R1 A more constexpr bitset

std::bitsetconstexpr対応させる提案。

以前の記事を参照

このリビジョンでの変更は、設計についての議論と実装経験についてを追記したことです。

MSVC STLbitset実装をベースにcosntexpr化を行い、実装できることと、既存のテストをパスすることを確かめたようです。

P2435R1 2021 Summer Library Evolution Poll Outcomes

2021年の夏(7月から9月にかけて)に行われた、LEWGの全体投票の結果。

以前の記事を参照。

このリビジョンの変更は引用する文書が間違っていたのを修正しただけです。

P2445R0 forward_like

クラス型のメンバ変数について、const性も含めた正しい完全転送を行うstd::forward_likeの提案。

これはDeducing Thisによって頻出するであろうコードを正しく書くためのユーティリティです。

template<typename T>
struct wrap {
  T v;

  template<typename Self>
  auto value(this Self&& self) -> decltype(auto) {
    return std::forward<Self>(self).v; // あってる?
  }
};

この.value()メンバ関数thisself)の値カテゴリに応じてメンバvの返す型(値カテゴリ)を変化させたいわけです。C++20まではstd::optional.value()のようにconstと参照修飾によって4つのオーバーロードに分けていましたが、C++23以降はDeducing Thisによってthisに対応する引数を明示的に取れるようになった事から1つの関数にまとめることができるようになります。

その際、thisの状態によってメンバを完全転送するにはおおよそ上記のように書くことになるのですが、メンバが参照である場合やthisconstである場合に正しい結果になりません。次の表は上記のように書いた時の.value()の戻り値型がthisの状態とメンバの状態によってどうなるかを示したものです

thisself メンバ(v std::forwad(self).v
&&
& &
&& &&
const const &&
const & const &
const && const &&
const const &&
& const const &
&& const const &&
const const const &&
const & const const &
const && const const &&
& &
& & &
&& & &
const & &
const & & &
const && & &
&& &
& && &
&& && &
const && &
const & && &
const && && &
const & const &
& const & const &
&& const & const &
const const & const &
const & const & const &
const && const & const &
const && const &
& const && const &
&& const && const &
const const && const &
const & const && const &
const && const && const &

表中の空白はconstも参照修飾もない状態、すなわち単なる値として宣言されている場合です(thisの場合はコピーされている、すなわちprvlaueの状態)。

これを見ると、std::forward<Self>(self).vのようなコードが問題ないのは、vが非参照である時くらいのものです。メンバが参照である場合はthisconstが正しく伝わっておらず、メンバがconst参照の場合はthisの値カテゴリ(特に右辺値&&)が正しく伝播されていません。

std::forward_likeはこの伝播を正しく行い完全転送するためのもので、std::forwardとよく似ています。forward_likeを使うと先ほどのコードは次のように書き換えられます

template<typename T>
struct wrap {
  T v;

  template<typename Self>
  auto value(this Self&& self) -> decltype(auto) {
    return std::forward_like<Self>(self.v);
  }
};

次の表は、先ほどの表にforward_likeの結果を追記したものです

this メンバ forwad forward_like
&& &&
& & &
&& && &&
const const && const &&
const & const & const &
const && const && const &&
const const && const &&
& const const & const &
&& const const && const &&
const const const && const &&
const & const const & const &
const && const const && const &&
& & &&
& & & &
&& & & &&
const & & const &&
const & & & const &
const && & & const &&
&& & &&
& && & &
&& && & &&
const && & const &&
const & && & const &
const && && & const &&
const & const & const &&
& const & const & const &
&& const & const & const &&
const const & const & const &&
const & const & const & const &
const && const & const & const &&
const && const & const &&
& const && const & const &
&& const && const & const &&
const const && const & const &&
const & const && const & const &
const && const && const & const &&

std::forwadの時と比較すると、メンバが参照である場合にも正しく(理想的に)constと値カテゴリが伝播しているのが分かります。Deducing Thisで可能になることを考えればこのようなコードは良く書かれることが予想され、forward_likeの必要性はDeducing Thisの提案中でも指摘されていました。

std::forward_likeは簡単には次のように実装されます。

// T->Uへ参照(値カテゴリ)を伝播する
template <typename T, typename U>
using __override_ref_t = std::conditional_t<std::is_rvalue_reference_v<T>,
                                            std::remove_reference_t<U>&&, U&>;

// T->Uへconstをコピーする
template <typename T, typename U>
using __copy_const_t = std::conditional_t<std::is_const_v<std::remove_reference_t<T>>,
                                          U const, U>;

// forward_likeの結果型を求める
template <typename T, typename U>
using __forward_like_t = __override_ref_t<T&&, __copy_const_t<T, std::remove_reference_t<U>>>;


template <typename T>
[[nodiscard]]
constexpr auto forward_like(auto&& x) noexcept -> __forward_like_t<T, decltype(x)> {
  return static_cast<__forward_like_t<T, decltype(x)>>(x);
}

各メタ関数のT*thisの、Uはメンバの型が来ます。すなわち、なるべくthisの値カテゴリを利用してメンバの値カテゴリを指定しつつthisconstを伝播しようとするものです。

値カテゴリによって振る舞いを変化するラムダ式によるコールバックのサンプル

auto callback = [m=get_message(), &scheduler](this auto&& self) -> bool {
  return scheduler.submit(std::forward_like<decltype(self)>(m));
};

callback();             // retry(callback)
std::move(callback)();  // try-or-fail(rvalue)

この例のように、Deducing thisによってキャプチャしているラムダを扱う際にforward_likeは特に重要となります。

間接的な所有権を持つクラスにおける、所有するメンバの転送のサンプル

struct fwd {
  std::unique_ptr<std::string> ptr;
  std::optional<std::string> opt;
  std::deque<std::string> container;

  auto get_ptr(this auto&& self) -> std::string {
    if (ptr) {
      return std::forward_like<decltype(self)>(*ptr);
    }
    return "";
  }

  auto get_opt(this auto&& self) -> std::string {
    if (opt) {
      return std::forward_like<decltype(self)>(*m);
    }
    return "";
  }

  auto operator[](this auto&& self, size_t i) -> std::string {
    return std::forward_like<decltype(self)>(container[i]); // dequeは右辺値用[]を持たない
  }
};

この例では、ポインタを介した所有などによってthisconst性および値カテゴリの伝播が断ち切られているケースでも、Deducing Thisとforward_likeによって望ましい転送を実現しています。

P2447R0 std::span and the missing constructor

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

現在のstd::spanにはstd::initializer_listを受け取るコンストラクタがなく、次のようなコードがエラーとなります。

void foo(std::span<const int>);

int main() {
  foo({1, 2, 3}); // error
}

この提案はstd::spanにはstd::initializer_listを受け取るコンストラクタを追加してこれがコンパイルできるようにしようとするものです。

この提案のモチベーションは、従来std::vectorを利用していた関数引数をstd::spanに置き換えた時に発生する問題を解決することにあります。

// C++17までのインターフェース
void foo(const std::vector<int>&);

// C++20からはspanを使い書き換えたい
void foo(std::span<const int>);

int main() {
  foo({1, 2, 3}); // C++17まではOK、C++20からはエラー
}

この変更が適用されると{}によって簡単にダングリングstd::spanを作れてしまうようになりますが、現状でもダングリングspanは割と簡単にできてしまい、std::spanviewである以上それは避けられないため、std::initializer_listを取れるようにすることのメリットの方が大きいという主張です。

また、std::initializer_listコンストラクタは{}初期化時に最優先で選択されることから、この変更は破壊的変更となります。しかし、std::initializer_listコンストラクタに制約を設けることでこの影響は低減でき、破壊的となるのは非常にレアケースとなるようです。

const std::vector<int>;

// この変更の後でも、これらの振る舞いは変化しない
auto sp1 = std::span{v};
auto sp2 = std::span{v.begin(), v.end()};
auto begin = v.data();
auto end = begin + v.size();
auto sp3 = std::span{begin, end};

// この提案以前はvoid*1つのspan
// この提案の後では、void*2つのspan
void* vp = nullptr;
span<void* const> sp4{&vp, &vp+1};

// この提案以前はany1つのspan
// この提案の後では、any2つのspan
std::any a;
std::any* ap = &a;
span<std::any> sa{&ap, &ap+1};

std::spanの主な用途の一つは関数引数で使用することです。その場合、spanの要素型をテンプレートパラメータにすることはできないためspanの要素型は明示的に指定されているはずです。従って、void*などの特殊な要素型を考慮する必要は通常ないため、破壊的変更はやはり問題とならないとのことです。

P2448R0 Relaxing some constexpr restrictions

constexpr関数がすべての引数について定数実行不可能となる場合でも、コンパイルエラーにしないようにする提案。

例えば、次のコードはC++20まではエラーとなりますが、C++23以降(P2231R1実装後)は定数実行可能となるためエラーになりません。

constexpr void h(std::optional<int>& o) {
  o.reset();  // C++20まで非constexpr
}

現在の仕様では、constexpr関数がそのあらゆる引数について定数実行可能ではない場合コンパイルエラー(もしくは診断不要のill-formed)とする事が求められています。そのため、constexpr関数内で非constexpr関数を呼び出す場合は常にコンパイルエラーとなります。

これを適応的にコンパイルするため、機能テストマクロを使用して次のようにすることができます

#if __cpp_lib_optional >= 202106
constexpr
#endif
void h(std::optional<int>& o) {
    o.reset();
}

しかし、この方法は適切でしょうか?

std::optionalのこの例に見えるように、ある関数がある時点でconstexprではなかったとしても将来的にconstexprになる可能性があります。constexprのルールはC++のバージョンごとに拡張されているため、それまでconstexpr対応できなかった処理でも、あるバージョンからはconstexpr化することができるようになり得ます。しかし、標準ライブラリのサポートはそれが可能になったタイミングから遅れてそれに対応し、在野のライブラリはさらに遅れるのが常です。またこのような機能テストマクロが非標準のものについて提供されることはほぼなく、在野のライブラリを使用している場合、このような対応は手作業で行う必要があります。

このような診断は定数式で実行可能なものが大きく制限されていたC++11の時代には有用だったのかもしれませんが、より多くのものが定数式で実行可能となった現在(C++23)の環境には即しておらず、constexprでの実行可否の診断は実際に定数式で実行する時にまで遅延しておく方が良いでしょう。この提案は、そのような規定を取り除こうとするものです。

P2451R0 2021 September Library Evolution Poll Outcomes

2021年の秋に行われた、LEWGの全体投票の結果。

以下の5つの提案が投票にかけられ、LWGに転送されることが可決されています。また、その際に寄せられたコメントが記載されています。

P2460R0 Relax requirements on wchar_t to match existing practices

wchar_tエンコーディングについての実態になじまない制約を取り除く提案。

現在のC++標準におけるwchar_tエンコーディングに関する規定は、ワイド文字エンコーディングのすべての文字を単一のコード単位としてエンコーディングする必要がある(すなわち、すべての文字がwchar_t1つに収まらなければならない)ことを要求しています。例えばWindowsではそれはUTF-16であり、UTF-16サロゲートペアを持つためその要件に違反していることになります。

さらに、wchar_tの値はサポートされているロケールで指定された拡張文字セットの個別の文字を表現できることも要求されているため、実行字文字集号がUTF-8の場合はすべてのユニコード文字をwchar_t1つで表現可能であることが求められ、実質的に2バイトエンコーディングを排除しています。

これらのことは実際の実装(主にWindows)に沿っておらず、C++を多様なOS上での開発に適さないものにしてしまっています。そのため、実装の慣行に従う形でこれを修正するのがこの提案の目的です。

この提案による変更は実装ですでに行われていることを標準化するだけなので、実装及びプログラマに影響はありません。

P2461R0 Closure-based Syntax for Contracts

属性likeな構文に代わるコントラクト構文の提案。

提案されている構文はpre, post, assertの3つの文脈依存キーワードに続いて、ラムダ式(に近い構文)によって契約条件を記述します。pre/postは後置戻り値型の後ろのrequires節と同じ位置に書き、assertは関数本体内に書きます。

auto plus(auto const x, auto const y) -> decltype(x + y)
  pre { x > 0 }
  pre {
    // オーバーフローのチェック
    // ここに書けるものは条件式だけなので、&&で結合する
    (x > 0 && y > 0 ? as_unsigned(x) + as_unsigned(y) > as_unsigned(x) : true) &&
    (x < 0 && y < 0 ? as_unsigned(x) + as_unsigned(y) < as_unsigned(x) : true)
  }
  // retはauto&&と宣言したかのような引数、戻り値が渡される
  post (ret) { ret == (x + y) }
{
  assert { x > 0 }; // この構文は現在有効(assert構造体の集成体初期化)
  auto cx = x;
  return cx += y;
}

ただし、この提案は純粋に構文のみを提案するもので、P2388の意味論をほぼ踏襲しており、P2388およびMVP(Minimum Viable Product)と呼ばれるコントラクトの最小セットに異を唱えるものではありません。この提案は、P2388で示されているいくつかの問題を解決可能な構文を探索し、提案するものです。

構文定義は次のようになっています

correctness-specifier:
    correctness-specifier-keyword correctness-specifier-introducer(opt) correctness-specifier-body

correctess-specifier-keyword:
    pre | post | assert

correctness-specifier-introducer:
    lambda-introducer(opt) return-value-id(opt)

return-value-decl:
    ( identifier )

correctness-specifier-body:
    { conditional_expression }

pre, post, assertの後にはラムダ式(のような)構文を置くことができて、ラムダキャプチャも機能します。省略された場合は[&]から始まったものとみなされています。ただし、現在合意が取れているMVPと呼ばれるコントラクトの最小セットに従う場合、ラムダ導入子は常に省略されなければなりません。 correctess-specifier-keywordに続く{}の中には条件式のみを書くことができて、これはrequires節と同じです。

ラムダ導入子を許可しなかったとしても、この構文ではP2388で導入される未定義動作を減らすことができるようです。

ラムダ導入子を許可すると、コピーキャプチャによってP2388に存在する事後条件で非const引数を参照できないという問題を解決することができます。これは事後条件から参照される実引数がタイミングによって変化しうることで条件の意味が変わってしまうことを防止するために現在(MVP)では禁止されています。

P2463R0 Slides for P2444r0 The Asio asynchronous model

P2444の解説スライド。

ASIOの非同期モデルとExecutor、非同期処理グラフの構成などが作者自らによって解説されています。

P2464R0 Ruminations on networking and executors

P0443のExecutorを使用するNetworking TSを標準化すべきではないという提案。

この提案で問題とされているのはNetworking TSの完了ハンドラがExecutorそのものがエラーを起こさないと仮定している事、およびP0443のexecutorにエラーや処理の成否を通知するチャネルが無いこと、Networking TSのExecutorと完了ハンドラによる非同期モデルはsender/recieverと比較するとジェネリックでも構成可能でも無いことです。

Networking TS(ASIO)の非同期処理ではI/O処理の継続作業を完了ハンドラで行います。完了ハンドラではAssociated Executorを取得し利用することでそのI/O処理や完了ハンドラの実行されているコンテキストに継続作業を投入することができます。このAssociated ExecutorはP0443のexecutorではありますがschedulerではなく、処理のスケジューリングに伴うエラーや、先行する処理の失敗を継続処理に通知する方法がありません。そして、Associated Schedulerのようなものを取得するAPIはなく、非同期処理グラブの構成もその場しのぎ的な書き方しかできません。また、実行コンテキストをAssociated Executor以外のExecutorのコンテキストに変更する方法もありません。

Executorライブラリの導入後、幾つものExecutorが作成されることになり、そこではExecutorの表現力不足に伴う問題を解決する事を繰り返し行う必要が出てきます。Executorはワークランナーの抽象化でしかなく、汎用的なエラー処理/検出の方法を提供するものではありません。従って、Networking TSのおよびP0443のexecutorとは、schedulerである必要があります。schedulerexecutorと同等の能力を持ちながら、recieverによる成否とスケジュールエラーの通知チャネル、senderによる非同期処理グラフのジェネリックな構成をサポートしています。

その場合、Networking TSの非同期処理は完了ハンドラの代わりにsenderを受け取るようになるでしょう。senderはあらかじめ構成された非同期処理のグラフを示す非同期エージェントであり、Networking TSから提供されるschedulerをそのまま使用することも、別のschedulerに実行コンテキストをスイッチすることもできます。さらに、senderによる構成では、前の処理およびスケジューリング段階でエラーが発生している場合後続の処理は実行されず、そのエラーをハンドリングする方法が提供されます。そして、それらの処理はジェネリックに構成することができ、特定のschedulerおよび特定の継続操作に依存せず、完了ハンドラのようにアドホック的ではありません。

// 擬似コードによる非同期I/Oと継続の例
void run_that_io_operation(scheduler auto sched,
                           sender_of<network_buffer> auto wrapping_continuation)
{
  // 特定のI/O操作
  launch_our_io();
  // I/O結果の取得
  auto [buffer, status] = fetch_io_results_somehow(maybe_asynchronously);
  // ユーザーの指定した継続を、ユーザーの指定したschedulerで実行する
  run_on(sched, queue_onto(wrapping_continuation,
                           make_it_queueable(buffer, status)));
}

ここでは、schedulerの具体的な型もsenderwrapping_continuation)の具体的な型も知る必要はありません。この関数の作者が詳しく知る必要があることはI/O操作に関することのみです。継続ラッパ(wrapping_continuation)が何をするかも知る必要がなく、それおよびそれがどこで実行されるのかを決めるのはこの関数を利用するユーザーです。

ユーザーはsenderの先に自由に継続を構成でき、そのユーザーがラップした処理を使用する別のユーザーもまた同様の方法によって継続をカスタマイズすることができます。この関数の作者は上位のユーザーが何をしたのかを知ることなくI/O処理を実行し、schedulersenderを投入でき、上位ユーザーはI/Oが何をしたのか継続をどう実行したのかを知ることなく、その結果から変換された指定した継続を実行することができます。

多数の実行戦略(executor/scheduler)とさらに多数の継続(完了ハンドラやsender)をサポートするために、Networking TSは多数のもの(追加の関数の変種)を必要としますがP2300(sender/reciever)では1つだけですみます。

P0443のexecutorはこれができません。executorはあらゆる種類の失敗やエラーを通知する方法を持ちません。そして、Networking TSでの入力実行戦略はexecutorのみです。

この提案が示しているのは次の2つの方向性です。

  1. Networking TSのExecutorモデルをP0443からP2300へ変更する
  2. C++23 Networking TSの標準化よりも、sender/reciever(P2300)の開発に集中する
    • ネットワークライブラリはその完成後にそれにフィットするものを標準化する

筆者の方は2つ目を推しているようです。

P2465R0 Standard Library Modules std and std.all

P2465R1 Standard Library Modules std and std.compat

標準ライブラリモジュールについて最小のサポートをC++23に追加する提案

これは以前のP2412R0を受けて、具体的にその内容を詰めるものです。

P2412R0のLEWGのレビューにおいて問題となった最大の事項は、import std;stdモジュール)がstd名前空間ではなくグローバル名前空間にあるものを含むかどうかという点でした。現在のC++標準では、<meow.h>::meowを確実に提供しstd::meowを提供するかは分かりません、<cmeow>::meowstd::meowの両方を確実に提供します。これは説明も対処も厄介な問題であり、またグローバル名前空間の暗黙の汚染をもたらします。標準ライブラリモジュールはこの問題を解決する唯一の機会だと言えます。

ただ、stdモジュールがグローバル名前空間にあるものを提供しない場合、既存のコードの標準モジュール対応が妨げられてしまいます。

そこでこの提案では、2つの標準ライブラリモジュールを提供することでユーザーに選択肢を与え、この問題に対処します。

  • import std;
    • C++標準ライブラリの全てをインポートする。グローバル名前空間を清潔に保ちたい場合
  • import std.compat;
    • std+Cライブラリヘッダの部分をインポートする。C由来のものがグローバル名前空間にインポートされる。既存コードベースの移行時など
    • import std;よりも重くなる可能性があるが、C互換ヘッダはサイズが小さいためそこまで問題にならないはず

stdstd.compatの違いはグローバル名前空間に何かをインポートするかどうかだけです。stdモジュール使用時、グローバル名前空間は清浄に保たれます。例えば、<cmath>std名前空間にもグローバル名前空間にもほぼ同じオーバーロードが定義されますが、import std;の場合はstd::~だけが使用可能となり、import std.compat;の時はそれに加えて::~のものも使用可能になります。

この提案は現在の内容でLWGに転送するために、次のLEWG全体投票にかけられます。

P2466R0 The notes on contract annotations

C++契約プログラミングのビジョンと設計目標について解説した文書。

関数コントラクト(function contract)という概念とコントラクト注釈(contract annotation)の関係を説明し、コントラクト注釈に求められることや役割について説明されています。また、事後条件で関数の非参照const引数を使用する場合の問題点と解決策の考察が行われています。

P2468R0 The Equality Operator You Are Looking For

==から導出される!=演算子とユーザー定義!=演算子が衝突してしまう問題を解消する提案。

次のコードはコンパイラによって扱いが変化します。

struct S {
  bool operator==(const S&) { return true; }  // (1) 非constメンバ関数
  bool operator!=(const S&) { return false; } // (2) 非constメンバ関数
};

int main() {
  bool b = S{} != S{};  // #1
}

#1の箇所ではユーザー定義!=(2)と(1)から導出された!=の2つが候補として上がります。Sの右辺値との比較でベストマッチものはないのでSの値への変換がどちらでも行われ、メンバ関数const修飾がなされていないことによってどちらの候補も最適とみなされ、その扱いがコンパイラによって変化しています。

  • GCC : (1)を使用する(警告なし)
    • 変換シーケンスの前に、パラメータ型が一致する複数の候補がある場合に生成された候補を無視する
  • clang : (1)を使用する(警告あり)
  • MSVC : (1)を使用する(警告なし)
    • 生成された候補を削除する

godbolt

現在の比較演算子a @ b)の自動導出に伴うオーバーロード解決のルールでは、@そのもの->生成された正順の式(a @' b)->生成された逆順の式(b @' a)の順で優先順位が高くなるというルールがあるのですが、このケース(@!=)では変換なしでベストマッチする!=が存在しないため、定義されているものと生成されたものとで優先順位が同等になってしまっているようです。

コンパイラから見ると先ほどの例は、次のようなコードと同じことをしています

struct S {
  friend bool f(S, const S&) { ... }
  friend bool f(const S&, S) { ... }
};
bool b = f(S{}, S{});

この提案では、現在の厳格なルールをMSVCにて実装(出荷されているMSVCの実装とは異なる)し、59のコードベースの調査を行なっています。その結果、20のプロジェクトでこの問題によるコンパイルエラーが観測されました。

template <typename T>
struct Base {
  bool operator==(const T&) const;
};

struct Derived : Base<Derived> { };

bool b = Derived{} == Derived{};

このケースでは、派生クラスから基底クラスへの変換が発生し、同様の問題に陥っています。これは受け入れられるべきです。

template <bool>
struct GenericIterator {
  using ConstIterator = GenericIterator<true>;
  using NonConstIterator = GenericIterator<false>;

  GenericIterator() = default;
  GenericIterator(const NonConstIterator&); // (1)

  bool operator==(ConstIterator) const;
};
using Iterator = GenericIterator<false>;

bool b = Iterator{} == Iterator{};

このケースは、IteratorGenericIterator<false>)の比較を暗黙変換コンストラクタ(1)を通すことによって、ConstIteratorGenericIterator<true>)の比較として実行しています。暗黙変換が行われることによって同様の問題が起きています。これは意図的であるので受け入れられるべきです。

struct Iterator {
  Iterator();
  Iterator(int*);

  bool operator==(const Iterator&) const; // (1)
  operator int*() const;  // (2)
};

bool b = nullptr != Iterator{};

このケースは、(2)によってIteratorからポインタへ暗黙変換することで、!=を提供しようとしています。暗黙変換が行われることによって同様の問題が起きています。しかしこの場合、逆順の==使用時にも暗黙変換によって組み込み==が使用されてしまい、より最適なはずのnullptrからIteratorを構築して==(1)が使用されないという問題があります。このケースはリジェクトされるべきです。

using ubool = unsigned char;

struct S {
  operator bool() const;
};
ubool operator==(S, S); // (1)

ubool b = S{} != S{};

これは厳密には先程までの問題と関係がありません。この例がエラーとなるのは、(1)の==の戻り値型がboolではないからです。!=の導出に使用される==の戻り値型は(cv)boolでなければならず、そうでない場合オーバーロード解決で選ばれるとコンパイルエラーとなります。

GCCの実装では、オーバーロード解決において変換を考慮する前に、候補集合の中に同じパラメータ型を持つ候補があり片方が生成された候補である場合に、その生成された候補を以降のオーバーロード解決で無視することで以前の振る舞いを維持しようとしています。このGCC実装をMSVCにも実装して再度テストしたところ、コンパイルに失敗したのは10プロジェクト(-10)となりました(上記の受け入れたい/リジェクトしたいケースは意図通りとなっている)。

この提案では、GCCの実装をベースとして実装の自由度も考慮して、「生成された演算子候補について、同じスコープで宣言された同じシグネチャを持つ別の候補が存在し、その候補が生成されていない(ユーザー定義である)候補である場合、生成された演算子オーバーロード候補から除外される」、というルールをオーバーロード解決のルールに追加することを提案しています。それによって、最初の例のようなコードは曖昧ではなくなり、上記のアクセプトしたい既存コード例はコンパイルされリジェクトしたいコード例はコンパイルされない、という振る舞いを手に入れることができます。

P2469R0 Response to P2464: The Networking TS is baked, P2300 Sender/Receiver is not.

P2464R0 Ruminations on networking and executors」の主張に反論する文書。

ASIO(NetworkingTS、以下ASIOとは=NetworkingTSも意味する)の定義する非同期モデルにおけるExecutorとは、非同期処理のtail callのカスタマイゼーションポイントであるという前提を説明し、P2300の抽象化はschedulerの具体的実装を知っていなければ一般的なアルゴリズムを構成できず、P2300は非同期モデルを定義するものではなく非同期処理グラフを作成するためのDSLに過ぎず、ASIO/NetworkingTSの非同期モデルはP2300のDSLを実装することができるより基礎的なモデルであると主張しています。

P2470R0 Slides for presentation of P2300R2: std::execution (sender/receiver)

P2300の紹介・解説スライド。

P2300のschedulersender/recieverによるアルゴリズムの紹介とそれによる非同期処理グラフの構成、thenアルゴリズムの実装解説、コルーチンとsenderの親和、senderにおけるキャンセルなどについて解説されています。

また、P2300の実装(libunifex)がFacebookにて広く実運用されているほか、NVIDIAがその実現にコミットしていくことや、Bloomberg社内でも好印象を得ていることが紹介されています。

P2471R0 NetTS, ASIO and Sender Library Design Comparison

P2471R1 NetTS, ASIO and Sender Library Design Comparison

ASIOとNetworkingTSとP2300(P0443)の設計を比較する文書。

それぞれのライブラリのモデルや要求することなどをコンセプトによって表現し、表にまとめて比較しています。

P2472R0 make_function_ref: A More Functional function_ref

function_refに適応的に型消去させるためのヘルパ関数make_function_ref()を追加する提案。

function_refは関数呼び出し可能なものに対するviewであり、主として関数引数で任意のCallableなものを受け取るときに使用します。そこでは、テンプレートによる受け取りとstd::functionによる受け取りのいいとこ取りのような性質を利用できます。

// テンプレートによる受け取り
// 関数のシグネチャなどの情報がなく、追加の制約が必要になる
// 可読性が低下し、複雑さが増大する
template<typeanme F>
void f(F&& f);

// std::functionによる受け取り
// 所有権が不明(`std::reference_wrapper`を保持しているかもしれない)、空かもしれない
// 型消去のための追加のオーバーヘッド(動的確保など)が必要となる
void f(std::function<bool(int)> f);

// function_refによる受け取り
void f(function_ref<bool(int)> f);

function_refを使用することによって、ステートフルなラムダを含めたあらゆるCallableを受け取れるようにしながら、求められている関数の情報が最小の形で引数型に表示され、追加のオーバーヘッドを回避できます。そして、function_refviewであり常に所有権を持ちません。

function_refは次のように実装されます。

template<typename F>
class function_ref;

template<typename R, typename... Args>
class function_ref<R(Args...)> {
  void* erased_object;
  R(*erased_function)(Args...);

public:

  template<typename F>
  function_ref(F&&);

  // ...
};

function_refはメンバとして2つのポインタを持ち、erased_objectは渡された関数のオブジェクトそのもの(ファンクタなど)のポインタを、erased_functionはその関数呼び出し演算子のポインタを保持します。

function_refはファンクタ/ステートフルラムダ、関数ポインタ/ステートレスラムダ、メンバ関数ポインタ、の3種類のCallableを保持することになりますが、2つのメンバがフルに活用されるのはファンクタ/ステートフルラムダの時だけで、それ以外のケースではerased_objectは有効利用されていません。メンバ関数ポインタに対するthisやフリー関数の最初の引数などを束縛した上でfunction_refに渡すにはラムダ式などでラップする必要がありますが、erased_objectを有効利用すればその必要なく一部の引数を型消去しながら束縛することができそうです。

このような単一関数に対する実行時ポリモルフィズムは、C#のデリゲートなどOOPの世界でよく見られ、C++においてもコミュニティから類似のもののアイデアがいくつか提示されています。それらは共通して、function_refの2つのメンバを直接指定して初期化するコンストラクタによって実装可能です。

この提案はその実現のため、function_refにその2つのポインタメンバを直接初期化可能なコンストラクタを追加した上で、その初期化をサポートするmake_function_ref()ヘルパ関数を追加するものです。

template<typename F>
class function_ref;

template<typename R, typename... Args>
class function_ref<R(Args...)> {
  void* erased_object;
  R(*erased_function)(void*, Args...);

public:

  template<typename F>
  function_ref(F&&);

  // 追加するコンストラクタ
  function_ref(void* obj, R(*func)(void*, Args...))
    : erased_object{obj}
    , erased_function{func}
  {}

  // ...
};


template<auto mf, typename T>
  requires std::is_member_function_pointer<decltype(mf)>::value
auto make_function_ref(const T& obj) {
  return function_ref(std::addressof(obj), mf);
}

template<auto mf>
  requires std::is_member_function_pointer<decltype(mf)>::value
auto make_function_ref() {
  return function_ref(mf);
}

template<auto f>
  requires is_function_pointer<decltype(f)>::value
auto make_function_ref() {
  return function_ref(f);
}

// 他多数のオーバーロード

make_function_refはNTTPとして(メンバ)関数ポインタを、関数引数で束縛対象のオブジェクトを受け取り、それらを指定してfunction_refの2つのメンバを初期化します。例えば次のように利用します

struct bar {
  void baz(bool b, int i, float f) {
      std::cout << "bar::baz" << std::endl;
  }

  void baznoe(bool b, int i, float f) noexcept {
      std::cout << "bar::baznoe" << std::endl;
  }
};

void third_party_lib_function1(tl::function_ref<void(bool, int, float)> callback) {
  callback(true, 11, 3.1459f);
}

void application_function_ref(function_ref<void(bar*, bool, int, float)> callback) {
  bar b;
  callback(b, true, 11, 3.1459f);
}

void free_baz1(bool b, int i, float f) {
  std::cout << "free_baz1" << std::endl;
}

int main() {
  bar b;

  // ステートフルラムダ(make_function_refがない場合の渡し方)
  third_party_lib_function1([&b](bool b1, int i, float f){ b.baz(b1, i, f); });
  // メンバ関数ポインタとthis(上記と同等な渡し方)
  third_party_lib_function1(make_function_ref<&bar::baz>(b));
  // 関数ポインタのみ
  third_party_lib_function1(make_function_ref<free_baz1>());

  // メンバ関数ポインタのみ(thisは後から渡す)
  application_function_ref(make_function_ref<&bar::baz>());
  application_function_ref(make_function_ref<&bar::baznoe>());
}

P2473R0 Distributing C++ Module Libraries

C++モジュールのビルド済み成果物の配布を容易にするための、ビルドツール・コンパイラ・静的解析ツール間の相互運用性のための共通フォーマットの提案。

この提案はP2409R0をベースとして、そこで挙げられている要件に沿うような規則を定めようとするものです。

パッケージマネージャを使用してビルド済みのライブラリを配布する環境では、ライブラリのコンテンツはディスク上のファイルとして表されているはずです。この提案はC++モジュールをそれらのビルド済みファイルとして配布できるようにするための規則を定めることを目的としています。

この提案の目標は以下の3点で、それ以外の点は目的としていません

  1. モジュールの発見可能性
    • ビルドシステムとパッケージマネージャ間で決定されたモジュール検索パスが与えられた時、それを用いてシステムで利用可能なモジュールを見つけるための規則を定める
  2. モジュールインターフェースのパース手順
    • C++モジュールでは、モジュールの解析とそれを使用する翻訳単位の区別が導入されていること、中間のバイナリモジュールインターフェースファイルが相互運用不可能であることから、この提案ではモジュールインターフェースソースファイルのパース方法を支持するメカニズムを定義する
  3. 依存関係の検出を最適化する規則
    • モジュールの依存関係グラフ構築の際に、プリプロセッサの実行を回避することは望ましい最適化の一つであるが必須ではない。この提案は、その選択をした実装者のための規約を定める

提案している規則は以下のようなものです。

モジュール名はファイル名と対応する

import宣言で使用されるモジュール名は次の規則によってファイル名に変換される。

モジュール名 検索ルートからの相対パス
foo foo.ixx
foo.bar foo/bar.ixx
foo.bar:baz foo/bar.part/baz.ixx

これは、モジュールのインストールプロセスがユーザーに変わって翻訳することを想定している。これにより、ビルドシステムの外部からモジュールを消費(importに対応した読み込み)する際に決定論的な探索が可能となる。

モジュールを消費する手順

モジュールを消費する手順はモジュールインターフェースファイルとともに(同じディレクトリに)配置され、拡張子は.meta-ixx-info

このファイルは次のキーを持つJSONオブジェクトとしてエンコードされる

  • include_path : インターフェース単位ファイルのプリプロセス時に必要な#include対象ファイルのリスト。標準ヘッダ類は含まれない。
  • definitions : 事前定義マクロのキーと値のペア。
  • imports : 対応するモジュールインターフェース単位によってインポートされているモジュール名のリスト。これは、ビルドシステムがモジュール単位を解析して依存関係を把握するのを回避するためのオプションのフィールド。
  • _VENDOR_extension : ベンダーはビルドシステムで使用される可能性のあるメタデータ拡張機能を指定するためにこのフィールドを使用できる

この規則の前提として、プリプロセス時に適用されるオプション以外のオプションは、モジュールを消費する翻訳単位とモジュールインターフェース単位のパースの間で一律に適用されなければならない。それは例えば、言語バージョンを指定するフラグやABI互換に影響するフラグなど。

また、モジュールを消費する翻訳単位からのインクルードパスとマクロ定義はインポートされるモジュールを消費する際に使用しないことも想定している。

バイナリモジュールインターフェースファイルの配布

バイナリモジュールインターフェース(BMI)の相互運用可能な範囲は非常に限られているが、コードがほぼ同じコンパイラによって生成される環境では重要な最適化となる。例えば、ほとんどのGNU/Linuxディストリビューションが該当する。ただし、BMIファイルがあるコンパイラに適用可能かどうかが識別可能である必要がある。したがって、このルールではコンパイラはサポートする互換性と同じくらい一意の識別子を提供する必要がある。

その識別子は、.bmi.vendor.idのようなパターンで用意する。例えば、g++がUUID-v4を使用してBMIファイルを再利用可能であることを表明するには、foo.barモジュールの場合ファイル名は「foo/bar.bmi.g++.20734238-4fc7-4725-bf22-be9700326774」になる。

この規則では、このような識別子のうち、コンパイラは1つの入力形式だけをサポートすることを想定している。コンパイラはファイル名の有効な拡張子となる任意の識別子を使用でき、複数の識別子パターンをサポート可能だが、ここでそれを規定すると探索の複雑さが増大する。

検索順序

モジュールの探索時は、最初にコンパイラ固有のBMI(標準ライブラリのものなど)を探索してから2回目の探索で.ixxファイルと.meta-ixx-infoファイルを探索できるはず。これにより、異なるビルドシステム間で使用するためにBMIファイルをキャッシュするローカルオーバーレイツールを作成できる。

BMIファイルの存在を最適化しない

ビルドシステムが現在のビルドと互換性のあるBMIファイルの存在を確認した時、依存関係の探索をやめて直接それを消費することが有効な最適化だが、そうすると、ビルドシステムは連携するツールからモジュールのインターフェースのパースを再現する方法の詳細を隠蔽してしまう。したがって、相互運用性を維持したいビルドシステムはモジュールインターフェースをパースする必要がないと判断した場合でもモジュールインターフェースのパースを継続して、その情報を現在のコンパイルデータベースのように利用する必要がある。

Discovery tooling

ライブラリとして提供されるモジュールの推移的な関係を解決するために、全ての必要なモジュールを解析するものが必要となる。そのために、c++-modules-configと呼ばれるツールを提案する。

このツールはモジュールの探索パス、BMIファイルの識別子、解決が必要なモジュール名のリストを入力とし、ビルドに関係するモジュールの完全なリスト、インターフェースのソース、インターフェースをパースするのに必要な手順、およびモジュールの依存関係を出力する。このツールは再帰的な探索を行い、ビルドシステムが消費する必要のあるシステムによって提供される全てのモジュールのパースを計画するのに十分な記述を返す。これにより、利用可能だが消費される予定のないモジュールは除外される。

このツールの出力はP1689R4のフォーマットと互換性を持たせる必要があり、またこの出力はmodule_config.jsonという名前で、現在compile_commands.jsonが保存されている場所と同じ場所に保存される必要がある。

P2475R0 WG21 2021-10 Virtual Meeting Record of Discussion

2021年10月4日(北米時間)に行われた、WG21全体会議のデイスカッション議事録。

CWG/LWG/LEWGの投票時のディスカッションの様子が記録されています。

P2477R0 Allow programmer to control and detect coroutine elision by static constexpr bool should_elide() and

コルーチンの動的メモリ確保を避ける最適化を制御し、起こったことを検出するAPIを追加する提案。

コルーチンはサスペンドと再開をサポートするためにその状態(コルーチンステータス)を保存しておく領域を必要とします。現在のC++コルーチンでは、この領域の確保は動的メモリ確保(operator new)によって行われ、それはコストがかかります。

それを軽減するために、HALOと呼ばれる動的メモリ確保を回避する最適化がデザインされており、これをcoroutine elisionと呼びます。clang/LLVMではそれはCoroElideとして実装されています。C++標準では名前付けされていないもののcoroutine elisionは許可されています(つまり必須ではない)。

これはコンパイラの最適化であり、必ず行われるわけではありません。そのため、コルーチンを利用するプログラマはこの最適化を制御することも、それが行われたことを察知することもできません。

また、組み込みのようにリソースが限られている環境ではcoroutine elisionを無効化したうえでpromise_type::operator newが静的ストレージのバイト配列からの領域を返すようにすることが好まれることがあるようです。しかし、coroutine elisionを無効化する方法もまた標準では提供されていません。

この提案では、coroutine elisionを制御するためのpromise_type::should_elide()coroutine elisionを検出するためのstd::coroutine_handle<>::elided()の2つの関数を追加することを提案しています。これによって、プログラマcoroutine elisionを制御し検出できるようになります。

should_elide()は次のようにcoroutine elisionの実行についてをコンパイラに指示します

  • promise_typeのスコープでshould_elide()という関数を見つけることができ、コンパイル時に評価した結果が
    • trueに評価された場合 : coroutine elisionが保証される
    • falseに評価された場合 : coroutine elisionが行われないことが保証される
  • should_elide()が見つからない、コンパイル時に評価できない場合
    • 従来通り(coroutine elisionコンパイラの裁量で行われる)

should_elide()promise_typeメンバ関数として実装されるもので、promise_typeはコルーチンの動作をカスタムするカスタマイゼーションポイントとしてユーザーが定義します。したがって、should_elide()もまた必要であればユーザーが定義するものであり、それはconstexpr関数として定義しなければなりません。

elided()std::coroutine_handleメンバ関数として追加され、coroutine elisionが行われていた時にtrueを返し、行われていない場合にfalseを返します。

namespace std {
  template<>
  struct coroutine_handle<void> { 
    // ...

    bool elided() const noexcept; // 追加
    
    // ...
  };

  template<class Promise>
  struct coroutine_handle<Promise> {
    // ...
    
    bool elided() const noexcept; // 追加
    
    // ...
  };
}

P2478R0 _Thread_local for better C++ interoperability with C

C++thread_local変数は動的初期化とデストラクタ呼び出しを伴うことができます。その初期化は、スレッド起動時に非ブロック(名前空間スコープ)変数に対して行われるか、最初に変数が使用されるときに行われる可能性があります。その主な実装は、変数が使用される場所に関数を挿入し、その関数が呼ばれることで非ブロック変数の遅延初期化を実行します。

そのような関数とその呼び出しによって実行時にサイズと時間のオーバーヘッドがかかります。これは変数が動的初期化もデストラクタ呼び出しも必要ないことが分かっていれば回避可能です。

このことは、単にオーバーヘッドがかかるだけではなく、Cの_Thread_localとの非互換を生み出しています。Cの_Thread_localC++thread_localとしての使用に必要なもの(初期化・破棄に必要な追加の関数など)を提供しません。このため、C_Thread_local変数をC++コードから参照しようとするとリンクエラーを起こすことがあります。

また、C++で定義された動的初期化されるか非トリビアルなデストラクタをもつ静的変数(非スレッドローカル)にCからアクセスすることは、C++から同じものにアクセスするのと比べて危険ではありません。なぜなら、主要な実装はプログラム起動時にそれらの初期化を行うためです。しかし、同様に宣言されているスレッドローカルな変数にアクセスした場合は同じことは言えません。Cからのアクセスは初期化前の変数を観測する可能性が高くなります。

この提案はC23に対して、_Thread_localthread_localキーワードで置き換えるのをやめるように提案するものです。上記のような問題があるため、安易にキーワードをC++と共通化してしまうことは危険です。一方、C++が上記問題の回避を行いたい場合にCと同様のセマンティクスで_Thread_localを使用する方向性を示しています。

P2479R0 Slides for P2464

P2464R0 Ruminations on networking and executorsの紹介スライド。

後半ではP2469に対する反論も行われています。

P2480R0 Response to P2471: "NetTS, Asio, and Sender library design comparison" - corrected and expanded

P2471R1の訂正や追記を行う文書。

P2471R1はASIO/NetrworkingTSとP2300との比較を行っている文書ですが、その内容について不正確な部分を修正したり追記したりする文書です。

P2481R0 Forwarding reference to specific type/template

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

std::tuple<Ts...>にはstd::tuple<Us...>からの変換コンストラクタが存在します。それは、Us...のそれぞれが対応するTs...のそれぞれに変換可能であれば利用可能となりますが、std::tuple<Us...>の状態によって4つの定義を必要とします。

template <typename... Ts>
struct tuple {
  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us&> && ...)
  tuple(tuple<Us...>&);

  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us const&> && ...)
  tuple(tuple<Us...> const&); // (1)

  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us&&> && ...)
  tuple(tuple<Us...>&&);      // (2)

  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us const&&> && ...)
  tuple(tuple<Us...> const&&);
};

これは簡易な例でしかないので正確ではありません。重要なことは、tuple<Us...>constの有無と値カテゴリの組み合わせで2x2=4つのコンストラクオーバーロードが必要となるうえ、制約もそれに応じて微妙に細部を調整しなければなりません。

これはかなり冗長なコードであるうえ、これらがオーバーロードであることから、ある適切なコンストラクタがその制約によって適切に候補から外れた時でも、別のコンストラクタが呼ばれることがあります。

void f(tuple<int&>);
auto g() -> tuple<int&&>;

void h() {
  f(g()); // ok!
}

これは、tuple<int&&> -> tuple<int&>の構築を行っているもので、int&& -> int&のような変換は不可能なので失敗してほしいし失敗するはずです。しかし実際には、(2)のコンストラクタが制約を満たさないために除外された後、(1)のコンストラクタが利用されます。g()の戻り値は右辺値であり、(1)のコンストラクタのtuple<Us...> const&でも受けることができて、その制約中のUs const&int&& const & -> int&となってint& -> int&の変換が可能であることから(1)のコンストラクタを呼び出してしまいます。

この問題を回避するためには、コンストラクタテンプレートを1つだけにする必要があります。必要なことは、tuple<Us...>を指定しながら、CV・参照修飾を推論してもらうことです。現在のところそれはできず、上記のように書くかテンプレートをこね回す必要があります。

template <typename... Ts>
struct tuple {
  template <typename Other>
      requires is_specialization_of<remove_cvref_t<Other>, tuple>
            && /* ???? */
  tuple(Other&& rhs);
};

しかし、このコンストラクタテンプレートの制約を正しく書く事は困難で、いい解決策はありません。結局、最低4つの似たようなオーバーロードを用意するのが一番簡易となります。

これと同じ問題は、std::getでも見ることができます。

template <size_t I, typename... Ts>
auto get(tuple<Ts...>&) -> tuple_element_t<I, tuple<Ts...>>&;

template <size_t I, typename... Ts>
auto get(tuple<Ts...> const&) -> tuple_element_t<I, tuple<Ts...>> const&;

template <size_t I, typename... Ts>
auto get(tuple<Ts...>&&) -> tuple_element_t<I, tuple<Ts...>>&&;

template <size_t I, typename... Ts>
auto get(tuple<Ts...> const&&) -> tuple_element_t<I, tuple<Ts...>> const&&;

これも一つの関数テンプレートにまとめることで記述を削減でき先ほどのような問題を回避できますが、やはりその制約を正しく書くことが困難で、std:tupleの派生型に対して機能しなくなってしまいます。

std::optional::transformのようなメンバ関数deducing thisを組み合わせた時にも、別の問題が浮かび上がります。

template <typename T>
struct optional {
  template <typename F>
  constexpr auto transform(F&&) &;

  template <typename F>
  constexpr auto transform(F&&) const&;

  template <typename F>
  constexpr auto transform(F&&) &&;

  template <typename F>
  constexpr auto transform(F&&) const&&;
};

このような冗長なコードは、deducing thisによって次のようにひとまとめにできます。

template <typename T>
struct optional {
  template <typename Self, typename F>
  constexpr auto transform(this Self&&, F&&);
};

しかし、今度はこのSelfの対象とする範囲が広すぎます。ここではstd::optionalの派生型を対象としたくはなく、やってほしいことはCV・参照修飾の推論だけです。

これらの問題は、テンプレートにおいてある程度型を指定しながらでもCV・参照修飾の推論が可能となってくれれば解決することができます。この文書では、いくつかの構文の候補を提示するとともにより良いアイデアを募っています(まだ提案には至っていません)。

以下の例では次のようなユーティリティを使用しています

#define FWD(e) static_cast<decltype(e)&&>(e)

template <bool RV, typename T>
using apply_ref = std::conditional_t<RV, T&&, T&>;

template <bool C, typename T>
using apply_const = std::conditional_t<C, T const, T>;

template <bool C, bool RV, typename T>
using apply_const_ref = apply_ref<RV, apply_const<C, T>>;

template <typename T, typename U>
using copy_cvref_t = apply_const_ref<
  is_const_v<remove_reference_t<T>>,
  !is_lvalue_reference_v<T>,
  U>;

T auto&&

これはコンセプトによるプレースホルダ型の制約(range auto&& r)を発展させて、コンセプトを指定する部分により具体的な型を指定できるようにしたものです

template <typename... Ts>
struct tuple {
  template <typename... Us>
  tuple(tuple<Us...> auto&& rhs)
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<
                Ts,
                copy_cvref_t<decltype(rhs), Us>
              > && ...);
};
template <typename T>
struct optional {
  template <typename F>
  auto transform(this optional auto&& self, F&& f) {
      using U = remove_cv_t<invoke_result_t<F,
          decltype(FWD(self).value())>;

      if (self) {
        return optional<U>(invoke(FWD(f), FWD(self).value()));
      } else {
        return optional<U>();
      }
  }
};
  • 利点
    • 簡潔にやりたいことを実現・表現できる
  • 欠点
    • 正確な型を取得するにはdecltype(param)としなければならない
      • このため、requires節を関数の後ろにしか書けなくなる
    • concept autoの記法と矛盾する(この構文は変換を行わない)

T&&&

先ほどの、T auto&&の代わりにT&&&を導入するものです。(サンプルは省略)

  • 利点
    • tuple<Us...>&&&は派生型からの変換を行うことができる
  • 欠点
    • T auto&&と同じ
    • T&&&という参照トークンそのもの

const(bool)

noexcept(bool)explicit(bool)のように、const&&を条件付きにするものです。

template <typename... Ts>
struct tuple {
  template <typename... Us, bool C, bool RV>
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<
                Ts,
                apply_const_ref<C, RV, Us>
              > && ...)
  tuple(tuple<Us...> const(C) &&(RV) rhs);
};
template <typename T>
struct optional {
  template <typename F, bool C, bool RV>
  auto transform(this optional const(C) &&(RV) self, F&& f) {
    using U = remove_cv_t<invoke_result_t<F,
        // apply_const_ref<C, RV, T>
        decltype(FWD(self).value())>;

    if (self) {
      return optional<U>(invoke(FWD(f), FWD(self).value()));
    } else {
      return optional<U>();
    }
  }
};
  • 利点
    • const性と参照修飾だけを指定していることが明確になる
    • requires節の配置自由度が保たれる
  • 欠点

Q修飾子

これは全く新しい形のテンプレートパラメータを導入するもので、これをqualifier(修飾子)と呼びます。これは、エイリアステンプレートを推論します

template <typename... Ts>
struct tuple {
  template <typename... Us, qualifiers Q>
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<Ts, Q<Us>> && ...)
  tuple(Q<tuple<Us...>> rhs);
};
template <typename T>
struct optional {
  template <typename F, qualifiers Q>
  auto transform(this Q<optional> self, F&& f) {
    using U = remove_cv_t<invoke_result_t<F, Q<T>>>;

    if (self) {
      return optional<U>(invoke(FWD(f), FWD(self).value()));
    } else {
      return optional<U>();
    }
  }
};

ここでは、Q<T>QTはそれぞれ別々に推論されますが、Qは次のエイリアステンプレートのいずれかとして推論されます

template <typename T>
using Q = T&;

template <typename T>
using Q = T const&;

template <typename T>
using Q = T&&;

template <typename T>
using Q = T const&&;
  • 利点
    • 推論したCV・参照修飾を適用(利用)するのが容易
    • 正確な型の取得が容易
  • 欠点
    • 斬新な構文であり、奇妙
    • Qの名前が問題となる
    • constだけを推論したいときに、誤用の可能性がある

作者の方はこれらの解決策に納得してないらしく、より良い構文を募るためにこの文書を書いたようです。

おわり

[C++]`std::array`のエイリアステンプレートとCTAD

twitterに密かに置いていた質問箱に次のような質問をいただきました。

#include <array>

template<auto N>
using std_array_with_int = std::array<int,N>;

template<typename T>
using std_array_with_3 = std::array<T,3>;

int main() {
  [[maybe_unused]] std::array ar1 = { 1, 2, 3 };          // ok
  [[maybe_unused]] std_array_with_int ar2 = { 1, 2, 3 };  // ng、なして?
  [[maybe_unused]] std_array_with_3 ar3 = { 1, 2, 3 };    // ng、なして?
}

ようするに、std::arrayのテンプレートパラメータの一部だけを束縛したエイリアステンプレートでCTADを期待すると、謎のコンパイルエラーに悩まされています。

エイリアステンプレートのCTADが絡み非常に複雑な問題であり、簡易に回答できなさそうなのでこの記事を書きました。

CTADの仕組み(雑)

あるクラステンプレートCについてのCTADは、Cのコンストラクタを変換して得た関数テンプレート(引数はそのコンストラクタの引数、テンプレートパラメータはCのテンプレートパラメータにそのコンストラクタのテンプレートパラメータを合併したもの、戻り値型はそのコンストラクタのテンプレートパラメータのうちCのものに対応するパラメータで特殊化されたC<Ts...>)と、Cに定義された推論補助を関数テンプレートとして抽出し、初期化式(C = c{args...};)に与えられている実引数列(args...)を用いてそれらの関数テンプレートを呼び出したかのようにオーバーロード解決を行い、最適にマッチした1つの関数テンプレートの戻り値型からテンプレートパラメータが補われたC<Ts...>を取得するものです。

CTADはコンストラクタ呼び出しよりも前に行われ、必ずしも使用される推論補助(コンストラクタからの関数テンプレート)と実際に呼び出されるコンストラクタが対応していなくても良く、推論補助に関してはほぼ関数テンプレートのように書くことができます(コンセプトやSFINAEによる制約などを行えます)。

重要なことは、コンストラクタおよび推論補助を関数テンプレートとして扱って、実引数を用いてオーバーロード解決を行なって、残った1つの戻り値型から完全な型を取得するという手順です。CTADはクラス型・集成体型・エイリアステンプレートの3つに対して行われますが、バックエンド部(関数テンプレートとしてオーバーロード解決以降)はそれらで共通で、異なるのはその関数テンプレートをなんとか抽出するフロントエンド部分のプロセスです。

エイリアステンプレートのCTADの仕組み

エイリアステンプレートのCTADは、元の型の推論補助を変換してエイリアステンプレートの推論補助を導出し、それを利用することで行われます。そのアルゴリズムはおおよそ次のようになります

  1. エイリアステンプレートの元の型のコンストラクタと推論補助を(関数テンプレートとして)全て取得する
    • 以下の手順はそうして取得した関数テンプレート1つ1つに対して行われる(その1つをDとする)
  2. エイリアステンプレートの右辺とDの右辺(戻り値型)から、テンプレートパラメータの対応を推論する
    • この処理は、関数テンプレートのテンプレートパラメータ推論と同様に行われる(ただし、推論できないパラメータがある場合でも失敗しない)
    • 推論できないコンテキストが存在しうるため、この時に全てのテンプレートパラメータを推論する必要はない
  3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、Dにフィードバックして置き換える
    • 2で全てのパラメータの推論が出来ていない可能性があるので、ここで得られた推論補助はエイリアステンプレートとDそれぞれからのテンプレートパラメータを含む可能性がある
  4. 結果の型(3で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する
    • 結果の型からエイリアスを推定するか、またどのように推定するかは実引数型に依存するため、追加の制約が必要となる場合がある

このような導出(あるいは推論補助の変換)は、エイリアステンプレートの元の型のコンストラクタと推論補助から抽出した関数テンプレート1つづつに対して行われ、そうして無事に導出できた(中には失敗するものがありうる)推論補助を、通常の推論補助と同じように扱ってエイリアステンプレートのCTADは行われます。

例1、単純な例

次のような型で動作例を見てみます。

// 今回の例の対象エイリアステンプレート
template<typename T>
using P = std::pair<int, T>;

このコードで、Pに対する推論補助がどのように導出されるかを先ほどの手順に沿って見てみます(以下、std::pairstdを省略します)。

2. エイリアステンプレートの右辺と元の推論補助の右辺から、テンプレートパラメータの対応を推論する

pairの推論補助は1つだけが用意されています(コンストラクタからのものは今回は無視します)

namespace std {

  // ...

  template<typename T1, typename T2>
  pair(T1, T2) -> pair<T1, T2>;
}

これとエイリアステンプレートP

template<typename T>
using P = pair<int, T>;

の右辺から、テンプレートパラメータのペア<T1, T2><int, T>から、T1 = int, T2 = T(とtypename T2 = typename T)の対応を得ます。

ここでは、pairの推論補助のテンプレートパラメータを持ち、その右辺の型(pair<T1, T2>)を引数型とする関数テンプレートに対して、エイリアステンプレートPの右辺の型(pair<int, T>)の値を引数として渡したときの関数テンプレートのテンプレートパラメータ推論とほぼ同じことを行なって、テンプレートパラメータの対応を得ます。

template<typename T1, typename T2>
void f(pair<T1, T2>);

f(pair<int, T>{});

このようなfT1, T2に推論される型を求めることで対応を得ます。この時、Tのように具体的な型ではないテンプレートパラメータに対しても推論を行い、テンプレートパラメータのまま対応させます。それによって、T1 = int, T2 = Tの対応が得られます。

3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、元の推論補助にフィードバックして置き換える

pairの推論補助

template<typename T1, typename T2>
pair(T1, T2) -> pair<T1, T2>;

に、先ほど得た対応(T1 = int, T2 = T)とエイリアステンプレートのテンプレートパラメータをフィードバックします。

まずエイリアステンプレートのテンプレートパラメータをそのまま、推論補助のテンプレートパラメータリストの先頭にフィードバックすると

template<typename T, typename T1, typename T2>
pair(T1, T2) -> pair<T1, T2>;

となり、次にpairの推論補助とエイリアステンプレートPのテンプレートパラメータの対応(T1 = int, T2 = T)をフィードバックします。

template<typename T>
pair(int, T) -> pair<int, T>;

この時、対応が確定しているpairの推論補助のテンプレートパラメータについてはすでにテンプレートではないので(対応先が具体的な型ではなく、Pのテンプレートパラメータだったとしても)、テンプレートパラメータ宣言(typename T1, typename T2)は削除します。

4. 結果の型(2で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する

最後に、得られた推論補助とエイリアステンプレート

template<typename T>
pair(int, T) -> pair<int, T>;

template<typename T>
using P = pair<int, T>;

の対応から、エイリアステンプレートのテンプレート実引数を推論し、推論補助を完成します。ここでは、エイリアスを戻すような形で推論補助の型を置き換えます。

template<typename T>
P(int, T) -> P<T>;

P p = {1, 2};のように書かれたとき、こうして得られた推論補助を用いてエイリアステンプレートのテンプレートパラメータが推論されます。

例2、単に名前を短縮したいだけの例

// とても長い名前のクラス
template<class T>
class VeryLongNameXXXXX { /* ... */ };

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

// 名前を短縮するためのエイリアステンプレート
template<class A>
using MyAbbrev = VeryLongNameXXXXXX<A>;

この場合に、MyAbbrevに対して導出される推論補助を見てみます。

2. エイリアステンプレートの右辺と元の推論補助の右辺から、テンプレートパラメータの対応を推論する

VeryLongNameXXXXXの推論補助は1つだけです。

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

これと

// 名前を短縮するためのエイリアステンプレート
template<class A>
using MyAbbrev = VeryLongNameXXXXXX<A>;

これらの右辺からテンプレートパラメータの対応を求めます。それは、関数テンプレートの引数推論と同様に行われるので、

template<class T>
void f(VeryLongNameXXXXX<decay_t<T>>);

f(VeryLongNameXXXXXX<A>{});

このように呼んだ時のTの推論によって対応を求めますが、ご存知のように?これは推論できないコンテキストとされ、Tの推論はできません。したがって、ここでは何の対応も得られません。

3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、元の推論補助にフィードバックして置き換える

先ほど得られた情報を元の推論補助にフィードバックするわけですが、何の対応も得られていないのでフィードバックは行われず、このステップは何もしません。よって、元の推論補助そのままが得られます。

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

4. 結果の型(2で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する

次の2つからエイリアステンプレートの推論補助を導出するわけですが

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

template<class A>
using MyAbbrev = VeryLongNameXXXXXX<A>;

これは何も難しいところはないでしょう

template<class T>
MyAbbrev(T) -> MyAbbrev<decay_t<T>>;

例3、規格書の意図的な例

template <class T, class U>
struct C {
  C(T, U);
};

template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

// 今回のエイリアステンプレート
template<class V>
using A = C<V*, V*>;

2. エイリアステンプレートの右辺と元の推論補助の右辺から、テンプレートパラメータの対応を推論する

Cの推論補助は1つだけです(コンストラクタからのものは無視)。

template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

これと

template<class V>
using A = C<V*, V*>;

これらの右辺からテンプレートパラメータの対応を求めます。

関数テンプレートで表すと

template<class T, class U>
void f(C<T, std::type_identity_t<U>>);

f(C<V*, V*>{});

のように呼んだ時のT, Uに行われるのと同じ推論が行われます。

ここで得られる対応は、T = V*のみで、Uとの対応は得られません(推論できないコンテキストです)。

3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、元の推論補助にフィードバックして置き換える

先ほど得られた情報(T = V*)とエイリアステンプレートのテンプレートパラメータをCの推論補助

template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

にフィードバックします。

まずテンプレートパラメータをフィードバックし

template<class V, class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

テンプレートパラメータの対応(T = V*)をフィードバックします

template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>;

4. 結果の型(2で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する

次の2つからエイリアステンプレートの推論補助を導出するわけです。

template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>;

template<class V>
using A = C<V*, V*>;

単純にエイリアスを戻すと次のようになりそうです。

template<class V, class U>
A(V*, U) -> A<V>;

が、これだとエイリアステンプレートの意味が変わってしまっています。そこで、std::type_identity_t<U> = V*という対応が見えていますので、これの情報を制約として加えたものが最終的なエイリアステンプレートAについての推論補助となります。

template<class V, class U>
A(V*, U) -> A<V> requires std::same_as<V*, std::type_identity_t<U>>;

ここでの制約はわかりやすくした一例です。例えばGCCはこの時requires std::same_as<A<V>, C<V*, std::type_identity_t<U>>>のような制約を生成しているようです。

より正確に書けば、ここで導出されるのは元の型に対する次のような推論補助です。

template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>> requires std::same_as<V*, std::type_identity_t<U>>;

実際は、エイリアステンプレートに対して推論補助を導出するのか、あくまで元の型に対するものを導出するのかは実装定義というか未規定で、GCCは元の型に対する推論補助を導出し使用するようです。

std::arrayエイリアステンプレート1

エイリアステンプレートに対するCTADがどうなっているのかを理解したところで、本題に戻ります。

まずは、std::array<T, N>Tを束縛するエイリアス

// 要素型を束縛するエイリアス
template<auto N>
using std_array_with_int = std::array<int, N>;

について導出される推論補助を求めてみます。

std::arrayには推論補助が1つしかなく、コンストラクタ(集成体初期化)からの推論補助相当のものは得られません(要素1つに対して初期化子複数となるため)。

namespace std {
  template <class T, class... U>
  array(T, U...) -> array<T, 1 + sizeof...(U)>;
}

この推論補助とエイリアスから、テンプレートパラメータの対応を求めます。

template <class T, class... U>
void f(std::array<T, 1 + sizeof...(U)>);

f(std::array<int, N>{});

明らかに要素数は推論できないコンテキストなので、得られるのはT = intという対応のみです。

対応が得られているので、まずエイリアステンプレートのテンプレートパラメータを推論補助へフィードバックし

template <auto N, class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

T = intの対応をフィードバックします。

template <auto N, class... U>
array(int, U...) -> array<int, 1 + sizeof...(U)>;

そしてエイリアスを推定する(戻す)と

template <auto N, class... U>
std_array_with_int(int, U...) -> std_array_with_int<1 + sizeof...(U)>;

省略していますが、std::arrayの元の推論補助にはTU...のすべての型が同じであることが制約されているので、それも継承されています。

そして、この推論補助の問題点はauto Nを解決できないことです。どのような初期化式が与えられてもこのNが推論されることはなく、そのためコンパイルエラーを起こします。

GCCの導出する推論補助

GCCは、CTADでエラーが起きた時に使用した推論補助の候補をエラーメッセージ中に出力してくれます。

prog.cc: In function 'int main()':
prog.cc:12:53: error: class template argument deduction failed:
   12 |     [[maybe_unused]] std_array_with_int ar2 = {1,2,3};//ng
      |                                                     ^
prog.cc:12:53: error: no matching function for call to 'array(int, int, int)'
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note: candidate: 'template<auto N, class _Tp, class ... _Up> std::array(_Tp, _Up ...)-> std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))> requires  __is_same(std::std_array_with_int<N>, std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)'
  267 |     array(_Tp, _Up...)
      |     ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note:   template argument deduction/substitution failed:
prog.cc:12:53: note:   couldn't deduce template parameter 'N'
   12 |     [[maybe_unused]] std_array_with_int ar2 = {1,2,3};//ng
      |                                                     ^

今回のケースでGCCが導出して使用している推論補助は

template<auto N, class _Tp, class ... _Up>
array(_Tp, _Up ...) -> array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))>
  requires  __is_same(std::std_array_with_int<N>,
                      std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)

というものです(見やすいように改変しています)。先ほどは省略していた関連制約が見えていますが、ほぼ求めた推論補助と同じものであることが分かります。ただし、テンプレートパラメータが1つ多いです。

GCC実装のstd::arrayの推論補助は次のように宣言されています。

template<typename _Tp, typename... _Up>
array(_Tp, _Up...) -> array<enable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp>, 1 + sizeof...(_Up)>;

_Tp_Upの各型が同じであることをenable_ifによって制約しているわけです。CTADとこの推論補助はともにC++17で導入されたものであるため、この実装は妥当です。そして、テンプレートパラメータ名的に、先ほどエラーメッセージから拾い上げた推論補助に含まれる余分なテンプレートパラメータ_Tpはここからのもので、_Tp = intの対応が取れていないことがうかがえます。

これは、2ステップ目のテンプレートパラメータの対応を求める際に

template <class T, class... U>
void f(array<enable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp>, 1 + sizeof...(_Up)>);

f(std::array<int, N>{});

となり、Tも推論できないコンテキストとなってT = intの対応が取れなくなるため起きています。そのためそれがフィードバックされず、導出された推論補助のテンプレートパラメータはauto N, class _Tp, class ... _Upの3つになっています。ただ、ステップ4ではenable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp> = intの対応が取れることが分かるため、それについての制約が追加のrequires節にておこなわれているようです。

とはいえ、そうであっても結論は変わらず、結局auto Nが推定できないためこの推論補助は使い物になりません。そしてそれは、先ほどのGCCのエラーメッセージにも表示されています(最後のほうの「couldn't deduce template parameter 'N'」)。

std::arrayエイリアステンプレート2

次に、std::array<T, N>Nを束縛するエイリアス

// 要素数を束縛するエイリアス(区別のためにT->Eへパラメータ名を変更
template<typename E>
using std_array_with_3 = std::array<E, 3>;

について導出される推論補助を求めてみます。

namespace std {
  template <class T, class... U>
  array(T, U...) -> array<T, 1 + sizeof...(U)>;
}

この推論補助とエイリアスから、テンプレートパラメータの対応を求めます。

template <class T, class... U>
void f(std::array<T, 1 + sizeof...(U)>);

f(std::array<E, 3>);

素数の方は推論できないコンテキストなので、得られるのは、T = Eという対応です。

これを推論補助へフィードバックすると

template <typename E, class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;
template <typename E, class... U>
array(E, U...) -> array<E, 1 + sizeof...(U)>;

そしてエイリアスを推定する(戻す)と

template <typename E, class... U>
std_array_with_3(E, U...) -> std_array_with_3<E> requires (1 + sizeof...(U) == 3);

省略していますが、std::arrayの元の推論補助にはTE)とU...のすべての型が同じであることが制約されているので、それも継承されています。

あれ?これはなんかいけそうな雰囲気がしていますが・・・?

GCCの導出する推論補助

実際エラーとなってるGCCの出力を見てみましょう。

prog.cc: In function 'int main()':
prog.cc:13:51: error: class template argument deduction failed:
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |                                                   ^
prog.cc:13:51: error: no matching function for call to 'array(int, int, int)'
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note: candidate: 'template<class T, class _Tp, class ... _Up> std::array(_Tp, _Up ...)-> std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))> requires  __is_same(std::std_array_with_3<T>, std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)'
  267 |     array(_Tp, _Up...)
      |     ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note:   template argument deduction/substitution failed:
prog.cc:13:51: note:   couldn't deduce template parameter 'T'
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |                                                   ^
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note: candidate: 'template<class T> array(std::array<T, 3>)-> std::array<T, 3>'
   95 |     struct array
      |            ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note:   template argument deduction/substitution failed:
prog.cc:13:51: note:   mismatched types 'std::array<T, 3>' and 'int'
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |                                                   ^
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note: candidate: 'template<class T> array()-> std::array<T, 3>'
   95 |     struct array
      |            ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note:   template argument deduction/substitution failed:
prog.cc:13:51: note:   candidate expects 0 arguments, 3 provided
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |     

使用されている推論補助は

template<class T, class _Tp, class ... _Up>
array(_Tp, _Up ...) -> array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))>
  requires  __is_same(std::std_array_with_3<T>,
                      std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)

先ほどど同じ理由で、エイリアステンプレートのTstd::arrayの推論補助の_Tpの対応が取れない結果、エイリアステンプレートのTが推論補助にフィードバックされてしまい、テンプレートパラメータがclass T, class _Tp, class ... _Upの3つになります。しかも、Tに対応する仮引数はないので(推論補助の左辺にTが現れていない)、結局auto Nと同じようにTを実引数から推論できず、コンパイルエラーを起こしています。

エラーメッセージを見ると、MSVCもほぼ同じ理由によってエラーとなっている様子です。 - godbolt

これは、推論補助に対する制約(_Tp_Up...のすべての型が同じ型である)をSFINAEによって行なっていることから生じています。

SFINAEを使わなくしたら行ける?

C++20からはコンセプトが使用でき、コンセプトによって制約してやればrequires節で(すなわち推論補助の右辺の外で)パラメータに対する制約を行えます。std::arrayの要素数を束縛するエイリアステンプレートの場合、そのようにすればCTADが正しく働く気がしてなりません。実験してみましょう。

#include <concepts>

template<typename T, std::size_t N>
struct my_array {
  T arr[N];
};

template<typename T, typename... Us>
  requires (std::same_as<T, Us> && ...)
my_array(T, Us...) -> my_array<T, sizeof...(Us) + 1>;


template<typename T>
using myarray_3 = my_array<T, 3>;

std::array相当のクラス(my_array)を作成して、これに対して要素数を束縛するエイリアステンプレート(myarray_3)を書いて試してみます。

int main() {
  // ok
  [[maybe_unused]]
  my_array a1 = {1, 2, 3, 4};

  // これは制約に引っかかって正しくエラーになる
  [[maybe_unused]]
  my_array a2 = {1, 2.0, 3, 4};
  
  // ok!
  [[maybe_unused]]
  myarray_3 a3 = {1, 2, 3};
}

無事正しく動きました。つまり、std::arrayの推論補助の実装でSFINAEを避ければ要素型の推論はできそうです。

(ただし、MSVCは受け入れてくれないようです)

ここでGCCがやっていることを確かめるために、わざとエラーを起こしてエラーメッセージを見てみます。

prog.cc: In function 'int main()':
prog.cc:24:28: error: class template argument deduction failed:
   24 |   myarray_3 a3 = {1, 2, 3.0};
      |                            ^
prog.cc:24:28: error: no matching function for call to 'my_array(int, int, double)'
prog.cc:10:1: note: candidate: 'template<class T, class ... Us>  requires (same_as<T, Us> && ...) my_array(T, Us ...)-> my_array<T, (sizeof... (Us) + 1)> requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>)'
   10 | my_array(T, Us...) -> my_array<T, sizeof...(Us) + 1>;
      | ^~~~~~~~
prog.cc:10:1: note:   template argument deduction/substitution failed:
prog.cc:10:1: note: constraints not satisfied
prog.cc: In substitution of 'template<class T, class ... Us>  requires (same_as<T, Us> && ...) my_array(T, Us ...)-> my_array<T, (sizeof... (Us) + 1)> requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>) [with T = int; Us = {int, double}]':
prog.cc:24:28:   required from here
prog.cc:10:1:   required by the constraints of 'template<class T, class ... Us>  requires (same_as<T, Us> && ...) my_array(T, Us ...)-> my_array<T, (sizeof... (Us) + 1)> requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>)'
prog.cc:10:1: note: the expression '(same_as<T, Us> && ...) [with T = int; Us = {int, double}]' evaluated to 'false'
prog.cc:4:8: note: candidate: 'template<class T> my_array(my_array<T, 3>)-> my_array<T, 3>'
    4 | struct my_array {
      |        ^~~~~~~~
prog.cc:4:8: note:   template argument deduction/substitution failed:
prog.cc:24:28: note:   mismatched types 'my_array<T, 3>' and 'int'
   24 |   myarray_3 a3 = {1, 2, 3.0};
      |                            ^
prog.cc:4:8: note: candidate: 'template<class T> my_array()-> my_array<T, 3>'
    4 | struct my_array {
      |        ^~~~~~~~
prog.cc:4:8: note:   template argument deduction/substitution failed:
prog.cc:24:28: note:   candidate expects 0 arguments, 3 provided
   24 |   myarray_3 a3 = {1, 2, 3.0};
      |   

ここでGCCが使用している推論補助は

template<class T, class ... Us>
  requires (same_as<T, Us> && ...)
my_array(T, Us ...) -> my_array<T, (sizeof... (Us) + 1)>
  requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>)

これは先ほど手(脳内CTADマシーン)で求めてみた推論補助の想定とほぼ一致しています。

std::arrayエイリアステンプレートでCTADするには結局どうすればいいんですか?

この質問は2つのパターンに分岐し、それぞれで答えが異なります。

  1. 素数を束縛したエイリアスの場合(最初のstd_array_with_int
    • 無理です(もしかしたら、推論補助の形を工夫すれば行けるかもしれませんが、わかりません・・・)
  2. 要素型を束縛したエイリアスの場合(最初のstd_array_with_3
    • 実装の推論補助をSFINAEを使わない形で書き直させれば行けます

1をなんとかするアイデアをお持ちの方がいましたら教えて欲しいです(多分LWG Issueとして提出できます)。

参考文献

謝辞

この記事の9割は次の方のご指摘によって成り立っています。

この記事のMarkdownソース