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

文書の一覧

全部で80本あります。

もくじ

N4961 2024-03 Tokyo meeting information

2024年3月末に東京で行われるWG21全体会議の案内。

N4962 WG21 agenda: 6-11 November 2023, Kona, HI

2023年11月にハワイのコナで行われるWG21全体会議のアジェンダ

N4963 2023 WG21 admin telecon meetings, rev. 2

11月の全体会議に先立って行われる、WG21管理者ミーティングの案内。

N4964 Working Draft, Programming Languages -- C++

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

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

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

新規に採択された提案はなく、編集上の変更のみです。また、C++23 DISに対するNBコメントの対応についても記されています。

P0447R23 Introduction of std::hive to the standard library

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

以前の記事を参照

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

  • Apendixのconstexprの使用法に関するセクションの更新
  • 「Design Decisions」セクションのbit_castreinterpret_castに置き換え
  • 「Erased-element location recording mechanism」セクションにどのブロックが消去されているかを追跡するためのさまざまなメカニズムに関するセクションを追加
  • 「Collection of element memory blocks + metadata」セクションの修正
  • 提案の最初の方にstd::hiveの構造に関するインフォグラフィックを追加
  • <=>を定義しないことについてのメモを削除
  • 標準の他の成長因子の記述と一致させるために、概要から、1より大きい成長因子を削除し、整数である必要はないを追加
  • 概要の'poem'を削除
  • LWGのフィードバックに基づいて、時間計算量を現在の実装の最大値で指定した(後から調整可能だが、ABI破壊が懸念される)
  • 小さな型をオーバーアライメントすることなく全ての型に対してO(1)で消去可能であるため、消去処理の時間計算量要件を削除
  • その他個人からのフィードバックの適用
  • 時間計算量に関するその他の修正
  • 代替実装方法の詳細を更新
  • 他の部分でカバーされていたため、clear()の説明を削除。同様に消去処理の説明も削除

などです。

P0876R14 fiber_context - fibers without scheduler

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

以前の記事を参照

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

  • uncaught_exceptions()current_exception()への変更を撤回し、結果が現在のスレッドで実行されている他のファイバーの例外を反映する可能性があることを明確化
  • “Cooperative Threads”セクションのタイトルから“User-Mode”を削除し、説明段落を削除
  • スタック領域を指定するコンストラクタからexplicitを削除
  • スタック領域を指定しないコンストラクタの例外条件にsystem_error: resource_unavailable_try_againを追加
  • スタック領域を指定するコンストラクタの例外条件にsystem_error: resource_unavailable_try_againを追加
  • ムーブ操作がムーブ元のfiber_contextの状態を空にすることを規定
  • 代入演算子からempty()事前条件を追加し、デストラクタと同様に! empty()効果(事後状態)を追加
  • “execution context.”へのresume_with()からの参照を削除
  • resume_with()のReturnsとThrowsの節を削除
  • can_resume()による複数スレッドからの呼び出しに関するRemarksを削除
  • 説明専用のメンバの型をvoid*へ変更
  • メンバ名等の調整
  • 機能テストマクロのセクションを移動
  • ヘッダ概要をクリーンアップ
  • 前方参照を持つクラスメンバをグループ化
  • std::swapの特殊化を追加
  • 段落番号を追加
  • 単一項目のダッシュリストを合理化
  • EnsuresをPostconditionsに変更
  • テンプレートパラメータの宣言のtypenameclassに変更
  • コンストラクタ規定の調整
  • entry_copy, stack_copy, deleter_copyfiber_constextのメンバになることを意図していないことを明確化
  • 説明専用オブジェクトの初期化の合理化
  • “Instantiates a fiber_context”を“Initializes state”へ言い換え
  • “empty() returns true”を“empty() is true”へ言い換え
  • スタック領域を指定するコンストラクタから、スタックサイズとアライメントについての事前条件を削除し、例外条件を例外種別ごとに記述
  • ムーブコンストラクタのEffectsを調整

などです。

P0952R1 A new specification for std::generate_canonical

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

std::generate_canonicalの現在の規定は、浮動小数点数の仕様を無視しているため誤って制約されています。それによって実質的に正しい実装が不可能になっています。この提案では主に2つの問題について指摘しています。

この関数の動作は次の3つの要件に従うはずです

  1. 結果は[0, 1)に入らなければならない
  2. アルゴリズムは正確に指定されており、使用するURBGは指定された引数に対して特定の固定回数だけ呼び出される必要がある
  3. 結果は一様分布となる

1つ目の問題は、これらの要件を満たしながらの実装が不可能である点です。現在の規定は、要件2を満たすように正確に数式によって指定されており、結果であるS/Rkは数学的には1未満の値を返します。しかし、浮動小数点数によってこの式が実行される場合、丸めによってこの結果は正確に1になる場合があります。このことは、generate_canonicalから取得したxに対して(1.0 - x)で除算している場合にバグを引き起こします。

すなわち、現在の規定では要件2を満たそうとすると、要件1に違反します。結果が1になってしまった場合に結果を修正しようとすれば要件3に違反します。要件1と3を満たすようにすると、結果が1となる場合にアルゴリズムを再実行する必要があり、それは要件2に違反します。

2つ目の問題は、除算と浮動小数点丸め(最近接偶数丸め)が組み合わさることで出力が均一とならない場合があることです。例えば、0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5という値に対して丸めを行うことを考える場合、最下位ビットを切り捨てれば0, 0, 1, 1, 2, 2, 3, 3となりこれは一様ですが、浮動小数点数において一般的な最近接偶数丸めを行うと0, 0, 1, 2, 2, 2, 3, 4となり偶数と奇数のバランスに偏りが生じる他、0にもバイアスが生じています。

これと同じことがstd::generate_canonicalの計算の過程で発生し、最下位ビットあるいはその周辺の数ビットにおいて結果のバイアスが生じることで最終的な結果の均一性が失われます(この辺りの説明はこの記事を書いている人が理解できていないので、提案の方を参照されるといいと思います・・・)。

この提案は、この2つの問題をstd::generate_canonicalのインターフェースを変更することなく解決するために、アルゴリズムや計算量についての規定を変更することで改善しようとするものです。

この提案は一部の引数の解釈やアルゴリズムそのものを変更することによって、non-trivial divisionを使用せず再近接偶数丸めの問題を回避しようとしています。そのため、副作用や計算量が変化し、また、同じ入力から生成される乱数列も変更前と異なることになります。そのため、それらに依存する既存のコードの動作は実行時に壊れます。

ただし、この提案ではstd::generate_canonicalの結果が区間[0, 1)内でRealTypeの表現可能な全ての値が含むようにすることは、定義が困難かつ実装が複雑化するとして行っていません。

P1144R9 std::is_trivially_relocatable

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

以前の記事を参照

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

  • 機能テストマクロ(__cpp_impl_trivially_relocatable__cpp_lib_trivially_relocatable)を追加
  • §3.1 "Design non-goals"セクションを追加し、P2785R3との比較を行う
  • Appendix C "Examples of non-trivially relocatable class types"を短い要約で置き換え
  • Appendix E "Open questions"は§5.2 "Confusing interactions with std::pmr"と重複しているため削除

などです。

P1729R3 Text Parsing

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

以前の記事を参照

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

  • イテレータではなくsubrangescanから返すようにした(scanの正常値のscan_resultsubrangeを内包するようになった)
  • formatterとの一貫性のため、scannerCharTテンプレートパラメータのデフォルトをcharにした
  • 数字文字列の3桁区切り(千毎の区切り)のパースフラグに関するオプションの設計議論を追加
  • 追加のエラー情報に関する設計の議論を追加
  • § 4.3.4 Width and precisionに幅と精度に関する説明を追加
  • § 2 Introductionの最後にこの機能の目的について追記
  • § 3.5 Alternative error handlingのエラー処理例を修正し明確化
  • § 4.2 Format stringsにwhitespaceの定義を追加し、whitespace以外のリテラル文字の一致を明確化
  • テキストエンコーディングに関するセクション§ 4.11 Encodingを追加し、読み取りコードユニットの処理に関するセクション§ 4.3.8 Type specifiers: CharTを追加
  • § 4.10 Localesにロケールの使用例を追加
  • 将来の拡張に関して§ 6.3 Reading code pointsを追加

などです。

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

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

以前の記事を参照

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

  • mask reductionの事前条件を削除。ただし、その決定の取り消しをLEWGに要求する予定
  • basic_simd_maskの単項演算子の戻り値型を修正
  • simd-select-implboolオーバーロードを修正
  • simd_splitにおける不必要な実装の自由度を削除
  • テンプレートパラメータの宣言時はtypenameではなくclassを使用する
  • ブロードキャストコンストラクタのconstexpr-wrapper-like引数の値に関するSFINAEの実装
  • basic_simd_maskに関係演算子を追加
  • size_tintの使用法に関するセクションを追加
  • 未解決の設計に関するセクションをすべて削除し、LWG向けの文言に関する質問のみを残すようにした
  • 実装ノートにLWGへの質問を追加
  • オーバーロード候補を削減するために二項演算子に制約を追加

などです。

P2019R4 Thread attributes

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

このリビジョンでの変更は、R3で提案されたmake_with_attributes()のアプローチがLEWGに好まれず、既存のstd::threadコンストラクタ呼び出しの前に属性を追加できるようにする方向性が好まれ、また、型がスレッド属性であることを検出する実装方法を提供すべきではないとされたためmake_with_attributes()およびthread_attribute基本クラスが削除されたことです。

このリビジョンでは、当初のように専用のクラスの値としてスレッド属性を渡しますが、その位置はスレッドで実行する処理の前になりました。

namespace std {
  
  // スレッド名を指定するクラス
  class thread_name {
  public:
    constexpr thread_name(std::string_view name) noexcept;
    constexpr thread_name(std::u8string_view name) noexcept;
  private:
    implementation-defined __name[implementation-defined]; // 説明専用
  };

  // スタックサイズを指定するクラス
  class thread_stack_size {
  public:
    constexpr thread_stack_size(std::size_t size) noexcept;
  private:
    constexpr std::size_t __size; // 説明専用
  };

  class thread {
    ...

    // デフォルトコンストラクタ(元からある
    thread() noexcept;

    // 処理とその引数を受け取るコンストラクタ(元からある
    // template<class F, class... Args>
    // explicit thread(F&& f, Args&&... args);

    // 属性、処理、その引数、を受け取るコンストラクタ
    template<class... Args>
    explicit thread(Args&&... args);

    ...
  
  private:
    template <class F, class... Attrs>
    thread(attribute-tag, F && f, Attrs&&... attrs); // 説明専用
  };

  // jthreadも同様
}
void f();

int main() {
  // スレッド名とスタックサイズの指定
  auto thread = std::jthread::(std::thread_name("Worker"),
                               std::thread_stack_size(512*1024),
                               f);
}

処理に渡す引数がある場合は、今まで通りこのスレッド処理の後ろに指定します。

P2075R3 Philox as an extension of the C++ RNG engines

<random>にPhiloxというアルゴリズムベースの新しい疑似乱数生成エンジンを追加する提案。

以前の記事を参照

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

  • 純化のためにいくつかのエイリアスを削除
  • 渡される値の数を制御するためにstd::counterAPIを変更
  • set_counter, get_counterの機能とAPIについて説明する設計上の考慮事項に関するセクションを拡張
  • コードの移植性を維持するために、set_counterに渡されるWord値の解釈についてリトルエンディアンであるという指定を削除
  • counter_sizeの計算ロジックの曖昧さを回避するために、pseudo_random_functionコンセプトをcounter_sizeパラメータを使用して拡張

などです。

P2267R0 Library Evolution Policies

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

ここでのポリシーとは、C++標準ライブラリの将来のユーティリティの提案者が従うべき技術ルールもしくは技術ガイドラインのことです。

この提案では、ポリシーの利点と欠点とを勘案しながら、LEWGのレビューを通過した提案全てに適用されるものとしてのポリシーを設定し、それを常設文書(SD-9)として設定することを提案しています。ただし、提案しているのはポリシーの確立と文書化のプロセスのみで、ポリシーそのものは例示するに留めています。

ポリシーを設定することの利点と欠点としては次のようなものが挙げられています

  • 利点
    1. ポリシーによって、標準ライブラリの様々な部分の動作におけるユーザーの期待に対して統一性を持たすことができる
    2. ポリシーによって、提案の著者とレビューする委員会の両方の時間が節約される
    3. ポリシーは共有された知識ベースから作成される必要がある(それによって、それら知識の断片化や解釈の不一致を最小限に抑えることができる)
    4. ポリシーによって、標準化プロセスを新規参入者にとって優しいものにすることができる
  • 欠点
    1. ポリシーは、委員会内で少数派領域の代表の意見を押し退けてしまう可能性がある
    2. 提案された一部のユーティリティに対しては、ポリシーによって誤った技術的解決策を強制してしまう可能性がある

欠点でも触れられているように、単一の原則が全てに適合することはなく、あるポリシーについてライブラリ全体の合意に達することには困難が伴います。この提案では、設定されたポリシーは常に強制されるものではなく、ポリシーに違反していることを明示しその理由について説明されていれば、ポリシーを常に守る必要はない、という運用にすることを提案しています。

小さなポリシー1つをとってもライブラリ全体での合意を得るには時間がかかりますが、そのような議論を提案ごとではなくポリシーを確立する1度だけ行っておくことで、長期的には時間の節約になり他の利点を得ることもできる、としています。

P2447R5 std::span over an initializer list

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

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを(再)追加したこと、Annex Cのための文言を追加したことなどです。

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

P2500R2 C++ parallel algorithms and P2300

P2300で提案されている実行コンテキストの指定を、C++17の並列アルゴリズムにも適用できるようにすることを目指す提案。

以前の記事を参照(番号が間違って公開されたとのことで提案番号が変更されています)

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

  • 逐次実行をデフォルトとするようにexecute_onの実装を変更
  • コンパイルエラーまたは実行時エラーを起こすexecute_onのカスタマイズを許可
  • アルゴリズムexecute_onのカスタマイズを行う例を追加
  • 提案しているアプローチの拡張性に関するサブセクションを追加
  • 遅延for_eachアルゴリズムの可能なシグネチャを追加

などです。

P2542R6 views::concat

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

このリビジョンでの変更は、!common_range && random_access_range && sized_rangeに関するセクションを追加したことです。

!common_range && random_access_range && sized_rangeという条件は、これを入力範囲の最後のrangeが満たしている場合にconcat_viewcommon_rangeになるべきではない、という条件でした。これを反映してconcat_viewcommon_rangeとなるかどうかは入力の最後のrangecommon_rangeであるかによっていました。

しかし、LWGのレビューにおいてconcat_viewbidirectional_rangeとしての動作はcommon_rangeの動作と一貫する必要がある事が示されました。これはすなわち、!common_range && random_access_range && sized_rangeを満たすような範囲をサポートしないことを推奨するものです。

これによるメリットは、concat_viewの実装および標準文言が簡素化されることがあります。また、random_acces_rangeとなる場合についても同様の要求をすることでさらに実装を簡素化することができます。

LEWGではその方向性が支持され、次のリビジョンにおいて適用されるようです。

!common_range && random_access_range && sized_rangeであるような範囲をconcatしたい場合でも、一度views::commonに通しておけばconcatすることができます。

P2642R4 Padded mdspan layouts

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

以前の記事を参照

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

  • 標準ドラフトのレイアウトに従うように修正
  • submdspanの文言を書き直し

などです。

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

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

以前の記事を参照

このリビジョンでの変更は、P1928R6でstd::simdに加えられた変更を適用したことです。

P2664R4 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

  • P1928R3に合わせて、simd/simd_maskbasic_simd/basic_simd_maskに変更
  • 推定されたマスクをネストされたtypedefに置き換え
    • basic_simd_mask<T, MaskABI>の代わりにbasic_simd<T, ABI>::mask_typeを使う
  • fixed_size型を対応する新しい型に置き換え
    • 例えば、fixed_size_simd<T, 5>simd<T, 5>になる
  • 圧縮及び展開で異なるサイズのsimd値を取る事の出来る設計オプションを削除
  • マスクパラメータの位置を並べ替え
    • マスクを最初に指定するようになり、simd_selectcopy_toなどと一貫するようになった

などです。

P2717R3 Tool Introspection

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

以前の記事を参照

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

  • 2023年6月の会議における投票結果を追記
  • イントロスペクション情報を取得するためのプログラム実行が望ましくない場合などでもその情報を提供するために、イントロスペクションファイルを使用できるようにした
  • 2種類のコマンドラインオプション構文のサポートを追加して、オプション構文解析を変更せずにこの提案の内容をサポートできるようにした
  • バージョンについて、セマンティックバージョニングを採用

などです。

P2762R2 Sender/Receiver Interface For Networking

現在のNetworking TSにP2300のsender/receieverサポートを入れ込む提案。

以前の記事を参照

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

  • Obtaining the Schedulerセクションにソケットコンテキストの利用について追記
  • schedulerインターフェースが非標準の操作に関して拡張可能である必要があるという要件について追記
  • コルーチンをより分かりやすくするために、awaitablesenderに関する説明を追加

などです。

P2774R1 Concurrent object pool (was: Scoped thread-local storage)

並行アルゴリズムにおけるキャッシュとして使用可能な並行オブジェクトプール機能の提案。

以前の記事を参照

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

  • SG1のレビューを受けて再設計
  • 設計をTLSから並行オブジェクトプールに向けたものに変更
  • 設計はstd::atomicで表現できる範囲に制限されなくなった

などです。

以前はTLSの観点から機能を定義し説明していましたが、このリビジョンでは並行アルゴリズムから使用されるオブジェクトプールとして機能を再定義しています。それに伴って、クラス名がstd::object_pool<T>へ変更されるとともにインターフェースも変更されています。

とはいえ、基本的な役割やモチベーションには変化がありません。

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

// 入力データ
std::span<Triangle> input = …;
double max_area = …;

// 並行オブジェクトプール
std::object_pool<std::vector<Triangle>> tmp;

// メインの並行処理
std::for_each(std::execution::par, input.begin(), input.end(),
  [&](const auto & tria) {
    // スレッド固有の領域を初期化
    auto handle{tmp.lease()};

    // スレッドローカルvectorを取得
    auto& object = *handle;

    // 結果(複数)をスレッドローカルな領域へ出力
    for (const auto & t : split(tria, max_area)) {
      object.emplace_back(t);
    }

    // handleのデストラクタによってobjectが再利用可能になる
  }
);

// 後処理、シングルスレッド
for(const auto & tria : tmp.lease_all() | std::views::join) {
  process(tria);
}

std::object_poolの実装例

template<default_initializable T, typename Allocator = allocator<T>>
class object_pool {
  mutable mutex mutex;
  mutable intrusive_list<T, Allocator> storage;

  class handle; // see below
  class snapshot; // see below

public:
  object_pool(Allocator allocator = Allocator{}) noexcept;
  object_pool(const object_pool &) =delete;

  auto operator=(const object_pool &) -> object_pool & =delete;
  ~object_pool() noexcept;
  
  // Tの単一オブジェクトの所有権を取得、プールが空なら新しいオブジェクトを割り当てる
  [[nodiscard]]
  auto lease() const -> handle; 
  
  // 現在のプールで利用可能なすべてのTオブジェクトの所有権を取得
  [[nodiscard]]
  auto lease_all() const noexcept -> snapshot;
};

std::object_pool::handleはRAIIによってプール内の単一オブジェクトへのアクセスを管理するクラスです。

実装例

template<default_initializable T, typename Allocator>
class object_pool<T, Allocator>::handle {
  object_pool & owner;
  typename decltype(storage)::node_type object;
public:
  handle() =delete;
  handle(const handle &) =delete;

  auto operator=(const handle &) -> handle & =delete;
  ~handle() noexcept;
  
  // プール上のある1つのTオブジェクトにアクセス
  auto operator*() const noexcept -> T &;
  auto operator->() const noexcept -> T *;
  auto get() const noexcept -> T *;
};

同様に、std::object_pool::handleはプール内の複数(全て)のオブジェクトへのアクセスを管理します。上の例のように、これは並行処理の後でプールにためたデータを消費する後処理において使用することを意図しています。

template<default_initializable T, typename Allocator>
class object_pool<T, Allocator>::snapshot {
  vector<handle> handles;
public:
  snapshot() =delete;
  snapshot(const snapshot &) =delete;

  auto operator=(const snapshot &) -> snapshot & =delete;
  ~snapshot() noexcept;
  
  class iterator { … };
  static_assert(forward_iterator<iterator>);
  
  auto begin() noexcept -> iterator;
  auto end() noexcept -> iterator;
};

P2786R3 Trivial Relocatability For C++26

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

以前の記事を参照

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

  • EWGをターゲットとし、LEWGをフォローアップ
  • 主要提案としてタイトルを変更
  • ライブラリ拡張機能をP2959R0とP2967R0へ移動
  • 文書レイアウト等の調整
  • モチベーションとなる使用例を追加
  • 前のリビジョンでの変更を考慮して例を修正
  • 提案する文言を完成させた
  • 未解決の質問セクションを追加
  • trivially_relocate()関数からconstexprを削除

などです。

P2800R0 Dependency flag soup needs some fiber

依存ライブラリとしてモジュールを使用する際に、必要となる情報について説明する文書。

この文書では、あらゆるプロジェクトにおいて共通する、そのプロジェクトを有意義に活用するために必要な一連の手順があり、その手順のために必要な各種情報(コンパイラ/リンカオプション、動的ローダーの環境変数プラグインローディング環境変数などを通じて伝達される傾向にあるあらゆるもの)のことをプロジェクトの使用要件(usage requirement)と呼んでいます。この使用要件を発見することはビルドシステムの仕事の中核をなしています。

また、その使用要件を収集するための(ツールチェーンに依存する)方法のことをフラグスープ(flag soup)と呼んでいます。

この文書は、現在のフラグスープに代わるより構造化された使用要件の伝達手段の必要性を説くものです。

