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

文書の一覧

SG22のWG14からのものを除いて、全部で41本あります。

N4940 WG21 2022-11 Kona Minutes of Meeting V2

2022年11月7-12日にハワイのKonaで行われた、WG21全体会議の議事録。

N4933の改訂版です。

N4941 INCITS C++/WG21 Agenda: 6-11 February 2023, Issaquah, WA USA

2023年2月6-11日にアメリカのIssaquahで行われた、WG21全体会議の全体予定表。

N4942 WG21 2023-01 Admin telecon minutes

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

N4943 WG21 February 2023 Issaquah Minutes of Meeting

2023年2月6-11日にアメリカのIssaquahで行われた、WG21全体会議の議事録。

おそらく、初日と最終日に行われた全体の会議の議事録です。

N4944 Working Draft, Standard for Programming Language C++

C++23のワーキングドラフト第9弾。

N4945 Editors' Report - Programming Languages - C++

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

2月のIssaquah会議で採択された提案とコア言語/ライブラリのIssue解決が適用されています。

P0876R13 fiber_context - fibers without scheduler

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

以前の記事を参照

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

  • コンストラクタに渡す関数オブジェクトをdecay-copyするようにした
  • std::span<std::byte, N>を受け取るコンストラクタ(使用するメモリ領域を指定する)の引数をstd::span<std::byte>に変更
    • また、デリータを指定できるようにした
  • コンストラクタの例外条件について追記
  • 空ではない(何か処理が進行中の)fiber_contextが破棄された場合、std::terminate()を呼ぶことを規定
  • resume_with()を呼ぶと、すぐにempty() == trueとなることを明確化
  • 文言の簡素化のために、fiber_context::stateという説明専用メンバを導入
  • concurrency_v2名前空間を削除
  • Equivalent toという言葉をAs-ifに変更
  • 事前条件と適格要件を明確化

などです。

P1144R7 std::is_trivially_relocatable

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

以前の記事を参照

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

  • P2786R0の登場で提起された疑問点を追記
  • std::vector<T>::insert()の実装のための、std::uninitialized_relocate_backwardの追加
  • trivially relocatableな型の要件から、ムーブ構築可能と破棄可能を削除
  • relocationによって可能となる最適化について、既存の在野のライブラリ機能と比較
  • 2月のIssaquah会議での投票結果を追記
  • 背景や議論などの説明を削除
  • EASTLのrelocationは少し意味が異なっていたので、言及を削除

などです。

2月のIssaquah会議では、別にほぼ同様の概念を提案するP2786R0が提出されレビューされました。この提案の著者の方とそちらの著者の方は、合同で2つの提案をマージした提案を準備しているようです。

P1673R12 A free function linear algebra interface based on the BLAS

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

以前の記事を参照

このリビジョンでの変更は多岐に渡りますが、ほとんどが文章そのものもしくは提案する文言の調整や修正です。それ以外のところでは

  • 実装経験の追記
  • 機能テストマクロの追加(__cpp_lib_linalg
  • transposedは読み取り専用mdspanを返さなくなった
  • 値渡しのパラメータからconstを削除
  • vector_norm2vector_two_normへ変更
  • symmetric_matrix_rank_k_updatehermitian_matrix_rank_k_updateにはalpha scalingパラメータを取らないオーバーロードが追加された
  • {symmetric,hermitian,triangular}_matrix_{left,right}_product{symmetric,hermitian,triangular}_matrix_productへ変更
    • パラメータの順序によって、left,rightを識別するようにした
    • これによって、triangular_matrix_product(in-place right productの場合)は、(入)出力パラメータが例外的に最後の引数に現れなくなった
  • [in]out-{matrix,vector,object}では、要素型がconstであるかをチェックする代わりに、要素型が参照型に代入可能(可変参照で束縛可能)であるかをチェックするようにした
  • 複素数に関する操作について、*-if-neededのような名前の関数を追加して、カスタムの複素数型を使用可能なように調整
  • std::absが符号なし整数型に対して定義されていないことに対処するために、説明専用のabs-if-neededを追加し、std::absの代わりに使用
  • in-placeで上書きする三角行列と行列の(左/右の)積の場合、その関数名のleft/rightを復元し、入出力パラメータを常に末尾配置する。
    • これによって、in-placeの場合にのみ、triangular_matrix_left_producttriangular_matrix_right_productが復活した(4つ上の変更の再修正)

などです。

この提案はLEWGのレビューを一旦終えて、LWGに転送するための投票待ちをしています。

P1885R11 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更はLWGフィードバックを判定したことです(変更が大量ですが、設計の変更はないはずです)。

この提案は、LWGのレビューを終えて次の全体会議で投票にかけられる予定です。

P2022R1 Rangified version of lexicographical_compare_three_way

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

以前の記事を参照

このリビジョンでの変更は、筆者の方による実装コードへのリンクを追記した事です。

P2287R2 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、以前に提案していた基底クラスを指定して指示付初期化を行う構文を削除した事です。ただし、基底クラスの非静的メンバを指定するR1で追加された構文は引き続き提案されています。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  // R0で提案されていた構文、R2(このリビジョン)で削除
  B b1{:A = {.a = 1}, b = 2};
  B b2{:A{.a = 1}, b = 2};
  B b3{:A{1}, .b{2}};

  // R1で追加され、R2でも可能な構文
  B b4{.a = 1, .b = 2};
  B b5{.a{1}, .b{2}};
}

このリビジョンではb4, b5の形式のみがサポートされ、基底クラスを指名する形のb1, b2, b3の形式は提案されていません。

P2407R3 Freestanding Library: Partial Classes

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

以前の記事を参照

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

  • // freestanding-delete// freestanding-deletedへ変更
  • 一貫性のために、bad_optional_accessを追加
  • string_viewstarts_with/ends_withの文言を変更
  • // freestanding-deletedの使用例を追加
  • ヘッダへの指定のための、// mostly freestandingを追加

などです。

P2447R3 std::span over an initializer list

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

以前の記事を参照

このリビジョンでの変更は、主著者の変更と機能テストマクロを削除したことなどです。

P2530R3 Hazard Pointers for C++26

標準ライブラリにハザードポインタサポートを追加する提案。

以前の記事を参照

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

P2545R4 Read-Copy Update (RCU)

標準ライブラリにRead-Copy-Update(RCU)サポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更とLWGレビューのフィードバックを反映した事などです。。

P2630R3 Submdspan

std::mdspanの部分スライスを取得する関数submdspan()の提案。

以前の記事を参照

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

  • 機能テストマクロの追加
  • 集成体型に関する文言を修正
  • sub_map_offsetを使用される前に定義するように移動
  • integral-constant-likeintegral_constantの代わりに使用

などです。

P2690R1 Presentation for C++17 parallel algorithms and P2300

P2500(以前のP2690R0)の紹介スライド

P2500(P2690R0)については以前の記事を参照

どうやら、P2690R0は間違った番号を使用して公開されてしまったようで、P2690としてはこのスライドを公開しP2690R0はP2500R0として公開する予定だったようです。そのためP2690R0はP2500に修正され、その紹介スライド(このスライド)はP2690R1として公開されたようです。

このスライドでは、P2500(P2690R0)の内容をSG1やLEWGのメンバに簡単に解説するものであり、主にAPIの概要やその意図などを解説しています。

P2746R1 Deprecate and Replace Fenv Rounding Modes

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

以前の記事を参照

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

  • 説明文書の修正
  • WG14からのコメントの反映
  • フリー関数アプローチに焦点を当て、より具体的なAPIの概要を追記

などです。

R0ではcorrectly_rounded<F>という浮動小数点数のラッパ型を追加して、そのメンバ関数として正しい丸めを行う関数を追加することを提案していました。

このリビジョンでは、それを浮動小数点数型と丸めモード指定を直接受けて、その丸めモードの下で正確な計算を行うフリー関数を追加する方向に切り替えています。

namespace std {
  // 以下のcr_*関数はIEC 60559に完全に適合しているかを返す
  template<floating_point F>
  constexpr bool conforms_to_iec_60559();

  // 指定された丸めモードの下で四則演算を行う

  template<floating_point F>
  constexpr F cr_add(F x, F y, float_round_style r = round_to_nearest);
  
  template<floating_point F>
  constexpr F cr_subtract(F x, F y, float_round_style r = round_to_nearest);
  
  template<floating_point F>
  constexpr F cr_multiply(F x, F y, float_round_style r = round_to_nearest);
  
  template<floating_point F>
  constexpr F cr_divide(F x, F y, float_round_style r = round_to_nearest);
  
