文書の一覧
全部で122本あります。
一部の記事は次の方々に手伝っていただきました、ご協力ありがとうございました!
もくじ
- N4946 2024-03 Tokyo meeting information
- N4947 INCITS C++/WG21 agenda: 12-17 June 2023, Varna, Bulgaria
- N4948 Working Draft, C++ Extensions for Library Fundamentals, Version 3
- N4949 Editor's Report: C++ Extensions for Library Fundamentals, Version 3
- N4950 Working Draft, Standard for Programming Language C++
- N4951 Editors' Report - Programming Languages - C++
- N4953 Concurrency TS2
- N4954 2023 WG21 admin telecon meetings, rev. 1
- P0342R2 pessimize_hint
- P0447R22 Introduction of std::hive to the standard library
- P0843R6 static_vector
- P1000R5 C++ IS schedule
- P1028R5 SG14 status_code and standard error object
- P1061R5 Structured Bindings can introduce a Pack
- P1068R7 Vector API for random number generation
- P1112R4 Language support for class layout control
- P1144R8 std::is_trivially_relocatable
- P1684R5 mdarray: An Owning Multidimensional Array Analog of mdspan
- P1759R6 Native handles and file streams
- P1885R12 Naming Text Encodings to Demystify Them
- P1901R2 Enabling the Use of weak_ptr as Keys in Unordered Associative Containers
- P1928R4 std::simd - Merge data-parallel types from the Parallelism TS 2
- P2019R3 Thread attributes
- P2022R2 Rangified version of lexicographical_compare_three_way
- P2141R1 Aggregates are named tuples
- P2300R7 std::execution
- P2447R4 std::span over an initializer list
- P2495R3 Interfacing stringstreams with string_view
- P2500R1 C++ parallel algorithms and P2300
- P2546R4 Debugging Support
- P2548R5 copyable_function
- P2552R2 On the ignorability of standard attributes
- P2561R2 A control flow operator
- P2621R3 UB? In my Lexer?
- P2637R2 Member visit
- P2641R3 Checking if a union alternative is active
- P2643R1 Improving C++ concurrency features
- P2654R0 Modules and Macros
- P2662R1 Pack Indexing
- P2663R2 Proposal to support interleaved complex values in std::simd
- P2663R3 Proposal to support interleaved complex values in std::simd
- P2664R2 Proposal to extend std::simd with permutation API
- P2664R3 Proposal to extend std::simd with permutation API
- P2685R1 Language Support For Scoped Objects
- P2686R1 constexpr structured bindings and references to constexpr variables
- P2689R2 atomic_accessor
- P2717R1 Tool Introspection
- P2727R2 std::iterator_interface
- P2728R1 Unicode in the Library, Part 1: UTF Transcoding
- P2728R2 Unicode in the Library, Part 1: UTF Transcoding
- P2728R3 Unicode in the Library, Part 1: UTF Transcoding
- P2741R2 user-generated static_assert messages
- P2746R2 Deprecate and Replace Fenv Rounding Modes
- P2748R1 Disallow Binding a Returned Glvalue to a Temporary
- P2752R2 Static storage for braced initializers
- P2757R2 Type checking format args
- P2767R0 flat_map/flat_set omnibus
- P2769R1 get_element customization point object
- P2771R1 Towards memory safety in C++
- P2774R0 Scoped thread-local storage
- P2775R0 2023-05 Library Evolution Polls
- P2781R1 std::constexpr_v
- P2781R2 std::constexpr_v
- P2786R1 Trivial relocatability options
- P2811R2 Contract Violation Handlers
- P2811R3 Contract Violation Handlers
- P2811R4 Contract Violation Handlers
- P2814R0 Trivial Relocatability --- Comparing P1144 with P2786
- P2821R1 span.at()
- P2828R1 Copy elision for direct-initialization with a conversion function (Core issue 2327)
- P2829R0 Proposal of Contracts Supporting Const-On-Definition Style
- P2831R0 Functions having a narrow contract should not be noexcept
- P2834R0 Semantic Stability Across Contract-Checking Build Modes
- P2835R0 Expose std::atomic_ref's object address
- P2837R0 Planning to Revisit the Lakos Rule
- P2839R0 Nontrivial relocation via a new "owning reference" type
- P2841R0 Concept Template Parameters
- P2842R0 Destructor Semantics Do Not Affect Constructible Traits
- P2843R0 Preprocessing is never undefined
- P2845R0 Formatting of std::filesystem::path
- P2846R0 size_hint: Eagerly reserving memory for not-quite-sized lazy ranges
- P2848R0 std::is_uniqued
- P2850R0 Minimal Compiler Preserved Dependencies
- P2852R0 Contract violation handling semantics for the contracts MVP
- P2853R0 Proposal of std::contract_violation
- P2855R0 Member customization points for Senders and Receivers
- P2857R0 P2596R0 Critique
- P2858R0 Noexcept vs contract violations
- P2861R0 The Lakos Rule: Narrow Contracts And noexcept Are Inherently Incompatible
- P2862R0 text_encoding::name() should never return null values
- P2863R0 Review Annex D for C++26
- P2864R0 Remove Deprecated Arithmetic Conversion on Enumerations From C++26
- P2865R0 Remove Deprecated Array Comparisons from C++26
- P2866R0 Remove Deprecated Volatile Features From C++26
- P2867R0 Remove Deprecated strstreams From C++26
- P2868R0 Remove Deprecated std::allocator Typedef From C++26
- P2869R0 Remove Deprecated shared_ptr Atomic Access APIs From C++26
- P2870R0 Remove basic_string::reserve() From C++26
- P2871R0 Remove Deprecated Unicode Conversion Facets From C++26
- P2872R0 Remove wstring_convert From C++26
- P2873R0 Remove Deprecated locale category facets for Unicode from C++26
- P2874R0 Mandating Annex D
- P2875R0 Undeprecate polymorphic_allocator::destroy For C++26
- P2876R0 Proposal to extend std::simd with more constructors and accessors
- P2878R0 Reference checking
- P2878R1 Reference checking
- P2880R0 Algorithm-like vs std::simd based RNG API
- P2881R0 Generator-based for loop
- P2882R0 An Event Model for C++ Executors
- P2883R0 offsetof Should Be A Keyword In C++26
- P2884R0 assert Should Be A Keyword In C++26
- P2886R0 Concurrency TS2 Editor's report
- P2887R0 SG14: Low Latency/Games/Embedded/Finance/Simulation virtual meeting minutes to 2023/05/11
- P2888R0 SG19: Machine Learning Virtual Meeting Minutes to 2023/05/12
- P2889R0 Distributed Arrays
- P2891R0 SG16: Unicode meeting summaries 2023-01-11 through 2023-05-10
- P2892R0 std::simd Types Should be Regular
- P2893R0 Variadic Friends
- P2895R0 noncopyable and nonmoveable utility classes
- P2897R0 aligned_accessor: An mdspan accessor expressing pointer overalignment
- P2898R0 Importable Headers are Not Universally Implementable
- P2901R0 Extending linear algebra support to batched operations
- おわり
2024年3月に東京で開催される、WG21全体会議のインフォメーション。
開催期間翌週の月火(2023/03/25-26)には、C++ to Japanというカンファレンスイベントが開催される予定です。
2023年6月にブルガリアのヴェルナで開催される、WG21全体会議のアジェンダ。
ここからは、C++26に向けた作業となります。
Library Fundamental TS v3のワーキングドラフト。
↑の変更点をまとめた文書。
新しく追加された機能などはなく、編集上の修正のみのようです。
C++23のワーキングドラフト第10弾。
これはC++23の最後のドラフトであり、おそらくC++23 標準規格文書と同等なものとなります。
↑の変更点をまとめた文書。
今回は新しく採択された提案はなく、編集上の修正のみです。
Concurrency TS v2のワーキングドラフト。
2023年(今年)のWG21管理者ミーティングの予定表。
<chrono>
の時計型のnow()
が最適化によって並べ替えられないようにする提案。
以前の記事を参照
このリビジョンではこの問題のより汎用的な解決のために、std::pessimize_hint()
という恒等関数を提案しています。
namespace std {
template <typename T> T& pessimize_hint(T& t ) noexcept;
template <typename T> T const& pessimize_hint(T const& t) noexcept;
}
これは値を生成する式に対して使用して、その値の最適化に関して実装が最大限悲観的な仮定を置くように指示するものです。
int main() {
auto start = std::chrono::high_resolution_clock::now();
auto result = std::pessimize_hint(fib(std::pessimize_hint(42)));
auto end = std::chrono::high_resolution_clock::now();
...
}
悲観的な仮定というのは、pessimize_hint()
の引数の式は適格なC++プログラムが実行可能なことは何でも実行できる(実行する)ということで、つまりはその式中で何が起こらないかを仮定できないということです。この例の場合は、fib()
が内部でhigh_resolution_clock::now()
を呼び出すかもしれないため、質の良い実装ではコード上に見える2回目のhigh_resolution_clock::now()
の呼び出しの前にfib()
が評価されることを保証します。
このアプローチはひとまずSG1では反対無しで合意され、EWGへ転送されています。
要素が削除されない限りそのメモリ位置が安定なコンテナであるstd::hive
(旧名std::colony
)の提案。
以前の記事を参照
このリビジョンでの変更は、Appendixにいくつかの項目(hive
の制約の概要、先行技術情報、代替実装に関する情報)を追加したことなどです。
静的な最大キャパシティを持ちヒープ領域を使用しないstd::vector
であるstatic_vector
の提案。
以前の記事を参照
このリビジョンでの変更は
std::vector
のものと共通するようにpush_back()
のセマンティクスを修正
std::optional
を返すtry_push_back()
を追加
- 最大キャパシティを超えた時に未定義動作となる
push_back_unchecked()
の追加
inplace_vector
に名前を変更したい事を追記
などです。
C++26策定までのスケジュールなどを説明した文書。
現在の<sysytem_error>
にあるものを置き換える、エラーコード/ステータス伝搬のためのライブラリ機能の提案。
以前の記事を参照
このリビジョンでの変更は
string_ref
は実装定義の型となった
status_code_ptr
はnested_status_code
に変更
make_nested_status_code()
はアロケータを受け取るように変更
erased<T>
は削除され、erased_status_code<T>
をstatus_code
の適切なタグ付き特殊化へのエイリアスとして追加
などです。
構造化束縛可能なオブジェクトをパラメータパックに変換可能にする提案。
以前の記事を参照
このリビジョンでの変更は、文言の修正のみです。
<random>
にある既存の分布生成器にイテレータ範囲を乱数で初期化するAPIを追加する提案。
以前の記事を参照
このリビジョンでの変更は、引数順の変更と文言の修正などです。
乱数エンジンをE
、分布生成器をD
、範囲をR
として、以前の引数順はstd::generate_random(E、D, R)
だったのが、他のアルゴリズムに合わせて範囲が先に来るように変更され、std::generate_random(R, E, D)
となりました。
std::array<float, N> array;
std::mt19937 eng(777);
std::uniform_real_distribution dis(1.f, 2.f);
std::ranges::generate_random(eng, dis, array);
std::ranges::generate_random(array, eng, dis);
クラスレイアウトを明示的に制御するための構文の提案。
構造体の非静的メンバ変数として異なるサイズやアライメントを持つ型を持たせると、それらの間や構造体末尾にパディングが発生します。そのクラスが多数のメンバ変数を持つ場合や配列の要素として扱われる場合、このようなパディングは無駄なメモリを消費することになります。
static_assert(sizeof(bool) < sizeof(int));
struct S1 {
bool b;
int n;
};
static_assert(sizeof(S1) == (sizeof(int) * 2));
struct S2 {
int n;
bool b;
};
static_assert(sizeof(S2) == (sizeof(int) * 2));
P = sizeof(int) - sizeof(bool)
とすると、通常S1
の場合はb
の後にP
バイトのパディングが挿入され、S2
の場合は構造体末尾(b
の後)にP
バイトのパディングが挿入されています。
このようなパディングが挿入されるかどうかは型のサイズだけではなく型のアライメントによっても変化し、メンバが増えたりクラス型だったりすると予測が難しくなります。また、環境の間でデータ型のサイズやアライメントが異なる場合があり、パディングのサイズと位置の予測をさらに困難にさせます。
クラスのパディングをなるべく無くそうとする時に取れる方法として、メンバ変数の順序を並べ替えるという方法があります。パディングが挿入される領域にそのパディングサイズ以下の型が来るようにメンバを並べ替えることで、パディングを最小にすることができます。しかし、C++のコード上からのクラスメンバの順序はその初期化順序と結びついており、メンバを並べ替えると初期化順序が変化します。また、可読性向上のためにメンバ変数を何かしらのグループにまとめる形で並べている場合、並べ替えることによってクラスの可読性が低下します。
従って、パディングを減らすために型のメンバの全てのサイズとアライメントを知っていたとしても、そのためにメンバを並べ替えることは望ましくありません。また、標準ライブラリ等ABIに気を使っている実装を除けば、クラス型のサイズは任意のタイミングで変化する可能性があります。そのため、メンバの順序について完全に制御化にあるクラスであってもそのメンバとなるクラス型について制御が及ばない場合、ある時点で最適なメンバ順序はそれ以降常に最適であり続ける保証はありません。
C++は高級言語であり、多くの場合クラスのメンバ変数はあるクラスのメンバとしての意味論のもとで使用され、それがメモリ上でどのように配置されているかを細かく気にすることは(このパディングの問題がなければほとんどの場合)気にする必要はありません。double
7個とbool
8個(8バイトx7 + 1バイトx8 = 64バイト)をメンバに持つクラスが64バイトの領域に収まって動作する場合、それが72バイトあるいは120バイトの領域を占めるとすればそれは完全にメモリの無駄遣いであり、配列要素として使用すると無駄な領域がその要素数で乗算され増大したり、キャッシュの局所性を損ねたりといったパフォーマンス上のデメリットをもたらします。
この提案は、パディングの問題をコンパイラが自動的に最適なものに解決するようにすることで、これらの問題を解決しようとするものです。
この提案はlayout
属性を追加し、その引数としてレイアウトに関する指示を与えてクラスの宣言に指定することでレイアウトの自動最適化を行うアプローチを提案しています。この属性は[[...]]
のようなものとは異なり、alignas
などに準ずるものです。
レイアウトに関する指示(strategy)は最初に最低1つ追加した後からベンダ拡張も含めて将来的に追加していく予定としていますが、とりあえず次の3つが例としてあげられています
layout(smallest)
- クラスのサイズを最小にするためにメンバ順序を変更する
layout(standard)
- 構造体がstandard-layoutであることを保証する
layout(explicit)
- 実装定義、何もしないことも並べ替えをすることも許可されるほか、PGOや外部ソースからの注入などによるレイアウト変更を許可する
提案文書より、サンプルコード
現在 |
この提案 |
struct Dog {
std::string name;
std::string bered;
std::string owner;
int age;
bool sex_male;
bool can_bark;
bool bark_extra_deep;
double weight;
double bark_freq;
};
|
struct layout(smallest) Dog {
std::string name;
std::string bered;
int age;
bool sex_male;
double weight;
std::string owner;
bool can_bark;
double bark_freq;
bool bark_extra_deep;
};
|
現在 |
この提案 |
struct cell {
int idx;
double fortran_input;
double fortran_output;
};
static_assert(std::is_standard_layout_v<cell>);
|
struct layout(standard) cell {
int idx;
double fortran_input;
double fortran_output;
};
|
現在 |
この提案 |
#define CELL_MEMBERS \
int idx; \
double fortran_input; \
double fortran_output;
struct cell {
CELL_MEMBERS
};
static_assert(std::is_standard_layout_v<cell>);
struct cell_ex {
CELL_MEMBERS
int extra_info;
};
static_assert(std::is_standard_layout_v<cell_ex>);
|
struct layout(standard) cell {
int idx;
double fortran_input;
double fortran_output;
};
struct layout(standard) cell_ex : cell {
int extra_info;
};
|
この提案によるレイアウト調整は実メモリ上のクラスメンバの配置順序を並べ替えたりしますが、C++コード上からのその順序を並べ替えません。すなわち、クラスメンバの初期化順序はコード上の順序によって行われます。
なお、layout
という属性名やレイアウトに関する指示名等はまだ確定した名前ではなく、今後の議論とともに固めていく予定です。
オブジェクトの再配置(relocation)という操作を定義し、それをサポートするためのユーティリティを整える提案。
以前の記事を参照
このリビジョンでの変更は
trivially_relocatable
な型の要件から、ムーブ構築可能であるか破棄可能であること、というのを削除
is_trivially_relocatable_v
の事前条件をis_trivially_copyable_v
に一致するように調整
is_nothrow_relocatable_v
を同期が不十分なため削除
std::relocate
に[[nodiscard]]
を付加し、戻り値型をremove_cv_t<T>
に変更
- 重複する可能性のあるサブオブジェクトに関する問題点に関する記述を削除(実装経験から問題ではないことが分かったため)
pmr
型に関する議論を拡張
- Design goalsセクションを単純化
などです。
改訂された要約によると、この提案は次の5つのユースケースを満足することを目指しています
static_assert(std::is_trivially_relocatable_v<std::unique_ptr<int>>);
struct RuleOfZero { std::unique_ptr<int> p_; };
static_assert(std::is_trivially_relocatable_v<RuleOfZero>);
struct [[trivially_relocatable]] RuleOf3 {
RuleOf3(RuleOf3&&);
RuleOf3& operator=(RuleOf3&&);
~RuleOf3();
};
static_assert(std::is_trivially_relocatable_v<RuleOf3>);
struct [[trivially_relocatable]] Wrap0 {
boost::movelib::unique_ptr<int> p_;
static_assert(!std::is_trivially_relocatable_v<decltype(p_)>);
};
static_assert(std::is_trivially_relocatable_v<Wrap0>);
struct [[trivially_relocatable]] Wrap3 {
Wrap3(Wrap3&&);
Wrap3& operator=(Wrap3&&);
~Wrap3();
int i_;
boost::interprocess::offset_ptr<int> p_ = &i_;
static_assert(!std::is_trivially_relocatable_v<decltype(p_)>);
};
static_assert(std::is_trivially_relocatable_v<Wrap3>);
多次元配列クラスmdarray
の提案。
このリビジョンでの変更は
- コンストラクタオーバーロードのオプションについて議論を追加
- コンテナと整数パックを取るコンストラクタを削除
- コンストラクタ引数順を調整し、extents/mappingはコンテナの前に来るようにする
- 推論補助が曖昧にならないように修正
- コンテナサイズが十分大きいことに関する関連機能に事前条件を追加
mdarray
からコンテナを移動するextract_container()
の追加
data()
をcontainer_data()
へ変更し、container_size()
を追加
data()
は非ユニークレイアウトの場合など、size()
と相性が悪かった
- ムーブ後状態の
data()
はmapping().required_span_size()
と一緒に動作しない可能性があった
data()
とpointer
はcontiguousコンテナのために必要な要件ではないという事実に対処
to_mdspan
とmdspan
への変換演算子を修正
などです。
標準ファイルストリームに、OSやプラットフォームネイティブのファイルを示すものを取得する方法およびその型エイリアスを追加する提案。
以前の記事を参照
このリビジョンでの変更は、native_handle()
をconst
かつnoexcept
にしたこと、LWGのフィードバックに伴う文言修正などです。
この提案は、2023年6月に行われた全体会議で投票にかけられ、C++26 WDへの導入が決定しています。
システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。
以前の記事を参照
このリビジョンでの変更は、text_encoding
オブジェクトがtext_encoding(text_encoding::other)
によって構築されたときに名前が存在しない問題に対処して不変条件を修正した事です。
この提案は、2023年6月に行われた全体会議で投票にかけられ、C++26 WDへの導入が決定しています。
std::weak_ptr
を非順序連想コンテナのキーとして使用できるようにする提案。
この提案の目指すところは、std::shared_ptr/std::weak_ptr
を所有権ベースで区別し、それを非順序連想コンテナ(特に、std::unordered_set
)で管理できるようにすることです。順序付き連想コンテナは比較方法をカスタマイズするだけでそれを達成でき、そのためにstd::owner_less
が用意されています。
現在の標準ライブラリにはそのサポートが無く、自前で用意しようとすると、所有権ベース同値比較はowner_before()
で行えてもstd::shared_ptr/std::weak_ptr
の(所有権ベースの)ハッシュを求めるポータブルな方法がありませんでした。
この提案は、標準ライブラリにそのためのユーティリティを用意することで、ポータブルかつ簡易にstd::shared_ptr/std::weak_ptr
を所有権ベースで非順序連想コンテナのキーとして使用可能にするものです。
この提案では、非順序連想コンテナが使用するstd::hash
とstd::equal_to
に対応するものとして、std::owner_hash
とstd::owner_equal
を標準ライブラリに追加します。これは、既存のstd::owner_less
を参考にしたAPIです。
namespace std {
struct owner_hash {
template <class T>
size_t operator()(const shared_ptr<T>&) const noexcept;
template <class T>
size_t operator()(const weak_ptr<T>&) const noexcept;
using is_transparent = unspecified;
};
struct owner_equal {
template <class T, class U>
bool operator()(const shared_ptr<T>&, const shared_ptr<U>&) const noexcept;
template <class T, class U>
bool operator()(const shared_ptr<T>&, const weak_ptr<U>&) const noexcept;
template <class T, class U>
bool operator()(const weak_ptr<T>&, const shared_ptr<U>&) const noexcept;
template <class T, class U>
bool operator()(const weak_ptr<T>&, const weak_ptr<U>&) const noexcept;
using is_transparent = unspecified;
};
}
この2つのクラスはoperator()
に渡された型のowner_hash()/owner_equal()
メンバ関数を使用して所有権ベースのハッシュ計算/同値比較を行います。そのために、std::shared_ptr/std::weak_ptr
のメンバ関数としてハッシュを求めるowner_hash()
、所有権ベース同値比較を行うowner_equal()
を追加します。
非順序連想コンテナのデフォルトの比較関数型/ハッシュ型をこれらによって置き換えることで、std::shared_ptr/std::weak_ptr
オブジェクトを所有権ベースで非順序連想コンテナに格納することができるようになります。
template<typename T>
using weak_ptr_hashset = std::unordered_set<std::weak_ptr<T>, std::owner_hash, std::owner_equal>;
int main() {
weak_ptr_hashset<int> set{};
auto sp1 = std::make_shared<int>(10);
set.insert(sp1);
auto sp2 = std::make_shared<int>(20);
set.insert(sp2);
auto sp3 = sp1;
set.insert(sp3);
assert(set.size() == 2);
}
この提案は既にLWGのレビューを終えており、C++26に向けて次の全体会議で投票にかけられる予定です。
std::simd<T>
をParallelism TS v2から標準ライブラリへ移す提案。
以前の記事を参照
このリビジョンでの変更は
- 文言のdiffを削除
- タイトルに
std::simd
を含むように変更
ranges
への対応とそれを通したstd::format
サポートについて議論を追加
- イテレータを値で受け取るように修正
- 添字演算子に左辺値参照修飾を追加
value_type
のオブジェクトに対して対応する演算子が有効であるように、各演算子を制約
- mask reductionsの名前を変更
- ABIに関する議論や疑問点の記述を削除
simd_mask
の最初のテンプレートパラメータに関する疑問点を追記
- マスク引数を取るロード/ストアのオーバーロードを追加
simd_mask
引数を使用するようにsimd
reductionsを修正
simd
を返すsimd_mask
演算子を追加
- 条件演算子のhidden friendsオーバーロードを
simd
とsimd_mask
に追加
simd
のためのstd::hash
についての議論を追加
- 比較が必要ないくつかの関数を
totally_ordered
で制約
- 変換ルールの再検討
- ロード/ストアのフラグ名を変更
- ロード/ストアのフラグを拡張して、変換を行うフラグを追加
hmin/hmax
の命名に関する議論を追加
simd
のフリースタンディング化について議論を追加
split
とconcat
について議論を追加
- P0788R3のライブラリ指定スタイルを適用
などです。
std::thread/std::jthread
において、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。
このリビジョンでの変更は
- コンストラクタを追加する代わりに
make_with_attributes()
ファクトリ関数を提案
- スレッドプロパティを
std::thread
に設定する際にNTTPで渡すAPIについての議論の追加
std::thread
にスレッドプロパティのセッター/ゲッターAPIを追加する事についての議論を追加
このリビジョンでは、コンストラクタでスレッドプロパティを渡してから構築するAPIから、ファクトリ関数でスレッドプロパティを指定して構築するものに変わっています
namespace std {
...
class thread {
...
thread() noexcept;
template<class F, class... Args>
explicit thread(F&& f, Args&&... args);
template <class F, class... Attrs>
static thread make_with_attributes(F && f, Attrs&&... attrs);
...
private:
template <class F, class... Attrs>
thread(attribute-tag, F && f, Attrs&&... attrs);
};
}
void f();
int main() {
auto thread = std::jthread::make_with_attributes(f, std::thread_name("Worker"),
std::thread_stack_size(512*1024));
}
std::lexicographical_compare_three_way
のRange版を追加する提案。
以前の記事を参照
このリビジョンでの変更は、提案する文言の修正のみです。
集成体(Aggregate)を名前付きのstd::tuple
であるとみなし、標準ライブラリにおけるstd::tuple
のサポートを集成体に拡張する提案。
以前の記事を参照
このリビジョンでの変更は
- 設計の決定に関するLEWGへの質問に対するLEWGの解答を追記
std::tuple_size
の特殊化ではなくstd::element_count
を使用するように変更
- libstdc++での試験実装に関して追記
などです。
集成体の要素数取得にstd::tuple_size
を使用しないようにしたのは、tuple
を平坦化する(tuple
のtuple
をtuple
にする)ようなコードが在野に存在しており、そこではstd::tuple_size
の使用可能性によってtuple
の要素が平坦化対象であるかを判定している場合があり、その場合にこの提案によって任意の集成体でstd::tuple_size
が提供されるとその動作が静かに変更されるため、それを回避するためです。
P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。
以前の記事を参照
このリビジョンでの変更は
- 修正
sender_of
コンセプトはT
とT&&
を区別しない
sender
ファクトリjust/just_error
にC配列を渡した際、減衰されるのではなくエラーになるようにした
- 機能拡張
sender
とreceiver
コンセプトに対しては、enable_sender
とenable_receiver
というオプトイン型特性が用意されるようになった
- ここには、入れ子の
is_sender
/is_receiver
型を探索するデフォルト実装が用意される
get_attrs
を削除し代わりにget_env
を使用するように
get_env
は対応するtag_invoke
オーバーロードが見つからない場合にempty_env{}
を返すようにフォールバックする
get_env
はその引数のCV参照修飾に影響されないようにする
get_env
、empty_env
、env_of_t
はstd
名前空間に移動
sender
の非同期プログラミングモデルを抽象的な用語で説明するセクションを追加(§11.3 Asynchronous operations [async.ops])
などです。
std::span
にinitializer_list
を受け取るコンストラクタを追加する提案。
以前の記事を参照
このリビジョンでの変更は、P2752への参照を追加したこと、HTMLの修正などです。
P2752はinitializer_list
の背後にある配列を静的ストレージに配置することを許可する提案でC++26に向けて採択されています。これはこの提案の懸念の一つであるinitializer_list
のダングリングに関する問題を回避しません(initializer_list
の生存期間外にそれが参照する配列を参照するのは相変わらず未定義動作)が、組み合わせることで最適化を促進する可能性があるとのことです。
P2752はinitializer_list
の背後にある配列を静的ストレージに配置することで不可視の余分なコピーを回避する最適化を促進するもので、この提案はstd::span
がinitializer_list
から構築できるようにするものです。この2つが組み合わさることで、initializer_list
から構築されたstd::span
は静的ストレージの配列を直接参照するものになり、initializer_list
のためのスタック消費すら回避することが可能になります。これは少なくとも、先行実装のclangにおいて行われることが確認されています。
std::stringstream
がstd::string_view
を受けとれるようにする提案。
以前の記事を参照
このリビジョンでの変更は、提案する文言の修正とis_convertible_v<const T&, const CharT*> == false
という制約を削除したことです。これによって、const CharT*
を取るコンストラクタにアロケータとオープンモードの指定ができるようになります。
この提案は2023年6月の全体会議でC++26に向けて採択されています。
P2300で提案されている実行コンテキストの指定を、C++17の並列アルゴリズムにも適用できるようにすることを目指す提案。
以前の記事を参照(番号が間違って公開されたとのことで提案番号が変更されています)
このリビジョンでの変更は
- CPOではなく、P2547のcustomizable functionを用いる定義例を追加
- この提案の対象の並列アルゴリズムをRangeアルゴリズムに設定(
std::ranges
名前空間以下のアルゴリズム)
- 並列Rangeアルゴリズムに対するデフォルトの実行ポリシーのサポートを提案
- 現在の並列アルゴリズム(非Range、
std
名前空間にあるもの)に対して、scheduler
を受け取る制約付きオーバーロードを追加
- 動作は
std::ranges
にある対応するものにリダレイクトする
execute_on
の動作の明確化
for_each
アルゴリズムで示すと、この提案によって追加されるものは次のものです
namespace std {
namespace ranges {
template<execution_policy Policy, input_iterator I, sentinel_for<I> S, class Proj = identity,
indirectly_unary_invocable<projected<I, Proj>> Fun>
constexpr ranges::for_each_result<I, Fun>
ranges::for_each(Policy&& policy, I first, S last, Fun f, Proj proj = {});
template<execution_policy Policy, 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(Policy&& policy, R&& r, Fun f, Proj proj = {});
template<policy_aware_scheduler Scheduler, input_iterator I, sentinel_for<I> S,
class Proj = identity, indirectly_unary_invocable<projected<I, Proj>> Fun>
constexpr ranges::for_each_result<I, Fun>
ranges::for_each(Scheduler sched, I first, S last, Fun f, Proj proj = {}) ;
template<policy_aware_scheduler Scheduler, 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(Scheduler sched, R&& r, Fun f, Proj proj = {}) ;
}
template <policy_aware_scheduler Scheduler, typename ForwardIterator, typename Function>
void for_each(Scheduler&& sched, ForwardIterator first, ForwardIterator last, Function f);
}
標準ライブラリにデバッグサポートの為のユーティリティを追加する提案。
以前の記事を参照
このリビジョンでの変更は、LEWGでの投票結果を記載したことです。
この提案はLEWGでのレビューを終えて、LWGに転送されています。
std::move_only_function
に対して、コピー可能なCallableラッパであるcopyable_function
の提案。
以前の記事を参照
このリビジョンでの変更は、命名に関するセクションを追加したこと、型消去関数ラッパの二重ラッピングを回避するための推奨プラクティスを追記したことなどです。
この提案は2023年6月の全体会議でC++26に採択されています。
属性を無視できるという概念について、定義し直す提案。
以前の記事を参照
このリビジョンでの変更は明確ではありませんが、この提案では以前に示していた3つの観点に基づいた3つの属性無視に関するルールを提案しています
- 属性の構文的な無視に関するルール
- 標準属性は構文的に無視できず、パースされなければならない
- 引数の構文エラーや固有の規則や追加の構文要件は診断されなければならない
- 属性引数のエンティティはODR-used
- 標準属性の意味的な無視に関するルール
- well-formedなプログラムが与えられた時、特定の標準属性のインスタンスを全て削除すると、プログラムの観測可能な振る舞いを変化させることが許可される
- ただし、削除後の動作が削除前のプログラムにとって適合した振る舞いである場合に限る
__has_cpp_attribute
の振る舞いに関するルール
- 標準属性の機能テストマクロは、実装がその属性のオプショナルなセマンティクスを実装している場合にのみ正の値を返す
- 1つ目のルールに要求されるように、単にそれを構文的にパースして構文をチェックするだけの場合には正の値を返してはならない
この提案は次のリビジョン(R3)が2023年6月の全体会議でC++26に採択されています。
std::expected
などを返す関数において、エラーの伝播を自動化させる演算子??
の提案。
以前の記事を参照
このリビジョンでの変更は、タイトルの変更と演算子名をe??
からe.try?
に変更したことです。
名前の変更の理由は、??
がnull合体演算子として他の言語で多用されていて、ここで提案しているエラー伝播とはかなり異なることをしており、演算子の意味の混同を回避するためです。
字句解析するだけで未定義動作を引き起こすものについて、未定義ではなくする提案。
以前の記事を参照
このリビジョンでの変更は、ベースとなるWDを更新したことです。
この提案は2023年6月の全体会議でC++26に向けて採択されています。
std::visit
やなどをメンバ関数として追加する提案。
以前の記事を参照
このリビジョンでの変更は、std::visit_format_arg
を非推奨にしたことと機能テストマクロを追加したことです。
この提案は2023年6月の全体会議でC++26に向けて採択されています。
定数式において、あるオブジェクトが生存期間内にあるかを調べるためのstd::is_within_lifetime()
の提案。
以前の記事を参照
このリビジョンでの変更は、機能テストマクロを追加したことと、この関数が参照ではなくポインタをとる理由を追記したことです。
is_within_lifetime()
が参照ではなくポインタを取るのは
- 一時オブジェクトを考慮しないで良くなる
- 他の低レベルの機能もポインタを取る(
std::construct_at()
やstd::start_lifetime_as()
など)
- 参照の有効性に関して考慮しなくて良くなる
などの理由によります。
この提案は2023年6月の全体会議でC++26に向けて採択されています。
C++20で追加された動機プリミティブ周りの機能を強化・改善する提案。
以前の記事を参照
このリビジョンでの変更は
pari<T, bool>
とoptional<T>
を戻り値として使用する際の利点/欠点について追記
<chrono>
の一部がフリースタンディングではないことを考慮して、時限待機関数のフリースタンディング指定に関する議論を追記
barrier::try_wait_for
とbarrier::try_wait_until
の提案する文言を追加
- サンプルコードを追加
barrier::try_wait_for/_until
がarrival_token
を受け取らないように修正
- ヒント付き待機メカニズムに関する議論を削除(別の提案とするため)
- 時間制限のない
try_wait
を削除
などです。
標準ライブラリで提供されるマクロを、標準ライブラリモジュール(std.compat
)からエクスポートするようにする提案。
C++23からstd/std.compat
モジュールが提供されるようになり、標準ライブラリの全体をモジュールとしてインポートできるようになります。ただし、これには標準ライブラリ(特に、C互換ヘッダ)でマクロとして提供される機能が含まれていません。
これはモジュールの仕様に基づくもので、名前付きモジュールからはマクロをエクスポートすることができないためです。この制限がないヘッダユニットと呼ばれる、従来のヘッダファイルをモジュールとしてimport
する方法もありこちらはマクロもエクスポートされますが、標準ライブラリのC互換ヘッダはヘッダユニットとしてimport
可能であるかは実装定義です。
結局、標準のマクロ機能を使用しようとすると、従来のヘッダファイルのインクルード以外に手段がありません。
この提案は、モジュールにおけるマクロの扱いに変更を加えることなくこの制限を取り除くために、標準ライブラリ中でマクロとして提供される機能の代替提供手段を検討するものです。
この提案では、その対象として次のものを挙げています
- リテラル値に置換されるマクロ
- これらは
#if
ディレクティブで多用されるため、constexpr
変数で置換できない
- テキスト置換を行わない新しいプリプロセッシングディレクティブにより解決(別提案)
assert
- このマクロはC++においては様々な問題を抱えている
- P2884R0では、
assert
をキーワード化して演算子として使用するようにすることを提案しており、懸念事項が取り上げられている
offsetof
setjmp/longjmp
- C++オブジェクトモデル及びオブジェクト生存期間の概念と直接関わるもの
- キーワード化して動作を提供することを提案
va_arg
- 言語の基礎的な機能であり、
import
で使用可能であるべき
- キーワード化して動作を提供することを提案
errno
ATOMIC_XXX_LOCK_FREE
- これらのマクロはサポートされる場合にコンパイラによって定義される(モジュールからエクスポートする必要がない)
ATOMIC_FLAG_INIT
- C23ライブラリで削除されているため、削除すれば解決
この提案では必ずしも個別の解決策全てを提案しておらず、他の提案に委ねている部分があります。
以前の記事を参照
パラメータパックにインデックスアクセスできるようにする提案。
以前の記事を参照
このリビジョンでの変更は、EWGのリクエストにより構文の代替案を検討したこと、提案する文言を改善したことなどです。
現在の構文はpack...[index]
のような構文ですが、異なる選択肢として次のような構文があげられています
pack.[index];
pack<index>
もしくはpack...<index>
std::nth_type<index, pack...>
もしくはstd::nth_value<index>(pack...)
packexpr(args, I);
[index]pack;
- パックオブジェクト(P2671R0)
この提案では今の所、現在のpack...[index]
が最善であるとしています。
この提案はEWGでのレビューを終えて、C++26目指してCWGに転送されています。
↓
std::simd
でstd::complex
をサポートできるようにする提案。
以前の記事を参照
R2での変更は、提案の概要を追加したことなどです。
このリビジョンでの変更は、提案する文言を追加したことです。
R2では、std::simd<std::complex<T>>
に対するアクセサ(real()/imag()
)や、数学関数の特殊化を用意するようにしています。
↓
Parallelism TS v2にあるstd::simd
に、permute操作のサポートを追加する提案。
以前の記事を参照
R2での変更は、提案の概要を追加したことなどです。
このリビジョンでの変更は
- 生成されたシーケンスを最適化するコンパイラの機能について追記
- メモリ操作(gather/scatter)を独自のクラスにした
- ジェネレータが返す特別なインデックスと、ジェネレータに対するサイズ引数を設計オプションから本文へ移動
- ジェネレータは入力配列の使用する要素を指定するインデックスを返す関数(ラムダ式等)
- 特別なインデックスを返すことで要素の初期化を制御したり、サイズ引数を追加で渡すことでインデックス計算を効率化する
- マスクを用いたcompress/expand操作を行う関数に、空いた場所を埋める値を指定する引数を追加
- gather/scatterによるメモリの並び替え関数の文言と例を追加
- コンパイル時/実行時/マスクによる並び替えに関する文言を追加
などです。
スコープ付アロケータモデル(scoped allocator model)に基づくアロケータのカスタマイズのための言語機能の提案。
以前の記事を参照
このリビジョンでの変更は
- スコープ付オブジェクトモデルをより明確にターゲットにした
- emダッシュとenダッシュを一貫性をもって使い分ける
- 多相アロケータの
constexpr
について質問を追記
などです。
構造化束縛にconstexpr
指定できるようにする提案。
以前の記事を参照
このリビジョンでの変更は、ローカルスコープの構造化束縛によって参照が使用される場合の問題が指摘されたことを受けて、それに対処した実装の選択肢を追加したことです。
参照を取得することは変数のアドレスを取得することとほぼ等価です。構造化束縛においては、tuple
-likeオブジェクトに対しての場合にのみ参照が暗黙的に使用されており、定数式においてこれが問題になります。
void f() {
constexpr auto [a] = std::tuple(1);
static_assert(a == 1);
constexpr auto __sb = std::tuple(1);
constexpr const int& a = get<0>(__sb);
}
ローカル変数のアドレスは不定でありその関数の実行の度に変化します。したがって、この場合の参照a
が保持するアドレスは関数の実行の度に変化します。一方でa
はconstexpr
変数であるので、コンパイル時に1度初期化された後はどのタイミングで参照しても定数であるはずです。すなわち、ローカル変数へのconstexpr
参照は定数になりえず、constexpr
であることと矛盾します。
これが問題になるのは、自動ストレージのtuple
-likeオブジェクトに対して構造化束縛する場合で、それはすなわちローカルスコープでtuple
-likeオブジェクトに対して構造化束縛する、ごく一般的なケースです。それ以外の場合(配列やクラス型オブジェクトに対する構造化束縛)では問題にならず、constexpr
参照の正しい用法は静的ストレージにあるオブジェクトを参照させることです。
CWGのレビューにおいてこれが問題視され、これを解決するための方向性を検討し1つを選択するためにEWGに差し戻されました。このリビジョンでは、そのための選択肢をいくつか用意して説明しています。
static
であるか非tuple
-likeの場合のみconstexpr
構造化束縛を許可する
constexpr
変数を暗黙static
にする
get()
の呼び出しを常に再評価する
tuple
-likeオブジェクトの構造化束縛の場合のみ、constexpr
参照の発生を受け入れる
- 記号的なアドレス指定(Symbolic addressing)
- コンパイル時参照はアドレスによって変数を参照するのではなく、特定のオブジェクトそのものを参照する
- そして、それを定数評価の間維持する
この提案では最も有望な選択肢として4番目の方法を推しています。記号的なアドレス指定は構造化束縛に特化したものではないため、より一般的なconstexpr
参照/ポインタを許可することができます。
記号的なアドレス指定によって許可されるconstexpr
参照は、その参照先のオブジェクトがコンパイル時定数であるかとは直行した概念となります。
int main() {
static int i = 0;
static constexpr int & r = i;
int j = 0;
constexpr int & s = j;
}
参照をコンパイル時定数にできるのは、定数評価中に参照がどのオブジェクトを参照しているのかを(そのオブジェクトが定数であるかに関係なく)追跡可能だからです。
EWGのレビューにおいては、thread_local
変数を除いた記号的なアドレス指定の方向性が支持され、文言調整のためにCWGに転送されました。ただし、最終的な承認のためには実装経験や実装者からのフィードバックが必要であるとしています。
アトミック操作を適用した参照を返すmdspan
のアクセッサである、atomic_accessor
の提案。
以前の記事を参照
このリビジョンでの変更は
atomic-ref-bounded
をatomic-ref-bound
に変更
atomic-ref-unbounded
をatomic-ref-unbound
に変更
basic-atomic-accessor::offset
とbasic-atomic-accessor::access
の文言を修正
- P2616R3が採択された場合同様の変更を
atomic-ref-bound
に加える必要があることを確認
などです。
この提案はLEWGにてレビュー中です。
C++周辺ツールが、Ecosystem ISにどれほど準拠しているのかを互いに通信する手段を標準化する提案。
以前の記事を参照
このリビジョンでの変更は
- スコープ、機能レベル、ユースケースおよび提案する文言を追加した
- イントロスペクションの実装を些細なものにし、宣言を素直にするために、イントロスペクションと宣言のインターフェースを簡素化
- この簡素化によって、境界付きイントロスペクションインターフェースを削除
などです。
イテレータを簡単に書くためのヘルパクラスの提案。
以前の記事を参照
このリビジョンでの変更は
iterator_category
を定義する方法(または定義するかどうか)を変更
iterator_concept
がforward_iterator_tag
(の派生)である場合にのみ定義する
pointer
がvoid
である場合、またはreference
が参照型ではない場合に->
を定義しないようにした
<=>
のサポート
input_iterator
の場合は、後置++
の戻り値型をvoid
にした
などです。
この提案は次のリビジョンがLEWGでの設計合意に至っており、提案する文言を揃えてからLWGに転送する予定です。
↓
↓
標準ライブラリにユニコード文字列の相互変換サポートを追加する提案。
以前の記事を参照
R1での変更は
- コードポイントを受けるインターフェースでは、
char32_t
を使用する
- コードユニットを受けるインターフェースでは、
charN_t
を使用する
- 変換をすぐ行うアルゴリズムを削除し、対応する
view
を残しておく
- 全ての
output_iterator
の削除
utfN_view
のテンプレートパラメータを、view
の実装に使用されるトランスコーディングイテレータの型ではなく、form-range
の型に変更
- 全ての
make
関数を削除
- 誤って作成された
as_utfN()
関数をas_utfN
アダプタに置き換え
transcoding_error_handler
コンセプトを追加
unpack_iterator_and_sentinel
をCPOにする
- UTFイテレータコンセプトを
input_range
に格下げ
R2での変更は
- バッファからの変換例を再導入
null_sentinel_t
をここ以外のところでも使用できるように一般化
- 不正な形式のエンコーディングを検索するユーティリティ関数では、イテレータペアの代わりに
range
を受け取る
utf{8,16,32}_view
を単一のutf_view
に置き換え
R3での変更は
noexcept
の付加
- 必須ではない定数とユーティリティ関数を削除し、残ったものの使用法を詳しく説明する
- P1629R1で提案されている似たものについて、その違いを追記
- 例を拡張
view
のセマンティクスの説明の誤りを修正し、その使用例を追加
などです。
static_assert
の診断メッセージ(第二引数)に、コンパイル時に生成した文字列を指定できるようにする提案。
以前の記事を参照
このリビジョンでの変更は、char8_t
のサポートを削除したことなどです。
結局、static_assert()
でサポートされる文字列の文字型はchar
のみとなりました。ただし、この提案としてはchar8_t
もサポートするべきという方向性を崩しておらず、導入をスムーズにするための措置であると思われます。
この提案は既に、2023年6月の全体会議においてC++26に向けて採択されています。
浮動小数点環境の丸めモード指定関数std::fesetround()
を非推奨化して置き換える提案。
以前の記事を参照
このリビジョンでの変更は、rint()
系関数に対応するcr_rint<R>()
(指定された丸めモードに従って浮動小数点数値を型R
の整数値に変換する)を追加したことです。
glvalueが暗黙変換によって一時オブジェクトとして参照に束縛される場合をコンパイルエラーとする提案。
以前の記事を参照
このリビジョンでの変更は
- 動機付けのための別の例を追加
- 評価されない文脈についての議論を追加
- 影響を受けるライブラリの規定について保護する文言を追加
などです。
std::initializer_list
の暗黙の配列がスタックではなく静的ストレージに配置されるようにする提案。
以前の記事を参照
このリビジョンでの変更は、定数評価中の未規定の振る舞いについて議論を追加したことです。
この提案は2023年6月の全体会議でC++26に向けて承認されています。
std::format()
のフォーマット文字列構文について、幅/精度の動的な指定時の型の検証をコンパイル時に行うようにする提案。
以前の記事を参照
このリビジョンでの変更は機能テストマクロを追加したことです。
この提案は2023年6月の全体会議でC++26に向けて承認されています。
flat_map
/flat_set
の仕様にあるいくつかの問題点とその解決策について報告する提案。
この提案は、libc++におけるflat_map
/flat_set
とそのファミリを実装する過程で明らかになった問題をまとめ、解決が可能なものはその解決策について報告するものです。
この提案で報告されている大きなものは次のような事項です
- 編集上の変更
- 一部のデフォルト引数を持つ
explicit
コンストラクタの分離
flat_set::insert_range()
において、要素をムーブするようにする
flat_set::insert_range()
において、要素をムーブするようにする
insert()
がemplce()
を使用しないようにする
- 挿入位置決めのために、まず最初に挿入予定の要素をスタック上に構築する必要があるが、引数で渡されているオブジェクトを使用することでこれを回避できる
- 同じ理由から、
falt_multiset
においてヘテロジニアスなinsert()
が有用となるため追加する(これは、他のmulti
な連想コンテナと異なる性質)
emplace()
の制約を削除
flat_set::insert()
に制約を追加し、イテレータペアを渡した時にヘテロジニアスinsert()
と曖昧にならないようにする
sorted_unique
をとるinsert()
のオーバーロードに、range
をとるものを追加
insert(sorted_unique, args...)
は、複数の要素がソート済で一意であることを前提に1操作で挿入するAPI
insert(first, last)
に対してinsert(sorted_unique, first, last)
、insert(il)
に対してinsert(sorted_unique, il)
はあった
- しかし、
insert(range)
に対してinsert(sorted_unique, range)
が欠けていたため、これを追加する
- ソートが必要なコンストラクタの計算量の指定の修正
- 一部のソート済みを仮定しないコンストラクタにおける計算量が
O(N)
と指定されている
- これを達成するのは容易ではなく、そのような規定を
ranges::sort()
と同等になるように修正
replace()
が右辺値参照ではなく値で受けるようにする
replace(key_container_type&&, mapped_container_type&&)
はキーと対応する値の配列を受けて、内部の配列をそれによって置換するAPI
- 引数としては、内部コンテナ型の右辺値参照を受けていた
replace()
は常に右辺値を渡さなければならないが、似た他の場所のAPIではこのような用法ではなかった
- 値で受け取るようにすることで、コピーして渡すことを容易にしつつムーブして渡す場合の使用感を維持する
flat_set::keys()
の追加
flat_map
には、そのキーと値の配列を参照するためのkeys(), values()
が用意されているが、flat_set
にはない
- 利便性向上と一貫性のために、
flat_set
にkeys()
(だけ)を追加する
他にも、解決策が提案されていないIssueがいくつか報告されています。
tuple-like
なオブジェクトから特定のインデックスの要素を抜き出すCPOの提案。
以前の記事を参照
このリビジョンでの変更は
- 構造化束縛の未使用変数の名付けの問題について、P2169R3の
_
を適用
tuple-like
コンセプトの要件緩和の可能性について追記
- P2141R1とP2547R1の影響について追記
std::ranges::get
名のAPI/ABI破壊を最小に抑えるアプローチを採用
- 機能テストマクロを追加
などです。
依存関係を追跡することによる時間的なメモリ安全性保証を言語に導入する提案。
以前の記事を参照
このリビジョンでの変更は、依存関係宣言をより明確にしたこと、コンパイル時と実行時のチェックをより明確に分離したこと、インターフェースについての議論を追加したことです。
この提案はSG23の議論でこれ以上レビューされないことが決定されています。
ローカル変数に束縛されたスレッドローカルストレージを簡易に扱うためのクラス、std::tls<T>
の提案。
C++17の並列アルゴリズムなどによってFork-Joinモデルのような並列化を行い、各スレッド毎に結果を出力する必要がある場合、その出力先の同期を取る必要があります。スレッド1つにつき1つ(単一のオブジェクト)の出力であればstd::atomic
等を用いることで同期を効率化できますが、出力が多数(コンテナなど)の場合、std::mutex
等による明示的なロックが必要となります。
そのような場合にスレッドローカルストレージ(thread_local
)を使用すると見た目はシンプルになりますが、全てのスレッドに対して隠れたコストが発生するなどローカルな問題をグローバル化してしまう等の欠点があります。
そこで、次のようなローカルオブジェクトに束縛されたスレッドローカルな領域を使用するとスレッドローカルストレージの欠点を回避することができます。ただ、これは多数のライブラリ機能を複合させた複雑なものであり、使用も煩雑になりがちです。
std::span<Triangle> input = …;
double max_area = …;
std::mutex m;
std::unordered_map<std::thread::id, std::vector<Triangle>> tmp;
std::for_each(std::execution::par, input.begin(), input.end(),
[&](const auto & tria) {
auto& ref{[&] -> std::vector<Triangle> & {
const auto tid{this_thread::get_id()};
const lock_guard lock{m};
const auto it{tmp.find(tid)};
if (it != tmp.end()) return it->second;
return *tmp.emplace(tid, {}).first;
}()};
for (const auto & t : split(tria, max_area)) {
ref.emplace_back(t);
}
}
);
for(const auto & tria : tmp | std::views::join) {
process(tria);
}
tmp.clear();
この提案は、このような非thread_local
なスレッドローカルストレージのためのラッパークラスを提供することで、このような用途(1スレッドが複数の出力を行う場合)におけるより効率的で使いやすいスレッドローカルストレージを提供しようとするものです。
提案されているstd::tls
はまさに上記のコード例におけるm
とtmp
およびその初期化部分をラップするようなクラスで、次のようなものです。
namespace std {
template<typename T, typename Allocator = allocator<T>>
class tls {
mutex m;
unordered_map<thread::id, T, hash<thread::id>, key_equal<thread::id>, Allocator> storage;
NOTE
unmovable_function<Allocator, T() const> init_func;
public:
tls(Allocator alloc = Allocator{}) noexcept requires is_default_constructible_v<T>;
tls(T value, Allocator alloc = Allocator{}) requires is_copy_constructible_v<T>;
tls(auto func, Allocator alloc = Allocator{}) requires is_convertible_v<T, invoke_result_t<decltype(func)>>;
tls(const tls &) =delete;
auto operator=(const tls &) -> tls & =delete;
~tls() noexcept;
[[nodiscard]]
auto local() -> tuple<T &, bool>;
void clear() noexcept;
class iterator { … };
static_assert(forward_iterator<iterator>);
auto begin() -> iterator;
auto end() -> iterator;
};
}
init_func
は最初に領域を取得しようとする場合にその領域を初期化するための関数であり、std::mutex
は領域の取得時に同期をとるために必要となります。領域の取得はlocal()
関数で行いますが、これはメンバで持っているstd::mutex
により保護されたスレッドセーフな関数となります。そして、local()
によって取得される領域はスレッドIDによって管理されているため、一度取得してしまえば以降はロックなしで使用することができます。
ただし、この例は単純なものであり、並行ハッシュマップを使用するなどより効率的な実装が考えられます。
std::tls
を使用すると、先程のサンプルコードは次のように単純化されます
std::span<Triangle> input = …;
double max_area = …;
std::tls<std::vector<Triangle>> tmp;
std::for_each(std::execution::par, input.begin(), input.end(),
[&](const auto & tria) {
auto [ref, _] = tmp.local();
for (const auto & t : split(tria, max_area)) {
ref.emplace_back(t);
}
}
);
for(const auto & tria : tmp | std::views::join) {
process(tria);
}
tmp.clear();
std::tls
はこのように、thread_local
の利点(見た目の単純さ)と明示的ロックによる利点(thread_local
に比べて低コスト)を両立し、なおかつロックの粒度を最小化しようとするクラス型です。
2023年5月にLEWGで行われるLEWG全体投票の予定表。
次の提案が、C++26導入を目指してLWGに転送することを決定するために投票にかけられます。
↓
コンパイル時定数オブジェクトを生成するクラスの提案。
この提案は、以前のP2725とそれをより一般化したP2772をうけてそれらを統合した提案で、整数専用のstd::integral_constant
に対してより広いNTTP値のラッパとなるstd::constexpr_v
を具体的に提案するものです。
std::constexpr_v
は擬似的なconstexpr
引数を実現するためのNTTPラッパクラスです。
namespace std {
template<auto X, class T>
struct constexpr_v {
using value_type = T;
using type = constexpr_v;
constexpr operator value_type() const { return X; }
static constexpr value_type value = X;
...
};
}
提案文書より、使用例
template<typename T>
struct my_complex {
T re, im;
};
inline constexpr short foo = 2;
template<typename T>
struct X {
void f(auto c) {
}
};
template<typename T>
void g(X<T> x) {
x.f(std::constexpr_v<1>{});
x.f(std::constexpr_v<2uz>{});
x.f(std::constexpr_v<3.0>{});
x.f(std::constexpr_v<4.f>{});
x.f(std::constexpr_v<foo>{});
x.f(std::constexpr_v<my_complex(1.f, 1.f)>{});
}
とはいえこれだと長くて使いづらいため、より簡易に生成するユーティリティであるstd::c<value>
が用意されます
namespace std {
template<auto X>
inline constexpr constexpr_v<X> c_{};
}
これを用いると、先ほどの例は次のようになります
template<typename T>
void g(X<T> x) {
x.f(std::c_<1>);
x.f(std::c_<2uz>);
x.f(std::c_<3.0>);
x.f(std::c_<4.f>);
x.f(std::c_<foo>);
x.f(std::c_<my_complex(1.f, 1.f)>);
}
さらに、std::constexpr_v
にはX
に応じて使用可能となる各種演算子が定義されます。ただし、値を変更するもの(複合代入演算子やインクリメント演算子)については直接的には無意味であるためまだ提案に含まれてはいません。式テンプレートなどユーザー定義型のオーバーロードを扱う際には必要となることが示されており、LEWGの決定待ちです。
また、std::constexpr_v
がNTTPX
だけではなくその型T
をわざわざテンプレートパラメータに取っているのは、std::constexpr_v
変数を起点とするADLにおいてX
の型T
の名前空間をその対象に含めるためです。
auto f = std::c_<strlit("foo")>;
std::cout << f << "\n";
この場合、std::constexpr_v<X, T>
がstd::constexpr_v<X>
だけだとX
の型T
(ここではstrlit
)の属する名前空間がADLによる検索対象に含まれないため、NTTP値の型T
のために定義されている演算子オーバーロードを呼び出すことができなくなります。
このstrlit
は文字列リテラルをNTTP化するラッパクラスです。文字列リテラルはNTTPで使用できないため、このようなものが必要となります(ただしこれは提案されていません)。
trivially relocatableをサポートするための提案。
以前の記事を参照
このリビジョンでの変更は
- P1144との比較についての部分を別の提案(P2814R0)に分離したこと
- リロケート操作関数に
constexpr
を付加
- リロケート操作関数をフリースタンディング指定
move_and_destroy
をuninitialized_move_and_destroy
に変更するとともに、規定を修正
uninitialized_move_and_destroy
の設計について追記
- 重複する範囲を扱う
relocate
の完全な仕様を追加
swap
へリロケーションを適用することに関する懸念を追記し、それに関する作業を別の提案に延期した
などです。
↓
↓
契約プログラミングに、ユーザー提供の違反ハンドラを許可する提案。
以前の記事を参照
R2での変更は
contract_semantic
の列挙値からignore
を削除(実際使用されていないため)
- 観測された違反のカウントについての議論を追加
- 安全な停止の使用例を追加
- 序文に、この提案による修正案についての明確な説明を追加
R3での変更は
- Designセクションを追加し、SG21からの疑問に回答
invoke_default_contract_violation_handler
の追加
contract_violation
をポリモルフィックかつコピー不可能にし、その生存期間を明確化
- 契約違反時にプログラムを終了させる場合契約チェック自体に違反するシグナルハンドラをガードするべき、と指摘
contract_kind
とcontract_violation_detection_mode
プロパティの目的について明確化
contract_violation_detection_mode
をdetection_mode
に変更
detection_mode::predicate_exception
をevaluation_exception
に変更
detection_mode::predicate_detected_undefined_behavior
をevaluation_undefined_behavior
に変更
- (これらによって)例外スローの意図が明確になった
R4での変更は
contract_violation
にwill_continue()
を追加
- 例外がどのように動作するかについて、セクション6で明示的に提案
- 例外に関して提案する文言を追加
contract_violation
の各操作に対するnoexcept
と[[noreturn]]
(オプション)の目的を明確化
などです。
このリビジョン時点では、<contract>
ヘッダは次のようになっています
namespace std::contracts {
enum class detection_mode : int {
predicate_false = 1,
evaluation_exception = 2,
evaluation_undefined_behavior = 3
};
enum class contract_semantic : /int {
enforce = 1
};
enum class contract_kind : int {
pre = 1,
post = 2,
assert = 3
};
class contract_violation {
public:
~contract_violation();
contract_violation(const contract_violation&) = delete;
contract_violation& operator=(const contract_violation&) = delete;
const char* comment() const noexcept;
detection_mode detection_mode() const noexcept;
bool will_continue() const noexcept;
contract_kind kind() const noexcept;
source_location location() const noexcept;
contract_semantic semantic() const noexcept;
};
void invoke_default_contract_violation_handler(const contract_violation&);
}
void handle_contract_violation(const std::contracts::contract_violation&);
契約条件チェックに伴って例外がスローされた場合の振る舞いについては次のようにすることを提案しています
- 事前条件/事後条件の評価中に発生した例外は関数本体内で発生したものとして扱われるべき
- 契約条件式の評価から脱出する例外は契約違反ハンドラを呼び出すべき
- この例外を呼び出し元に伝播したい場合、それを行うカスタムハンドラを定義できる
- 例外は、契約違反ハンドラの呼び出し中にスローされる可能性がある
- このような例外はすべて、対応する契約条件式の評価中にスローされる例外と同じようにスタック巻き戻しを実行する
すなわちここでは、関数に対する契約条件は全て、その関数が呼び出す他の関数の評価に伴うものと同様に、現在のC++の例外伝播とnoexcept
ルールに従うようにすることを提案しています。これは、無条件noexcept
指定を行う関数に対する基準であるLakos Ruleに従うものでもあります。
この提案の内容(正確にはR6の内容)はSG21でコンセンサスを得たようで、C++26に向けたContratcts MVPにマージされます。
オブジェクトの再配置(relocation)という操作に関する2つの提案を比較する文書。
relocationについての2つの提案については以前の記事を参照
2023年2月のIssaquah会議において、relocationに関する2つの提案(P1146R7とP2786R0)がEWGIにてレビューされました。結果、この2つの提案には重複する部分が多くあることから、EWG/EWGIがC++におけるrelocation操作についてのよりよい方針を決定するために、2つの提案の重複する部分をまとめ、また異なる部分を明確にすることで2つの提案を比較検討する必要性が示されました。
この文書はそれを受けて、P1146R7とP2786R0が提案するrelocation(特に、trivially relocatable)に関しての設計や構文等の差異を比較しまとめるものです。
2つの提案の主要な違いは次のような事項です
事項 |
P1144R7 |
P2786R0 |
relocationについて |
ムーブ+破棄に相当 |
ムーブ+破棄とは異なる |
ムーブ代入の扱い |
考慮する |
考慮しない |
正しい利用について |
ユーザーを信頼するアプローチ |
間違っている可能性のある用法はエラー |
提供するもの |
ユーザーが利用するための汎用アルゴリズム群を提供する |
コア言語の変更に焦点を当てている |
オプトアウト方法 |
提供しない |
提供する |
2つの提案のいうrelocationという操作とその利点等は共通していますが、P2786R0がその中でもtrivially relocatableに特化したものであることによって、これらの差異が生まれています。
std::span
に.at()
メンバ関数を追加する提案。
以前の記事を参照
このリビジョンでの変更は、例外をスローするためこの関数がフリースタンディングではないことを追加、機能テストマクロを追加したことなどです。
型変換時のコピー省略のためのルールを明確化する提案。
以前の記事を参照
このリビジョンでの変更は、提案する文言を追加したことと例10を追加したことです。
追加された10個目の例では、コピーコンストラクタでもムーブコンストラクタでも無いものを省略している例です。
#include <type_traits>
template <bool has_copy_constructor>
struct Cat {
Cat();
Cat(const Cat&) requires has_copy_constructor;
Cat(Cat&&) requires has_copy_constructor;
template <class C = Cat>
Cat(std::type_identity_t<C>&&) = delete;
};
struct Dog {
operator Cat<false>();
};
Dog d;
Cat<false> c(d);
この例では、現在のC++及びGCC/MSVCはill-formedとなります。これは、Cat<false>
に対して呼ばれるコンストラクタがテンプレート化されたものしかなく、これは省略可能では無いためです。しかし、clangとEDGはこの呼び出しを省略します。
このリビジョンでの提案は、この10個目の例でコピー省略が行われないように省略可能なコンストラクタを制限した上で、EDGのアプローチを採用するようにするものです。また、同時にクラス型のオブジェクトがそのクラスのオブジェクトを1つだけ要素にもつ初期化子リストからリスト初期化される(非常によく似た)ケースもカバーしようとしています。
struct Cat {
Cat() = default;
private:
Cat(const Cat&) = delete;
Cat(Cat&&) = delete;
};
struct Dog {
operator Cat();
};
Dog d;
Cat c1(d);
Cat c2 = {d};
Cat c3{d};
これらの例は現在でも変換コンストラクタが呼ばれコピー省略がなされていますが、それは明確な規定に基づくものではありませんでした。この提案ではそれを明確に規定するとともにこれと類似のケースで変換関数が呼ばれる条件をきちんと制限することでこの提案が紹介しているような実装間の振る舞いの差異が生じないようにしています。そして、その制限とはこの提案で説明されているEDGのアプローチをベースとして修正を加えたものです。
契約プログラミングにおける事後条件の条件式から参照される関数引数がconst
でなければならないことをサポートする関数宣言・定義スタイルの提案。
契約プログラミングにおける事後条件特有の問題として、事後条件から関数引数が参照される場合に事後条件で評価する値がいつキャプチャされるのかが重要になるというものがあります。
int generate(int lo, int hi)
[[pre lo <= hi]]
[[post r: lo <= r && r <= hi]];
int generate(int lo, int hi) {
int result = lo;
while (++lo <= hi)
{
if (further())
++result;
}
return result;
}
この例では、generate()
の戻り値は呼び出し時点のlo
よりも大きくなりますが、関数終了時点のlo
よりも小さくなる場合があります。呼び出し側からみると、関数宣言はコピー渡しであるので変更されず、また事後条件は渡した時点でのlo
の値で評価されるように読み取れます。しかし、関数定義からみるとそうではなく、事後条件は関数の終了直後に評価されることから呼び出し側の期待と異なる結果を生じてしまいます。
この問題は、事後条件から参照される関数引数のうち、非参照(参照引数なら呼び出し側から見ても変更されうることがわかる)であり非const
(const
引数なら関数実行中に変更されない)な関数引数に問題があります。
SG21ではこの問題の対策として、事後条件から参照される関数引数は非参照ならばconst
でなければならないことを決定しました。したがって、上記の例のようなコードはコンパイルエラーとなります。
int generate(int lo, int hi)
PRE(lo <= hi)
POST(r: lo <= r && r <= hi);
int generate(int& lo, const int& hi)
PRE(lo <= hi)
POST(r: lo <= r && r <= hi);
int generate(int lo, int hi)
PRE(lo <= hi)
POST(r: r >= 0);
int generate(const int lo, const int hi)
PRE(lo <= hi)
POST(r: lo <= r && r <= hi);
一方、C++は関数型を決定する場合、関数宣言の解析後に引数型のトップレベルconst
を削除してから関数型を形成します。これはすなわち、次のような関数宣言と定義は同じ関数に対する宣言と定義として有効であるということです
void f(int x);
void f(const int x) { }
別の言い方をすると、関数引数をconst
にする場合それは定義でだけ行えば良いということで、関数引数のconst
は関数のインターフェースの一部ではないということです。
この提案では、このような関数宣言・定義スタイルのことを「const-on-definition style」と呼んでいます。そして、この提案は契約プログラミングにおいて事後条件から関数引数を参照する場合に、このconst-on-definition styleを言語サポートしようとするものです。
具体的には、関数引数がその関数の事後条件内から参照されている場合
- 定義ではない関数宣言では、コンパイラによってその引数は暗黙
const
とみなされる
- 関数定義では、明示的に
const
としなければならない
というようにします。
void f(int x)
POST(is_const_v<decltype(x)>);
void f(const int x)
{
}
現状の契約仕様では、事後条件内から関数引数を参照している場合、その関数の全ての宣言におけるその変数に対して明示的にconst
を付加する必要がありますが、このconst-on-definition styleのサポートによってそれは関数定義だけでよくなります。
標準ライブラリのnoexcept
指定に関する設計について、現在のLakos Ruleを維持すべきとする提案。
Lakos Ruleとは、標準ライブラリの関数にnoexcept
を指定する際のルールのことです。Lakos Ruleでは関数に関する契約(Contract)を定義し、その契約に基づいてnoexcept
指定がされるかどうかを決めます。
Lakos Ruleにおける契約には2種類あり、事前条件を持たない関数は広い契約(Wide Contracts)がなされており、それ以外の関数(何かしらの事前条件を持つ)は狭い契約(Narrow Contracts)がなされているとします。そして、広い契約がなされている関数で例外をスローしない関数に関しては無条件でnoexcept
を指定し、それ以外の関数(特に狭い契約がなされている関数)に関してはnoexcept
を指定しない、とします。
現在の標準ライブラリ関数のnoexcept
指定はこのLakos Ruleに基づいて行われており、これによって"Throws: Nothing"のように指定されているのにnoexcept
指定はない関数が存在しています。
より積極的なnoexcept
指定を目的として、このルールを見直そうという動きがLEWGにおいてあるようで、現在のLEWGのガイドライン(P2148R0)では、狭い契約がなされている関数であってもLWGで例外を投げないという合意が取れれば無条件でnoexcept
を指定することになっています。他にも、P1656R2ではLakos Ruleを標準ライブラリ設計原則から外すべきだと主張されています。
この提案はそのような動きに反対し、Lakos Ruleが現在でも必要かつ有用であり標準ライブラリ設計原則として維持されるべき理由を解説するものです。
主にライブラリ関数のテスト(特にネガティブテスト)における有用性が主張されている他、noexcept
を指定することがコンパイラの最適化にとって有利であることを示した報告はなく(むしろ低下させることを示した報告はある)、パフォーマンス向上を目的としてライブラリ関数に片っ端からnoexcept
をつけて回ることは間違っているとも述べられています。
契約プログラミングにおいて、契約述語の存在がビルドモードによって異なる影響を他の言語機能に及ぼさないようにする提案。
契約条件がその評価に伴って例外を投げる場合、その契約がなされている関数がnoexcept
指定されているとすると、その関数に対するnoexcept
演算子はどのように振る舞えばいいのかが問題になります。あるいは、契約条件が満たされない場合に例外を投げるような場合(ビルドモードもしくは例外ハンドラによる)にも同様の問題が発生します。
まだこの問題の結論は出てはいませんが、1つの方針として、契約(事前条件)の評価は関数の呼び出し前に行われるため、契約条件が例外を投げるかどうかはその関数の例外仕様の一部では無い、とするものがあります。その場合、関数のnoexcept
指定は契約の有無や内容によらず常に有効であり、その関数に対するnoexcept
演算子はtrue
を返すことになります。
void my_func(int i) [[pre: i >= 0]];
void your_func(int i) noexcept [[pre: i >= 0]];
int x;
static_assert( false == noexcept(my_func(x)) );
static_assert( true == noexcept(your_func(x)) );
しかし、契約条件(事前条件)の評価に伴う例外(契約条件式からのものであれ、契約が破られた時のものであれ)は全て、関数が呼び出される前にスローされます。例外が発生するのが関数の呼び出し前なのか後なのかを判断する仕組みはなく(そしておそらくそのような仕組みは意味がなく)、上記のyour_func()
は契約条件を評価するビルドモードでは常に例外をスローする可能性があります。そのため、事前条件のチェックが関数の例外仕様の外側にある場合に、契約条件を評価するビルドモードではnoexcept
演算子は契約がなされている関数に対してfalse
を返す以外の選択肢がありません。
int x;
static_assert( true == noexcept(your_func(x)) );
static_assert( false == noexcept(your_func(x)) );
すなわち、noexcept
演算子の振る舞いはビルドモードによって変化してしまいます。
このようなビルドモードによる例外仕様の意図しない変化は、noexcept
演算子によって関数の例外仕様をチェックしそれによって処理を分岐させている(これはnoexcept
の正しい用法です)コードに対して、静かにバグを埋め込んでしまう可能性があります(例は提案を参照)。
結局、契約条件を評価するビルドモードにおける関数の例外仕様の問題を回避するためだけに、事前条件を関数呼び出し前(または事後条件を呼び出し後)に評価するという戦略は、有効性が疑わしく実行可能ではありません。実行可能な唯一の選択肢は、関数の宣言から観測可能な例外仕様がnoexcept
演算子やその他のコンパイル時クエリの動作を、全ての契約チェックビルドモードで同じになるように制御することです。
この提案ではまず、次のような原則を提示しています
- ビルドモードの独立性
- 契約がなされている関数は、
noexcept
演算子をはじめとするコンパイル時のクエリについて、コンパイル時のセマンティクスが契約チェックのビルドモードによって変化することはない
- 契約がなされている関数においてその契約条件がコンパイルされwell-formedだったならば、
noexcept
演算子は全てのビルドモードで(すなわち契約が評価されるかどうかに関わらず)同じ動作をする
- Lakos Rule
noexcept
指定された関数の例外仕様と狭い契約は、本質的に互換性がなく、矛盾している
- つまり、何かしらの契約がなされている関数は狭い契約を持つ(引数等に関して事前条件を持つ)ため、
noexcept
指定されるべきではない
- 無視される契約条件のオーバーヘッドをゼロにする
- 契約条件が無視された(ビルドモードによって)場合、その契約がなされている関数等付近のコードは、あたかもその契約条件がコメントアウトされたかのように振る舞う
- ただし、ビルドモードに関わらず、契約条件から参照されているものはODR-useされる
その上で、Lakos Ruleを言語機能として組み込み強制させること(つまり、noexcept
指定されている関数に対する契約の指定をコンパイルエラーとすること)は回避します。テストのためなど、noexcept
指定と契約チェックを両立したいユースケースは想定され、また、嘘のnoexcept
指定(実際は例外を投げうるが開発者が追加の情報からそれを考慮しなくて良いと判断している場合など)にも有効なユースケースがあります(例外を投げうるムーブコンストラクタを持つ型をラップして、ムーブコンストラクタをnoexcept
にするなど)。そのように、関数の持つプロパティの一部をコンパイラが強制することはC++プログラマに利益をもたらしません。
これらのことをベースに、この提案では契約プログラミング導入後のnoexcept
に関して次のことを提案しています
- 関数の引数を初期化した後、未処理の例外をスローする
noexcept
関数のそれ以降のステップは、 [except.spec]/5に従ってstd::terminate()
を呼び出す
すなわち、noexcept
指定されている関数に契約を付与することができ、noexcept
関数ではその契約は評価及び破られた時にも例外を投げないとみなされます。もしその仮定が裏切られ、その契約が評価中に例外を投げるか、契約が満たされなかった時に例外を投げた場合、現在のnoexcept
関数から例外を投げた時と同様にstd::terminate()
を呼び出してプログラムを終了させます。
この提案は、契約条件が例外を投げるかどうかはその関数の例外仕様の一部では無いとする方針の特別なケースであり、この方針によって示された利点(契約が評価されるか否かを翻訳単位の外で決定できるなど)を享受しつつ、ビルドモードによるコンパイル時プロパティの変化という欠点を回避することができます。
std::atomic_ref
が参照しているオブジェクトのアドレスを取得できるようにする提案。
一部のハードウェアには、同じコア上で実行され同じプログラムステップを実行している、同じプログラムの異なるスレッドを検出するための命令が備わっています。
そのようなハードウェアではその命令を使用して、複数のスレッドで実行されるアトミック操作を1つのスレッドでだけ実行される単一の操作に集約することができます。そのようなパターンを用いると、複数スレッド間での同期のコストを削減し、パフォーマンスを向上させられる可能性があります。
単純なコードで記述すると、次のようなコードパターンになります
void unsynchronized_aggregated_faa(atomic<int>& acc, int upd) {
auto thread_mask = __discover_threads_with_same(acc, upd);
auto thread_count = popcount(thread_mask);
if(__pick_one(thread_mask))
acc.fetch_add(thread_count * upd, memory_order_relaxed);
}
そのようなハードウェアにはたとえばNVIDIAのGPUが該当し、同じWarpに所属しているスレッドが空間的に近いスレッドとなります。NVIDIAのGPU(CUDA)では、そのような命令として__match_any_sync()
(と__activemask()
)が提供されています。
そのような組み込みの命令(上記例の__discover_threads_with_same()
)では、スレッドグループが共有している変数のポインタを受け取って、同じポインタを渡してきたスレッドを同じスレッドグループだと判定するものがあります。複数のスレッドで共有する変数なのでstd::atomic
を使用するのは自然で、そのような命令にはstd::atomic
変数のアドレスを渡すことになります。
この時に、std::atomic_ref
を用いているとそのような命令を使用することができなくなります。なぜなら、std::atomic_ref
はそもそも参照セマンティクスを持つ型なので関数には値渡しをするはずで、そうすると、各スレッドが持っているstd::atomic_ref
オブジェクトはローカルのものになり、そのアドレスはおそらく一致の保証がありません。
void unsynchronized_aggregated_faa(atomic_ref<int> acc, int upd) {
...
}
int main() {
int n = 0;
std::atomic_ref<int> ar{n};
unsynchronized_aggregated_faa(ar, 0);
}
std::atomic_ref
を参照渡しすれば解決できるかもしれませんが、それは無意味な二重参照であり、ともすれば間接参照のコストがかかってきます。あるいは、std::atomic
の参照/ポインタを用いても解決できますが、提案によるとそれが必ずしもできない場合があるとのことです。
std::atomic_ref
を使用している時でも、複数のスレッドで同じ1つのオブジェクトをアトミックに共有しているということは変わっておらず、この場合に欲しいのはstd::atomic_ref
が参照しているオブジェクトのアドレスです。しかし、現在のstd::atomic_ref
はそれを完全に隠蔽しており、取得する方法がありません。
この提案は、このような目的のためにstd::atomic_ref
に.data()
を追加して、その参照先のアドレスを取得できるようにしようとするものです。
namespace std {
template<class T>
struct atomic_ref {
...
T const* data() const noexcept;
...
};
}
Lakos Ruleの見直しを、契約プログラミング機能が固まるまで延期する事を推奨する提案。
Lakos Ruleは標準ライブラリの関数にnoexcept
を付加する際の基本的なルールです。C++11で導入されて以来10年以上経過しており、最近のライブラリ設計者はこのルールを改定することを頻繁に提案しているようです。
一方、現在契約プログラミング機能のC++26への導入に向けて活発な作業が続いています。もしそれが標準入りした場合、標準ライブラリ実装に対して契約を適用する事を許可するかどうかという事が議論され、それを許可する場合は現在文書で指定されている契約条件がどのように契約コードにエンコードされるべきかのガイドライン(ルール)を策定する必要があります。
契約プログラミングの事前・事後条件が必要になる関数というのは、Lakos Ruleでいうところの狭い契約を持つ関数であり、そのような契約プログラミングに関するガイドラインにはLakos Ruleが密接にかかわってくることは明らかです。
Lakos Ruleは標準ライブラリの上に構築されるプログラムが外的要因などによって標準ライブラリ機能の使用を制限されることが無いように、意図的に保守的なルールになっています。そのため、Lakos Ruleを順守するライブラリの上にLakos Ruleに従わないプログラムを書くことができる一方で、Lakos Ruleを順守しないライブラリの上にLakos Ruleに従うプログラムを書くことはできません。
契約プログラミングの機能がまだあまり固まっていないこともあり、契約プログラミングを標準ライブラリに適用する際のルールや原則がどのようになるかはまだ明らかではありません。しかし、そこにはLakos Ruleが関わってくることは明らかです。
そのため、この提案は、C++標準ライブラリの基礎的な設計指針としてのLakos Ruleを少なくとも契約プログラミングの準備が整うまでは現状を維持する(改訂を延期する)ことを提案するものです。
リロケーション(relocation)の言語サポートのための、新しい参照型の提案。
リロケーションについての2つの提案については以前の記事を参照
リロケーション、特にトリビアルなリロケーション操作は、ムーブを効率化(オブジェクト全体のmemcpy
)しムーブ後オブジェクトの問題を解決することができます。
この提案は、トリビアルなリロケーション操作の背景を解説することを目的とし、上の2つの提案のようなライブラリサポートではなくムーブとよく似た機構による言語サポートを提案するものです。
この提案では、型T
に対するowning referenceT~
をまず導入します。これは、リロケーションされようとしているリロケーション元のオブジェクトを指す参照であり、そのような状態のオブジェクトの値カテゴリはrlvalueとなります。
owning reference型の値はengagedとdisengagedのどちらかの状態にあり、engaged状態のowning referenceはオブジェクトを所有しています。engaged状態のowning referenceがその生存期間を終えると、所有している(参照している)オブジェクトは破棄されます。なお、プログラムの特定の地点でowning referenceがengagedであるか否かは後述するルールに従って静的に決定されます。
この提案ではリロケーション(rlvalueあるいはT~
へのキャスト)はreloc
演算子によって行います。左辺値のオブジェクトははreloc
演算子によってrlvalueに変換され、その後元のオブジェクトを参照しようとする式は全てコンパイルエラーとなります。なお、T~
型の変数名自体は左辺値です(右辺値参照型の変数が左辺値なのと同様)。
struct T {
int m;
};
void g(T& x);
void f(T~ ref) {
g(ref);
T~ ref2 = reloc ref;
g(ref);
++ref.m;
g(ref2);
if (rand() % 2) {
{
T~ ref3 = reloc ref2;
}
g(ref2);
} else {
g(ref2);
}
g(ref2);
}
このように、制御フローが分岐する場合にその分岐の一端でowning referenceがdisengaged状態になった場合、その制御フローが合流する地点(disengagedになってない分岐パスの終了地点)で同じowning referenceはdisengaged状態に移行します。
このような関数と同様に、T~
を引数にとるコンストラクタを定義することができます。それはリロケーションコンストラクタ(relocation constructor)と呼ばれ、上記のT~
及びrlvalueの性質からムーブコンストラクタよりも強く所有権を引き取るコンストラクタです。コンストラクタから値を返すことは(例外を除けば)できないので、リロケーションコンストラクタに渡したowning reference(及びその参照元オブジェクト)の寿命は、そのコンストラクタが終了する時に終了することになります。
ある特定の型では、リロケーションコンストラクタが暗黙定義されます。暗黙定義されたリロケーションコンストラクタは既存の特殊メンバ関数と同様のルールに従いますが、常に無条件noexcept
である点だけが異なります。あるいは、明治的にdefault
定義しておくこともでき、その場合も暗黙定義された時と同じ性質を持ちます。そのようなデフォルトリロケーションコンストラクタはクラス型によって次のように動作します
- トリビアルにリロケーション可能な型では、
memcpy
によってオブジェクト表現をコピーしたかのようにオブジェクトを初期化する、トリビアルリロケーションコンストラクタが定義される
- トリビアルリロケーションコンストラクタでは、ソースオブジェクトの生存期間を終了させるもののそのデストラクタを呼び出さない
- そうではなく、自身のxvalueから直接初期化が可能で有効なデストラクタを持つような型(
C
とする)の場合
C
のxvalueからC
を直接初期化するために選択されたコンストラクタとC
のデストラクタの両方がdefault
宣言されているならば
C
の基底クラス及びメンバ変数の再起的なリロケーションを行う
- それ以外の場合、
C
のムーブコンストラクタに委譲する
C(C~ source) : C(static_cast<C&&>(source)) {}
- ソースオブジェクトはこのコンストラクタの完了後に破棄される
- それ以外の場合、リロケーションコンストラクタは
delete
される
型T
のrlvalueは例えば次のような変換が可能です
- rlvalue(
T~
)が.
もしくは.*
の左辺のオペランドである時、rvalue(T&&
)に暗黙変換される
- prvalue(
T
)はrlvalue(T~
)に暗黙変換できる
- オーバーロード解決において、この変換は右辺値参照または
const
左辺値参照(T&&
/const T&
)への束縛よりも良い変換とみなされる(優先順位が上になる)
- glvalue(
T&
/T&&
)はstatic_cast
によって明治的にrlvalue(T~
)へ変換できる
- この場合でも、owning reference型は参照先オブジェクトの所有権を引き取り、その生存期間の終わりにengaged状態だったらそのオブジェクトを破棄する
オブジェクトの一部(サブオブジェクト)だけをリロケーションするのは危険なため、継承関係にある型(基底クラスB
と派生クラスD
)の間でB~ -> D~
のような変換は禁止されています。
この提案ではまた、reloc
演算子を使用して自動変数をリロケーションできるようにするために、このowning referenceの観点から自動変数のモデルと定義し直します。owning referenceの導入後、自動変数x
に対して暗黙的に所有参照__x~
が定義されます。__x~
はx
を所有しているため、reloc
演算子によって他の関数やコンストラクタ、owning referenceに所有権が移されない場合、そのスコープの終了時に__x~
がx
を破棄することになります。その後、x
を指名するid式はill-formedとなります。
struct T {
int m;
};
int main() {
T x = {0};
T y;
T~ r = reloc x;
++x.m;
++r.m;
}
reloc
演算子の振る舞いは、このようなモデルをベースとして定義されます。reloc
演算子はid式(変数名)に対して適用できて、結果としてその変数名に結び付けられているオブジェクトのowning referenceを取得し、値カテゴリはrlvalueの式となります。
reloc
演算子は次のように動作します
- オペランドが、直接囲んでいる関数定義に関連するブロックスコープまたは関数パラメータスコープに属する
T~
型の自動変数x
である場合
reloc x
の結果はx
が参照しているオブジェクトを指すrlvalueであり、x
はそれによってdisengagedとなる
- オペランドが、直接囲んでいる関数定義に関連するブロックスコープに属するオブジェクト型の自動変数
x
である場合
ABIによっては、関数引数のオブジェクト型の破棄責任が呼び出し先(関数内)ではなく呼び出し側にあるものがあり、それを考慮するとT~
型ではない関数引数をreloc
することはできないため、2番目の動作ではそれを除いています。ただし、コピー/ムーブコンストラクタを持たずリロケーションコンストラクタだけを持つようなリロケーション専用の型ではこれを認めることも提案しています。
提案では、これらのこと以外にも既存のムーブや右辺値/転送参照周りの仕様を参考にしながら、owning referenceとリロケーションサポートのための言語機能について解説されています。提案は大きく4つのパートに分かれており、それぞれのパートはそれ以前のパートに依存するようになっているため、この提案の内容は全てを一気に導入するのではなく一部を少しづつ導入していくことができます。
コンセプトを受け取るためのテンプレートテンプレートパラメータ構文の提案。
現在テンプレートテンプレートパラメータで渡すことができるのは型のみで、変数テンプレートやコンセプトを渡すことはできません。
この提案は、より高レベルの構成を可能とするために、テンプレートテンプレートパラメータ構文を拡張してコンセプト(と変数テンプレート)を渡せるようにしようとするものです。
これによって例えば、コンセプトアダプタのようなものが可能になったり
template<std::regular T>
void f1(T&&);
template<typename T>
requires std::regular<std::decay_t<T>>
void f2(T&&);
template<typename T, template <typename concept C>>
concept decay_to = C<std::decay_t<T>>;
template<std::decay_to<std::regular> T>
void f3(T&&);
int main() {
int n;
f1(n);
f2(n);
f3(n);
}
あるいは複数のコンセプトを用いる制約を1つにまとめることができたり
template<typename R, template <typename> concept C>
concept range_of = std::ranges::range<R> && C<std::ranges::range_value_t<R>>;
auto f(range_of<std::integral> auro&& r);
auto f(range_of<std::floating_point> auro&& r);
template<typename T, template<typename>... concept Cs>
concept all_of = (Cs<T> && ...);
auto g(all_of<std::regular, std::convertible_to<int>> auto v);
auto g(all_of<std::ranges::view, std::ranges::forward_range> auto v);
現在の標準ライブラリにも見られるコンセプト内の制約の重複を共通化して括り出せたり
namespace now {
template<class F, class I>
concept IndirectUnaryInvocable =
Readable<I> &&
CopyConstructible<F> &&
Invocable<F&, iter_value_t<I>&> &&
Invocable<F&, iter_reference_t<I>> &&
Invocable<F&, iter_common_reference_t<I>> &&
CommonReference<
invoke_result_t<F&, iter_value_t<I>&>,
invoke_result_t<F&, iter_reference_t<I>>>;
template<class F, class I>
concept IndirectRegularUnaryInvocable =
Readable<I> &&
CopyConstructible<F> &&
RegularInvocable<F&, iter_value_t<I>&> &&
RegularInvocable<F&, iter_reference_t<I>> &&
RegularInvocable<F&, iter_common_reference_t<I>> &&
CommonReference<
invoke_result_t<F&, iter_value_t<I>&>,
invoke_result_t<F&, iter_reference_t<I>>>;
template<class F, class I>
concept IndirectUnaryPredicate =
Readable<I> &&
CopyConstructible<F> &&
Predicate<F&, iter_value_t<I>&> &&
Predicate<F&, iter_reference_t<I>> &&
Predicate<F&, iter_common_reference_t<I>>;
template<class F, class I1, class I2 = I1>
concept IndirectRelation =
Readable<I1> && Readable<I2> &&
CopyConstructible<F> &&
Relation<F&, iter_value_t<I1>&, iter_value_t<I2>&> &&
Relation<F&, iter_value_t<I1>&, iter_reference_t<I2>> &&
Relation<F&, iter_reference_t<I1>, iter_value_t<I2>&> &&
Relation<F&, iter_reference_t<I1>, iter_reference_t<I2>> &&
Relation<F&, iter_common_reference_t<I1>,
iter_common_reference_t<I2>>;
template<class F, class I1, class I2 = I1>
concept IndirectStrictWeakOrder =
Readable<I1> && Readable<I2> &&
CopyConstructible<F> &&
StrictWeakOrder<F&, iter_value_t<I1>&, iter_value_t<I2>&> &&
StrictWeakOrder<F&, iter_value_t<I1>&, iter_reference_t<I2>> &&
StrictWeakOrder<F&, iter_reference_t<I1>, iter_value_t<I2>&> &&
StrictWeakOrder<F&, iter_reference_t<I1>,
iter_reference_t<I2>> &&
StrictWeakOrder<F&, iter_common_reference_t<I1>,
iter_common_reference_t<I2>>;
}
namespace future {
template <template <typename...> concept Direct,
typename F, typename... Is>
concept Indirect =
(Readable<Is> && ...) &&
CopyConstructible<F> &&
Direct<F&, iter_value_t<Is>&...> &&
Direct<F&, iter_reference_t<Is>...> &&
Direct<F&, iter_common_reference_t<Is>...> &&
CommonReference<
invoke_result_t<F&, iter_value_t<I>&...>,
invoke_result_t<F&, iter_reference_t<Is>...>>;
template<class F, class I>
concept IndirectUnaryInvocable =
Indirect<Invocable, F, I>;
template<class F, class I>
concept IndirectRegularUnaryInvocable =
Indirect<RegularInvocable, F, I>;
template<class F, class I>
concept IndirectUnaryPredicate =
Indirect<Predicate, F, I>;
template<class F, class I1, class I2 = I1>
concept IndirectRelation =
Indirect<Relation, F, I1, I2>;
template<class F, class I1, class I2 = I1>
concept IndirectStrictWeakOrder =
Indirect<StrictWeakOrder, F, I1, I2>;
}
などの利点があります。
この例を見ればわかるように、コンセプトテンプレートパラメータは通常のテンプレートテンプレートの構文(template<template<typename> typename T>
)をベースに、最後のtypename
(もしくはclass
)のところをconcept
(コンセプト)もしくはauto
(変数テンプレート)で置き換えることで記述します。
template<
typename T,
auto V,
template<typename> typename TT,
template<typename> auto VT,
template<typename> concept C,
>
void f();
この提案の内容はclangのフォークにて実装されており、Compiler Explorerで試すことができます。実装にあたっては特に困ったことは起こらなかったようです。
コンストラクタに関する型特性がデストラクタのセマンティクスに影響を受けないようにする提案。
例えば、is_nothrow_copy_constructible
型特性はnoexcept
演算子を用いて次のようにコンパイラマジックなしで実装でき(そうに思え)ます。
template<typename T>
struct is_nothrow_copy_constructible<T> {
static constexpr bool value = noexcept(T{declval<T const &>()});
};
この時問題になるのはnoexcept
演算子の内部にはT
のデストラクタの実行(一時オブジェクトの破棄)も含まれてしまっていることで、T
のコピーコンストラクタがnoexcept
な時でも、デストラクタがそうではない場合にこれはfalse
になってしまいます。
C++11規格完成前にはこの問題は把握されており、標準の文言はこの問題を回避するために巧妙な言葉遣いをしています。is_nothrow_copy_constructible
の場合はまずis_nothrow_constructible<T, const T&>
に委譲したうえで、is_nothrow_constructible
ではその結果がどうなるかは次のように指定されています([meta.unary.prop]/4)
is_constructible_v<T, Args...>
is true
and the variable definition for is_constructible
, as defined below, is known not to throw any exceptions ([expr.unary.noexcept]).
ここで重要なのは「variable definition (for is_constructible
)」という言葉であり、これはこのチェックのために変数を定義した場合にその定義に当たって例外を投げないということが言いたいらしく、その際例外を投げないとはどういうことかについてコア言語のnoexcept
演算子に投げています。
この変数定義については直接書かれていませんが、この文言を導入したN3142によると次のようなものです
template <class T>
typename add_rvalue_reference<T>::type create();
T t(create<Args>()...);
この変数定義において、変数t
は一時オブジェクトではなく左辺値であり、この変数定義そのものにはデストラクタの実行は含まれていません。従って、この変数定義が有効であり例外を投げない場合にis_nothrow_constructible
はtrue
になるという事です。
このような文言の意図は同様の問題(判定時にデストラクタの実行が混じってしまう)がある他の型特性についても同じ意図で導入されており、この文言が暗に示しているのはコンパイラマジックによってこれらの型特性を実装する(ただし、個別の式に分解した後でそれぞれの式の例外判定にnoexcept
演算子を用いることを許可する)べき、ということです。
このような巧妙な言いまわしはしかし、標準ライブラリ実装者には伝わらなかったようで、C++11および現在に至るまで標準ライブラリ実装はこの文章(上記変数定義)のことを直接チェックするべき式だと思って実装しているようです。つまり結局、冒頭のサンプルコードのような実装になってしまっているようです。
#include <type_traits>
struct Test {
Test() = default;
Test(Test const&) = default;
~Test() noexcept(false) {}
};
static_assert(std::is_trivially_copy_constructible<Test>::value, "non-trivial");
static_assert(std::is_nothrow_copy_constructible<Test>::value, "may throw");
このコードは、現在の主要な標準ライブラリ実装において失敗します(godbolt)。
この提案は、この問題の解決のために、標準の規定のオリジナルの意図と実際の実装のどちらを重視するのかを決定し、それによって標準文書とライブラリ実装のどちらかを修正することを迫るものです。
この提案としては、問題があるのは実装の方だとして、現在標準ライブラリに報告されている関連Issueを欠陥ではない(NAD)として全てクローズし、既存標準ライブラリ実装に対してバグレポートを提出することを推奨しています。また、そのうえで上記のようなチェックすべき変数定義について明確化する事を提案しています。
一方で、既存のライブラリ実装を重視する場合についても考慮されており、いくつかオプションがあるもののその特性の判定にデストラクタの実行が関与することを明確にするように推奨しています。
プリプロセッサに存在する未定義動作を取り除く提案。
未定義動作というのはWell-formedなプログラムの実行時の振る舞いに関する指定であって、コンパイル時に起こるものではありません。現在のプリプロセッサ仕様
には未定義動作がいくつか含まれていますがそれは正しい指定ではなく、それをill-formed, no diagnostic requiredに変更しようとするものです。
この提案の対象は次のものです
#if
ディレクティブの条件式内のマクロを置換したときにdefined
が現れた場合
#if
ディレクティブの条件式のdefined
の使用がおかしい場合
#include
ディレクティブのヘッダ名部分のマクロ置換の結果が"header-name"
か<header-name>
のどちらでもない場合
- 関数マクロ呼び出し時の引数内にプリプロセッシングディレクティブが存在する場合
#
による文字列化の結果が有効な文字列リテラルにならない場合
##
の結果がユニバーサル文字名を形成する場合
##
の結果が有効なプリプロセッシングトークンとならない場合
- 現在の行数を変更する
#line
ディレクティブの行数指定に、0もしくは2147483647以上の数が指定されている場合
#line
に続くトークンをマクロ置換した結果が、#line
ディレクティブとして有効ではない場合
- 事前定義マクロ名もしくは
defined
が、#define
、#undef
の対象となる場合
これらのケースは現在未定義動作とされています。この提案でもこれらの場合の挙動が変わるわけではありませんが、それはIFNDR(不適格だが診断不用)と指定されるようになります。
std::filesystem::path
をstd::format()
でフォーマット可能にする提案。
std::filesystem::path
に対するstd::formatter
特殊化は以前に提案(P1636)されていました。その出力は、std::quoted
をラップしたostream
出力演算子の観点からのフォーマットとして提案され、つまりはpath
オブジェクトを<<
で出力した時と同じフォーマットによって文字列化するものでした。
std::cout << std::format("{}", std::filesystem::path("/usr/bin"));
ただこれには多くの問題がありました。
まず、std::quoted
は"
と\
のみをエスケープします。そのため、パスに改行等の他の制御文字が含まれていると出力はパス文字列として使用できないものになります。
std::cout << std::format("{}", std::filesystem::path("multi\nline"));
この出力はC++やシェル言語等における有効な文字列ではなく、この出力はパス文字列として使用可能ではありません。
もう一つの問題はエンコーディングで、path::native()
はシステムの文字コードとしてbasic_string<value_type>
を返し、value_type
は実行環境のOSによって決定されます。それは通常、POSIX環境ではchar
、Windowsではwchar_t
になります。
<<
による出力ではpath::string<CharT, Traits>()
からパス文字列を取得するため、CharT
によってはその際に内部で保持しているパス文字列の文字コードからの変換が行われる場合があります。std::cout
の場合はPOSIX環境では変換が起きませんがWindows環境では変換が行われ、それによって文字化けが発生します。
例えば次のパスをベラルーシ語で出力しようとすると
std::print("{}\n", std::filesystem::path(L"Шчучыншчына"));
全てのコードページと地域設定がベラルーシ語に設定され、ソースエンコーディングとリテラルエンコーディングがUTF-8である場合でも、Windowsでは次のような出力が得られます
"�����������"
std::print
もpath
も両方ともユニコードをサポートしているにもかかわらず、path::string()
内部の中間の変換でchar
(CP1251)を経由することによって文字間の対応関係が切られ、文字化けが発生します。
これらと同種の問題は、C++23のstd::print
(P2093)と<ranges>
のフォーマット(P2286)で議論され解決されています。
そこでこの提案は、それらの経験を踏まえたエスケープとWindows上でのユニコード変換を行うstd::formatter
のpath
特殊化を追加することで、以前の問題を解決しpath
をフォーマット可能にすることを提案しています。
実装としては、path::native()
から変換なしで文字列を取得して、それを文字列範囲としてRangeのフォーマットに移譲します。その際、エスケープに関してはC++23で追加されたフォーマット指定子?
と同様の処理を行い、これは入力文字列と等価な文字列を生成可能なC++文字列リテラルを出力とするようにエスケープ処理がなされるものです。
path::native()
から変換なしで文字列を取得しているので、std::print
による出力は他の文字列と同様にその内部で適切なユニコード変換によって(入力がユニコードならユニコードtoユニコードの直接変換によって)出力されます。
これによって、先ほどの問題があった例の出力は次のように修正されます
std::cout << std::format("{}", std::filesystem::path("multi\nline"));
std::print("{}\n", std::filesystem::path(L"Шчучыншчына"));
※この部分は@Reputelessさんに執筆していただきました
遅延評価のため要素数が確定しない range の ranges::to
を行う際に、推定の要素数をヒントとして知らせる ranges::size_hint
CPO を追加する提案。
文字列内の小文字を大文字に変換する次のような uppercase_view
を仮定します。"ß" を大文字にすると "SS" と 2 文字になることに注意します。
U"In C++ ist es schwieriger, sich selbst in den fuß zu schießen."sv
| views::uppercase
| ranges::to<std::u32string>();
IN C++ IST ES SCHWIERIGER, SICH SELBST IN DEN FUSS ZU SCHIESSEN
ここで uppercase_view
は sized_range
でも random_access_range
でもありません。そのため、ranges::to
が結果を構築する際に事前に要素数がわかりません。結果、push_back
のような操作でアロケーションが繰り返し起こり、実行時性能にネガティブな影響を及ぼす可能性があります。
しかし、例えば長さ L の文字列を大文字に変換するときは、結果の長さが少なくとも L であることは推定できるはずです。こうしたサイズのヒントを知らせることができれば、同様のケースで、ranges::to
や、range から構築するコンストラクタにおけるアロケーションの回数を抑制できます。
この提案では、そうした機能の実現のために、ranges::size_hint
CPO と、approximately_sized_range
コンセプトを導入し、既存の各種 views に size_hint
への対応を実装します。
ranges::size_hint
CPO は、sized_range に対しては ranges::size
を呼び、それ以外には size_hint
メンバ関数、それが無ければ ADL 経由で見つかった size_hint
を呼びます。また、approximately_sized_range
コンセプトは、range に対して ranges::size_hint
を使って償却定数時間で推定要素数を得られることを示します。
この提案を踏まえると、先ほどの uppercase_view
は次のように実装されるでしょう。ここでは、変換結果が基底の range と同じ長さであると仮定しています。"ß" が含まれるなど特殊なケースでは実際にはもう少し長くなる可能性もあります。この view は要素数を確定できないため size
メンバ関数は持ちません。
template <input_range V>
class uppercase_view {
constexpr const V & base() const;
constexpr auto begin() const;
constexpr auto end() const;
constexpr auto size_hint() requires approximately_sized_range<View> {
return ranges::size_hint(base());
}
constexpr auto size_hint() const requires approximately_sized_range<const View> {
return ranges::size_hint(base());
}
};
※この部分は@Reputelessさんに執筆していただきました
範囲内に重複する隣接要素がないかを調べる std::is_uniqued
, std::ranges::is_uniqued
を <algorithm>
に追加する提案。
現在の標準ライブラリのアルゴリズムを整理すると、次のような対応表を作ることができます。
イテレータを返す |
bool を返す |
等価な実装 |
is_sorted_until |
is_sorted |
is_sorted_until == end() |
is_heap_until |
is_heap |
is_heap_until == end() |
mismatch |
equal |
mismatch == {end(), end()} |
find_if |
none_of |
find_if == end() |
find |
contains |
find != end() |
search |
contains_subrange |
search != end() |
adjacent_find |
(該当なし) |
adjacent_find == end() |
範囲を操作する関数 |
範囲を調べる関数 |
sort |
is_sorted |
make_heap |
is_heap |
unique |
(該当なし) |
この提案では、上記の表の空白を埋める is_uniqued
を標準ライブラリに追加します。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v1 = { 1, 1, 2, 2, 3, 3 };
std::vector<int> v2 = { 1, 2, 3, 1, 2, 3 };
std::cout << std::boolalpha;
std::cout << std::is_uniqued(v1.begin(), v1.end()) << '\n';
std::cout << std::is_uniqued(v2.begin(), v2.end()) << '\n';
}
この提案の著者は、すでに libc++ への is_uniqued
の実験的な実装 を行っています。
並行処理における処理の進行順序認識のために、コーナーケースにおける順序導出を例示する文書。
並行処理プログラムにおいて何もない場所から値を読み出ししてしまう問題をThin-air(read)問題といいます。Thin-airは現在のC++ではメモリモデルと注釈によって強く禁止されていますが、それでも完全に禁止できない場合があり、あるいはそれを許可したい場合があるようです。
そのようなThin-air問題の解決策として検討されているのが、semantic dependency(sdep)と呼ばれるもので、これはプログラム中のデータや制御フロー、アドレスの依存関係に暗黙的に含まれる順序のことで、それをコンパイラが認識できるようにしようとするものです。sdepは、コンパイラが最適化した後にも残らなければならない依存関係でもあり、最適化を禁止する側面と許可する側面を持っています。
この文書は、Thin-airが起こりうるような並行プログラムのコーナーケースのような場合にsemantic dependencyがどのように構築されるかを示すことによって、semantic dependencyがどのような最適化(Thin-air)を許可し、あるいは最適化(Thin-air)を禁止するのかを示すものです。
この文書の目的は、sdepが無ければコンパイラが最適化を行えないケースとコンパイラが自由に最適化できなければsdepを求められないケースを定義することで、semantic dependencyを標準に導入するための技術報告書のようなものにつなげようとするものです。
契約プログラミングの違反ハンドラとそれに伴うセマンティクスに関する提案。
現在C++26に向けて、SG21では契約プログラミングの最小限の設計を確立するための作業が進められています。
最近そこで議論されているのは、契約条件のチェックに伴って起こりうることについて、そのセマンティクス(意味論)をどのように指定するか?ということです。
特に、P2811R1では、ユーザー定義の違反ハンドラを許可することで契約違反が起きた場合の振る舞いをカスタマイズできるようにすることを提案しており、それによって契約違反時のセマンティクスをコア言語とビルドモードによる指定からユーザーによるカスタマイズによる指定に、設計を変化させようとしています。
この提案はP2811の方向性を支持し、ユーザー定義の違反ハンドラの振る舞いを確かなものにするために、違反ハンドラが呼ばれる場合と違反ハンドラが呼ばれた後の振る舞いに関するセマンティクスを規定しようとするものです。
この提案は、契約違反と違反ハンドラの振る舞いについて、次のようなことを提案しています
- P2811R1で提案されている違反ハンドラを必要なら修正を加えて採用する
- 実装が提供するデフォルトの違反ハンドラは、実装定義のアクション(エラーメッセージ表示など)の後で
std::abort()
を呼び出す実装とする
- ユーザー定義の違反ハンドラはデフォルトの違反ハンドラを呼び出すことができる
- これによって、ユーザー定義違反ハンドラで何かした後で、エラーメッセージ表示などデフォルトの動作をデフォルトのハンドラに委任できる
- 契約条件のチェックおよび違反ハンドラの呼び出しのセマンティクスは次のように指定される
動作 |
指定されるセマンティクス |
契約条件はチェックされなかった |
well-defined; 実行継続 |
契約条件がチェックされ、true を返した |
well-defined; 実行継続 |
契約条件がチェックされ、false を返した |
well-defined; 違反ハンドラが呼び出され、それが正常にリターンした場合は実行継続 |
契約条件がチェックされ、未定義動作に遭遇した |
未定義動作 |
契約条件がチェックされ、例外が送出された |
未規定の動作 |
契約条件がチェックされ、std::longjmp() が呼ばれた |
well-defined; 呼ばれたstd::longjmp() に従って実行継続 |
違反ハンドラが異常終了した |
well-defined; プログラムは呼び出されたハンドラの指定に従って終了する |
違反ハンドラが正常にリターンした |
well-defined; 実行継続 |
違反ハンドラが例外を送出した |
未規定の動作 |
違反ハンドラがstd::longjmp() を呼んだ |
well-defined; 呼ばれたstd::longjmp() に従って実行継続 |
この提案による設計では、C++標準は契約のビルドモード(契約条件を評価するかしないか)を認識する必要が無くなります。実装は、違反ハンドラがリターンしないことや例外を投げない等の仮定を置いた最適化を実行することができます。
この提案はこれをC++26最終仕様とすることを意図しておらず、コンセンサスが得られている事項については動作と意味論を定義し、そうでない事項については未規定や未定義として別の提案によって詰めていくことを意図しています。
ただし、SG21では別の提案によって検討されている方向性を議論しておくことになったらしく、この提案の追求はストップされています。
契約プログラミングにおける、ユーザー定義可能な違反ハンドラのAPIの提案。
C++26の契約プログラミング導入に向けて、SG21ではP2811の方向性を採用し違反ハンドラをカスタマイズ可能とすることを決定したようです。
この提案は、P2811で提案されいてるAPIをベースに修正を加えるものです。
この提案の修正は次のような点です
- ヘッダは
<contract_violation>
std::contract_violation
(契約違反が起きた条件に関する情報を保持するクラス)はstd::exception
の派生型
std::contract_violation
はsemiregular
な型
std::contract_violation
はABI安定(インラインpimplイディオムによる)
std::contract_violation
はヒープを使用せず、大きな固定サイズバッファーを持つ
- そのバッファ先頭にプライベートメンバが配置され、残りはメッセージ(
.what()
で取得するもの)の保持に使用する
- デフォルトの違反ハンドラは、
contract_violation::what()
のメッセージをstderr
に出力し、std::contract_resolution::abort_program
列挙値を返す
namespace std {
enum class contract_resolution {
abort_program
};
enum class contract_kind : {
empty,
pre,
post,
assert
};
class contract_violation : public std::exception {
public:
contract_violation() noexcept;
contract_violation(const contract_violation &) noexcept;
contract_violation(contract_violation &&) noexcept;
~contract_violation();
contract_violation &operator=(const contract_violation &) noexcept;
contract_violation &operator=(contract_violation &&) noexcept;
const char *what() const noexcept override;
const char* source_code() const noexcept;
contract_kind kind() const noexcept;
const source_location& source_location() const noexcept;
private:
static constexpr size_t size = 512;
alignas(std::max_align_t) mutable char storage[size];
};
}
そして、違反ハンドラはcontract_resolution
列挙値を返すように変更されます
std::contract_resolution handle_contract_violation(const std::contract_violation &);
戻り値型が変更されているのは将来的な違反後継続モードなどをサポートすることを目したもので、将来的な後方互換のためです。現在はabort_program
列挙値しかないためプログラム中断のみがサポートされており、将来的に別のモードをサポートする場合は列挙値を追加したうえで、ユーザーが自身の違反ハンドラの戻り値を変更することで行います。これによって、後から別のモードを追加したときにもその時点で使用されている違反ハンドラの振る舞いに影響を与えないようにしています。
このAPIを使って、Eval_and_abortモード(つまりデフォルトの違反ハンドラ)は次のように実装でき
std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
std::cerr << v.what() << std::endl;
return std::contract_resolution::abort_program;
}
Eval_and_throwモード(P2698R0)は次のように実装できます
std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
throw v;
}
std::contract_violation
がstd::exception
の派生クラスになっていることから、このような単純な実装によってEval_and_throwモードを実装可能です。
その他の例。
std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
if (std::exception_ptr e = std::current_exception())
std::rethrow_exception(e);
else
;
}
std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
std::contract_kind kind = v.kind();
const char* code = v.source_code();
std::source_location location = v.source_location();
ErrorMessage msg = FormatErrorMessage(kind, code, location);
DisplayErrorMessageAndWait(msg);
return std::contract_resolution::abort_program;
}
contract_violation_detection_mode
が削除されたのは1つ目の例のようにstd::current_exception
で検出することができるため(かつunknown
とundefined_behavior
の使い分けが不明瞭だったため)です。
P2300で使用されるtag_invoke
にアダプトするために、非メンバ関数ではなくメンバ関数と専用タグ型を使用するようにする提案。
P2300ではCPOの実装のためにtag_invoke
と呼ばれるユーティリティを使用しています。tag_invoke
はtag_invoke
という名前の関数にCPO毎のタグ型(CPOそのものが使用される)と追加の引数を渡して、ADLによって非メンバ関数(Hidden friendがよく使用されている)のユーザー定義tag_invoke
を探して呼び出します。
従来のCPOではそこにアダプトするために同名の関数(非メンバ/メンバ)を定義する必要があり、その呼び出しはコンセプトでチェックされるものの完全に区別されるわけではなかったため、実質的に名前を占有していました。これによって、ユーザーは標準CPOで使用されている名前の使用を控えざるを得なくなったり、CPOの呼び出しに伴う探索範囲が広く候補関数が増大しやすかったりと言った問題がありました。
tag_invoke
を使用すると、あるCPOにアダプトするための関数名は全てtag_invoke
という名前の関数になり、tag_invoke
はADLオンリーかつタグによって関数を識別するようになるため、それらの課題が解決されます。
struct my_scheduler {
friend std::execution::sender auto tag_invoke(std::execution::schedule_t, auto&& self) {
...
}
};
とはいえ、tag_invoke
という名前の関数がどのCPOにアダプトしているのかが視認しづらいことや、CPOにアダプトするための関数定義が複雑になりがち、CPOの型名を露出しなければならないなどの問題があります。
そのため、P2300も含めた将来的なカスタマイゼーションポイントを備えたライブラリを見据えて、C++20のCPOやtag_invoke
が持つ問題を解決した関数カスタマイゼーションのための仕組みを言語機能で備えようとする動きがあります。
この提案は、(非メンバ関数ではなく)メンバ関数とCPO個別のタグ型を用いることによって、そのような言語機能を必要とせずにtag_invoke
の持つ問題を改善できる、とするものです。
この提案の利点は次の2点です
- ADLを用いない
- カスタマイゼーションポイントの定義がかなりシンプルになる
この提案前後のstd::execution::schdule
CPOの実装は簡単には次のようになります
struct schedule_t {
auto operator()(auto&& s) const {
return tag_invoke(auto(*this), s);
return s.schedule(auto(*this));
}
};
新しい定義によるstd::execution::schdule
CPOにアダプトするためには、次のようにメンバ関数でschdule
を実装します
struct my_scheduler {
std::execution::sender auto schedule(std::execution::schedule_t) {
...
}
};
非メンバ関数ではなくメンバ関数を使用するようにすることで、探索にADLを使用しなくなるため名前が占有される空間をクラススコープに限定することができ、その上でタグ型のチェックを行うことでCPOにアダプトしている関数を区別します。これによって、CPOを定義する側とそれを利用する側のコードが単純化されます。
その他の例
struct my_op_state {
friend void tag_invoke(std::execution::start_t, recv_op& self) noexcept;
void start(std::execution::start_t) noexcept;
};
struct my_sender {
template <typename _Self, receiver _Receiver>
requires sender_to<__copy_cvref_t<_Self, _Sender>, __receiver<_Receiver>>
friend auto tag_invoke(std::execution::connect_t, _Self&& __self, _Receiver __rcvr);
template <typename _Self, receiver _Receiver>
requires sender_to<__copy_cvref_t<_Self, _Sender>, __receiver<_Receiver>>
auto connect(this _Self&& __self, std::execution::connect_t, _Receiver __rcvr);
}
このconnect
関数を呼び出すには次のようにします
struct S {
template<typename Sender, typename Receiver>
auto operator()(Sender&& s, Receiver r) const {
return tag_invoke(std::execution::connect, std::forward<Snd>(s), r);
return std::forward<Snd>(s).connect(std::execution::connect, r);
}
};
ただし、クエリを行うCPOに関してはこのようにせず、tag_invoke
の代わりにtag_query
という統一的な名前使用するtag_invoke
のアプローチ(ただし、非メンバではなくメンバ関数のみを探索)を使用することを提案しています。
struct S {
friend in_place_stop_token tag_invoke(std::execution::get_stop_token_t, const S& __self) noexcept;
in_place_stop_token tag_query(std::execution::get_stop_token_t) const noexcept;
};
クエリの場合は、クエリ呼び出しそのものを転送する場合があるため、このようになっているとのことです(よくわからなかった)。
P2596(std::hive
の容量モデルを修正する提案)への反対を表明する提案。
P2596はstd::hive
の容量モデルが複雑で意図しない振る舞いをするとして、それを単純化することを提案するものです。P2596に関しては以前の記事を参照
この文書はP2596を添削する形でその間違いを指摘し、主張のまとめを行うものです。
提案による、現状維持する(容量モデルを変更しない)ことを指示するポイント
- ライブラリ/実装はキャッシュラインサイズを知らないため、ユーザーは参照局所性向上のために要素ブロックをキャッシュラインサイズの倍数にしたい場合がある
- ライブラリ/実装はユーザーがどのようにデータを消去/挿入をするかのパターンを知らず、
std::hive
のイテレートの効率とメモリ消費はブロック容量に影響される
- 固定量で挿入/削除を行うユーザーはそのサイズに一致するブロックサイズを設定するだろう
- 要素の削除があるため、最大ブロックサイズが大きいことは良いことでばない
- ブロックはその全ての要素が削除されるまでは有効であり続けるため、要素の削除が行われブロックの空室率が高まると要素間のギャップが大きくなる
- これは統計的にみて、ブロックサイズが大きいほど無駄なメモリが増えることになる
- 要素間のギャップは参照局所性を低下させ、メモリの浪費は組み込みや性能が求められる環境で問題となる可能性がある
- キャッシュ制限のため、最大ブロックサイズが大きいことは良いことでばない
- ブロックサイズがキャッシュラインサイズより大きくなっても参照局所性が向上することはなく、挿入/削除時のメモリ確保/解放が少なくなるだけ
- 過剰なメモリ確保を防ぐために最大ブロックサイズが必要
- アロケータによっては、その内部の特定のチャンクで割り当てを行うものがあり、
std::hive
の容量モデルをはそれを支援するもの
- 必ずしも全てのユーザーのニーズを予測できるわけではないため、合理的な範囲で柔軟な使い方を目指すべき
std::hive
は特定条件下でSIMD処理で使用できる
- ブロックの制限はユーザーが指定するかどうかに関係なく実装に存在する
- 最小値側では、最初のブロックのメタデータのサイズよりも大きな妥当な最小値を持つことが理にかなっている
- 最大値側では、ジャンプカウントのスキップフィールドのビット深度によって決定される。例えば16ビットのスキップフィールドではブロック内で最大65535要素のジャンプが可能なので、ブロックの最大サイズ制限は65536になる
- スキップフィールドのビット深度が大きいほどメモリを浪費し性能向上につながるとは限らない。そのため、ユーザーのブロックサイズ制限をサポートするためのコードはほとんど付随的なもの
- この容量モデルとそのAPIは、実際のユーザーから好評だった機能であり、個人的な美学を理由に削除を選択する人がいるのは奇妙なこと
一方で、変更を指示するポイントは次のような点です
- 他のコンテナに同様のものがない
deque
とvector
にはcapacity()
があるが、list
にはない。map
とset
にはキーがあるがdeque
、vector
、list
にはない。
std::hive
には容量制限があり、他のコンテナにはない
- 制限があるとコンストラクタが増加する
- 追加されたコンストラクタは全て委譲によって実装できるため、コードが肥大化することはない
- 実装負荷の増加
- 前述(現状維持ポイント9)したように、これによって実装負荷は増加しない。実際の作業は全て、コンテナの仕様と3つの中核的な設計面に費やされる。
- 2つの主要ベンダーがリファレンス実装をフォークすると表明しており、もう1つのベンダーも参考にする可能性があるため、最小限の追加負荷は既に完了している
この提案の著者はstd::hive
実装者かつ提案者の方です。
事前/事後条件をもつnoexcept
関数における、契約違反時の例外送出に関する設計上の問題点を指摘する文書。
現在の契約プログラミング議論では、契約違反時の振る舞いの一つとしてEval_or_throwモードが提案されたことでnoexcept
関数の事前/事後条件の評価に伴って例外が送出されうる場合のnoexcept
プロパティの扱いが問題となっています。
void fun() noexcept [[pre: false]];
constexpr bool mystery = noexcept(fun());
using nothrow_callback_t = void(*)() noexcept;
nothrow_callback_t p = fun;
void overload(void(*)());
void overload(void(*)() noexcept);
overload(&fun);
この問題解決のために、すでにいくつかの実装論や意味論に関する提案が提出されています。この提案はそれらの議論を踏まえつつ、noexcept
の考え方などを説明し、それらの提案にある問題点について報告するものです。
- 契約チェックから例外を投げられるようにするには、
noexcept
に関する全ての静的なプロパティについて明確な意味論を定義する必要がある
- そのために、
noexcept
とは何かを明確に説明する必要がある
- オーバーロードを制御するための表明と、事前条件を充足することによる失敗しない保証を混同することは有益でない場合がある
- あるいは、違反ハンドラからの例外送出をひとまず禁止しておき、
noexcept
関連の議論に時間をかける
- 契約違反時に停止することを回避するアプリケーションにおいて、契約違反を検知してから例外によってそれを報告することは間違っている
- 例外それ自体が早期終了の原因となる
noexcept
関数からの例外送出は無条件終了となり、例外は契約と関係の無い場所からでも投げられるため
直接的には主張されて位はいませんが、雰囲気的には契約違反時に例外を投げること(Eval_or_throwモード)に反対しているようです。
標準ライブラリの関数にnoexcept
を付加する基準であるLakos Ruleを維持すべき理由を解説する文書。
この文書は非常に長いですが、次のような構成になっています
- 契約について議論する前に用語を定義する
- ライブラリUBと言語UBを区別する
- 例外を投げないという例外指定を持つ関数は、狭い契約(Narrow Contracts)を持つことができない
- ある特定の狭い契約をを持つライブラリ関数を考え、それを複数のバージョンにわたって拡張していくことの価値をC++の側面から検討する
- 最初のバージョンで
noexcept
を追加していたらどう(ひどいことに)なっていたかを調べる
- 狭い契約と契約チェック(言語機能)がどのように相互作用するかを見る
- 契約違反ハンドラが例外をスローすることの必要性を、いくつか正当化する
- 特に、完全な回復ではないにせよ一時的な継続、およびネガティブテストの手段として、契約違反時に例外をスローすることを検討する
noexcept
指定の自由な使用が例外を使用して稀なエラーや予期しないエラーを伝達するソフトウェア設計に及ぼす悪影響について検討する
noexcept
演算子を広く利用することでコードサイズを削減することができるが、実行時のパフォーマンスが大幅に改善されることやそれが測定可能であることを示す理論や経験則は存在しない
- Lakos Ruleの再検討
- Lakos Ruleの例外とはどのようなものなのかについて、仮説を立ててそれが4つの基準を満たすかを調べる
- 最後に、Lakos Ruleの唯一の例外を紹介する
- 標準ライブラリの仕様、その具体的な実装、サードパーティライブラリ、エンドユーザーライブラリに対して、
noexcept
指定を有効に活用するための推奨事項とその正当性を示す
最終的にこの文書の主張するところは、「技術的にやむを得ない正当な理由がない限り、Lakos Ruleから外れることは常に非常に悪いアイデアである。特に、標準ライブラリの仕様内でそうすることは絶対に避けるべき」というものです。
なお、この文書の筆者の方はLakos Ruleを提唱した方です。
※この部分は@Reputelessさんに執筆していただきました
std::text_encoding
の提案(P1885R12)に含まれる text_encoding::name()
の仕様を変更する提案。
具体的には、text_encoding::name()
が名前を返すことができない場合、ヌルポインタではなく空の文字列を返すように仕様を変更することを提案しています。
P1885R12 の設計では、ICU や iconv のような広く導入されているライブラリと互換性を持たせることを目指しています。それらのライブラリでは、エンコード名をヌルポインタとして扱うことをサポートしていない場合があります。次のコードはその一例で、セグメンテーション違反を引き起こします。
iconv_open(nullptr, "utf-8");
同様に、次のようなシンプルな C++ コードでも、text_encoding::name()
がヌルポインタを返す場合、容易に未定義動作となります。
std::cout << te.name();
std::format("Name: {}", te.name());
""sv == te.name();
text_encoding::name()
がヌルポインタを返すべき強い理由が見あたらなかったため、戻り値の型が const char*
である source_location::file_name()
が常に null 終端文字列を返す、という既存事例にならい、text_encoding::name()
についても空の文字列を返すようにすることを提案しています。
現在非推奨とマークされている機能について、C++26で削除/復帰を検討する提案。
C++23までの間に非推奨とされたコア言語/ライブラリの機能は、必ずしも削除されずに残されており、規格書のAnnex Dセクションにまとめられています。
現在そのような機能は29個(コア言語9, ライブラリ20)あり、この提案はそれらの機能を取り巻く環境や非推奨化の背景を検討したうえで、非推奨のままにしておくのか、非推奨を取り消すのか、削除するのか、を決定しようとするものです。
ただし、この提案はそれらのまとめとインデックスのような文書で、個々の機能それぞれについては個別の提案で詳しく検討されます。C++23では1つの提案にまとめて同じことを行おうとしていましたが、複数のフィードバックが寄せられた結果処理がパンクし提案の改訂が間に合わず、結局C++23設計サイクル中にほとんど議論できなかったためのようです。
現在非推奨となっているコア言語機能の一覧
機能 |
導入時期 |
非推奨時期 |
Arithmetic conversion on enumerations |
C++98 |
C++20 |
Implicit capture of *this by reference |
C++11 |
C++20 |
Array comparisons |
C++98 |
C++20 |
Deprecated use of volatile |
C++98 |
C++20 |
Redeclare static constexpr members |
C++11 |
C++17 |
Non-local use of TU-local entities |
C++98 |
C++20 |
Implicit special members |
C++98 |
C++11 |
Some literal operator declarations |
C++11 |
C++23 |
template keyword before qualified names |
C++98 |
C++23 |
現在非推奨となっているライブラリ機能の一覧
C++20の一貫比較仕様に伴って非推奨とされた、列挙値から算術型への暗黙変換を削除する提案。
C++20の宇宙船演算子ではenum
値と浮動小数点数型や異なる列挙型間の比較を禁止していますが、従来の比較演算子ではそれは暗黙変換によって可能となっており、異なる列挙型の間では算術演算すら可能です。それらの挙動はバグであると思われるため、C++20で非推奨とされました。
ただ、列挙型から浮動小数点数型への暗黙変換は、比較以外の場所では元々禁止されていました。
この提案は、C++26にてそれらの非推奨化されている暗黙変換を削除しようとするものです。この提案が削除しようとしているのは次の2つのものです
- 列挙型から浮動小数点数型への暗黙変換
- 異なる列挙型の値から同じ整数型への暗黙変換
int main() {
enum E1 { e };
enum E2 { f };
bool b = e <= 3.7;
int k = f - e;
}
この仕様はC++98にて導入されたもののようで、削除する事は破壊的変更となります。しかし、このような変換はそのメリットよりも意図せず起こしてしまう場合のデメリットの方が大きいため、削除することを提案しています。また、C++26で削除するとするとC++20で非推奨とされてから6年経過しており、その間に主要な実装はこの変換に警告を出すようになっています。
なお、列挙型と整数型の間の演算(列挙型から整数型への暗黙変換)は非推奨とされていないためC++20以降も影響を受けておらず、異なる列挙型間の演算や比較については単項+
演算子を使用して片方を整数昇格させることで回避することができたりします。
int main() {
enum E1 { e };
enum E2 { f };
int k = f - e;
int x = +f - e;
}
C++20の一貫比較仕様に伴って非推奨とされた、配列間の比較を削除する提案。
従来の比較演算子では配列型と配列型の比較が可能で、それは配列の先頭ポインタの比較をおこなっていました。C++20の宇宙船演算子はそれを禁止しており、それに倣って従来の比較演算子では非推奨とされました。
すなわち、配列と配列の等価比較を行っていてもそれは要素ごとの比較ではなく配列の先頭アドレスの比較になっており、true
となるのは同じ配列同士を比較した時だけです。順序付比較(<
など)はより悪く、特定の条件下を除いてほとんどの場合結果は未規定です。
オブジェクトの同一性チェックはそのアドレスを明示的に取得して行うのが一般的かつ最良です。このような暗黙的な変換に頼ることは非常に稀であると思われ、その意味を知らないプログラマがそのコードを見てもその意図を見抜くことはできないでしょう。
したがって、この提案はC++20で非推奨とされた配列同士の比較をC++26で削除することを提案するものです。
より正確には、比較演算子のオペランドの型変換の際に、片方のオペランドが配列の場合は他方のオペランドがポインタの場合にのみ配列からポインタへの変換を適用する、というように修正します。
そのため、C++20及びこの提案採択後でも、配列とポインタの間の比較(配列オペランドのポインタへの減衰)は非推奨ではなく合法的な動作です。
int main() {
int arr1[5];
int arr2[5];
bool same = arr1 == arr2;
bool idem = arr1 == +arr2;
}
C++20で非推奨とされたvolatile
関連の機能を削除する提案。
C++20では、無意味だったり危険なvolatile
関連の使用法がコア言語・ライブラリ両方において非推奨とされ、その後C++23にて、このうち複合代入演算子の非推奨化は解除されました。
この提案は、C++20で非推奨化されC++23で残っているすべてのvolatile
の用法について、削除しようとするものです。
複合代入演算子を除いては、非推奨化に反対するフィードバックは寄せられていないようで、これらの用法を削除することで誤解を招くようなコードを書けないようにすることができます。
長く非推奨となっていた、std::strstream
を削除する提案。
std::strstream
は生配列をラップする文字列ストリームであり、std::stringstream
がstd::string
をラップする文字列ストリームであるのと比較すると、こちらはchar[N]
をラップするストリームです。
std::strstream
のメンバ関数.str()
はchar*
を返しますが、こうして返された領域をユーザーが解放すべきなのか気にしなくていいのか、どのように管理すべきかが不透明でした。コンストラクタでの構築時はユーザーがその領域を指定することもstd::strstream
に確保させることもでき、std::strstream
に確保させた場合はその領域がどのように確保されたのかはどこにも記載がありません。
正解は、構築時に領域を渡していない場合に.str()
で文字列を取得した場合はデストラクタ実行までの間に.freeze(false)
を呼び出すことでstd::strstream
のデストラクタがその領域を解放してくれます。しかし、この挙動は分かりづらく、実際あまり周知されていなかったようで、簡単に間違って使うことができてしまっていました。
#include <strstream>
int main() {
{
std::strstream s1;
s1 << "dynamic buffer";
s1 << std::ends;
std::cout << "Contents : " << s1.str() << '\n';
s1.freeze(false);
}
{
char buffer[20];
std::strstream s2{buffer, std::size(buffer)};
s2 << "static buffer";
s2 << std::ends;
std::cout << "Contents : " << s2.str() << '\n';
}
}
他にも、.str()
のnull終端のためにはユーザーがそれを(std::ends
を利用するなどして)ストリームに入力しなければならないなどやはり使いづらいところがあり、これらの問題からC++98で非推奨とされました。
代替としてはstd::stringstream
を用いることができるのですが、こちらはこちらで内部文字列をいつもコピーして返すなどの問題があり、std::strstream
のようにあらかじめ用意した静的な領域を渡すことで動的確保を避けたいような用途としては代替機能がなく、削除されずに残されていました。
C++20では、std::stringstream
に.view()
が追加されたり、.str()
がムーブして返すことができるようになるなど、文字列の取得に伴うコピー回避の手段が提供されたほか、std::spansstream
がstd::strstream
の完全かつ安全な代替機能として提供されました。
この提案は、std::strstream
を削除する準備が整ったとして、C++26でstd::strstream
と関連する機能を削除しようとするものです。
std::allocator
にある非推奨化された入れ子型定義を削除する提案。
std::allocator
はsize_type
やpointer
等の入れ子型を持っていましたが、これはstd::allocator_traits
によって自動で導出可能であったため、C++17で非推奨とされました。これらについてはC++20で削除されています。
その後、C++23にてis_always_equal
も非推奨とされました。この提案は、これはC++26で削除しようとするものです。
std::allocator::is_always_equal
は、アロケータがステートレスであるかを調べる入れ子型で、デフォルトではstd::true_type
が使用されます。std::allocator
から派生して独自のアロケータを実装しようとする時にそのアロケータがステートレスではない場合、is_always_equal
を上書きしない場合デフォルトのis_always_equal
(ステートレスであると表明)が使用されてしまい、静かなバグを埋め込むことになります。
このような誤用を防止し、またわざわざその必要性などを説明する必要をなくすために、この提案ではstd::allocator::is_always_equal
を削除しようとしています。
C++20で非推奨とされた、std::shared_ptr
のアトミックフリー関数を削除する提案。
std::shared_ptr
には、そのポインタ値そのもの(not参照先)にアトミックアクセスするためのフリー関数が用意されていました。しかし、これらはフリー関数であるため、アトミックアクセスしたい対象のstd::shared_ptr
オブジェクトはプログラマが区別する必要がありました。そのため、直接対象のstd::shared_ptr
オブジェクトにアクセスすれば非アトミックアクセスとなり、それが複数スレッド間で同時に起きればデータ競合として未定義動作となります。
std::shared_ptr<int> atomic_ptr{};
void thread_f() {
std::atomic_store(&atomic_ptr, std::make_shared<int>(20));
auto n = *atomic_ptr;
auto ptr = std::atomic_load(&atomic_ptr);
auto m = *ptr;
}
void f() {
atomic_ptr = std::make_shared<int>(20);
auto n = *atomic_ptr;
}
このように、std::shared_ptr
のアトミックアクセス用フリー関数は簡単に誤って使用することができ危険だったためC++20で非推奨とされ、代わりにstd::shared_ptr
(とstd::weak_ptr
)のstd::atomic
特殊化が追加されました。こちらを用いると、どこからアクセスした時でもstd::shared_ptr
のポインタ値にアトミックにアクセスすることができます。
安全かつ完全に代替できる機能がすでに追加されており、削除することで危険な利用をコンパイルエラーとして報告することができるようになります。また、コードの変更も対象のstd::shared_ptr
をstd::atomic<std::shared_ptr>
に書き換えるだけで済みます。この提案はこれらの理由からstd::shared_ptr
のアトミックアクセス用フリー関数を削除しようとする提案です。
ただし、以前にこれらの関数を使用していたコードはstd::shared_ptr
をstd::atomic<std::shared_ptr>
に書き換えた後でstd::atomic*
を引数に取るフリー関数を呼び出すようになります。これそのものに問題はないのですが、これは<atomic>
ヘッダで定義されているためヘッダ依存関係が変更されます。これを回避するために、削除対象の関数と同名のstd::atomic*
を引数に取るフリー関数を<memory>
ヘッダで宣言しておくことも提案されています。
C++20で非推奨とされたstd::string::reserve()
をC++26に向けて削除する提案。
std::string::reserve()
は元々、キャパシティを増大させるだけではなく減少させることもサポートしていました。これはstd::vector::reserve()
の挙動とは異なっており、引数の値によってはパフォーマンス低下を引き起こすなどの問題がありました。
このため、C++20にてstd::string::reserve()
はキャパシティを減少させないことが規定され、それに伴ってデフォルト引数(0
)を取っていたオーバーロードが非推奨とされました(このオーバーロードは減少しかしないため)。
この提案は、std::string::reserve()
のデフォルト引数を持つオーバーロードを削除する提案です。
元々C++20での非推奨時に、その後のLEWGのレビューまでの間に重大な懸念が明らかにならなければこのオーバーロードを削除することに合意されていました。削除を急ぐ理由は特にないようですが、この提案ではその以前の合意に従って削除することを推奨しています。
C++17で非推奨とされた<codecvt>
ヘッダをC++26で削除する提案。
<codecvt>
ヘッダではユニコードとの間で文字コードの変換を行う機能が提供されていましたが、不正なユニコード文字列を入力されるような攻撃を受けた際にそれをエラーとして安全にハンドルする方法がなく、細かい仕様も曖昧だったりで、その必要性に反して文字コード変換のための機能としては不適当なものでした。
そのため、<codecvt>
はヘッダごとC++17で非推奨とされ、SG16はこの議論を契機としてC++により適切なユニコードサポートをもたらすための作業を開始しました。そこでは<codecvt>
ヘッダの機能に代わる文字コード変換機能も目標に入っていますが、C++23時点ではまだそのようなものは利用可能ではありません。
SG16がこれを改善する計画や余裕を持たないこと、C++26出荷時点で非推奨期間の方が長くなることなどの理由から、この提案では、<codecvt>
をC++26でヘッダごと削除することを提案しています。ただし、その名前(codecvt_utf8
など)を規格書のゾンビ名セクションに追加しておくことで、標準ライブラリ実装がC++26以降もそれを提供し続けることを許可する(ゾンビ名セクションはこのためにあるようです)ようにしておくことを提案しています。
C++17で非推奨とされたwstring_convert
をC++26で削除する提案。
1つ前の<codecvt>
と同様の理由によって、wstring_convert
をC++26で削除しようとする提案です。
wstring_convert
は<codecvt>
にある機能を使用する窓口のようなもので、C++17でそれらと一緒に非推奨とされました。<codecvt>
を削除したとしてもユーザーが代替のものを提供して使い続けることは可能であり、また標準でそれを提供することができる可能性もあるとして、C++23サイクル中のSG16での議論では削除に慎重な意見が聴かれていたようですが、一方で削除に反対する意見はなかったようです。
ここでは削除を提案していますが、<codecvt>
とは異なりそれをゾンビ名セクションに入れとくことは提案されていないようです。
C++20で非推奨とされたロケールカテゴリファセットをC++26で削除する提案。
ロケールカテゴリファセットとはstd::codecvt
/std::codecvt_byname
の特殊化のことで、この提案の対象となっているのは次の4つのものです
codecvt<char16_t, char, mbstate_t>
codecvt<char32_t, char, mbstate_t>
codecvt_byname<char16_t, char, mbstate_t>
codecvt_byname<char32_t, char, mbstate_t>
削除の理由や経緯に関しては、前の<codecvt>
、wstring_convert
と同じです。
Annex Dセクションにある機能の規定について、標準の他の部分と記法を合わせる提案。
Annex Dには過去に非推奨とされまだ削除されていない機能が移動されています。C++20で事前条件や適格要件の書き方が変更(P0788R3)された際、議論時間の都合からAnnex Dの内容はその変更が適用されず、古い記法のまま記述されました。その後、変更が適用された機能が非推奨とされて移動されたことで新旧の記述が入り混じっています。
この提案はAnnex DセクションにもP0788R3を適用し、標準の書き方を完全に統一しようとするものです。
この提案はすでに、2023年6月の全体会議でC++26に適用されることが決まっています。
C++20で非推奨とされたpolymorphic_allocator::destroy
の非推奨化を解除する提案。
polymorphic_allocator::destroy()
は与えられた領域にあるオブジェクトのデストラクタ呼び出しを行う関数です。これは、std::allocator_traits::destroy()
が提供するデフォルト実装と全く同じであり、polymorphic_allocator
がアロケータとして使用されることを考えると冗長なものです。そのため、C++20で非推奨とされました。
しかし、polymorphic_allocator
は語彙型として設計されており、必ずしも従来のアロケータのようにコンテナでstd::allocator_traits
を介して使用されるだけのものではありません。そのような場合、std::allocator_traits
が提供している関数を単体で提供する必要があります。
この提案は、それらの理由とconstruct()
との対称性を確保するためにもpolymorphic_allocator::destroy()
を非推奨としないようにする提案です。
std::simd
に対して、利便性向上のために標準ライブラリにあるデータ並列型等のサポートを追加する提案。
std::simd
クラスは、SIMDレジスタとそれに対する演算・操作をラップするようなクラス型で、std::simd
のオブジェクトに対してC++コードとして記述した計算をそのまま(自動で)SIMD演算に落とし込むことを目的とするものです。
std::simd
は現在、C++26導入を目指して作業が進められています(P1928)。
この提案は、std::simd
オブジェクトの入出力の利便性を向上させるために、P1928のstd::simd
クラスに欠けている標準ライブラリのクラス型との相互変換を追加しようとするものです。対象となるものは次のものです
std::bitset
simd_mask
型にstd::bitset
を受け取るコンストラクタを追加
simd_mask
型にstd::bitset
への変換(変換演算子or明示的な変換関数)を追加
- 整数値のビット表現の利用
simd_mask
型に符号なし整数値を受け取るコンストラクタを追加
constexpr simd_mask(auto std::unsigned_integral bits) noexcept;
simd_mask
型に、マスクを整数値のビット表現として取得する.to_ullong()
を追加
std::initializer_list
std::simd
にstd::initializer_list
を受け取るコンストラクタを追加
contiguous_range
std::simd
にcontiguous_range
を受け取って初期化するコンストラクタを追加
constexpr simd(std::ranges::contiguous_range auto x);
std::array
、std::span
に対しては推論補助も追加
これによって、std::simd
及びstd::simd_mask
はそれと意味的に同一視できるものから変換する形で構築したり、逆に変換することで値をストアすることができるようになります。
↓
プログラマが明示的に関数の戻り値に関するライフタイム注釈を行えるようにする提案。
ここで提案されているのは、Rustのexplicit lifetimeと呼ばれる機能に近いものです
fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
x
}
このような機能は静的解析のような外部のツールによっても達成できるかもしれませんが、それはユーザーがかなりの手間をかけて導入し使用した場合にのみ機能するものでしかありません。このような機能を限定的であったとしても言語機能として持つことで、ライフタイムにまつわる問題を警告ではなくコンパイル時のエラーとして検出できるようになり、より効果的に言語の安全性を高めることができます。
この提案では、コンパイル時に参照に対して4つの生存期間に関するプロパティを付加します。
- (生存期間が)グローバル
- ローカル
- 一時的(一時オブジェクト)
- その他(不明)
参照は初期化が必須であるため、これらのプロパティは初期化時に確定する性質です。
const int GLOBAL = 42;
void f(int* ip, int& ir) {
int local = 42;
int& r1 = *ip;
int& r2 = ir;
int& r3 = GLOBAL;
int& r4 = local;
}
そして、参照がコピーされるときはこのプロパティも同時にコピーされます。
const int GLOBAL = 42;
void f(int* ip, int& ir) {
int local = 42;
int& r1 = *ip;
int& r2 = ir;
int& r3 = GLOBAL;
int& r4 = local;
int& r5 = r1;
int& r6 = r2;
int& r7 = r3;
int& r8 = r4;
}
このプロパティだけでも、ローカル変数の参照をreturn
する関数をエラーにすることができます
const int GLOBAL = 42;
int& f(int* ip, int& ir) {
int local = 42;
int& r1 = *ip;
int& r2 = ir;
int& r3 = GLOBAL;
int& r4 = local;
int& r5 = r1;
int& r6 = r2;
int& r7 = r3;
int& r8 = r4;
return r8;
}
たとえば次のようにして、ある参照の生存期間を別の参照の生存期間に関連付けることができるようにします
const int GLOBAL = 42;
[[dependson(left, right)]]
const int& f1(const int& left, const int& right) {
if (randomBool()) {
return left;
} else {
return right;
}
}
この時、先程のプロパティはtemporary < local < global
の順で生存期間が短いとされます。この順序によって、この関数の戻り値の参照の生存期間は引数left/right
の生存期間のより短いものに制限されます。
これによってさらに次のチェックが可能となります
- 一時オブジェクトへの参照を返すとエラー
- 一時的な生存期間を持つ参照(一時オブジェクトへの参照)を初期化した後、別の行(別の完全式)で使用するとエラー
int& f2() {
int local = 42;
const int& r1 = f1(local, local);
const int& r2 = f1(GLOBAL, GLOBAL);
const int& r3 = f1(42, 42);
const int& r4 = f1(local, GLOBAL);
const int& r5 = f1(local, 42);
const int& r6 = f1(GLOBAL, 42);
if (randomBool()) {
return r1;
}
if (randomBool()) {
return r2;
}
if (randomBool()) {
return r3;
}
if (randomBool()) {
return r4;
}
if (randomBool()) {
return r5;
}
if (randomBool()) {
return r6;
}
int x1 = r3 + 43;
int x2 = r5 + 44;
int x3 = r6 + 45;
return f1(f1(GLOBAL, 4), f1(local, 2));
}
これによって、ローカル変数の間接的なダングリングだけでなく、一時オブジェクトの間接的なダングリングも修正されます。
さらに、クラス型が参照をpublic
メンバとして持っている場合も、同様の事を行うことができます。
struct S {
int& first;
const int& second;
};
int& f2() {
int local = 42;
S s1{GLOBAL, local};
S s2{local, f1(GLOBAL, 24)};
const int& r1 = s1.first;
const int& r2 = s1.second;
const int& r3 = s2.first;
const int& r4 = s2.second;
if (randomBool()) {
return r1;
}
if (randomBool()) {
return r2;
}
if (randomBool()) {
return r3;
}
if (randomBool()) {
return r4;
}
int x = r4 + 43;
return 42;
}
S f3() {
int local = 42;
S s1{GLOBAL, local};
S s2{local, f1(GLOBAL, 24)};
if (randomBool()) {
return s1;
}
return s2;
}
また、言語組み込み機能であればpublic
ではなくても同様の事を検出できます
auto lambda() {
int local = 42;
const int& ref_temporary = f1(GLOBAL, 24);
return [&local, &ref_temporary]() -> const int& {
if(randomBool()) {
return local;
}
return ref_temporary;
};
}
auto coroutine() {
int local = 42;
const int& ref_temporary = f1(GLOBAL, 24);
return [&local, &ref_temporary]() -> generator<const int&> {
if(randomBool()) {
co_return local;
}
co_return ref_temporary;
};
}
クラス型が非public
な形で参照を内部に含む場合、それを取得しようとするメンバ関数に対してそのオブジェクトそのものに依存するライフタイムを注釈する追加の構文によって同様の検出を行います
namespace std {
template <class charT,
class traits = char_traits<charT>,
class Allocator = allocator<charT> >
class basic_string {
...
constexpr std::string::operator [[dependson(this)]] std::basic_string_view<CharT, Traits>() const noexcept;
}
}
int main() {
std::string_view sv = "hello world"s;
sv.size();
}
これはstd::span
やstd::function_ref
などの他の参照セマンティクスを持つ型でも使用できます。ただし、reference_wrapper
のように再束縛できる(あとから参照先を切り替えられる)ものについてはこれを適用できません。他にもポインタ型やstd::unique_ptr
当のポインタセマンティクスを持つ型が該当します。
ただし、そのような型でもconst
であれば初期化時に非null
で初期化されていると推定でき、また後から参照先が変化しないため同様のことが行えます。
[[dependson(left, right)]]
const std::reference_wrapper<const int> f(const int& left, const int& right) {
if(randomBool()) {
return std::cref(left);
} else {
return std::cref(right);
}
}
最後に、これらのチェックはnew
式による初期化時にも適用できます
struct S { int mi; const std::pair<int,int>& mp; };
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} };
もし参照がpublic
ではない場合は、そのコンストラクタでその依存関係を指定する追加の構文が必要になります
class S {
int mi;
const std::pair<int,int>& mp;
public:
[[parameter_dependency(dependent{"this"}, providers{"mp"})]]
S(int mi, const std::pair<int,int>& mp);
};
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} };
この提案では構文の説明のために属性構文が使用されていますが、属性として採用すべきか別の言語機能として採用すべきかは提案しておらず、むしろその能力を獲得することを目的としています。
この提案はSG23でレビューされ、引き続き議論されないことが決定しています。
複数の乱数の効率的な生成のためのAPIとして、提案中のベクターAPIとstd::simd
によるAPIを比較する提案。
P1068では大量の乱数を効率的に生成するための高レベルなベクターAPIを提案しており、そこでは範囲に対して乱数を充填するAPIによって実装が効率的な乱数生成方法を選択できるようにしています。その実装には例えばSIMD演算によるものが想定されています。
一方で、P1928で議論されているstd::simd
はSIMDレジスタと命令のラッパクラスであり、std::simd
そのものの操作あるいはAPIによって直接的にそのような大量乱数生成の効率実装を行うことができます。そのため、std::simd
導入を見据えた場合にP1068の高レベルAPIによる複数乱数生成は必要なのか?あるいはstd::simd
に乱数生成のためにどのようなAPIを持たせるべきか?と言ったことが疑問として浮かんできます。
この提案はそのような疑問に答えるために、両者のAPIによるコードを比較することで利点欠点を洗い出し、std::simd
の乱数生成APIのいくつかの可能性を示すものです。
この提案では、"European options pricing"というベンチマーク中から複数の乱数を生成しているコードを抽出し、それをP1068とstd::simd
を用いて実装してみるとどうなるかを示すことで比較を行なっています。
std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);
double v0 = 0, v1 = 0;
for (std::size_t p = 0; p < npath; ++p) {
double rand = distribution(engine);
double res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
v0 += res;
v1 += res * res;
}
result = v0 / npath;
confidence = v1 / npath;
P1068R7の高レベルAPI
まずP1068R7ではstd::ranges::generate_random
という関数(正確にはCPO)を用いて、範囲に対して指定されたエンジンと分布によって生成した乱数を充填します。それによって、先ほどのコードは次のようになります
std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);
std::array<double, npath> rand;
std::ranges::generate_random(rand, engine, distribution);
double v0 = 0, v1 = 0;
for(std::size_t p = 0; p < npath; ++p) {
double res = std::max(0., S * exp(sqrt(T) * vol * rand[p] + T * mu) - X);
v0 += res;
v1 += res * res;
}
result = v0 / npath;
confidence = v1 / npath;
std::ranges::generate_random
によってループ前に使用予定の乱数を全て生成しているため、ループの中は乱数利用コードのみになります。前述のように、これはSIMD命令等を用いて効率的に乱数生成を行う実装がなされるはずなので、ループで生成しながら利用するコードよりも効率的になることが期待されます。
ただし、この場合生成した乱数を受けるためにそこそこ巨大な配列を最初に用意しなければなりません。これはキャッシュヒット率を低下させることが予想されるため、最適なコードとは言えません。そこで効率化のために、バッファリングを行います
std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);
std::array<double, nbuffer> rand;
double v0 = 0, v1 = 0;
for(std::size_t p = 0; p < npath; p += nbuffer) {
std::size_t local_size = (p + nbuffer <= npath) ? nbuffer : (npath - p);
std::ranges::generate_random(std::span(rand.begin(), local_size), engine, distribution);
for(std::size_t b = 0; b < local_size; ++b) {
double res = std::max(0., S * exp(sqrt(T) * vol * rand[p] + T * mu) - X);
v0 += res;
v1 += res * res;
}
}
result = v0 / npath;
confidence = v1 / npath;
nbuffer
分づつ処理を分けることで、1度に生成する乱数とそれを保存するためのストレージサイズをnbuffer
に減らします。その代わり乱数生成を一括で行う単位も減ってしまいますが、キャッシュミスによるペナルティよりも一度の乱数生成オーバーヘッドの方が小さい間はこちらの方が効率的になります。
std::simd
のAPI
次に、std::simd
でも同じことを考えます。ただし、std::simd
はまだそのようなAPIを持たないため、その可能な設計として幾つかのパターンが考えられます。この設計で重要なことは、ユーザーが要求するstd::simd
型に関する情報をどのレベルで取得するか?ということです
1. エンジンのテンプレートパラメータ
1つ目の例は、エンジンと分布生成器の両方がstd::simd
型に関する情報を持って構築されるものです。
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, 16>> E(777);
std::normal_distribution<std::fixed_size_simd<double, 16>> D(0., 1.);
auto rand = D(E);
エンジンと分布生成器の数値型が異なっており、既存の実装はこのような場合normal_distribution
のdouble
値1つに対してエンジンのuint_fast32_t
値を2つ消費します(全体では、乱数16個に対してエンジン出力32個を消費する)。そのため、実装によっては次のように定義した方が効率的である場合があります
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, 32>> E(777);
std::normal_distribution<std::fixed_size_simd<double, 16>> D(0., 1.);
auto rand = D(E);
より洗練された分布生成器では結果値ごとに異なった個数の入力エンジン値を消費する分布の実装が一般的となるため、分布生成器とSIMD幅が与えられた時にそれに最適なエンジンのSIMD幅の適切なサイズの普遍的な解答はありません。
そのような分布生成器において、エンジンが生成した固定幅の乱数配列の一部しか消費しない場合、使用しなかった残りの部分について選択肢が生まれます
- 残りの部分は分布オブジェクトが内部に保存する
- 保存する領域のオーバーヘッドやどう使用されるかが問題となる
- 残りの部分が次の生成に使用されるとすると、分布のランダム性に残った値の利用という要素が追加される
- 残りの部分は廃棄する
このことを念頭に置いて、最初のコードをこのAPIで書き直したのが次のコードです
constexpr std::size_t size = 16;
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, size>> E(777);
std::normal_distribution<std::fixed_size_simd<double, size>> D(0., 1.);
double v0 = 0, v1 = 0;
std::size_t p = 0;
for(; p + size <= npath; p += size) {
auto rand = D(E);
auto res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
v0 += std::reduce(res);
v1 += std::reduce(res * res);
}
if (p != npath) {
auto rand_tail = D(E);
auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
for(std::size_t i = 0; p + i < npath; ++i) {
v0 += res[i];
v1 += res[i] * res[i];
}
}
result = v0 / npath;
confidence = v1 / npath;
このコードでは、npath
がsize
(16)の倍数ではない場合に必要以上の数の乱数を生成してしまうため最初のコードど完全に同じことをしているわけではありませんが近いコードではあります。そして、端数の処理の際に生成した乱数(rand_tail
)の一部を捨ててしまうことになりますが、その捨て方が問題となる可能性があります(エンジンや分布生成器を再利用する場合など)。
2. エンジンのテンプレートパラメータ + 再バインドコンストラクタ
それらの問題を念頭に置いて次のAPI設計案では、異なるSIMD幅やスカラ型を持つエンジンの再バインド構築によって未使用部分の問題を回避します。再バインドによって以前のエンジンの内部状態を引き継ぐことで、余分な乱数の生成を防止し、捨てられる値や目に見えない状態を回避します。
constexpr std::size_t size = 16;
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, size>> E(777);
std::normal_distribution<std::fixed_size_simd<double, size>> D(0., 1.);
double v0 = 0, v1 = 0;
std::size_t p = 0;
for (; p + size <= npath; p += size) {
auto rand = D(E);
auto res = std::max(0., S*exp(sqrt(T) * vol * rand + T * mu)-X);
v0 += std::reduce(res);
v1 += std::reduce(res * res);
}
if (p != npath) {
std::mt19937 E_tail(E);
std::normal_distribution D_tail(0., 1.);
for (; p < npath; ++p) {
auto rand_tail = D_tail(E_tail);
auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
v0 += res;
v1 += res * res;
}
E = E_tail;
}
result = v0 / npath;
confidence = v1 / npath;
乱数エンジンの再バインド機構によって、乱数エンジンの生成値の型を変更しつつエンジンの内部状態を引き継ぐことができるようにしています。
この場合、ユーザーレベルの柔軟性が得られる一方で余計なコピーが追加されており、状態の大きなエンジンではそのオーバーヘッドが問題となる可能性があります。
3. アルゴリズムlikeな関数のテンプレートパラメータ
更なる代替案として、std::simd
型の情報を受け取る点をエンジン/分布生成器からその使用地点に移すAPIが考えられます。これによって、エンジンはその使用モードを意識することなく、内部のレイアウトは実装によってスカラ/ベクターどちらの生成も可能とするバランスの取れた形に選択されます。
std::mt19937 E(777);
std::normal_distribution D(0., 1.);
auto rand = std::generate_random_simd<std::fixed_size_simd<double, 16>>(E, D);
エンジンレイアウトの厳密な(環境のSIMD幅に合わせた)最適化が制限される代わりに、標準ライブラリに実装の自由が与えられ、エンジンからベース乱数を消費する方法はプラットフォームによって異なる可能性があリます。
constexpr std::size_t size = 16;
std::mt19937 E(777);
std::normal_distribution D(0., 1.);
double v0 = 0, v1 = 0;
std::size_t p = 0;
for (; p+size <= npath; p += size) {
auto rand = std::get_random_simd<std::fixed_size_simd<double, size>>(E, D);
auto res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
v0 += std::reduce(res);
v1 += std::reduce(res * res);
}
for (; p < npath; ++p) {
auto rand_tail = D(E);
auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
v0 += res;
v1 += res * res;
}
result = v0 / npath;
confidence = v1 / npath;
この例は最初のコードと完全に一致しており、余分な乱数を生成してエンジン状態が不明になったり、それを回避するためにエンジン状態をコピーするなどの問題を回避しています。
これらの比較と観察から得られる結論は次のようなものです
- 高レベルのAPIは通常のC++開発者が作成する乱数利用アプリケーションの大部分をサポートすることを目的としている
- APIを実装するベンダがHWアクセラレータを有効にする実装をとれば、そのようなアプリケーションのパフォーマンスを向上できる
- SIMDを利用した実装はその一種であり、同じAPIを使用して利用可能となる
- 低レベルの(
std::simd
による)APIはC++の慣習に則った上でよりHWに近いコーディングを必要とする上級開発者を対象としている
- ただし、
std::simd
によるAPIではエンジンと分布生成器の概念に基づいた乱数生成アルゴリズムのSIMD実装の詳細を開発者に理解させることは避けるべき
従って、P1068の高レベルのAPIとstd::simd
によるAPIはターゲットが異なるため排他的なものではなく、両方を標準ライブラリに持つことは合理的であると考えられます。
範囲for
文に新しいループカスタマイゼーションポイントを追加する提案。
<ranges>
のRangeアダプタなどに見られるようにイテレータの定義は複雑で、何か処理を範囲for
文でループさせるように書き直そうとするとその対象の状態をoperator*
とoperator++
に分割してエンコードしてやる必要があり、これによって処理のイテレータ(range
)への移行は非常に難しくなっています。
C++20のコルーチンとC++23std::generator
の利用によってそれは劇的に簡単になります。例えばstd::generator
を使用するとviews::filter
、views::join
、views::concat
は次のように簡単に定義できます
template <typename Rng, typename Predicate>
auto filter(Rng&& rng, Predicate predicate) -> std::generator<…> {
for (auto&& elem : rng) {
if (predicate(elem)) {
co_yield std::forward<decltype(elem)>(elem);
}
}
}
template <typename Rng>
auto join(Rng&& rng_of_rng) -> std::generator<…> {
for (auto&& rng : rng_of_rng) {
co_yield std::ranges::elements_of(std::forward<decltype(rng)>(rng));
}
}
template <typename ... Rng>
auto concat(Rng&& ... rng ) -> std::generator<…> {
((co_yield std::ranges::elements_of(std::forward<decltype(rng)>(rng))), ...);
}
ただしstd::generator
もいいとこづくめではなく、いくつかデメリットがあります
- パフォーマンスでイテレータに劣る
- コルーチンステート保存のためのメモリ確保や、関数の中断のサポート、例外機構などによるオーバーヘッドが回避できない
- ネストした文脈で
co_yield
を使用できない
- スタックレスコルーチンはその内部で呼び出した関数内などのスタックフレームの異なる場所で中断できない
2つ目の問題は、ツリー構造のようなネストした構造に対してstd::generator
を直接適用できない場合がある問題で、次のようなものです
struct tree {
using leaf = int;
std::variant<leaf, std::vector<tree>> impl;
};
std::generator<int> tree_data(const tree& t) {
std::visit(
overloaded(
[&](int data) {
co_yield data;
},
[&](const std::vector<tree>& children) {
for (auto& child : children)
co_yield std::ranges::elements_of(tree_data(child));
}),
t.impl);
}
これは、コルーチンがその内部で呼び出した関数で中断(co_yield
やco_await
)できないというスタックレスコルーチンの特性によるものです。ネストした部分を別のコルーチンにすることで回避はできます
std::generator<int> tree_data(const tree& t) {
auto sub =
std::visit(
overloaded(
[&](int data) -> std::generator<int> {
co_yield data;
},
[&](const std::vector<tree>& children) -> std::generator<int> {
for (auto& child : children) {
co_yield std::ranges::elements_of(tree_data(child));
}
}),
t.impl);
co_yield std::ranges::elements_of(sub);
}
この提案は、コルーチンによるgenerator
の利点を享受しつつこれらのデメリットを回避するような、generator
にちかい記述によって処理をループに落とし込むための仕組みを提案し、それを範囲for
文のカスタマイゼーションポイントとして追加しようとするものです。
この提案によるgenerator
はgenerator ranges(ジェネレータ範囲)と呼ばれており、それは、範囲for
文の処理本体をラムダ式として受け取ってそれを適宜呼び出しながら処理を実行する関数オブジェクト的な何かです。ジェネレータ範囲による範囲for
文は、begin()/end()
を使用したイテレータループを行う代わりに、そのループ本文をラムダ式としてジェネレータ範囲のオブジェクトの関数呼び出し演算子として渡して、ジェネレータ範囲のオブジェクトの関数呼び出し演算子では、そうして受け取ったループ本文に適宜各要素を渡して呼び出すことでループを実行します。
struct generator123 {
auto operator()(auto&& sink) const {
std::control_flow flow;
flow = sink(1);
if (!flow) return flow;
flow = sink(2);
if (!flow) return flow;
return sink(3);
}
};
for (int x : generator123{}) {
std::print("{}\n", x);
if (x == 2) {
break;
}
}
std::control_flow
はジェネレータ範囲による範囲for
文の本体内のbreak/continue
の結果を表現するクラス型です。これは基本的に強く型付けされたbool
型で、次のように定義されます
namespace std {
struct continue_t {
constexpr operator std::true_type() const noexcept {
return {};
}
constexpr std::false_type operator!() const noexcept {
return {};
}
friend std::strong_ordering operator<=>(continue_t, continue_t) noexcept = default;
};
inline constexpr continue_t continue_;
struct [[nodiscard("need to forward break")]] break_t {
constexpr operator std::false_type() const noexcept {
return {};
}
constexpr std::true_type operator!() const noexcept {
return {};
}
friend std::strong_ordering operator<=>(break_t, break_t) noexcept = default;
};
inline constexpr break_t break_;
class [[nodiscard("need to forward control flow")]] control_flow {
public:
constexpr control_flow(continue_t) noexcept;
constexpr control_flow() noexcept : control_flow(continue_) {}
constexpr control_flow(break_t) noexcept;
constexpr explicit operator bool() const noexcept;
constexpr friend bool operator==(control_flow, control_flow) noexcept;
constexpr friend std::strong_ordering operator<=>(control_flow, control_flow) noexcept;
};
}
std::continue_
とstd::break_
はstd::true_type/std::false_type
への定数変換演算子を持つ個別のタグ型として定義されており、これによって常に継続や常に中断といった一般的なケースを型システムにエンコードして最適化を保証することができます。
ジェネレータ範囲による範囲for
文は通常の範囲for
と同様に言語組み込みマクロのようなもので、次のように展開されます
for (T binding : object)
{
body
}
{
auto __body = [&](T&& __element) -> see-below {
T binding = std::forward<T>(__element);
body
return std::continue_;
};
auto __flow = object(__body);
see-below
}
関数本体であるbody
で特に終了(return
やbreak
)をしなければ、デフォルトでstd::continue_
が返されることで処理を継続させます。body
に何か制御文を書くとそれはstd::control_flow
の値を返すように変換されます
continue;
break;
return;
return implementation-defined
std::break_;
と同じ効果となるが、コンパイラはループの後ろにreturn
を配置する
return expr;
expr
を実行してその結果をどこかに保存してから、return implementation-defined
std::break_;
と同じ効果となるが、コンパイラはループの後ろにreturn
を配置し戻り値を返す
goto
return implementation-defined
std::break_;
と同じ効果となるが、コンパイラはループの後ろにgoto
を配置する
throw
co_await/co_yield/co_return
これによって、ジェネレータ範囲による範囲for
文は利用者から見ると通常のfor
文とほとんど透過的に使用することができます。
for (int x : generator123{}) {
if (x == 0)
continue;
if (x == 2)
break;
std::printf("%d\n", x);
}
{
auto __body = [&](int&& __element) -> std::control_flow {
int x = __element;
if (x == 0)
return std::continue_;
if (x == 2)
return std::break_;
std::printf("%d\n", x);
return std::continue_;
};
auto __flow = generator123{}(__body);
(void)__flow;
}
この新しい範囲for
文では、co_yield
の代わりに関数呼び出し演算子を通してループ本体に要素(値)を提供することで擬似的な関数の中断と値の生成を実現し、ループの処理が終わると自動的に再開されます。それはコルーチンのような複雑な仕組みを全く用いておらず、そのために導入されていたオーバーヘッドも全くありません。そのため、パフォーマンスではイテレータと同等かそれ以上のものを達成でき、コルーチンあるいはstd::generator
の制約によるデメリットも回避されます。
views::filter
の例
template <typename Rng, typename Predicate>
auto filter(Rng&& rng, Predicate predicate) {
return [=](auto sink) {
for (auto&& elem : rng) {
if (predicate(elem)) {
auto result = sink(std::forward<decltype(elem)>(elem));
if (result == tc::break_) {
return result;
}
}
}
return tc::continue_;
};
}
ネストした構造に対しても、ほぼそのまま適用できます
auto tree_data(const tree& t) {
return [&](auto sink) {
auto flow =
std::visit(
overloaded(
[&](int data) {
return sink(data);
},
[&](const std::vector<tree>& children) {
for (auto& child : children) {
auto flow = tree_data(child)(sink);
if (flow == tc::break_) {
return flow;
}
}
return tc::continue_;
}),
t.impl);
return flow;
}
}
実行コンテキスト間でやりとりするための標準的な方法を提供するための設計や問題点について探るための文書。
P2300では実行コンテキストをscheduler
コンセプトによって抽象化しており、そこではscheduler
を提供する以外のことを要求していません。そのため、異なる実行コンテキストから制御を移すためのインターフェースが欠けており、それを行うためのtransfer()
アルゴリズムのようなものの実装が難しくなっています。
例えば、次のようなネットワークから音源をダウンロードしてきて、それをデコードし再生(再生デバイスへ転送)するような処理を考えます
void receive() {
SnapClient srv{srvAddr, srvPort};
while (true) {
std::span<uint8_t> buf = srv.receiveWireChunk();
opusQueue.wait_push(buf);
}
}
void decode() {
while (true) {
std::span<uint8_t> inBuf;
opusQueue.wait_pop(inBuf);
int samples = opus_decode(decoder,
inBuf.data(), inBuf.size(),
decodeBuf.data(), maxFrameSamples,
0);
std::span outBuf(decodeBuf.data(), samples);
pcmQueue.wait_push(outBuf);
}
}
void play() {
while (true) {
std::span<uint8_t> inBuf;
pcmQueue.wait_pop(inBuf);
uint32_t const *start = inBuf.data();
size_t offset = 0;
while (offset < size) {
size_t bytesDone;
i2s_channel_write(tx,
start + offset,
size - offset,
&bytesDone,
noTimeout);
offset += bytesDone;
}
}
}
この3つの関数はそれぞれ別のスレッドで実行されます。この時、各スレッド(実行コンテキスト)間のやりとりにはキューが使用されています。
これをP2300のsender/receiver
によって実装すると例えば次のようになります
void network_speaker() {
exec::scheduler auto sched0 = cppos::ContextCore0::LoopScheduler();
exec::scheduler auto sched1 = cppos::ContextCore1::LoopScheduler();
AudioServer srv(sched0, srvAddr, audioPort);
srv.readPacket()
| bufferedTransfer<netBufferSize>(sched1)
| then(soundDecode)
| bufferedTransfer<pcmBufferSize>(sched0)
| then(sendI2sChunk())
| runForever();
}
このbufferedTransfer
は内部的にキューを使用しているものとすると、この場合も実行コンテキスト間のやりとりにはキューが使用されています。
また、コルーチンを使用しても実装できます
CoHandle receive() {
while (true) {
std::span<uint8_t> buf = co_await srv.coReceiveWireChunk();
co_await opusQueuePush.wait_push(buf);
}
}
CoHandle decode() {
while (true) {
std::span<uint8_t> inBuf;
co_await opusQueuePull.wait_pop(inBuf);
int samples = opus_decode(decoder,
inBuf.data(), inBuf.size(),
decodeBuf.data(), maxFrameSamples,
0);
std::span outBuf(decodeBuf.data(), samples);
co_await pcmQueuePush.wait_push(outBuf);
}
}
CoHandle play() {
while (true) {
std::span<uint8_t> inBuf;
co_await pcmQueuePull.wait_pop(inBuf);
uint32_t const *start = inBuf.data();
size_t offset = 0;
while (offset < size) {
size_t bytesDone;
co_await co_i2s_channel_write(tx,
start + offset,
size - offset,
&bytesDone,
noTimeout);
offset += bytesDone;
}
}
}
void network_speaker() {
while (true) {
co_await receive();
co_await decode();
co_await pray();
}
}
他にもファイバーによる実装も考えることができ、それはスレッド版とほぼ同じようなコードになります。
現在のC++はこれらの実装方法のうち最初のスレッド版だけをサポートしています。ただし、スレッド間で共有可能な並行キューはありません。とはいえ、コルーチンはすでに利用可能であり、P2300やファイバーも程なく利用可能になる予定なので、残りのコードも近いうちにサポートされます。すると、そこに欠けているのは実行コンテキスト間同期に使用している並行キューのようなものです。
この文書は、このキューのような実行コンテキスト間で通信(同期)を取るための標準的なメカニズムに必要な要件や設計について検討し、それに伴って浮かんだいくつかの疑問についてSG1に問うものです。まだ何かを提案しているわけではありません。
実行コンテキストに特化した同期メカニズムを提供するのは簡単ですが、全ての実行コンテキストでうまく動作するものを提供するのは困難です。例えば、上記例のキューはスレッド版とコルーチン版で異なるメンバ関数を提供しなければならないほか、動機を取る方法についても実行コンテキストによって最適なものが変わるでしょう。
それぞれの実装に特化したものを用意しても、複数の実行コンテキストを組み合わせて使用する場合にうまくいかなくなります。例えば、コルーチンによる実行においてその処理の一部(デコード)を別のスレッドで行おうとすると、コルーチンとスレッドという異なる実行方法にまたがって動作する同期方法(例ではキュー)が必要になります。
P0073R2ではそのような同期のためのキューではなくevent
というクラス型を提案しています。
class event {
void signal();
}
void block(event until);
このような抽象化は有用であると思われますが、異なる実行コンテキスト間で動作しようとするとき、signal()
はイベントを受信する(送信先の)実行コンテキストを、block()
は現在の実行コンテキストを知っていなければならないようです。そのために、event
クラスはそのオブジェクトでブロック中の全てのタスクのリストを保持する必要があります。実行コンテキストが静的にわかっていれば問題はないですが、動的にしかわからない場合が想定されるようで、その場合には実行時に現在の実行コンテキストを知る手段が必要となります
並行キューについては、キュー本体とキューのインターフェースを分離することで実装を効率化できる可能性が見出されました。その場合、キューは実行コンテキストによってテンプレート化され、実行コンテキストはそのブロッキングと通知のためのAPI(上記event
クラスのインターフェースと同様のもの)を提供する必要があります。
すなわち、event
クラスのようなAPIの設計はそれそのものやキューに限らず、同様の目的の同期メカニズムに対して一般化することができるはずです。すると、それらのものは実行コンテキストを知っている必要がありますが、これは必ずしも静的に検出可能ではない場合があります。
また、そのようなメカニズムではsignal()
に相当するイベント通知処理がブロック中のタスクとその実行コンテキストを覚えておく必要があります。そのリストを保持するために追加のメモリ確保は許容されるのか、あるいは回避されるべきでしょうか。また、そのようなリストはおそらく型消去されることになります。すると型消去そのもののために動的確保が必要となる可能性があります。
この文書では、このようなまず浮かび上がった疑問をSG1に投げかけています。
- SG1はこのような汎用同期APIにより力を入れるべきか?
- P0073R2の
event
は使用可能な抽象であるか?
- あるスレッドで実行中の処理内から、それを包含する最上位の実行コンテキスト(スレッドに対するスレッドプールなど)を検出するような実装を要求できるか?
- 実行コンテキストの完全なチェーンは必要か?
- 標準ライブラリにあるブロックする可能性がある関数のリストは必要か?
- 実行エージェントのペアのためのカスタマイズを許可するか?
この文書はこれらの解答を受けて、さらなる検討や作業を進めるつもりのようです。
offsetof
をキーワード化し、offsetof
マクロの機能を言語機能とする提案。
この提案には、上の方のP2654R0の項も関連しています。
offsetof
はマクロなのでモジュールからエクスポートすることができず、std
モジュールからもエクスポートされません。そのため、使用する場合は引き続き#include <cstddef>
が必要となりますが、このヘッダはインポート可能であると指定されてはいないため、ヘッダユニットとしてインポートすることもできません。
この提案は、主にこの問題の解消のために、offsetof
をキーワードとしてその機能を言語機能に昇格することでヘッダのインクルードによらず使用可能にしようとするものです。
これによって、offsetof
の他の問題点の解決を図ることができます
- マクロ引数に
,
が含まれていると展開がバグる
offsetof
の第一引数に標準レイアウトクラス型以外を指定した場合、もしくは第二引数にデータメンバ以外を指定した場合のUB
- 結果をポインタ演算に使用するとUBとなる
この提案によって解消される問題の例
import std.compat;
import <cstddef>;
#include <cstddef>
template <typename A, typename B>
struct Test {
int data;
};
using TestInts = Test<int, int>;
static_assert(offsetof( TestInts, data) == 0);
static_assert(offsetof( Test<int, int> , data) == 0);
static_assert(offsetof((Test<int, int>), data) == 0);
class S1 {
int data;
public:
S2(int n) : data{n} {}
};
struct S2 {
int data;
void f();
};
static_assert(offsetof(S1, data) == 0);
static_assert(offsetof(S2, f) == 0);
struct T {
int i;
double j;
short k;
void *p;
};
int main() {
using namespace std;
T x = {};
size_t y = offsetof(T, k);
short *p = (short*)((byte*)&x + y);
*p =123;
printf("%d", x.k);
}
この提案では、キーワード化されたoffsetof
は演算子となり、上記のUBをエラーにしたり結果をポインタ演算に使用できるようにするなどの意味論が整備されます。
assert
をキーワード化し、assert
マクロの機能を言語機能とする提案。
この提案には、1つ前のP2883及び上の方のP2654R0の項も関連しています。
assert
はマクロなのでモジュールからエクスポートすることができず、std
モジュールからもエクスポートされません。そのため、使用する場合は引き続き#include <cassert>
が必要となりますが、このヘッダはインポート可能であると指定されてはいないため、ヘッダユニットとしてインポートすることもできません。
この提案は、主にこの問題の解消のために、assert
をキーワードとしてその機能を言語機能に昇格することでヘッダのインクルードによらず使用可能にしようとするものです。
1つ上のoffsetof
と同様にキーワード化されたassert
は演算子となり、それによって式に,
が含まれている場合に展開がバグる問題が解消されます。
この提案によって解消される問題の例
import std.compat;
import <cstddef>;
#include <cassert>
int main() {
assert( std::is_same_v<int, int> );
assert((std::is_same_v<int, int>));
assert( std::vector{1, 2, 3}.size() == 3 );
assert((std::vector{1, 2, 3}.size() == 3));
int x = 0;
int y = 0;
assert( [x, y]{ return test(x, y);}() );
assert(([x, y]{ return test(x, y);}()));
}
このassert
とよく似た機能は、関数中の不変条件の表現とチェックのための構文としてC++26予定の契約プログラミング機能においても議論されています。ただし、契約プログラミングにおいては契約の構文をどうするかまだ決定されていないため、その検討の一環としてもassert
のキーワード化をここで議論してSG21(契約プログラミング作業グループ)にフィードバックすることもこの提案の目的の一つです。
また、assert
を演算子化した場合には、その有効無効の切り替え(NDEBUG
マクロによるなど)の方法の可否や、ビルドモードのよる構文評価の有無などの問題と向き合う必要がありますが、それらの細かい議論はSG21で提案されている実行時アサーションの機能の議論と合流させる(ここでは取り扱わない)ことを推奨しています。
Concurrency TS v2の最新のドラフト(N4953)の変更点をまとめた文書。
次の提案と編集上の修正が適用されているようです。
2023年5月11日に行われたSG13の議事録。
2023年3月9日と4月13日に行われたSG19の議事録。
機械学習に関連のある提案のレビューが行われているようです。
複数の翻訳単位で分散している配列を1つの配列として扱う機能の提案。
C++のリンカには、複数の翻訳単位からシンボルを取り出してそれを1つの配列にまとめる機能があります。それは例えば、ある同じ名前の外部リンケージを持つ配列に対して各翻訳単位でそれぞれ初期化した後、リンカが最終的なプログラムを出力する際にそれぞれの翻訳単位(オブジェクトファイル)で初期化されている配列要素を何かしらの方法でマージします。
この機能はC++の単体テストフレームワークにおいて活用されており、各翻訳単位で定義されている単体テストを集めて管理するグローバルなシングルトンの実装に使用されます。このようなグローバルな分散配列の初期化は、コンパイル時(リンク時)にはその初期化子は判明していますが、言語サポートなどはないのでその初期化は実行時に行われます。
register
というキーワードでこの分散配列を指定することにすると、例えば次のコードのような雰囲気のことが行われています
using test_func = bool (*)();
extern const test_func g_tests[register];
#include "test_framework.h"
bool always_pass() {return true;}
const test_func g_tests[register] = {always_pass};
#include "test_framework.h"
bool fail1() {return false;}
bool fail2() {return false;}
const test_func g_tests[register] = {fail1, fail2};
#include "test_framework.h"
int main() {
for (test_func f : g_tests) {
if(!f()) return 1;
}
return 0;
}
この機能は他のところでは、C++の実装(コンパイラ)が静的初期化子やスレッドローカルストレージ、例外処理テーブルなどを実装する際に使用されるほか、動的リフレクションの実装にも使用されているようです。
単体テストフレームワークにおける利用例はこの機能が有効に利用されている大きな例であり、その他の利用例もこの機能の有用性を物語っています。また、この機能をポータブルかつ簡易に利用できるようにすることで、static initialization order fiascoとして知られる問題をプログラマの希望に沿った初期化を確実に行われるように解決することができます。例えば、初期化関数と優先順位のペアからなる分散配列を用意しておき、プログラムの任意のタイミングでそれを優先順位でソートして対応する初期化を実行する、などの方法が可能となります。さらに、この方法は動的ライブラリの初期化にも活用できます。
既存の幅広い利用例や想定される利用法など、分散配列を言語サポートしてポータブルかつ簡易に利用可能にすることにはかなりの価値があり、この提案はそれを提案するものです。
この提案ではまだ具体的な構文が固まっていないようですが__distributed_array
に分散配列の名前を渡して配列宣言に指定することで、分散配列であることを明示し、その初期化子は1つの配列にリンク時に統合されます。
using test_case_callback = void();
extern test_case_callback* const test_cases[];
__distributed_array(test_cases)
test_case_callback* const my_test_case = my_test_case_function_1;
__distributed_array(test_cases)
test_case_callback* const my_test_case_array[] = {
my_test_case_function_2,
my_test_case_function_3,
};
void run_unit_tests() {
test_case_callback* const* const test_data = ::test_cases;
size_t const test_count = std::distributed_array_size(test_data);
std::span const tests = std::span(test_data, test_count);
for (test_case_callback* const test : tests) {
test();
}
}
分散配列そのものの宣言はextern
による要素数不明の配列であり、これは新しい構文やキーワードを追加する負担を避けたものです。別の提案(P2268)で提案されていたように(最初の例のように)register
キーワードを使用するなど、別の構文を妨げるものではありません。
分散配列の型に関しては、普通の配列型と区別しておくとコンパイル時の診断が行いやすくなったり、既存の範囲のためのユーティリティを直接分散配列で利用できるようになるなどのメリットがあるため、分散配列専用の配列型を追加することを提案しています。具体的には決まっていませんが、register
キーワードを用いる場合はT[register]
やT(&)[register]
のようなものが考えられます。
静的な変数(配列)の定義に対して__distributed_array(A)
のように指定すると、その配列は分散配列A
の部分定義として機能します。最終的なプログラムでは、それら部分定義は全てマージされてA
の定義となります。分散配列がextern cv T A[];
のように定義されている時、その部分定義はcv T
型の変数もしくはcv T
型の要素を持つ配列宣言であり、かつ名前空間スコープもしくは静的メンバ変数である必要があります。
分散配列のサイズを取得するためには、ここではstd::distributed_array_size()
というライブラリ関数を利用していますが、sizeof
や専用言語機能による取得方法も考慮されており、まだ決定していません。
分散配列の要素の順序はほぼ不定ですが、1つの翻訳単位で定義された(非inline
)部分定義はその定義の順番で順序づけされます。
有効な利用例を見出せないため現在のところはthread_local
な分散配列は提案されていません。
動的ライブラリにおいて実行時にそれらに含まれる分散配列を統合することはおそらくほぼ不可能であり、動的リンクはC++標準の範囲外のことであるため提案されていません(Windowsでは問題とならないとのことです)。ここではあくまで、静的なリンク時点で定義される分散配列を提案しており、静的ライブラリでは同じ分散配列定義は統合されますが、動的ライブラリにおいてはそれらシンボルは競合するとみなすことにすることを提案しています。言い換えると、動的リンカは分散配列を扱う必要はありません。これは既存のリンカの動作と一致しているようです。
この提案は、EWGIの初期レビューで好意的に受け止められており、さらなる作業が続行される予定です。
SG16(ユニコード関連の作業部会)の2023年1月11日から5月10日の間のミーティングの議事録
9回分のミーティングの参加者や議題、発言、投票行動などが記録されています。
提案中のstd::simd
がregular
であるようにする提案。
C++26に向けて提案中のstd::simd
型はoperator==
が要素ごとの比較を行った結果をbool
ではなく要素ごとの比較結果を1/0
で保存したsimd_mask
型として返します。そのため、regular
コンセプトを満たしません。
これは例えば、std::simd
型をメンバとして保持するクラスにoperator==
をデフォルト実装しようとした時に問題となります。
using uint32_4v = std::fixed_size_simd<std::uint32_t, 4>;
class Color {
public:
bool operator==(const Color &) const = default;
private:
uint32_4v data_;
};
void f() {
Color a, b;
...
if ( a == b )
}
std::simd
は値セマンティクスを持つ値型として設計されており、その演算をSIMD演算にエンコードする以外の部分では組み込み型と同様に動作することを意図しています。regular
コンセプトはそのような値型が満たすべき基本的な性質です。また、regular
な型には次のような関係があることが期待されます
T a = b; assert(a == b);
T a; a = b;
とT a = b;
は同値
T a = c; T b = c; a = d; assert(b == c);
T a = c; T b = c; zap(a); assert(b == c && a != b);
これらの性質は例えばstd::find
のように内部で比較を行うような処理が暗黙的に要求していることでもあり、標準ライブラリの中でも一般のライブラリにおいても広く活用されています。
また、在野のライブラリでは多くのSIMD型が提供されており、そこではSIMDレジスタをラップするものから、より大きな数学的なベクトル・行列を表現するもの(DSL)まであります。そこでもoperator==
による比較が提供されており、多くの場合は要素ごとの比較を行いその結果を(非bool
で)返します。
そのようなライブラリのうち、DSLを目的とするライブラリはstd::simd
とは目的が異なっているため、そこからstd::simd
へ移行することはまずないと思われるためstd::simd
の比較がどう選択されてもそのユーザーにはあまり関係ないでしょう。それ以外のライブラリのユーザーやDSLの実装者はstd::simd
へ移行することにはかなりの利点があり、その場合のstd::simd
の比較は他のC++の場合と同じ意味論が自然に要求されるでしょう。
C++の標準ライブラリにはstd::simd
のようなデータ並列型は存在していませんが、値のシーケンスを表す型はいくつかあり(bitset, vector, array, valarray
など)、std::valarray
を除いて全ての型がbool
を返すoperator==
を持っています。
この提案はこれらの背景から、std::simd
のoperator==
をマスクではなくbool
を結果として返すように変更し、結果をマスクで取得するのは別のフリー関数によって行うようにすることを提案するものです。
friend
宣言でのパック展開を許可する提案。
クラステンプレート定義内でテンプレートパラメータに対してfriend
指定を行う際、そこにパラメータパックを指定することはできません。
template <typename T>
class Foo1 {
friend T;
public:
};
template <typename... Ts>
class Foo2 {
friend Ts...;
public:
}
この提案はfriend
宣言でのパック展開を許可し、パラメータパックに含まれるそれぞれのクラス型に対してfriend
を適用するようにするものです。
この提案のモチベーションの一つとして、Passkeyイディオムというテクニックのサポートが挙げられています。
あるクラスC
の定義内で他のクラスD
をfrined
指定すると、D
はC
の全てのプライベートメンバへアクセスすることができます。この時、アクセスしたいのがある1つのプライベートメンバだけだったとしても、その公開範囲を制限する方法はありません。
class D;
class C {
friend D;
...
};
Passkeyイディオムは、この時に別のクラスPasskeyを間に挟むことでプライベートメンバの公開範囲を制限するものです。
class D;
class Passkey {
friend D;
Passkey() = default;
};
class C {
...
public:
auto do_something(Passkey) {
...
}
};
プライベートアクセスは関数経由にはなりますが、これによって必要なプライベートメンバだけにプライベートアクセス範囲を絞ることができます。
PasskeyイディオムにおけるPasskey
クラスはテンプレートにすることで使いまわすことができ、そうするとPasskey
を使用する場所でアクセス可能なクラス名が明示されるようになります。
class D;
template<typename T>
class Passkey {
friend T;
Passkey() = default;
};
class C {
...
public:
auto do_something(Passkey<D>) {
...
}
};
この場合でもdo_something()
にはD
からしかアクセスできません。
さらに、複数のクラスに対してアクセスを許可したい場合のためにPasskey
クラスのテンプレートパラメータを可変にすることが考えられます。
class D;
class E;
class F;
template<typename... Ts>
class Passkey {
friend Ts...;
Passkey() = default;
};
class C {
...
public:
auto do_something(Passkey<D, E, F>) {
...
}
};
ただし、現在はfriend
宣言におけるパック展開が不可能であるため、これはできません。
提案では他にも、CRTPにおいて基底クラスから派生クラスのプライベートメンバアクセスを許可する場合に派生クラスで可変長テンプレートを使用する場合の例を挙げています。
※この部分は@Reputelessさんに執筆していただきました
派生クラスをコピー不可 / ムーブ不可にするユーティリティクラス std::noncopyable
, std::nonmovable
を <utility>
に追加する提案。
リソースを複製しない RAII クラスの実装のために、クラスをムーブオンリーに、あるいはムーブもコピーも禁止にしたいことがあります。Boost ライブラリが提供する noncopyable
のようなクラスを継承すると、そうした実装を簡単に記述でき、意図も明確になります。この方法はコード検索でも多数ヒットするため、一般的なイディオムであると考えられます。
ただし、boost::noncopyable
は、ムーブセマンティクスが導入される C++11 以前に設計されたもので、現在の C++ では、その派生クラスはコピー不可かつムーブ不可になります。これは標準に導入されたコンセプト std::copyable
, std::movable
と並べたときに違和感があります。C++ のクラスは std::copyable
を満たさない場合でも、std::movable
を満たすことはできるからです。
Boost の noncopyable の実装概略:
class noncopyable {
protected:
noncopyable() = default;
~noncopyable() = default;
noncopyable(const noncopyable&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
};
struct ObjectNonCopyable : noncopyable {};
int main()
{
std::cout << std::boolalpha;
std::cout << std::copyable<ObjectNonCopyable> << '\n';
std::cout << std::movable<ObjectNonCopyable> << '\n';
}
この提案では、コピーを禁止させるためのクラス std::noncopyable
と、ムーブおよびコピーを禁止させるためのクラス std::nonmovable
をそれぞれ標準ライブラリで提供し、std::noncopyable
ではムーブを禁止しないことで、標準のコンセプト std::copyable
, std::movable
と一貫させることを目指します。
提案の実装概略は次の通りです。
struct noncopyable {
noncopyable() = default;
noncopyable(noncopyable&&) = default;
noncopyable& operator=(noncopyable&&) = default;
};
struct nonmovable {
nonmovable() = default;
nonmovable(nonmovable const&) = delete;
nonmovable& operator=(nonmovable const&) = delete;
};
struct ObjectNonCopyable : noncopyable {};
struct ObjectNonMovable : nonmovable {};
int main()
{
std::cout << std::boolalpha;
std::cout << std::copyable<ObjectNonCopyable> << '\n';
std::cout << std::movable<ObjectNonCopyable> << '\n';
std::cout << std::copyable<ObjectNonMovable> << '\n';
std::cout << std::movable<ObjectNonMovable> << '\n';
}
いずれも Empty base optimization がはたらくため、派生クラスに余分なオーバーヘッドは生じません。
mdspan
のアクセサポリシークラスに、参照する領域ポインタにstd::assume_aligned
を適用してアクセスするaligned_accessor
の提案。
mdspan
のアクセサポリシーとは、領域へのポインタとインデックスを受け取ってどのようにその領域へアクセスするか(要素を引き当てるか)を指定するポリシークラスです。デフォルトのアクセサ(std::default_accessor<T>
)は要素型T
のポインタptr
とインデックスidx
に対してptr[idx]
のようにアクセスしてその結果を返します。
この提案は、この場合に領域ポインタptr
をstd::assume_aligned
に通してからアクセスすることで、mdspan
に対してアライメント要件を宣言しつつコンパイラの最適化を適用する能力を与えるものです。
提案しているのはaligned_accessor<T, N>
というクラスで、次のようなものです
namespace std {
template<class ElementType, size_t the_byte_alignment>
struct aligned_accessor {
using offset_policy = default_accessor<ElementType>;
using element_type = ElementType;
using reference = ElementType&;
using data_handle_type = ElementType*;
static constexpr size_t byte_alignment = the_byte_alignment;
constexpr aligned_accessor() noexcept = default;
template<class OtherElementType, size_t other_byte_alignment>
constexpr aligned_accessor(aligned_accessor<OtherElementType, other_byte_alignment>) noexcept;
constexpr operator default_accessor<element_type>() const {
return {};
}
constexpr reference access(data_handle_type p, size_t i) const noexcept {
return assume_aligned<byte_alignment>(p)[i];
}
constexpr typename offset_policy::data_handle_type
offset(data_handle_type p, size_t i) const noexcept {
return p + i;
}
constexpr static bool is_sufficiently_aligned(data_handle_type p);
};
}
アクセサクラスはmdspan
に指定すると内部で勝手によしなにしてくれるので、通常これを直接扱う必要はないはずです(is_sufficiently_aligned()
はアライメントチェックのために使うことがあるかもしれません)。
簡単な使用例
#include <mdspan>
#include <ranges>
template<typename T, size_t byte_alignment>
using aligned_mdspan_Nx4 = std::mdspan<T, std::extents<size_t, std::dynamic_extent, 4>, std::layout_right, std::aligned_accessor<T, byte_alignment>>;
int main() {
using namespace std::views;
alignas(16) float array1[] = {...};
alignas(16) float array2[] = {...};
aligned_mdspan_Nx4<float, 16> mat44_1{array1, 4};
aligned_mdspan_Nx4<const float, 16> mat44_2{array2, 4};
for (auto [y, x] : cartesian_product(iota(0, 4), iota(0, 4))) {
mat44_1[y, x] *= mat44_2[y, x];
}
}
提案文書より、サンプルコード
template<size_t byte_alignment>
using aligned_mdspan =
std::mdspan<float, std::dextents<int, 1>, std::layout_right, std::aligned_accessor<float, byte_alignment>>;
extern void vectorized_axpy(aligned_mdspan<32> y, float alpha, aligned_mdspan<32> x);
extern float vectorized_norm(aligned_mdspan<32> y);
extern void fill_x(aligned_mdspan<16> x);
extern void fill_y(aligned_mdspan<16> y);
template<class ElementType>
struct delete_raw {
void operator()(ElementType* p) const {
std::free(p);
}
};
template<class ElementType>
using allocation = std::unique_ptr<ElementType[], delete_raw<ElementType>>;
template<class ElementType, std::size_t byte_alignment>
allocation<ElementType> allocate_raw(const std::size_t num_elements) {
const std::size_t num_bytes = num_elements * sizeof(ElementType);
void* ptr = std::aligned_alloc(byte_alignment, num_bytes);
return {ptr, delete_raw<ElementType>{}};
}
float user_function(size_t num_elements, float alpha) {
constexpr size_t max_byte_alignment = 32;
auto x_alloc = allocate_raw<float, max_byte_alignment>(num_elements);
auto y_alloc = allocate_raw<float, max_byte_alignment>(num_elements);
aligned_mdspan<max_byte_alignment> x(x_alloc.get(), num_elements);
aligned_mdspan<max_byte_alignment> y(y_alloc.get(), num_elements);
fill_x(x);
fill_y(y);
vectorized_axpy(y, alpha, x);
return vectorized_norm(y);
}
モジュールにおけるインポート可能なヘッダ(importable header)というものを修正する提案。
インポート可能なヘッダ(importable header)とは、非モジュールのヘッダファイルのうち、ヘッダユニットとしてインポートすることができる種類のヘッダのことです。インポート可能なヘッダの#include
は実装によってimport
に置換される可能性があります。
ただし、標準ライブラリのC互換ではないヘッダを除いて、C++標準はインポート可能なヘッダが何かを規定しておらず、それは実装定義とされています。
ヘッダユニットはモジュールの一種ではありますが、モジュール宣言によって作成される名前付きモジュールとは異なりあくまでヘッダファイルです。そのため、インポート可能なヘッダと名前付きモジュールには根本的な違いがあります
- 名前付きモジュールはその識別と探索のために以前には存在しなかったモジュール名という探索空間を提供するが、ヘッダユニット名はヘッダ名と同じ探索空間を共有する
- ヘッダユニットのインポートは、インポートされた先の翻訳単位にプリプロセッサの状態を漏洩する
この違いにより、いくつかの問題が生じています
ヘッダユニットの問題点
1. インポート可能なヘッダとインクルードされるヘッダの識別
現在のC++にはヘッダ名を正確に識別するための仕様やメカニズムがありません。そのため、インポート可能なヘッダの#include
をimport
に置換した場合にそれが同じヘッダファイルを指しているかすら保証できません。ともすれば異なるファイルを処理してしまったり、あるコンパイラでは意図通りになっていても別のコンパイラでは異なる結果が生じたりする可能性があります。
このことは、#pragma once
が有用であり実質ポータブルでありながら標準化されない(できない)理由にも通じます。#pragma once
では、何が一度だけインクルードされるべきなのかを指定する方法がなかったため、標準化に至りませんでした。実際、コンパイラによって同じヘッダを区別する方法が異なっています。
2. 依存関係スキャンの依存関係
モジュールのimport
はプリプロセッサとして処理されるため、あるソースファイルの依存関係をスキャンする場合には少なくともそのファイルのプリプロセスを完了させる必要があります。
その時、ヘッダユニットのインポートはそのプリプロセス状態(マクロ)もエクスポートするため、ヘッダユニットのインポートはその内容によって依存関係スキャンの結果に影響を与える可能性があります。そのため、依存関係スキャン処理は次のいずれかの対応をしなければなりません
- 依存関係スキャンへの入力としてヘッダユニットのビルド済モジュールインターフェースを受け入れる
- 一貫した処理のため、ヘッダユニットからのコマンドライン引数を受け入れて現在のプリプロセッサの状態を更新し、その後にヘッダユニットのプリプロセッサ状態をマージする
clang/MSVCの初期の依存関係スキャン実装は、あたかもヘッダのインクルードを行なっているかのようにヘッダユニットのインポートを処理しているようです(つまり、どちらの方法でもない)。そのアプローチはモノリシックリポジトリのような環境ではうまく動きますが、ビルド済バイナリが依存関係として入ってくるようなより複雑なビルドではインポート可能なヘッダが上手く扱われることを保証できません。
そのような場合は、次のように依存関係スキャンプロセスへの入力を補うことで解決が図れます
- 既知のインポート可能なヘッダの全てのリストを依存関係スキャン処理への入力とする
- インポート可能なヘッダのローカルプリプロセッサの引数を依存関係スキャン処理への入力とする
- 現在の翻訳単位のローカルプリプロセッサの引数を依存関係スキャン処理への入力とする
しかし、この代償としてある翻訳単位の依存関係に大きなボトルネックが生じます。つまり、これらの入力(インポート可能なヘッダのリストやプリプロセッサ引数)のどれかを少しでも変更すると、そこ以降の依存関係スキャン結果は全て無効になり、影響を受ける翻訳単位は全て依存関係スキャンのやり直しとその結果を受けてのリビルドが必要となります。これはC++プロジェクトのビルドに大きな追加のコストを導入します。
名前付きモジュールのimport
はマクロをエクスポートしないため、インポート先の翻訳単位のプリプロセッサ状態に何ら影響を与えません。
一方でヘッダユニットはそうではなく、ヘッダユニットのインポートを使用している場合にそこからエクスポートされるマクロについて知るには、そのヘッダユニットがどのようにコンパイルされるか(コンパイルオプション)を調べて、その状態がどのようにインポート先のプリプロセッサ状態にマージされるかを調べるためにビルドシステム(コンパイラ/依存関係スキャナ)の仕様を知りに行く必要があります。
この時、#include
がimport
に置換される場合、プログラマはどのヘッダがその対象なのかを知らなければプリプロセッサの状態を推測することができません。コンパイラによってその方法・基準が異なるという状況では、それはより困難となります。
このことは特にC++の教育時に問題となります。このような質問に対してどう答えるかは使用するビルドシステムにも依存することを考えると、この問題はより深刻です。
ヘッダユニットの目的
ヘッダユニットという仕様の目標は次のようなものでした
- 事前コンパイル済ヘッダの経験を教訓として、共通の一貫した仕様を策定する
- Clang Header Moduleの経験から、一貫した仕様を策定する
- モジュールへの移行を容易にする
これらの目標そのものには価値があり重要な目標ではありますが、現在のヘッダユニットの仕様ではこれらのことを達成できていません。
事前コンパイル済ヘッダにあった制限が受け継がれていない
事前コンパイル済ヘッダの実装はコンパイラによってまちまちでしたが、共通のサブセット要件が存在していました。それは、事前コンパイル済ヘッダの内容はそれを使用する翻訳単位内のコードの影響を受けないというものです。
すなわち、あるヘッダファイルの#include
は事前コンパイル済ヘッダが利用可能であるかどうかでその動作が変わることがありません。
Clang Header Moduleはそのヘッダがそれを使用する場所のプリプロセッサ状態の影響を受けないことを前提として実装されてており、それに違反するとエラーとなりこれはユーザーの責任とされていました。言い換えるとこれは、C++言語のサブセットを採用するコードベースに対して適用されます。
C++モジュール仕様におけるヘッダユニットにおいてはそのように適用可能な場所に言語サブセットを要求することは当然回避されるため、Clang Header Moduleのこの側面の経験をC++エコシステム全体にどのように適用するかという問題には対処していません。
ヘッダユニットが移行の役に立たない
事前コンパイル済ヘッダとClang Header Moduleの両方を経験したコードベースにおいては、どちらの場合の解釈もあくまでヘッダのインクルードであり、ツールが行うことはインクルードの最適化だと理解されています。
そのため、それら及び#include
がimport
に切り替えられた時の解釈は、インポート時点でのプリプロセッサの状態に影響されることなくヘッダをインクルードするセマンティクスに依存することになります。
しかし、よく使用するヘッダ(ソース)ファイルで新しい構文を使用するとそのファイルの使用可能性は、その構文をサポートするコンパイラとビルドシステムのみに制限されます。この時点で、インポート可能なヘッダに移行するか名前付きモジュールに移行するかの労力の差はそれほど変わりません。
実際、モジュールセマンティクスを必要とするコードベースにとっては与えられたヘッダのラッパモジュールを自動的に生成して、その名前付きモジュールでヘッダのエンティティをエクスポートすることはそれほど難しくないでしょう。それだけだとマクロをサポートできませんが、マクロは専用のヘッダにまとめて配置しそれをインクルードするようにすることは難しくなく、これらを組み合わせることは名前付きモジュールのための良い移行経路となるはずです。
ヘッダユニットのパフォーマンス上の利点は、ボトムアップに採用した場合に限られる
ヘッダユニットの初期の経験では、ヘッダユニットの採用が最も下位から(ネストした依存関係の内側から)開始されない場合にコンパイラのパフォーマンスが低下することがわかっています。
これは、同じヘッダのインクルードを含むようなより大きなヘッダ単位でインポートしてしまうと、それぞれのヘッダユニットのビルドにおいて同じヘッダを複数回処理することになってしまうためです。インクルードの場合、インクルードガードの処理が効率化されたことでそのスキップが効果的となるようです。
またそのような大きなヘッダをインポートした側では、同じヘッダをインクルードしていることで重複するエンティティ情報の統合を行う必要も出てきます。
これらのことを受けてこの提案では、インポート可能なヘッダの意味論を別に指定せず、インポート可能なヘッダを実装がインクルード処理を最適化する方法として指定することで、現在起きている問題を解決し元々の目標を達成することを提案しています。
このアプローチの重要な点は、ヘッダファイルの取り込みに関してはインクルードのセマンティクスが依然として標準的かつ期待される動作であり、実装に対して、そうすることが有利な場合に異なることを行う許可を与えて実装がその最適化に関する独自の注意事項を定義できるようにすることにあります。
この提案は、SG15の議論において方向性に同意が得られれば、このための文言を提供する改訂版を提出する予定です。
P1673で提案中のBLASベース線形代数ライブラリに、バッチ操作のサポートを追加する提案。
BLASのAPIは基本的に1関数につき1つの計算を行います。例えば、倍精度実数行列の行列積を計算するcblas_dgemm()
という関数は、入力として3つの行列A, B, C
とその係数alpha, beta
を受け取って、C = alpha * AB + beta * C
のような計算を行うものですが、これは3つの行列を使った1つの行列積を計算するものです。行列のサイズ(行数/列数)はA, B, C
で一貫している必要はありますが任意であり、問題を1つの行列に落とし込めればこれだけでも並列計算が可能ではあります。
対して、BLASにおけるバッチ操作(batched operation)では1つの関数呼び出しで複数の行列を使った複数の計算を行うものです。1つの行列に問題を落とし込むことができず独立した複数の行列で独立に同じ計算を行う必要がある場合、バッチAPIを用いると個別の関数呼び出しを(並列に)繰り返すよりも効率的に行列計算を行うことができます。
例えば、cblas_dgemm()
に対応するバッチ操作はcblas_dgemm_batch()
という関数で、行う計算自体は同じですが、パラメータは全て配列で受け取り複数の行列積を1つの関数呼び出しで並列実行します。実際どのように実行されるかは実装によるのですが、個別の関数を独立して呼び出すよりも効率的に実行されることが期待でき、実際そのように実装されます。
現在P1673で提案され作業中のBLASベース線形代数ライブラリはBLASの基本部分APIに対応するもので、バッチ操作は含まれていません。この提案はそこにバッチ操作も入れようとするものです。
バッチ操作の利点は次のようなものです
- 同じ処理による大量の問題を1度に解くというユーザーの意図を明らかにすることで、単一の小さな問題を解く処理がもつものよりもはるかに大きな並列化・ベクトル化の機会を得られる
- 個別の問題をBLAS APIに引き渡すために関数呼び出しの引数として表現することによるオーバーヘッドを償却できる
- バッチ引数に関する制約や仮定をインターフェースによって表現し、それに応じた計算の効率化を行える
- メモリアクセスパターンの改善
- 共通のデータの読み込みの再利用
- 全て同じ係数を使用する
cblas_dgemm_batch()
における係数alpha, beta
の再利用(ブロードキャスト)など
- 潜在的に共通する計算の再利用
- バッチ操作はBLASが利用されるさまざまな分野で計算効率化の役に立つ
- NVIDIA, AMD, intelなどのハードウェアベンダはバッチ操作を実装したBLAS実装とそれを高速に実行するハードウェアを提供している
- MAGMAやKokkosなどのオープンソースライブラリはクロスプラットフォームでバッチ操作を提供している
この提案では、std::mdspan
で入力を表現することによってcblas_dgemm()
に対するcblas_dgemm_batch()
のようにAPI名を分岐する必要はなく、関数に対する要求事項の変更のみによってP1673のAPIを拡張できるとしています。
また、バッチ操作の実装では基本的に使用する行列のサイズやストライドは行列ごとに可変とすることができるようですがそれを考慮すると効率化が妨げられるため、この提案では基本のケースである全ての行列のサイズとストライドは等しい場合のみをサポートすることにし、その効率化に焦点を当てています。
さらに、バッチ操作の入力行列(複数)の表現についても自由度があり(配列の配列、ストライドによる分離、インターリーブ)、この提案では効率性の観点からストライドとインターリーブのみをサポートすることにしています。それはまたstd::mdspan
のカスタムレイアウトによって表現可能であり、これによってインターフェースを1つにまとめることができます。
おわり
この記事のMarkdownソース