文書の意図は、モジュールを使用するクライアントはクライアントによって異なる使用要件を持ち、同じプロジェクト内でさえも同じモジュールに対して翻訳単位ごとに異なるBMIを取得する可能性があり、現在のフラグスープではその用途のために十分でないことを示すことにあります。また、そのような使用要件をより完全に伝達することができるようにすることで、BMIを再利用しやすくなるメリットもあります。

P2809R2 Trivial infinite loops are not Undefined Behavior

自明な無限ループを未定義動作ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、寄せられたフィードバックの適用などです。

EWGにおける投票では、提案する2つのうちオプション2を採用する方に合意され、CWGに転送されています。

P2841R1 Concept Template Parameters

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

以前の記事を参照

このリビジョンでの変更はよくわかりませんが、構文等に変更はないようです。

P2845R3 Formatting of std::filesystem::path

P2845R3 Formatting of std::filesystem::path

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

以前の記事を参照

R3での変更は

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

P2862R1 text_encoding::name() should never return null values

std::text_encoding の提案(P1885R12)に含まれる text_encoding::name() の仕様を変更する提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなるドラフトをP1885R12がマージされたN4958に更新した事です。

P2863R2 Review Annex D for C++26

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

以前の記事を参照

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

  • この文書の変更履歴と文書内の派生提案の参照時にリビジョン番号を付加するようにした
  • 派生提案のステータスを更新

などです。

P2885R3 Requirements for a Contracts syntax

C++契約プログラミング機能の構文の選択のための評価基準や要件をまとめる文書。

以前の記事を参照

このリビジョンでの変更は、この提案の採択の投票後に寄せられた構文要件をAppendix Aに追加したことです。

追加された構文要件は次の3つです

  1. 契約注釈に対する属性の指定
  2. 契約ラベルの事前宣言
    • ラベルやメタアノテーションを後の宣言にのみ付加する場合に、その存在を最初の宣言でほのめかす構文が必要となる
  3. 関数型に対する契約注釈
    • 関数型に対して契約注釈を行う方法を提供する必要がある
    • 関数そのものと関数型に対するものとの間で、構文上の曖昧さがあってはならない

 

P2893R1 Variadic Friends

friend宣言でのパック展開を許可する提案。

以前の記事を参照

このリビジョンでの変更は、clangのフォークでこの提案を実装したうえでCompiler Explorerから利用可能になったことへの謝辞を追加したことです。

P2897R1 aligned_accessor: An mdspan accessor expressing pointer overalignment

mdspanのアクセサポリシークラスに、参照する領域ポインタにstd::assume_alignedを適用してアクセスするaligned_accessorの提案。

以前の記事を参照

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

  • gcd変換コンストラクタのConstraintをMandateに変更
  • is_sufficiently_alignedを利用してポインタのオーバーアライメントの事前条件をチェックする例を追加
  • aligned_allocを使用してオーバーアライメントされたメモリ確保を行う例を追加
  • aligned_mdspan y{x.data_handle(), x.mapping()}の代わりにaligned_mdspan y{x}のように使用できるように、default_accessorからのexplicitコンストラクタを追加
  • aligned_accessorのMandateにbyte_alignment >= alignof(ElementType)trueであることを追加。これにより、無効なaligned_accessorオブジェクトの構築が防止される
  • aligned_mdspanエイリアスを含めない理由の説明
  • aligned_accessor構築の安全性について説明

などです。

P2900R0 Contracts for C++

P2900R1 Contracts for C++

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

C++20で一度標準入りしてから撤回され、MVPという最小仕様を確立すべくSG21で議論を重ねられてきました。この提案はC++26に向けて現時点でのContracts MVP仕様をまとめ、それを正式な契約プログラミング機能としてC++26に導入するためのものです。

この提案は大きく設計と文言のセクションに分かれており、設計セクションではMVPの契約機能の構文と意味論について説明され、文言セクションでは設計セクションで説明された機能をC++言語で有効化するための標準のための文言の変更が記載されています(現時点では文言は未整備ですが)。設計セクションを読むことでC++26契約プログラミング機能がどういうものかを把握することができるでしょう。

ただし、現在のところ構文がまだ決まっておらず、いくつか小さめの未解決の問題が残されているため、まだ完全なものではありません。

P2911R1 Python Bindings with Value-Based Reflection

提案中の値ベースリフレクションによってPythonバインディングの作成を簡素化できることを示す文書。

以前の記事を参照

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

  • この提案が対象としない事項について明確化
  • 関数引数名のリフレクションとそれによって無効なリフレクションエラーが発生する場合についての議論の追加
  • デフォルトのバインディングによって引き起こされる危険な動作のよりよい例を追加
  • リフレクションされたエンティティの範囲を拡張して、その名前の範囲を生成するためのmeta::name_of/meta::names_of関数の詳細な説明
  • オーバーロードされた演算子バインディングに関するセクションを追加
  • Classdesc frameworkと値ベースリフレクション、および将来の作業とを比較するセクションの追加

などです。

P2927R0 Observing exceptions stored in exception_ptr

std::exception_ptrを再スローせずに例外オブジェクトの取得を試みる関数の提案。

std::exception_ptrは例外オブジェクトを型消去して可搬にすることができるラッパー型であり、例外オブジェクトを取り扱いやすくすることに貢献します。しかし、std::exception_ptrはかなり制限されたAPIしか持たず、現在参照している例外オブジェクトの型の情報などを取得することができません。

それによって、他のエラー伝達手段と比較してエラーを処理するための方法が制限されます。例えば、std::optional/std::expectedのようなモナディックインターフェースを実装しようと思うと、std::exception_ptrの参照する例外オブジェクトを一旦再スローし、ハンドルされない場合はキャッチして再びstd::exception_ptrに格納しなおすようなことをしなければならず、これはとても重い処理になります。

この提案はそのために、再スローをすることなくstd::exception_ptrの例外オブジェクトを取得するためのAPIを追加しようとするものです。

この提案では、try_cast<T>()という関数を提案しています。これは、std::any_caststd::get_ifなどとよく似たAPIで、std::exception_ptrを引数で渡してTに想定する型を指定し、std::exception_ptrの参照する例外オブジェクトの型がT(もしくはその曖昧でない基底クラスがT)である場合に例外オブジェクトへのポインタを返し、そうでない場合はnullptrを返します。

template <typename T>
const std::remove_cvref_t<T>* 
  try_cast(const std::exception_ptr& e) noexcept;

提案文書より、サンプル

struct Foo {
  virtual ~Foo() {}
  int i = 1;
};

struct Bar : Foo, std::logic_error {
  Bar() : std::logic_error("This is Bar exception") {}
  int j = 2;
};

struct Baz : Bar {};


int main() {
  const auto exp = std::make_exception_ptr(Baz());

  if (auto* x = try_cast<Baz>(exp)) {
    printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j); 
  }
  if (auto* x = try_cast<Bar>(exp)) {
    printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j);
  }
  if (auto* x = try_cast<Foo>(exp)) {
    printf("got '%s' i: %d\n", typeid(*x).name(), x->i);
  }
}

この出力は、例えば次のようになります

got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' i: 1

try_cast<T>(exptr)は、exptrの中身を再スローした時にcatch節に記述してマッチングする型がTに指定された場合に例外オブジェクトへのポインタをconst T*として返し、マッチングしない場合はnullptrを返します。

このような機能の実装のためには例外機構に手を加える必要がありそうですが、GCC/MSVC(libstdc++/MSVC STL)は非公開ながらそのような機能を持っており、それぞれの実装者も実装可能であると言っているようです。さらに、metaのfollyというライブラリではこれとよく似た機能がポータブルに実装されており、metaの様々なサービス内部で使用されているようです。

このような機能はまた、将来的にパターンマッチング機能においてstd::exception_ptrのパターンマッチングを可能にすることもできます。

P2932R1 A Principled Approach to Open Design Questions for Contracts

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

以前の記事を参照

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

  • ラムダキャプチャの回避策を明確化
  • 信頼性(reliability)、直交性(orthogonality)、その他議論の原則を追加
  • コンパイル時評価を処理するアプローチの明確化
  • 提案1について代替案を追加

などです。

提案1はトリビアルメンバ関数に対する契約注釈に関するものでした。この提案ではR0での提案を提案1.Aとして、新しい解決案を提案1.Bとして追加しています

  1. トリビアルな特殊メンバ関数と契約
    • 提案A
      • 契約注釈は関数のトリビアル性に影響を与えない
      • そのような契約注釈は評価されない可能性がある
    • 提案B

2023年11月のKona会議において、このリビジョンで追加された提案1.BをC++26の契約プログラミング機能における制約として採用したようです。

P2935R3 An Attribute-Like Syntax for Contracts

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

以前の記事を参照

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

  • 事前・事後条件指定位置をpure-specifier= 0)の前に移動
  • 契約注釈そのものに対する属性指定位置を明確化
  • [[assert : expr]]が式とする具体的な提案の追加

などです。

P2959R0 Container Relocation

ブロックベースコンテナ(特にstd::vector)の再配置時の動作を修正する提案。

標準ライブラリのコンテナは主にノードベースコンテナ(std::list, std::mapなど)とブロックベースコンテナ(std::vector, std::dequeなど)の2つに大別することができます。

コンテナが管理する個別のオブジェクトの事を要素(element)と呼び、その要素の状態の事を値(value)と呼ぶとするとき、要素と値の区別は理論的なもので、この違いが実際のコードで現れることはほとんどありません。

しかし、ノードベースコンテナとブロックベースコンテナの間でこの違いが顕在化する場合があります。

using element = std::tuple<int &>;

static_assert(std::is_move_assignable_v<int &>);
static_assert(std::is_move_assignable_v<element>);

template<typename C>
void test() {
  int a = 1;
  int b = 2;
  int c = 3;

  C x;
  x.emplace_back(a);
  x.emplace_back(b);
  x.emplace_back(c);

  std::cout << "a:\t" << a << "\n"                            // 1
            << "b:\t" << b << "\n"                            // 2
            << "c:\t" << c << "\n";                           // 3


  auto const mid = std::next(x.begin());

  std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"    // 1
            << "x[1]:\t" << std::get<0>(*mid)      << "\n"    // 2
            << "x[2]:\t" << std::get<0>(x.back())  << "\n";   // 3

  x.erase(mid);

  std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"    // 1
            << "x[1]:\t" << std::get<0>(x.back())  << "\n";   // 3

  b = 4;
  c = 5;

  std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"    // 1
            << "x[1]:\t" << std::get<0>(x.back())  << "\n";   // 5 or 4
}

int main() {
  test<std::vector<element>>();
  std::cout << "----------------\n";
  test<std::list<element>>();
}

この実行結果は次のようになります

a:   1
b:  2
c:  3
x[0]:   1
x[1]:   2
x[2]:   3
x[0]:   1
x[1]:   3
x[0]:   1
x[1]:   4
----------------
a:  1
b:  2
c:  3
x[0]:   1
x[1]:   2
x[2]:   3
x[0]:   1
x[1]:   3
x[0]:   1
x[1]:   5

一番最後のx[1]の出力結果だけが異なっています。

std::tuple<int&>の構築時は、メンバの参照がコンストラクタ引数のオブジェクトへ束縛されますが、代入時はメンバの参照の切り替えではなくメンバ参照の参照先のオブジェクトへの代入が行われます。

std::vectorの要素が削除される時、特に中間位置にある要素が削除されるとき、現在のブロック全体を確保しなおすのではなく、削除された位置よりも後ろの要素を前にずらすようにして再配置が行われます。この時、各要素が破棄されてから再構築されるのではなく、ムーブ代入によって要素の移動が起こります。そのため、x.erase(mid)ではbへの参照をメンバに持つtuple要素を削除しますが要素の破棄はせず、そこにはすぐ後ろのcへの参照をメンバに持つtupleが代入されます。それによって、b = cのような値の移動が起こり、vectorから削除されるのはbの参照ではなくcの参照です。そのため、その後b = 4をするとvectorの2番目の要素からもそれが観測できます。

std::list(他ノードベースコンテナ)の場合は単に1つのノードが削除されリンクが修正されるだけなので、このような要素の再利用は起きず、結果は意図通りになります。

このことはまた、std::vectorそのものの状態によっても挙動に差異が生まれる場合があります。

int main() {
  int a = 1;
  int b = 2;

  using element = std::tuple<int &>;
  std::vector<element> v;
  v.reserve(4);
  assert(4 == v.capacity());

  // aの参照で埋める
  auto fill = [&](int & i ) {
    v.clear();
    for (int j = 0; j != 4; ++j) {
      v.emplace_back(i);
    }
  };

  // 真ん中にbの参照を挿入
  auto inject = [&](int & i) { v.emplace(v.begin() + 2, i); };

  // 値と参照先変数名を出力
  auto report = [&] {
    for (auto& j : v) {
      std::cout << std::get<0>(j);
    }

    for (auto& j : v) {
      if (&a == &std::get<0>(j)) {
        std::cout << 'a';
      }
      else if (&b == &std::get<0>(j)) {
        std::cout << 'b';
      }
      else {
        std::cout << '?';
      }
    }

    std::cout << '\n';
  };

  for (int dummy : {1, 2}) {
    fill(a);
    inject(b);
    report();
  }
}
11211aabaa
22222aaaaa

std::vectorのキャパシティの状態によってこの挙動の差異が生じており、キャパシティが丁度4つ分の場合はinject()においてブロック再確保が発生し全ての要素は再構築されますが、キャパシティが充足している場合は再確保が発生せず、挿入は再代入によって行われます。

これらの振る舞いは現在の規格の規定に則ったもので、現時点でもこの仕様によって引き起こされる懸念がいくつかありますが、トリビアルリロケーションを考慮するとそれらの懸念はより大きいものになり得ます。そして、この懸念はコンテナの要素型とコンテナのアロケータ型の2つの異なる原因から生じています。

  1. ムーブ構築ではなくムーブ代入による要素の置換
    • 要素型のムーブ代入によって、破棄とムーブ構築とは異なる状態が生成される場合、ブロックベースコンテナはノードベースコンテナとは異なる振る舞いをする
  2. アロケータは要素の同一性を考慮する必要がある
    • ムーブ代入によって内部再配置が行われる場合に、構築された要素のID(アドレス)が重要である場合、アロケータの.destroy()の期待に反する可能性がある
  3. 強い例外安全保障が破られる可能性がある
    • アロケータのconstruct()がカスタマイズされている場合、構築時に要素型のコンストラクタ以外から例外が投げられないことを仮定できない
    • 現在のブロックベースコンテナに対する強い例外安全保障は要素型のムーブコンストラクタのみを考慮しており、アロケータを考慮していない
  4. トリビアルリロケーションのサポートにおいて問題が起こりうる
    • アロケータがconstruct()を提供する場合、(3と同様の理由により)要素型のトリビアルリロケーション可能性の情報を利用できない
  5. 再配置は標準ライブラリの未初期化アルゴリズムと同様にオブジェクトを作成する
    • <memory>に現在のuninitialized_*アルゴリズムに対応する、ムーブ構築を基本とし利用可能な場合はリロケーションを利用して最適化する汎用の関数を追加する必要がある

この提案はこれらの懸念に対処するために次のような変更を標準ライブラリに加えることを提案しています

  1. ムーブ構築/代入が一貫しない振る舞いをする型のために、新しい型特性を追加する
  2. アロケータが独自の実装を提供せず1の特性がfalseとなる場合に、現在の動作をデフォルトとする内部再配置(置換)をサポートするための非静的メンバ関数std::allocator_traitsに追加する

1つめの新しい型特性はstd::container_replace_with_assignment<T>というもので、これはTのムーブ代入と破棄+ムーブ構築が異なる振る舞いをする(上記のstd::tuple<int&>のように)ことを通知するものです。

namespace std {
  template <class T>
  struct container_replace_with_assignment : is_move_assignable<T>::type {};

  template <class T>
  constexpr bool container_replace_with_assignment_v = container_replace_with_assignment{};
}

下位互換性のために、ムーブ代入が可能な型ではtrueとなるのがデフォルトとされます。意図的にfalseとするには部分特殊化を定義します

namespace std {
  template <class ...TYPES>
  struct container_replace_with_assignment<tuple<TYPES...>>
    : conjunction<container_replace_with_assignment<tuple<TYPES>>...>::type
  {};
}

2つめの関数はstd::allocator_traits::relocate()という関数で、これは主にコンテナ内で再配置が起こる場合に再配置の方法をカスタマイズするものです。次のような動作をします

  1. アロケータ型がrelocate()メンバ関数を定義する場合、それを呼び出す
  2. アロケータ型が少なくとも1つのconstruct()/destroy()を定義している場合、再配置対象の右辺値を渡してそれらを利用する
  3. 要素型がトリビアルコピー可能ならば、memmoveを利用する
    • トリビアルなリロケーションが導入された場合、それで置き換えられる
  4. それ以外の場合、ムーブ代入/構築によって再配置
    • std::container_replace_with_assignment<T>trueならばムーブ代入によって再配置(現在の動作)
    • std::container_replace_with_assignment<T>falseならば破棄とムーブ構築によって再配置

コードで書くと、次のようになります

if constexpr (requires requires{ allocator::relocate(...); }) {
  // forward to allocator::relocate
} else if constexpr(requires requires{ allocator::construct(...); }) {
  // `allocator_traits::destroy(); allocator_traits::construct(rvalue)` each element
} else if constexpr (is_trivially_relocatable_v<T>) {
  // trivially relocate
} else if constexpr (container_replace_with_assignment_v<T>) {
  // move-assign elements
} else {
  // destroy-move-construct elements
}

現在動作しているコードは基本的に引き続いて4番目の動作を選択することで動作を維持します。3番目の動作はさらに、トリビアルコピー可能な型に対しての最適化を組み込んでいます。また、4番目の動作では同時に提案しているstd::container_replace_with_assignment<T>を使用してムーブ代入の使用が適切かどうかを判定し、不適切な場合は再配置先要素の破棄の後そこにムーブ構築することで再配置を行います。

これらの変更によって、後方互換性を維持しつつ現在の振る舞いを修正するとともに、将来的なトリビアルリロケーションを適切に有効化することができます。ただし、この提案は現在の実行時の動作を静かに変更する可能性があります。

P2961R1 A natural syntax for Contracts

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

以前の記事を参照

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

  • default/delete指定と一貫させるために、pre-or-postcondition(事前/事後条件指定)がpure-specifier= 0)の前に来るように調整
  • 事後条件における戻り値を指す名前の指定が必須ではないことを明確にした
  • assertキーワードの代替案に関する説明の追加
    • 代替案の候補としてcontract_assertassertexprを追加
  • Cにおける実現可能性についての議論を追加
  • C++26でこの構文によって契約機能を追加した後の、クラス不変条件の構文に関する議論を追加
  • P2885R3からの新しい3つの構文要件に関する議論の追加

などです。

この提案のR2が2023年11月のKona会議でレビューされ、アサートのキーワードとしてはcontract_assertを採用することに決定したようです。同時に、C++26の契約プログラミング機能のための構文としてこの提案のnatural syntaxが採用されることになったようです。

P2962R0 Communicating the Baseline Compile Command for C++ Modules support

Baseline Compile Commandの説明と、それを伝達する方法についての提案。

ある翻訳単位におけるヘッダユニットのインポートは、その翻訳単位のプリプロセッサ状態の影響を受けず、ヘッダユニットはマクロをエクスポートするためヘッダユニットがインポートされるとその翻訳単位のプリプロセッサ状態は更新されます。一方で、ヘッダユニットはその翻訳単位に指定されているコンパイラオプション(コンパイラコマンド)は適用されなければなりません。

ただし、ヘッダユニットのインポートにおいてはその翻訳単位のプリプロセッサ状態を含めたローカルプリプロセッサ引数を適用しないようにする必要があります。この制約は、同じヘッダユニットを異なる翻訳単位でインポートした時でも、双方の翻訳単位において同じようにインポート可能なヘッダをパースするためのものです。特に、推移的なインポートが起こる場合に同じヘッダユニットの内容が異ならないようにするために求められることです。そのため、ローカルプリプロセッサ引数とはある翻訳単位に固有な、コンパイル中のプリプロセッサ状態に影響を与えうる引数のことです。

また、SG15における合意ではビルドシステム以外のものがコンパイルコマンドの構成を行うべきではないというものがあるため、ローカルプリプロセッサ引数の区別を行うのはビルドシステムであるとして、依存関係スキャンプロセス(これを行うのはコンパイラや静的解析ツールなど)にはその翻訳単位自身のコマンドライン引数と、インポートされた全てのヘッダユニットをコンパイルするために使用されるBaseline Compile Commandの2つの入力(コマンドライン引数)が必要となります。

この提案におけるBaseline Compile Commandは、次のような情報のことです

  1. どのファイルがコンパイルされるか
  2. どのような出力が生成されるべきか
  3. ローカルプリプロセッサ引数を含まないコンパイルオプションの一部

ビルドシステムは、各翻訳単位をビルドするために必要なコンパイルコマンドを構成し、そこからBaseline Compile Commandを区別する役割を担います。したがって、ある翻訳単位におけるBaseline Compile Commandはそのコンパイルにおいて使用されるコンパイルオプションの一部分であり、それを区別する方法にはいくつか問題があります。

この提案では、コンパイルオプションからBaseline Compile Commandを独立させてファイルに保存し、依存関係スキャンプロセスへの入力にはコンパイルオプション及びBaseline Compile Commandを記録したファイルパスを渡すようにすることを提案しています。

提案より、LLVMJSON Compilation Database(いわゆるcompile_commands.json)をBaseline Compile Commandを含むように拡張する例

{
    "directory": "/path/to/build/dir",
    "file": "/path/to/source/main_translation_unit.cpp",
    "arguments": [ "g++", "-o" ,"main_translation_unit.o",
                   "-DFOO=1", "-DBAR=2", "-I/one/path",
                   "-I/other/path" ],
    "output": "main_translation_unit.o",
    "baseline-arguments": ["g++", "-DFOO=1", "-I/one/path" ]
}

