以下の内容はhttps://onihusube.hatenablog.com/entry/2025/01/26/185126より取得しました。


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

文書の一覧

全部で31本あります。

もくじ

N4988 Working Draft, Programming Languages -- C++

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

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

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

P2414R4 Pointer lifetime-end zap proposed solutions

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

以前の記事を参照

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

  • 2024年6月のSG1レビューを受けての更新
    • 無効なポインタへの操作の合法化に関する部分をP3347に分離
    • 関数とクラスの追加
  • フィードバックを受けてドラフトの文言を追加し更新
  • Historyセクションを最後に移動

などです。

このリビジョンでは次の事を提案しています

  1. make_ptr_prospective()を提案
    • ポインタを受け取り、そのポインタに対応する見込みポインタ(prospective pointer)を返す関数
  2. 指しているオブジェクトの寿命が尽きた後でも使用可能なポインタlikeな型であるクラステンプレート、usable_ptr<T>を提案
  3. 見込みポインタ値を生成して格納するようにアトミック操作を再定義
  4. 見込みポインタ値を生成して格納するようにvolatile操作を再定義

見込みポインタ(prospective pointer)値とは、寿命が開始されていないオブジェクトに対応するポインタ値のことです。これには、ストレージ領域がまだ作成されていないオブジェクトへのポインタも含まれ、見込みポインタもまた無効なポインタのように比較と間接参照以外の操作が許可されます(することを提案しています)。

見込みポインタを作成する唯一の方法は有効なポインタをuintptr_tにキャストしてからまた元のポインタ値に戻すことです。

make_ptr_prospective()関数はT型のポインタpに対するreinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p))の様な操作(見込みポインタの取得処理)を行うライブラリ関数です。

これらの概念および操作は、ポインタの指す先のオブジェクトが並行に破棄および再構築されるような場合においてその有効なポインタを取得するためのものです(例えば、ポインタ値をロードした後にそのポインタに対応するオブジェクトが別のスレッドで破棄されうる場合に、それを気にせずにそのポインタ値を使用する場合など)。P2434R1の"Consequences for pointer zap"セクションにサンプルコードと説明が少しあります。

P2822R2 Providing user control of associated entities of class types

ADLにおいて考慮される関連名前空間を制御する言語機能の提案。

以前の記事を参照

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

  • 関連エンティティの指定子によってテンプレートに名前を付ける機能の削除
  • モジュールからエクスポートされた明示的な関連エンティティ指定子を持つADLのルールについての文言を現在のセマンティクスに近づけるために文言を改善

などです。

P2897R4 aligned_accessor: An mdspan accessor expressing pointer overalignment

P2897R5 aligned_accessor: An mdspan accessor expressing pointer overalignment

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

以前の記事を参照

R4での変更は、is_sufficiently_aligned()aligned_accessorの静的メンバ関数ではなく非メンバ関数にしたことです。

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

  • is_sufficiently_aligned()<bit>ではなく<memory>に移動
  • is_sufficiently_aligned()に“Throws: Nothing”を指定し、その説明を追加

などです。

`std::is_sufficiently_aligned()`はこの提案で追加されている関数で、ポインタが期待するアライメントでアラインされているかどうかを調べる関数です。

namespace std {
  template<size_t Alignment, class T>
  bool is_sufficiently_aligned(T- r);
}

これは、あるポインタをaligned_accessorで使用する前にそのポインタが期待するアライメント要件を満たしているかどうかをチェックするための関数です。当初はaligned_accessorの静的メンバ関数でしたが、より一般に使用できるものであるためフリー関数になり、<memory>ヘッダに配置されました。

P2900R8 Contracts for C++

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

以前の記事を参照

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

  • 仮想関数でのpre()/post()のサポートを追加
    • P3097R0の内容
  • 契約アサーションをobservable checkpointにした
    • P3328R0の内容
  • メンバ関数ポインタについての説明を追加
  • 契約アサーションの定数評価に関する説明を追加
  • 契約違反ハンドラの同時呼び出しについての説明を追加
  • 契約述語はfull-expressionであることを明確化

などです。

P2988R6 std::optional<T&>

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

以前の記事を参照

このリビジョンでの変更は、右辺値参照型の特殊化(std::optional<T&&>)が追加されたことと、std::optional<T>Tに使用可能な型についての制約を修正したことです。

P2996R5 Reflection for C++26

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

以前の記事を参照

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

  • クエリ関数からフィルタを削除
  • accessibilityインターフェースをクリーンアップし、access_pair型を削除し、APIaccess_contextに基づいて再設計
  • is_noexceptの仕様を削減
  • span<info const>からinitializer_list<info>へ変更
  • test_traitを削除
  • (u8)name_of(u8)qualified_name_ofを削除し、(u8)identifier_of, operator_of, define_static_stringを追加
  • display_name_ofdisplay_string_ofに名前変更
  • 不足していた述語関数を追加
    • is_copy_constructor
    • is_move_constructor
    • is_assignment
    • is_move_assignment
    • is_copy_assignment
    • is_default_constructor
    • has_default_member_initializer
    • is_lvalue_reference_qualified
    • is_rvalue_reference_qualified
    • is_literal_operator(_template)
    • is_conversion_function(_template)
    • is_operator(_template)
    • is_data_member_spec
    • has_(thread|automatic)_storage_duration
  • offset APIを名前付きメンバを持つ型を返す1つの関数に変更
  • data_member_specの呼び出しの制約を厳しくし、それによって返されるリフレクション間の比較を定義
  • is_aliasis_(type|namespace)_aliasに変更
  • is_incomplete_typeis_complete_typeに変更
  • CWGのフィードバックを受けて文言を変更