  // 浮動小数点数型 G -> Fへの丸めを伴う変換
  // sizeof(F) >= sizeof(G) の時丸めは正確になる(はず
  template<floating_point F. floating_point G>
  constexpr F cr_cast(G x, float_round_style r = round_to_nearest);
 
  // 浮動小数点数文字列をFの値に変換する
  // どのような文字列がサポートされるかは実装定義、サポートされない場合は例外をスローする
  template<floating_point F> consteval F cr_const(string s);

  // 指定した丸めモードの下で平方根を計算する
  template<floating point F>
  constexpr F cr_sqrt(F x, float_round_style r = round_to_nearest);
}

P2752R1 Static storage for braced initializers

std::initializer_listの暗黙の配列がスタックではなく静的ストレージに配置されるようにする提案。

以前の記事を参照

このリビジョンでの変更は、マングリングに関する議論を削除した事(問題とならなかったため)、GCC-fmerge-all-constantsに関する議論を追記したことなどです。

この提案の内容は、GCCがすでに-fmerge-all-constantsで有効化される独自拡張の一部として実装済みだったようです。

P2757R1 Type checking format args

std::format()のフォーマット文字列構文について、幅/精度の動的な指定時の型の検証をコンパイル時に行うようにする提案。

以前の記事を参照

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

  • basic_format_parse_contextコンストラクタの変更(削除)を元に戻し、check_dynamic_spec_arithmeticを削除
    • "arithmetic"型にboolcharを含めるようにした
    • これによって、check_dynamic_spec_integral()がその機能を包含する

などです。

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

P2780R0 Caller-side precondition checking, and Eval_and_throw

契約プログラミングのEval_and_throwモード実装のための動作(実装)モデルの提案。

P2698R0では、契約違反が起きた時に無条件終了するのではなく例外を投げて継続するモードであるEval_and_throwモードが提案されています。

このモードで問題となっているのはnoexceptとの兼ね合いで、Eval_and_throwモードでは契約がなされている関数は全て例外を投げる可能性があるため、その関数の(あるいはその関数に対する)noexceptがどうなるのかが問題となっています。

この提案は、契約条件(事前条件)のチェックが関数内部ではなく関数呼び出し側で行われるという動作モデルによって、この問題を解決しようとするものです。

Eval_and_throwモードとnoexceptの問題は、契約条件の評価がその関数の内側にあるという実行モデルに基づいています。そこで、契約条件(事前条件)のチェックは関数の呼び出し側で、関数呼び出しの前で行われるようにして、契約条件の評価と関数呼び出しを分離します。これによって、noexceptの扱いはこれまで通りとなり、noexcept指定されている関数で契約指定を行うことができるようになります。

この動作モデルでは、呼び出される側の準備や協力がなくても、完全に呼び出し側の責任範囲で契約のチェックを行うことができます。すなわち、呼び出される側の関数実態が別の翻訳単位にある時でも、翻訳単位間で何かを共有する必要はなく、呼び出し側で契約評価の有効/無効を切り替える時でも同じバイナリを使用し続けることができ、契約機能の状態によるABIの変化はありません。これによって、プレビルドバイナリを配布するようなライブラリでは契約機能の状態に応じた数のバイナリを配布するのではなく従来通りに1つのバイナリだけを配布し、そのAPIにおける契約の有効/無効はライブラリ使用者の任意で切り替えることができるようになります。

このことは、単にEval_and_throwモードの実装という枠を超えて契約機能全体にとって有用である可能性があります。この提案が採用されないにしても、契約機能がABIに影響を与えないように実装されることは非常に重要だと筆者の方は述べています。

この提案の動作モードは、単純なコードで示すと次のようなものになります

// 関数呼び出しは
f();

// このように書き換えられて実行される
((precond() ? nop() : violation()), f());

precond()とは事前条件チェックの全体であり、nop()は何もしないダミーの関数で、violation()は違反ハンドラの呼び出しです。violation()は評価されると、Eval_and_abortモードではstd::terminate()を呼び出し、Eval_and_throwモードでは例外を送出します。

このコードでは、violation()が呼び出されるとf()の呼び出しには到達しないため、violation()が例外を投げるかどうかはf()noexcept性に影響を与えません。そのため、noexcept関数でも契約指定を行うことができます。また、このような実装は、コンパイラのフロントエンドだけで実装することができます。

この提案は、MVPの一部あるいはEval_and_throwモードのためというわけではなく、契約機能そのものに対して次のことを提案しています

  1. 呼び出される関数の事前条件チェックを、現在の翻訳単位内のコードによって可能とする
  2. オーバーロード解決済の関数呼び出しのみチェックすることを可能とするために、対象となる関数を名前を指定する関数とメンバ関数に限定する
  3. 現在の翻訳単位内での契約アサートのチェックを可能とする
  4. 現在の翻訳単位内での事後条件評価を可能としない
  5. 事前条件が何回評価されるかは保証されない

また、Eval_and_throwモードを採用する場合でも、それを有効にするとこれらのことが有効となり、これ以上の何かが有効にはならないことを提案しています。

P2806R1 do expressions

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

以前の記事を参照

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

  • 最後の値を暗黙的に戻り値とすることと明示的なreturnについての議論を追加
  • リフレクションについての議論を追加
    • リフレクションのようなコードインジェクション機能があってもこの機能は必要か?という問いに対する回答
  • 文法の修正

などです。

最後の値を暗黙的に戻り値とすることとは、do式のスコープで最後に表れた値を暗黙的に戻り値とみなすことです。つまり、Rustなどで一般的に行われていることをdo式限定でできないか?ということです。

暗黙`return` `do return`
auto foo(int i) -> std::expected<int, E>

auto bar(int i) -> std::expected<int, E> {
    int j = do {
        auto r = foo(i);
        if (not r) {
            return std::unexpected(r.error());
        }
        *r // <== NB: no semicolon
    };

    return j * j;
}
auto foo(int i) -> std::expected<int, E>

auto bar(int i) -> std::expected<int, E> {
    int j = do {
        auto r = foo(i);
        if (not r) {
            return std::unexpected(r.error());
        }
        do return *r;
    };

    return j * j;
}

単純な例では短くなりますが、C++の場合は早期returnができないためその有効性は限定的です。(do return必要性の有無を排他的とすると)例えばdo式内のループから戻ることができません。それをサポートするにはさらにif式が必要となりますが、それはこの提案の設計にさらなる複雑さを加えることになります。

P2809R0 Trivial infinite loops are not Undefined Behavior

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

C++では副作用のない無限ループは未定義動作となり、たびたび話題になります。例えば次のようなコードはなぜかHello world!が出力されます。

#include <iostream>

int main() {
  while (true)
    ; 
}

void unreachable() {
  std::cout << "Hello world!" << std::endl;
}

これはC++11でスレッドを言語としてサポートした際に、並行プログラムにおける進行保証(forward progress guarantee)の一環としてメモリモデルとともに導入されたものです。同じことはC11でも行われ、C言語forward progress guaranteeを言語として定義していますが、そこではループの条件が定数式であるような自明な無限ループは未定義動作とはなりません。

forward progress guaranteeは並行プログラムが正しく進行することを保証するための枠組みであり、この制約がきつすぎると並行処理を行うソフトウェア及びそれを実行するハードウェアの設計を妨げてしまう可能性があります。C++における無限ループの扱いも、並行プログラム及びハードウェアの設計自由度を確保するための措置であるようです。

しかし、このことは逆にシングルスレッドのプログラムにおいては問題となり、ベアメタルのシステムやカーネルなど、低レベルのプログラムにおいてプログラムの進行を意図的に停止させておくことは一般的なイディオムであり、この問題はそれを妨げています。

コンパイラがこれを診断しそれをプログラマに伝えることは容易なはずですが、現在の実装はそれを行っておらず、上記例のように最適化に利用してしまっています。

