文書の一覧
全部で80本あります。
もくじ
2024年3月末に東京で行われるWG21全体会議の案内。
2023年11月にハワイのコナで行われるWG21全体会議のアジェンダ。
11月の全体会議に先立って行われる、WG21管理者ミーティングの案内。
C++26のワーキングドラフト第2弾
↑の変更点をまとめた文書。
新規に採択された提案はなく、編集上の変更のみです。また、C++23 DISに対するNBコメントの対応についても記されています。
要素が削除されない限りそのメモリ位置が安定かつメモリ局所性の高いコンテナであるstd::hive
(旧名std::colony
)の提案。
以前の記事を参照
このリビジョンでの変更は
- Apendixの
constexpr
の使用法に関するセクションの更新
- 「Design Decisions」セクションの
bit_cast
をreinterpret_cast
に置き換え
- 「Erased-element location recording mechanism」セクションにどのブロックが消去されているかを追跡するためのさまざまなメカニズムに関するセクションを追加
- 「Collection of element memory blocks + metadata」セクションの修正
- 提案の最初の方に
std::hive
の構造に関するインフォグラフィックを追加
<=>
を定義しないことについてのメモを削除
- 標準の他の成長因子の記述と一致させるために、概要から、1より大きい成長因子を削除し、整数である必要はないを追加
- 概要の'poem'を削除
- LWGのフィードバックに基づいて、時間計算量を現在の実装の最大値で指定した(後から調整可能だが、ABI破壊が懸念される)
- 小さな型をオーバーアライメントすることなく全ての型に対してO(1)で消去可能であるため、消去処理の時間計算量要件を削除
- その他個人からのフィードバックの適用
- 時間計算量に関するその他の修正
- 代替実装方法の詳細を更新
- 他の部分でカバーされていたため、
clear()
の説明を削除。同様に消去処理の説明も削除
などです。
スタックフルコルーチンのためのコンテキストスイッチを担うクラス、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に変更
- テンプレートパラメータの宣言の
typename
をclass
に変更
- コンストラクタ規定の調整
entry_copy, stack_copy, deleter_copy
がfiber_constext
のメンバになることを意図していないことを明確化
- 説明専用オブジェクトの初期化の合理化
- “Instantiates a fiber_context”を“Initializes state”へ言い換え
- “empty() returns true”を“empty() is true”へ言い換え
- スタック領域を指定するコンストラクタから、スタックサイズとアライメントについての事前条件を削除し、例外条件を例外種別ごとに記述
- ムーブコンストラクタのEffectsを調整
などです。
std::generate_canonical
の仕様を改善する提案。
std::generate_canonical
の現在の規定は、浮動小数点数の仕様を無視しているため誤って制約されています。それによって実質的に正しい実装が不可能になっています。この提案では主に2つの問題について指摘しています。
この関数の動作は次の3つの要件に従うはずです
- 結果は
[0, 1)
に入らなければならない
- アルゴリズムは正確に指定されており、使用する
URBG
は指定された引数に対して特定の固定回数だけ呼び出される必要がある
- 結果は一様分布となる
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
の表現可能な全ての値が含むようにすることは、定義が困難かつ実装が複雑化するとして行っていません。
オブジェクトの再配置(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"と重複しているため削除
などです。
std::format
の対となるテキストスキャン機能の提案。
以前の記事を参照
このリビジョンでの変更は
- イテレータではなく
subrange
をscan
から返すようにした(scan
の正常値のscan_result
がsubrange
を内包するようになった)
formatter
との一貫性のため、scanner
のCharT
テンプレートパラメータのデフォルトを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を追加
などです。
std::simd<T>
をParallelism TS v2から標準ライブラリへ移す提案。
以前の記事を参照
このリビジョンでの変更は
- mask reductionの事前条件を削除。ただし、その決定の取り消しをLEWGに要求する予定
basic_simd_mask
の単項演算子の戻り値型を修正
simd-select-impl
のbool
オーバーロードを修正
simd_split
における不必要な実装の自由度を削除
- テンプレートパラメータの宣言時は
typename
ではなくclass
を使用する
- ブロードキャストコンストラクタの
constexpr-wrapper-like
引数の値に関するSFINAEの実装
basic_simd_mask
に関係演算子を追加
size_t
とint
の使用法に関するセクションを追加
- 未解決の設計に関するセクションをすべて削除し、LWG向けの文言に関する質問のみを残すようにした
- 実装ノートにLWGへの質問を追加
- オーバーロード候補を削減するために二項演算子に制約を追加
などです。
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... Args>
explicit thread(Args&&... args);
...
private:
template <class F, class... Attrs>
thread(attribute-tag, F && f, Attrs&&... attrs);
};
}
void f();
int main() {
auto thread = std::jthread::(std::thread_name("Worker"),
std::thread_stack_size(512*1024),
f);
}
処理に渡す引数がある場合は、今まで通りこのスレッド処理の後ろに指定します。
<random>
にPhiloxというアルゴリズムベースの新しい疑似乱数生成エンジンを追加する提案。
以前の記事を参照
このリビジョンでの変更は
- 単純化のためにいくつかのエイリアスを削除
- 渡される値の数を制御するために
std::counter
のAPIを変更
set_counter, get_counter
の機能とAPIについて説明する設計上の考慮事項に関するセクションを拡張
- コードの移植性を維持するために、
set_counter
に渡されるWord値の解釈についてリトルエンディアンであるという指定を削除
counter_size
の計算ロジックの曖昧さを回避するために、pseudo_random_function
コンセプトをcounter_size
パラメータを使用して拡張
などです。
C++標準ライブラリ設計のためのポリシーについて検討する提案。
ここでのポリシーとは、C++標準ライブラリの将来のユーティリティの提案者が従うべき技術ルールもしくは技術ガイドラインのことです。
この提案では、ポリシーの利点と欠点とを勘案しながら、LEWGのレビューを通過した提案全てに適用されるものとしてのポリシーを設定し、それを常設文書(SD-9)として設定することを提案しています。ただし、提案しているのはポリシーの確立と文書化のプロセスのみで、ポリシーそのものは例示するに留めています。
ポリシーを設定することの利点と欠点としては次のようなものが挙げられています
- 利点
- ポリシーによって、標準ライブラリの様々な部分の動作におけるユーザーの期待に対して統一性を持たすことができる
- ポリシーによって、提案の著者とレビューする委員会の両方の時間が節約される
- ポリシーは共有された知識ベースから作成される必要がある(それによって、それら知識の断片化や解釈の不一致を最小限に抑えることができる)
- ポリシーによって、標準化プロセスを新規参入者にとって優しいものにすることができる
- 欠点
- ポリシーは、委員会内で少数派領域の代表の意見を押し退けてしまう可能性がある
- 提案された一部のユーティリティに対しては、ポリシーによって誤った技術的解決策を強制してしまう可能性がある
欠点でも触れられているように、単一の原則が全てに適合することはなく、あるポリシーについてライブラリ全体の合意に達することには困難が伴います。この提案では、設定されたポリシーは常に強制されるものではなく、ポリシーに違反していることを明示しその理由について説明されていれば、ポリシーを常に守る必要はない、という運用にすることを提案しています。
小さなポリシー1つをとってもライブラリ全体での合意を得るには時間がかかりますが、そのような議論を提案ごとではなくポリシーを確立する1度だけ行っておくことで、長期的には時間の節約になり他の利点を得ることもできる、としています。
std::span
にinitializer_list
を受け取るコンストラクタを追加する提案。
以前の記事を参照
このリビジョンでの変更は、機能テストマクロを(再)追加したこと、Annex Cのための文言を追加したことなどです。
この提案は2023年11月のKona会議において採択され、C++26 WDにマージされています。
P2300で提案されている実行コンテキストの指定を、C++17の並列アルゴリズムにも適用できるようにすることを目指す提案。
以前の記事を参照(番号が間違って公開されたとのことで提案番号が変更されています)
このリビジョンでの変更は
- 逐次実行をデフォルトとするように
execute_on
の実装を変更
- コンパイルエラーまたは実行時エラーを起こす
execute_on
のカスタマイズを許可
- アルゴリズムと
execute_on
のカスタマイズを行う例を追加
- 提案しているアプローチの拡張性に関するサブセクションを追加
- 遅延
for_each
アルゴリズムの可能なシグネチャを追加
などです。
同じ要素型を持つ異なる型の範囲を連結するRangeファクトリ、views::concat
の提案。
このリビジョンでの変更は、!common_range && random_access_range && sized_range
に関するセクションを追加したことです。
!common_range && random_access_range && sized_range
という条件は、これを入力範囲の最後のrange
が満たしている場合にconcat_view
がcommon_range
になるべきではない、という条件でした。これを反映してconcat_view
がcommon_range
となるかどうかは入力の最後のrange
がcommon_range
であるかによっていました。
しかし、LWGのレビューにおいてconcat_view
のbidirectional_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
することができます。
std::mdspan
でpadding strideをサポートするためのレイアウト指定クラスを追加する提案。
以前の記事を参照
このリビジョンでの変更は
- 標準ドラフトのレイアウトに従うように修正
submdspan
の文言を書き直し
などです。
std::simd
でstd::complex
をサポートできるようにする提案。
以前の記事を参照
このリビジョンでの変更は、P1928R6でstd::simd
に加えられた変更を適用したことです。
std::simd
に、permute操作のサポートを追加する提案。
以前の記事を参照
このリビジョンでの変更は
- P1928R3に合わせて、
simd/simd_mask
をbasic_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_select
やcopy_to
などと一貫するようになった
などです。
C++周辺ツールが、Ecosystem ISにどれほど準拠しているのかを互いに通信する手段を標準化する提案。
以前の記事を参照
このリビジョンでの変更は
- 2023年6月の会議における投票結果を追記
- イントロスペクション情報を取得するためのプログラム実行が望ましくない場合などでもその情報を提供するために、イントロスペクションファイルを使用できるようにした
- 2種類のコマンドラインオプション構文のサポートを追加して、オプション構文解析を変更せずにこの提案の内容をサポートできるようにした
- バージョンについて、セマンティックバージョニングを採用
などです。
現在のNetworking TSにP2300のsender/receiever
サポートを入れ込む提案。
以前の記事を参照
このリビジョンでの変更は
- Obtaining the Schedulerセクションにソケットコンテキストの利用について追記
scheduler
インターフェースが非標準の操作に関して拡張可能である必要があるという要件について追記
- コルーチンをより分かりやすくするために、
awaitable
なsender
に関する説明を追加
などです。
並行アルゴリズムにおけるキャッシュとして使用可能な並行オブジェクトプール機能の提案。
以前の記事を参照
このリビジョンでの変更は
- 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()};
auto& object = *handle;
for (const auto & t : split(tria, max_area)) {
object.emplace_back(t);
}
}
);
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;
class snapshot;
public:
object_pool(Allocator allocator = Allocator{}) noexcept;
object_pool(const object_pool &) =delete;
auto operator=(const object_pool &) -> object_pool & =delete;
~object_pool() noexcept;
[[nodiscard]]
auto lease() const -> handle;
[[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;
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;
};
trivially relocatableをサポートするための提案。
以前の記事を参照
このリビジョンでの変更は
- EWGをターゲットとし、LEWGをフォローアップ
- 主要提案としてタイトルを変更
- ライブラリ拡張機能をP2959R0とP2967R0へ移動
- 文書レイアウト等の調整
- モチベーションとなる使用例を追加
- 前のリビジョンでの変更を考慮して例を修正
- 提案する文言を完成させた
- 未解決の質問セクションを追加
trivially_relocate()
関数からconstexpr
を削除
などです。
依存ライブラリとしてモジュールを使用する際に、必要となる情報について説明する文書。
この文書では、あらゆるプロジェクトにおいて共通する、そのプロジェクトを有意義に活用するために必要な一連の手順があり、その手順のために必要な各種情報(コンパイラ/リンカオプション、動的ローダーの環境変数、プラグインローディング環境変数などを通じて伝達される傾向にあるあらゆるもの)のことをプロジェクトの使用要件(usage requirement)と呼んでいます。この使用要件を発見することはビルドシステムの仕事の中核をなしています。
また、その使用要件を収集するための(ツールチェーンに依存する)方法のことをフラグスープ(flag soup)と呼んでいます。
この文書は、現在のフラグスープに代わるより構造化された使用要件の伝達手段の必要性を説くものです。
文書の意図は、モジュールを使用するクライアントはクライアントによって異なる使用要件を持ち、同じプロジェクト内でさえも同じモジュールに対して翻訳単位ごとに異なるBMIを取得する可能性があり、現在のフラグスープではその用途のために十分でないことを示すことにあります。また、そのような使用要件をより完全に伝達することができるようにすることで、BMIを再利用しやすくなるメリットもあります。
自明な無限ループを未定義動作ではなくする提案。
以前の記事を参照
このリビジョンでの変更は、寄せられたフィードバックの適用などです。
EWGにおける投票では、提案する2つのうちオプション2を採用する方に合意され、CWGに転送されています。
コンセプトを受け取るためのテンプレートテンプレートパラメータ構文の提案。
以前の記事を参照
このリビジョンでの変更はよくわかりませんが、構文等に変更はないようです。
↓
std::filesystem::path
をstd::format()
でフォーマット可能にする提案。
以前の記事を参照
R3での変更は
このリビジョンでの変更は、SG16での投票結果を追加したことです。
std::text_encoding
の提案(P1885R12)に含まれる text_encoding::name()
の仕様を変更する提案。
以前の記事を参照
このリビジョンでの変更は、ベースとなるドラフトをP1885R12がマージされたN4958に更新した事です。
現在非推奨とマークされている機能について、C++26で削除/復帰を検討する提案。
以前の記事を参照
このリビジョンでの変更は
- この文書の変更履歴と文書内の派生提案の参照時にリビジョン番号を付加するようにした
- 派生提案のステータスを更新
などです。
C++契約プログラミング機能の構文の選択のための評価基準や要件をまとめる文書。
以前の記事を参照
このリビジョンでの変更は、この提案の採択の投票後に寄せられた構文要件をAppendix Aに追加したことです。
追加された構文要件は次の3つです
- 契約注釈に対する属性の指定
- 契約ラベルの事前宣言
- ラベルやメタアノテーションを後の宣言にのみ付加する場合に、その存在を最初の宣言でほのめかす構文が必要となる
- 関数型に対する契約注釈
- 関数型に対して契約注釈を行う方法を提供する必要がある
- 関数そのものと関数型に対するものとの間で、構文上の曖昧さがあってはならない
friend
宣言でのパック展開を許可する提案。
以前の記事を参照
このリビジョンでの変更は、clangのフォークでこの提案を実装したうえでCompiler Explorerから利用可能になったことへの謝辞を追加したことです。
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
構築の安全性について説明
などです。
↓
C++ 契約プログラミング機能の提案。
C++20で一度標準入りしてから撤回され、MVPという最小仕様を確立すべくSG21で議論を重ねられてきました。この提案はC++26に向けて現時点でのContracts MVP仕様をまとめ、それを正式な契約プログラミング機能としてC++26に導入するためのものです。
この提案は大きく設計と文言のセクションに分かれており、設計セクションではMVPの契約機能の構文と意味論について説明され、文言セクションでは設計セクションで説明された機能をC++言語で有効化するための標準のための文言の変更が記載されています(現時点では文言は未整備ですが)。設計セクションを読むことでC++26契約プログラミング機能がどういうものかを把握することができるでしょう。
ただし、現在のところ構文がまだ決まっておらず、いくつか小さめの未解決の問題が残されているため、まだ完全なものではありません。
提案中の値ベースリフレクションによってPythonバインディングの作成を簡素化できることを示す文書。
以前の記事を参照
このリビジョンでの変更は
- この提案が対象としない事項について明確化
- 関数引数名のリフレクションとそれによって無効なリフレクションエラーが発生する場合についての議論の追加
- デフォルトのバインディングによって引き起こされる危険な動作のよりよい例を追加
- リフレクションされたエンティティの範囲を拡張して、その名前の範囲を生成するための
meta::name_of/meta::names_of
関数の詳細な説明
- オーバーロードされた演算子のバインディングに関するセクションを追加
- Classdesc frameworkと値ベースリフレクション、および将来の作業とを比較するセクションの追加
などです。
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_cast
やstd::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
のパターンマッチングを可能にすることもできます。
契約機能に関する未解決の問題についての設計原則に基づく解決策の提案。
以前の記事を参照
このリビジョンでの変更は
- ラムダキャプチャの回避策を明確化
- 信頼性(reliability)、直交性(orthogonality)、その他議論の原則を追加
- コンパイル時評価を処理するアプローチの明確化
- 提案1について代替案を追加
などです。
提案1はトリビアルなメンバ関数に対する契約注釈に関するものでした。この提案ではR0での提案を提案1.Aとして、新しい解決案を提案1.Bとして追加しています
- トリビアルな特殊メンバ関数と契約
- 提案A
- 契約注釈は関数のトリビアル性に影響を与えない
- そのような契約注釈は評価されない可能性がある
- 提案B
2023年11月のKona会議において、このリビジョンで追加された提案1.BをC++26の契約プログラミング機能における制約として採用したようです。
C++契約プログラミングのための構文として属性構文を推奨する提案。
以前の記事を参照
このリビジョンでの変更は
- 事前・事後条件指定位置を
pure-specifier
(= 0
)の前に移動
- 契約注釈そのものに対する属性指定位置を明確化
[[assert : expr]]
が式とする具体的な提案の追加
などです。
ブロックベースコンテナ(特に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"
<< "b:\t" << b << "\n"
<< "c:\t" << c << "\n";
auto const mid = std::next(x.begin());
std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"
<< "x[1]:\t" << std::get<0>(*mid) << "\n"
<< "x[2]:\t" << std::get<0>(x.back()) << "\n";
x.erase(mid);
std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"
<< "x[1]:\t" << std::get<0>(x.back()) << "\n";
b = 4;
c = 5;
std::cout << "x[0]:\t" << std::get<0>(x.front()) << "\n"
<< "x[1]:\t" << std::get<0>(x.back()) << "\n";
}
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());
auto fill = [&](int & i ) {
v.clear();
for (int j = 0; j != 4; ++j) {
v.emplace_back(i);
}
};
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つの異なる原因から生じています。
- ムーブ構築ではなくムーブ代入による要素の置換
- 要素型のムーブ代入によって、破棄とムーブ構築とは異なる状態が生成される場合、ブロックベースコンテナはノードベースコンテナとは異なる振る舞いをする
- アロケータは要素の同一性を考慮する必要がある
- ムーブ代入によって内部再配置が行われる場合に、構築された要素のID(アドレス)が重要である場合、アロケータの
.destroy()
の期待に反する可能性がある
- 強い例外安全保障が破られる可能性がある
- アロケータの
construct()
がカスタマイズされている場合、構築時に要素型のコンストラクタ以外から例外が投げられないことを仮定できない
- 現在のブロックベースコンテナに対する強い例外安全保障は要素型のムーブコンストラクタのみを考慮しており、アロケータを考慮していない
- トリビアルリロケーションのサポートにおいて問題が起こりうる
- アロケータが
construct()
を提供する場合、(3と同様の理由により)要素型のトリビアルリロケーション可能性の情報を利用できない
- 再配置は標準ライブラリの未初期化アルゴリズムと同様にオブジェクトを作成する
<memory>
に現在のuninitialized_*
系アルゴリズムに対応する、ムーブ構築を基本とし利用可能な場合はリロケーションを利用して最適化する汎用の関数を追加する必要がある
この提案はこれらの懸念に対処するために次のような変更を標準ライブラリに加えることを提案しています
- ムーブ構築/代入が一貫しない振る舞いをする型のために、新しい型特性を追加する
- アロケータが独自の実装を提供せず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()
という関数で、これは主にコンテナ内で再配置が起こる場合に再配置の方法をカスタマイズするものです。次のような動作をします
- アロケータ型が
relocate()
メンバ関数を定義する場合、それを呼び出す
- アロケータ型が少なくとも1つの
construct()/destroy()
を定義している場合、再配置対象の右辺値を渡してそれらを利用する
- 要素型がトリビアルコピー可能ならば、
memmove
を利用する
- トリビアルなリロケーションが導入された場合、それで置き換えられる
- それ以外の場合、ムーブ代入/構築によって再配置
std::container_replace_with_assignment<T>
がtrue
ならばムーブ代入によって再配置(現在の動作)
std::container_replace_with_assignment<T>
がfalse
ならば破棄とムーブ構築によって再配置
コードで書くと、次のようになります
if constexpr (requires requires{ allocator::relocate(...); }) {
} else if constexpr(requires requires{ allocator::construct(...); }) {
} else if constexpr (is_trivially_relocatable_v<T>) {
} else if constexpr (container_replace_with_assignment_v<T>) {
} else {
}
現在動作しているコードは基本的に引き続いて4番目の動作を選択することで動作を維持します。3番目の動作はさらに、トリビアルコピー可能な型に対しての最適化を組み込んでいます。また、4番目の動作では同時に提案しているstd::container_replace_with_assignment<T>
を使用してムーブ代入の使用が適切かどうかを判定し、不適切な場合は再配置先要素の破棄の後そこにムーブ構築することで再配置を行います。
これらの変更によって、後方互換性を維持しつつ現在の振る舞いを修正するとともに、将来的なトリビアルリロケーションを適切に有効化することができます。ただし、この提案は現在の実行時の動作を静かに変更する可能性があります。
契約プログラミング機能のための構文の提案。
以前の記事を参照
このリビジョンでの変更は
default/delete
指定と一貫させるために、pre-or-postcondition
(事前/事後条件指定)がpure-specifier
(= 0
)の前に来るように調整
- 事後条件における戻り値を指す名前の指定が必須ではないことを明確にした
assert
キーワードの代替案に関する説明の追加
- 代替案の候補として
contract_assert
とassertexpr
を追加
- Cにおける実現可能性についての議論を追加
- C++26でこの構文によって契約機能を追加した後の、クラス不変条件の構文に関する議論を追加
- P2885R3からの新しい3つの構文要件に関する議論の追加
などです。
この提案のR2が2023年11月のKona会議でレビューされ、アサートのキーワードとしてはcontract_assert
を採用することに決定したようです。同時に、C++26の契約プログラミング機能のための構文としてこの提案のnatural syntaxが採用されることになったようです。
Baseline Compile Commandの説明と、それを伝達する方法についての提案。
ある翻訳単位におけるヘッダユニットのインポートは、その翻訳単位のプリプロセッサ状態の影響を受けず、ヘッダユニットはマクロをエクスポートするためヘッダユニットがインポートされるとその翻訳単位のプリプロセッサ状態は更新されます。一方で、ヘッダユニットはその翻訳単位に指定されているコンパイラオプション(コンパイラコマンド)は適用されなければなりません。
ただし、ヘッダユニットのインポートにおいてはその翻訳単位のプリプロセッサ状態を含めたローカルプリプロセッサ引数を適用しないようにする必要があります。この制約は、同じヘッダユニットを異なる翻訳単位でインポートした時でも、双方の翻訳単位において同じようにインポート可能なヘッダをパースするためのものです。特に、推移的なインポートが起こる場合に同じヘッダユニットの内容が異ならないようにするために求められることです。そのため、ローカルプリプロセッサ引数とはある翻訳単位に固有な、コンパイル中のプリプロセッサ状態に影響を与えうる引数のことです。
また、SG15における合意ではビルドシステム以外のものがコンパイルコマンドの構成を行うべきではないというものがあるため、ローカルプリプロセッサ引数の区別を行うのはビルドシステムであるとして、依存関係スキャンプロセス(これを行うのはコンパイラや静的解析ツールなど)にはその翻訳単位自身のコマンドライン引数と、インポートされた全てのヘッダユニットをコンパイルするために使用されるBaseline Compile Commandの2つの入力(コマンドライン引数)が必要となります。
この提案におけるBaseline Compile Commandは、次のような情報のことです
- どのファイルがコンパイルされるか
- どのような出力が生成されるべきか
- ローカルプリプロセッサ引数を含まないコンパイルオプションの一部
ビルドシステムは、各翻訳単位をビルドするために必要なコンパイルコマンドを構成し、そこからBaseline Compile Commandを区別する役割を担います。したがって、ある翻訳単位におけるBaseline Compile Commandはそのコンパイルにおいて使用されるコンパイルオプションの一部分であり、それを区別する方法にはいくつか問題があります。
この提案では、コンパイルオプションからBaseline Compile Commandを独立させてファイルに保存し、依存関係スキャンプロセスへの入力にはコンパイルオプション及びBaseline Compile Commandを記録したファイルパスを渡すようにすることを提案しています。
提案より、LLVMのJSON 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です。
リロケーションサポートのためのライブラリ機能の提案。
この提案は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>) {
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)) {
while (T* dest = new_location + begin - end; dest != new_location) {
::new (--dest) T(std::move(*--end));
std::destroy_at(end);
}
return dest;
} else {
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);
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()
とは異なりオーバーラップ領域には使用できません。
ビルドシステムとコンパイラが相互にやり取りをするためのAPIの提案。
現在のC++のコンパイルは、人間かビルドシステムがコンパイラの実行ファイルを呼び出すことで行われています。提案によれば、ビルドシステムがコンパイラの機能を実行ファイルではなく共有ライブラリ経由で使用するようにすることで25~40%のコンパイル速度向上が見込めるとのことです。
速度が向上する理由は次の2点です
- ビルドシステムはAPIを利用してコンパイラが読み込んだファイルを知ることができる。その情報を利用すれば、複数のファイルのコンパイルで同じキャッシュファイルを使用可能になる
- モジュールのビルドにおいては、本ビルドの前にモジュール間の依存関係を調べる必要がある。その役割は基本的にコンパイラが担っているが、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;
};
struct compile_output {
void *compiler_state;
string stdout;
string stderr;
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;
bool waiting_on_module;
bool completed;
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);
}
このようなAPIをコンパイラ共有ライブラリが提供し、ビルドシステムはこのAPIを介してコンパイラとコンパイルを制御します。次のように利用することを意図しています。
最初に、new_compile()
にコンパイラオプションを引数として渡してコンパイルを開始します(このオプションには依存関係の情報は含まれていません)。その後、コンパイル実行中にモジュールのインポートに遭遇した場合、戻り値のwait_string_or_logical_name
にその依存関係の名前を指定しwaiting_on_module
をtrue
に設定してこの関数はリターンします。また、コンパイル実行中にヘッダユニットのインポートに遭遇した場合、wait_string_or_logical_name
にそのヘッダユニットのパスを指定しwaiting_on_module
をfalse
に設定してリターンします。
ビルドシステムは一連のビルドの間で得られたcompiler_state
を保存しておき、必要なファイルが既にビルドされているか、ビルドする必要があるか、ビルド中であるかを管理します。あるビルドが待機しているファイルが利用可能になると、そのcompiler_state
と新たに利用可能になったモジュール(ヘッダユニット)のBMIを渡してresume_compile()
を呼び出すことでコンパイルを再開します。
ビルドが完了すると、コンパイラはcompile_output
オブジェクトのcompleted
をtrue
に設定し、output_files
とoutput_files_paths
に成果物の名前とパスを設定してリターンします。
あるcompiler_state
に対して依存関係が解消されコンパイルが完了するまでresume_compile()
は繰り返し呼び出され、それらのビルドは複数並行で行われます。
このAPIの実行モデルでは、コンパイラは依存関係スキャンを行わず依存関係(モジュール関連)にぶつかるとそこでコンパイルを一時停止しビルドシステムに制御を返します。ビルドシステムはプロジェクト内の全てのモジュール/ヘッダユニットについて並行的にnew_compile()
を走らせ、その成果物をもってresume_compile()
を呼び出すことで依存関係を自動的に解決しながらビルドを完了します。
この提案は、API定義を通してそのようなモジュールのビルドモデルを定義しようとしてもいます。
LEWG/EWGでの機能設計のための設計ポリシーを整備する提案。
WG21に参加する人々は、C++標準を可能な限り最高のものにする目標を共有していますが、誰もが同じ原則に基づいて設計を選択するわけではありません。そのため、関連するすべての原則が先に合意されない限り設計に関する議論を行き詰まってしまう可能性があります。一見の一致を見た場合でも、投票の後で1つ以上の重要な設計原則について十分な情報が共有されていなかったことが後で反目する場合があります。
この提案では、原則に基づいた設計を採用し、WG21の議論プロセスにおいて、現在のように初めに関連する問題について議論した後に解決策の一つに投票するのではなく、原則に基づいた設計のプロトコルに従ってまず関連する設計原則を明確にした上で優先順位を付けるようにすることを提案しています。
また、そのような設計原則に基づいた決定を文書化して共有することで、別の議論における同様の決定の際に再検討を避け将来の議論を合理化できます。
現在では、そのような設計原則やそれに基づく過去の決定などの情報は属人化しており、その人がたまたまその議論において欠けていることで以前の議論や決定が継承されず、設計に矛盾が生じることが少なからずあったようです。提案では、クラスのデフォルト特殊メンバ関数に対するnoexcept
指定のバージョン間での振る舞いの不一致や、LEWGにおけるラコスルールの軽視の例をあげています。
この提案はそのようなポリシーの概要を説明するもので、具体的な提案は個別の2つの提案で行おうとしています
- 議論のある設計の決定を仲裁するための原則に基づいたアプローチ (P3004)
- 確立された設計ポリシーを文書化してアクセスするための体系的なメカニズム(P3005)
なお、これらの提案はまだ公開されていません。
物理量と単位を扱うライブラリ機能の導入について説明する提案。
この提案では、物理量と単位を扱うライブラリを導入する理由やモチベーションを説明し、設計目標や現時点での例を示すものです。
モチベーションとしては次のようなものが挙げられています
- 安全性
- 単位を間違えた計算がコンパイルエラーとなる
- 物理量とその単位を扱うライブラリは多くの機械の制御コードの安全性を向上させる
- 語彙型となること
- 認証されたライブラリ
- MISRA等の安全性基準に従ったソフトウェア開発においてはOSSを使用できない場合が多い
- 標準ライブラリとして提供されていることでより安全なコードを記述できるようになる
- 独自実装は複雑で難しい
- 拡張性
- 非SI単位などに対応するために単位の追加を容易にする
- 幅広いドメインで使用可能(されている)
提案より、サンプルコード
#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';
std::cout << v2 << '\n';
std::println("{}", v3);
std::println("{:*^14}", v4);
std::println("{:%Q in %q}", v5);
std::println("{0:%Q} in {0:%q}", v6);
std::println("{:%Q}", v7);
}
この提案及び将来的に標準に導入しようとしているライブラリ機能は、mp-unitsというライブラリで試験実装が進められています。
提案ではこの機能をC++29に導入することを目指しており、そのための機能ごとのカテゴライズやその中での優先順位付けを行っています。掲載されている予定表によれば、C++26サイクル中にLEWGのレビューを完了し、C++29サイクル中でLWGの承認を取り付け、標準に導入するような予定を組んでいるようです(詳細な予定表が提案にはあります)。ただし、まだ確度の高いものではなく、計画をLEWGに承認してもらってスムーズに進めようとしています。
↑の物理量と単位を扱うライブラリ機能について、コンパイル時の安全性に関する側面を解説する文書。
この文書では主に、P2980のライブラリ(以下単位ライブラリと呼びます)がC++コードの安全性向上に役立つことを解説しており、単位ライブラリの必要性や使用可能な産業領域等を説き、単位付きの量の取り扱いを間違ったことで起きた事故を紹介し、現在よく見られる単位付きの量を使用している危ういコード例について紹介したうえで、単位ライブラリがそれらの問題をどう解決できるかを示しています。
単位ライブラリによる安全性は主に、算術演算コードにおいてその値の単位をコンパイル時にチェックすることで間違った計算を防止するものです。それをベースに、ダングリング参照に対する配慮や、同じ単位を持つ異なる種類の量のサポートなどの安全性への配慮が行われています。
↑の物理量と単位を扱うライブラリ機能について、数値と計算の側面に関する設計の説明をする文書。
あまりに長いので詳しくは見ませんが
- 量体系
- 同じ次元を持つ量でもその意味が異なる場合があり、それを考慮しなければならない
- 単位系だけではなく量体系もモデリングする必要がある
- 国際量体系(International System of Quantities)に基づいてそれを行う
- 単位系
- 単位の種類・組み合わせ・スケール・単位を指す記号などについて
- 単位は量の種類に関連づけられ、特定の量に対して制約される
- 単位・次元・量を表す型の操作
- 単位・次元・量を表す型に対して定義された演算や変換などについて
- 全ての単位・次元・量を表す型はそれぞれ一意な型を持つ
- 演算の結果は、量と単位の型の階層や関係性に基づいて決定される
- 量(
std::quantity
)
quantity
の概念や表現、構築や演算などについて
quantity
は数値と単位のペアであり、単位の変換や算術演算が可能
quantity
はこのライブラリの中心クラス型であり、std::chrono::duration
の一般化ではあるが直接の互換性はない
- ジェネリックインターフェース
quantity
をテンプレートで受け取る関数や変数を定義する方法について
- 関数のインターフェースや戻り値の受け取り、クラスメンバへの保存時などには単位の選択が必要となる
quantity
を制約するためにコンセプトを使用できる
- 数値を表現する型のカスタマイズ
quantity
の数値表現型として任意の型を使用する方法について
quantity
の数値表現型はその値の範囲や精度に影響する
- アフィン空間
- アフィン空間の概念とこのライブラリによってそれを表現する方法について
- アフィン空間の中心の2つの概念(点とベクトル)はそれぞれ、
PointOrigin
コンセプト+quantity_point
型、quantity
型によって表現される
のようなことが書かれています。
static constexpr
メンバ変数のクラス外定義の非推奨の扱いを検討する提案。
C++11でstatic constexpr
メンバ変数が宣言できるようになりましたが、その定義が必要となる場合は通常の静的メンバ変数と同様にクラス外に定義を置く必要がありました。これはC++17のインライン変数の導入時にstatic constexpr
メンバ変数は暗黙inline
とされるようになったことでクラス外での定義が不要になりました。それとともに、クラス外の定義は不要な再宣言となり非推奨とされました。
struct A {
static constexpr int n = 5;
};
constexpr int A::n;
__cpp_inline_variables
機能テストマクロを用いてクラス外定義の存在を切り替えることでこのようなコードはバージョン間でポータブルにすることができます。とはいえ、C++14以前にこの書き方をされている既存のコードはおそらくたくさんあり、必ずしも全てがそのような対応を行えるわけではないかもしれません。
現在の主要なコンパイラ(フロントエンド)は、C++17以降のモードにおいてもこれらの冗長な再宣言(以前の定義)に対して非推奨である旨の警告を発しません。
C++26に向けて現在非推奨とされているものを整理し可能なら削除しようとする取り組み(P2863)のEWGにおけるレビューにおいてこの問題も議論され、そこでは現状維持(非推奨のまま)とする方向性のようですが、さらにこれの非推奨化を解除する方向性について提案が望まれたようで、この提案はそれを受けてのものです。
この提案では、その歴史的経緯や現状を説明するとともに、現状維持・非推奨解除・削除のいずれかを選択することを促しています。
その後のEWGのレビューでは、C++26サイクルでは現状維持とすることになったようです。
あるクラスが他のクラスの仮想基底になっているかを判定する型特性の提案。
このような判定はクラス型のポインタの変換を行う場所において必要となり、特にスマートポインタの変換コンストラクタにおいて必要となります。
ポインタderived* d
をbase*
に変換するには、base
がderived
の仮想基底クラスであるかどうかによって実装が分岐します
base
がderived
の仮想基底クラスではない場合
d
がnullptr
かをチェックする
nullptr
ではない場合、d
にコンパイル時にわかっている定数オフセットを加算する
nullptr
の場合、nullptr
を返す
base
がderived
の仮想基底クラスである場合
d
がnullptr
かをチェックする
nullptr
ではない場合、d
の参照する領域の仮想テーブルを検査して適切なポインタ値を得る
この処理はユーザーが実装するものではなく、コンパイラが挿入する変換処理です。
2でd
がnullptr
ではない場合に問題なのは、仮想テーブルにアクセスしてポインタ値を得るのが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))
{}
この実装には
T
がY
の仮想基底であり
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;
}
ただ、これは今度はT
がY
の仮想基底ではないほとんどのケースで非効率となります。
この実装を正しくかつ効率的に行うにはT
がY
の仮想基底であるかどうかによって実装を分岐する必要があります。
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 {
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;
}
定数式でも使用可能な関数ポインタ専用の型消去型を追加する提案。
現在利用可能な関数ポインタの型消去機能(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)
{}
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))
{}
};
template<class T>
constexpr static auto get(storage obj) {
if constexpr (std::is_const_v<T>) {
return static_cast<T*>(obj.cp_);
} else if constexpr (std::is_object_v<T>) {
return static_cast<T*>(obj.p_);
} else {
return reinterpret_cast<T*>(obj.fp_);
}
}
};
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() {
constexpr std::function_ptr_t fp1 = nullptr;
constexpr std::function_ptr_t fp2 = f;
constexpr auto p_f = static_cast<int(*)()>(fp2);
static_assert( p_f == f );
static_assert( p_f() == 42 );
fp_f();
*fp_f;
}
例えばstd::function_ref
においては、このstd::function_ptr_t
を使用して関数ポインタを保存する部分を書き換えることでreinterpret_cast
を使用する必要がなくなり、構築も呼び出しも定数式で行えます。
EWGIによるレビューでは、この問題はキャストの仕様調整によって解決することが望ましいという方向性のようで、この提案の方向性は支持を得られていないようです。
std::optional
が参照を保持することができるようにする提案。
現在、std::optional<T>
のT
には参照型を指定することができません。当初の提案では検討されていましたが、ポインタとの意味的な差異が不明瞭であることや非参照のT
に対するメインのstd::optional<T>
の導入を優先するために途中で提案から削除されました。
std::optional<T&>
は単純には無効値を取れる参照です。それは一見すると確かにポインタと意味的な違いが無いようにも思えます。
std::optional
がC++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);
|
要素を検索する関数で戻り値型としてスマートポインタを使用する場合との比較
ポインタ |
この提案 |
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;
ro = 15;
int m = 20;
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;
ro = m;
*ro = 15;
std::optional<const int&> cro = n;
*ro = 25;
ro = m;
}
より限定されたユニバーサルテンプレートパラメータの提案。
ユニバーサルテンプレートパラメータは型・非型・テンプレートを統一的に受けることのできるテンプレートパラメータのことで、P1985で提案されました。P1985の提案は言語のあらゆるコンテキストでユニバーサルテンプレートパラメータを使用可能にしようとするもので、仕様と実装に追加する複雑さが大きくなりメリットが相対的に小さくなっていました。
この提案はそれを改善しつつほぼ同じ機能を追加しようとするもので、のP1985との違いは、ユニバーサルテンプレートパラメータの構文としてuniversal template
を選択し、その導入を純粋にテンプレートパラメータとしての使用のみに限定したことです。
- ユニバーサルテンプレートパラメータは関数・クラス・変数テンプレートのテンプレートヘッド(
template<...>
の中)でのみ宣言できる
- ユニバサールテンプレートパラメータ名はテンプレート引数としてのみ使用できる
- ユニバーサルテンプレートパラメータはパックを取れる
- ユニバーサルテンプレートパラメータのデフォルト引数は設定できない
- ユニバーサルテンプレートパラメータを処理するには、別のテンプレートに転送するか、部分特殊化によって行う
提案しているユニバーサルテンプレートパラメータ(UTP)によるライブラリ機能とその実装例
template <universal template T>
inline constexpr bool is_typename_v = false;
template <typename U>
inline constexpr bool is_typename_v<U> = true;
template <universal template U>
inline constexpr bool is_nttp_v = false;
template <auto U>
inline constexpr bool is_nttp_v<U> = true;
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;
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;
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;
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;
template<
template<universal template...> auto Primary,
universal template... Args
>
inline constexpr bool is_specialization_of_v<Primary<Args...>, Primary> = true;
P2098R1で提案されていたもの(汎用性が低いとしてリジェクト)とは異なり、この実装の場合はクラステンプレートと変数テンプレートの特殊化をチェックすることができ、さらに特殊化しているものが型でない場合についてもチェックすることができます。
この提案のユニバーサルテンプレートパラメータは他のテンプレートに渡す以外は何もできないため、このように最終的には部分特殊化によって処理することになるでしょう。そのため、P1985R3の例のいくつかは実装に工夫が必要となります。
template<universal template F, universal template... Args>
using apply = F<Args...>;
template<template <universal template...> typename F, universal template... Args>
struct apply {
using type = F<Args...>;
};
ただし、この提案ではコンセプトのテンプレートパラメータで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>>>;
モジュールのエコシステムのために必要な作業についての提案。
この提案は、モジュールを実際に利用可能にするために必要なツールのサポートのために現在欠けているものを特定し、SG15(tooling study group)においてそのために必要な作業とその優先度を提案するものです。
提案では、現状のエコシステム(コンパイラやビルドシステム、静的解析ツールなど)におけるモジュールのサポート状況を紹介し、それらツールがモジュールを相互に運用するために欠けているものについて説明したうえで、それを解消するために必要な作業について優先度を付けて提示しています。
提案されているのは次のようなロードマップです
- 単一のプロジェクトにおいてモジュールを利用可能にする
- モジュールインターフェースの複数回のコンパイル
- 1つのモジュールインターフェースが異なる翻訳単位において異なるビルドオプションを用いてコンパイルされることがよくある
- あるモジュールインターフェースが何回コンパイルされるのかを特定することに関して、ビルドシステムとコンパイラ間の相互運用性の疑問に答える必要がある
- 静的解析ツールのビルドシステム外部におけるサポート
- IDEにおけるコード補完など、静的解析ツールはビルドシステムと深く統合することなく動作することが求められる
- ビルドシステムはそれらツールが動作するのに十分な情報を提供する必要がある
- 事前ビルドライブラリにおけるモジュールの利用
- 事前ビルドライブラリでのモジュールのメタデータ
- 標準ライブラリモジュールのメタデータ
- インポート可能なヘッダ
- インポート可能ヘッダの識別
- ビルドシステムやパッケージマネージャーがプロジェクト内でインポート可能なヘッダを見つける時の問題について、ツールの実装者との協力が必要
- 依存関係スキャンとプリプロセッサ状態
- 依存関係スキャン実行時のインポートメカニズムのエミュレーションの要件について未解決の問題が残っている
この提案は、SG15における行動喚起を促す事と同時に、ツール開発者がモジュールサポートのための投資を行っても安全であることを示す目的があります。
SG15ではこのロードマップに沿った作業を行っていくことに合意が取れており、Githubのcplusplus/modules-ecosystem-trで作業を行っていくことにしたようです。
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;
result.push_back(std::move(string));
}
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とすることも提案しています。
式の結果を破棄することを明示する[[discard]]
属性の提案。
[[nodiscard]]
属性を付加された関数の戻り値を消費しない場合、警告が発せられます。これは基本的にはとても便利なのですが、場合によっては[[nodiscard]]
な関数の戻り値を使用せずに破棄したい場合もあります。例えば
- テスト
- スモークテストにおいて広い契約を持つ関数がクラッシュしないことを確かめたい場合など
- この場合に戻り値に興味はなく、警告は必要ない
- 部分的なドメイン
- 例えば、エラーコードを返す
[[nodiscard]]
な関数がある特定の引数を渡された場合に決して失敗しないことがわかっている場合にユーザーがそれを確認して呼び出してる時
- この場合、戻り値を消費する必要はなく、安全に破棄できる
- 古い関数
- 当初は成否を戻り値で返していた関数が後のバージョンで決して失敗しないようになったものの、API/ABI保護のために戻り値型を維持し続けている場合
- この場合、戻り値は意味がないため安全に破棄できる
などの場合があります。
そのような場合に警告を抑制しつつ戻り値を破棄するのに使用可能な方法は主に次の2つがあります
void
キャスト
std::ignore
[[nodiscard]]
int f();
int main() {
f();
(void)f();
void(f());
std::ignore = f();
}
しかしこれらの方法には欠点があります
void
キャスト
- キャスト式の濫用であり、警告を抑制するために言語の難解なルールを使用しているだけ
- 初心者への教育が困難
- コードベースで検索(grepなど)できない
void
キャストを[[maybe_unused]]
として使用している場合がある
- 破棄の根拠はコメントとしてのみ提供できるため、コードベースにそのような根拠を義務付けるルールを強制するのが困難
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();
int x = (a() [[discard]], 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]];
[[discard]] a(), b();
([[discard]] a(), b());
([[discard]] a()), b();
int x = ([[discard]] a(), b());
int y = ([[discard]] a()), b();
struct S {
S(int i)
: m_i(([[discard]] check(i)), i)
{}
int m_i;
};
こちらのアプローチでは対象の式を一々かっこで括る必要があるものの、式の左側という自然な位置に属性を導入でき、かっこで括ることによってどの式に属性を指定しているのかが明確になります。
この提案ではこれらのアプローチをのどちらを選択するかの決定をEWG/EWGIに委ねています。
EWGIの投票では、この提案の[[discard]]
属性については好まれたものの、個別の式に対する属性指定にはコンセンサスが得られませんでした。おそらく、式文に対する属性としての[[discard]]
としてEWGに転送されています。
パラメータパックそのものを指定する構文を検討する提案。
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)としておくことを提案しています。
SG16(Unicode Study Group)の2023年5月~9月にかけてのミーティングの議事録。
7回のミーティングにおいての提案等のレビュー時の様子が簡単に記録されています。
値ベースの静的リフレクションの提案。
この提案は以前に提案されていたP1240R2のサブセットであり、主に次のものからなります
- 定数式におけるプログラム要素を表現する鏡像値(reflection value)は、不透明型
std::meta::info
の値となる
- 鏡像値は略して、単に鏡像(reflection)と呼ぶ
- 与えられたオペランドの鏡像を生成する反射演算子(reflection operator)
^
- 鏡像に対して作用する(別の鏡像を導出する事を含む)多くの
consteval
メタ関数
- 鏡像から文法要素を生成するスプライサー(splicer)
- 追加のマイナーな変更
この提案はP1240R2の中から有用なコアな部分を抽出したもので、静的リフレクション及びそれを使用したコンパイル時メタプログラミングに関する最後の提案ではありません。P1240R2の残りの部分や更なる機能はこの提案のコア機能をベースとして後から成長させていく事を意図しています。
鏡像を表現するのにstd::meta::info
という単一の型を使用しているのは、言語エンティティに対応する個別の型を用意してしまうとその存在が言語に制約を与えてしまうためです。例えば、C++03では変数と呼ばれるものには参照が含まれていませんでしたが、C++11では変数は参照を含むようになりました。もしC++03にstd::meta::variable
のようなものが存在していた場合、変数のカテゴライズの変更がそれを使用しているコードに影響を与えるため不可能だったでしょう。この提案では、将来の言語に不当な制約を課さないためにあえて鏡像を保持する型を単一のstd::meta::info
に限定しています。
リフレクション領域と構文領域を接続する例
constexpr auto r = ^int;
typename[:r:] x = 42;
typename[:^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};
s.[:member_number(1):] = 42;
s.[:member_number(5):] = 0;
}
型のリストから型のサイズのリストを作成する例
constexpr std::array types = {^int, ^float, ^double};
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::make_integer_sequence
を実装する例
#include <utility>
#include <vector>
template<typename T>
consteval std::meta::info make_integer_seq_refl(T N) {
std::vector args{^T};
for (T k = 0; k < N; ++k) {
args.push_back(std::meta::reflect_value(k));
}
return std::meta::substitute(^std::integer_sequence, args);
}
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::vector
やstd::span
などを使用しながら通常の定数式としてリフレクション処理を記述できる点があります。
列挙値の文字列化
template <typename E>
requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
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;
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()) {
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";
int count = 1;
};
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 {
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...>>& {
return t.data.[:nonstatic_data_members_of(^decltype(t.data))[I]:];
}
nsdm_description()
は型の鏡像をその型の非静的メンバの鏡像となるものに変換するメタ関数です(nsdm=non static data member)。そして、synth_struct()
は非静的メンバの鏡像のリストを受け取って、その非静的メンバを持つ構造体型の鏡像を返すメタ関数です。nsdm_description()
は特に指定しなければ結果の非静的メンバ変数名は未規定となりますが、この例のget<I>()
実装ではNTTPインデックスを用いてそのインデックスに対応するメンバ名を直接取得しており、そのメンバ名を知る必要がないようになっています。
そのほかにも様々な例が提案には記載されています。
なお、この記事における訳語は筆者の独断によるもので、何かしらの合意を得たものではありません。
イテレータを介した間接呼び出し系のコンセプトから、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
を整えるための部分でしかありません。そしてこのR
はstd::generator<C>
と同じ性質を備えています。
これを範囲for
とranges::for_each
で普通にループを回してみると異なった振る舞いが得られます
void test(R r) {
for (auto&& c : r) {
c.f();
}
std::ranges::for_each(r, [](auto&& c){
c.f();
});
}
エラーメッセージを見てみると、const C&
なc
でc.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>>
が指定されており、F
はranges::for_each
に渡した各要素に対する処理を記述した呼び出し可能オブジェクト(例ではラムダ式)です。
プロジェクションがデフォルトのままなので、Proj
はstd::identity
であり、projected<iterator_t<R>, identity>>
はI
と同じ性質を備えたイテレータっぽい型になります。したがって、ここではprojected
を無視できます。
F
が適切にR
の要素型に対して呼び出し可能として定義されているとして(範囲for
では同等の処理でコンパイルが通るのでその仮定は満たされているはず)、indirectly_unary_invocable<F, Iterator>
の各制約を検討します。
I
はinput_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>
はI
のiter_value_t
とiter_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
など
- 新しい参照型を生成しようとしている時
つまり、アルゴリズムの実装においては共通参照型を使用することは無く、実際に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
によって呼び出し可能であることをチェックする制約のみを単純に削除することを提案しています。
P2300のsender
アルゴリズムがカスタマイズを見つける手段を修正する提案。
P2300で提案されているsender
アルゴリズム(execution::then
やexecution::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)...);
};
struct then_t {
template <sender Sender, class Fun>
requires
auto operator()(Sender&& sndr, Fun fun) const
{
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),
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_t
のoperator()
が呼ばれてsender
を取得した時点であることがわかります。scheduler
そのものはこのグラフの上から(予め)でも下から(後から)でも指定することができますが、sender
アルゴリズムのカスタマイズ実装が検出されるのは上からscheduler
が指定されている場合のみです。
現在のこのような仕組みにはいくつかの欠点があります
just(42)
(値を投入するだけのsender
アルゴリズム)のような単純なsender
はその完了scheduler
を知らない
- その処理が開始された実行コンテキストで完了するが、これは
sender
が構築された時点ではわからない
on(sched, then(just(), fun))
(実行コンテキストの指定/切り替え)のようなsender
の場合、ネストしたthen(just(), fun)
の部分のsender
はscheduler
を知らない状態で構築される
- ここで
sched
に対してカスタマイズされたthen
実装を使用させるにはどうすればいい?
when_all(sndr1, sndr2)
(指定された全ての処理の完了待機)のような複合sender
は一般的にはその完了scheduler
を知ることができない
sndr1, sndr2
の両方が完了scheduler
を知っていたとして、それぞれをsched1, sched2
としてもsndr1, sndr2
のどちらが先に完了するかによってwhen_all
のsender
の完了scheduler
はsched1, 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 {
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
が与えられていないため、後続のbulk
はthread_pool_scheduler
に対するカスタマイズを見つけられない)、デフォルト実装が使用された結果としてバルク操作はスレッドプールの1つのスレッドだけを使用して実行されます。
このことは明らかに間違った動作であり、修正が必要となります。この提案はその修正を行おうとするものです。
この提案による変更は次のようなものです
- 他に決定可能なドメインがない場合に使用する
default_domain
型を追加
- 新しい
get_domain(env) -> domain-tag
転送クエリを追加
- カスタマイズ不可能な
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
- 標準の遅延
sender
型は構造化束縛によって[tag, data, …children]
に分解可能なsender
型を返す
- 全ての引数のドメイン型が同じにならない限り、
when_all
アルゴリズムの呼び出しはill-formed
when_all
の返すsender
はその環境を介してそのドメインを公開する
on(sch, sndr)
アルゴリズムはtransfer
アルゴリズムの後からのカスタマイズを拾い上げるためにtransfer
に対して指定されるべき
sender
ファクトリjust, just_error, just_stopped
では、タグ型を指定する必要がある
let_value(sndr, fun)
アルゴリズムでは、先行するsndr
(sender
オブジェクト)が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
を使用する
schedule_from(sched, sndr)
アルゴリズムはget_domain(get_env(s))
がget_domain(sched)
と同じになるようなsender
オブジェクトs
を返す必要がある
- 次のアルゴリズムについては、デフォルトの実装では結果の
sender
を返す前に作業を行う必要があり、それはdefault_domain::transform_sender
のオーバーロードで行われる
- 次のアルゴリズムはデフォルトの実装が他のより原始的な操作を利用して指定されているが、それらを
default_domain::transform_sender
のオーバーロードによって置き換えた形になる
transfer
transfer_just
transfer_when_all
transfer_when_all_with_variant
when_all_with_variant
let_value(snd, fun)
アルゴリズムでは、入力関数fun
が返す可能性のあるsender
の型は全て同じドメインを持っていなければならない
- そうでなければ、
let_value
の呼び出しはill-fomred
let_value
のsender
はそれを自身のドメインとして報告する
let_error
とlet_stopped
でも同様
sender
消費アルゴリズムstart_detached
とsync_wait
では、タグディスパッチのためにアルゴリズムタグと入力sender
のドメインをタグとして使用する
この提案では、sender
アルゴリズムは指定された完了scheduler
ではなくその処理グラフのドメインからアルゴリズムのカスタマイズ実装を発見するようにしています。コード上ではドメインはドメインタグ型として表現され、when_all
のようなアルゴリズムでは入力のsender
のドメインが全て一致(すなわち、ドメイン型が一致)している場合にのみ使用可能となります。そして、sender
アルゴリズムによる処理グラフの構成時に加えて、全ての情報が揃った時(sender
がreceiver
にconnect
された時)に再度カスタマイズ実装を探索する後からのカスタマイズ(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
を返します。
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
の標準化作業を続けることに合意されています。
標準ライブラリ機能がアロケータを使用する際のポリシーの提案。
この提案は、上の方のP2267やP2979で提言されていたポリシー整備に関する具体的なものの一つです。ここではLEWGに向けて、新しいライブラリ機能がアロケータを使用するべきかや使用する際のガイドラインとして2つのポリシーを提案しています。
- クラスはどのような場合にアロケータを使用する必要があるか?
- 動的にメモリを確保するクラスは、構築時にアロケータを受け入れ、確保と解放のためにそのアロケータのコピーを保持する必要がある
- アロケータを使用するサブオブジェクト(基底クラス/非静的メンバ)を含むクラスは、それらのサブオブジェクトに転送するためにアロケータを構築時に受け入れる必要がある
- 型が
std::pmr
名前空間にエイリアスを持つ必要があるのはどのような場合か?
- デフォルトで
std::allocator
の特殊化に設定されたアロケータパラメータをもつクラステンプレートには、std::pmr
名前空間にpolymorphic_allocator
についてのエイリアスが必要
- アロケータパラメータを
std::allocator
の特殊化に設定するもエイリアスを持つクラステンプレートは、std::pmr
名前空間にpolymorphic_allocator
についてのエイリアスが必要
これらのポリシーは、現在の標準ライブラリのアロケータサポートをベースとしたものです。
数値コンセプトの設計についての文書。
この文書のいう数値コンセプトとは、数学的な意味での数というものをC++コンセプトで定式化しようとするものです。主に、<numeric>
にあるもののコンセプト対応や将来の物理量と単位を扱うライブラリにおいて使用することを意図しています。
まず全ての数値コンセプトはオプトインが必要で、そのためのいくつかの型特性が用意されます
number
コンセプトを有効化するためのenable_number
、enable_complex_number
- 特定の数値を示す
number_zero
、number_one
- 例えば、
number_zero_v<T>
のようにしてnumber
型T
の零元を取得する
- 数値に関連した型を取得する
number_difference_t
、vector_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> &&
negative<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>>;
この文書による数値コンセプトの設計はまだ完全ではなく経験も不足しているため、この文書は現状報告であり、将来の標準数値コンセプトの実現のための1つの足がかりとして提出されたものです。
浮動小数点数型の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
- 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は事前条件違反で未定義動作となり、その場合全ての実装で第一引数を返すようです。
min(qNaN, 2.f);
max(qNaN, 2.f);
min(2.f, qNaN);
max(2.f, qNaN);
min(-0.f, +0.f);
max(-0.f, +0.f);
min(+0.f, -0.f);
max(+0.f, -0.f);
この振る舞いは並行プログラミングにおいては問題となる可能性があり、異なるスレッドからの値の出力を待機してこのような比較を行う処理がある場合に、その処理結果はデータの到着順によって変化し、実行ごとに異なった結果になる(特に符号が異なる)可能性があります。
Cのfmin/fmax
の場合、符号付0の扱いはstd::min/std::max
と同じですが、QoI(実装品質)として-0 < +0
とすることが許可されています。また、aNaNは欠落したデータとしてもう片方の値を返します。
fmin(qNaN, 2.f);
fmax(qNaN, 2.f);
fmin(2.f, qNaN);
fmax(2.f, qNaN);
fmin(-0.f, +0.f);
fmax(-0.f, +0.f);
fmin(+0.f, -0.f);
fmax(+0.f, -0.f);
fminimum(qNaN, 2.f);
fmaximum(qNaN, 2.f);
fminimum(2.f, qNaN);
fmaximum(2.f, qNaN);
fminimum(-0.f, +0.f);
fmaximum(-0.f, +0.f);
fminimum(+0.f, -0.f);
fmaximum(+0.f, -0.f);
fminimum_num(qNaN, 2.f);
fmaximum_num(qNaN, 2.f);
fminimum_num(2.f, qNaN);
fmaximum_num(2.f, qNaN);
fminimum_num(-0.f, +0.f);
fmaximum_num(-0.f, +0.f);
fminimum_num(+0.f, -0.f);
fmaximum_num(+0.f, -0.f);
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::atomic
のfetch_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::atomic
のfetch_max()/fetch_min()
はstd::min/std::max
とは異なるセマンティクスを提供することを提案しています。また、既存のAPIとの非一貫性を和らげるために、C23のfminimum/fmaximum
およびfminimum_num/fmaximum_num
を<cmath>
に追加しstd
名前空間で利用できるようにすることも提案しています。
クラステンプレート内での特殊化名を指すクラス名を基底クラスリストでも使用可能にする提案。
クラステンプレート内部において、そのクラスの名前は現在のクラステンプレートの特殊化名を指しています。これは、クラスの内部でのみ使用可能であり、特に基底クラス指定の場所では使用できません。
template<typename D>
class crtp_base {...};
template<typename T, typename U, typename V = int>
class sample
: crtp_base<sample>
, crtp_base<sample<T, U>>
, crtp_base<sample<T, U, V>>
{
sample* p;
void f() {
const sample& r = *this;
}
friend bool operator==(sample, sample) = default;
};
このような名前のことを規格用語ではinjected-class-name(注入されたクラス名)と言います。
基底クラスで注入されたクラス名が使用できないことにより、主にCRTPパターンの記述時にその記述が冗長かつ複雑になります。この例のように短いテンプレートパラメータ名が小数だけならさほど変化はありませんが、標準のコンテナ型のように多様なテンプレートパラメータを取る場合に問題は大きくなります。
また、注入されたクラス名が使用できない場所でそのつもりでクラス名を使用しても、テンプレート名でしかないことから通常エラーになりますが、テンプレートパラメータにデフォルトパラメータが指定されている場合はそのパラメータについての指定を忘れていたとしてもエラーにはなりません。これはともすれば見つけづらいバグの元になる可能性があります。
Deducing thisのおかげでCRTPを記述する必要性は大きく減少していますが、C++20以前の環境でも使用されるコードなどにおいて依然としてCRTPを使用したいケースは残っています。そのため、この提案はクラス定義における基底クラスリスト内部でも注入されたクラス名を使用可能にしようとするものです。
ただし、クラステンプレートの基底クラスリストにおけるそのクラスの名前は、テンプレートテンプレート名としては有効であるため、基底クラスがテンプレートテンプレートに対して部分特殊化していたり、テンプレートテンプレートに対して動作が変わるような記述をしていると、現在有効なコードがコンパイルエラーとなるようになります。
struct WasType {};
struct WasTemplate {};
template <typename Type>
auto foo() -> WasType;
template <template <typename...> class Template>
auto foo() -> WasTemplate;
template <typename Type>
struct CurrentlyUnambiguousBase
: decltype(foo<CurrentlyUnambiguousBase>())
{
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;
template <template <typename...> class>
auto bar(long) -> WasTemplate;
template <typename Type>
struct DifferentBehavior
: decltype(bar<DifferentBehavior>(0))
{
using InsideBody = decltype(bar<DifferentBehavior>(0));
};
static_assert(std::is_base_of_v<WasTemplate, DifferentBehavior<void>>);
static_assert(std::is_same_v<WasType, DifferentBehavior<void>::InsideBody>);
先ほどの例はクラス名が注入されたクラス名としても扱われるようになることで2つの関数の間でオーバーロード解決が失敗していましたが、この例では追加の引数の一致によって順序がつく(0
はlong
よりもint
によりマッチする)ことによってエラーにならずに選択される関数が変化します。
これらのコードは標準仕様としては曖昧ではなく明確であったとしてもコードの読者にとっては既に曖昧であり、2つ目の例などは1行場所が異なるだけで異なることをしているのはほとんどの読者が気づかないものであるとして、このような例は考慮しないことを提案しています。
EWGのレビューにおいては消極的な推進の合意が得られており、実装経験を求めています。
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
オブジェクトが何のどのような情報を保持しているのかわからなくなる点などの使いにくさや、ユーザー定義属性の必要性などについて報告しています。
std::hive
(元plf::colony
)の標準への導入のために、コンテナの実使用実体を探るためのアンケートとその結果を記載した報告書。
LEWG/SG14の一部のメンバはstd::hive
のようなコンテナが本当に実使用されている、あるいは必要とされているのかに疑問を持っている人がいるようで、彼らを納得させるためとstd::hive
の標準導入へのサポートのために、C++を使用している企業やコミュニティに向けてアンケートを取りました。この文書は、その結果を報告するものです。
次のようなアンケートを、企業へメール送信、reddit/discordでポスト、その他ゲーム開発系Webコミュニティへポスト、の形でアンケートを募りました
- 業務において次のようなタイプのコンテナを使用していますか?
- 複数のメモリブロックまたは単一のメモリブロック内のシーケンシャルストレージ
- 要素は削除時に何かしらのマーキングがなされ、マーキングされた要素はイテレーション中にスキップされる
- 1がyesの場合、そのコンテナは複数のメモリブロックからなるのか、単一であるか?
plf::colony
もしくは提案中のstd::hive
を知っていますか?
- このタイプのコンテナが標準化されることで、メリットがあると思いますか?
- その他質問やコメントがあれば
このアンケートに対して、企業メールは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
提案者)の経験や実感と一致しているとのことです。
文書には、得られたコメントなどが詳細に記録されています。
std::hive
(元plf::colony
)の標準への導入のために、std::list
との比較やstd::list
の使用実態を報告する文書。
std::list
は要素の挿入・削除に伴って他の要素の移動が発生せず安定しており、それによってポインタやイテレータの安定性を保証しているコンテナです。ただし、std::vector
やstd::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
を標準に導入することで切り替えることができるようになり、先に示したようなパフォーマンスの向上が見込めます。
std::expected<T, E>::value()
が投げる例外をカスタマイズする方法を提供する提案。
P0260R7で提案されている並行キューではそのAPIとしてstd::filesystem
と同様のerror_code
によるAPIを用意することで無例外インターフェースを提供しています。
P2921R0ではerror_code
によるAPIの代わりにstd::expected
を使用したAPIが検討されましたが、それが現在のAPIに比べて明確に優れているとは言えないと報告されています。
void push(const T&);
bool push(const T&, error_code& ec);
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)
|
q.push(5).or_else([](auto code) {
throw conqueue_error(code);
});
...
catch(const conqueue_error& e)
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 {
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
によって投げられる例外を変更することができます。
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年ごろからの長い歴史の中で、人類の科学技術の発展に伴って段階的に導入されたものであり、時・分の単位が下位単位の値を気にしないのは歴史的経緯でありライブラリ設計の問題ではない、としています。
std::valarray
と初期化子リストに対してstd::begin
とstd::cbegin
を呼んだ場合の他のコンテナ等との一貫しない振る舞いを修正する提案。
まず、std::valarray
に対してstd::begin
とstd::cbegin
を呼んだ場合の挙動に違いがあります(libstdc++以外)。
#include <iterator>
#include <valarray>
int main() {
std::valarray<int> v = {1,2,3};
std::begin(v);
std::cbegin(v);
}
std::valarray
は非メンバのstd::begin
オーバーロードを備えており、それが呼び出されています。
一方、std::cbegin
にはstd::valarray
用のオーバーロードが無いためプライマリのstd::cbegin
が使用され、そこではADLによってconst
なstd::valarray
に対してbegin()
を探索しますが、std::cbegin
は<iterator>
ヘッダで定義されているためそのコンテキストからはstd::valarray
の定義が見つからずにエラーになります。
<valarray>
ヘッダが<iterator>
をインクルードしているため、ユーザー側でインクルード順を入れ替えてもこの問題は解決しません。
次に、ほぼ同様の問題が初期化子リストに対してもあります。
#include <iterator>
int main() {
std::begin({1,2,3});
std::cbegin({1,2,3});
}
初期化子リスト({...}
)そのものには型が付かないため、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::begin
とstd::cbegin
に対する一貫しない動作について、std::valarray
の方は機能するように、初期化子リストの方は機能しないように修正するものです。
この提案では、std::valarray
及びstd::initializer_list
の双方からフリーのstd::begin/std::end
オーバーロードを削除し、std::valarray
に対してはメンバのbegin/end
をconst
オーバーロードも含めて追加します。また、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});
|
#include <initializer_list>
#include <iterator>
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});
|
現在 |
この提案 |
#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時点で不要になっていたようです。
低レベルかつ扱いやすい整数演算ライブラリ機能の提案
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機能を特定し、またそれらに優先順位をつけてリストアップしています。
- 基礎的な操作であり、全てのハードウェアでサポートされているため実装が簡単なもの
add_overflow
sub_overflow
mul_overflow
- 実装が簡単だがそれほど有用ではない可能性があるもの
div_overflow
rem_overflow
neg_overflow
- 一部のユースケースで有用だが、実装が難しく実装経験やフィードバックが必要なもの
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
を付加した集成体型となり、計算結果の値と操作ごとの追加情報をまとめて返します。
例えば次のようになります
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に提出することが決定されています。
動的メモリ領域に構築されたオブジェクトを扱うためのクラス型の提案。
クラスの実装の一部がそのクラスの内側にない場合(つまりヒープメモリ等他の場所に配置されているオブジェクトを保持する場合)、多くの場合ポインタによって間接的に保持することになります。スマートポインタを使用すればその際の領域解放忘れを防ぐことができますが、それを保持するクラスに対して幾つかの問題をもたらします。
- 特殊メンバ関数の実装伝播の阻害
T*
もstd::unique_ptr<T>
も、T
の特殊メンバ関数とは無関係にそのメンバ関数を提供する
- 深いコピーの実装
- デフォルトのコピーは禁止か浅いコピーになるため、コピーコンストラクタの実装などを通して手動で実装する必要がある
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_;
Allocator allocator_;
public:
...
};
template <class T, class Allocator = std::allocator<T>>
class polymorphic {
control_block* control_block_;
Allocator allocator_;
public:
...
};
}
polymorphic<T>
の場合はコンストラクタでT
の派生オブジェクトを受け取って、型消去機構を通して保持します。
どちらのクラス型もムーブ後にnull
に相当する状態になります。これはどちらもムーブの実装を簡易かつ効率的にするためです。ただし、この状態をユーザーが通常観測することは意図していないためoperator bool
のようなものは提供されておらず、valueless_after_move()
という専用のメンバ関数でそれをチェックできるようにしています。また、この性質により、両クラス型に対してstd::optional
の特殊化が提供され、余分な領域を使用しないようにしています。
これらのクラス型はそれぞれ以前に個別の提案(P1950とP0201)に分かれて提案されていましたが、このようにその設計が非常に似通っているためこの提案でまとめて提案されることになりました。
2023年9月に行われたLEWGの投票の結果を報告する文書
投票にかけられた提案は次のものです
全てC++26に向けてLWGに転送されています。文書では、投票の際に寄せられたコメントが記載されています。
メンバ関数呼び出しを拡張する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)
)にフォールバックすることを提案するものです。
この提案は現在認識されている次のような問題を解決します
- ジェネリックコードにおける呼び出し構文の汎用化を達成する
- 単一の構文によってメンバ/非メンバ関数を呼び出すことができる
- ライブラリの回避策やそれにしか使用できない言語機能を用意することなくメソッドチェーンを可能にする
<ranges>
の|
演算子やそれを改善することを目指す|>
等は必要なく、.
によってチェーンできる
- それにしか使用できない言語機能を用意することなく拡張メソッドを可能にする
- この提案の
.
拡張は自然に拡張メソッドを可能にする
- 既存のコードに手を入れることなく有効化され、この提案の機能は拡張メソッドのためだけのものではない(むしろその効果はおまけ)
さらに次のような恩恵があります。
- 一貫性・単純さ・教えやすさの向上
- 関数呼び出し構文が1つになるため言語がシンプルになり教えやすさが向上する
- 非メンバ関数呼び出しも引き続き有効だが次善の機能となる(引数なしの非メンバ関数呼び出しを除く)
- 既存のコードの発見しやすさと使いやすさを向上させる
- この提案は既存のC++ライブラリコードを呼び出すとすぐに(何の変更も必要とせず)機能する
- C標準ライブラリを含むCライブラリについても同様
- これによって、既存のライブラリを学習して使用するための強力な方法となる
- ツールサポートの改善
- C++での作業をより便利にする強力な新しいツール機能の作成を直接支援する
- 既存のツール、特にコードのオートコンプリート機能(VSのIntelliSenseなど)を強化する
2に関しては例えば、Cのライブラリ機能を使用する既存のコードを次のように改善します
void f( FILE* file ) {
fseek(file, 9, SEEK_SET);
}
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 );
std::ranges::for_each( in.std::views::transform(rot13), show );
std::ranges::for_each( in.transform(rot13), show );
また、この|
演算子オーバーロードの複雑性解消のための言語サポートを行うためのUFCSとして、|>
演算子の提案(P2672R0)が以前提出されていました。
using namespace std::ranges;
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(筆者の方のプロジェクト)で実装されているようです。
std::thread/std::jthread
に対してスタックサイズとスレッド名を実行開始前に設定できるようにするAPIの提案。
OSの提供するスレッド作成APIではいくつもの追加のオプションがあり(例えばpthreadの場合は12以上)、現在のC++スレッドAPIはそのほとんど全てを指定することができません。そのため、将来的に指定可能にしたいスレッドのオプションはスタックサイズとスレッド名以外にも多数に及ぶ可能性があり、将来のバージョンで少しづつ増やしていった場合に関数インターフェースを複雑にせずになおかつABI破壊を避けるようにAPIを設計しなければなりません。
現在のところ、同様の機能はP2019で提案されています。
P2019R3では、ファクトリ関数によってスタックサイズとスレッド名を指定できるようにするAPIを提案しています。
void f(int);
int main() {
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:
void set_name(const char* name) noexcept;
void set_name(std::string_view name) noexcept;
void set_name(std::string&& name) noexcept;
void set_stack_size_hint(std::size_t size) noexcept;
};
}
将来的にスレッドオプションを増やしたくなった場合は、同様のオプションクラス(jthread::attributes2
など)とそれを受け取るコンストラクタを追加することで、ABIの破壊を回避しながら機能拡張ができます。
また、この提案ではstd::jthread
への移行を促すためにもこのAPIをstd::thread
に対しては追加しないようにしています。
2023年11月のKonaで行われたLEWGのレビュー投票においては、この提案のAPIよりもP2019のAPIの方が好まれたため、この提案の追求は停止したようです。
C++標準化委員会の目標・見通しについて問い直す文章。
昨今のC++を取り巻く状況や個人個人の標準化参画意図などから独立して、標準化委員会が目指すべき方向性や委員会に参加する際に持つべき心得みたいなものについて記述されています。
これは未完成版であり、R1が既に出ているのでそちらを見た方が良いでしょう。
おわり
この記事のMarkdownソース