一番最後のbaseline-argumentsフィールドがBaseline Compile Commandです。

P2967R0 Relocation Is A Library Interface

リロケーションサポートのためのライブラリ機能の提案。

この提案はP2786で提案されているトリビアルリロケーションをサポートするための、追加のライブラリ機能を提案するものです。P2786では主にコア言語にトリビアルなリロケーション可能性の概念を提案し、それを検出して活用するために必要な最小限のライブラリ機能のみが提案されています。この提案は、標準ライブラリ全体でトリビアルリロケーションを活用するための機能、特にユーザーが自身のコードでリロケーションを活用するときに必要となる機能についてを提案するものです。

ここで提案されているライブラリ機能は2つだけで、まず1つはstd::relocate()です

namespace std {
  template <class T>
    requires (is_trivially_relocatable_v<T> || is_nothrow_move_constructible_v) && !is_const_v<T>
  constexpr T* relocate(T* begin, T* end, T* new_location);
}

これは[begin, end)の領域にあるTのオブジェクトをnew_locationの領域へリロケーションするものです。その際、トリビアルではないリロケーションも含めてあらゆる手段でリロケーションを行おうとします。

この関数は次のようなコードと等価な振る舞いをします

if constexpr (is_trivially_relocatable_v<T>) {
  // Tがトリビアルリロケーション可能ならそうする
  return std::trivially_relocate(begin, end, new_location);
} else if (less{}(end, new_location) || less{}(new_location + begin - end, begin)) {
  // 未初期化アルゴリズムを使用して、ムーブ&破棄によってリロケーション(領域にオーバーラップがない場合
  std::uninitialized_move(begin, end, new_location);
  std::destroy(begin, end);

  return new_location;
} else if (std::less{}(begin, new_location)) {
  // ↓こんな感じの領域の重なり方をしている場合の処理
  // |begin ...       ... end|
  //                  |nee_location...        ...|

  // 要素ごとにムーブ&破棄によってリロケーション(後から
  while (T* dest = new_location + begin - end; dest != new_location) {
    ::new (--dest) T(std::move(*--end));
    std::destroy_at(end);
  }

  return dest;
} else {
  // ↓こんな感じの領域の重なり方をしている場合の処理
  //                |begin ...       ... end|
  // |nee_location...        ...|

  // 要素ごとにムーブ&破棄によってリロケーション(前から
  while (begin != end) {
    ::new (new_location++) T(std::move(*begin++));
    std::destroy_at(begin);
  }

  return new_location;
}

複雑な分岐はほとんど、リロケーション元と宛先の領域がオーバーラップしている場合にも正しく動作させるためのものです。

この関数は効率的なリロケーションのためにTのムーブコンストラクタが例外を投げないことを求めています。それを満たさない型での使用やイテレータ範囲によって同等のことを行うために、2つ目の機能であるstd::uninitialized_move_and_destroy()が用意されています。

namespace std::ranges {
  // イテレータ版
  template<forward_iterator I, sentinel_for<I> S1,
           nothrow-forward-iterator O, nothrow-sentinel-for <O> S2>
    requires constructible_from<iter_value_t<O>, iter_rvalue_reference_t<I>>
  uninitialized_move_and_destroy_result<I, O>
    uninitialized_move_and_destroy(I ifirst, S1 ilast, O ofirst, S2 olast);

  // range版
  template<forward_range IR, nothrow-forward-range OR>
    requires constructible_from<range_value_t<OR>, range_rvalue_reference_t<IR>>
  uninitialized_move_and_destroy_result<borrowed_iterator_t<IR>, borrowed_iterator_t<OR>>
    uninitialized_move_and_destroy(IR&& in_range, OR&& out_range); 
}

ここでは代表としてRangeアルゴリズムのものを抜粋しましたが、非Rangeのものや並行アルゴリズム_n付きのものも提案されています。

これらのアルゴリズムは未初期化領域[ofirst, olast)out_range)に対して[ifirst, ilast)in_range)の領域のオブジェクトを、ムーブ&破棄によってリロケーションするものです。

これも含めた未初期化メモリに対するアルゴリズム全体の指定として、例外がスローされた場合は出力領域は未初期化状態にリセットされます。また、既存の未初期化メモリに対するアルゴリズムと同様に、事前条件で入出力領域がオーバーラップしていないことを求めており、std::relocate()とは異なりオーバーラップ領域には使用できません。

P2971R1 Implication for C++

P2978R0 A New Approach For Compiling C++

ビルドシステムとコンパイラが相互にやり取りをするためのAPIの提案。

現在のC++コンパイルは、人間かビルドシステムがコンパイラの実行ファイルを呼び出すことで行われています。提案によれば、ビルドシステムがコンパイラの機能を実行ファイルではなく共有ライブラリ経由で使用するようにすることで25~40%のコンパイル速度向上が見込めるとのことです。

速度が向上する理由は次の2点です

  1. ビルドシステムはAPIを利用してコンパイラが読み込んだファイルを知ることができる。その情報を利用すれば、複数のファイルのコンパイルで同じキャッシュファイルを使用可能になる
  2. モジュールのビルドにおいては、本ビルドの前にモジュール間の依存関係を調べる必要がある。その役割は基本的にコンパイラが担っているが、APIを使用することでビルドシステムがその解決を行うことができ、依存関係スキャンを行う必要がなくなる

ビルドシステムがコンパイラの持つ情報を得ようとすると一々コンパイラを呼び出す必要があり、それはオーバーヘッドが大きいため現在は避けられているか時間がかかっています。API経由でコンパイラの個別機能を使用することでビルドシステムの任意のタイミングでその情報を得られるようになり、そのコストはおそらくコンパイラを呼び出すオーバーヘッドよりもかなり小さくなると思われます。

この提案ではWindows11上のMSVCを使用したSFLMおよびLLVMコンパイル時間の分析を行うことでその効果を見積もっており、25~40%のコンパイル速度向上が見込めるとしています。主に、翻訳単位それぞれでの依存関係スキャンをスキップできることと、コンパイル全体で読み取る必要のあるファイル数を削減すること、立ち上げるべきプロセス数の削減によって高速化されるようです。

提案しているAPIは次のようなものです。これはおそらくC++標準ではなくSG15(Tooling Study Group)で議論中のEcosystem International Standardに対して適用されるものだと思われます。

namespace buildsystem {

  struct string {
    const char *ptr;
    unsigned long size;
  };

  // Those char pointers that are pointing to the path are platform dependent i.e. whcar_t* in-case of Windows
  struct compile_output {
    // completed == false : コンパイラ状態へのポインタ
    // それ以外の場合 : nullptr
    void *compiler_state;

    // completed == true : コンパイラの出力メッセージ
    // それ以外の場合 : nullptr
    string stdout;
    string stderr;

    // completed == true : コンパイラが待機しているモジュール名orヘッダユニットのビルド済モジュールorヘッダユニット名(すなわち依存関係の情報)
    // それ以外の場合 : エクスポートされたモジュールがある場合その論理名
    string wait_string_or_logical_name;

    // インクルードされているヘッダの数と名前(パス)
    unsigned long header_includes_count;
    string *header_includes;

    // コンパイル成果物のファイル名、パス、数
    string *output_files;
    string *output_files_paths;
    unsigned short output_files_count;

    // コンパイラがモジュールの入力を待っている場合true
    bool waiting_on_module;

    // コンパイルが完了していれば(エラーでも)true
    bool completed;

    // completed == trueの場合、コンパイルエラーが起きていればtrue、成功していればfalse
    bool error_occurred;
  };

  compile_output new_compile(string compile_command, string (*get_file_contents)(string file_path));
  compile_output resume_compile(void *compiler_state, string bmi_file);
  string get_object_file(string bmi_file);

} // namespace buildsystem

このようなAPIコンパイラ共有ライブラリが提供し、ビルドシステムはこのAPIを介してコンパイラコンパイルを制御します。次のように利用することを意図しています。

最初に、new_compile()コンパイラオプションを引数として渡してコンパイルを開始します(このオプションには依存関係の情報は含まれていません)。その後、コンパイル実行中にモジュールのインポートに遭遇した場合、戻り値のwait_string_or_logical_nameにその依存関係の名前を指定しwaiting_on_moduletrueに設定してこの関数はリターンします。また、コンパイル実行中にヘッダユニットのインポートに遭遇した場合、wait_string_or_logical_nameにそのヘッダユニットのパスを指定しwaiting_on_modulefalseに設定してリターンします。

ビルドシステムは一連のビルドの間で得られたcompiler_stateを保存しておき、必要なファイルが既にビルドされているか、ビルドする必要があるか、ビルド中であるかを管理します。あるビルドが待機しているファイルが利用可能になると、そのcompiler_stateと新たに利用可能になったモジュール(ヘッダユニット)のBMIを渡してresume_compile()を呼び出すことでコンパイルを再開します。

ビルドが完了すると、コンパイラcompile_outputオブジェクトのcompletedtrueに設定し、output_filesoutput_files_pathsに成果物の名前とパスを設定してリターンします。

あるcompiler_stateに対して依存関係が解消されコンパイルが完了するまでresume_compile()は繰り返し呼び出され、それらのビルドは複数並行で行われます。

このAPIの実行モデルでは、コンパイラは依存関係スキャンを行わず依存関係(モジュール関連)にぶつかるとそこでコンパイルを一時停止しビルドシステムに制御を返します。ビルドシステムはプロジェクト内の全てのモジュール/ヘッダユニットについて並行的にnew_compile()を走らせ、その成果物をもってresume_compile()を呼び出すことで依存関係を自動的に解決しながらビルドを完了します。

この提案は、API定義を通してそのようなモジュールのビルドモデルを定義しようとしてもいます。

P2979R0 The Need for Design Policies in WG21

LEWG/EWGでの機能設計のための設計ポリシーを整備する提案。

WG21に参加する人々は、C++標準を可能な限り最高のものにする目標を共有していますが、誰もが同じ原則に基づいて設計を選択するわけではありません。そのため、関連するすべての原則が先に合意されない限り設計に関する議論を行き詰まってしまう可能性があります。一見の一致を見た場合でも、投票の後で1つ以上の重要な設計原則について十分な情報が共有されていなかったことが後で反目する場合があります。

この提案では、原則に基づいた設計を採用し、WG21の議論プロセスにおいて、現在のように初めに関連する問題について議論した後に解決策の一つに投票するのではなく、原則に基づいた設計のプロトコルに従ってまず関連する設計原則を明確にした上で優先順位を付けるようにすることを提案しています。

また、そのような設計原則に基づいた決定を文書化して共有することで、別の議論における同様の決定の際に再検討を避け将来の議論を合理化できます。

現在では、そのような設計原則やそれに基づく過去の決定などの情報は属人化しており、その人がたまたまその議論において欠けていることで以前の議論や決定が継承されず、設計に矛盾が生じることが少なからずあったようです。提案では、クラスのデフォルト特殊メンバ関数に対するnoexcept指定のバージョン間での振る舞いの不一致や、LEWGにおけるラコスルールの軽視の例をあげています。

この提案はそのようなポリシーの概要を説明するもので、具体的な提案は個別の2つの提案で行おうとしています

  1. 議論のある設計の決定を仲裁するための原則に基づいたアプローチ (P3004)
  2. 確立された設計ポリシーを文書化してアクセスするための体系的なメカニズム(P3005)

なお、これらの提案はまだ公開されていません。

P2980R0 A motivation, scope, and plan for a physical quantities and units library

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

この提案では、物理量と単位を扱うライブラリを導入する理由やモチベーションを説明し、設計目標や現時点での例を示すものです。

モチベーションとしては次のようなものが挙げられています

  • 安全性
    • 単位を間違えた計算がコンパイルエラーとなる
    • 物理量とその単位を扱うライブラリは多くの機械の制御コードの安全性を向上させる
  • 語彙型となること
    • ベンダーがそれぞれ独自実装をしなくてよくなる
  • 認証されたライブラリ
    • MISRA等の安全性基準に従ったソフトウェア開発においてはOSSを使用できない場合が多い
    • 標準ライブラリとして提供されていることでより安全なコードを記述できるようになる
  • 独自実装は複雑で難しい
  • 拡張性
    • 非SI単位などに対応するために単位の追加を容易にする
  • 幅広いドメインで使用可能(されている)

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

// mp-unitsというライブラリで試験実装が行われている
#include <mp-units/systems/si/si.h>

using namespace mp_units;
using namespace mp_units::si::unit_symbols;

// 単純な数値計算
static_assert(10 * km / 2 == 5 * km);

// 単位変換
static_assert(1 * h == 3600 * s);
static_assert(1 * km + 1 * m == 1001 * m);

// 組立量
static_assert(1 * km / (1 * s) == 1000 * m / s);
static_assert(2 * km / h * (2 * h) == 4 * km);
static_assert(2 * km / (2 * km / h) == 1 * h);

static_assert(2 * m * (3 * m) == 6 * m2);

static_assert(10 * km / (5 * km) == 2 * one);

static_assert(1000 / (1 * s) == 1 * kHz);
#include <mp-units/format.h>
#include <mp-units/ostream.h>
#include <mp-units/systems/international/international.h>
#include <mp-units/systems/isq/isq.h>
#include <mp-units/systems/si/si.h>
#include <iostream>

using namespace mp_units;

constexpr QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto d,
                                                QuantityOf<isq::time> auto t)
{
  return d / t;
}

int main()
{
  using namespace mp_units::si::unit_symbols;
  using namespace mp_units::international::unit_symbols;

  constexpr quantity v1 = 110 * km / h;
  constexpr quantity v2 = 70 * mph;
  constexpr quantity v3 = avg_speed(220. * km, 2 * h);
  constexpr quantity v4 = avg_speed(isq::distance(140. * mi), 2 * isq::duration[h]);
  constexpr quantity v5 = v3.in(m / s);
  constexpr quantity v6 = value_cast<m / s>(v4);
  constexpr quantity v7 = value_cast<int>(v6);

  std::cout << v1 << '\n';                // 110 km/h
  std::cout << v2 << '\n';                // 70 mi/h
  std::println("{}", v3);                 // 110 km/h
  std::println("{:*^14}", v4);            // ***70 mi/h****
  std::println("{:%Q in %q}", v5);        // 30.5556 in m/s
  std::println("{0:%Q} in {0:%q}", v6);   // 31.2928 in m/s
  std::println("{:%Q}", v7);              // 31
}

この提案及び将来的に標準に導入しようとしているライブラリ機能は、mp-unitsというライブラリで試験実装が進められています。

提案ではこの機能をC++29に導入することを目指しており、そのための機能ごとのカテゴライズやその中での優先順位付けを行っています。掲載されている予定表によれば、C++26サイクル中にLEWGのレビューを完了し、C++29サイクル中でLWGの承認を取り付け、標準に導入するような予定を組んでいるようです(詳細な予定表が提案にはあります)。ただし、まだ確度の高いものではなく、計画をLEWGに承認してもらってスムーズに進めようとしています。

P2981R0 Improving our safety with a physical quantities and units library

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

この文書では主に、P2980のライブラリ(以下単位ライブラリと呼びます)がC++コードの安全性向上に役立つことを解説しており、単位ライブラリの必要性や使用可能な産業領域等を説き、単位付きの量の取り扱いを間違ったことで起きた事故を紹介し、現在よく見られる単位付きの量を使用している危ういコード例について紹介したうえで、単位ライブラリがそれらの問題をどう解決できるかを示しています。

単位ライブラリによる安全性は主に、算術演算コードにおいてその値の単位をコンパイル時にチェックすることで間違った計算を防止するものです。それをベースに、ダングリング参照に対する配慮や、同じ単位を持つ異なる種類の量のサポートなどの安全性への配慮が行われています。

P2982R0 std::quantity as a numeric type

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