この提案は、この問題を解決するために、ループ継続条件が定数式であるようなループをプログラマの意図的なものと解釈し、そのような自明なループはC++においても未定義動作として扱わないようにしようとするものです。ただし、これは次のことを変更するものではありません

  • 継続条件が定数式ではないループは、引き続き未定義動作となる
    • この種のループは終端保証による最適化の恩恵がある
  • gotosetjmp/longjmp、無限末尾再帰などの他の方法によるループの扱い
  • 並列・並行プログラムにおける進行保証(forward progress guarantee
    • 自明なループを持つ場合を除く

実際には、forward progress guaranteeの一環として無限ループが未定義動作となってしまったのは、C++11/C11時点の既存の慣行(特にコンパイラ実装)を反映したもののようで、必ずしも並行プログラムにおけるforward progress guaranteeを満たすためだけのものではなかったようです。

当時のコンパイラはループを変形する最適化を行うためにループが終了することを仮定していましたが、これは当時のC99の規定(ループは継続条件が0になると終了する)に違反していました。並行プログラミングを言語としてサポートしつつもその性能を妨げないために、C++11/C11ではこの慣行を標準化することでそれらの最適化を受け入れました。

ただ、ループが終了するかを判定することは通常停止性問題を解くことと同値でありそれは不可能であるため、この種の最適化の条件を「停止性問題を解く」から「観測可能な副作用を保つ」に変更することで扱いやすくするとともに、それをforward progress guaranteeに組み込むことで並行アプリケーションにおいてもそのような最適化を有効化したようです。

P2811R0 Contract Violation Handlers

P2811R1 Contract Violation Handlers

契約プログラミングに、ユーザー提供の違反ハンドラを許可する提案。

C++20で削除された契約プログラミング機能では、契約違反時の違反ハンドラを上書きすることでユーザーがカスタマイズすることができました。それによって、契約違反時の振る舞いをビルドモードとは無関係にカスタマイズすることができ、例えば次のような事に利用することを意図していました

  • 契約違反時の報告方法や形式のカスタマイズ
  • 契約違反時のセマンティクス管理
    • 即終了する、例外を投げる、longjmpによる終了、無視して継続、など

現在のContract MVP仕様では、違反ハンドラはシステムが提供するデフォルトのものから置き換えることはできず、契約違反時の振る舞いもプログラム終了のみとなっています。すなわち、現在のMVPは契約条件チェックのセマンティクスの制御がビルドモードによって担われており、それも現状は選択肢が1つしかありません。

契約違反時の振る舞いの最適な選択はユーザーによって異なり、違反時の振る舞いをカスタマイズすることができる移植可能で一貫した方法を提供することは、ほとんどのユーザーによって有益であり、契約プログラミング機能の有用性を大きく高めることになります。

ただし、C++20契約機能で揉めたように、契約違反時に任意のコードを実行することにはリスクがあり、カスタムの違反ハンドラで行える有用なこととバランスを取る必要があります。それでも、契約違反時の処理をカスタムできることは一般的に必要であり、殆どの場合にはプラスに働きます。

この提案は、現在のMVP仕様に対して次の2点の変更を加えることで、違反ハンドラのカスタマイズをサポートできるようにしようとするものです

  1. 契約違反時のプロセスの一部として呼び出される関数(違反ハンドラ)をユーザーが提供する能力を追加する
    • Eval_and_abortモードでは、違反ハンドラが終了した後でプログラムを終了する
  2. デフォルトの違反ハンドラの動作について、実装可能ならば採用することが望ましい推奨事項を作成する

ただし、特殊なプラットフォームにおいては違反ハンドラをカスタムすること(契約違反時に任意コードを実行可能であること)は受け入れ難いセキュリティリスクとみなされる場合があるため、違反ハンドラのカスタムは条件付きサポートとし、そのようなプラットフォームにおいて違反ハンドラをカスタムする場合はエラーとすることを提案しています。

違反ハンドラはグローバルモジュールに属する::handle_contract_violation()という関数を定義することでカスタムします。

// 違反ハンドラの宣言
void handle_contract_violation(const std::contracts::contract_violation&);

この関数は[[noreturn]]であってもよく、noexceptの指定も任意です。引数のstd::contracts::contract_violationは契約違反が発生した状況についての情報を持つクラス型で、次のように定義されています

// <contract> ヘッダで定義
namespace std::contracts {

  enum class contract_violation_detection_mode : /*unspecified*/ {
    unknown,
    predicate_false,
    predicate_exception,
    predicate_undefined_behavior
  };

  enum class contract_semantic : /*unspecified*/ {
    ignore,
    enforce
  };

  enum class contract_kind : /*unspecified*/ {
    pre,
    post,
    assert
  };

  class contract_violation {
  public:

    // 破られた契約の条件式のテキスト表現 
    const char* comment() const noexcept;

    // 契約違反の起こり方
    contract_violation_detection_mode detection_mode() const noexcept;

    // 破られた契約の種別
    contract_kind kind() const noexcept;

    // 違反を起こした場所の情報
    source_location location() const noexcept;

    // ビルドモードに関する情報
    contract_semantic semantic() const noexcept;
  };
}

contract_violationクラスはABI互換を保つように実装されることを意図しています。

違反ハンドラが提供されない場合、実装はデフォルトの違反ハンドラを使用し、それは有用な診断情報をstderrなどに出力するものであることを推奨としています。

ユーザー提供のコールバック(違反ハンドラ)を介して契約違反時の振る舞いをカスタマイズすることは、他の同等のアサーションの実装において一般的かつ十分にテストされたアプローチであり、(筆者の方の)Bloombergにおいても2004年に投入されて以降使用され続けているようです。

また、このアプローチはEWG/LEWGのレビューを通過した上でC++20契約機能に組み込まれており、その後削除されたのは違反ハンドラカンスタマイズの有用性や設計とは関係がなく、C++20契約機能全体の設計によるものだったとのことです。

P2817R0 The idea behind the contracts MVP

Contracts MVPと呼ばれるものを解説する文書。

Contracts MVPとは、C++20で最終的に削除されたContracts仕様から物議を醸していない基礎的な部分を抽出した最小のContracts仕様です。ここには事前/事後条件とアサーションのための注釈構文とそのセマンティクスが含まれています。

これはC++契約プログラミング機能の全てではなく、始まりとなるもので、合意が取れている部分からインクリメンタルに機能を拡張していくための基盤となることを目指すものです。

この文書は、そのようなContracts MVPの背景や方針、考え方などについてまとめられたものです。

P2818R0 Uniform Call Syntax for explicit-object member functions

明示的オブジェクトパラメータを持つ関数をフリー関数呼び出し構文によって呼び出し可能とする提案。

この提案は一様関数呼び出し構文(Uniform Function Call Syntax : UFCS)の一種として提案されています。UFCSはC++に対してこれまで何度か提案されてきており、その必要性は次のようにまとめられます

  • ジェネリックコードでは、より汎用的であるためフリー関数呼び出し(f())が好まれる
  • IDEユーザーは、 オートコンプリートが効きやすくなるためメンバ関数呼び出し(o.f())を好む
  • より多くの選択肢があれば、より多くの文脈でidomaticなコードを書くことができるようになる

明示的オブジェクトパラメータとはいわゆるDeducing thisと呼ばれる機能の事です。明示的オブジェクトパラメータを持つ関数はメンバ関数としてしか呼び出すことができません。

この提案は、明示的オブジェクトパラメータを持つ関数にfriendとマークすることで、フリー関数呼び出し構文からでも呼び出し可能とするものです。

struct S {
  // Deducing this + friend
  friend int f(this S) {
    return 42;
  }
};

int g() {
  S s{};

  s.f();  // OK, returns 42 (これは現在の振る舞い
  f(s);   // OK, 同上 (この提案の振る舞い
  (f)(s); // Error; fはADLのみで発見される
}

// 前方宣言の扱いについて
int f(S);    // Error, int f(S) conflicts with S::f(S)
int f(S) {}; // Error, int f(S) conflicts with S::f(S)

int f(int);  // OK

この提案による明示的オブジェクトパラメータを持つ関数のfriend宣言は、メンバ関数メンバ関数でありながらHidden friendsでもあるような状態にしています。

明示的オブジェクトパラメータを持つ関数はそもそもフリー関数に近いものであり、実際の扱いもほとんどフリー関数と同じ扱いをされますが、呼び出し周りはあくまでメンバ関数であるかのように扱われています。この提案では、明示的オブジェクトパラメータを持つ関数宣言にfriendを追加することで、見た目通りのフリー関数(Hidden friends)としての性質を有効化します。

また、この提案による変更は、明示的オブジェクトパラメータを用いた拡張メソッドのような将来の提案を妨げないことも意識されています。

P2819R0 Add tuple protocol to complex

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

現在のstd::complex.real().imag()によってそれぞれ実部と虚部を取得/設定することができますが、この関数は参照ではなく値を返します。この関数を変更することはABI破壊になるため実質不可能であり、実部と虚部の値への参照を取得する唯一の方法は参照もしくはポインタにreinterpret_castすることです(このキャストは安全であることが保証されています)。しかし、reinterpret_castが定数式で禁止されているため、定数式でそれを行う方法はありません。

この提案は、std::complexにタプルインターフェースを追加することで、後方互換性を維持しつつより直感的かつ簡易な方法によって、複素数値のそれぞれの値の参照を取得できるようにしようとするものです。

数学的にも、複素数 \mathbb{C}はベクトル空間としての \mathbb{R}^2と同型であり、 \phi:\mathbb{C} \to \mathbb{R}^2の対応として \phi(a + bi) = (a, b)のような同型写像があります。これによって、複素数を実数2つからなるタプルだと思うことの理論的根拠が与えられます。

簡単な例

現在 この提案
std::complex<double> c{...};
auto& [r, i] = reinterpret_cast<double(&)[2]>(c);
std::complex<double> c{…};
auto& [r, i] = c;

実部と虚部を入れ替える例

現在 この提案
template<typename T>
constexpr
auto swap_parts(complex<T> c) -> complex<T> {
  if not consteval {
    auto & [r, i]{reinterpret_cast<double(&)[2]>(c)};
    swap(r, i);
  } else {
    // reinterpret_castが定数式で使用できないためのフォールバック
    const auto r{c.real()};
    const auto i{c.imag()};
    c.imag(r);
    c.real(i);
  }
  return c;
}
template<typename T>
constexpr
auto swap_parts(complex<T> c) -> complex<T> {
  auto& [r, i]{c};
  swap(r, i);
  return c;
}

さらに、これは将来のパターンマッチングでも有効である可能性があります。

現在 この提案
complex<double> c{…};

// P1371R3のパターンマッチング
inspect(reinterpret_cast<double(&)[2]>(c)) {
  [0, 0] => { cout << "on origin"; }
  [0, i] => { cout << "on imaginary axis"; }
  [r, 0] => { cout << "on real axis"; }
  [r, i] => { cout << r << ", " << i; }
};

// P2392R2のパターンマッチング
inspect(reinterpret_cast<double(&)[2]>(c)) {
  is [0, 0] => cout << "on origin";
  is [0, _] => cout << "on imaginary axis";
  is [_, 0] => cout << "on real axis";
  [r, i] is _ => cout << r << ", " << i;
}
complex<double> c{…};

// P1371R3のパターンマッチング
inspect(c) {
  [0, 0] => { cout << "on origin"; }
  [0, i] => { cout << "on imaginary axis"; }
  [r, 0] => { cout << "on real axis"; }
  [r, i] => { cout << r << ", " << i; }
};

// P2392R2のパターンマッチング
inspect(c) {
  is [0, 0] => cout << "on origin";
  is [0, _] => cout << "on imaginary axis";
  is [_, 0] => cout << "on real axis";
  [r, i] is _ => cout << r << ", " << i;
}

P2821R0 span.at()

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

.at()メンバ関数は、メモリ連続性を持つ(contiguousな)コンテナが持つ要素アクセス関数で、添字演算子[]が境界チェックを行わない(範囲外アクセスは未定義動作)のに対して境界チェックを行う(範囲外アクセスはstd::out_of_range例外)ものです。

std::vectorstd::array等、メモリ連続性を持つ他の標準コンテナは全て[]at()の両方を持っていますが、std::span[]しかありません。

在野のspan実装ライブラリにおいては.at()メンバ関数がその実装の早い段階から提供されている他、chromiumfirefoxにバックポートされたstd::span実装においては[]で境界チェックを行なっているため、std::spanに移行した際に対応する安全な関数が存在しないなど問題があります。

この提案は、一貫性と安全性の向上のために、std::spanにも.at()メンバ関数を追加しようとするものです。

namespace std {

  template<class ElementType, size_t Extent = dynamic_extent>
  class span {
    ...

    // 添字演算子(既にある
    constexpr reference operator[](size_type i) const;

    // at()関数(この提案
    constexpr reference at(size_type i) const;

    ...
  };
}

この提案の.at()メンバ関数は既存の他のコンテナと同様に、実行時に境界チェックを行った上で要素アクセスを行う関数であり、範囲外アクセス時にはstd::out_of_range例外をスローします。

P2824R0 WG21 February 2023 Issaquah meeting Record of Discussion

2023年2月6-11日にアメリカのIssaquahで行われた、WG21全体会議。

N4943との差異はよくわかりません。

P2825R0 calltarget(unevaluated-call-expression)

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

関数がオーバーロードされている場合や関数テンプレートの場合、その関数名から関数ポインタを取得しようとするとうまくいかないことがあります。

#include <functional>

void f(int) {}
void f(double) {}

template<typename T>
void g(T) {}

int main() {
  std::function<void(int)> func1{f}; // ng
  std::function<void(int)> func2{g}; // ng
}

これは対象となる関数が1つに定まらないために起きています。

このような場合、取得するオーバーロードを確定させてから関数ポインタを取得する必要があります。

#include <functional>

void f(int) {}
void f(double) {}

template<typename T>
void g(T) {}

int main() {
  std::function<void(int)> func1{static_cast<void(*)(int)>(f)}; // ok
  std::function<void(int)> func2{g<int>}; // ok
}

このことはstd::functionだけではなく、呼び出し可能なものをテンプレートで受け取るようなところ(特に、型消去コールラッパのようなものを利用するところ)ではほぼ確実に発生します。回避策としては

  • static_castする
  • 一旦関数ポインタに受ける
  • 常に関数呼び出し(f())を使用する(汎用性を捨てる)
  • ラムダでラップする

などがあります。とはいえ、これらの解決策にはどれも様々な問題があります。

この提案は、このような関数ポインタ取得時のオーバーロードを考慮した対象関数の問い合わせを行う言語機能を追加することで、コンパイル時にコンパイラがこれを解決するようにしようとするものです。

この提案では、__builtin_calltarget(postfix-expression)という組み込み関数を言語機能として追加し、これに関数呼び出しの式を渡すことでその式で呼び出される関数のポインタを得られるようにします。ここでのpostfix-expressionは評価されないオペランドであり、実行されるわけではありません。

#include <functional>

void f(int) {}
void f(double) {}

template<typename T>
void g(T) {}

int main() {
  std::function<void(int)> func1{__builtin_calltarget(f(1))}; // ok
  std::function<void(int)> func2{__builtin_calltarget(g(1))}; // ok
}

__builtin_calltargetにはメンバ関数呼び出しを渡すこともできて、その場合はメンバ関数ポインタが得られます。渡された式のトップレベルのASTに関数呼び出しが含まれていない場合(式の一番最後に評価されるのが関数呼び出しでない場合)はコンパイルエラーとなります。

なお、__builtin_calltargetとは仮の名前であり、後で適切な名前に置き換えることを意図しているようです。

提案より、振る舞いの例

void g(long x) { return x+1; }
void f() {}                                                // #1
void f(int) {}                                             // #2
struct S {
  friend auto operator+(S, S) noexcept -> S { return {}; } // #3
  auto operator-(S) -> S { return {}; }                    // #4
  auto operator-(S, S) -> S { return {}; }                 // #5
  void f() {}                                              // #6
  void f(int) {}                                           // #7
  S() noexcept {}                                          // #8
  ~S() noexcept {}                                         // #9
  auto operator->(this auto&& self) const -> S*;           // #10
  auto operator[](this auto&& self, int i) -> int;         // #11
  static auto f(S) -> int;                                 // #12
  using fptr = void(*)(long);
  auto operator void(*)() const { return &g; }             // #13
  auto operator<=>(S const&) = default;                    // #14
};
S f(int, long) { return S{}; }                             // #15
struct U : S {}

void h() {
  S s;
  U u;
  __builtin_calltarget(f());                     // ok, &#1             (A)
  __builtin_calltarget(f(1));                    // ok, &#2             (B)
  __builtin_calltarget(f(std::declval<int>()));  // ok, &#2             (C)
  __builtin_calltarget(f((short)1));             // ok, &#2 (!)         (D)
  __builtin_calltarget(s + s);                   // ok, &#3             (E)
  __builtin_calltarget(-s);                      // ok, &#4             (F)
  __builtin_calltarget(-u);                      // ok, &#4 (!)         (G)
  __builtin_calltarget(s - s);                   // ok, &#5             (H)
  __builtin_calltarget(s.f());                   // ok, &#6             (I)
  __builtin_calltarget(u.f());                   // ok, &#6 (!)         (J)
  __builtin_calltarget(s.f(2));                  // ok, &#7             (K)
  __builtin_calltarget(s);                       // error, constructor  (L)
  __builtin_calltarget(s.S::~S());               // error, destructor   (M)
  __builtin_calltarget(s->f());                  // ok, &#6 (not &#10)  (N)
  __builtin_calltarget(s.S::operator->());       // ok, &#10            (O)
  __builtin_calltarget(s[1]);                    // ok, &#11            (P)
  __builtin_calltarget(S::f(S{}));               // ok, &#12            (Q)
  __builtin_calltarget(s.f(S{}));                // ok, &#12            (R)
  __builtin_calltarget(s(1l));                   // ok, &#13            (S)
  __builtin_calltarget(f(1, 2));                 // ok, &#15            (T)
  __builtin_calltarget(new (nullptr) S());       // error, not function (U)
  __builtin_calltarget(delete &s);               // error, not function (V)
  __builtin_calltarget(1 + 1);                   // error, built-in     (W)
  __builtin_calltarget([]{
       return __builtin_calltarget(f());
    }()());                                      // ok, &2              (X)
  __builtin_calltarget(S{} < S{});               // error, synthesized  (Y)
}

基本的には、組み込みの演算子やコンストラクタ/デストラクタなどはアドレスを取得することができないようになっています。GやJのケースは、定義されているクラス(UではなくS)におけるポインタを取得することを示しています。また、Nのケースは対象となる関数呼び出しの決定は構文定義によることを示しており、postfix-expressionsの構文は左結合であるためASTのトップに来るのは->ではなくf()となり、S::f(#6)のアドレスが取得されます。

P2826R0 Replacement functions

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

C++の様々なところで、何かラッパを挟むことなくある関数を別の名前あるいは呼び出しパターンの関数のオーバーロード集合に追加したいことがよくあります。標準ライブラリ自体にも、expression-equivalentとして指定されているところ(主にCPOの呼び出し)では、まさにそれが求められていますが、そのような方法はないのでexpression-equivalentという言葉で規定されています。

そのような場所でやりたいことは次のようなことです

  1. 関数の呼び出しパターンを検出する
  2. そのケースに適した関数を呼び出す

(CPOの定義に関して考えてみると実感が湧きやすいかもしれません)

問題となるのは、パターンの検出もその結果を受けての関数呼び出しも、どちらも多くの場合にC++オーバーロード解決メカニズムに適合しないことです。そのため、CPOのようなディスパッチを行うラッパが必要となります。

しかし、ラッパを使用すると、その内部でどのように工夫して実装したとしても入力のprvalueを他の右辺値と区別する方法がないため、コピー省略を妨げてしまいます。また、そのようなラッパはテンプレートの肥大化の原因ともなり、余分なコールスタックを追加することによってコピー省略以外の最適化を阻害したりデバッガーによるデバッグのしやすさを低下させます。

この提案は、expression-equivalentのようなユースケースをカバーし、なおかつ上記のような問題が起こらない、言語機能による関数エイリアスを提案するものです。

この提案による関数エイリアスは次のようなものです

// フリー関数の例

struct S {};
struct U {
  // Sへの暗黙変換演算子
  operator S() const { return{}; }
};

int g(S) { return 42; }

// g(S)をf(U)としてfのオーバーロード集合に入れる
auto f(U) = &g;

long f(S) { return 1l; }

int h() {
  return f(U{}); // returns 42
}
// メンバ関数の例

template <typename T>
struct Container {
  auto cbegin() const -> const_iterator;
  auto begin() -> iterator;

  auto begin() const = &cbegin; // saves on templates
};

関数宣言(定義)において、関数本体を= constant-expression;で定義し、右辺の式が定数式で何らかの関数ポインタに解決されれば左辺のシグネチャでその関数を呼び出すことができるようになります。ちょうど、関数のdelete宣言で= delete;、純粋仮想関数で= 0;と定義するように、左辺の関数名(とシグネチャ)を右辺の別の関数で定義します。

オーバーロード解決においては、左辺のシグネチャオーバーロード集合に参加し、オーバーロード解決でそれが選択されたらそれを右辺の関数で置換します。この時、置換後の関数がそのシグネチャで呼び出し可能ではない場合、そのままエラーになるだけです。

この提案による関数宣言の右辺の定数式がnull(ptr)に評価された場合、その関数はdelete定義されたものとみなされます。これによって、C++の式として関数のdeleteを制御できるようになります。ただしこれは、仮想関数の場合は= 0;で定義されたものとみなされます。

template <bool enable_implementations>
struct A {
  long g(int) { return 42; }

  long h(int) = enable_implementations ? &g : nullptr;
  
  virtual long f(int) = enable_implementations ? &g : nullptr;
};

struct Concrete : A<true> {};
struct Abstract : A<false> {};
struct Concrete2 : Abstract { 
  long f(int) override { return 3; }
};

void impl() {
  Concrete x;  // ok
  x.h(2);      // ok, 42

  Concrete2 z; // ok
  z.f(2);      // ok, 3、f(int)はConcrete2でオーバーライドされている
  z.h(2);      // Error、h()は削除されている

  Abstract y;  // Error、f(int)は純粋仮想関数であり、Abstractは抽象クラス
};

この提案によって解決される言語の問題には例えば、P2481で報告されている、型のCV/参照修飾だけを推論したい場合の構文が無い問題があります。

struct A {};
struct B : A {};

// 関数f()の実装
template <typename T>
auto _f_impl(T&& x) {
  // A&, A&&, A const&, A const&& に対してのみインスタンス化される
};

// cvref-derived-fromのようなコンセプト
template <typename D, typename B>
concept derived_from_xcv = std::derived_from<std::remove_cvref_t<D>, B>;

// Aの派生型TをAにアップキャストしつつそのCV修飾をコピーして、_f_impl()に渡すエイリアス
template <derived_from_xcv<A> T>
void f(T&&) = &_f_impl<copy_cvref_t<T, A>>;

void use_free() {
  B b;
  f(b);                // OK, calls _f_impl(A&)
  f(std::move(b));     // OK, calls _f_impl(A&&)
  f(std::as_const(b)); // OK, calls _f_impl(A const&)
}

同じことは明示的オブジェクトパラメータを持つ関数でも問題になる可能性があり、この提案による関数定義はそれもサポートしています。

struct C {
  // 実装関数、Cだけを受け取る
  void f(this std::same_as<C> auto&& x) {
    ...
  }

  // Cの派生型TをCにアップキャストしてf()に渡すエイリアス
  template <typename T>
  void f(this T&& x) = static_cast<void (*)(copy_cvref_t<T, C>&&)>(f);

  // 前項のP2825の __builtin_calltarget()を使用すると、対象関数の選定をコンパイラに委託できる
  template <typename T>
  void f(this T&& x) = __builtin_calltarget(std::declval<copy_cvref_t<T, C>&&>().f());
};

struct D : C {};

void use_member() {
  D d;
  d.f();                // OK, calls C::f(C&)
  std::move(d).f();     // OK, calls C::f(C&&)
  std::as_const(d).f(); // OK, calls C::f(C const&)
}

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

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

std::from_charsは文字列から数値への変換を行う関数であり、変換後の値は引数に渡された数値型オブジェクトの参照へ出力します。変換後の数値型はその引数型から決まり、変換後の値がその型で表現できない場合は、戻り値のfrom_chars_result::ecstd::errc::result_out_of_rangeになると規定されています。

ただ、これは整数型におけるオーバーフローが起きた場合に合わせた仕様であり、浮動小数点数型におけるアンダーフローを判別できなくなっています。すなわち、std::errc::result_out_of_rangeが帰ってきた時に、変換後の値が大きすぎてdouble値に出力できなかったのか、変換後の値(の絶対値)が小さすぎてdouble値に出力できなかったのかが判別できません。これによって例えば、Pythonインタプリタが行う次のような振る舞いをstd::from_charsでは実現できないことになります

>>> 3.14e-2000
0.0
>>> -1.1e360
-inf

std::from_charsの規定はCのstrtod()を参照する形で規定されていますが、その移行時にstrtod()のこの種の機能性が失われていることがLWG Issue 3081で指摘されていました。

strtod()double型への変換の際に、オーバーフローする場合はHUGE_VALを返してerrnoERANGEをセットし、アンダーフローする場合は最小の正の正規化数を返しerrnoERANGEをセットするかは実装定義と規定されています。これだけみるとstd::from_charsとほぼ変わらないように見えますが、実際にはstrtod()の適切な実装としては、アンダーフローが起きた場合は0.0/-0.0を返してerrnoERANGEをセットするのがデファクトスタンダードとなっています。

すなわち、strtod()では、errnoERANGEである場合に結果が0であるかを確認することで、オーバーフローとアンダーフローを区別できます。

// C標準の適合実装に準じたコード
{
  errno = 0;
  double n = strtod(p, NULL);
  if (errno == ERANGE && (n == HUGE_VAL || n == -HUGE_VAL)) {
    // オーバーフロー検出
  }
}

// 実際のデファクト実装に準じたコード
{
  errno = 0;
  double n = strtod(p, NULL);
  if (errno == ERANGE) {
    if (n != 0.0) {
      // オーバーフロー検出
    } else {
      // アンダーフロー検出
    }
  }
}

浮動小数点数値(IEEE 754準拠)の比較では0.0-0.0は同値であると比較されるため、どちらが返されていたとしても0.0との比較がtrueを返すかによってアンダーフローを識別できます。

std::from_charsでこの問題を解決するために取れる方法は限られており

  • 例外を投げる
    • <charconv>の目的にそぐわない
  • グローバル状態の変更
    • 同上
  • std::from_chars_result::ecに報告する値の変更
    • 後方互換性を損ねる
    • 現在オーバーフロー/アンダーフローを区別する必要がないコードで、アンダーフロー時の動作が変わってしまう可能性がある
  • 出力値に特別な値を出力

実質的には最後の方法、結果を出力する変数に特別な値を出力すること、に限られています。

この提案では、その値としてアンダーフロー時には±0.0、オーバーフロー時には±1.0を出力するように変更することを規定しています。なお、現在は戻り値のエラーコードでresult_out_of_rangeを返す以外のことはしていません(変換失敗には出力先変数は変更されない)。

出力値の符号は変換後の値の符号から決まります。すなわち、vを変換後(出力前)の値とすると

  • +0.0 : vが正の値であり、アンダーフローが起きた時
  • -0.0 : vが負の値であり、アンダーフローが起きた時
  • +1.0 : vが正の値であり、オーバーフローが起きた時
  • -1.0 : vが負の値であり、オーバーフローが起きた時
// この提案によるオーバーフロー/アンダーフロー検知コード

double v;
if (auto [ptr, ec] = std::from_chars(first, last, v); ec == std::errc::result_out_of_range) {
  if (v != 0.0) {
    // オーバーフロー検出
  } else {
    // アンダーフロー検出
  }
}

ただ、std::from_charsの規定をよく読むと非有限値を出力することがないことがわかるため、それを利用した次のようなエラーハンドリングコードが考えられます

// 先にNaN(非有限値)で初期化
auto v = quiet_NaN_v<double>;

std::from_chars(first, last, v);

// 有限値ではない場合はエラーを仮定できる
if (not std::isfinite(v)) {
  /* ec != errc() */
}

このようなコードは稀であると思われるため、機能テストマクロの数値をバンプすることでユーザー側で対処してもらうことを提案しています。

P2828R0 Copy elision for direct-initialization with a conversion function (Core issue 2327)

CWG Issue 2327では、次のようなコードにおいてコピー省略が起きないことが報告されていました。

struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d); // Catのムーブコンストラクタでprvalueが実体化する

Dog::operatorCat()がコピー省略可能なように実装されていたとして、このコードでは、Cat cの初期化においてCatのムーブコンストラクタが選択され、dを変換演算子によってCatに変換した結果が渡されます。ムーブコンストラクタはCat&&でそれを受けるため、ここでprvalueの実体化が発生し、これによってコピー省略の要件を満たさなくなるためCatのムーブコンストラクタの呼び出しを削除できなくなります(Catのムーブコンストラクタがdeleteされているとコンパイルエラーとなる)。

ただ、少なくとも現時点のClang/GCC/MSVC/NVC++(EDG)はどれもこの場合のコピー省略を実装しており、Catのムーブコンストラクタがdeleteされていても上記コードはコンパイルエラーとなりません。

ただし、その実装のアプローチにはいくつかの違いがあるようで、それを調べていくとこの場合の(変換演算子を用いた直接初期化における)コピー省略の設計空間がかなり広いことが分かったようです。この提案は、CWG2327及び将来のコピー省略の拡大のために、その報告とそれらのアプローチの比較を行うものです。

その実装の戦略の違いは次のようになります(EDG/MSVCはソースコードがオープンではないので推測を含んでいます)

  • EDG
    • Cat初期化時の)オーバーロード解決は標準に従って行う
    • 直接初期化を行うためにCatのムーブ(orコピー)コンストラクタが選択され、コンストラクタの引数(Cat&&型)が変換演算子によって変換された結果のprvalueを束縛するためにそのprvalueが実体化する場合、一時オブジェクトを実体化させる代わりにそのprvalueを使って初期化対象のオブジェクトを初期化する
  • Clang
    • オーバーロード解決の時に、Catのムーブ(orコピー)コンストラクタに加えてDog::operator Cat()も直接初期化の候補として加える
    • オーバーロード解決では、Catのムーブ(orコピー)コンストラクタがDogの変換演算子呼び出しを必要とするのに対して、Dog::operator Cat()dをその暗黙のオブジェクトパラメータ(thisパラメータ)に束縛するのみであるため、Dog::operator Cat()が選択される
    • その結果、Dog::operator Cat()の呼び出しのみでd -> cの変換とcの初期化が完了し、ムーブコンストラクタは呼び出されない
  • GCC
    • オーバーロード解決は標準に従って行う
    • オーバーロード解決の前に、その候補となっているムーブ(orコピー)コンストラクタの参照パラメータ(Cat&&)が変換演算子の結果のprvalueを束縛する場合、そのコンストラクタを変換演算子そのもので置き換える
    • Clangの場合と同様に、変換演算子の優先度は残りうる他の物(変換コンストラクタなど)より高くなるため、(ムーブ(orコピー)コンストラクタを置き換えた)変換演算子が選択される
    • その結果、Dog::operator Cat()の呼び出しのみでd -> cの変換とcの初期化が完了し、ムーブコンストラクタは呼び出されない
  • MSVC
    • EDGに近い実装と思われるが若干異なる振る舞いをする。明確なルールを見出すことができなかった