などです。

P3050R2 Fix C++26 by optimizing linalg::conjugated for noncomplex value types

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

以前の記事を参照

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

  • conj(E)Eをきちんと定義する
  • __cpp_lib_linalgマクロのバンプ
  • “Presentation”セクションの追加

などです。

P3068R4 Allowing exception throwing in constant-evaluation

定数式においてthrow式による例外送出およびtry-catchによる例外処理を許可する提案。

以前の記事を参照

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

などです。

P3126R2 Graph Library: Overview

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

以前の記事を参照

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

  • 隣接リストのピアとして、抽象データ構造としてのエッジリストを追加
    • これにより、エッジリストの定義を完成するための未解決だった問題が解消
  • エッジリストのコンセプト・型特性・型にstd::graph::edgelist名前空間を付加
    • 同じ名前になってしまう型をエッジリストの型と区別する
  • Getting StartedセクションにP3337(グラフ比較提案)のリンク追加
  • 並行アルゴリズムが提案に含まれていない事を明確化

などです。

P3130R2 Graph Library: Graph Container Interface

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、グラフの実体となるコンテナのインターフェースについてまとめた文書。

以前の記事を参照

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

  • 隣接リストのピアとして、抽象データ構造としてのエッジリストを追加
    • これにより、エッジリストの定義を完成するための未解決だった問題が解消
  • コンセプト内の不要なエッジパラメータEを削除
  • 型特性is_unordered_edgeis_ordered_edgeを削除
    • 対応するコンセプトunordered_edge, ordered_edgeで必要なかったため
  • edge_id(g,uv)edge_id_t<G>を削除
  • basic_targeted_edgeコンセプトのtarget_id(g,uv)の戻り値型が検証されない理由の説明を追加

などです。

P3131R2 Graph Library: Graph Containers

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、グラフの実体となるコンテナ実体についてまとめた文章。

以前の記事を参照

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

  • 隣接リストのピアとして、抽象データ構造としてのエッジリストを追加
    • エッジリストに関するセクションがUsing Existing Data Structuresセクションに追加
  • compressed_graphの機能概要にis_directedを追加

などです。

P3284R1 finally, write_env, and unstoppable Sender Adaptors

新しいsenderアルゴリズムの提案。

以前の記事を参照

このリビジョンでの変更はよくわかりません。

P3315R0 2024-07 Library Evolution Poll Outcomes

2024年2月に行われたLEWGの投票の結果。

次の2つの提案が投票にかけられ、どちらもC++26を目指してLWGに転送されました。

賛否の票数や投票に当たって寄せられたコメントが記載されています。

P3325R3 A Utility for Creating Execution Environments

std::exceutionにおけるExecution Environmentsを扱うためのライブラリ機能の提案。

以前の記事を参照

このリビジョンでの変更は、propenvの代入演算子を削除したことです。

P3347R0 Invalid/Prospective Pointer Operations

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

この提案はP2414R3で提案されていた言語への変更を分離したものです。

P2414R3ではオブジェクトの寿命が尽きた後にそのオブジェクトを指していたポインタが無効化される(ほとんどの操作が未定義もしくは実装定義になる)問題(Pointer lifetime-end zapと呼ばれる)を解決するために言語とライブラリの双方からの提案を行っていました。しかし、言語パートのみを単独でEWGに進めるため(議論およびレビューをしやすくするためと思われる)にその言語提案部分を単独分離したのがこの提案です。

P2414R3およびこの提案では、無効なポインタおよび暫定的なポインタ(無効化された後にその領域に再びオブジェクトが構築された場合のポインタ)に対して、比較と間接参照以外の操作において無効および暫定的なポインタ値の表現バイト列を忠実に計算することを要求するようにすることを提案しています。より詳細には

  1. 比較および間接参照以外のポインタに対する操作は、そのポインタが無効であるかどうかなどに関わらずその表現バイトを忠実に計算しなければならない
    • 実装は、ポインタが指しているオブジェクトの有効期間の終了に伴ってポインタの表現バイトを変更することはできない
  2. 比較ではないポインタの操作においては、そのオペランドに一致した表現バイトを生成しなければならない
    • ある無効なポインタに整数定数を加算した場合、それと同じ初期表現バイトを持つ有効なポインタに同じ値を加算した時と同じ表現バイトにならなければならない
  3. 比較操作は決定論的でなければならない。すなわち、ポインタAとBのペアがあるとき、これを連続して何回比較しても一貫した結果が得られる必要がある
    • ただしこれはポインタAとBがそれぞれ同一(provenanceも含めて)である場合にのみ保証される
      • 例えば、Aから派生したポインタCが翻訳単位を超えるなどしてそのprovenanceが再計算されている場合、AとBの比較結果とCとBの比較結果が一貫している必要はない
  4. ポインタの実装にトラップ表現(値を表現しないビットパターン)が存在する実装では、特別な注意が必要

のようなことを提案しています。