あまりに長いので詳しくは見ませんが

  • 量体系
    • 同じ次元を持つ量でもその意味が異なる場合があり、それを考慮しなければならない
    • 単位系だけではなく量体系もモデリングする必要がある
    • 国際量体系(International System of Quantities)に基づいてそれを行う
  • 単位系
    • 単位の種類・組み合わせ・スケール・単位を指す記号などについて
    • 単位は量の種類に関連づけられ、特定の量に対して制約される
  • 単位・次元・量を表す型の操作
    • 単位・次元・量を表す型に対して定義された演算や変換などについて
    • 全ての単位・次元・量を表す型はそれぞれ一意な型を持つ
    • 演算の結果は、量と単位の型の階層や関係性に基づいて決定される
  • 量(std::quantity
    • quantityの概念や表現、構築や演算などについて
    • quantityは数値と単位のペアであり、単位の変換や算術演算が可能
    • quantityはこのライブラリの中心クラス型であり、std::chrono::durationの一般化ではあるが直接の互換性はない
  • ジェネリックインターフェース
    • quantityをテンプレートで受け取る関数や変数を定義する方法について
    • 関数のインターフェースや戻り値の受け取り、クラスメンバへの保存時などには単位の選択が必要となる
    • quantityを制約するためにコンセプトを使用できる
  • 数値を表現する型のカスタマイズ
    • quantityの数値表現型として任意の型を使用する方法について
    • quantityの数値表現型はその値の範囲や精度に影響する
  • アフィン空間
    • アフィン空間の概念とこのライブラリによってそれを表現する方法について
    • アフィン空間の中心の2つの概念(点とベクトル)はそれぞれ、PointOriginコンセプト+quantity_point型、quantity型によって表現される

のようなことが書かれています。

P2984R0 Reconsider Redeclaring static constexpr Data Members

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

C++11でstatic constexprメンバ変数が宣言できるようになりましたが、その定義が必要となる場合は通常の静的メンバ変数と同様にクラス外に定義を置く必要がありました。これはC++17のインライン変数の導入時にstatic constexprメンバ変数は暗黙inlineとされるようになったことでクラス外での定義が不要になりました。それとともに、クラス外の定義は不要な再宣言となり非推奨とされました。

struct A {
  static constexpr int n = 5;  // 定義(C++14までは宣言)
};

constexpr int A::n;            // 冗長な再宣言(C++14までは定義)

__cpp_inline_variables機能テストマクロを用いてクラス外定義の存在を切り替えることでこのようなコードはバージョン間でポータブルにすることができます。とはいえ、C++14以前にこの書き方をされている既存のコードはおそらくたくさんあり、必ずしも全てがそのような対応を行えるわけではないかもしれません。

現在の主要なコンパイラ(フロントエンド)は、C++17以降のモードにおいてもこれらの冗長な再宣言(以前の定義)に対して非推奨である旨の警告を発しません。

C++26に向けて現在非推奨とされているものを整理し可能なら削除しようとする取り組み(P2863)のEWGにおけるレビューにおいてこの問題も議論され、そこでは現状維持(非推奨のまま)とする方向性のようですが、さらにこれの非推奨化を解除する方向性について提案が望まれたようで、この提案はそれを受けてのものです。

この提案では、その歴史的経緯や現状を説明するとともに、現状維持・非推奨解除・削除のいずれかを選択することを促しています。

その後のEWGのレビューでは、C++26サイクルでは現状維持とすることになったようです。

P2985R0 A type trait for detecting virtual base classes

あるクラスが他のクラスの仮想基底になっているかを判定する型特性の提案。

このような判定はクラス型のポインタの変換を行う場所において必要となり、特にスマートポインタの変換コンストラクタにおいて必要となります。

ポインタderived* dbase*に変換するには、basederivedの仮想基底クラスであるかどうかによって実装が分岐します

  1. basederivedの仮想基底クラスではない場合
    • dnullptrかをチェックする
      • nullptrではない場合、dコンパイル時にわかっている定数オフセットを加算する
      • nullptrの場合、nullptrを返す
  2. basederivedの仮想基底クラスである場合
    • dnullptrかをチェックする
      • nullptrではない場合、dの参照する領域の仮想テーブルを検査して適切なポインタ値を得る
        • 実際の仕組みはABIに依存する

この処理はユーザーが実装するものではなく、コンパイラが挿入する変換処理です。

2でdnullptrではない場合に問題なのは、仮想テーブルにアクセスしてポインタ値を得るのが1の場合に比べてコストがかかることと、dの参照先が既に破棄されている場合に未定義動作となることです。

weak_ptrの変換コンストラクタを実装することを例として考えてみます。

weak_ptrのクラス構造は簡単には次のようになっています

template <typename T>
class weak_ptr {
  control_block *m_cb;
  T *m_data;
};

この場合にweak_ptr<Y*>からweak_ptr<T*>へムーブしつつ変換するコンストラクタを考えると、単純には次のようになります

template <typename Y>
  requires std::is_convertible_v<Y*, T*>
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(std::exchange(other.m_cb, nullptr)),
    m_data(std::exchange(other.m_data, nullptr)) // <-- danger
  {}

この実装には

  • TYの仮想基底であり
  • other.m_dataの参照するオブジェクトが既に破棄されている場合

に前述の理由により、dの領域の仮想テーブルにアクセスしようとして未定義動作となります。

正しい実装は、きちんとotherの領域が有効であるかを調べる必要があります

template <typename Y> 
  requires std::is_convertible_v<Y*, T*>
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(other.m_cb),
    m_data(other.lock().get())
  {
     other.m_cb = nullptr; other.m_data = nullptr;
  }

ただ、これは今度はTYの仮想基底ではないほとんどのケースで非効率となります。

この実装を正しくかつ効率的に行うにはTYの仮想基底であるかどうかによって実装を分岐する必要があります。

// 非仮想基底の場合の単純な実装
template <typename Y> 
  requires (std::is_convertible_v<Y*, T*> && !std::is_virtual_base_of_v<T, Y>)
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(std::exchange(other.m_cb, nullptr)),
    m_data(std::exchange(other.m_data, nullptr)) 
    {}
    
// 仮想基底の場合の安全だが重い実装
template <typename Y> 
  requires (std::is_convertible_v<Y*, T*> && std::is_virtual_base_of_v<T, Y>)
weak_ptr(weak_ptr<Y> &&other)
  : m_cb(other.m_cb),
    m_data(other.lock().get())
  {
     other.m_cb = nullptr; other.m_data = nullptr;
  }

同様の問題はobserver_ptrというスマートポインタ(標準にはない)の変換コンストラクタにおいても発生し得ます。

この提案は、主にスマートポインタの安全な実装のために、この例のis_virtual_base_of型特性を標準ライブラリに導入しようとするものです。

namespace std {

  // 追加するis_virtual_base_ofの宣言例
  template<class Base, class Derived>
  struct is_virtual_base_of;

  template<class Base, class Derived>
  constexpr bool is_virtual_base_of_v = is_virtual_base_of<Base, Derived>::value;
}

P2986R0 Generic Function Pointer

定数式でも使用可能な関数ポインタ専用の型消去型を追加する提案。

現在利用可能な関数ポインタの型消去機能(std::function等)は全て、定数式で使用可能ではありません。これはstd::function等の実装の都合上、構築も呼び出しも定数式で行えないためです。

例えば、C++26で導入される予定のstd::function_refのCallableを保存するストレージの実装を見てみると

struct _function_ref_base {
  union storage {
    void *p_ = nullptr;
    void const *cp_;
    void (*fp_)();

    constexpr storage() noexcept = default;

    // 関数オブジェクトを受け取るコンストラクタ
    template<class T>
      requires std::is_object_v<T>
    constexpr explicit storage(T *p) noexcept
      : p_(p)
    {}

    // 関数オブジェクトをconstで受け取るコンストラクタ
    template<class T>
      requires std::is_object_v<T>
    constexpr explicit storage(T const *p) noexcept
      : cp_(p)
    {}

    // 関数ポインタを受け取るコンストラクタ
    template<class T>
      requires std::is_function_v<T>
    constexpr explicit storage(T *p) noexcept
      : fp_(reinterpret_cast<decltype(fp_)>(p)) // 👈 reinterpret_castは定数式で実行不可
    {}
  };

  template<class T>
  constexpr static auto get(storage obj) {
    if constexpr (std::is_const_v<T>) {
      // C++26ではvoid*からのキャストは定数式で可能(本来の型が合っていれば)
      return static_cast<T*>(obj.cp_);
    } else if constexpr (std::is_object_v<T>) {
      // C++26ではvoid*からのキャストは定数式で可能(本来の型が合っていれば)
      return static_cast<T*>(obj.p_);
    } else {
      return reinterpret_cast<T*>(obj.fp_); // 👈 reinterpret_castは定数式で実行不可
    }
  }
};

std::function_refは構築後に保持するCallableを切り替える必要がないためそのストレージの実装はかなり単純になります。そのため、constexpr対応も可能なように思えます。

しかし実際には、関数ポインタがvoid*に変換できないため関数ポインタの保存においては特別扱いが必要となります。すると、void(*)()というポインタ型で型消去することになりますが、関数ポインタのこのキャストにはreinterpret_castが必要となり、それは定数式で実行できません。これに引っ張られる形で、std::function_refは構築も呼び出しも定数式では行えません。

C++26時点でも、これを解決するソリューションは存在していません。この提案はこの解決のために、定数式で使用可能な関数ポインタ型専用の型消去ポインタ型を用意しようとするものです。

提案されているのはstd::function_ptr_tという名前のものです。これは言語組み込みの型で、この型の値は任意の関数ポインタ型を代入することができます。そして、この型の操作は全て定数式で行うことができます。

constexpr int f() {
  return 42;
}

int main() {
  // nullptrを代入可能
  constexpr std::function_ptr_t fp1 = nullptr;

  // 任意の関数ポインタをそのまま受け入れられる
  constexpr std::function_ptr_t fp2 = f;

  // static_castを用いて関数ポインタを復帰できる
  // これは型が合っていれば定数式で行える
  constexpr auto p_f = static_cast<int(*)()>(fp2);
  static_assert( p_f == f );
  static_assert( p_f() == 42 );

  // 直接の呼び出しやデリファレンスはできない
  fp_f(); // ng
  *fp_f;  // ng
}

例えばstd::function_refにおいては、このstd::function_ptr_tを使用して関数ポインタを保存する部分を書き換えることでreinterpret_castを使用する必要がなくなり、構築も呼び出しも定数式で行えます。

EWGIによるレビューでは、この問題はキャストの仕様調整によって解決することが望ましいという方向性のようで、この提案の方向性は支持を得られていないようです。

P2988R0 std::optional<T&>

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

現在、std::optional<T>Tには参照型を指定することができません。当初の提案では検討されていましたが、ポインタとの意味的な差異が不明瞭であることや非参照のTに対するメインのstd::optional<T>の導入を優先するために途中で提案から削除されました。

std::optional<T&>は単純には無効値を取れる参照です。それは一見すると確かにポインタと意味的な違いが無いようにも思えます。

std::optionalC++17に導入されてからおよそ7年の現在において、コンテナ等から特定の要素を見つけたいがその所有権を引き取りたくはないような場合など、std::optionalに参照を保持させたい場合は稀によくあります。

要素を検索する関数で戻り値型として生ポインタを使用する場合との比較

ポインタ この提案
Cat* cat = find_cat("Fido");
if (cat!=nullptr) { return doit(*cat); }
std::optional<Cat&> cat = find_cat("Fido");
return cat.and_then(doit);

要素を検索する関数で戻り値型としてスマートポインタを使用する場合との比較

ポインタ この提案
// use a smart pointer instead of optional<T&>
std::shared_ptr<Cat> cat = find_cat("Fido");
if (cat != nullptr) {...
std::optional<Cat&> cat = find_cat("Fido");
cat.and_then([](Cat& thecat){...

このような用途にスマートポインタを使用する場合はobserver_ptr<T>のようなものを使用するべきですが、それはまだ標準に存在していません。また、スマートポインタを使用してもnullptrチェックの必要性からは逃れられません。

要素を検索する関数で戻り値型としてイテレータを使用する場合との比較

ポインタ この提案
std::map<std::string, Cat>::iterator cat
    = find_cat("Fido");
if (cat != theunderlyingmap.end()){ ...
std::optional<Cat&> cat = find_cat("Fido");
cat.and_then([](Cat& thecat){...

このような用途にイテレータを使用するのは標準アルゴリズムでも見られるパターンですが、イテレータ型をとおして元のコンテナ型が漏洩し、さらには見つかったかどうかをチェックするのに元のコンテナ(の終端イテレータ)が必要になります。

生ポインタに比べたstd::optional<T&>には次のような利点があります

  • 所有権を引き取っていないことが明確
  • アドレス演算ができない
  • 変なポインタキャストできない
  • それを引数に取る関数へ渡す際に素直に渡せる(&等が必要ない)
  • optionalの便利なメンバ関数群を使用できる(nullチェックを明示的に行う必要が無い)

特に最後の利点はC++23で追加されたモナディックインターフェースによるところが大きく、それがこの提案を大きく後押ししてもいます。

この提案はこれらの利点や利便性から、std::optional<T&>を可能にすることを提案するものです。

std::optional<T&>の実装としてはポインタの薄いラッパとして実装することで無効値のための領域を節約するとともに、通常のstd::optionalのような複雑な実装を回避できることが知られており、この提案ではstd::optional<T>の部分特殊化としてstd::optional<T&>を追加することでそのような実装を取るようにすることを提案しています。

また、std::optional<T&>に対する代入のセマンティクスは参照の再バインドであり、参照先に対する代入ではありません。そのため、代入時には変換不可能な代入が拒否されます。

import std;

int main() {
  int n = 10;
  
  std::optional<int&> ro = n; // nの参照を保持
  ro = 15;  // ng、int&&はint&で束縛できない

  int m = 20;

  ro = m;   // ok、roはmの参照を保持する
}

これはstd::optional<T&>の再代入を内部参照に対してスルーするようにすると利点よりもむしろバグの発生源となるためです(詳しくはP1683R0を参照)。

さらに、この提案ではstd::optional<T&>に対するconstは浅いconstとなるようにすることを提案しています。これはポインタやstd::spanと同じであり、参照先に対するconstが必要な場合はstd::optional<const T&>のようにする必要があります。

import std;

int main() {
  int n = 10;
  int m = 20;
  
  const std::optional<int&> ro = n; // nの参照を保持

  ro = m;   // ng、roはconst
  *ro = 15; // ok、nの値が変更される

  std::optional<const int&> cro = n; // nの参照を保持

  *ro = 25; // ng、const int&に代入できない
  ro = m;   // ok、roはmの参照を保持する
}

P2989R0 A Simple Approach to Universal Template Parameters

より限定されたユニバーサルテンプレートパラメータの提案。

ユニバーサルテンプレートパラメータは型・非型・テンプレートを統一的に受けることのできるテンプレートパラメータのことで、P1985で提案されました。P1985の提案は言語のあらゆるコンテキストでユニバーサルテンプレートパラメータを使用可能にしようとするもので、仕様と実装に追加する複雑さが大きくなりメリットが相対的に小さくなっていました。

この提案はそれを改善しつつほぼ同じ機能を追加しようとするもので、のP1985との違いは、ユニバーサルテンプレートパラメータの構文としてuniversal templateを選択し、その導入を純粋にテンプレートパラメータとしての使用のみに限定したことです。

  • ユニバーサルテンプレートパラメータは関数・クラス・変数テンプレートのテンプレートヘッド(template<...>の中)でのみ宣言できる
  • ユニバサールテンプレートパラメータ名はテンプレート引数としてのみ使用できる
  • ユニバーサルテンプレートパラメータはパックを取れる
  • ユニバーサルテンプレートパラメータのデフォルト引数は設定できない
  • ユニバーサルテンプレートパラメータを処理するには、別のテンプレートに転送するか、部分特殊化によって行う

提案しているユニバーサルテンプレートパラメータ(UTP)によるライブラリ機能とその実装例

// UTPが型名かを判定
template <universal template T>
inline constexpr bool is_typename_v = false;

template <typename U>
inline constexpr bool is_typename_v<U> = true;

// UTPがNTTPかを判定
template <universal template U>
inline constexpr bool is_nttp_v = false;

template <auto U>
inline constexpr bool is_nttp_v<U> = true;

// UTPがテンプレートかを判定
template <universal template U>
inline constexpr bool is_template_v = false;

template <template<universal template....> universal template U>
constexpr bool is_template_v<U> = true;

// UTPが型テンプレートかを判定
template <universal template U>
inline constexpr bool is_type_template_v = false;

template <template<universal template....> typename U>
inline constexpr bool is_type_template_v<U> = true;

// UTPが変数テンプレートかを判定
template <universal template U>
inline constexpr bool is_var_template_v = false;

template <template<universal template....> auto U>
inline constexpr bool is_var_template_v<U> = true;

// UTPがコンセプトかを判定
template <universal template U>
inline constexpr bool is_concept_v = false;

template <template<universal template....> concept U>
inline constexpr bool is_concept_v<U> = true;

これを利用したis_specialization_ofの実装例

template<universal template T, universal template Primary>
  requires is_var_template_v<Primary> || requires is_type_template_v<Primary>
inline constexpr bool is_specialization_of_v = false;

// 型についてのテンプレート特殊化を検出
template<
  template<universal template...> typename Primary,
  universal template... Args
>
inline constexpr bool is_specialization_of_v<Primary<Args...>, Primary> = true;

// NTTPについてのテンプレート特殊化を検出
template<
  template<universal template...> auto Primary,
  universal template... Args
>
inline constexpr bool is_specialization_of_v<Primary<Args...>, Primary> = true;

P2098R1で提案されていたもの(汎用性が低いとしてリジェクト)とは異なり、この実装の場合はクラステンプレートと変数テンプレートの特殊化をチェックすることができ、さらに特殊化しているものが型でない場合についてもチェックすることができます。

この提案のユニバーサルテンプレートパラメータは他のテンプレートに渡す以外は何もできないため、このように最終的には部分特殊化によって処理することになるでしょう。そのため、P1985R3の例のいくつかは実装に工夫が必要となります。

// メタ関数FにArgsを適用したい、P1985の実装
template<universal template F, universal template... Args>
using apply = F<Args...>; // これはできない、エイリアスでUTPを使用できない

// メタ関数FにArgsを適用したい、この提案における実装
template<template <universal template...> typename F, universal template... Args>
struct apply {
  using type = F<Args...>;  // ここではFは型テンプレートパラメータなので普通に使える
};

ただし、この提案ではコンセプトのテンプレートパラメータでUTPを使用可能にすることは提案していないため(コンセプトは部分特殊化できないため)、コンセプトでは使用できません。

// これはできない・・・
template <typename R, template<universal template....> concept C>
concept range_of =
  ranges::input_range<R> &&
  C<remove_cvref_t<ranges::range_reference_t<R>>>;

P2990R0 C++ Modules Roadmap

モジュールのエコシステムのために必要な作業についての提案。

この提案は、モジュールを実際に利用可能にするために必要なツールのサポートのために現在欠けているものを特定し、SG15(tooling study group)においてそのために必要な作業とその優先度を提案するものです。

提案では、現状のエコシステム(コンパイラやビルドシステム、静的解析ツールなど)におけるモジュールのサポート状況を紹介し、それらツールがモジュールを相互に運用するために欠けているものについて説明したうえで、それを解消するために必要な作業について優先度を付けて提示しています。

提案されているのは次のようなロードマップです

  1. 単一のプロジェクトにおいてモジュールを利用可能にする
    1. モジュールインターフェースの複数回のコンパイル
      • 1つのモジュールインターフェースが異なる翻訳単位において異なるビルドオプションを用いてコンパイルされることがよくある
      • あるモジュールインターフェースが何回コンパイルされるのかを特定することに関して、ビルドシステムとコンパイラ間の相互運用性の疑問に答える必要がある
    2. 静的解析ツールのビルドシステム外部におけるサポート
      • IDEにおけるコード補完など、静的解析ツールはビルドシステムと深く統合することなく動作することが求められる
      • ビルドシステムはそれらツールが動作するのに十分な情報を提供する必要がある
  2. 事前ビルドライブラリにおけるモジュールの利用
    1. 事前ビルドライブラリでのモジュールのメタデータ
    2. 標準ライブラリモジュールのメタデータ
  3. インポート可能なヘッダ
    1. インポート可能ヘッダの識別
      • ビルドシステムやパッケージマネージャーがプロジェクト内でインポート可能なヘッダを見つける時の問題について、ツールの実装者との協力が必要
    2. 依存関係スキャンとプリプロセッサ状態
      • 依存関係スキャン実行時のインポートメカニズムのエミュレーションの要件について未解決の問題が残っている

この提案は、SG15における行動喚起を促す事と同時に、ツール開発者がモジュールサポートのための投資を行っても安全であることを示す目的があります。

SG15ではこのロードマップに沿った作業を行っていくことに合意が取れており、Githubのcplusplus/modules-ecosystem-trで作業を行っていくことにしたようです。

P2991R0 Stop Forcing std::move to Pessimize

std::move()がNRVOを阻害しないようにする提案。

C++の学習においては、新しいオブジェクトを構築するときにそのオブジェクトが別の左辺値オブジェクトから構築され、構築元のオブジェクトが以降使用されないような場合にstd::move()を使用して新しいオブジェクトを構築するように教えられます。しかし、return文においてローカル変数を返そうとするときには逆にstd::move()を使用すべきではないとも教わります。その理由はreturn文におけるstd::move()がNRVOを妨げるためですが、このことは一貫しておらず、return文における例外の理由についても非常に複雑なものがあります。

std::vector<std::string> readStrings(int numStrings) {
  std::vector<std::string> result;
  std::string string;

  while (numStrings--) {
    std::cin >> string;
    // ここではmove()がいる
    result.push_back(std::move(string));
  }

  // ここではmove()がいらない
  return result;
}

例えばこのコードにおいては、ローカルのstring及びresultは他のオブジェクトの初期化に使われた後で再び参照されることがなくコピーが重いクラスであるため、他のオブジェクトの初期化時にはムーブすることが適切です。しかし、プログラマが明示的にstd::move()によってムーブする必要があるのはループ中のstringに対してのみです。

全てのC++バージョンにおいてNRVOは許可されており(必須ではない)、NRVOが行われる場合ムーブすらも省略されresultは最初から呼び出し側の変数であったかのように使用されます。NRVOが行われない場合でも、暗黙ムーブによってreturn文における変数名を指定する式は値カテゴリがxvalueとなるため、resultは自動でムーブされます。

return std::move(result)と書くことはむしろ有害であり、NRVOを確実に行われなくします。これはNRVOの対象となるものがローカル変数の変数名を指定する式のみであるためであり、std::move()を追加すると変数名を指定する式ではなくなるためNRVOの対象でもなくなるためです。

このことによって、C++初学者にはstd::move()を使用する時のルールと、std::move()を使用してはならない時のルールの2つを教えなければならなくなります。しかも、後者のルールに違反するとパフォーマンス上のペナルティとして帰ってきます。

この提案は、return文におけるstd::move()を特別扱いすることでstd::move()がNRVOを妨げることがないようにして、std::move()に関してはstd::move()を使用する時のルールのみを教えれば済むようにしようとするものです。

そのために現在の規定で、return文のオペランドとしてNRVO eligibleとされている式Eについて、次の形式に当てはまる場合の式もNRVO eligibleであるというように判定を行うことを提案しています

  • Fが名前解決の後でstd::move()になる場合の式F(E)
  • Tが戻り値型への右辺値参照となる場合の式static_cast<T>(E)
  • Tが戻り値型への右辺値参照となる場合の式(T)E
  • Tが戻り値型への右辺値参照となる場合の式T(E)

さらに、コンパイラが以前の言語バージョンでもこの動作を実行できるように、このことを以前の言語バージョンに対するDRとすることも提案しています。

P2992R0 Attribute [[discard]] and attributes on expressions

式の結果を破棄することを明示する[[discard]]属性の提案。

[[nodiscard]]属性を付加された関数の戻り値を消費しない場合、警告が発せられます。これは基本的にはとても便利なのですが、場合によっては[[nodiscard]]な関数の戻り値を使用せずに破棄したい場合もあります。例えば

  • テスト
    • スモークテストにおいて広い契約を持つ関数がクラッシュしないことを確かめたい場合など
    • この場合に戻り値に興味はなく、警告は必要ない
  • 部分的なドメイン
    • 例えば、エラーコードを返す[[nodiscard]]な関数がある特定の引数を渡された場合に決して失敗しないことがわかっている場合にユーザーがそれを確認して呼び出してる時
    • この場合、戻り値を消費する必要はなく、安全に破棄できる
  • 古い関数
    • 当初は成否を戻り値で返していた関数が後のバージョンで決して失敗しないようになったものの、API/ABI保護のために戻り値型を維持し続けている場合
    • この場合、戻り値は意味がないため安全に破棄できる

などの場合があります。

そのような場合に警告を抑制しつつ戻り値を破棄するのに使用可能な方法は主に次の2つがあります

  1. voidキャスト
  2. std::ignore
[[nodiscard]]
int f();

int main() {
  f();  // 警告される

  // 1. voidキャスト
  (void)f();  // 警告されない
  void(f());  // 警告されない

  // 2. ignore
  std::ignore = f();  // 警告されない
}

しかしこれらの方法には欠点があります

  1. voidキャスト
    • キャスト式の濫用であり、警告を抑制するために言語の難解なルールを使用しているだけ
    • 初心者への教育が困難
    • コードベースで検索(grepなど)できない
    • voidキャストを[[maybe_unused]]として使用している場合がある
    • 破棄の根拠はコメントとしてのみ提供できるため、コードベースにそのような根拠を義務付けるルールを強制するのが困難
  2. std::ignore
    • 右辺がvoid式の場合にコンパイルエラーになるため、ジェネリックコードで使用しづらい
    • C互換性がない
    • std::ignoreはライブラリソリューションであり、言語の問題の解決には言語によるソリューションの方が適している
    • voidキャストと比べて冗長
    • 破棄の根拠はコメントとしてのみ提供できる

この提案は、これらの欠点を解決する言語機能による戻り値の明示的破棄のソリューションとして、[[discard]]属性を提案するものです。

[[nodiscard]]
int f();

int main() {
  [[discard]] f();  // 警告されない
  [[discard("just testing")]] f();  // 警告されない
}

これには次のような利点があります

  • [[discard]]は使用法と構文において[[nodiscard]]と対称になっている
    • [[nodiscard]]は呼び出し元において無視して欲しくない関数/型宣言に置かれ、[[discard]]は呼び出し側において結果を明示的に破棄したい式に置かれる
  • 理由を書いておくことができる
    • これはコンパイラからは使用されないが、ユーザーや周辺ツールにとって有用となる
  • void式でも使用可能
  • 警告抑制のために標準ライブラリのものを持って来なくてもいい
  • Cとの互換性を図ることができる
  • voidキャストと比べて適度に冗長

この提案ではこの属性を式に対する属性指定として提案しています。現在のC++では式に対する属性指定を行うことができず、現在の文法も文に対する属性指定は可能でも式に対する属性指定には問題があります。

現在の式(expression)の文法定義は次のようになっています

expression:
    assignment-expression
    expression , assignment-expression

ここに属性指定を単純に追加すると次のようになるでしょう

expression:
    attribute-specifier-seq(opt) assignment-expression
    expression , assignment-expression

しかし、これは既存の文(statement)の文法と衝突します

statement:
    attribute-specifier-seq(opt) expression-statement

expression-statement:
    expression(opt) ;

従って、現在の文法のもとでは提案している式に対する属性は実際には文に対するものになっています。

[[discard]] f();  // 文に対する属性指定

ただし、このような単純な関数呼び出し式のみを含む文に対する[[discard]]属性の適用はこの属性の最も一般的な使い方であり、式文(expression-statement)に対して属性適用が行われていればこの提案の目的には十分です。

その場合に問題となるのは、組み込みカンマ演算子を使用した場合です。

[[discard]] a(), b(), c();  // 全ての関数の戻り値破棄警告が抑制されてしまう

個別の式に対して属性指定が必要となるのはこのように組み込みカンマ演算子を使用した場合のみであり、これを追求するのは完全性を追求する二次的な目標ではあります。

この提案ではそれでもあえて式に対する属性指定を提案しており、そのアプローチとして2つのものを提案しています。

1つ目のアプローチは式の右側に属性を指定するものです。

expression:
    assignment-expression attribute-specifier-seq(opt)
    expression , assignment-expression
[[discard]] f();  // 警告されない(文に対する属性)
f() [[discard]];  // 警告されない(式に対する属性)

[[discard]] a(), b();  // 警告されない(文に対する属性)
a(), b() [[discard]];  // 警告されない(式全体に対する属性)
a() [[discard]], b();  // a()は警告されないが、b()は警告されうる

int x = (a() [[discard]], b()); // 警告されない、a()は抑制され、b()は破棄されていない

struct S {
  S(int i) 
    : m_i((check(i) [[discard]], i))   // 警告されない
  {}

  int m_i;
};

このアプローチにはいくつか問題があります

  • 配列のnew式との競合
    • auto ptr = (new T[123] [[someattribute]]);が現在合法なコード
  • 変換関数を指定する式における競合
    • struct S { operator int() const; };がある時
    • auto ptr = (&S::operator int [[attribute]]);が現在合法なコード

このアプローチを採用する場合、この既存のコードとの衝突の影響を評価した上でどうするかを決定する必要があります。

2つ目のアプローチはかっこで括った上で式の左側に属性を指定するものです。

primary-expression:
    literal
    this
    ( attribute-specifier-seqopt expression )
    id-expression
    lambda-expression
    fold-expression
    requires-expression
[[discard]] f();   // 警告されない(文に対する属性)
([[discard]] f()); // 警告されない(式に対する属性)
f() [[discard]];   // ill-formed

[[discard]] a(), b();     // 警告されない(文に対する属性)
([[discard]] a(), b());   // 警告されない(式全体に対する属性)
([[discard]] a()), b();   // a()は警告されないが、b()は警告されうる

int x = ([[discard]] a(), b()); // 警告されない、a()は抑制され、b()は破棄されていない
int y = ([[discard]] a()), b(); // 警告されない、a()は抑制され、b()は破棄されていない

struct S {
  S(int i) 
    : m_i(([[discard]] check(i)), i)  // 警告されない
  {}

  int m_i;
};

こちらのアプローチでは対象の式を一々かっこで括る必要があるものの、式の左側という自然な位置に属性を導入でき、かっこで括ることによってどの式に属性を指定しているのかが明確になります。

この提案ではこれらのアプローチをのどちらを選択するかの決定をEWG/EWGIに委ねています。

EWGIの投票では、この提案の[[discard]]属性については好まれたものの、個別の式に対する属性指定にはコンセンサスが得られませんでした。おそらく、式文に対する属性としての[[discard]]としてEWGに転送されています。

P2994R0 On the Naming of Packs

パラメータパックそのものを指定する構文を検討する提案。

C++11で導入された可変長テンプレートとパラメータパックはとても便利な機能ですが、基本的にパックそのものにできることは展開のみです。C++17で畳み込み式が追加されましたが、パックそのものに対する操作はまだ導入されておらず、いくつかの提案が進行中です。

そのような機能の難しい点は、パック自体に操作を適用する構文とパックを展開してその要素に操作を適用する構文を区別するようにしなければならない点です。例えばインデックスアクセスの場合、パックの最初の要素にアクセスするのにpack[0]のような構文を選択できません。なぜなら、f(pack + pack[0]...)は現在有効な式であり、これはパック最初の要素をパック内の全ての要素に加算するという意味にはならないためです。

このため、パックそのものに操作を適用する機能についての提案は、それぞれの機能のために個別の提案を選択します。

機能 単一要素 パック
インデックスアクセス elem[0] pack...[0]
展開ステートメント template for(auto x : elem) 次のうちのどれか
template for(auto x : {pack...})
template for...(auto x : pack)
for...(auto x : pack)
リフレクション ^elem なし
スプライス [: elem :] ... [: pack :] ...

elemは何か単一の値(非パック)、packは関数引数パックです。

パラメータパックに対する構文は単一要素に対するものと異なっているだけでなく、パラメータパックそのものに対する操作の間でも異なっています。ここには直交性がなく、パックに対して操作を適用したい場合に...をどこに置くのかは場合により変化します。

ここで問題にしているのは個別の機能そのものについてではなく、それらの間でパックそのものを指定する構文に一貫性がないことです。

この提案は、パラメータパックそのものを指定する構文をまず考案し各操作ではそれをベースとした構文を採用するようにすることで、パックそのものに対する操作ごとに個別の構文を導入するのを回避し、直交性と一貫性を回復しようとするものです。

ただし、パックそのものを指定する構文は捻り出す必要もなく現在すでに存在しています。それは、パラメータパックの(関数引数パックの)宣言やラムダ式の初期化キャプチャで現れる...packという構文です。この提案はこれをそのまま採用し、各機能に展開していくことを提案しています。先ほどの表に当てはめると次のようになります

機能 単一要素 パック この提案
インデックスアクセス elem[0] pack...[0] ...pack[0]
展開ステートメント template for(auto x : elem) 次のうちのどれか
template for(auto x : {pack...})
template for...(auto x : pack)
for...(auto x : pack)
template for(auto x : ...pack)
リフレクション ^elem なし ^...pack
スプライス [: elem :] ... [: pack :] ... [: ...pack :] ...

この提案による構文では、単一要素の構文においてelem...packで置き換えた形になっており、パックに対する各種操作の間でも一貫しています。

個別の機能の個別の提案を見ると、一番左の列の構文よりも中列の構文を好む人はいるかもしれませんが、パック操作の全体を俯瞰したときにはこの構文による一貫性と直交性がその小さな好みを上回るだろうとしています。

この提案による構文には1つ空白地帯があります。

// この提案が採用されたとして
template <typename... Ts>
void foo(Ts... pack) {
  // パックの最初の要素を取得
  auto first = ...pack[0];

  // パックの各要素をイテレートする
  template for (auto elem : ... pack) { }

  // では、これはどういう意味?
  auto wat = ...pack;
}

ここを突き詰めると言語タプルのような用法が開かれる可能性もありますが、この提案ではそれは将来の発明に期待するとしてとりあえずは禁止(ill-formed)としておくことを提案しています。

P2995R0 SG16: Unicode meeting summaries 2023-05-24 through 2023-09-27

SG16(Unicode Study Group)の2023年5月~9月にかけてのミーティングの議事録。

7回のミーティングにおいての提案等のレビュー時の様子が簡単に記録されています。

P2996R0 Reflection for C++26

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

この提案は以前に提案されていたP1240R2のサブセットであり、主に次のものからなります

  • 定数式におけるプログラム要素を表現する鏡像値(reflection value)は、不透明型std::meta::infoの値となる
    • 鏡像値は略して、単に鏡像(reflection)と呼ぶ
  • 与えられたオペランドの鏡像を生成する反射演算子reflection operator^
  • 鏡像に対して作用する(別の鏡像を導出する事を含む)多くのconstevalメタ関数
  • 鏡像から文法要素を生成するスプライサー(splicer
    • [: refl :]のような構文
  • 追加のマイナーな変更

この提案はP1240R2の中から有用なコアな部分を抽出したもので、静的リフレクション及びそれを使用したコンパイルメタプログラミングに関する最後の提案ではありません。P1240R2の残りの部分や更なる機能はこの提案のコア機能をベースとして後から成長させていく事を意図しています。

鏡像を表現するのにstd::meta::infoという単一の型を使用しているのは、言語エンティティに対応する個別の型を用意してしまうとその存在が言語に制約を与えてしまうためです。例えば、C++03では変数と呼ばれるものには参照が含まれていませんでしたが、C++11では変数は参照を含むようになりました。もしC++03にstd::meta::variableのようなものが存在していた場合、変数のカテゴライズの変更がそれを使用しているコードに影響を与えるため不可能だったでしょう。この提案では、将来の言語に不当な制約を課さないためにあえて鏡像を保持する型を単一のstd::meta::infoに限定しています。

リフレクション領域と構文領域を接続する例

constexpr auto r = ^int;  // int型の鏡像を取得

// 鏡像から型名の情報を取り出して構文要素にする
typename[:r:] x = 42;       // 次と等価 : int x = 42;
typename[:^char:] c = '*';  // 次と等価 : char c = '*';

クラスメンバをインデックスアクセスする例

struct S {
  unsigned i:2, j:6; 
};

// 指定された数値に対応するメンバの鏡像を取得する
consteval auto member_number(int n) {
  if (n == 0) {
    return ^S::i;
  } else if (n == 1) {
    return ^S::j;
  } else {
    return std::meta::invalid_reflection("Only field numbers 0 and 1 permitted");
  }
}

int main() {
  S s{0, 0};

  // member_number()の返すメンバ変数の鏡像をメンバ名にする
  s.[:member_number(1):] = 42;  // 次と等価 : s.j = 42;
  s.[:member_number(5):] = 0;   // Error (likely with "Only field numbers 0 and 1 permitted" in text).
}

型のリストから型のサイズのリストを作成する例

// 型の鏡像のリスト
constexpr std::array types = {^int, ^float, ^double};

// typesの各要素の型のサイズのリスト
constexpr std::array sizes = []{
  std::array<std::size_t, types.size()> r;
  std::ranges::transform(types, r.begin(), std::meta::size_of);
  return r;
}();
// 次と等価
// std::array<std::size_t, 3> sizes = {sizeof(int), sizeof(float), sizeof(double)}:

std::make_integer_sequenceを実装する例

#include <utility>
#include <vector>

template<typename T>
consteval std::meta::info make_integer_seq_refl(T N) {
  // 整数型Tの鏡像を先頭に入れておく
  std::vector args{^T};

  for (T k = 0; k < N; ++k) {
    // 各kをその定数値の鏡像へ変換しvectorへ詰める
    args.push_back(std::meta::reflect_value(k));
  }

  // argsの各鏡像をテンプレート引数要素へ変換しinteger_sequenceのテンプレート引数として埋めてその鏡像を返す
  return std::meta::substitute(^std::integer_sequence, args);
}

// std::integer_sequence<T, 0, ..., N - 1>となる
template<typename T, T N>
  using make_integer_sequence = [:make_integer_seq_refl<T>(N):];

std::meta::substitute()はテンプレートの鏡像とテンプレート引数の鏡像のリストを受けて、1つ目の引数のテンプレート引数としてリストの各要素の鏡像の実体で埋めた型の鏡像を返すものです。

namespace std::meta {
  consteval auto substitute(info templ, span<info const> args) -> info;
}

値ベースリフレクションの強い点として、これらの例のようにstd::vectorstd::spanなどを使用しながら通常の定数式としてリフレクション処理を記述できる点があります。

列挙値の文字列化

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  // 列挙型Eの列挙値毎にif文を生成する
  template for (constexpr auto e : std::meta::members_of(^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::name_of(e));
    }
  }

  return "<unnamed>";
}

enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

ここでは、template for(拡張ステートメント)を使用していますが、これはこの提案の一部ではなく別に提案(P1306R1)されているものです。template forは通常のfor文のように評価される時にループするのではなく、それ(を含む関数)が実体化されたときにループし、ループとともに本体内のコードをその場に順番にコピペしていくような動作をします。すなわち、鏡像オブジェクトのリストからのコード生成をサポートするものです。

プログラムオプションをパースする例

using std::meta;

template<typename Opts>
auto parse_options(std::span<std::string_view const> args) -> Opts {
  Opts opts;

  // オプション定義型Optsの非静的メンバ毎にループ
  template for (constexpr auto dm : nonstatic_data_members_of(^Opts)) {
    auto it = std::ranges::find_if(args,
      [](std::string_view arg){
        return args.starts_with("--") && args.substr(2) == name_of(dm);
      });

    if (it == args.end()) {
      // no option provided, use default
      continue;
    } else if (it + 1 == args.end()) {
      std::print(stderr, "Option {} is missing a value\n", *it);
      std::exit(EXIT_FAILURE);
    }

    using T = typename[:type_of(dm):];
    auto iss = std::ispanstream(it[1]);
    if (iss >> opts.[:dm:]; !iss) {
      std::print(stderr, "Failed to parse option {} into a {}\n", *it, display_name_of(^T));
      std::exit(EXIT_FAILURE);
    }
  }

  return opts;
}

struct MyOpts {
  string file_name = "input.txt";  // Option "--file_name <string>"
  int    count = 1;                // Option "--count <int>"
};

int main(int argc, char *argv[]) {
  MyOpts opts = parse_options<MyOpts>(std::vector<std::string_view>(argv+1, argv+argc));
  // ...
}

この例ではparse_options()自体は実行時に実行される関数ですが、その内部で使用されている拡張ステートメントやスプライサーなどはコンパイル時に(parse_options()が実体化したときに)処理されます。

シンプルなtuple実装例

#include <meta>

template<typename... Ts>
struct Tuple {
  // Tsの各型を非静的メンバとして保持する構造体を生成
  using storage = typename[:std::meta::synth_struct({nsdm_description(^T)...}):];
  storage data;

  Tuple(): data{} {}
  Tuple(Ts const& ...vs): data{ vs... } {}
};

template<typename... Ts>
  struct std::tuple_size<Tuple<Ts...>>: public integral_constant<size_t, sizeof...(Ts)> {};

template<typename I, typename... Ts>
  struct std::tuple_element<I, Tuple<Ts...>> {
    using type = [: template_arguments_of(^Tuple<Ts...>)[I] :];
  };

template<typename I, typename... Ts>
  constexpr auto get(Tuple<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {
    // Tuple::storageのメンバ名をインデックスから求めてアクセス
    return t.data.[:nonstatic_data_members_of(^decltype(t.data))[I]:];
  }

// Similarly for other value categories...

nsdm_description()は型の鏡像をその型の非静的メンバの鏡像となるものに変換するメタ関数です(nsdm=non static data member)。そして、synth_struct()は非静的メンバの鏡像のリストを受け取って、その非静的メンバを持つ構造体型の鏡像を返すメタ関数です。nsdm_description()は特に指定しなければ結果の非静的メンバ変数名は未規定となりますが、この例のget<I>()実装ではNTTPインデックスを用いてそのインデックスに対応するメンバ名を直接取得しており、そのメンバ名を知る必要がないようになっています。

そのほかにも様々な例が提案には記載されています。

なお、この記事における訳語は筆者の独断によるもので、何かしらの合意を得たものではありません。

P2997R0 Removing the common reference requirement from the indirectly invocable concepts

イテレータを介した間接呼び出し系のコンセプトから、common_reference要件を取り除く提案。

問題の説明のために、次のようなrange型を仮設します。

struct C {
  auto f() -> void;
};

struct Iterator {
  using value_type = C;
  using difference_type = std::ptrdiff_t;
  using iterator_category = std::input_iterator_tag;

  auto operator*() const -> C&&;
  auto operator++() -> Iterator&;
  auto operator++(int) -> void;
  auto operator==(Iterator const&) const -> bool;
};

static_assert(std::input_iterator<Iterator>);
static_assert(std::same_as<std::iter_value_t<Iterator>, C>);
static_assert(std::same_as<std::iter_reference_t<Iterator>, C&&>);

struct R {
  auto begin() -> Iterator;
  auto end() -> Iterator;
};

static_assert(std::ranges::range<R>);
static_assert(std::same_as<std::ranges::range_reference_t<R>, C&&>);

ここで重要なことは、このRの要素の参照型がC&&である点であり、他の部分はイテレータrangeを整えるための部分でしかありません。そしてこのRstd::generator<C>と同じ性質を備えています。

これを範囲forranges::for_eachで普通にループを回してみると異なった振る舞いが得られます

void test(R r) {

  // これはok
  for (auto&& c : r) {
    c.f();
  }
  
  // こっちはng
  std::ranges::for_each(r, [](auto&& c){
    c.f();  // const修飾がマッチしないらしい
  });
}

エラーメッセージを見てみると、const C&cc.f()を呼び出そうとしているけれどC::f()const修飾がないため呼び出すことができずエラーになっているようです。

この例のコードにはどこにも、Rの要素をconst化するコードはありません。const C&は一体どこから来たのでしょうか?

ranges::for_eachでしか起こらないことからranges::for_eachそのものに問題があると考えられます。その宣言は次のようになっています

template<input_range R, class Proj = identity,
         indirectly_unary_invocable<projected<iterator_t<R>, Proj>> Fun>
constexpr ranges::for_each_result<borrowed_iterator_t<R>, Fun>
  ranges::for_each(R&& r, Fun f, Proj proj = {});

呼び出しに関して制約してるのはindirectly_unary_invocableコンセプトで、これは次のように定義されています

template<class F, class I>
concept indirectly_unary_invocable =
  indirectly_readable<I> &&
  copy_constructible<F> &&
  invocable<F&, indirect-value-t<I>> &&
  invocable<F&, iter_reference_t<I>> &&
  invocable<F&, iter_common_reference_t<I>> &&
  common_reference_with<
    invoke_result_t<F&, indirect-value-t<I>>,
    invoke_result_t<F&, iter_reference_t<I>>>;

ranges::for_eachの場合、このIにはprojected<iterator_t<R>, Proj>>が指定されており、Franges::for_eachに渡した各要素に対する処理を記述した呼び出し可能オブジェクト(例ではラムダ式)です。

プロジェクションがデフォルトのままなので、Projstd::identityであり、projected<iterator_t<R>, identity>>Iと同じ性質を備えたイテレータっぽい型になります。したがって、ここではprojectedを無視できます。

Fが適切にRの要素型に対して呼び出し可能として定義されているとして(範囲forでは同等の処理でコンパイルが通るのでその仮定は満たされているはず)、indirectly_unary_invocable<F, Iterator>の各制約を検討します。

Iinput_iteratorであることは予め確認しており、Fは今ラムダ式クロージャ型でありムーブオンリー型をキャプチャしない限りはコピー構築可能になります。したがって最初の2つの制約はパスしています。

残りのイテレータ経由の呼び出しに関わるコンセプトを調べる前に、イテレータIに関する関連型をまず明らかにしておきます。

関連型
indirect-value-t C&
iter_reference_t C&&
iter_value_t C
iter_common_reference_t const C&

iter_common_reference_t<I>Iiter_value_titer_reference_tの間のcommon_referenceを求めるもので、common_reference_t<C&&, C>は通常const C&になります。

これらの型を使って残りのコンセプトの妥当性をチェックしていくと次のようになります

制約 結果
invocable<F&, indirect-value-t<I>>
invocable<F&, iter_reference_t<I>>
invocable<F&, iter_common_reference_t<I>>
common_reference_with<...>

したがって、冒頭の例のエラーの原因はindirectly_unary_invocableコンセプトの中のinvocable<F&, iter_common_reference_t<I>>が満たされないことによるもので、const C&iter_common_reference_t<I>で発生していることがわかりました。

この制約が満たされないのはまさに、[](auto&& c){ c.f(); }というF&const C&を渡して呼び出そうとしているためです。

イテレータの共通参照型(iter_common_reference_t)の要件は(上記ranges::for_eachのような)アルゴリズムに呼び出し可能オブジェクト(以下callable)を渡す際に、そのcallableの引数型をジェネリックにする必要をなくすためのものです。それによって、ユーザーはイテレータ型の性質に踏み込まなくても自然にそのようなアルゴリズムを使用できます。

ところがそのような共通参照型は実際にアルゴリズムの実装で使用されるわけではありません。現在の標準ライブラリ内で実際に共通参照型を使用しているのは次の場所のどちらかのみです

  • 明示的にマージを行っているため、実際に複数の範囲の共通参照が必要な場合
    • views::join_withや提案中のviews::concatなど
  • 新しい参照型を生成しようとしている時
    • views::as_constなど

つまり、アルゴリズムの実装においては共通参照型を使用することは無く、実際にiter_common_reference_tが使用されるべきなのはユーザーがアルゴリズムに渡すcallableオブジェクトの引数型においてであり、そのようなcallableアルゴリズム中で使用可能かどうかの制約は上2つの値型と参照型による制約でチェックされています。

共通参照型が存在するのは、複数の範囲をマージできるようにするためと、一般的ではない呼び出し可能オブジェクトを書く方法をユーザーに提供するためですが、一般的なアルゴリズムの実装にとって有益な要件ではありません。この呼び出し(invocable<F&, iter_common_reference_t<I>>のような)を要求する一方で、使用されていないこの呼び出しが実際にアルゴリズム中で使用されている呼び出し方法と互換性があるかどうか(最後のcommon_reference_with<...>のような)はチェックされていません。したがって、このチェックは有効なコードを拒否するだけで何ら価値を提供していません。

この提案は、これらの理由により全てのイテレータを介した間接呼び出し系のコンセプトからこのようなイテレータの共通参照型に関する要件を削除することを提案するものです。

対象は次の6つのコンセプトです

  • indirectly_unary_invocable
  • indirectly_regular_unary_invocable
  • indirect_unary_predicate
  • indirect_binary_predicate
  • indirect_equivalence_relation
  • indirect_strict_weak_order

現在これらのコンセプト内に存在するinvocable<F&, iter_common_reference_t<I>>のようなiter_common_reference_tによって呼び出し可能であることをチェックする制約のみを単純に削除することを提案しています。

P2999R0 Sender Algorithm Customization

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

P2300で提案されているsenderアルゴリズムexecution::thenexecution::justなど)は全てカスタマイゼーションポイントオブジェクトであり、tag_invokeによるディスパッチによってユーザーによってカスタマイズされた実装を検出するとそれを優先して使用します。そのようなカスタマイズが見つからない場合にのみデフォルトの実装が使用されます。senderアルゴリズムのカスタマイズは実行コンテキストに対して行われるものであり、P2300では実行コンテキストの表現はschedulerが担っています。

実行コンテキストは処理が実行される場所を指す抽象であり現在の環境ではCPU/GPU/FPGAなどが該当しますが、将来的に全く性質の異なるアクセラレータが登場する可能性があります。そうした現時点で姿形のない将来の実行コンテキストにおいてもsenderアルゴリズムを効率的に実行できるようにするために、senderアルゴリズムのカスタマイズはscheduler(実行コンテキスト)に対して行われます。

したがって、senderアルゴリズムがそのカスタマイズを検出するにはsenderアルゴリズムが構成される時点においてそのアルゴリズムが完了する実行コンテキスト(completion scheduler、完了scheduler)を知っていなければなりません。それがわからない場合はデフォルトの実装が使用され、開始時などに後から与えられたschedulerの実行コンテキストで処理を行い完了します。

現在のthenアルゴリズムの実装例

// アルゴリズムのカスタマイズを検出するヘルパコンセプト
template <class AlgoTag, class SetTag, class Sender, class... Args>
concept has-customization =
  requires (Sender sndr, Args... args) {
    tag_invoke(AlgoTag(),
               get_completion_scheduler<SetTag>(get_env(sndr)),
               std::forward<Sender>(sndr),
               std::forward<Args>(args)...);
  };

// `then`アルゴリズムの実装型かつタグ型
struct then_t {
  template <sender Sender, class Fun>
    requires /* requirements here */
  auto operator()(Sender&& sndr, Fun fun) const
  {
    // sndrは先行する処理を表すsender
    // funはthenによって継続する処理

    // 先行senderから完了schedulerを取得でき
    // かつカスタムthen実装が見つかる場合はそちらにディスパッチする
    if constexpr (has-customization<then_t, set_value_t, Sender, Fun>)
    {
      auto&& env = get_env(sndr);
      return tag_invoke(*this,
                        get_completion_scheduler<set_value_t>(env), // 完了schedulerの取得
                        std::forward<Sender>(sndr),
                        std::move(fun));
    }
    else
    {
      // デフォルト実装を使用する
      return then-sender<Sender, Fun>(std::forward<Sender>(sndr), std::move(fun));
    }
  }
};

inline constexpr then_t then {};

これはたとえば、just(42) | then([](int) { ... })のように使用してsenderアルゴリズムによって処理グラフを構成しますが、その実装のカスタマイズが検出されるのはthen_toperator()が呼ばれてsenderを取得した時点であることがわかります。schedulerそのものはこのグラフの上から(予め)でも下から(後から)でも指定することができますが、senderアルゴリズムのカスタマイズ実装が検出されるのは上からschedulerが指定されている場合のみです。

現在のこのような仕組みにはいくつかの欠点があります

  1. just(42)(値を投入するだけのsenderアルゴリズム)のような単純なsenderはその完了schedulerを知らない
    • その処理が開始された実行コンテキストで完了するが、これはsenderが構築された時点ではわからない
  2. on(sched, then(just(), fun))(実行コンテキストの指定/切り替え)のようなsenderの場合、ネストしたthen(just(), fun)の部分のsenderschedulerを知らない状態で構築される
    • ここでschedに対してカスタマイズされたthen実装を使用させるにはどうすればいい?
  3. when_all(sndr1, sndr2)(指定された全ての処理の完了待機)のような複合senderは一般的にはその完了schedulerを知ることができない
    • sndr1, sndr2の両方が完了schedulerを知っていたとして、それぞれをsched1, sched2としてもsndr1, sndr2のどちらが先に完了するかによってwhen_allsenderの完了schedulersched1, sched2のどちらかになる
    • これは実行時の動的な性質であり、アルゴリズムのカスタマイズを検出するのに適していない

1と2の場合、正しいアルゴリズム実装を見つけるために必要な情報がそれを探索する時点で利用できないという問題であり、3の場合はアルゴリズムのセマンティクスによってアルゴリズムのどのカスタマイズを使用すべきかを静的に決定できないという問題です。

2の問題は特にプログラマの意図しない動作につながる可能性があるため深刻です

正しい 間違い
my::thread_pool_scheduler sch = /*...*/;

// スレッドプール上でのバルク処理の記述
auto work =
  ex::transfer_just(sch, data)
| ex::bulk(data.size(),
           [](int i, auto& data) {
             ++data[i];
           });

// 処理の実行
std::this_thread::sync_wait(std::move(work));
my::thread_pool_scheduler sch = /*...*/;

// バルク処理の記述
auto work =
  ex::just(data)
| ex::bulk(data.size(),
           [](int i, auto& data) {
             ++data[i];
           });

// 処理をスレッドプール上で実行する
std::this_thread::sync_wait(ex::on(sch, std::move(work)));

この2つの例の違いは処理を実行する場所であるschedulerを先に与えるか後に与えるかの違いのみです。当然、この2つの例は同じ動作をすることが期待されます。

しかしここで、my::thread_pool_schedulerの提供者がそのスレッドプールのためにカスタマイズしたbulkアルゴリズムを提供していたとすると

namespace my {
  // thread_pool_schedulerのためのbulkアルゴリズムのカスタマイズ
  template <ex::sender Sender, std::integral Shape, class Function>
  auto tag_invoke(ex::bulk_t,
                  thread_pool_scheduler sched,
                  Sender&& sndr,
                  Shape shape,
                  Function fun) {
    /*...*/
  }
}

左側のコードはこのカスタマイズを検出し(transfer_justによって処理グラフの最初のsenderに完了schedulerが与えられているため)意図通りにスレッドプールのスレッドをフルに使用してバルク処理を実行します。しかし、右側のコードではこのカスタマイズを検出できないため(just(data)には完了schedulerが与えられていないため、後続のbulkthread_pool_schedulerに対するカスタマイズを見つけられない)、デフォルト実装が使用された結果としてバルク操作はスレッドプールの1つのスレッドだけを使用して実行されます。

このことは明らかに間違った動作であり、修正が必要となります。この提案はその修正を行おうとするものです。

この提案による変更は次のようなものです

  1. 他に決定可能なドメインがない場合に使用するdefault_domain型を追加
  2. 新しいget_domain(env) -> domain-tag転送クエリを追加
  3. カスタマイズ不可能なtransform_sender(domain, sender [, env]) -> senderを追加
    • これは初期のカスタマイズと後からのカスタマイズの両方に使用される
    • 初期のカスタマイズ(Early customization
      • senderアルゴリズムのカスタマイゼーションポイントオブジェクト内から呼ばれる
      • 完了schedulerをタグとして使用してカスタマイズを検出する現在の仕組みを置換する
      • 環境変数なしで呼び出される
      • 次のいずれかによってsenderからドメインを取得する
        • get_domain(get_env(sender))
        • get_domain(get_completion_scheduler<completion-tag>(get_env(sender)))
        • default_domain()
    • 後からのカスタマイズ(Late customization
      • connectカスタマイゼーションポイントオブジェクト内から、connect_tのカスタマイズ検出の前に呼び出される
      • receiver環境変数によって呼び出される
      • 次のいずれかによってreceiverからドメインを取得する
        • get_domain(get_env(receiver))
        • get_domain(get_scheduler(get_env(receiver)))
        • default_domain()
    • transform_sender(domain, sender [, env])は次のいずれか有効なものを返す
      • domain.transform_sender(sender [, env])
      • default_domain().transform_sender(sender [, env])
      • sender
  4. 標準の遅延sender型は構造化束縛によって[tag, data, …children]に分解可能なsender型を返す
  5. 全ての引数のドメイン型が同じにならない限り、when_allアルゴリズムの呼び出しはill-formed
    • when_allの返すsenderはその環境を介してそのドメインを公開する
  6. on(sch, sndr)アルゴリズムtransferアルゴリズムの後からのカスタマイズを拾い上げるためにtransferに対して指定されるべき
  7. senderファクトリjust, just_error, just_stoppedでは、タグ型を指定する必要がある
  8. let_value(sndr, fun)アルゴリズムでは、先行するsndrsenderオブジェクト)がset_valueの完了schedulerを持っている場合、二次接続側(後続のsender)に接続されるrecieverはそのschedulerを現在の環境のschedulerとして公開する
    • 言い換えると、先行するsndrが値vs...で完了する場合、fun(vs...)の結果はget_scheduler(get_env(r))get_completion_scheduler<set_value_t>(get_env(sndr))と等しくなるようなrecieverオブジェクトrに接続される
    • let_errorも同様に、先行senderの完了schedulerをクエリする時にset_error_tを使用する
  9. schedule_from(sched, sndr)アルゴリズムget_domain(get_env(s))get_domain(sched)と同じになるようなsenderオブジェクトsを返す必要がある
  10. 次のアルゴリズムについては、デフォルトの実装では結果のsenderを返す前に作業を行う必要があり、それはdefault_domain::transform_senderオーバーロードで行われる
    • split
    • ensure_started
  11. 次のアルゴリズムはデフォルトの実装が他のより原始的な操作を利用して指定されているが、それらをdefault_domain::transform_senderオーバーロードによって置き換えた形になる
    • transfer
    • transfer_just
    • transfer_when_all
    • transfer_when_all_with_variant
    • when_all_with_variant
  12. let_value(snd, fun)アルゴリズムでは、入力関数funが返す可能性のあるsenderの型は全て同じドメインを持っていなければならない
    • そうでなければ、let_valueの呼び出しはill-fomred
    • let_valuesenderはそれを自身のドメインとして報告する
    • let_errorlet_stoppedでも同様
  13. sender消費アルゴリズムstart_detachedsync_waitでは、タグディスパッチのためにアルゴリズムタグと入力senderドメインをタグとして使用する

この提案では、senderアルゴリズムは指定された完了schedulerではなくその処理グラフのドメインからアルゴリズムのカスタマイズ実装を発見するようにしています。コード上ではドメインドメインタグ型として表現され、when_allのようなアルゴリズムでは入力のsenderドメインが全て一致(すなわち、ドメイン型が一致)している場合にのみ使用可能となります。そして、senderアルゴリズムによる処理グラフの構成時に加えて、全ての情報が揃った時(senderreceiverconnectされた時)に再度カスタマイズ実装を探索する後からのカスタマイズ(Late customization)を有効化でしています。

初期のカスタマイズ/後からのカスタマイズ双方共にその処理はデフォルトのsenderドメイン及び環境に基づいて変換する作業になり、そのための関数としてtransform_sender()を追加しています。transform_sender()ドメインsender及びオプションで環境オブジェクトを受け取って、それらの情報を使用してそのsenderを現在のドメインと環境によって発見されるカスタマイズを適用したものに変換します。後からのカスタマイズではこれをexecution::connectの呼び出し内で行うことで処理グラフの構成後に全ての情報が揃った状態でsenderアルゴリズムのカスタマイズが発見できるようになります。

transform_sender()自体は指定されたドメインtransform_sender()senderと環境を渡して返すか、それができない場合はデフォルトのドメインを使用して同じことを行うか、それもできなければ元のsenderをそのまま返します。ドメインtransform_sender()ではそのドメインに沿った形でsender型のカスタマイズを発見する処理を行いますが、デフォルトのドメインでは単にsender型に対してそれを行い、標準のsender型の多くはtransform_sender()を用意しておりそのデフォルトの動作を行うsenderを返します。

P3001R0 std::hive and containers like it are not a good fit for the standard library

std::hiveは標準ライブラリのコンテナとしてふさわしくないとする提案。

まず、標準ライブラリの要素は理想的には次のいずれかに該当するものです

  • コンパイラサポートが必要な型や関数
    • 標準ライブラリはコンパイラと一緒に出荷されるため、コンパイラサポートの必要な物を配置できる唯一の場所
  • コアな語彙型
    • std::optional, std::span, std::string_viewなどの語彙型は汎用性と表現力に優れ、C++プログラミングの基礎部品となる
    • 語彙型を標準化しない場合、同じ目的に対応する独自実装がライブラリ毎に定義され、それの間の相互変換のために余計なオーバーヘッドがかかる
  • クロスプラットフォームの抽象
    • 標準ライブラリはプラットフォームの専門家によって実装されており、ほとんどのプラットフォームはI/Oやメモリ割り当て、スレッドなどの機能を提供する
    • これを標準化することで、ユーザーはクロスプラットフォームで統一的にそれを利用でき、実装者はプラットフォームの専門知識を活かしてそれを実装できる
  • 基礎的なアルゴリズムとデータ構造
    • コンテナ(動的配列やキュー、スタックなど)とアルゴリズム(ソートや検索など)はほとんどすべてのプログラミングタスクの基礎であり、必須の機能である
    • 頻繁に必要となるこれらのものを標準化することで、ユーザーはそれを再実装する必要がなくなる
    • またこれらの機能は、広く理解できるセマンティクスと安定した実装を備えてもいる
    • これらのエンティティは、エンティティを再発明することなく作業を完了するために重要であるという点で、語彙型とは異なる
      • 語彙型は規則を確立するために重要であり、異なるコード間の相互運用に利用される

これに該当しないものが標準ライブラリとしてふさわしくないわけではありませんが、該当しないものを標準化するにはそれなりの根拠が必要となります。

賛否はあれど、C++標準ライブラリは安定したABIとAPIを維持しており、そこからの逸脱はユーザーに大きな混乱をもたらします。std::vector<bool>のように明らかに失敗とみなされる機能であっても非推奨や削除されることは稀であり、残り続けます。そのため、標準化委員会はインターフェースが確立されていない限りライブラリ機能を標準化することはできず、一度標準化されるとライブラリのAPIとABIは事実上凍結されます。それによって、実装も変更できなくなる場合があります。

標準化された機能はすべてのプラットフォーム間で移植可能である必要があり、その実装や品質はプラットフォームによって異なります。そのため、すべてのプラットフォームで利用できるわけではないAPIや、パフォーマンスなどの特定の実装特性に依存したAPIの標準化には注意が必要です。

そして、標準化委員会の時間は限られており、ある機能の議論に時間をかけるということは別の提案の議論の時間が取られるということを意味しています。

std::hiveは高性能コンテナに該当するタイプのコンテナであり、実行時の動作やメモリ使用量などの点で既存の標準コンテナに対して優位性を持つコンテナです。このようなコンテナには次のような特徴があります

  • 時間・空間計算量で他の実装よりも優れている
  • 関連するベンチマークで他の実装よりも明確に優れた実行時の速度やメモリ使用量が計測される
  • 語彙型である必要はない
    • APIの特定の部分にカスタムの特化型を使用することはパフォーマンス上のメリットがある
  • 積極的にメンテナンスされている
    • CPUは進化し続けており、より優れたアルゴリズムがすぐに利用可能になる
    • 高性能コンテナがそうあり続けるためには、これらの変化に適用し改善し続けなければならない

この性質はその実装が安定しておらず、プログラミングにおいて必須というわけでもないことを示しています。さらに、高性能コンテナはコンパイラのサポートやOSのAPIのサポートを必要とせず、語彙型でもないため、最初に挙げた標準ライブラリ要素のカテゴリのいずれにも該当しないことになります。

むしろ、高性能コンテナを標準化した場合の欠点を上げることができます

  • 安定性の要件は高性能コンテナの進化を妨げる
    • メンバ変数の追加などはABI破壊であり、内部実装のほとんどはAPIの要件を介して公開されている
  • 標準化のためには委員会の多大な時間を必要とする
    • 標準化に時間を食っている間に想定する実装が時代遅れになっていたとしても、ABIの問題から更新できない
  • 標準ライブラリはインターフェースのみを標準化しており、実装は標準化していない
    • 実装は複数の標準ライブラリベンダによって行われ、そのパフォーマンスはプラットフォームごとに異なる可能性がある
    • 標準ライブラリ機能を使用する場合、パフォーマンスの保証はない

それでも高性能コンテナを実装することの利点はせいぜい、外部ライブラリに依存せずにそのコンテナを利用できるようになるくらいのものです。

ここまでの高性能コンテナの批判は一般的な話ですが、std::hiveは高性能コンテナなのですべて該当します。

提案者によって提供されているリファレンス実装は堅牢であるようで、有用性は疑うべくもありません。しかし、委員会が標準化するのはリファレンス実装ではなくインターフェースであり、それは標準ライブラリ実装者が独自のトレードオフを行うのに十分な余地を残すのと同時に、後の最適化によって重大な変更が発生する可能性があるほど具体的です。わざわざstd::hiveを使用するほど性能にこだわるのに、標準ライブラリの実装品質や外部ライブラリの保証に無頓着であるということは考えられません。

これらのマイナス面を無視したとして、std::hiveを導入するメリットを考えてみます。

前述のように、最初に挙げた要素にはいずれも該当していません。残るは標準ライブラリに載せることでサードパーティライブラリを入手するためのメカニズムをセットアップする必要がなくなるため使いやすくなるという利便性の向上です。しかし、現在使用したい人はリファレンス実装ないし同等の特性を持つ代替実装をなんとかして使用していると考えられ、現在それを使用していないプロジェクトで使用されるようになるかは疑問があります。

この提案は、ここまで述べたように、std::hiveを標準化するメリットはほぼなくstd::hiveそのものの利点も保証されない可能性があるため、C++26の限られた時間サイクルを割いてまでstd::hiveを標準化するべきではない、とするものです。

この提案を受けてのLEWGにおける投票では、std::hiveの標準化作業を続けることに合意されています。

P3002R0 Guidelines for allocators in new library classes

標準ライブラリ機能がアロケータを使用する際のポリシーの提案。

この提案は、上の方のP2267やP2979で提言されていたポリシー整備に関する具体的なものの一つです。ここではLEWGに向けて、新しいライブラリ機能がアロケータを使用するべきかや使用する際のガイドラインとして2つのポリシーを提案しています。

  1. クラスはどのような場合にアロケータを使用する必要があるか?
    • 動的にメモリを確保するクラスは、構築時にアロケータを受け入れ、確保と解放のためにそのアロケータのコピーを保持する必要がある
    • アロケータを使用するサブオブジェクト(基底クラス/非静的メンバ)を含むクラスは、それらのサブオブジェクトに転送するためにアロケータを構築時に受け入れる必要がある
  2. 型がstd::pmr名前空間エイリアスを持つ必要があるのはどのような場合か?
    • デフォルトでstd::allocatorの特殊化に設定されたアロケータパラメータをもつクラステンプレートには、std::pmr名前空間polymorphic_allocatorについてのエイリアスが必要
    • アロケータパラメータをstd::allocatorの特殊化に設定するもエイリアスを持つクラステンプレートは、std::pmr名前空間polymorphic_allocatorについてのエイリアスが必要

これらのポリシーは、現在の標準ライブラリのアロケータサポートをベースとしたものです。

P3003R0 The design of a library of number concepts

数値コンセプトの設計についての文書。

この文書のいう数値コンセプトとは、数学的な意味での数というものをC++コンセプトで定式化しようとするものです。主に、<numeric>にあるもののコンセプト対応や将来の物理量と単位を扱うライブラリにおいて使用することを意図しています。

まず全ての数値コンセプトはオプトインが必要で、そのためのいくつかの型特性が用意されます

  • numberコンセプトを有効化するためのenable_numberenable_complex_number
  • 特定の数値を示すnumber_zeronumber_one
    • 例えば、number_zero_v<T>のようにしてnumberTの零元を取得する
  • 数値に関連した型を取得するnumber_difference_tvector_scalar_t
    • number_difference_t<T>Tの差の結果型
    • vector_scalar_t<T>はベクトル空間Tのスカラ型

これによって、一番基本的なnumberコンセプトが次のように定義されます

template<typename T>
concept number = enable_number_v<T> && std::regular<T>;

template<typename T, typename U>
concept common_number_with =
  number<T> && number<U> && std::common_with<T, U> && number<std::common_type_t<T, U>>;

common_number_withは2つのnumber型を関連づけるためのコンセプトです。

他の数値コンセプトはこのnumberコンセプトをベースとして組み立てられます。例えば

// 順序付け可能な数
template<typename T>
concept ordered_number = number<T> && std::totally_ordered<T>;

// 数直線
template<class T>
concept number_line =
  ordered_number<T> &&
  requires(T& v) {
    number_one_v<number_difference_t<T>>;
    { ++v } -> std::same_as<T&>;
    { --v } -> std::same_as<T&>;
    { v++ } -> std::same_as<T>;
    { v-- } -> std::same_as<T>;
  };

また、基本的な数値演算に関するコンセプトも用意されています。例えば加算の場合

template<class T, class U> concept addition-with =
  number<T> &&
  number<U> &&
  requires(const T& c, const U& d) {
    { c + d } -> common_number_with<T>;
    { d + c } -> common_number_with<T>;
  };

template<class T, class U> concept compound-addition-with =
  addition-with<T, U> &&
  requires(T& l, const U& d) {
    { l += d } -> std::same_as<T&>;
  };

これらは今の所説明専用として定義されています。

最後に、これらによって代数的構造を表すコンセプトが定義されます。例えば

template<typename T, typename U>
concept point_space_for =
  subtraction-with<T, U> && // TとUの値t, uに対して、t - uが可能
  negative<U> &&            // Uは負の数を含む
  common_number_with<number_difference_t<T>, U>;

template<typename T, typename U>
concept compound_point_space_for = point_space_for<T, U> && compound-subtraction-with<T, U>;

// 点付き空間
template<typename T>
concept point_space = compound_point_space_for<T, number_difference_t<T>>;

// ベクトル空間
template<typename T>
concept vector_space = 
  point_space<T> && 
  compound-scales-with<T, vector_scalar_t<T>>;  // vector_scalar_t<T>はベクトル空間Tにおいてスカラ型としての性質を満たすこと

この文書による数値コンセプトの設計はまだ完全ではなく経験も不足しているため、この文書は現状報告であり、将来の標準数値コンセプトの実現のための1つの足がかりとして提出されたものです。

P3008R0 Atomic floating-point min/max

浮動小数点数型のstd::atomicにおけるfetch_max()/fetch_min()の問題を解消する提案。

P0493ではstd::atomicに対して指定した値との大小比較を条件として値の入れ替えを行うfetch_max()/fetch_min()を提案しており、これはC++26導入目前まで進んでいます。しかし、2023年6月の全体会議において、浮動小数点数型の場合の動作について問題が提起されたことで足踏みをしています。

fetch_max()/fetch_min()はその大小比較についてstd::min/std::maxをベースとしていますが、そのstd::min/std::max浮動小数点数型の特定の値の比較に対して望ましい結果(IEEE754に定義され、多くのハードウェア実装やCのライブラリ関数が返す結果)を返さないという問題がありました。

  • 符号付の0
    • -0 < +0と判断するのか否か
  • quiet NaN(qNaN)
    • 引数の片方がqNaNの場合、それを欠落したデータ(Missing Data)として扱うのか、エラーを伝播させるのか
C C++ signed 0 qNaN
std::min/std::max 同値 UB
fmin/fmax 同値、QoIとして-0 < +0 Missing Data
fminimum/fmaximum -0 < +0 error
fminimum_num/fmaximum_num -0 < +0 Missing Data

C++std::min/std::maxの場合qNaNは事前条件違反で未定義動作となり、その場合全ての実装で第一引数を返すようです。

// std::min/std::maxの動作例
min(qNaN, 2.f); // UB: qNaN
max(qNaN, 2.f); // UB: qNaN
min(2.f, qNaN); // UB: 2
max(2.f, qNaN); // UB: 2
min(-0.f, +0.f); // -0
max(-0.f, +0.f); // -0
min(+0.f, -0.f); // +0
max(+0.f, -0.f); // +0

この振る舞いは並行プログラミングにおいては問題となる可能性があり、異なるスレッドからの値の出力を待機してこのような比較を行う処理がある場合に、その処理結果はデータの到着順によって変化し、実行ごとに異なった結果になる(特に符号が異なる)可能性があります。

Cのfmin/fmaxの場合、符号付0の扱いはstd::min/std::maxと同じですが、QoI(実装品質)として-0 < +0とすることが許可されています。また、aNaNは欠落したデータとしてもう片方の値を返します。

// Cのfmin/fmaxの動作例
fmin(qNaN, 2.f); // 2
fmax(qNaN, 2.f); // 2
fmin(2.f, qNaN); // 2
fmax(2.f, qNaN); // 2
fmin(-0.f, +0.f); // -0 or +0
fmax(-0.f, +0.f); // -0 or +0
fmin(+0.f, -0.f); // -0 or +0
fmax(+0.f, -0.f); // -0 or +0

// Cのfminimum/fmaximumの動作例
fminimum(qNaN, 2.f); // qNaN
fmaximum(qNaN, 2.f); // qNaN
fminimum(2.f, qNaN); // qNaN
fmaximum(2.f, qNaN); // qNaN
fminimum(-0.f, +0.f); // -0
fmaximum(-0.f, +0.f); // +0
fminimum(+0.f, -0.f); // -0
fmaximum(+0.f, -0.f); // +0

// Cのfminimum_num/fmaximum_numの動作例
fminimum_num(qNaN, 2.f); // 2
fmaximum_num(qNaN, 2.f); // 2
fminimum_num(2.f, qNaN); // 2
fmaximum_num(2.f, qNaN); // 2
fminimum_num(-0.f, +0.f); // -0
fmaximum_num(-0.f, +0.f); // +0
fminimum_num(+0.f, -0.f); // -0
fmaximum_num(+0.f, -0.f); // +0

fminimum/fmaximumおよびfminimum_num/fmaximum_numはIEE754にある同名操作(頭のfを省いたもの)に対応する関数で、C23で追加されたものです。どちらの関数も-0 < +0となり、fminimum/fmaximumはqNaN入力に対してエラー伝播としてqNaN引数を返し、fminimum_num/fmaximum_numはqNaNを欠落したデータとして扱いもう片方の値を返します。

Cのこれらの関数はIEEE754の規定によく従った振る舞いとなります。

また、std::min/std::maxは現在のGPUのISAにおける浮動小数点数比較命令の結果とも一貫していません

ベンダ ISA 命令 対応 signed 0 aNaN
AMD CDNA2+ MIN/MAX minimum_num/maximum_num -0 < +0 Mssing Data
intel Xe ISA AOP_FMIN/AOP_FMAX minimum_num/maximum_num -0 < +0 Mssing Data
NVIDIA PTX atom red minimum_num/maximum_num -0 < +0 Mssing Data
SPIR V OpAtomicFMinEXT/OpAtomicFMaxEXT C fmin/fmax 同値、QoIとして-0 < +0 Mssing Data

この結果を受けて、C++における浮動小数点数型のstd::atomicfetch_max()/fetch_min()の設計指針は2つあり、std::min/std::maxと一貫させるかどうかです。両選択肢の比較は次のようになります

カテゴリ 一貫させる 一貫させない
x.fetch_min(y);
x.fetch_fminimum_num(y);
x.fetch_min(y, std::less{});
x.fetch_min(y);
利点 セマンティクスの一致
一貫性
安全なセマンティクスがデフォルト
ハードウェア命令のデフォルト
欠点 minという一般的な名前がポータブルではない振舞いをする
一般的な名前の処理がパフォーマンス的に不利になる
同名関数との非一貫性
atomicに移行する際の微妙な挙動の違い
教育の必要性 間違った使用とパフォーマンス 浮動小数点数型の微妙な動作変更
デフォルト 安定性 正しさとパフォーマンス
オプトイン 正しさとパフォーマンス 安定性

この提案では、浮動小数点数型のstd::atomicfetch_max()/fetch_min()std::min/std::maxとは異なるセマンティクスを提供することを提案しています。また、既存のAPIとの非一貫性を和らげるために、C23のfminimum/fmaximumおよびfminimum_num/fmaximum_num<cmath>に追加しstd名前空間で利用できるようにすることも提案しています。

P3009R0 Injected class name in the base specifier list

クラステンプレート内での特殊化名を指すクラス名を基底クラスリストでも使用可能にする提案。

クラステンプレート内部において、そのクラスの名前は現在のクラステンプレートの特殊化名を指しています。これは、クラスの内部でのみ使用可能であり、特に基底クラス指定の場所では使用できません。

template<typename D>
class crtp_base {...};

template<typename T, typename U, typename V = int>
class sample
  : crtp_base<sample>       // ng、sampleはテンプレート名
  , crtp_base<sample<T, U>> // ok、これでクラス名、ただしVはデフォルト
  , crtp_base<sample<T, U, V>> // ok、全てのテンプレートパラメータの指定を反映している
{
  // このクラス内部でのsampleの名前は、sample<T, U>としたのと同じになる
  sample* p;  // ok

  void f() {
    const sample& r = *this;  // ok
  }

  // ここでも使用可能
  friend bool operator==(sample, sample) = default; // ok
};

このような名前のことを規格用語ではinjected-class-name(注入されたクラス名)と言います。

基底クラスで注入されたクラス名が使用できないことにより、主にCRTPパターンの記述時にその記述が冗長かつ複雑になります。この例のように短いテンプレートパラメータ名が小数だけならさほど変化はありませんが、標準のコンテナ型のように多様なテンプレートパラメータを取る場合に問題は大きくなります。

また、注入されたクラス名が使用できない場所でそのつもりでクラス名を使用しても、テンプレート名でしかないことから通常エラーになりますが、テンプレートパラメータにデフォルトパラメータが指定されている場合はそのパラメータについての指定を忘れていたとしてもエラーにはなりません。これはともすれば見つけづらいバグの元になる可能性があります。

Deducing thisのおかげでCRTPを記述する必要性は大きく減少していますが、C++20以前の環境でも使用されるコードなどにおいて依然としてCRTPを使用したいケースは残っています。そのため、この提案はクラス定義における基底クラスリスト内部でも注入されたクラス名を使用可能にしようとするものです。

ただし、クラステンプレートの基底クラスリストにおけるそのクラスの名前は、テンプレートテンプレート名としては有効であるため、基底クラスがテンプレートテンプレートに対して部分特殊化していたり、テンプレートテンプレートに対して動作が変わるような記述をしていると、現在有効なコードがコンパイルエラーとなるようになります。

struct WasType {};
struct WasTemplate {};

template <typename Type>
auto foo() -> WasType; // overload 1

template <template <typename...> class Template>
auto foo() -> WasTemplate; // overload 2

template <typename Type>
struct CurrentlyUnambiguousBase
  : decltype(foo<CurrentlyUnambiguousBase>()) // 現在はok、overload 2が選択される、この提案ではエラー
{
  // 現在でもエラー、オーバーロード解決に失敗
  using InsideBody = decltype(foo<CurrentlyUnambiguousBase>());
};

// 現在の動作
static_assert(std::is_base_of_v<WasTemplate, CurrentlyUnambiguousBase<void>>);

さらに巧妙なコードを考えると、エラーにせずに動作を変更することもできます。

struct WasType {};
struct WasTemplate {};

template <typename Type>
auto bar(int) -> WasType; // overload 1

template <template <typename...> class>
auto bar(long) -> WasTemplate; // overload 2

template <typename Type>
struct DifferentBehavior
  : decltype(bar<DifferentBehavior>(0)) // overload 2が選択される、この提案ではoverload 1が選択される
{
  using InsideBody = decltype(bar<DifferentBehavior>(0)); // overload 1が選択される
};

// 現在の動作
static_assert(std::is_base_of_v<WasTemplate, DifferentBehavior<void>>);
static_assert(std::is_same_v<WasType, DifferentBehavior<void>::InsideBody>);

先ほどの例はクラス名が注入されたクラス名としても扱われるようになることで2つの関数の間でオーバーロード解決が失敗していましたが、この例では追加の引数の一致によって順序がつく(0longよりもintによりマッチする)ことによってエラーにならずに選択される関数が変化します。

これらのコードは標準仕様としては曖昧ではなく明確であったとしてもコードの読者にとっては既に曖昧であり、2つ目の例などは1行場所が異なるだけで異なることをしているのはほとんどの読者が気づかないものであるとして、このような例は考慮しないことを提案しています。

EWGのレビューにおいては消極的な推進の合意が得られており、実装経験を求めています。

P3010R0 Using Reflection to Replace a Metalanguage for Generating JS Bindings

P2320の値ベースリフレクションを利用して、Javascriptバインディングを記述した経験の報告書。

この文書では、Bloomberg社内で使用されているROBというC++プリプロセッサ言語フレームワークをP2320で提案されている値ベースリフレクションを用いて書き換え、その際に得られた経験を報告するもので、主に値ベースリフレクションの使用経験や改善案を報告することを目的とするものです。

ROBはBloomberg社内でCで記述されたアプリケーションサーバの処理のために使用されており、.robファイルに書かれたC++クラスに似た記述によるクラス定義をパースしてそこから実行時にJavascriptのクラスにマーシャリングを行うために必要な情報を抽出し、C++のクラス定義や関連するボイラープレートコードとともに生成して.cppと.hファイルに保存します。生成された.cpp/.hファイルをコンパイルすることで、実行時にその型のオブジェクトとJavascriptのクラスオブジェクトの間で相互に変換する処理が利用可能になります。

つまりは、C++コードとしてコンパイルする前に.robファイルをコンパイルするプログラムが必要となります。この文書では、現在使用されているROBをベースにrob2というフレームワークを試作し、そこで値ベースリフレクションによってそのような前処理を純粋なC++コードで記述したものです。

その使用経験をもとに、meta::infoが関数引数で渡した時に定数式の文脈で使用不可能になること(constexpr引数の必要性)やあらゆるリフレクション情報がmeta::infoに畳まれてしまうことによってある時点のmeta::infoオブジェクトが何のどのような情報を保持しているのかわからなくなる点などの使いにくさや、ユーザー定義属性の必要性などについて報告しています。

P3011R0 Supporting document for Hive proposal #1: outreach for evidence of container-style use in industry

std::hive(元plf::colony)の標準への導入のために、コンテナの実使用実体を探るためのアンケートとその結果を記載した報告書。

LEWG/SG14の一部のメンバはstd::hiveのようなコンテナが本当に実使用されている、あるいは必要とされているのかに疑問を持っている人がいるようで、彼らを納得させるためとstd::hiveの標準導入へのサポートのために、C++を使用している企業やコミュニティに向けてアンケートを取りました。この文書は、その結果を報告するものです。

次のようなアンケートを、企業へメール送信、reddit/discordでポスト、その他ゲーム開発系Webコミュニティへポスト、の形でアンケートを募りました

  1. 業務において次のようなタイプのコンテナを使用していますか?
    • 複数のメモリブロックまたは単一のメモリブロック内のシーケンシャルストレージ
    • 要素は削除時に何かしらのマーキングがなされ、マーキングされた要素はイテレーション中にスキップされる
  2. 1がyesの場合、そのコンテナは複数のメモリブロックからなるのか、単一であるか?
    • また、可能であれば何に使用していますか?
  3. plf::colonyもしくは提案中のstd::hiveを知っていますか?
  4. このタイプのコンテナが標準化されることで、メリットがあると思いますか?
  5. その他質問やコメントがあれば

このアンケートに対して、企業メールは8社から、discordは4人、redditは11人、その他Webコミュニティ(TIGsource)は1人の回答が得られました。

1の回答

  • email: 5/7 yes
  • discord: 2/4 yes
  • reddit: 6/11 yes
  • TIGsource: 1/1 yes

2の回答

  • Email: 3 multiple, 2 singular, 1 both.
  • Discord: no responses
  • Reddit: 1 multiple, 3 singular, 1 both.
  • TIGsource: 1 singular.

3の回答

  • email: 4 yes, 3 no.
  • Discord: 省略
  • Reddit: 省略
  • TIGsource: 投稿漏れ

4の回答

  • email: 6 yes, 1 yes but in the future.
  • Discord: 省略?
  • Reddit: 省略?
  • TIGsource: 1 no opinion

得られた回答数はあまり多くないですが、筆者の方(std::hive提案者)の経験や実感と一致しているとのことです。

文書には、得られたコメントなどが詳細に記録されています。

P3012R0 Supporting document for Hive proposal #2: use of std::list in open source codebases

std::hive(元plf::colony)の標準への導入のために、std::listとの比較やstd::listの使用実態を報告する文書。

std::listは要素の挿入・削除に伴って他の要素の移動が発生せず安定しており、それによってポインタやイテレータの安定性を保証しているコンテナです。ただし、std::vectorstd::dequeと異なり、要素はメモリ上でバラバラに配置されており、要素の挿入時は個別のリストノードが割り当てられ、削除時はそのノードごと削除されます。これによって、std::vector/std::dequeに比べてイテレーションと要素追加のコストが高くなります。

std::hiveは両者のいいとこどりをするような性質を持つコンテナで、要素はブロック(大きな配列)内に割り当てられ、削除時は破棄された後でそのブロック内位置が削除済みとしてマーキングされるだけです。これによって、要素はメモリ上でかなり隣接(std::vector程ではないにせよ)することになりイテレーションパフォーマンスが向上し、要素ごとにノードを確保・削除しないため要素の挿入時のコストが低下しコンテナ全体でのメモリ使用量が削減できます。

std::hiveは標準コンテナに準じたインターフェースを持っているため、現在std::listを使用しているところをstd::hiveで置き換えることでそのようなコードのパフォーマンスが向上しメモリ使用量が低下するはずです。

この文書は、std::listとの比較によってその効果を示すとともに、大規模なオープンソースコードベースでどれくらいstd::listが使用されているか(std::hiveによってどれだけパフォーマンス改善が見込めるか)を報告するものです。

10から初めて1000000まで1.1倍しながら5種類のデータ(1, 2, 4, 40, 490 [byte])を挿入・削除・イテレーションした時のパフォーマンスの差は次のようになったようです

  • 挿入(単一要素)のパフォーマンスはstd::listよりも最大520%高速
    • 型のサイズが小さいほど差がついた
  • 消去のパフォーマンスは、最大71%高速
    • 型のサイズが大きいほど差がついた
  • イテレーション(25%の要素を削除した後)のパフォーマンスは、大きな型と大きな要素数(~1000)で最大50%高速
    • CPUのキャッシュに収まりきらなくなると(要素数1000越えくらい)最大218%高速

そのうえで、Githubのトレンド等から合計13件の大規模なオープンソースC++コードベースを調査したところ、13のうち7プロジェクトでstd::listが使用されていました。1件(libreoffice)を除いて要素安定を目的として使用しているかどうかは不明ですが、中にはspliceを使用してリストの結合や移動を行っているものもあったようです。

これらのプロジェクトでは標準にあるからstd::listを使用しているものと思われるため、std::hiveを標準に導入することで切り替えることができるようになり、先に示したようなパフォーマンスの向上が見込めます。

P3014R0 Customizing std::expected's exception

std::expected<T, E>::value()が投げる例外をカスタマイズする方法を提供する提案。

P0260R7で提案されている並行キューではそのAPIとしてstd::filesystemと同様のerror_codeによるAPIを用意することで無例外インターフェースを提供しています。

P2921R0ではerror_codeによるAPIの代わりにstd::expectedを使用したAPIが検討されましたが、それが現在のAPIに比べて明確に優れているとは言えないと報告されています。

// P0260の現在のAPI
void push(const T&);  // 失敗したら例外を投げる
bool push(const T&, error_code& ec);  // 無例外

// std::expectedによるAPI
auto push(const T&) -> expected<void, conqueue_errc>;

無例外APIとの比較

P0260R7 `std::expected`
std::error_code ec;
if (q.push(5, ec)) {
  return;
}
println("got {}", ec);
if (auto result = q.push(5)) {
  return;
} else {
  println("got {}", result.error());
}

例外を投げるAPIとの比較

P0260R7 `std::expected`
q.push(5);
  ...
catch(const conqueue_error& e)
// Awkward use.
q.push(5).or_else([](auto code) {
  throw conqueue_error(code);
});
  ...
catch(const conqueue_error& e)

// Awkward exception type.
q.push(5).value();
...
catch(const bad_expected_access<conqueue_errc>& e)

この提案は、std::expected.value()が投げる例外をカスタマイズできるようにすることで、std::expectedを使用するAPIの使用感を改善するものです。

P0260R7 この提案
q.push(5);
  ...
catch(const conqueue_error& e)
q.push(5).value();
...
catch(const conqueue_error& e)

この提案ではstd::expected_traits<E>というクラステンプレートを追加して、.value()の動作をこれによって変更することを提案しています

namespace std {

  // デフォルト実装、bad_expected_accessを投げる
  template <typename E>
  struct expected_traits {

    [[noreturn]]
    static void throw_error(E e) {
      throw std::bad_expected_access<E>(std::move(e));
    }

  };
}

現在のstd::expected.value()は有効値を保持していない場合にstd::bad_expected_access(error())throwして終了しますが、この提案ではその代わりにstd::expected_traits<E>::throw_error(error())を実行するようにすることを提案しています。

std::expected<T, E>Eについてこのstd::expected_traitsを特殊化し、.throw_error()をカスタマイズしておくことで、エラー型Eによって投げられる例外を変更することができます。

P3015R0 Rebuttal to Additional format specifiers for time_point

P2945R0で提案されている%Sの変更に反対する提案。

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

P2945R0の提案2では、time_point値に対するフォーマット指定の一つである%S指定の動作を秒数を2桁整数で出力するものに変更しようとしています。現在は秒単位未満の値を全て10進少数として一緒に出力する動作のため、これは破壊的変更となります。

この提案は、既存のコードの実行時の動作を変更するとバージョンアップに伴って既存のコードが実行時エラーやセキュリティリスクに見舞われる可能性があり、C++の標準は過去のバージョンに対して下位互換性を提供するという重要な約束も失われるとして、P2945R0の提案に反対するものです。

ただし、反対しているのは%S指定の動作を変更するというその部分だけで他の提案に関しては反対していません。

またこの提案では、<chrono>ライブラリが精度に中立に設計されていることから現在の%Sの動作はそれと一貫しているとも指摘しています。

提供される値の精度を決めるのはOSで、その値を使用するときの精度を決めるのはクライアントであり、<chrono>は両者の仲介を行うAPIに過ぎ無いため<chrono>ライブラリの中で勝手に精度を変更してしまうような機能はそもそもの<chrono>の設計趣旨に反しています。

%Sの指定はフォーマット時は対象のtime_point型からその精度が提供され、パース時は入力時刻文字列からその精度が提供されるという対称性があります。フォーマット時のみその動作を変えることはこの対称性も破壊することになります。

P2945R0で疑問が提起されていた時・分単位の出力(%H %M)が秒単位を考慮しないのはなぜか?という問いに対しては、時・分・秒という単位はそれぞれ同時に確立されたわけではなく、紀元前1500年ごろからの長い歴史の中で、人類の科学技術の発展に伴って段階的に導入されたものであり、時・分の単位が下位単位の値を気にしないのは歴史的経緯でありライブラリ設計の問題ではない、としています。

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

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

まず、std::valarrayに対してstd::beginstd::cbeginを呼んだ場合の挙動に違いがあります(libstdc++以外)。

#include <iterator>
#include <valarray>

int main() {
  std::valarray<int> v = {1,2,3};
  std::begin(v);  // OK
  std::cbegin(v); // Error
}

std::valarrayは非メンバのstd::beginオーバーロードを備えており、それが呼び出されています。

一方、std::cbeginにはstd::valarray用のオーバーロードが無いためプライマリのstd::cbeginが使用され、そこではADLによってconststd::valarrayに対してbegin()を探索しますが、std::cbegin<iterator>ヘッダで定義されているためそのコンテキストからはstd::valarrayの定義が見つからずにエラーになります。

<valarray>ヘッダが<iterator>をインクルードしているため、ユーザー側でインクルード順を入れ替えてもこの問題は解決しません。

次に、ほぼ同様の問題が初期化子リストに対してもあります。

#include <iterator>

int main() {
  std::begin({1,2,3});  // OK
  std::cbegin({1,2,3}); // Error
}

初期化子リスト({...})そのものには型が付かないため、std::cbeginの呼び出しでconst C&に束縛することができずにエラーになります。しかし、初期化子リストからinitializer_list<E>に変換が可能であるため、std::beginの場合はinitializer_listオーバーロードが選択されることで呼び出し可能になっています(initializer_listには非メンバstd::beginはあるがstd::cbeginはない)。

とはいえ、std::begin({1,2,3})std::end({1,2,3})は背後にある配列が異なる可能性があるため必ずしも範囲を形成せず、単にダングリングイテレータを返しいるため、このコードは意味がないどころか危険です。

この提案はこのstd::beginstd::cbeginに対する一貫しない動作について、std::valarrayの方は機能するように、初期化子リストの方は機能しないように修正するものです。

この提案では、std::valarray及びstd::initializer_listの双方からフリーのstd::begin/std::endオーバーロードを削除し、std::valarrayに対してはメンバのbegin/endconstオーバーロードも含めて追加します。また、std::initializer_listに対してメンバの.data().empty()を追加し、<iterator>ヘッダに定義されているinitializer_listオーバーロードを削除しています。

これらの変更によるコードの動作の変更は次のようになります

現在 この提案
#include <initializer_list>


void f(std::initializer_list<int> il) {
  auto it = std::begin(il);
}

struct S {

  S(std::initializer_list<int> il) :
    S(il.begin(), il.size()) {}

};

auto dangle = std::begin({1,2,3});  // ok
#include <initializer_list>
#include <iterator> // for std::begin

void f(std::initializer_list<int> il) {
  auto it = std::begin(il);
}

struct S {

  S(std::initializer_list<int> il) :
    S(il.data(), il.size()) {}

};

auto dangle = std::begin({1,2,3});  // ng
現在 この提案
#include <valarray>
#include <utility>

std::valarray<int> va;
auto it = std::begin(std::as_const(va));
#include <valarray>
#include <iterator>

std::valarray<int> va;
auto it = std::cbegin(va);

initializer_listにフリー関数のstd::begin/std::endオーバーロードが用意されているのは、当初の範囲for文がフリー関数版のbegin/endしか考慮しなかったためのようです。しかし、C++11の最終仕様ではメンバ関数の方を優先して呼び出すようになったためC++11時点で不要になっていたようです。

P3018R0 Low-Level Integer Arithmetic

低レベルかつ扱いやすい整数演算ライブラリ機能の提案

C++組み込みの整数演算はC言語から受けつがれたもので非常に長い歴史がありほとんど変更されることなく今日に至っています。C言語誕生当時やC++誕生当時の整数演算の要件は現在とは大きく異なっており、さまざまな実装における整数演算および整数の表現について共通の抽象化を見出すことにが主な課題でした。

一方、現在の整数演算の環境はかなり均質化されており、C++20でもそれを反映して符号付き整数型の表現が2の補数であることが規定されました。ただし、これを反映して組み込みの整数演算のセマンティクスを変更するの下位互換性の問題から困難です。

最も問題なのは、C++言語内で整数演算を実行する方法がそのような組み込みの整数演算しかないことです。これによって、基底のハードウェアによって提供される整数演算と一致するセマンティクスによって整数演算を行う必要のあるライブラリ作成が困難になっています。

例えば、ほぼ全ての整数演算がオーバーフロー時に未定義動作となるため、C++でオーバーフロー対応整数演算を記述する方が同等のコードをアセンブラで直接記述するよりも複雑になリます。その結果得られるC++コードは人間にとってもコンパイラにとっても理解するのが難しいコードになります。

#include <limits>

struct Result {
  int64_t sum;
  bool overflow;
};

Result safe_add(int64_t a, int64_t b) {
  using lim = std::numeric_limits<int64_t>;

  const auto max = lim::max();
  const auto min = lim::min();
  
  bool overflow = false;

  if (a >= 0) {
    if (max - a < b) {
      overflow = true;
    }
  } else {
    if (b < min - a) {
      overflow = true;
    }
  }

  return overflow ?
    Result{ .sum = 0, .overflow = true } :
    Result{ .sum = a+b, .overflow = false };
}

例えばこのコードのコンパイル結果(x86-64)は次のようになります

safe_add(long, long):
  test rdi, rdi
  js .L2
  movabs rax, 9223372036854775807
  sub rax, rdi
  cmp rax, rsi
  jl .L6
.L4:
  xor ecx, ecx
  lea rax, [rdi+rsi]
  movzx edx, cl
  ret
.L2:
  movabs rax, -9223372036854775808
  sub rax, rdi
  cmp rax, rsi
  jle .L4
.L6:
  mov ecx, 1
  xor eax, eax
  movzx edx, cl
  ret

整数加算はハードウェア(x86-64に限らず)でハンドリングされ、オーバーフローチェックも後から簡単に行うことができます。整数演算のオーバーフローは現代の多くのハードウェアでは未定義動作ではなく予期される現象であり、ハードウェアはそれをハンドリングすることができます。しかしC++の整数演算セマンティクスはそれを反映し活用することができません。また、現在C++コンパイラはこの例のようなC++コード上での整数演算のオーバーフローチェックパターンを認識することができず、それをハードウェアの命令に直接的にマップすることができません。

この提案は、現在の整数演算を取り巻く環境を反映した整数演算のセマンティクスを持つ整数演算APIをライブラリ機能として追加することで、整数演算に大きく依存する処理やライブラリの記述のための基礎部品として提供することを目指す物です。特に、提案する整数演算機能は未定義動作を起こしません。

提案するAPIを用いると先ほどの例は次のように書き直されます

#include <integer>

struct Result {
  int sum;
  bool overflow;
};

Result safe_add(int64_t a, int64_t b) {
  auto const [sum, overflow] = std::integer_i64_add_overflow(a, b);

  return Result{ .sum = sum, .overflow = overflow };
}

そしてこのコンパイル結果(x86-64)は例えば次のようになります

safe_add(long, long):
  mov rax, rdi
  mov edx, 0
  add rax, rsi
  seto dl
  ret

この提案のAPIはハードウェアが持つ整数演算の能力を直接的に活用することができ、その結果はほぼハードウェアの持つ整数演算命令と対応します。

この提案では、Rust/Swiftの持つ同様のライブラリAPIおよびC++コンパイラの組み込み関数を参考にし、さらにx86-64/ARM64/RISC-Vの対応する命令の振る舞いについても検討しています。それによって必要と思われるAPI機能を特定し、またそれらに優先順位をつけてリストアップしています。

  1. 基礎的な操作であり、全てのハードウェアでサポートされているため実装が簡単なもの
    • add_overflow
    • sub_overflow
    • mul_overflow
  2. 実装が簡単だがそれほど有用ではない可能性があるもの
    • div_overflow
    • rem_overflow
    • neg_overflow
  3. 一部のユースケースで有用だが、実装が難しく実装経験やフィードバックが必要なもの
    • mul_wide
    • div_wide
    • div_rem_overflow
    • add_with_carry
    • sub_with_borrow
    • mul_with_carry

これらの名前をベースとして、この提案では関数テンプレート(どの整数型でその操作が使用可能かが分かりづらい)やオーバーロードオーバーロード解決ルールを回避して実装するのが難しい)を避けて、integer_プリフィックスとして整数型に対応する略称(i8_, u8_, i16_, u16_, i32_, u32_, i64_, u64_のいずれか)を付加した個別の関数とすることを提案しています。

例えば

  • std::integer_i64_add_overflow()
  • std::integer_i8_neg_overflow()
  • std::integer_u32_mul_with_carry()

などになります。

これらの関数の戻り値型はその関数名に_resultを付加した集成体型となり、計算結果の値と操作ごとの追加情報をまとめて返します。

例えば次のようになります

// <integer>で定義
namespace std {

  struct integer_i32_add_overflow_result {
    int32_t sum;
    bool does_overflow;
  };

  integer_i32_add_overflow_result integer_i32_add_overflow(int32_t a, int32_t b);
}

提案する整数演算ライブラリは例えば、安全な整数演算ライブラリや標準のものよりも大きな幅の整数型ライブラリなどの実装の基盤として使用することができ、また、将来的にそのような機能が標準ライブラリに導入されるときの基盤となることができます。前述のように、このようなライブラリを現在のC++環境で書こうとすると純粋なC++コードでは困難となるため、環境ごとの組み込み関数やアセンブラを直接記述することになります。

SG6のレビューではこの提案の目指す方向性については全会一致で合意されており、先んじてC++26に導入されている類似機能である飽和演算ライブラリ機能(P0543)とAPIを一貫させた上でLEWGに提出することが決定されています。

P3019R0 Vocabulary Types for Composite Class Design

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

クラスの実装の一部がそのクラスの内側にない場合(つまりヒープメモリ等他の場所に配置されているオブジェクトを保持する場合)、多くの場合ポインタによって間接的に保持することになります。スマートポインタを使用すればその際の領域解放忘れを防ぐことができますが、それを保持するクラスに対して幾つかの問題をもたらします。

  • 特殊メンバ関数の実装伝播の阻害
  • 深いコピーの実装
    • デフォルトのコピーは禁止か浅いコピーになるため、コピーコンストラクタの実装などを通して手動で実装する必要がある
  • const伝播の阻害
    • ポインタのconst性はその参照先のconst性とは無関係
    • クラスのメンバ関数const性がポインタの先まで伝播しない

すなわち、ポインタはスマートポインタも含めて参照セマンティクスを持つため、クラスのメンバとして保持する場合に使いづらい側面があります。

この提案は、ポインタに代わってヒープ領域上に構築されたオブジェクトを扱うための値のセマンティクスを持つ2つのクラス型、indirect<T>polymorphic<T>を提案するものです。

indirect<T>はヒープ領域上のTのオブジェクトを表現するクラス型で、polymorphic<T>はヒープ領域上のTの派生クラス型オブジェクトを表現するためのクラス型です。どちらも次のような性質を持ちます

  • 特殊メンバ関数Tのものになるべく従う
    • デフォルトコンストラクタ : Tがデフォルト構築可能なら利用可能
    • コピーコンストラクタ/代入演算子
      • indirect<T> : Tがコピー可能なら利用可能
      • polymorphic<T> : 無条件で利用可能
    • ムーブコンストラクタ/代入演算子 : 無条件で利用可能
    • デストラクタ : 保持するオブジェクトを破棄する
  • 値のセマンティクスを持つ
    • コピー可能な場合、コピーはディープコピーとなる
      • 新しい領域を確保し、その領域に元のオブジェクトをコピーする
    • constメンバ関数const Tオブジェクトにアクセスする
    • indirect<T>は比較演算子ハッシュ関数Tで利用可能なら利用可能
  • 動的に確保した領域の所有権を保有する
    • 標準コンテナ同様のアロケータインターフェースによって確保戦略のカスタマイズが可能

宣言の例

namespace std {

  template <class T, class Allocator = std::allocator<T>>
  class indirect {
    T* p_; // exposition only
    Allocator allocator_;  // exposition only
  public:
    ...
  };

  template <class T, class Allocator = std::allocator<T>>
  class polymorphic {
    control_block* control_block_; // exposition only
    Allocator allocator_; // exposition only
  public:
    ...
  };
}

polymorphic<T>の場合はコンストラクタでTの派生オブジェクトを受け取って、型消去機構を通して保持します。

どちらのクラス型もムーブ後にnullに相当する状態になります。これはどちらもムーブの実装を簡易かつ効率的にするためです。ただし、この状態をユーザーが通常観測することは意図していないためoperator boolのようなものは提供されておらず、valueless_after_move()という専用のメンバ関数でそれをチェックできるようにしています。また、この性質により、両クラス型に対してstd::optionalの特殊化が提供され、余分な領域を使用しないようにしています。

これらのクラス型はそれぞれ以前に個別の提案(P1950P0201)に分かれて提案されていましたが、このようにその設計が非常に似通っているためこの提案でまとめて提案されることになりました。

P3020R0 2023-09 Library Evolution Poll Outcomes

2023年9月に行われたLEWGの投票の結果を報告する文書

投票にかけられた提案は次のものです

全てC++26に向けてLWGに転送されています。文書では、投票の際に寄せられたコメントが記載されています。

P3021R0 Unified function call syntax (UFCS)

メンバ関数呼び出しを拡張するUFCSの提案。

C++には大きく分けるとメンバ関数呼び出し(x.f(), x->f())と非メンバ関数呼び出し(f(x))の2つの形の関数呼び出し構文があります。これら2つの間に互換性はなく、どちらかの呼び出しがもう片方にフォールバックすることはありません。そのため、コードを書く際に呼び出したい関数がメンバ関数なのかそうでないのかを認識しながら書かなければなりません。

それによって、ジェネリックコードで関数呼び出しが必要となる場合に何かしらの分岐が必要となり、現在のC++では真にジェネリックなコードを記述することができなくなっています。

この問題は以前から認識されており、解決のために一様関数呼び出し構文(Unified function call syntax : UFCS)の提案がいくつも提出されてきました。提案された機能は非メンバ関数呼び出しを拡張してメンバ関数を呼び出せるようにするものでした。

この提案はそれら既存の提案とは逆方向に、メンバ関数呼び出し構文(x.f(a,b)x->f(a, b))を一般化して、非メンバ関数の呼び出し(f(x, a, b)f(*x, a, b))にフォールバックすることを提案するものです。

この提案は現在認識されている次のような問題を解決します

  1. ジェネリックコードにおける呼び出し構文の汎用化を達成する
    • 単一の構文によってメンバ/非メンバ関数を呼び出すことができる
  2. ライブラリの回避策やそれにしか使用できない言語機能を用意することなくメソッドチェーンを可能にする
    • <ranges>|演算子やそれを改善することを目指す|>等は必要なく、.によってチェーンできる
  3. それにしか使用できない言語機能を用意することなく拡張メソッドを可能にする
    • この提案の.拡張は自然に拡張メソッドを可能にする
    • 既存のコードに手を入れることなく有効化され、この提案の機能は拡張メソッドのためだけのものではない(むしろその効果はおまけ)

さらに次のような恩恵があります。

  1. 一貫性・単純さ・教えやすさの向上
    • 関数呼び出し構文が1つになるため言語がシンプルになり教えやすさが向上する
    • メンバ関数呼び出しも引き続き有効だが次善の機能となる(引数なしの非メンバ関数呼び出しを除く)
  2. 既存のコードの発見しやすさと使いやすさを向上させる
    • この提案は既存のC++ライブラリコードを呼び出すとすぐに(何の変更も必要とせず)機能する
    • C標準ライブラリを含むCライブラリについても同様
    • これによって、既存のライブラリを学習して使用するための強力な方法となる
  3. ツールサポートの改善
    • C++での作業をより便利にする強力な新しいツール機能の作成を直接支援する
    • 既存のツール、特にコードのオートコンプリート機能(VSのIntelliSenseなど)を強化する

2に関しては例えば、Cのライブラリ機能を使用する既存のコードを次のように改善します

// 現在のC code
void f( FILE* file ) {
  fseek(file, 9, SEEK_SET); // 引数は関数名の後にあるため、コード補完は機能しない
}

// 提案によるC++コード
void f( FILE* file ) {
  file.fseek(9, SEEK_SET); // .を入力するとコード補完が候補を表示する
}

この時、エディタのオートコンプリート機能が効いていれば、使用したいライブラリの関数名を知らない/覚えていない状態からでもオートコンプリート機能がサジェストする候補から使用する関数を選択し、また他に使用できる機能を探すことができます。C++のコードにおいてはそれは現在メンバ関数に限定されており、Cのコードに対しては機能していません。

このような例はC++にも存在しており、一番目立つのは配列からのイテレータ取得時でしょう。

template<typename T, std::size_t N>
void f(T(&array)[N]) {
  using namespace std::ranges;
  // 現在
  auto it = begin(array);

  // この提案
  auto it = array.begin();
}

メソッドチェーンはその記述と処理の流れが一致することで視覚的に見やすくなり、また上記のようなコード補完による恩恵を受けやすいなどのメリットからC++言語内だけではなく他言語でも好まれる傾向にあります。

first().second().third().fourth();  // 評価と実行の順序と一致する
fourth(third(second(first())));     // 評価と実行順序と逆になる

<ranges>ではこれを解決するために|演算子オーバーロードして使用しています。これはこの提案の.演算子オーバーロードによってシミュレートしているものであるため、この提案の.によって置き換えることで同様に機能します。

// 現在
std::ranges::for_each( in | std::views::transform(rot13), show );
std::ranges::for_each( in | transform(rot13), show ); // with ‘using std::views’

// この提案
std::ranges::for_each( in.std::views::transform(rot13), show );
std::ranges::for_each( in.transform(rot13), show );   // with ‘using std::views’

また、この|演算子オーバーロードの複雑性解消のための言語サポートを行うためのUFCSとして、|>演算子の提案(P2672R0)が以前提出されていました。

using namespace std::ranges;

// HTMLタグの中身を抽出する
auto filter_out_html_tags(std::string_view sv) -> std::string {
  return sv
    |> transform(%, [](char c){ return c == '<' or c == '>'; })
    |> zip_transform(std::logical_or{}, %, % |> rv::scan_left(%, std::not_equal_to{}))
    |> zip(%, sv)
    |> filter(%, [](auto t){ return not std::get<0>(t); })
    |> values(%)
    |> to<std::string>(%);
}

これはプレースホルダがあるため少し手直しが必要ですが、ほぼそのままこの提案で書き直せます。

auto filter_out_html_tags(std::string_view sv) -> std::string {
  auto call = []<typename T, typename F>(T&& o, F&& f) {
    return std::forward<F>(f)(std::forward<T>(o));
  };

  return sv
    .transform([](char c){ return c == '<' or c == '>'; })
    .call([](auto const& x) { return zip_transform(std::logical_or{}, x, x.scan_left(true, std::not_equal_to{})); })
    .zip(sv)
    .filter([](auto t){ return not std::get<0>(t); })
    .values()
    .to<std::string>();
}

この提案の機能はここまで説明しているように|演算子オーバーロードを置き換えるためだけのものではなくより一般的なものであり、.でチェーンされていることによって視覚的にも優れています。

この提案の内容はcppfront(筆者の方のプロジェクト)で実装されているようです。

P3022R0 A Boring Thread Attributes Interface

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

OSの提供するスレッド作成APIではいくつもの追加のオプションがあり(例えばpthreadの場合は12以上)、現在のC++スレッドAPIはそのほとんど全てを指定することができません。そのため、将来的に指定可能にしたいスレッドのオプションはスタックサイズとスレッド名以外にも多数に及ぶ可能性があり、将来のバージョンで少しづつ増やしていった場合に関数インターフェースを複雑にせずになおかつABI破壊を避けるようにAPIを設計しなければなりません。

現在のところ、同様の機能はP2019で提案されています。

P2019R3では、ファクトリ関数によってスタックサイズとスレッド名を指定できるようにするAPIを提案しています。

void f(int);

int main() {
  // P2019R3のスレッドオプション指定例
  auto thread = std::jthread::make_with_attributes(
    []{ f(3); },
    std::thread_name("Worker"),
    std::thread_stack_size(512 * 1024)
  );
}

これに対して、この提案は20年以上に渡って実使用されてきたBoost.Threadの設計をベースとしたAPIを提案するものです。

void f(int);

int main() {
  // この提案のスレッドオプション指定例
  std::jthread::attributes attrs;
  attrs.set_name("Worker");
  attrs.set_stack_size_hint(512 * 1024);
  
  auto thread = std::jthread(attrs, f, 3);
}

このAPIに比べるとP2019R3のAPIには次のような使いづらい点があります

  • コンストラクタの代わりにファクトリ関数が使用される
  • スレッドの追加引数を取れないため、引数を渡す場合はラムダでラップする必要がある
  • スレッドオプションは型によってディスパッチされる

このAPIはBoost.Threadの設計をベースにしており、Boost.Threadが20年前に登場して以降に現れた多くのスレッドライブラリにおいても同様のAPIによってスレッドオプションを指定するようになっています。そのため、多くのプログラマはこの設計に慣れており、P2019R3の込み入ったAPIと比べてシンプルで使いやすいと主張しています。

std::jthreadにおける変更は次のようになります

namespace std {

  class jthread {
  public:
    // オプションを保持する内部クラス型を追加
    class attributes;
    
    // デフォルトコンストラクタ
    jthread() noexcept;

    // 追加 : オプション指定のみを受け取るコンストラクタ
    jthread(attributes&& attrs) noexcept;

    // 実行する関数とその引数を受け取るコンストラクタ
    template<class F, class... Args>
    explicit jthread(F&& f, Args&&... args);

    // 追加 : 実行する関数とその引数に加えてオプション指定を受け取るコンストラクタ
    template<class F, class... Args>
    explicit jthread(attributes&& attrs, F&& f, Args&&... args);

    ...
  
    // オプションを取得する関数を追加
    [[nodiscard]] std::string_view get_name() const noexcept;
    [[nodiscard]] std::size get_stack_size() const noexcept;

    ...
  };

  // オプションを保持するクラス型                
  class jthread::attributes {
    attributes() noexcept;                                
    ~attributes() = default;

  public:
    // set thread name
    void set_name(const char* name) noexcept;
    void set_name(std::string_view name) noexcept;
    void set_name(std::string&& name) noexcept;
    // set thread stack size hint
    void set_stack_size_hint(std::size_t size) noexcept;
  };
}

将来的にスレッドオプションを増やしたくなった場合は、同様のオプションクラス(jthread::attributes2など)とそれを受け取るコンストラクタを追加することで、ABIの破壊を回避しながら機能拡張ができます。

また、この提案ではstd::jthreadへの移行を促すためにもこのAPIstd::threadに対しては追加しないようにしています。

2023年11月のKonaで行われたLEWGのレビュー投票においては、この提案のAPIよりもP2019のAPIの方が好まれたため、この提案の追求は停止したようです。

P3023R0 C++ Should Be C++

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

昨今のC++を取り巻く状況や個人個人の標準化参画意図などから独立して、標準化委員会が目指すべき方向性や委員会に参加する際に持つべき心得みたいなものについて記述されています。

これは未完成版であり、R1が既に出ているのでそちらを見た方が良いでしょう。

おわり

この記事のMarkdownソース