どの実装の場合でもコピー省略が起こるのは、変換演算子の結果はCatそのもの(CV修飾は考慮される)でなければなく、その派生クラスや参照であってはいけません。すなわち、変換演算子cv Cat型のprvalueを返さないとコピー省略されません。

比較すると、EDGは最も保守的かつシンプルなアプローチをとっており、Clangはそれと対照的に最もアグレッシブなアプローチをとっています。GCCは両者の中間的な実装となり、MSVCはEDGに近い保守的なアプローチと思われます。

このアプローチの差は、当然その振る舞いにも差をもたらします。特に、EDGのアプローチは基本的に後方互換性を保っていますが、Clang(GCC)のアプローチはそうではない場合があります。

struct X {
  X(int);
  // X(X&&);  // implicitly declared
};

struct Y {
  operator X();
  operator int();
};

X x(Y{}); // clang/gccは曖昧にならない

この例では、現在のC++およびEDGではXのコンストラクタが曖昧となるためエラーとなります。一方、Clang/GCCは曖昧とならず、Y::operator X()によって初期化されます。これは、Clangの場合はY::operator X()オーバーロード解決候補に入れられ、変換が最小であるためそれが選択されており、GCCの場合はX::X(&&)Y::operator X()に置き換えられてオーバーロード解決によって(Clangと同様の理由で)選択されるためです。