この提案はまた、P2434R1で提案されているangelic provenance(整数からポインタへのキャストの際、プログラムに定義された動作をもたらすポインタ値が1つ以上存在する場合そのような値の一つが結果となる、のような非決定的なポインタの選択方法)に基づいて、その提案が採択されたことを前提として、無効および暫定的なポインタに対する操作(比較と間接参照以外)を定義しています。これは、元々P2414R3が救済しようとしていた無効なポインタを活用する並行アルゴリズムがP2434のメリットを享受できるようにするためです。

P3348R0 C++26 should refer to C23 not C17

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

とはいえ、C++から参照するCの部分とはほぼCライブラリに関する部分のみなので、C23に合わせたライブラリの更新を行うことを提案するものです。

提案で挙げられている差分は次のようなものです

  • <stddef.h>における__STDC_VERSION_STDDEF_H__など、ヘッダーにバージョンマクロが追加された
    • C++では、これらのマクロを必須にするか、定義しないことを必須にするか、実装定義または未指定にする必要がある
  • <stddef.h>nullptr_tというtypedefとunreachableというマクロが追加された
    • これらはC++にはすでに存在する(<cstddef><utility>
  • once_flag型とcall_once関数が<stdlib.h>に追加された
    • C++には<mutex>に独自のstd::once_flagstd::call_onceがあるため、これらは不要
  • alignasalignofマクロがキーワードになった
    • その結果、C23の<stdalign.h>ヘッダーは空になった
    • C++では非推奨になった2つのマクロのみを定義していたがC23ではこれらは存在しないため、ヘッダー全体を非推奨として最終的には削除することが望ましいものの、それはC23の範囲ではない
  • booltruefalseマクロがキーワードになった
    • その結果、C23の<stdbool.h>ヘッダーは、廃止(非推奨)されたマクロ__bool_true_false_are_definedを除いて空となった
    • このマクロはC++23ですでに非推奨になっている
    • stdalign.hと同様に、ヘッダー全体を非推奨にしてから削除することが望ましいが、同様の理由によりヘッダーは変更されずその内容は非推奨のままとする
  • asctime関数とctime関数が非推奨になった
    • C++でも同様にする必要がある
  • DECIMAL_DIGマクロが非推奨となった
    • C++でも同様にする必要がある
  • FLT_HAS_SUBNORM, DBL_HAS_SUBNORM, DBL_HAS_SUBNORMマクロは廃止予定とされている
    • SG6はC++でも同様にするかどうかを決定する必要がある
  • INFINITYマクロとNANマクロが<float.h>で定義された
    • C17では<math.h>で定義されておりC23でもまだ存在するが、そこで定義することは非推奨になっている
    • C++では、これをどうするか決定する必要がある(おそらくC23と同じようにする必要がある
  • FLT_SNANDBL_SNANLDBL_SNAN<float.h>に追加された
    • C++にはstd::numeric_limitsに相当するものがあるものの、これらを追加しても問題ないように思われる
  • <fenv.h>への機能追加:
    • femode_t型とFE_DFL_MODEマクロ
    • 新しい丸め方向であるFE_TONEARESTFROMZERO
    • FENV_ROUNDプラグマ
    • fesetexcept()関数とfetestexceptflag()関数
    • fegetmode()関数とfesetmode()関数
    • C++では、これらのものが必要かどうかを検討する必要がある
  • <math.h>への追加:
    • 10進浮動小数点数
    • 新しい関数fromfp(), ufromfp(), fromfpx(), fromupx()
    • FP_INT_UPWARDなどの新しい数学的丸め方向
    • FP_FAST_FMAFP_FAST_FMAFFP_FAST_FMALなどの新しいマクロ
    • FP_FAST_FADDなど、18個の新しいマクロ
    • C++では、これらのものが必要かどうかを検討する必要がある
    • fromfpufromfpfromfpxfromupxfmaximumfminimumfmaximum_magfminimum_magfmaximum_numfminimum_numfmaximum_mag_numfminimum_mag_numnextdownnextupなどの新しい関数
  • 同じく<math.h>に、新しいiscanonicalマクロと、浮動小数点型における正規表現と非正規表現というコア言語の概念
    • これらは主に10進浮動小数点型に必要なものであり、現時点ではこれらを<cmath>に追加する必要はない
  • <bit>と機能が重複する新しいヘッダー<stdbit.h>
    • LEWGは、C++の機能を使用して再指定する形で<stdbit.h>を追加することについて合意したが、Cと同じ名前を使用している
    • これはこの提案(リベース目的)の一部ではないため、別の提案として検討する必要がある
      • なお、<cstdbit>C++に追加することについては合意がない
  • 加算、減算、乗算におけるオーバーフローをチェックするための関数を備えた新しいヘッダー<stdchkint.h>
    • C++には現在相当するものはないものの、Cのような型ジェネリックマクロはおそらく不要
    • LEWGはC++の機能を使用して再指定する形で<stdchkint.h>を追加することについて合意した
      • これはこの提案(リベース目的)の一部ではないため、別の提案として検討する必要がある

LEWGは2024-07-30の電話会議でこの提案について議論し、<string.h><time.h><stdlib.h>に新しい関数を追加すること(C23で追加された上にないもの?)、strftimeに新しい%OBおよび%Ob形式を含めることについて合意を得たようです。

P3361R0 Class invariants and contract checking philosophy

P3361R1 Class invariants and contract checking philosophy

契約プログラミング機能とクラス不変条件や違反時の扱いなどについて議論されている文書。

この文書では契約プログラミングとテストの関係性などから、クラス不変条件についてなどが主に理論的な側面から議論されています。

SG21における議論を促すためのもの、とあるので、今のところ何か提案をしているわけではなさそうです。

P3362R0 Static analysis and 'safety' of Contracts, P2900 vs. P2680/P3285

静的解析の観点からP2900のContracts機能は不十分であるため、P2680/P3285で提案されている副作用や未定義動作を予め制限するアプローチを取るようにする提案。

P2900のContracts機能では契約条件式に対して特段の制約を設けておらず、そこではほとんど通常のC++コードを記述可能で、そのような述語コードは任意の副作用やUBを伴うことができます。

これに対してP3285ではコンベア関数の概念を導入し、コンベア関数内では未定義動作につながりうる操作を一切禁止し一部のUBの動作を定義することでUBフリーな関数の定義を可能にし、なおかつ非緩和契約という概念(その関数の呼び出しによって発生する副作用はその評価コーンの内側に閉じていること)によってUBと副作用のない関数を作成できるようにしています。これを契約述語として契約条件式に用いることでContracts機能はUBと副作用を禁止できます(P3285ではさらに緩和契約によってP2900相当の制限のない契約も行うことができる)。

この提案は、契約条件式におけるUBと副作用の問題点という軸でP2900とP3285の提案を比較した場合、特に静的解析において有利なのがP3285であるとして、そちらのアプローチを採用することを提案するものです。

例えば次のようなコードにおいて

// 別の翻訳単位で定義されている述語
bool pred(int x);

// pred()を用いて契約された関数
int func_a() post(r: pred(r));
void func_b(int x) pre(pred(x));

int main() {
  func_b(func_a()); // このコードは正しいか?
}

これらの宣言のみから、main()関数内における関数呼び出しの妥当性は判断できるでしょうか?答えはできない、でしょう。先に呼ばれるfunc_a()の事後条件と後から呼ばれるfunc_b()の事前条件が同じであるため、正しいかもしれないと推測するかもしれません。しかし、そこで使用されているpred()は定義が見えておらず、任意の副作用やUBを伴う可能性があります。

pred()が副作用やUBを伴っている場合、この2回の呼び出しで両方の結果が同じになる保証がなく、これらの事前条件と事後条件は接続されているとみなすことはできません。人間に判断できないように、これは静的解析機でも判断できません。

一方で、P3285のコンベア関数(と非緩和契約)による述語だとどうでしょうか?

// 別の翻訳単位で定義されている述語(コンベア関数
bool pred(int x) conveyor;  // 構文は未定

// pred()を用いて契約された関数
int func_a() post(r: pred(r));
void func_b(int x) pre(pred(x));

int main() {
  func_b(func_a()); // このコードは正しいか?
}

この場合、pred()はUBや副作用がないことが分かっているため、人間も静的解析機もfunc_a()の事後条件と後から呼ばれるfunc_b()の事前条件は同じ値に評価され、接続していることが分かります。重要なことは、これはpred()の定義を見に行かなくても分かるという点で、静的解析の負荷が大きく軽減されます。

この特性はさらに、静的解析だけではなく実行時チェックにも適しています。副作用やUBが無ければ、プログラマがチェックしたいことをチェックするためのコードの記述が非常に簡単になるためです。

このようなことはP2900の緩和契約のみでは達成することはできず、P2900のContracts機能は実際には静的解析の助けになりません。ただしこの提案はP2900が不要であると言っているのではなく、緩和契約とともにコンベア関数のように厳格に制限された契約も同時に必要であると主張しています。当然すべての契約条件がコンベア関数のように厳格な形で記述できるわけではなく、緩和契約でしか記述できない場合の方が多いでしょう。

ただし、P2900の緩和契約を先に標準化してしまった場合、この厳格な契約を後から導入することはできないか、困難になります。両方は同時に標準化される必要があります。

P2680/P3285の提案はSG21およびSG23においてあまり好意的に受け取られておらず、主な反対意見は「言語内で別のより一般的な保護を導入すれば(プロファイル提案など)、契約条件を特別扱いする必要はない」というものです。これが間違っている理由としては

  1. そのようなより一般的な保護はオプトインである可能性が高い
  2. 副作用の制限についてはあまり考慮されていない

の2点を挙げています。

ここで強く主張されていることは、言語の他の部分が副作用とUBフリーであるかどうかに関係なく、契約注釈内が副作用とUBフリーであることが重要であり契約注釈には特別な保護が必要である、ということです。

P3364R0 Remove Deprecated u8path overloads From C++26

非推奨だったu8path()を削除する提案。

u8path()C++17の<filesystem>ライブラリの一部であり、char8_tが無かった時代にUTF-8エンコードされたcharの文字列からfilesystem::pathオブジェクトを構築するために追加されました。しかしその後のC++20にてchar8_t文字型とそれを使用した文字列型が導入され、filesystem::pathのコンストラクタでも文字型でエンコーディングが識別できるようになったため不要になり、非推奨とされていました。

この提案は、これを削除しようとするものです。

この提案の内容そのものはC++23のサイクル中にLEWGにおいて合意が取れていましたが、筆者の方のリソース不足のため間に合いませんでした。その間にLWG Issue 3840にて非推奨を解除すべきとするIssueが提出されC++26サイクルで改めてレビューされたようです。

SG16ではこの関数の現状維持が推奨され、LEWGでは非推奨解除と削除のどちらにも支持者がおり、どちらの方向性でも合意を得ることは難しい雰囲気のようです。

この提案はどうやら、SG16において改めて方向性を確認して議論するために個別の提案として分離されたようです。

P3365R0 Remove the Deprecated iterator Class Template from C++26

非推奨だったstd::iteratorクラステンプレートを削除する提案。

std::iteratorはカテゴリや値型等を指定して継承することでイテレータの定義を簡単にするためのクラステンプレートでしたが、入れ子型の定義しかしてくれず、どのテンプレートパラメータが何を指定しているのかがusing/typedefで書いてある時よりも分かりづらくなり、さらにそれらの入れ子型定義が継承しているクラス内から使用できないなどの問題があり、あまり役に立たなかったためC++17で非推奨とされました。

C++20ではiteartor_traitsのアップデートなどによってこのクラステンプレートを改めて使用する意義は完全になくなっており、むしろ間違って使用すると有害になる可能性があるとして、この提案では削除しようとしています。

C++20時点では、std::iteratorのデフォルトテンプレート引数を利用する有効な使用例が存在しており、そのようなコードが壊れてしまう懸念などから削除にはコンセンサスが得られませんでした。

しかし、C++23時点では<ranges>の導入に伴うiteartor_traits周りのアップデートによってstd::iteratorに依存していたコードが減ったため、削除へのコンセンサスが高まりました。それを受けてこの提案が単独で分離されたようです。

P3366R0 Remove Deprecated Atomic Initialization API from C++26

非推奨だったstd::atomicの初期化関数を削除する提案。

当初のstd::atomicクラステンプレートのデフォルトコンストラクタは内部に保持する値を未初期化のまま構築していました。これはCとの互換性のためだったとのことです。このため、std::atomicを値初期化するためのユーティリティである`std::atomic_init()/ATOMIC_VAR_INIT`が用意されていました。

しかし、C++20でstd::atomicのデフォルトコンストラクタの振る舞いが修正されたためこれらのものは不要になり、非推奨にされました。この提案はこれらのものを削除しようとするものです。

ATOMIC_VAR_INITマクロはCからのものでしたが、C17で非推奨化されC23で削除されています。C規格への参照をC23に更新するならば、これを削除する必要があります。非推奨とはいえ文言を標準に維持するにはコストがかかるため、ATOMIC_VAR_INITと一緒に同種の`std::atomic_init()`も削除しようとしています。

P3369R0 constexpr for uninitialized_default_construct

uninitialized_default_construct()cosntexpr指定する提案。

P2283R2にて未初期化領域の初期化を行うアルゴリズム関数の多くがconstexpr対応されました。そこでは定数式での初期化をstd::construct_at()を用いて指定していましたが、std::construct_at()は値初期化しか行うことができず、デフォルト初期化を行うuninitialized_default_construct()constexpr対応されませんでした。

P2747R2にて配置newの定数式での許可が提案されており、これは2024年6月の全体会議でC++26に導入されています。これを前提にすると、規定の変更を全く行うことなくuninitialized_default_construct()constexpr指定できるため、この提案ではこれらの関数にconstexprを追加することだけを提案しています。

この提案の対象は`std::uninitialized_default_construct()std::ranges::uninitialized_default_construct()の並行アルゴリズムではないオーバーロードです。

この提案はこのリビジョンがそのまま2024年11月の全体会議で採択されています。

P3370R0 Add new library headers from C23

C23で追加された新しいライブラリヘッダ<stdbit.h><stdchkint.h>C++にも追加する提案。

<stdbit.h><bit>にあるようなビット演算関数が提供され、<stdchkint.h>ではオーバーフロー検査付きの整数演算関数が提供されます。これらのヘッダはどちらもC23で追加されたものであり、Cとの互換性のためにC++でも利用可能にしようとする提案です。

ただし、あくまでCとの相互運用性向上のための提案であるため<cstd...>形式のC++ヘッダを提供するわけではありません。また、型ジェネリックマクロは関数テンプレートに置き換えられて追加されます。

P3371R0 Fix C++26 by making the symmetric and Hermitian rank-k and rank-2k updates consistent with the BLAS

<linalg>の一部の関数を対応するBLAS関数の仕様と整合させる提案。

<linalg>にある関数群は基本的に、BLASの関数のどれか1つ(以上)に対応しており、対応するBLAS関数で行える計算はすべて行えるように設計されています。ただし、一部の関数はそうなっていなかったことが報告されました。

BLASDSYRK関数はC := βC + αAA^Tの計算行いますが、<linalg>で対応するsymmetric_matrix_rank_k_update()C := C + αAA^Tを計算します。つまり係数βを適用する方法が無くなっています。

そして、同種のrank-k update系の関数でも同様の問題があることが分かりました

  • linalg::symmetric_matrix_rank_k_update(): C := C + αAA^T
  • linalg::hermitian_matrix_rank_k_update(): C := C + αAA^H
  • linalg::symmetric_matrix_rank_2k_update(): C := C + αAB^H + αBA^H
  • linalg::hermitian_matrix_rank_2k_update(): C := C + αABH + ᾱBAHᾱα複素共役

いずれの関数でも、Cに対して係数βを適用しなくなっています。

これらの関数は行列と行列の積の特殊なケース(入力が三角行列であり、結果が対称行列orエルミート行列になる)を処理する関数ですが、通常の行列と行列の積(xGEMM: C := βC + αAB)に対応するlinalg::matrix_product()では、次の2種類のオーバーロードが用意されています

  1. 上書き: C := AB
  2. 更新: C := E + AB

更新オーバーロードではスケール係数α, βlinalg::matrix_product(scaled(alpha, A), B, scaled(beta, C), C)(引数順はA, B, E, CECエイリアスしていることを許容)のようにすることで処理しています。BLASの関数は基本的にこのように、BLASの関数の処理をC++で記述して実行できるように上書きと更新の2つのオーバーロードを提供しています。

しかし、symmetric_matrix_rank_k_update()をはじめとする問題の関数は更新オーバーロードがなく、linalg::matrix_product()の更新・上書きのどちらのオーバーロードとも異なるセマンティクスを持っています。また、BLASDSYRK関数(および同種の対応するその他の関数)はβ係数を0にすることで上書きを行うことができます。

この提案では、この問題の解決のために上記4つの関数をlinalg::matrix_product()等の関数と同じように動作させるようにすることを提案しています。具体的には次の2つの変更を提案しています

  1. 入力行列引数Eを持つ更新オーバーロードを追加
    • 例えば、symmetric_matrix_rank_k_update(A, E, C, upper_triangle)C := E + AA^Tを計算し、CEエイリアスを許可することでEscaled(beta, C)のように指定して係数βを適用するようにする
    • Eは入力行列(in-matrix)となり、Cは入出力行列(possibly-packed-inout-matrix)から出力行列(possibly-packed-out-matrix)に変更
  2. 既存のオーバーロードの動作を、呼び出し方法を変更せずに上書きオーバーロードに変更
    • 例えば、symmetric_matrix_rank_k_update(A, C, upper_triangle)C := AA^Tを計算するようになる
    • Cは入出力行列(possibly-packed-inout-matrix)から出力行列(possibly-packed-out-matrix)に変更

2つ目の変更により、BLASでは提供されている上書き動作(C := αAA^T)を提供できるようになりますが、この変更は破壊的変更ともなります。そのため、この提案はC++26サイクル中に完了する必要があるとしています。

P3372R0 constexpr containers and adapters

標準のコンテナを全てconstexprにする提案。

この提案の前後の比較をまとめた表

container / adapter 現在 この提案 note
std::array C++11/14から
std::deque
std::forward_list
std::list
std::vector C++20 から
std::map node_handle::key()の使用がUBになる (CWG-2514)
std::multimap node_handle::key()の使用がUBになる (CWG-2514)
std::set
std::multiset
std::unordered_map node_handle::key()の使用がUBになる (CWG-2514)
std::unordered_multimap node_handle::key()の使用がUBになる (CWG-2514)
std::unordered_set
std::unordered_multiset
std::queue
std::priority_queue
std::stack
std::flat_map C++23で追加された新しいコンテナアダプタ
std::flat_multimap C++23で追加された新しいコンテナアダプタ
std::flat_set C++23で追加された新しいコンテナアダプタ
std::flat_multiset C++23で追加された新しいコンテナアダプタ
std::span
std::mdspan
std::basic_string C++20 から
std::basic_string_view C++17から

この変更のために必要な言語機能は現在すべて定数式で実行可能であるため、これらがcosntexprではない理由はもはやなく、標準ライブラリは適切に定数式サポートをすべき、という提案です。

実際、筆者の方はこれらをlibc++のフォークにおいて実装しており、いくつか問題はあったものの実装可能だったと報告しています。

問題があったのは主に連想コンテナ関連で、1つはノードの構築時にUBが起きていたことで、ノードの領域が割り当てられた後、ノード型のオブジェクト全体が構築される前にそのサブオブジェクトの一部だけが先に構築されていたことです。もう1つは、ハッシュの計算時にunionstatic_castを使用してsize_tからunsigned charにtype punningしていたことです。この問題はどちらもstd::bit_castによって回避可能だったとのことですが、こうした問題が検出できたことこそがconstexprを付加して定数式で実行可能にすることの有効性を物語っています(定数式ではUBにつながる操作が禁止されているため、それに遭遇するとコンパイルエラーになる)。

非順序連想コンテナにおける残りの問題は、ceilf(float)hash.cpp素数計算関数がconstexprではないこと、および非constexprstd::hash型テンプレートがないことでした。

std::hash<T*>constexprにするために解決する必要のあるUBとして、ポインタを整数値へ変換することがあります。現在定数式ではこれを行えませんが、この提案の実装に当たってはstd::bit_castを拡張することで対処したようです。ただしこの方法だと定数評価中のハッシュに対して一貫性のない結果を返すという問題があるようです(とはいえこれはstd::less<T*>等を使用したポインタ比較にも当てはまります)。この提案では、std::bit_castによってポインタからのキャストのみを許可(ポインタにキャストすることは不許可)することを提案しています。

連想コンテナのノードハンドルについても解決が必要なUBが1つあり、node_handle::key()node_type内ではconstであるキー型の非const参照を返すことです(node_handle::key()を通してキーを変更できるようになっている)。CWG Issue 2514として報告されている問題であり、constexprの問題というよりは連想コンテナの仕様の問題です(このAPIの妥当性など)。

P3373R0 Of Operation States and Their Lifetimes

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

C++26のstd::executionでは、senderreceiverという抽象を中心に据えた非同期操作のフレームワークとなるライブラリを提供しています。そこでは、非同期操作はsenderアルゴリズムをチェーンしていくことによって構成され、そのような非同期操作を表すsenderreceiverを接続してoperation_stateを取得し、operation_stateによって非同期操作を開始できます。

// 簡単な非同期操作の例
// 5を生成し、floatにキャストして返す
std::execution::sender
    auto async_op = std::execution::just(5) | std::execution::then([](const int i) noexcept { return float (i); });

senderによって表現された非同期操作はその内部にいくつもの状態を抱えており、その状態はoperation_stateが有効である間有効であり続けるため、非同期操作の実行中に非同期操作が参照する状態が先に寿命を終えている、といったことが起こらないようになっています。

// デストラクタでログ出力
struct print_from_destructor {
  ~print_from_destructor() noexcept {
    std::cout << "Destructor" << std::endl;
  }
};

auto ptr = std::make_unique<print_from_destructor>();

// 単純なreceiverの実装
struct receiver {
  using receiver_concept = std::execution::receiver_t;

  void set_value(const int& i) noexcept {
    std::cout << "*" << &i << " = " << i << std::endl;
  }

  void set_error(std::exception_ptr) noexcept {}
  void set_stopped() noexcept {}
};

int main() {
  // scheduler(処理を実行する場所を指定するもの)を取得
  // スレッドプールなど
  const auto scheduler = ctx.get_scheduler();
  
  // 非同期操作の構成
  auto async_op = std::execution::just()
                | std::execution::then(
                    [p = std::move(ptr), vec = std::vector<int>{1, 2, 3}] () noexcept
                    {
                      std::cout << &vec.front() << std::endl;
                      return std::cref(vec.front());
                    }
                  )
                | std::execution::continue_on(scheduler);
  
  // 非同期操作状態(operation_state)の取得
  auto op_state = std::execution::connect(async_op, receiver{});

  // 実行の開始
  std::execution::start(op);
  
  // 実行コンテキスト固有の操作
  ctx.run();
}

これを実行すると、例えば次のような出力が得られます

0x6020000000f0
*0x6020000000f0 = 1
Destructor

非同期操作(async_op)に内包される状態の生存期間は非同期操作が実行されている間、ではなく、非同期操作状態(op_state)の生存期間に紐づけられています。そのため、2つめのthen()に渡されているラムダ式から返されるローカルstd::vectorの要素の参照は、then()の実行が終わった後も全体の処理が終了した後も生存しており、receiverset_valueチャネルが呼ばれる際(非同期操作全体が正常終了して値が返される時)でも安全に使用できます。

一方、これと同等の処理を同期的に書くと、例えば次のようになります

int main() {
  const auto a = []() {
    const print_from_destructor print;
    const std::vector<int> vec{1, 2, 3};

    std::cout << &vec.front() << std::endl;
    return std::cref(vec.front());
  };

  const auto b = [](const int& i) {
    std::cout << "*" << &i << " = " << i << std::endl;
  };

  b(a());
}

結果としては同じことをしてはいますが、このコードはUBになります。なぜなら、a()がダングリング参照を返しているからです(vecの生存期間はa()の呼び出し内で終わっています)。

このようにstd::executionの非同期操作にまつわる状態の生存期間は、operation_stateについて束縛されていて、必ずしも対応する同期コードの場合とは異なっています。

この挙動は動作を安全側に倒したものでもあり望ましい動作でもあります。しかし、次のように非同期操作がネストしていると少し気になる部分が出てくるかもしれません

template<typename Writable>
auto write(Writable& writable, std::span<const std::byte> span) noexcept {
  return std::execution::just(std::move(span))
       | std::execution::let_value(
          [&writable](std::span<const std::byte>& span) noexcept
          {
            return ::exec::repeat_effect_until(
                      std::execution::just() | std::execution::let_value(
                                                [&writable, &span]() noexcept(noexcept(writable.write(span)))
                                                {
                                                  return writable.write(span);
                                                })
                                             | std::execution::then(
                                                [&span](const std::size_t bytes_transferred) noexcept
                                                {
                                                  span = span.subspan(bytes_transferred);
                                                  return span.empty();
                                                }
                                               )
                    );
          }
        );
}

この処理は

  • 全てのバイトの書き込みが完了する
  • エラーが発生する
  • キャンセルが要求される

のいずれかが発生するまでspanの参照する領域を少しづつwritableへ書き込み続けます。exec::repeat_effect_untilは標準のものではなありませんが、渡された非同期操作(sender)を繰り返し実行するものです。

このように非同期操作(senderが複雑に入れ子化している場合でも)、入れ子のものも含めて非同期操作の状態は実行に伴って得られるoperation_stateに関連付けられています。そのため、内部の入れ子ループ(repeat_effect_until())では何度操作を繰り返しても入力のspanの寿命が尽きることはありません。

逆にこの場合、入れ子を実行中の親の前段階の処理や、入れ子の各ループの処理などにおける状態も、最終的な一番外側のsenderから構成されたoperation_stateの終了に関連付けられています。

このように、std::executionの現在の設計では非同期操作内の状態の生存期間を最大限に確保しており

  • 最大量のストレージを使用する
  • 同期コードでは危険なアクセス方法でも、安全に動作する(最初は驚きを伴うだろう
  • ネストした非同期操作においてRAIIでリソース(特にロック)管理しようとする場合、上手くいかない可能性がある

のようなデメリットがあります。

これに対してこの提案は、ネストした非同期操作における生存期間をより短くとるようにすることで、これらのデメリットを緩和しようとするものです。

提案では代替案として

  1. 最小限: ネストした非同期操作の操作状態はreceiverによって破棄されるようにする
    • ストレージ消費を最小化
    • 未定義動作が発生しやすくなる
    • RAIIで管理されるリソースのスコープをより直感的に定義できる
    • 欠点として
      • ネストした非同期操作状態の生存期間を手動で管理する必要がある
      • set_value, set_error, set_stopped関数内で、外側の非同期操作に関連付けられたreceiverの生存期間が終了する可能性がある
  2. アドホック: アルゴリズム毎にネストした非同期操作の状態についてどうするかを決める
    • この場合、最大限と最小限のアプローチの欠点の両方に苦しむことになる
  3. 実装定義: 実装者の決定にゆだねる
    • 実装自由度を優先する物にも見えるが、ユーザーは常に最小限の生存期間を仮定してコードを書かざるを得ない(が最小限のアプローチのメリットが必ずしも得られない)
    • その選択は、最初の数人の実装者が意図的かそうでないかにかかわらず選択したものがデファクトスタンダードとなる、ハイラムの法則の世界に生きることになる

の3つを提示し、提案としては最小限のアプローチを推しています。

最小限のアプローチの未定義動作が発生しやすくなる、というのは一見するとデメリットですが、その状況は大きく「分かっている人間が意図的に(実際には動作すると知って)書いた場合」と「分かっていない人間が誤って書いてしまった場合」に分けられます。

前者の場合、この変更が適用された後でもその人は別の書き方を選択できます。

後者の場合、これは実際には利点として捉えられます。なぜなら、現状維持をする場合、標準のsenderアルゴリズム群が最大限の生存期間の保証をしているからと言って非標準の在野のアルゴリズムがそれを提供しているとは限らず、非標準のアルゴリズムを使用しながら標準と同じ書き方をしてしまう、というリスクを回避することができるからです。

P3374R0 Adding formatter for fpos

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

iostream APIはI/Oを徹底的に抽象化しており、その重要な一部としてストリームの現在位置の表現があります。これはchar_traits<T>::pos_typeによって取得されることが多く、デフォルトではfpos<typename char_traits<T>::state_type>が使用されており、実際に標準ライブラリで使用されるのはfpos<mbstate_t>のみです。

fpos<mbstate_t><<によって標準出力ストリームには出力可能ですが、フォーマッタが無いためstd::printを使用できません。

std::ofstream s{"some_file"};

// Do some output...
std::cout << s.tellg();        // ❓ 全ての実装で動作するがロバストではない
std::println("{}", s.tellg()); // ❌ Compile error...

// <<と同じように動作させるにはこう書かなければならない
std::println("{}", (std::streamoff)s.tellg());    // 😢What?
std::println("{}", s.tellg() - std::streampos{}); // 😢No way...

また、位置の状態(fposの持つ追加情報)は通常無視されることから、<<で出力するにはロバストではない場合があります。

多くのユーザーはストリーム位置を単なる整数値だと捉えていますが実際にはそうではなく、fpos<mbstate_t>は整数値に暗黙変換可能であるもののそれ以上の情報を持っています。<<による出力は整数値へ暗黙変換されたものが出力されているため、この追加の情報が無視されています。

そのため、ここで提案するfpos<mbstate_t>のフォーマッタも単なる整数値としての出力を行うものではなく、追加の情報を出力できるようにしています。ただし、それは実装定義な部分が多いため、完全に規定することが難しいものでもあります。

ここで提案するフォーマッタは次のような動作をします

  • {}, {:}などオプションが指定されていない場合、"(position, mbstate descriptor)"のような出力を行う
    • mbstate descriptorfposの状態を完全に復元するために使用できる情報を示す文字列
  • オプションによって、位置の整数値のみを出力する
std::ofstream s{"some_file"};

// Do some output...
std::cout << s.tellg();           // ❓ 全ての実装で動作するがロバストではない
std::println("{}", s.tellg());    // ✅ ロバストに動作
std::println("{:d}", s.tellg());  // ✅ ロバストではないがユーザーが明示的に指定する


std::stringstream s2{"ABC"};

std::println("{:d}", s2.tellg()); // ✅ 特に、codecvtを使用しないストリームの場合はこれで良い

この提案では、整数値で位置のみを出力するオプションをとりあえずdとしています。

おわり

この記事のMarkdownソース




以上の内容はhttps://onihusube.hatenablog.com/entry/2025/01/26/185126より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14