CWGのコンセンサスは、Clang(GCC)のアプローチを好み、この例はコンパイルされるべき、という方向で合意されているようです。

struct Dog;

struct Cat {
  Cat(const Dog&);
};

struct Dog {
  operator Cat();
};

Cat cat(Dog{}); // EDG/MSVCは変換コンストラクタを呼ぶ
                // Clang/GCCは変換演算子を呼ぶ

この例では、Dog -> Catの変換にCatの変換コンストラクタとDogの型変換演算子のどちらが使われるのかが実装によって異なります。EDG/MSVCはCatの変換コンストラクタを選択し呼び出しますが、Clang/GCCDogの変換演算子を選択し呼び出します。これは、Clang/GCCがどちらもDog::operator Cat()Cat::Cat(const Dog&)の間でオーバーロード解決を行うためで、型変換なしのconst参照への束縛よりもthisパラメータへの束縛の方が優先順位が高くなるため変換演算子が選択されます。

また、この場合にDog::operator Cat()が次のように実装されていると、Clang/GCCでは無限ループに陥ります。

Dog::operator Cat() {
  return Cat(*this);  // Clang/GCCは無限再帰する・・・
}

この例のClang/GCCの振る舞いは既存のコードの振る舞いを静かに変化させています。CWGのコンセンサスはこの例の動作を変更させないことで合意されているようです(つまり、GCC/Clangのアプローチはそのまま標準化できません)。

struct T {
  T(T const&);
};

struct S {
  operator T();
  operator T&();
};

S s;
T t(s); // Clangのみ、operator T()を呼び出す

この例では、現在のC++及びEDG/MSVC/GCCS::operatorT&()を呼び出します。これは、SからT const&への変換において一時オブジェクトを生成するよりも参照を束縛する事の方が優先されるためです。しかし、Clangはオーバーロード候補にS::operator T()Tのコンストラクタを追加し、結果としてS::operator T()が選択されます。S::operatorT&()は使用されません。

struct Y;

struct X {
  X(const Y&);
};

struct A {
  operator X();
};

struct B {
  operator X();
};

struct Y : A, B { };

X x(Y{});  // Clangは曖昧になる

この例は、Clangのアプローチだけが曖昧となるように考えられた例であり、現実的なコードでは見られないものかもしれません。オーバーロードの解決候補に、Xのコンストラクタに加えてA, Bに定義された2つのoperator X()が加えられ、それらの間で順序が付かない事から曖昧になります。この場合、X::X(Y&&)を追加すると曖昧さを取り除くことができます。

このように、Clang(およびGCC)のアプローチで行われているオーバーロード解決ルールに変更を加えることは、現行のC++及びEDGの保守的なアプローチと比較して既存のコードに問題を起こすことがあります。CWGの議論では、Clangのアプローチが当初は好まれていましたが、上記例のようないくつかの問題が明らかになった結果として、現在はどのアプローチを採用するかのコンセンサスがなくなっています。

そのためこの提案では、CWG Issue 2327の解決としては保守的なEDGのアプローチを採用し、アグレッシブなコピー省略のためのオーバーロード解決ルールの変更は別の機会に回すことを提案しています。EDGの保守的なアプローチを取ったとしても、将来的にClangの様なアプローチを採用することを妨げないため、CWG Issue 2327の問題はとりあえず解決しつつより時間をかけて議論したうえでさらなるコピー省略保証を導入することができます。

P2830R0 constexpr type comparison

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

std::type_info::before()は実装定義ではあるものの、2つの型の間に順序を付ける関数です。これは本来コンパイル時のプロパティであり、C++20-23でstd::type_infoの取得と==による比較がconstexpr対応しているにも関わらず、この関数はconstexprではなく定数式で使用できません。

複数のポリシー型を受け取るテンプレートなどにおいてそのテンプレートパラメータを一定の順序でソートすることができれば、異なる翻訳単位の間で同じエンティティに対して同じシンボルを提供できるようになるなど、コンパイル時に型のソートを行うことは有用である可能性があります。ソートを行うためには<による比較が必要であり、しかもそれが移植可能であるためには実装の間で型の順序が一貫している必要があります。

この提案では、std::type_info::before()constexprにするために、C++における全ての型の順序付けがどのように決定されるかを詳細に検討するものです。この提案の内容はまた、std::meta::infoに全順序比較を提供するための順序付けのサブセットでもあるため、リフレクションの設計にも影響を与える可能性があります。

この提案のアプローチは次の手順によって順序付けを行います

  1. 言語内のすべての型について、key-tupleへの変換を定義する
  2. このkey-tupleによって順序を定義する

key-tupleは次の2つのいずれかを要素とするタプルです

key-tuple内の要素はatom優先の辞書式比較によってソートされ、2つのkey-tupleの比較は短い方に合わせて詰められます。

あるエンティティに対してこのkey-tupleを求める操作をsort_key(entity)とすると、例えば次のような型は

namespace foo::bar {
  struct i;
}

namespace baz {
  struct j;
}

それぞれ次のようなkey-tupleに変換されます

sort_key(foo::bar::i) = ((namespace, foo), (namespace, bar), (type, i))
sort_key(baz::j)      = ((namespace, baz), (type, j))

このとき、名前空間baz < fooであるため、baz::j < foo::bar::iのように順序付けされます。

atomは次のいずれかのものです

  1. kinds
    • トークンの種別を示す、次のいずれか
      1. value
      2. namespace
      3. type
      4. class template
      5. type alias template
      6. variable template
      7. concept
      8. function
  2. simple names
    • atom要素の名前を示す文字列
  3. qualifiers
    • 修飾子、次のいずれか
      • &
      • &&
      • const
      • volatile
  4. [] (要素数不明の配列型)
  5. [n] (要素数既知の配列型)
  6. * (ポインタ型)
  7. ... (ellipsis)
  8. パラメータパック (ellipsisと区別される)

atomはこの順番で順序付けされます。

名前空間namespace-nameに対するsort_key(namespace-name)(namespace, namespace-name)のようなkey-tupleに変換されます。これは、対応する位置で名前空間名がアルファベット順で並べられることを反映しています。

namespace outer1 {
  struct i;
}

namespace outer2 {
  namespace inner1 {
    struct i;
  }
  namespace inner2 {
    struct i;
  }
}

この3つの構造体は、sort_key(outer1::i) < sort_key(outer2::inner1::i) < sort_key(outer2::inner2::i)のように並べられます。

typeに対するsort_key(type)(type, <simple names>, <qualifiers>)のようなkey-tupleに変換されます。

qualifiersは次のようなスコアをすべて加算して、その結果によって昇順で並べられます。

&: 1
&&: 2
const: 3
volatile: 6

修飾なしの型をTとすると、可能な修飾子による順序は次のようになります

0  T
1  T &
2  T &&
3  T const
4  T const &
5  T const &&
6  T volatile
7  T volatile &
8  T volatile &&
9  T const volatile
10 T const volatile &
11 T const volatile &&

列挙型を除いた組み込みのスカラ型は、複合型よりも前に順序付けられます。スカラ型間の順序は次のようになります

  1. void
  2. std::nullptr_t
  3. bool
  4. char, signed char, unsigned char
  5. 整数型
    • ビット幅で順序付けした後で、符号付 < 符号なし で順序付け
  6. 上記のいずれかの型エイリアスではない、残りの文字型
  7. 浮動小数点数
    • float < double < long doubleの後に、そのほかの浮動小数点数型をサイズ順で順序付け
  8. 関数型
    • 戻り値型 -> 引数型 の辞書式順序によって順序付け
  9. ポインタ型
    • 参照先の型(*を取った型)によって順序付け
  10. メンバポインタ型
    • 参照先の型(*を取った型)によって順序付け
  11. 配列型
  12. クラス型

関数型のkey-tupleは、sort_key(<function>) = (function, <name>, sort_key(<return type>), (sort_key(<parameter>)...))のように変換されます。例えば

sort_key(void foo(int i)) = (function, foo, (type, void), ((type, int)))
sort_key(void foo(int)) = (function, foo, (type, void), ((type, int)))
sort_key(void foo(int, double)) = (function, foo, (type, void), ((type, int), (type, double)))

配列型は、sort_key(T[]) = ([], sort_key(T))もしくはsort_key(T[n]) = ([n], sort_key(T))のように変換され、要素の型->要素数の辞書式順序で順序付けされ、要素数不明の配列型は要素数既知の配列型の前に順序付けされます。

T[]
T[10]
T[11]
T[][2]
T[10][2]
T[3][2]

は、T[] < T[10] < T[11] < T[][2] < T[3][2] < T[10][2]のように並べられます。

非テンプレートのクラス型は、クラス名のアルファベット順で順序付けされます。

struct Apple {};
class Banana {};
struct Carrot {};

は、Apple < Banana < Carrotの順で並べられます。

クラステンプレートの場合は、sort_key(<class template>) = (type, (<name>, (sort_key(<parameter>)...)))のように変換され、クラス名 -> テンプレートパラメータの辞書式順序、によって順序付けされます。

template <typename T, typename U>
struct Apple;

struct Banana;
struct Carrot;

Apple<Banana, Carrot>;
Apple<Banana, Banana>;
Apple<Carrot, Carrot>;

に対して

sort_key(Apple<Banana, Carrot> = (type, (Apple, (sort_key(Banana), sort_key(Carrot)), )
                               = (type, (Apple, ((type, Banana, ), (type, Carrot, )), )

のようになり、Apple<Banana, Banana> < Apple<Banana, Carrot> < Apple<Carrot, Carrot>のように並べられます。

他にも、NTTP、メンバ関数型、可変長関数型、パラメータパック、変数テンプレート、エイリアステンプレート、コンセプト、等の順序付けについて検討されています。いずれも、エンティティ名としてアルファベット以外を考慮していませんが、その範囲内では直感的な順序付けになっています。

P2833R0 Freestanding Library: inout expected span

C++23のライブラリ機能の一部をFreestanding指定する提案。

この提案のFreestanding化候補は次のものです

また、std::out_ptr/std::inout_ptrファミリはstd::shared_ptrに対して使用することができ、std::shared_ptrは現状フリースタンディング指定されていないため、reestanding-deletedと指定することも提案されています。この指定がなされたクラステンプレートが実装(定義)されるか未定義であるかは実装の自由とされ、これによって、std::shared_ptrの実装が可能なライブラリ実装ではstd::shared_ptrをフリースタンディング環境で提供することができ、その実装が不可能な環境では未定義とすることで使用された場合にコンパイルエラーを発することができます。

P2836R0 std::const_iterator often produces an unexpected type

std::const_iterator<I>とコンテナ型のconst_iteratorの不一致を正す提案。

C++23で追加されたstd::const_iterator<I>は、イテレータIの間接参照結果をas_constして返すようなイテレータラッパです。これは、Iの参照型が既にconstである場合を除いてstd::basic_const_iterator<I>を使用し、std::basic_const_iteratorは単にIをラッパするクラス型です。

これによって例えば、std::const_iterator<int*>int const*ではなく、std::basic_const_iterator<int*>になります。これ自体も期待と一致しないかもしれませんが、さらに、コンテナ型からの定数イテレータの取得方法によって取得されるイテレータに差が生じる場合があります。

using V = std::vector<int>;
auto v = V();

using I1 = decltype(ranges::cbegin(v));
using I2 = ranges::const_iterator_t<V>;

static_assert(std::same_as<I1, I2>);  // fail!!

I1V::const_iteratorであるのに対し、I2std::basic_const_iterator<V::iterator>となり、このstatic_assertは失敗します。

ranges::cbegin(r)は、rangeのオブジェクトrに対してas_const(r).begin()を呼ぶのとほぼ同等の事を行い、標準コンテナの場合これはそのconst_iteratorを返すため、I1V::const_iteratorになります。

ranges::const_iterator_t<R>const_iterator<iterator_t<R>>に置換され、RイテレータIの読み取りがすでにconstでない場合はstd::basic_const_iterator<I>になり、標準コンテナをそのまま渡す場合はstd::basic_const_iterator<R::iterator>になります。

このように、定数イテレータを取得する経路の違いによって、このような差が生じています。

あるイテレータに対する定数イテレータの決定はこのようにその文脈によって変化し、振る舞いとしては一致しているかもいせませんが、型としての一貫性がありません。ranges::iterator_t<const R>ranges::const_iterator_t<R>は類似している必要があると思われ、この差異はその期待を破っています。これは、C++のライトユーザーにとっても専門家にとっても、期待通りの事ではないはずです。

この提案は、この問題を解決するために、まずconst_iterator_forを導入して

namespace std {
  // プライマリテンプレート
  template<input_iterator>
  struct const_iterator_for {};

  // const修飾を外すための部分的特殊化
  template<input_iterator I>
  struct const_iterator_for<const I> : const_iterator_for<I> {};
  
  // ポインタ型に対する部分的特殊化
  template<input_iterator I>
    requires is_pointer_v<I>
  struct const_iterator_for<I> {
    // T* -> T const * へ変換
    using type = const remove_pointer_t<T>*;
  };

  // 入れ子型const_iterator_forを持っている型に対する部分的特殊化
  template<input_iterator I>
    requires requires { typename I::const_iterator_for; }
  struct const_iterator_for<I> {
    using type = typename I::const_iterator_for;
  };

  // const_iterator_forを得るためのエイリアステンプレート
  template<input_iterator I>
    requires requires { typename const_iterator_for<I>::type; }
  using const_iterator_for_t = typename const_iterator_for<I>::type;
}

次に、標準ライブラリの全てのコンテナ型のイテレータ型のメンバ型としてconst_iterator_forを追加し、それを対応するconst_iteratorへリンク(エイリアス)することで、const_iterator_for_tを介してイテレータIから対応する定数イテレータを取得できるようにします。

そして、これを用いて次の2つの解決策を提案しています

  1. エイリアステンプレートstd::const_iteratorの定義を修正する
    • const_iterator_for_t<I>が利用できない場合にのみ、basic_const_iteratorを使用する
  2. std::basic_const_iteratorconst_iterator_for_t<I>への暗黙変換演算子を追加する

この提案は1の解決策を推しており、それはMSVC/STLをフォークして実装され、MSVC/STL実装者のレビューを受けているとのことです。

ただし、1の変更は元のconst_iteratorの意図的な設計上の選択を変更することになるため、ABIを破損します。とはいえ、現時点でMSVCのプレビュー版でしか実装されておらず、MSVCがそれを含んだABI安定版をリリースしていないため、今ならまだそれは間に合います。

P2838R0 Unconditional contract violation handling of any kind is a serious problem

契約プログラミング機能において、違反時の振る舞いを規定するモードをハードコードするのではなく、違反ハンドラをカスタマイズすることをデフォルトとする提案。

現在進行中のC++契約プログラミング機能においては、契約違反が発生した場合の振る舞いをビルドモードを通してカスタムするようにしており、現在はEval_and_abortモードのみがサポートされています。ここに、Eval_and_throwモードを追加しようとする提案(P2698)もあり、おそらく将来的には違反後にそれを無視して継続するようなモードも想定されます。

いずれにしても、これらの契約違反時の振る舞いはビルドモード(つまり、コンパイラオプション)によって変更されるものであり、契約違反時に異なる振る舞いをさせたくなった場合はプログラム全体の再コンパイルが必要となります。これはユーザーが直接書いているコードだけでなく、依存しているライブラリについても全て再コンパイルが必要です。どう考えてもそれは重すぎるコストであり、必ずしも常に可能であるわけではありません。それに対応するために、ビルド済みライブラリを提供するベンダー(あるいはOSS)は、契約機能のビルドモード毎にビルド済みバイナリを提供することになるかもしれません。これにもコストがかかります。

もしもこれを解消できこのようなコストを回避できる方法があるならば、それを最初から採用しておくべきです。

このことは、C++26(予定)以降の漸進的な修正・変更によって解決できる技術的な問題ではなく、契約機能をどのように使用できるのか、それを使用するコードをどのようにパッケージ化して配置可能なのか、どのような場合にバイナリの再コンパイルが必要となり、複数のバイナリを事前に提供しておく必要があるのか、まだどのような場合にその必要がないのかに直接影響します。すなわちこれは、ライブラリベンダーとそのユーザーの双方に直接影響を与え、OSSであるかに関係なくビルド済みバイナリの出荷方法に影響を与えます。

この提案によるこれら問題の解決策は、P2811を最初のC++契約機能に組み込むことです。

(P2811は上の方で解説済みなので詳しくはそちらを参照)

P2811では、契約違反時に呼ばれる関数である違反ハンドラをユーザーがカスタマイズ可能とすることを提案しています。これによって、契約違反時の振る舞いは違反ハンドラ(とそのカスタマイズ)を通して実装可能になります。

例えば、Eval_and_abortモードはデフォルトの振る舞いであり、違反ハンドラがその処理を終えて正常にリターンした場合にプログラムを中止することで実装されます。Eval_and_throwモードは、違反ハンドラをカスタマイズ可能(ユーザーが置換可能)とすることで、ユーザー定義の違反ハンドラ内から任意の例外をスローすることで実装可能となります。他にも、呼び出されるとそのスレッドをスリープさせたり、スピンループに入るように違反ハンドラをカスタムすると、あるスレッドで契約違反が起きた時でもプログラム全体を停止させずに続行させる動作を実装することができます。

現在のMVPの仕様の下でも、後からこのような違反ハンドラを通した実装に変更することは純粋な仕様の上では可能です。ただし、現在のMVPは実装がそのQoIの範疇で違反ハンドラの実装を省略(同等の振る舞いをするコードを翻訳単位にハードコーディング)することが可能であり、この選択をした実装が現れてしまえば、後からこの選択を取り消すことはできなくなります。その場合結局、最初の問題から逃れることができなくなります。

おわり

次回の提案公開時期は5月の終わりごろとのことなので、次回の投稿は6月になりそうです。

この記事のMarkdownソース