文書の一覧
全部で139本あります。
もくじ
- N4983 WG21 agenda: 24-29 June 2024, St. Louis, MO, USA
- P0260R9 C++ Concurrent Queues
- P0843R12 inplace_vector
- P0963R2 Structured binding declaration as a condition
- P1000R6 C++ IS schedule
- P1083R8 Move resource_adaptor from Library TS to the C++ WP
- P1112R5 Language support for class layout control
- P1144R11 std::is_trivially_relocatable
- P1255R13 A view of 0 or 1 elements: views::nullable And a concept to constrain maybes
- P1306R2 Expansion statements
- P1494R3 Partial program correctness
- P1928R9 std::simd - Merge data-parallel types from the Parallelism TS 2
- P2019R6 Thread attributes
- P2034R4 Partially Mutable Lambda Captures
- P2079R4 System execution context
- P2413R1 Remove unsafe conversions of unique_ptr
- P2434R1 Nondeterministic pointer provenance
- P2689R3 Atomic Refs Bound to Memory Orderings & Atomic Accessors
- P2719R0 Type-aware allocation and deallocation functions
- P2758R3 Emitting messages at compile time
- P2761R1 Slides: If structured binding (P0963R1 presentation)
- P2786R6 Trivial Relocatability For C++26
- P2822R1 Providing user control of associated entities of class types
- P2830R4 Standardized Constexpr Type Ordering
- P2835R4 Expose std::atomic_ref's object address
- P2841R3 Concept and variable-template template-parameters
- P2846R2 reserve_hint: Eagerly reserving memory for not-quite-sized lazy ranges
- P2849R0 async-object - aka async-RAII objects
- P2876R1 Proposal to extend std::simd with more constructors and accessors
- P2900R7 Contracts for C++
- P2963R2 Ordering of constraints involving fold expressions
- P2964R1 Allowing user-defined types in std::simd
- P2967R1 Relocation Is A Library Interface
- P2971R2 Implication for C++
- P2976R1 Freestanding Library: algorithm, numeric, and random
- P2988R5 std::optional<T&>
- P2996R3 Reflection for C++26
- P3045R1 Quantities and units library
- P3051R1 Structured Response Files
- P3059R1 Making user-defined constructors of view iterators/sentinels private
- P3064R1 How to Avoid OOTA Without Really Trying
- P3067R0 Provide predefined simd permute generator functions for common operations
- P3068R2 Allowing exception throwing in constant-evaluation
- P3085R2 noexcept policy for SD-9 (throws nothing)
- P3091R2 Better lookups for map and unordered_map
- P3094R2 std::basic_fixed_string
- P3096R1 Function Parameter Reflection in Reflection for C++26
- P3100R0 Undefined and erroneous behaviour are contract violations
- P3103R2 More bitset operations
- P3111R0 Atomic Reduction Operations
- P3119R1 Tokyo Technical Fixes to Contracts
- P3125R0 Pointer tagging
- P3126R1 Graph Library: Overview
- P3130R1 Graph Library: Graph Container Interface
- P3131R1 Graph Library: Containers
- P3137R1 views::to_input
- P3138R1 views::cache_last
- P3139R0 Pointer cast for unique_ptr
- P3149R3 async_scope -- Creating scopes for non-sequential concurrency
- P3154R1 Deprecating signed character types in iostreams
- P3157R1 Generative Extensions for Reflection
- P3175R1 Reconsidering the std::execution::on algorithm
- P3175R2 Reconsidering the std::execution::on algorithm
- P3179R1 C++ parallel range algorithms
- P3183R1 Contract testing support
- P3210R1 A Postcondition is a Pattern Match
- P3214R0 2024-04 Library Evolution Poll Outcomes
- P3228R1 Contracts for C++: Revisiting contract check elision and duplication
- P3234R1 Utility to check if a pointer is in a given range
- P3235R0 std::print more types faster with less memory
- P3236R1 Please reject P2786 and adopt P1144
- P3238R0 An alternate proposal for naming contract semantics
- P3239R0 A Relocating Swap
- P3247R1 Deprecate the notion of trivial types
- P3248R0 Require [u]intptr_t
- P3249R0 A unified syntax for Pattern Matching and Contracts when introducing a new name
- P3250R0 C++ contracts with regards to function pointers
- P3251R0 C++ contracts and coroutines
- P3253R0 Distinguishing between member and free coroutines
- P3254R0 Reserve identifiers preceded by @ for non-ignorable annotation tokens
- P3255R0 Expose whether atomic notifying operations are lock-free
- P3257R0 Make the predicate of contract_assert more regular
- P3258R0 Formatting charN_t
- P3259R0 const by default
- P3263R0 Encoded annotated char
- P3264R0 Double-evaluation of preconditions
- P3264R1 Double-evaluation of preconditions
- P3265R0 Ship Contracts in a TS
- P3265R1 Ship Contracts in a TS
- P3266R0 non referenceable types
- P3267R0 C++ contracts implementation strategies
- P3267R1 Approaches to C++ Contracts
- P3268R0 C++ Contracts Constification Challenges Concerning Current Code
- P3269R0 Do Not Ship Contracts as a TS
- P3270R0 Repetition, Elision, and Constification w.r.t. contract_assert
- P3271R0 Function Usage Types (Contracts for Function Pointers)
- P3273R0 Introspection of Closure Types
- P3274R0 A framework for Profiles development
- P3275R0 Replace simd operator[] with getter and setter functions - or not
- P3276R0 P2900 Is Superior to a Contracts TS
- P3278R0 Analysis of interaction between relocation, assignment, and swap
- P3279R0 CWG2463: What 'trivially fooable' should mean
- P3281R0 Contact checks should be regular C++
- P3282R0 Static Storage for C++ Concurrent bounded_queue
- P3283R0 Adding .first() and .last() to strings
- P3284R0 finally, write_env, and unstoppable Sender Adaptors
- P3285R0 Contracts: Protecting The Protector
- P3286R0 Module Metadata Format for Distribution with Pre-Built Libraries
- P3287R0 Exploration of namespaces for std::simd
- P3288R0 std::elide
- P3289R0 Consteval blocks
- P3290R0 Integrating Existing Assertions With Contracts
- P3292R0 Provenance and Concurrency
- P3293R0 Splicing a base class subobject
- P3294R0 Code Injection with Token Sequences
- P3295R0 Freestanding constexpr containers and constexpr exception types
- P3296R0 let_with_async_scope
- P3297R0 C++26 Needs Contract Checking
- P3298R0 Implicit user-defined conversion functions as operator.()
- P3299R0 Range constructors for std::simd
- P3301R0 inplace_stoppable_base
- P3302R0 SG16: Unicode meeting summaries 2024-03-13 through 2024-05-08
- P3303R0 Fixing Lazy Sender Algorithm Customization
- P3304R0 SG14: Low Latency/Games/Embedded/Financial Trading virtual Meeting Minutes 2024/04/10
- P3305R0 SG19: Machine Learning virtual Meeting Minutes to 2024/04/11-2024/05/09
- P3306R0 Atomic Read-Modify-Write Improvements
- P3307R0 Floating-Point Maximum/Minimum Function Objects
- P3308R0 mdarray design questions and answers
- P3309R0 constexpr atomic and atomic_ref
- P3310R0 Solving partial ordering issues introduced by P0522R0
- P3311R0 An opt-in approach for integration of traditional assert facilities in C++ contracts
- P3312R0 Overload Set Types
- P3313R0 Impacts of noexept on ARM table based exception metadata
- P3316R0 A more predictable unchecked semantic
- P3317R0 Compile time resolved contracts
- P3318R0 Throwing violation handlers, from an application programming perspective
- P3319R0 Add an iota object for simd (and more)
- P3320R0 EWG slides for P3144 "Delete if Incomplete"
- P4000R0 To TS or not to TS: that is the question
- おわり
N4983 WG21 agenda: 24-29 June 2024, St. Louis, MO, USA
P0260R9 C++ Concurrent Queues
標準ライブラリに並行キューを追加するための設計を練る提案。
以前の記事を参照
- P0260R5 C++ Concurrent Queues - [C++]WG21月次提案文書を眺める(2023年01月)
- P0260R7 C++ Concurrent Queues - [C++]WG21月次提案文書を眺める(2023年07月)
- P0260R8 C++ Concurrent Queues - [C++]WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- 提案する文言の拡充
capacity
の修正push()/pop()
の文言に"strongly happens before"を使うように変更- discussion pointsを追加
- コンストラクタとデストラクタが同じスレッドに戻ることに関する段落を削除
try_*
はブロックしなくなったtry_*
のspurious failureに関する文言を追加- キューを事前に埋める
bounded_queue
のコンストラクタを削除 - P3282への参照を追加
try_push(T &&, T &)
に関する議論をHistoric Contentsに移動
などです。
P0843R12 inplace_vector
静的な最大キャパシティを持ちヒープ領域を使用しないstd::vector
であるinplace_vector
の提案。
以前の記事を参照
- P0843R5 static_vector - [C++]WG21月次提案文書を眺める(2022年08月)
- P0843R6 static_vector - [C++]WG21月次提案文書を眺める(2023年05月)
- P0843R8
inplace_vector
- [C++]WG21月次提案文書を眺める(2023年07月) - P0843R9
inplace_vector
- [C++]WG21月次提案文書を眺める(2023年09月) - P0843R10
inplace_vector
- [C++]WG21月次提案文書を眺める(2024年02月) - P0843R11
inplace_vector
- [C++]WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- 以前のリビジョンを復帰して、条件付き
constexpr
を有効化 - R8で削除された
constexpr
対応の議論を再導入
などです。
この提案は、2024年6月の全体会議でC++26に向けて採択されています。
P0963R2 Structured binding declaration as a condition
構造化束縛宣言を条件式を書くところで書けるようにする提案。
以前の記事を参照
このリビジョンでの変更は、分解前に条件をチェックするという評価順序を規定しそれについての議論を追加したことと、提案する文言の改善などです。
この提案は、2024年6月の全体会議でC++26に向けて採択されています。
P1000R6 C++ IS schedule
C++26策定までのスケジュールなどを説明した文書。
このリビジョンはR5のものとほぼ同じですが、決定済みの会議開催地の地名が記載されるようになっています。
P1083R8 Move resource_adaptor from Library TS to the C++ WP
pmr::resource_adaptor
をLibrary Foundermental TSからワーキングドラフトへ移動する提案。
以前の記事を参照
- P1083R4 Move
resource_adaptor
from Library TS to the C++ WP - WG21月次提案文書を眺める(2022年01月) - P1083R5 Move
resource_adaptor
from Library TS to the C++ WP - WG21月次提案文書を眺める(2022年03月) - P1083R6 Move
resource_adaptor
from Library TS to the C++ WP - WG21月次提案文書を眺める(2022年07月) - P1083R7 Move
resource_adaptor
from Library TS to the C++ WP - WG21月次提案文書を眺める(2022年10月)
このリビジョンでの変更は、resource-adaptor-imp
に可変長引数コンストラクタを追加した事などです。
この提案は現在LWGでレビュー中です。
P1112R5 Language support for class layout control
クラスレイアウトを明示的に制御するための構文の提案。
以前の記事を参照
このリビジョンでの変更は
- Cでは構造体メンバの宣言順調整でこの問題を解消できるが、C++ではそれは実行可能ではないことについての説明の追加
- リフレクションではこの問題を解決できないことについてのセクションを追加
- Design principlesセクションを追加
eval
ストラテジーを追加smallest
をsmall
に変更し、アルゴリズムの説明を追加- 対象読者を追加
- 文言作成に関する戦略を追記
などです。
今回レイアウトに関する指示を行う方法が1つ追加されました
layout(eval(consteval_func))
- 元のレイアウトをエントリーの配列として取得し、オフセットフィールドを変更できる
consteval
関数を呼び出す
- 元のレイアウトをエントリーの配列として取得し、オフセットフィールドを変更できる
layout(eval(func))
に渡すconsteval
関数は、レイアウトを変更する場合にtrue
を返し、変更しないならばfalse
を返す必要があります。そして、その引数には次のような構造体の配列が渡されます
// クラスの元のレイアウトについての、各メンバ毎の情報を持つ struct layout_entry { enum type_id { base, member, tech }; type_id type; string_view name; // メンバ名 size_t index; // 宣言順のインデックス size_t size; // sizeof()の結果の値 int alignment; // alignas()の結果の値 bool noua; // [[no_unique_address]]の有無 bool fixed; // ムーブ可能かどうか size_t orig_offset; // 元のレイアウトにおけるオフセット mutable size_t offset; // 変更するオフセット(初期値はorig_offsetの値) }; // layout(eval(f))に渡す関数の例 consteval bool my_layout_func(span<const layout_entry> entries);
これによって、プログラマはかなり詳細にそのオフセットを制御することができるようになります。
この提案はこれ以上追求しないことがEWGで決定されたようです。
P1144R11 std::is_trivially_relocatable
オブジェクトの再配置(relocation)という操作を定義し、それをサポートするための基盤を整える提案。
以前の記事を参照
- P1144R6 Object relocation in terms of move plus destroy - WG21月次提案文書を眺める(2022年06月)
- P1144R7
std::is_trivially_relocatable
- WG21月次提案文書を眺める(2023年04月) - P1144R8
std::is_trivially_relocatable
- WG21月次提案文書を眺める(2023年05月) - P1144R9
std::is_trivially_relocatable
- WG21月次提案文書を眺める(2023年10月) - P1144R10
std::is_trivially_relocatable
- WG21月次提案文書を眺める(2024年02月)
このリビジョンでの変更は
- この提案に準拠済みのライブラリについての参照の追加
uninitialized_relocate_n
のテンプレートパラメータのためにNoThrowInputIterator
要件を追加- P3279への相互参照を追加
などです。
P1255R13 A view of 0 or 1 elements: views::nullable And a concept to constrain maybes
任意のオブジェクトやstd::optional
等のmaybeモナドな対象を要素数0か1のシーケンスに変換するRangeアダプタviews::maybe
/views::nullable
の提案。
以前の記事を参照
- P1255R6 : A view of 0 or 1 elements:
views::maybe
- [C++]WG21月次提案文書を眺める(2020年04月) - P1255R7 : A view of 0 or 1 elements:
views::maybe
- [C++]WG21月次提案文書を眺める(2022年05月) - P1255R8 A view of 0 or 1 elements:
views::maybe
- [C++]WG21月次提案文書を眺める(2022年07月) - P1255R9 A view of 0 or 1 elements:
views::maybe
- [C++]WG21月次提案文書を眺める(2022年08月) - P1255R10 A view of 0 or 1 elements:
views::maybe
- [C++]WG21月次提案文書を眺める(2023年09月) - P1255R12 A view of 0 or 1 elements:
views::maybe
- [C++]WG21月次提案文書を眺める(2024年01月)
このリビジョンでの変更は
views::maybe
を削除し、std::optional
のrange
化を好むstd::maybe
コンセプトの追加std::optional
やポインタ型などmaybee
型のためのフリー関数を追加
などです。
std::maybe
コンセプトは次のような単純なものです
template <class T> concept maybe = requires(const T t) { bool(t); *(t); };
これに対して次のフリー関数を追加します
template <class T, class R = optional<decay_t<T>>> constexpr auto yield_if(bool b, T&& t) -> R { return b ? forward<T>(t) : R{}; } template <maybe T, class U, class R = common_reference_t<iter_reference_t<T>, U&&>> constexpr auto reference_or(T&& m, U&& u) -> R { static_assert(!reference_constructs_from_temporary_v<R, U>); static_assert(!reference_constructs_from_temporary_v<R, T&>); return bool(m) ? static_cast<R>(*m) : static_cast<R>((U&&)u); } template <maybe T, class U, class R = common_type_t<iter_reference_t<T>, U&&>> constexpr auto value_or(T&& m, U&& u) -> R { return bool(m) ? static_cast<R>(*m) : static_cast<R>(forward<U>(u)) } template <maybe T, class I, class R = common_type_t<iter_reference_t<T>, invoke_result_t<I>>> constexpr auto or_invoke(T&& m, I&& invocable) -> R { return bool(m) ? static_cast<R>(*m) : static_cast<R>(invocable()); }
P1306R2 Expansion statements
コンパイル時にステートメントをループ生成することのできる、展開ステートメントの提案。
展開ステートメントとは、通常のrange
(配列やstd::vector
等コンテナ)に加えてタプルやクラス、波かっこのリストを反復することのできるコンパイル時のループ構文であり、ループの結果としてはループ本体のコード(ステートメント)をループの度にその場に展開していきます。
例えば次のような拡張ステートメントは
auto tup = std::make_tuple(0, ‘a’, 3.14); template for (auto elem : tup) { std::cout << elem << std::endl; }
次のようにコードをべた書きしたのと等価になります
auto tup = std::make_tuple(0, ‘a’, 3.14); { auto elem = std::get<0>(tup); std::cout << elem << std::endl; } { auto elem = std::get<1>(tup); std::cout << elem << std::endl; } { auto elem = std::get<2>(tup); std::cout << elem << std::endl; }
また展開ステートメントでは、波かっこリストが特別扱いされており、上記のコードは次のように書くこともできます
template for (auto elem : {0, 'a', 3.14}) { std::cout << elem << std::endl; }
この波かっこリストは初期化子リストではなく、この波かっこリストは初期化を行わずに各要素に対して直接ループが展開されます。
展開ステートメントは次のものに対して使用できます
- 構造化束縛可能なクラス型のオブジェクト
- プレーンな構造体やタプルなど
constexpr
な範囲オブジェクト- 主に
std::vector
- 主に
- 波かっこで区切られた式のリスト(展開初期化子リスト、"expansion-init-lists")
- 例えば、パラメータパックも
{pack...}
とすることでループ対象になる
- 例えば、パラメータパックも
展開ステートメントはループな風を装っていますが、実際のところコンパイル時にループによってステートメントを生成していくのではなく、予めイテレーション対象の各要素によって生成されたステートメントの列として、範囲for
同様に言語組み込みのマクロのような形で、一発で置換されます。とはいえ、結果のコードのインスタンス化順序や実行された時の実行順はあたかもループによって順番にステートメントが生成されていったのと同じように見えます。
この展開ステートメントは特に静的リフレクション機能と非常に相性が良い機能です。P2996R5より、列挙値を列挙値名の文字列に変換するサンプルコード
template <typename E> requires std::is_enum_v<E> constexpr std::string enum_to_string(E value) { template for (constexpr auto e : std::meta::enumerators_of(^E)) { if (value == [:e:]) { return std::string(std::meta::identifier_of(e)); } } return "<unnamed>"; } enum Color { red, green, blue }; static_assert(enum_to_string(Color::red) == "red"); static_assert(enum_to_string(Color(42)) == "<unnamed>");
^E
は列挙型E
からそのリフレクション情報を取り出しており、それに対するstd::meta::enumerators_of()
は列挙型の各列挙子のリフレクション情報を取り出しています。ここでのイテレーション対象e
はstd::meta::info
の値で、[:e:]
(スプライシング)によって対応する列挙値の定数値に変換することで、展開ステートメントの各ステートメントは列挙型E
の列挙子1つに対してマッチするif
文を生成します。
std::meta::identifier_of()
はリフレクション情報からそれに対応する識別子名を文字列で得るもので、ここではe
は列挙子を映したものなので、列挙子名がコンパイル時文字列で得られます。
このtemplate for
以外のものはP2996の静的リフレクション提案ですべて用意されているものです。
P1494R3 Partial program correctness
因果関係を逆転するような過度な最適化を防止するためのバリアであるstd::observable()
の提案。
以前の記事を参照
このリビジョンでの変更は
- フリースタンディングでのサポートを要求
- 実装可能性を明確化
- I/O関数を暗黙的なチェックポイントとして扱うようにする代替案についての説明の追加
などです。
P1928R9 std::simd
- Merge data-parallel types from the Parallelism TS 2
std::simd<T>
をParallelism TS v2から標準ライブラリへ移す提案。
以前の記事を参照
- P1928R1 Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2022年10月)
- P1928R2 Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2023年01月)
- P1928R3 Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2023年02月)
- P1928R4
std::simd
- Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2023年05月) - P1928R6
std::simd
- Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2023年07月) - P1928R7
std::simd
- Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2023年10月) - P1928R8
std::simd
- Merge data-parallel types from the Parallelism TS 2 - WG21月次提案文書を眺める(2023年12月)
このリビジョンでの変更は
- ベクトル化可能な型として、C++23の拡張浮動小数点数型を追加
- "selected indices”と“selected elements”の定義を改善
- ABIタグの意図を紹介する文言の改善
size
を呼び出し可能として一貫して使用- 不足していた
reduce
のtype_identity_t
を追加 basic_simd_mask
のデフォルトテンプレート引数をnative-abi
に修正simd_mask
のデフォルトテンプレート引数をsimd
と一貫するように修正<simd>
ヘッダを追加するのに必要な変更を追加- 2つの投票結果を追加
simd_size(_v)
を説明専用にするreduce_min_index
とreduce_max_index
事前条件を復元
などです。
この提案は現在、LWGでレビュー中です。
P2019R6 Thread attributes
std::thread/std::jthread
において、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。
- P2019R1 Usability improvements for
std::thread
- [C++]WG21月次提案文書を眺める(2022年08月) - P2019R2 Usability improvements for
std::thread
- [C++]WG21月次提案文書を眺める(2022年10月) - P2019R3 Thread attributes - [C++]WG21月次提案文書を眺める(2023年05月)
- P2019R4 Thread attributes - [C++]WG21月次提案文書を眺める(2023年10月)
- P2019R5 Thread attributes - [C++]WG21月次提案文書を眺める(2024年01月)
このリビジョンでの変更は、安全性向上のためにスレッド属性引数が左辺値参照にならないように制約を追加したことです。
現在のリビジョンでの使い方は次のようになっています
void f(int); int main() { std::jthread thread{ std::thread::name_hint{"Worker"}, // スレッド名の指定 std::thread::stack_size_hint{512*1024}, // スタックサイズの指定 f, 42 // 実行する処理とその引数 }; return 0; }
P2034R4 Partially Mutable Lambda Captures
ラムダ式の全体をmutable
とするのではなく、一部のキャプチャだけをmutable
指定できるようにする提案。
以前の記事を参照
- P2034R1 Partially Mutable Lambda Captures - WG21月次提案文書を眺める(2020年4月)
- P2034R2 Partially Mutable Lambda Captures - WG21月次提案文書を眺める(2020年4月)
- P2034R2 Partially Mutable Lambda Captures - WG21月次提案文書を眺める(2024年4月)
このリビジョンでの変更は、Meta-Motivationを追記したことと、いくつかのサンプルを改善したことなどです。
メタモチベーションでは、const
な変数は正しく扱うのが簡単で間違って扱うのが難しいはずであり、ラムダキャプチャの文脈でconst
をシンプルかつ正確に指定できるようになすることはプログラムの安全性とセキュリティを向上させるものである、としています。
P2079R4 System execution context
ハードウェアの提供するコア数(スレッド数)に合わせた固定サイズのスレッドプールを提供するSchedulerの提案。
- P2079R1 Parallel Executor - [C++]WG21月次提案文書を眺める(2020年8月)
- P2079R2 System execution context - [C++]WG21月次提案文書を眺める(2022年1月)
- P2079R3 System execution context - [C++]WG21月次提案文書を眺める(2022年07月)
このリビジョンでの変更は
- 設計上の考慮事項と目標をさらに追加
- さまざまな置き換え可能性オプションの比較を追加
- 置き換え可能性ABI標準化のモチベーションを追加
- 置き換え用ABIの例を追加
- 生存期間保証を強化
system_context
がその上で起動されたすべての作業よりも長く生存しなければならないことが明記され、破棄時に未完了の作業がある場合はstd::terminate
が呼び出されるようにした
などです。
置き換え可能性(replaceabiilty)とは、system_context
の実装をユーザーが置き換えられるようにすることをサポートする機能の事です。これは、厳密なシングルスレッドしか利用可能ではない組み込み環境やoneTBBのようなベンダー固有実装の利用など、多様な実行環境に対応するためです。
この方法としては3つの方法が考えられます
- コンパイル時の置き換え
- リンク時の置き換え
- 実行時の置き換え
方法 | メリット | デメリット |
---|---|---|
リンク時 | * 標準の先行例あり: operator new の置換と類似している* 予測可能性が高い: アプリケーション全体に適用されることが保証される * リンク時最適化により、型消去や間接参照をある程度削減できる可能性がある |
* ABI の定義が必要となるため、型消去や非効率性が発生する可能性がある * 共有ライブラリを使用する場合、異なる置換バージョンが混在する可能性があり、正しく動作させることが難しい * 置換がリンクの順序に依存する可能性がある |
実行時 | * 標準の先行例あり: std::set_terminate() と類似している* 共有ライブラリ (例: Windows の DLL における C++ 標準ライブラリの同一バージョン) を使用したアプリケーションで、一貫した動作を容易に実現できる * 1つのプログラムに system scheduler の複数の実装を含めることができる |
* system scheduler の置換と、それを用いた作業の生成との間で競合状態が発生する可能性がある * ABI を介した処理が必要となるため、リンク時最適化ができない * 異なる実装が起動時に system scheduler のためのリソースを割り当てた後、 main の開始時に実装が置換される可能性がある (主に QOI の問題) * 安全性を確保するため、およびユーザーが明示的に正しい処理を行うために、厳密なライフタイムと所有権の制御が必要となる |
コンパイル時 | * ユーザーは、あらゆる場所で利用できる型定義を使用して置換を行うことができ、簡単に切り替えることができる |
* ODR 違反が発生する可能性がある * 同一プロセス内の異なるバイナリ間で共有できない |
提案では、コンパイル時の置き換えはsystem_context
の重要な設計原則の一つである、オーバーサブスクリプションを回避するためにアプリケーション全体で共通な1つの実行コンテキストを持つ(ためのもの)、というのを破ってしまうため有効なオプションではないとしており、実行時かリンク時のどちらかがオプションとして残っています。
P2413R1 Remove unsafe conversions of unique_ptr
std::unique_ptr
における、危険な暗黙変換を禁止する提案。
以前の記事を参照
このリビジョンでの変更は
- 変換先の型にdestroying operator deleteがある場合でも変換を許可する
default_delete::operator()
を制約する- 生ポインタを取る
std::unique_ptr
のメンバ関数を制約する - 不完全型を処理する
- 破壊的変更を文書化
などです。
P2434R1 Nondeterministic pointer provenance
現在のC++のポインタ意味論をポインタのprovenanceモデルに対して整合させるための提案。
このリビジョンでの変更は
- P2318R1の分析と提案に関する議論を明確化
- pointer zapへの影響についての調査を追加
などです。
pointer zapへの影響については、皆無ではないものの小さな追加の変更だけで対応できるとしています。
P2689R3 Atomic Refs Bound to Memory Orderings & Atomic Accessors
mdspan
の要素にアトミックにアクセスするためのアクセサである、atomic_accessor
の提案。
以前の記事を参照
- P2689R0
atomic_accessor
- WG21月次提案文書を眺める(2022年10月) - P2689R1
atomic_accessor
- WG21月次提案文書を眺める(2023年01月) - P2689R2
atomic_accessor
- WG21月次提案文書を眺める(2023年07月)
このリビジョンでの変更は
atomic-ref-bound
を算術型・ポインタ型とそれ以外の型用に分離- メンバ型
deference_type
の定義の有無のため
- メンバ型
atomic-ref-bound
が説明専用であるかどうかについての議論を追加- メモリオーダーの指定に関する変換コンストラクタを持たないことについての議論を追加
- west
const
スタイルの使用
などです。
P2719R0 Type-aware allocation and deallocation functions
型を指定する形のnew/delete
演算子カスタマイズ方法の提案。
ある型について、new/delete
演算子をカスタマイズする方法は、クラスの静的メンバ関数として定義するか、グローバルなnew/delete
演算子をオーバーライドするかの2択です。new/delete
演算子を型ごとにカスタマイズする場合はその型についての知識が必要となりますが、前者の方法だとそれが得られるものの型に対して侵入的にしか定義できません。一方後者の方法は非侵入的に定義できるものの、グローバルなnew/delete
を丸ごと置き換えることになる(型毎にオーバーロード可能なものの、現実には::operator new(void*)
を丸ごと置換しているケースがほとんど)ため影響範囲が大きく、複数の翻訳単位(ライブラリ)が独自にそれを行っているとODR違反の未定義動作で厄介なバグを抱え込むことになります。
この提案では、ある型についてのnew/delete
演算子の非侵入的なカスタマイズ方法を提案するものです。
この提案によるカスタム方法は、既存のoperator new/delete
のオーバーロードがタグ型を取るようになっていることの延長で、ある型T
についてのnew/delete
演算子オーバーロードはタグ型としてstd::type_identity<T>
を取るようにするものです。そして、new/delete
式が演算子を探索する過程を少し調整して、このようなオーバーロードが発見されるようにします。
この提案の前後で、あるnew
演算子呼び出し(new
式)に対してコンパイラが考慮するnew
演算子のシグネチャ候補と順番は次のように変化します
現在 | この提案 |
---|---|
// ユーザーの書くnew演算子 new (args...) T(...) // コンパイラが考慮する演算子 T::operator new(sizeof(T), args...) ::operator new(sizeof(T), args...) |
// ユーザーの書くnew演算子 new (args...) T(...) // コンパイラが考慮する演算子 T::operator new(sizeof(T), args...) operator new(sizeof(T), type_identity<T>{}, args…) // 👈追加 ::operator new(sizeof(T), args...) |
現在 | この提案 |
---|---|
// ユーザーの書くdelete演算子 delete ptr // コンパイラが考慮する演算子 T::operator delete(void-ptr) ::operator delete(void-ptr) |
// ユーザーの書くdelete演算子 delete ptr // コンパイラが考慮する演算子 T::operator delete(void-ptr) operator delete(void-ptr, type_identity<T>{}) // 👈追加 ::operator delete(void-ptr) |
配列版も同様です。
セキュリティ向上のために、型毎に異なるメモリ領域に配置する(同じ型のオブジェクトを同じメモリ領域に配置する)というテクニックがあり、これによってType Confusion攻撃に対する耐性を向上させることができるようです。実際に、Appleでは自社開発のOSカーネルでこれを導入することでType Confusion脆弱性の軽減に大きな効果があったとのことです(筆者の方はAppleの方)。
カーネルに限らず、セキュリティが重要なコードベースではこのような手法を取り入れることが合理的な場合がありますが、現在のC++のnew/delete
オーバーロードはこのような手法を大規模かつシステム全体で達成することをサポートできていません。この提案の内容はそのような手法を達成するための構成要素であり、間接的にC++プログラムのセキュリティを向上させることに繋がります。
提案文書より、サンプルコード
namespace lib { struct Foo { }; void* operator new(std::size_t, std::type_identity<Foo>); // (1) struct Foo2 { }; } struct Bar { static void* operator new(std::size_t); // (2) }; void* operator new(std::size_t); // (3) void f() { new lib::Foo(); // calls (1) new Bar(); // calls (2) new lib::Foo2(); // (1)が見つかるがオーバーロード解決に失敗するため、(3)が呼び出される new int(); // calls (3) }
現在コンパイラはnew
演算子の探索において、まず対象の型(new T{}
のT
)のクラススコープでT::operator new()
を探索し、見つかればそれを使用します。見つからない場合、次にグローバルの::operator new()
を探索し、見つかればそれを使用します。どちらかが見つかれば例えばそれがエラーでも探索はそれで終了します。
この提案では、グローバルな::operator new()
を探索する前に、フリー関数のoperator new()
を考慮するようにします。その際のシグネチャは次のものです
operator new(sizeof(T), std::type_identity<T>{}, placement-args...)
型T
に対して第二引数でstd::type_identity<T>
を受け取るようにオーバーロードしておくことで、この挿入された2ステップ目の探索でそれを使わせることができるようになるわけです。そして、この探索はADLによって行われるため、名前空間内でnew/delete
演算子を定義しておくこともでき、これによってODR違反を緩和することができます。
P2758R3 Emitting messages at compile time
コンパイル時に任意の診断メッセージを出力できるようにする提案。
以前の記事を参照
- P2758R0 Emitting messages at compile time - WG21月次提案文書を眺める(2023年01月)
- P2758R1 Emitting messages at compile time - WG21月次提案文書を眺める(2023年12月)
- P2758R2 Emitting messages at compile time - WG21月次提案文書を眺める(2024年102月)
このリビジョンでの変更は
- 他の提案(P2741R3及びP2738R1)が採択されていることを前提として提案を整理
- すべてのAPIに追加されるタグについて議論を追記
などです。
このリビジョンで導入されたタグとは、エラーメッセージの種別を表すものです。これは、エラーメッセージを発行するかどうかを制御するためのもので、実装定義の方法で特定のタグを持つコンパイル時メッセージ出力関数の出力を無効化することができます。たとえば
std::constexpr_warning( "format-too-many-args", // 👈タグの指定 "Format string consumed {} arguments but {} were provided.", current_arg, total);
(fmtライブラリ中で)このようなコンパイル時出力が使用されている場合、-Wno-format-too-many-arg
オプションによってこの出力を無効化する、のような使い方を想定しています。ただし、この制御方法は実装定義とされています。また、タグに使用できるのはコンパイル時の文字列かつ。大文字小文字と数字アンダーバーくらいに制限することを提案しています。これは、コンパイラフラグとしてタグ文字列に対応する文字列をCLIから指定して制御を行うことを意図しているためです。
このリビジョンでは提案する関数は基本的にこのタグを受け取るようになりました
namespace std { // 定数式中でメッセージを出力 constexpr void constexpr_print_str(string_view) noexcept; constexpr void constexpr_print_str(tag-string, string_view) noexcept; // 定数式中で警告メッセージを出力 constexpr void constexpr_warning_str(tag-string, string_view, string_view) noexcept; // 呼ばれるとコンパイルエラー、指定されたエラーメッセージを出力 constexpr void constexpr_error_str(tag-string, string_view) noexcept; }
P2761R1 Slides: If structured binding (P0963R1 presentation)
P0963R1の紹介スライド。
このリビジョンでの変更は明示的ではありませんが、スライドが3ページ分足されています。
P2786R6 Trivial Relocatability For C++26
trivially relocatableをサポートするための提案。
以前の記事を参照
- P2786R0 Trivial relocatability options - WG21月次提案文書を眺める(2023年02月)
- P2786R1 Trivial relocatability options - WG21月次提案文書を眺める(2023年05月)
- P2786R2 Trivial relocatability options - WG21月次提案文書を眺める(2023年07月)
- P2786R3 Trivial relocatability options - WG21月次提案文書を眺める(2023年10月)
- P2786R4 Trivial relocatability options - WG21月次提案文書を眺める(2024年02月)
- P2786R5 Trivial relocatability options - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更はロードマップを追加したことです。
P2822R1 Providing user control of associated entities of class types
ADLにおいて考慮される関連名前空間を制御する言語機能の提案。
以前の記事を参照
このリビジョンでの変更は
- クラスに関連エンティティ指定がなされている場合、そのクラスを囲う最も内側の名前空間を関連名前空間として暗黙的に追加しないようにした
friend
と宣言された関数を発見するADLの動作が影響を受けないようにした- 関連エンティティ指定がなされたクラス型の前方宣言の意図されたセマンティクスと、同等の前方宣言を構成する方法を明確化
- mp_unitsライブラリにおけるADLに関する問題について追記
- CWG Issue 2857が解決されたことを示すように更新
- 検討されたオプション(代替案)の長所と短所を説明するために、構文の選択に関するセクションを追加
- “Proposed Design”セクションを拡張し、予想されるセマンティクスについて詳細に解説する
- 設計に関するセクションの追加
- “Naming a class template as an associated entity”
- “Naming a template template parameter bound to an alias template”
- “Class template instantiation”
- 関連エンティティの計算中にテンプレートのインスタンス化を回避しようとするのをやめた
- 他のエンティティ(非クラス?)の関連名前空間という言葉を個別に定義し、どの名前空間が関連エンティティであり、したがってADLで考慮されるべきかを明示するように提案する文言を変更
- 参照型を処理するために[basic.lookup.argdep]の文言を修正していたのを取り消し
- この問題はCWG Issue 2888で補足されている
などです。
P2830R4 Standardized Constexpr Type Ordering
std::type_info::before()
をconstexpr
にする提案。
以前の記事を参照
- P2830R0 constexpr type comparison - WG21月次提案文書を眺める(2023年04月)
- P2830R1 constexpr type comparison - WG21月次提案文書を眺める(2023年12月)
- P2830R3 constexpr type comparison - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- EWGの議論のフィードバックを適用
__PRETTY_FUNCTION__
の相違点の例を追加- ステータスの更新(EWGの構文アクセプトにより、LEWGレビュー待ち)
SORT_KEY(x, y)
を適切に定義するための更なる検討の追加
などです。
P2835R4 Expose std::atomic_ref
's object address
std::atomic_ref
が参照しているオブジェクトのアドレスを取得できるようにする提案。
以前の記事を参照
- P2835R0 Expose
std::atomic_ref
's object address - WG21月次提案文書を眺める(2023年05月) - P2835R1 Expose
std::atomic_ref
's object address - WG21月次提案文書を眺める(2023年07月) - P2835R2 Expose
std::atomic_ref
's object address - WG21月次提案文書を眺める(2024年01月) - P2835R3 Expose
std::atomic_ref
's object address - WG21月次提案文書を眺める(2024年02月)
このリビジョンでの変更は
address()
の戻り値型にuintptr_t
を使用するようにした- 戻り値型を
const void*
から、T
の修飾をコピーするaddress_return_t
に戻した
- 戻り値型を
- 戻り値型の変更に伴ってサンプルコードやgodboltの例を修正
などです。
この提案はLWGに転送されてレビュー待ちをしています。
P2841R3 Concept and variable-template template-parameters
コンセプトを受け取るためのテンプレートテンプレートパラメータ構文の提案。
以前の記事を参照
- P2841R0 Concept Template Parameters - WG21月次提案文書を眺める(2023年05月)
- P2841R1 Concept Template Parameters - WG21月次提案文書を眺める(2023年10月)
- P2841R2 Concept Template Parameters - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は提案する文言の改善のみです。
P2846R2 reserve_hint
: Eagerly reserving memory for not-quite-sized lazy ranges
遅延評価のため要素数が確定しない range の ranges::to
を行う際に、推定の要素数をヒントとして知らせる ranges::size_hint
CPO を追加する提案。
以前の記事を参照
- P2846R0
size_hint
: Eagerly reserving memory for not-quite-sized lazy ranges - WG21月次提案文書を眺める(2023年05月) - P2846R1
size_hint
: Eagerly reserving memory for not-quite-sized lazy ranges - WG21月次提案文書を眺める(2023年09月)
このリビジョンでの変更は
size_hint
からreserve_hint
ヘリネームtake_view
は無条件でreserve_hint
を提供するように修正
などです。
P2849R0 async-object - aka async-RAII objects
sender
チェーンにRAIIリソースを注入するための、非同期オブジェクトの提案。
sender/receiver
自体は非同期関数の実装の一部であり、P2300のフレームワークでは非同期関数は値・エラー・キャンセルのいずれかの結果によって非同期的に完了します。
非同期関数の内部で使用されるリソース(非同期オブジェクト)は、非同期処理が単一のローカルなブロックに収まるものではないため、通常手動でメモリを管理する必要があります。
sender
アルゴリズムによるチェーンに対して値を注入するには、let_value(value, func)
アルゴリズムを使用します。これは、func(value)
の結果を(receiver
の値チャネルで)返すsender
を返すもので、そこから得られる値(func(value)
の結果)は安定したストレージに保持されることが保証されます。
これと同種なものとして、let_stop_source(sender, sender(inplace_stop_source&))
というアルゴリズムを考えることができます。これは、sender
チェーンに対してinplace_stop_source
を注入し、なおかつそれに安定的なストレージを提供するものです。この延長で、sender
チェーンに対して多様な非同期リソースを注入するめのlet_xxx
アルゴリズムが個別に考案されつつあります。このことは、このような非同期リソースをsender
チェーンで扱うための汎用的な設計が欠けていることを表しています。
そのような非同期コードでは多くの場合、shared_ptr
を使用してアドホックなガーページコレクションが実装されます。このようなアドホックな非同期オブジェクトの非同期的な構築と破棄はある統一的な汎用設計に従っていない場合、汎用的なアルゴリズムからそのような非同期リソースを複数まとめて扱うことを困難にします。
この提案は、sender/receiver
を用いた非同期オブジェクトの構築と破棄のルールをライブラリに実装する方法についてを提案するものです。
この提案では、非同期オブジェクトのための3つの基本的な要件について定義しています
- 非同期構築(async constructio)
- オブジェクトの中には構築中に実行される非同期関数がある
- ファイルを開く、通信を確立する、など
- スレッドをブロックすることなく、構築中に非同期関数を使用できるようにする必要がある
- コンストラクタはこれを満たすことができない
- オブジェクトの中には構築中に実行される非同期関数がある
- 非同期破棄(async destruction)
- オブジェクトの中には破棄中に実行される非同期関数がある
- ファイルをフラッシュする、通信を中断する、など
- スレッドをブロックすることなく、破棄中に非同期関数を使用できるようにする必要がある
- デストラクタはこれを満たすことができない
- オブジェクトの中には破棄中に実行される非同期関数がある
- 構造化された正しい構築(structured and correct-by-construction)
これらの要件の元、この提案ではつぎの3つのものを提案しています
- 非同期オブジェクト
- 非同期構築が正常に完了するまで使用できない
- 非同期構築がエラーで完了する場合がある
- 非同期構築のキャンセルをサポートする
- 非同期破棄が失敗せず、停止できないことを保証する
async_using
アルゴリズム- 常に、内部の非同期関数を呼び出す前に非同期構築を完了する
- 常に、内部の非同期関数が完了する前に非同期破棄を完了する
- 常に、内部の非同期関数の完了に伴って非同期破棄を呼び出す
- 常に、複数の非同期オブジェクトの非同期破棄を、その非同期構築の逆順で呼び出す
- 常に、非同期構築に成功した非同期オブジェクトに対してだけ、非同期破棄を呼び出す
async_tuple
- 常に、
async_tuple
そのものの非同期構築完了の前に、内包する全ての非同期オブジェクトの非同期構築を完了する - 常に、内包する全ての非同期オブジェクトの非同期破棄を、その非同期構築の逆順で呼び出す
- 常に、非同期構築に成功した内包する非同期オブジェクトに対してだけ、非同期破棄を呼び出す
- 常に、
非同期オブジェクト同士は組み合わせて使用することができ、その要件は次のものです
- 構成(composition)
- 複数の非同期オブジェクトは、ネストせずに同時に使用可能になる
- オブジェクト間の依存関係はネストによって表現される
- 構成は、複数のオブジェクトの並行非同期構築をサポートする
- 構成は、複数のオブジェクトの並行非同期破棄をサポートする
この提案が提供するのは、これらの要件を具体化したasync_object<T>
をはじめとするコンセプトと、async_using
とasync_tuple
、およびpackaged_async_object
(非同期オブジェクトとその非同期コンストラクタ引数をパッケージングするもの)です。
提案より、サンプルコード。
非同期オブジェクト型(Foo
)の実装例
// Fooはint値を格納する非同期オブジェクト struct Foo { // 非同期オブジェクトを定義 // 非同期オブジェクトはムーブ不可である必要がある struct object : __immovable { object() = delete; int v; private: // only allow Foo to run the constructor friend struct Foo; explicit object(int v) noexcept : v(v) {} }; // 構築された非同期オブジェクトのハンドル型 // これは非同期構築(async_construct())によって生成される using handle = std::reference_wrapper<object>; // 非同期オブジェクト用のストレージを予約する型 // nothrowデフォルト構築可能 かつ ムーブ不可である必要がある using storage = std::optional<object>; // async_construct()は構築された非同期オブジェクトのハンドルで完了するsenderを返す auto async_construct(storage& stg, int v) const noexcept { return then( just(std::ref(stg), v), [](storage& stg, int v) noexcept { auto construct = [&]() noexcept { return object{v}; }; stg.emplace( __conv{construct}); printf("foo constructed, %d\n", stg.value().v); return std::ref(stg.value()); } ); } // async_destruct() はストレージ内の非同期オブジェクトの破棄を実行し、非同期破棄が完了した後に完了するsenderを返す auto async_destruct(storage& stg) const noexcept { return then( just(std::ref(stg)), [](storage& stg) noexcept { printf("foo destructed %d\n", stg.value().v); stg.reset(); } ); } }; // Fooはasync_object static_assert(async_object<Foo>); // Fooはintから非同期構築可能 static_assert(async_object_constructible_from<Foo, int>);
2つのFoo
型の非同期オブジェクトを作成し、sender
式で状態を変更する
int main() { // 2つの非同期オブジェクトを使用するsender auto inner = [](Foo::handle o0, Foo::handle o1) noexcept { return then( just(o0, o1), [](Foo::handle o0, Foo::handle o1) noexcept { auto& ro0 = o0.get(); auto& ro1 = o1.get(); ro0.v = ro0.v + ro0.v; ro1.v = ro1.v + ro1.v; printf("foo pack usage, %d, %d\n", ro0.v, ro1.v); fflush(stdout); return ro0.v + ro1.v; } ); }; // 非同期オブジェクトとコンストラクタ引数をパッケージング packaged_async_object foo7{Foo{}, 7}; packaged_async_object foo12{Foo{}, 12}; // ストレージを予約し、2つの非同期オブジェクトを非同期構築し // それらが完了してからinnerの処理にそれらのハンドルを渡し // innerの処理が完了したら非同期オブジェクトを非同期破棄する // そして、それら全てが完了すると完了する非同期処理を表すsender auto use_s = async_using(inner, foo7, foo12); // 非同期処理開始と待機、結果取得 auto [v] = sync_wait(use_s).value(); printf("foo pack result %d\n\n", v); }
この提案の内容は純粋なライブラリ機能であり、仮実装が公開されています : https://godbolt.org/z/rrbW6veYd
P2876R1 Proposal to extend std::simd with more constructors and accessors
std::simd
に対して、利便性向上のために標準ライブラリにあるデータ並列型等のサポートを追加する提案。
以前の記事を参照
- P2876R0 Proposal to extend std::simd with more constructors and accessors - WG21月次提案文書を眺める(2023年05月)
このリビジョンでの変更は
などです。
P2900R7 Contracts for C++
以前の記事を参照
- P2900R1 Contracts for C++ - WG21月次提案文書を眺める(2023年10月)
- P2900R3 Contracts for C++ - WG21月次提案文書を眺める(2023年12月)
- P2900R4 Contracts for C++ - WG21月次提案文書を眺める(2024年01月)
- P2900R5 Contracts for C++ - WG21月次提案文書を眺める(2024年02月)
- P2900R6 Contracts for C++ - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
quick_enforce
セマンティクスの追加- P3191R0/P3226R0の内容
enforce
セマンティクスの終了方法をstd::abort()
呼び出しから実装定義の方法に変更- P3191R0の内容
- 契約条件の評価回数に実装定義の上限を追加
- 重複評価回数を指定し、重複評価なしをデフォルトとすることを推奨
- P3119R0の内容
post()
の条件式で配列引数をODR使用するのを禁止- P3119R0の内容
- 全ての契約注釈において、
va_start
の使用を禁止- P3119R0の内容
- ライブラリパートで提案している列挙型の基底型を未規定にする
- P3198R0の内容
contract_kind
列挙型の名前をassertion_kind
へ変更- P3198R0の内容
contract_semantic
列挙型の名前をevaluation_semantic
へ変更- P3198R0の内容
- 評価セマンティクスについて、checked/uncheckedと呼称していたところをchecking/non-checkingに変更
- 新しいサブセクション追加
- “Function Type Aliases”
- “Constructors and Destructors”
- “Differences Between Contract Assertions and the assert Macro”
- “Design Principles”セクションの拡張
- 様々な細かい説明とコード例を追加
などです。
P2963R2 Ordering of constraints involving fold expressions
コンセプトの制約式として畳み込み式を使用した場合に、意図通りの順序付を行うようにする提案。
以前の記事を参照
- P2963R0 Ordering of constraints involving fold expressions - WG21月次提案文書を眺める(2023年09月)
- P2963R1 Ordering of constraints involving fold expressions - WG21月次提案文書を眺める(2024年01月)
このリビジョンでの変更は
- CWGのレビューを受けての文言の改善
- 特にコンセプトがどう満たされるかの説明を追加
- 包摂のチェックが短絡されることを明確化
- 設計の説明を追加
- 畳み込み式の制約は、両方に同じ畳み込み演算子が使用されている場合にのみ包含関係が成立するように変更
- 空のパックがある場合に、包含関係の成立とチェックの間に矛盾が生じるのを回避
などです。
提案より、(R1で)追加されたサンプルコード
template <std::ranges::bidirectional_range R> void f(R&&); // #1 template <std::ranges::random_access_range R> void f(R&&); // #2 template <std::ranges::bidirectional_range... R> void g(R&&...); // #3 template <std::ranges::random_access_range... R> void g(R&&...); // #4 int main() { f(std::vector{1, 2, 3}); // ok(C++23及びこの提案)#2が呼ばれる g(std::vector{1, 2, 3}); // ng(C++23)、ok(この提案)#4が呼ばれる }
この提案は2024年6月の全体会議で採択され、C++26WDにマージされています。
P2964R1 Allowing user-defined types in std::simd
std::simd
のデータ型としてユーザー定義型を使用できるようにする提案。
以前の記事を参照
このリビジョンでの変更は
- SG1/SG6からのフィードバックの反映
- 要素型の制限(サイズ等)を追加
- スカラ演算子から有効な
std::simd
演算子を構築するための方法として推論(inferencing)を追加 - 型変換を追加
- オプトインではなくオプトアウトに変更
- 明示的なユーザー定義のストレージを削除
- 推論の例を追加
このリビジョンで追加されたスカラ演算子からの推論とは、要素型となるユーザー定義型で定義されている演算子(1要素の演算を行うもの)を検出してそれを使用するものです。これは、std::simd
による複数要素並列計算の結果は要素単位に同じ計算を適用した時の結果と一致するはず、という仮定に基づくものです。
例えば、std::simd
の二項加算演算子(operator+
)の実装は次のようになります
constexpr friend basic_simd operator+(const basic_simd& lhs, const basic_simd& rhs) requires (details::simd_has_custom_binary_plus || details::element_has_plus) { if constexpr (details::is_standard_simd_type) { // int, float, complex, etc. return details::plus(lhs, rhs); } else if constexpr (details::simd_has_custom_binary_plus) { // user customisation point return simd_binary_plus(lhs, rhs); } else { return details::element_wise_plus(lhs, rhs); // 👈 Infer from scalar operator } }
この3つ目のカスタマイズがスカラ演算子からの推論です。この場合、要素型に対して定義済みの演算子を使用してそれを要素毎に適用して計算を行います。ただし、これは最低ラインのサポートであり、必ずしも最適なSIMDコードにならない可能性があります。その場合は、2つ目のようにカスタマイズポイントにアダプトすることでより効率的なコード生成を補助することができます。
これによって、ユーザー定義型U
に対するstd::simd<U>
を有効化する場合に個々の操作をアダプトしなければならないようにすると(オプトインだと)過度に不便でありメリットが薄くなるとして、要素型の制限を満たす任意のU
に対してstd::simd<U>
はデフォルトで全ての操作が有効化されるようになります。それが望ましくない場合(std::atomic<T>
など)はオプトアウトのメカニズムによってstd::simd<U>
を使用不可能にするようにします。
P2967R1 Relocation Is A Library Interface
リロケーションサポートのためのライブラリ機能の提案。
以前の記事を参照
このリビジョンでの変更は
- アブストラクトの単純化
- 関数名を
relocate
からuninitized_relocate
に変更 - イテレータコンセプトを使用してオーバーロードを追加する
- 失敗しない関数には
noexcept
を付加 contiguous_iterator
ではない場合に範囲の重複のサポートを削除- 実装プロトタイプを追加
std::swap
の最適化をP3239に委ねる
などです。
P2971R2 Implication for C++
以前の記事を参照
R1での変更は
- §3 (Intuition): 拡張し、例を追加
- §4 (Properties): 追加
- §6 (Why Core?): 再構成、選択肢を増やして拡張
- §7 (Design): 再構成、設計選択について更新し、より深く記述する
- §9 (Implementation): 再構成、構文解析の問題に関する新しい議論を追加
- §10 (Core Wording): 更新された設計決定を反映、Annex Cを追加
などです。
このリビジョンでの変更は
- §1 (What Is Implication?): Adaの構文についての簡単な説明と、必要十分(条件)の説明を追加
- §8 (Library Impact): 2023年11月のLEWGのレビューを反映し、投票結果を追記
- §11 (Library Wording): LEWGレビューを反映
などです。
P2976R1 Freestanding Library: algorithm, numeric, and random
<algorithm>, <numeric>, <random>
にある一部の機能をフリースタンディング指定する提案。
以前の記事を参照
R1での変更は
- 機能テストマクロを追加する代わりに
__cpp_lib_freestanding_numeric
を更新 is_execution_policy
をフリースタンディング対象として追加- 並列アルゴリズムを
freestanding-deleted
としてマーク <random>
のストリーム演算子の仕様を改善し、戻り値を明確化
などです。
P2988R5 std::optional<T&>
std::optional
が参照を保持することができるようにする提案。
以前の記事を参照
- P2988R0
std::optional<T&>
- WG21月次提案文書を眺める(2023年10月) - P2988R1
std::optional<T&>
- WG21月次提案文書を眺める(2024年01月) - P2988R3
std::optional<T&>
- WG21月次提案文書を眺める(2024年02月) - P2988R4
std::optional<T&>
- WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は、value_or()
に関してP3091の変更を取り込んだことです。
template<typename T> class optional<T&> { ... // P3091からの宣言の例 template <class U = remove_cv_t<T>, class... Args> U value_or(Args&&...) const; template <class U = remove_cv_t<T>, class X, class... Args> constexpr U value_or(initializer_list<X>, Args&&...) const; ... }
P2996R3 Reflection for C++26
値ベースの静的リフレクションの提案。
以前の記事を参照
- P2996R0 Reflection for C++26 - WG21月次提案文書を眺める(2023年10月)
- P2996R1 Reflection for C++26 - WG21月次提案文書を眺める(2023年12月)
- P2996R2 Reflection for C++26 - WG21月次提案文書を眺める(2024年02月)
このリビジョンでの変更は
- リフレクション(鏡像)間の等価性と、鏡像によって特殊化されたテンプレートエンティティのリンケージについて詳細に説明
- TS時代の合意を復元するために
accessible_members_of()
を追加 value_of()
をextract()
にリネームし、関数を操作できるように拡張- 定数式ではなく、値とオブジェクトのリフレクションサポートを明確化
- clangの試験実装へのリンクを追加
can_substitute, is_value, is_object
及びvalue_of
(extract
とは別)を追加- 型特性の命名に関するissueを追記
- 名前付きタプルの別の実装例を追加
meta::info
のデフォルト/値/ゼロ初期化は、nullな鏡像を生成する- 実装されていたが以前の提案では省略されていたaddressed splicing(スプライシング結果のアドレス取得)を追加
- テンプレート引数をサポートするために、
reflect_invoke
にオーバーロードを追加 - 名前の衝突を回避するために、全ての型特性の名前を
type_
で始める - より汎用的な
is_const, is_final, is_volatile
を追加 is_noexcept
を追加is_explicit
をメンバ関数テンプレートではなくメンバ関数にのみ作用するように修正- handling textセクションを追加
- ODRに関する懸念事項についてのセクションを追加
などです。
P3045R1 Quantities and units library
物理量と単位を扱うライブラリ機能の提案。
以前の記事を参照
このリビジョンでの変更は
- 依存関係(提案)の表に1つ追加
- Safe unit conversionsの章を
value_cast()
を使用して拡張 - The affine spaceの章をほぼ書き直し
- Magnitudes章を追加
qp.quantity_from_zero()
はユーザーの指定した原点では動作しなくなったqp.quantity_from()
は他の量の点に対しても機能するようになったbasic_symbol_text
をsymbol_text
ヘリネームsymbol_text
から[[nodiscard]]
を削除- 文字列リテラルを受け取る
symbol_text
コンストラクタがconsteval
になった symbol_text
は常にシンボル文字列をchar8_t
とchar
で保持するようになった- UTF-8シンボルが使用される場合、
u8
リテラルを使用する必要がある - Symbols of derived dimensions節を追加し、シンボル生成に関する文章をSymbols for derived entities章にリファクタリング
- 数量のフォーマットをVictor Zverovichと合意した新しい構文にリファクタリング
mag<ratio{N, D}>
がmag_ratio<{N, D}>
に置き換えられ、ratio型がライブラリの公開インターフェースではなく実装詳細になった- ライブラリの最新の設計を反映するようにCompiler Explorerのリンクを更新
などです。
P3051R1 Structured Response Files
ツールが他のツールにコマンドラインオプションをファイルで引き渡す方法についての提案。
以前の記事を参照
このリビジョンでの変更は、SG15のフィードバックの適用(options
をarguments
に変更)などです。
P3059R1 Making user-defined constructors of view iterators/sentinels private
<ranges>
の内部イテレータ型のコンストラクタを非公開にする提案。
以前の記事を参照
このリビジョンでの変更は、この提案による破壊的変更についての議論を追加したことです。
MSVC STLおよびlibc++の開発者はいずれも、この変更による影響は大きくないと見積もっているようです。libstdc++の場合はそもそも実装が標準のものと異なっているため実質的に影響はないとしています。
P3064R1 How to Avoid OOTA Without Really Trying
C++コンパイラによる実装においては、OOTA問題が発生しないことを解説する文書。
以前の記事を参照
このリビジョンでの変更はよくわかりません(提案ではないので、何かしらの修正のみのはずです)。
P3067R0 Provide predefined simd permute generator functions for common operations
P2664で提案されている、std::simd
の汎用permute APIを設計し直す提案。
SIMDにおけるpermute命令とは、SIMDレジスタ内で要素を並び替える命令の1つです。ある計算の際に効率的なデータの並びとその後の別の計算の際に効率的なデータの並びが異なる場合など、SIMDレジスタ内にデータを載せたまま並べ替えが必要になることはよくあり、その際にpermute命令を使用できます。
P2664ではstd::simd
に対してpermute命令に対応する並べ替えを行うためのstd::permute()
を提案していましたが、P2664のstd::permute()
APIはほとんどのユーザーの最も一般的な要求をサポートするように汎用的に設計されていたため、必要なことは何でもできるもののよく使用される特定の並べ替えパターンをシンプルに記述することができず、ユーザーが個別にその操作を行う関数を定義する必要性が頻発することが想定されたようです。
この提案ではそのような並べ替え操作の目的やパターンをまとめて、この浮かび上がった要求を元にしてstd::simd
の要素並べ替えを行うフリー関数のセットを特定し提案するものです。
今のところ挙げられているものは次のものです
関数 | 説明 |
---|---|
take<int>(simd) |
simd の先頭からN 要素を含む新しいsimd オブジェクトを返す |
grow<int>(simd, value={}) |
入力されたsimd よりも大きいサイズで、指定されたvalue で初期化された新しい要素を含んだsimd を返す |
stride<int N>(simd) |
入力されたsimd のN 番目の要素のみを含む新しいsimd を返す |
chunk<int>(simd) |
既存のsimd::split を名前変更および移動したもの |
reverse(simd) |
入力されたsimd と同じサイズで、要素を逆順にした新しいsimd を返す |
repeat_all<int>(simd) |
入力されたsimd をN 回繰り返した新しいsimd を返す(例: repeat_all<3>([a, b]) は[a, b, a, b, a, b] になる) |
repeat_each<int>(simd) |
入力されたsimd の各要素をN 回繰り返した新しいsimd を返する(例: repeat_each<3>([a, b]) は[a, a, a, b, b, b ] になる) |
transpose<ROWS, COLS>(simd) |
入力されたsimd を、ROWS 行とCOLS 列の行優先の行列として扱い、転置した結果(つまり列優先にした)を返す |
zip(simd...) |
入力された複数のsimd の対応する要素を交互に並べて新しいsimd を生成する。(例: zip([a, b, c], [0, 1, 2]) は[a, 0, b, 1, c, 2] を返す |
unzip<N>(simd...) |
入力されたsimd をN 個のsimd に分割する。(例: unzip<2>([a, 0, b, 1, c, 2]) はmake_tuple([a, b, c], [0, 1, 2]) を返す |
cat(simd...) |
simd_cat の名前変更版 |
extract<N, M>(simd) |
[N...M) の範囲内の要素を含む新しいsimd を抽出する |
rotate<MIDDLE>(simd) |
simd の要素を左に回転し、MIDDLE インデックスの要素が最初の要素になるようにする |
shift_left<N>(simd)/shift_right<N>(simd) |
入力されたsimd と同じサイズで、要素を左または右にシフトし、値初期化された要素を挿入した新しいsimd を返す |
align<N>(simd a, simd b) |
2つのsimd オブジェクトを受け取り、cat(a, b) からsimd を抽出します |
これらの良く使用する並べ替え操作(順列のジェネレーター関数)をあらかじめ用意しておくことで
- 同じジェネレーター関数を繰り返し何度も重複定義することになるのを回避する
- よく使用する操作について、分かりやすい名前付き関数を提供することで、コードの可読性を向上させる
- よく使用する操作について、あらかじめ用意しておくことでバグの混入を回避する
等の利点があるとしています。
- P2664R6 Proposal to extend std::simd with permutation API - WG21月次提案文書を眺める(2024年01月)
- P2638R0 Intel's response to P1915R0 for std::simd parallelism in TS 2 - WG21月次提案文書を眺める(2022年09月)
- P3067 進行状況
P3068R2 Allowing exception throwing in constant-evaluation
定数式においてthrow
式による例外送出およびtry-catch
による例外処理を許可する提案。
以前の記事を参照
- P3068R0 Allowing exception throwing in constant-evaluation. - WG21月次提案文書を眺める(2024年02月)
- P3068R1 Allowing exception throwing in constant-evaluation - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- ライブラリの文言追加
- 新しい例を追加
- 実装の説明を追加
- 変更に影響について追記
などです。
P3085R2 noexcept
policy for SD-9 (throws nothing)
ライブラリ関数にnoexcept
を付加する条件についてのポリシーの提案。
以前の記事を参照
- P3085R0
noexcept
policy for SD-9 (throws nothing) - WG21月次提案文書を眺める(2024年02月) - P3085R1
noexcept
policy for SD-9 (throws nothing) - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- § 12 Appendix: Principled design exerciseに2024年東京会議で使用した原則的な設計例を追加
- § 4.3 Minimal noexceptに最小限の
noexcept
ポリシーについて追加 - § 8.3.4 Vectorizationで
noexcept
がベクトル化に与える影響について説明 - § 8.7.7 Don’t discard useful informationで、有用な情報を捨てないことについて説明
- § 8.3.2 Other implementationsでP3166R0について議論
- § 8.4.1.3 Allow contract build modes to change the behavior of noexceptで、P3205R0について議論
- § 8.4.1.2 Retrieve precondition and post-condition results via reflectionで、Bengt GustafssonのContract testing supportについて議論
- § 8.9 noexcept as Quality of Implementationで、
noexcept
を完全にQoIにすることについて説明 - § 8.4.8 Death testsでDejaGnuについて議論を追加
- § 8.3.3 Bloatで、"Can’t write it better yourself"について議論
などです。
P3091R2 Better lookups for map
and unordered_map
連想コンテナからキーによって要素を検索するより便利な関数の提案。
以前の記事を参照
このリビジョンでの変更は、P2988R4の内容(value_or
は常にprvalueを返す)を反映した事です。
などです。
P3094R2 std::basic_fixed_string
NTTPとして使用可能なコンパイル時文字列型であるstd::basic_fixed_string
の提案。
以前の記事を参照
- P3094R0
std::basic_fixed_string
- WG21月次提案文書を眺める(2024年02月) - P3094R1
std::basic_fixed_string
- WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- Design discussion章を拡張
- Open questions章を更新
- 実装経験の章にコンパイラエクスプローラのリンク追加
- Synopsis章の名前をWordingに変更し提案する文言として修正
traits
テンプレートパラメータのサポートを追加reverse_iterator
のサポートを追加- 文字列リテラルを受け取るコンストラクタを
consteval
にした range
コンストラクタを追加view()
メンバ関数を追加- コンテナと文字列のインターフェースを追加
- 文字列リテラルと単一文字で動作する二項
+
演算子を追加 - 文字列リテラルとの比較演算子を追加
array<charT, N>
からの推論補助を追加- debug-enabled string typeとして
basic_fixed_string
が追加されたため、formatter
特殊化を削除
などです。
P3096R1 Function Parameter Reflection in Reflection for C++26
C++26に向けた静的リフレクションに対して、関数仮引数に対するリフレクションを追加する提案。
以前の記事を参照
このリビジョンでの変更は
- 提案されたAPIのSynopsisを追加
- 提案する文言を追加
- 投票結果の追加
- 提案するソリューションの明確化
- 文言の改善
などです。
P3100R0 Undefined and erroneous behaviour are contract violations
UB(及びEB)を契約違反として扱うようにする提案。
C++言語を安全ではなくしている要因は実行時の未定義動作にあります。正しくコンパイルされたWell-definedなC++プログラムでも、実行時に任意の未定義動作に陥る可能性があり、未定義動作を回避できない場合それはプログラムのバグとして安全性/セキュリティ上の問題を引き起こします。特に、無効なメモリアクセスによる未定義動作は特に大きな問題で、GoogleやMicrosoftの調査によればセキュリティの脆弱性の7割が無効なメモリアクセスによる未定義動作によって発生しているとされます。
近年の安全性・セキュリティの重要性の高まりを受けて、C++プログラムが未定義動作を起こす可能性を減らすための様々な取り組みがなされています。例えば
- コードレビュー
- コーディングガイドラインの適用
- コードの様々場側面を対象にした自動テスト
- 静的解析ツールの使用
- 実行時のサニタイザーの使用
など、これらの手法を正しく適用することで、実行時に発生する可能性のある未定義動作を減らすことができます。
これらのアプローチとは異なり、C++標準化委員会はC++プログラムの未定義動作リスクを低減するために、言語そのものを進化させることができる唯一の立場にあります。C++における未定義動作は基本的に実行時の性質であることから主に2つのアプローチが可能です。
- 未定義動作に陥るコードパスを静的に検出できる場合、そのプログラムをill-formedにできる
- 未定義動作に陥るコードパスに実行時に到達した場合に取り得る動作の範囲を指定し、実行時にその動作を緩和する
この2つのアプローチは排他的なものではなく、相互に補完し合うことができます。
一方、現在C++26を目指して契約プログラミング機能の提案(P2900)が進行しています。契約プログラミング機能はC++言語の安全性を高める機能ではあるものの、万能薬ではなく、契約プログラミング機能はすべての未定義動作をコンパイル時に静的に検出可能にするようなものではありません。しかし、コンパイル時に検出されなかったすべての未定義動作に対して実行時にそれをハンドリングして処理するための包括的なフレームワークを、契約プログラミング機能は提供します。
この提案は、契約プログラミング機能を拡張して、未定義動作を契約違反としてハンドリングできるようにしようとするものです。
この提案のアプローチは、明示的な契約アサーションだけに契約セマンティクスを指定するのではなく、実行時に未定義動作に陥る可能性のあるコア言語構成要素に対して暗黙的に契約を指定することで、未定義動作に陥る場合を契約違反として扱えるようにし、それによって契約プログラミング機能のフレームワークの中で未定義動作をハンドリングできるようにすることを目指すものです。
例えば符号付整数の加算を考えてみます。2つのint
型の値a, b
を加算する場合、結果がint
の最大値を超える(オーバーフローする)場合は現在未定義動作とされていますが、これを再解釈して、符号付整数型の加算には加算結果がオーバーフローしないという暗黙の事前条件ある、とすることができます。
符号付整数型の加算が組み込みのoperator+(int, int)
を呼び出して行われると考えると、その組み込み演算子は次のように宣言されているかのように扱われます
int operator+(int a, int b) pre ((b >= 0 && a <= INT_MAX - b) // 暗黙の事前条件アサーション || (b < 0 && a >= INT_MIN - b));
この事前条件アサーションを「暗黙的」としているのは、通常の契約注釈がユーザーの手によって明示的に付加されるのに対して、この事前条件アサーションはコンパイラが暗黙的に生成して付加するものだからです。
この符号付整数型の加算オーバーフローと同様に、「操作Xについて、Yが偽(false
)の場合、動作は未定義」というようなC++標準における未定義動作の出現を、「操作Xは、Yが真(true
)であるという事前条件を持つ」というように置き換えることができて、明示的な(通常の)契約注釈同様にこの暗黙の契約注釈もまとめて契約プログラミング機能の上で扱うことができるようになります。
こうして実行時にハンドルされうる未定義動作には2つの種類があり、それは、安全なフォールバック動作を定義できるものとできないものです。例えば、符号付整数型の加算のオーバーフローの安全なフォールバック動作はその結果の値が有効な数値になることです。その具体的な数値の性質(飽和演算、ラップアラウンドなど)を指定する代わりに、規格のレベルで未定義動作を解除するには、有効だが未規定な値になると規定するだけで十分です。この数値を計算で使用するとおそらく間違い(バグ)ではあるものの未定義ではなくなります。整数の加算を上記のように組み込み演算子とみなすと、この安全なフォールバック動作はその本体が実行する動作そのものです。
別の例として、未初期化変数を読み取る場合、未定義動作の代わりの安全なフォールバック動作として代わりに未規定だが有効な値(例えばゼロ)を読み取る、とすることで未定義動作を安全に解除することができます。この話をどこかで聞いたことがある人もいるかもしれません、これはP2795R5によって提案されC++26に既に導入されている、Erroneous Behaviour(EB)そのものです。
EBはUB時の動作として安全なフォールバック動作を実装定義で指定し、同時に診断を推奨することでUBを解消しようとする取り組みです。P2795R5で誤ったコードの結果として動作Xを取る、としているところは、この提案において、暗黙の事前条件アサーションが破られた場合の安全なフォールバック動作がXである、というのと全く同じことです。暗黙の契約アサーションがチェックされたかどうかとは関係なく、どちらの場合も誤った状態にあるプログラムの動作について述べており、なおかつその動作は未定義ではなくWell-definedとなります。
このように、P2795R5の「Erroneous Behaviour」とこの提案の「暗黙の契約アサーション違反」という2つの言葉には互換性があります。
そして、P2795R5によればErroneous Behaviourが発生した場合、実装は次のいずれかを行うことが許可されます
- 問題の報告
- プログラムの終了
- 何もしない
- ただしUBではない
EB時の可能な動作のこのリストは、契約違反が起きた場合の4(+1)つの契約注釈のセマンティクスのサブセットです(quick_enforce, enforce, observe, ignore, assume)。
- 診断を発行 -> observe
- プログラム終了 -> quick_enforce/enforce
- 何もしない -> ignore
- 安全なフォールバック動作を実行する
逆に、EBのフレームワークにはこのような欠陥のハンドリングに役立つ機能が欠けています。契約プログラミング機能では、契約違反時に違反ハンドラが呼びされ、なおかつこの違反ハンドラはユーザーがカスタマイズできます。EBでは安全なフォールバック動作のない未定義動作を処理できませんが、契約の場合は可能です。
そして、この提案ではC++23以前(契約もEBもない時代)との完全な下位互換を保つオプションとして、契約注釈のセマンティクスにassumeセマンティクスを追加することを提案しています。これは[[assume]]
属性同様に契約注釈をコードの仮定として利用することが許可されており、なおかつ契約条件はチェックされず違反ハンドラは呼び出されません。契約違反が起こると未定義動作になります。
assumeセマンティクスは従来(現在)のC++と同じ挙動であり、パフォーマンスをとにかく重視する場合に選択できるものです。これは契約プログラミング機能のオーバーヘッドを取り除き、動作とパフォーマンスの両面で現在のC++23以前の動作との後方互換性を保つためのものです。EBの場合、過去の動作に戻すためにEB個別にオプトアウトメカニズム(例えば[[indeterminate]]
)が必要となり、規格の複雑さの面でもユーザー負担の面でもスケールしません。
その概念に互換性があり、動作状態を包含しており、C++23以前との後方互換性の一括確保も備えていることから、この提案はErroneous Behaviourを完全に包含しているといえます。そのためこの提案では、EBもまたUB同様に暗黙の契約アサーションをもつものとして扱うことを提案しています。
2種類の未定義動作のもう一つ、安全なフォールバック動作を定義できないタイプの未定義動作に対して暗黙的に付加された契約の事をこの提案では、無視できない契約アサーション(Non-ignorable implicit contract assertions)と呼んでいます。
例えば、配列の添え字アクセスを先程と同様に組み込み演算子としてみてみると次のようになります
template <typename T, size_t N> T& subscript(T(&array)[N], size_t index) pre(index < N) // 暗黙の事前条件アサーション { return array[index]; }
この場合、この暗黙の契約アサーションが破られたときにその本体の動作を安全なフォールバック動作として扱うことはできません。メモリの安全性や型の安全性に違反する操作はこのカテゴリに分類されます。
無視できない契約アサーションに対する契約注釈のセマンティクスは制限され、ignore/observeセマンティクスで評価することができません。指定可能なセマンティクスはquick_enforce/enforce/assumeのみです。これらの評価セマンティクスは例えば、次のような実装戦略にマッピングできます
- quick_enforce : 範囲外アクセスを実行時にトラップするclangの-fbounds-safety
- enforce : アドレスサニタイザーによる実行時の検出と診断発行、およびプログラム中断
- assume : C++23以前(現在)の安全ではないデフォルトの動作
安全なフォールバック動作を持たないC++の未定義動作に対しても、同様に無視できない契約アサーションを持つものとして再解釈することで、同様のマッピングが存在します。
ここでは、契約セマンティクスのマッピングの1つとしてサニタイザーのような外部ツールがあげられています。C++プログラムのためのサニタイザーはいくつかの実装がすでに提供されていますが、いずれの実装もユーザーコードと対話するためのAPI(コールバック)が非常に貧弱であり、その統一的な指針のようなものも存在していません。
この提案では、サニタイザーも含めた外部ツールのユーザーコードとの対話APIとして、契約プログラミング機能の違反ハンドラを使用するようにするアイデアを挙げています。現時点の契約違反ハンドラは現在のサニタイザーが備えるコールバックAPIよりも充実(ユーザーが置換可能、引数で詳細な情報を取得可能)しており、この包括的なAPIによってサニタイザーをC++標準の範囲内に配置することができます。
また、このAPIのサポートは既存のサニタイザーに対して変更を強いるものではありません(そうするかは完全にオプションです)。現在の契約プログラミング機能の仕様では、デフォルトの契約違反ハンドラの動作は実装定義であり、ユーザーが違反ハンドラをカスタムすることを許可しなくても良いとされているため、既存のサニタイザーはすでにそのようなAPIに準拠していると言えます。
P3103R2 More bitset operations
<bit>
にあるビット操作関数に対応するメンバ関数をstd::bitset
にも追加する提案。
以前の記事を参照
- P3103R0 More bitset operations - WG21月次提案文書を眺める(2024年02月)
- P3103R1 More bitset operations - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は、P3104R2への言及を追加、§4 Design considerationsを拡張したことなどです。
P3111R0 Atomic Reduction Operations
std::atomic
にアトミックリダクション操作を追加する提案。
アトミックリダクション操作とは、アトミックなRMW操作(fetch_add()
のような操作)ではありますが古い値を実際にはフェッチせず、メモリモデルの観点からも読み取りを行わないようなRMW操作の事です。
そのような、フェッチされた古い値を破棄するようなアトミックRMW操作を実行する並行アルゴリズムは高性能コンピューティングでは一般的であり、現代のハードウェアはそのような操作に対して効率的な命令を備えています。
例えば次のコードは並列にヒストグラムを求めるものです
std::span<unsigned> data; std::array<std::atomic<unsigned>, N> buckets; constexpr T bucket_sz = std::numeric_limits<T>::max() / (T)N; unsigned nthreads = std::thread::hardware_concurrency(); std::for_each_n(std::execution::par_unseq, std::views::iota(0).begin(), nthreads, [&](int thread) { unsigned data_per_thread = data.size() / nthreads; T* data_thread = data.data() + data_per_thread * thread; for (auto e : span<T>(data_thread, data_per_thread)) { // ここでアトミック値に対して足しこみ(fetch_add())が行われている buckets[e / bucket_sz].fetch_add(1, std::memory_order_relaxed); } });
このコードには次の2つの問題点があります
- 正しさ(UB)
- アトミック操作が次のいずれも満たしていないため、この並列版
for_each_n
ではexecution::par
を使用するべきだった(par_unseq
を使用しているためにUB)- 潜在的に同時実行され、シーケンス化されていないコンテキストでデータ競合が発生する
- 他の関数呼び出しと同期するため、ベクトル化セーフ
- アトミック操作が次のいずれも満たしていないため、この並列版
- パフォーマンス
- このプログラムでアトミックリダクション操作を使用するようにするためには、高度なコンパイラの分析が必要
std::atomic
でアトミックリダクション操作をサポートすることでこの2つの問題を解決でき、この提案はそれを提案するものです。
この提案では、既存のstd::atomic
およびstd::atomic_ref
のメンバ関数である.fetch_xxx()
に対してvoid
戻り値型のxxx()
メンバ関数を追加することを提案しています。
提案文書より、サンプルコード
現在 | この提案 |
---|---|
#include <algorithm> #include <atomic> #include <execution> using namespace std; using execution::par_unseq; int main() { size_t N = 10000; vector<int> v(N, 0); atomic<int> atom = 0; for_each_n(par_unseq, v.begin(), N, [&](auto& e) { // UB+SLOW: atom.fetch_add(e); }); return atom.load(); } |
#include <algorithm> #include <atomic> #include <execution> using namespace std; using execution::par_unseq; int main() { size_t N = 10000; vector<int> v(N, 0); atomic<int> atom = 0; for_each_n(par_unseq, v.begin(), N, [&](auto& e) { // OK+FAST atom.add(e); // 👈 }); return atom.load(); } |
また、この延長として、順序付けされていないときの(並列実行時の)読み取りでないアトミックメモリ操作を許可し、浮動小数点演算が結合的であると仮定して浮動小数点数型の算術リダクション操作を拡張することも追加で提案しています。
浮動小数点数の演算のように非結合的な演算では、並列に実行される演算の順序が結果に影響を与える可能性があるため、アトミックリダクション操作の効果が制限されます。例えば、x = a + (b + c)
という計算を並列に実行する場合、アトミックリダクション操作では(a + b) + c
やa + (c + b)
などの異なる順序で計算される可能性があり、浮動小数点演算が結合的ではないことから結果が一致しない可能性があります。この提案の一般化アトミックリダクション操作では、浮動小数点数型のアトミックリダクション操作において浮動小数点演算が結合的であると仮定して実行することを許可することで、実装はこの制限を受けずに最適化が可能になります。
この提案では、一般化アトミックリダクション操作は浮動小数点数型の特殊化のみのメンバ関数として、上記のアトミックリダクション操作とは別名のメンバ関数として追加することを提案しています
template <floating-point> class atomic { ... // アトミックリダクション操作 void add(floating-point, memory_order); // 一般化アトミックリダクション操作 void add_generalized(floating-point, memory_order); ... };
P3119R1 Tokyo Technical Fixes to Contracts
現在のContarcts仕様の小さな問題を解決する提案。
以前の記事を参照
このリビジョンでの変更は
- 無制限の評価について更新
- 提案1(配列引数と事後条件)と提案2(Cの可変長関数引数と事前/事後条件)についての投票結果を追加
などです。
P3125R0 Pointer tagging
タグ付きポインタをサポートするためのライブラリ機能の提案。
タグ付きポインタとは、ポインタのビットのうち使用されていない部分に情報を埋め込むこんで利用しようとするものです。主に、メモリ安全性の向上のために使用されていますが、これを利用した特殊なデータ構造もあるようです。
現代のC++の環境の多くはポインタのサイズは64bitですが、現状のメモリ容量の場合そのメモリ空間をすべて表現するのに64bitもの長さは必要ありません。アーキテクチャによるようですが、48bit目くらいまでしか使われていないようです。すると、余った上位16bitを無視するようにしてもらえば、その部分を何かしら利用することができます。x64/arm共にこのためのCPU拡張を備えています。
さらに、アドレスがNバイトにアラインされている時そのアドレスはNの倍数になるため、その分の下位数ビット(log2(N)
ビット)も同様に利用できます。例えば、int
型が4バイトの場合int
型変数のアドレスは通常4の倍数の値になるため、下位2ビットはポインタ値に参加していません。
しかし、C++の規格としては有効なポインタの一部のビットを操作することは未定義動作であり、タグ付きポインタは許可されていません。この提案は、特殊なライブラリ関数を通してのみそのような操作を認めるようにしようとするものです。
提案しているのはまず、ポインタからタグ部分を取り出すビットマスクを返す関数です。
namespace std { template <typename Pointee, size_t Alignment = alignof(Pointee)> constexpr auto tag_bit_mask() noexcept -> uintptr_t; }
このtag_bit_mask()
はポインタ型Pointee
とアライメントAlignment
に対して、そのポインタ型のポインタ値のうちタグとして使用可能な部分のビットが1になったビットマスクを返します。下位のビットはアーキテクチャによらず共通(アライメントで決まる)ですが、上位ビットがどこまで利用可能かはアーキテクチャおよびサニタイザーの存在によって変化します。
次に、タグ付きポインタを表す型std::tagged_pointer<T, Alignment>
を用意して、これに対してタグ付け、タグのクリア、タグに保存されている値の取り出しを行う関数を提案しています。
namespace std { // ポインタにタグ付けを行う template <typename T, size_t Alignment = alignof(T)> constexpr auto tag_pointer(T* original, uintptr_t value) noexcept -> tagged_pointer<T, Alignment>; // タグ付きポインタを元のポインタに戻す template <typename T, size_t Alignment = alignof(T)> constexpr auto untag_pointer(tagged_pointer<T, Alignment> ptr) noexcept -> T*; // タグ付きポインタからタグの値を取り出す template <typename T, size_t Alignment = alignof(T)> constexpr auto tag_value(tagged_pointer<T, Alignment> ptr) noexcept -> uintptr_t; }
この提案のタグ付きポインタサポートは特殊なデータ構造(Hash-Array-Mapped-Trie (HAMT)など)の作成を許可することに重きを置いているため、これらの関数はconstexpr
が付加されています。
std::tagged_pointer<T, alignment>
はvoid*
と同じサイズでvoid*
に変換できる必要があるという要件は定まっているものの、それを何にするか(void*
、T*
、クラス型など)はまだ決まっていないようです。
- Memory Tagging Extension: Enhancing memory safety through architecture
- 64bit環境におけるObjective-Cのポインタ | GREE Engineering
- HWAddressSanitizer | Android Open Source Project
- 57-bits の仮想メモリアドレス空間と新機能 UAI が実装される将来の AMD プロセッサ | Coelacanth's Dream
- P3125 進行状況
P3126R1 Graph Library: Overview
グラフアルゴリズムとデータ構造のためのライブラリ機能の提案の概要をまとめた文書。
以前の記事を参照
このリビジョンでの変更は、報告されて対処中の問題についてまとめるためのIssues Statusセクションを追加したことです。
P3130R1 Graph Library: Graph Container Interface
グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、グラフの実体となるコンテナのインターフェースについてまとめた文書。
以前の記事を参照
このリビジョンでの変更は
num_edges(g)
とhas_edge(g)
関数を追加- 関数の表が大きくなり過ぎたので、グラフ・頂点・エッジの3種類に分割
- グラフコンテナインターフェースから、Load Graph Dataセクションとそれに関する関数を削除
compressed_graph
の実装経験を反映して、partition関数を改訂し使用法を反映- 他の名前と一致するように、
partition_count(g)
をnum_partitions(g)
に名前変更 partition_id(g,u)
をpartition_id(g,uid)
に変更- この関数が呼ばれたときに頂点が存在しない場合があるため
edges(g,u, pid)
を削除- ターゲット頂点が異なるパーティションにある可能性がある場合、
<ranges>
の機能を利用して簡単にフィルタを実装できるため
- ターゲット頂点が異なるパーティションにある可能性がある場合、
- 他の名前と一致するように、
などです。
P3131R1 Graph Library: Containers
グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、グラフの実体となるコンテナ実体についてまとめた文章。
以前の記事を参照
このリビジョンでの変更は
- 典型的CSR実装の先にある
compressed_graph
の機能概要を追加 compressed_graph
のnum_edges(g)
とhas_edge(g)
に計算量の指定を追加- P3130R0のロード関数削除を補完するために、
compressed_graph
に- オプションの
partition_start_ids
パラメータを踏む
- オプションの
などです。
P3137R1 views::to_input
入力の範囲をinput_range
に弱めるRangeアダプタ、views::input
の提案。
以前の記事を参照
このリビジョンでの変更は、sized_sentinel_for
である場合にoperator-
(二項)を追加したこと、このアダプタの分割に関する議論の追加などです。
P3138R1 views::cache_last
入力範囲の現在の要素をキャッシュするRangeアダプタ、views::cache_last
の提案。
以前の記事を参照
このリビジョンでの変更は、const
メンバ関数のスレッドセーフ性保証の例外をこの提案のアダプタに関連するもののみに留めたことです。
P3139R0 Pointer cast for unique_ptr
std::const_pointer_cast
とstd::dynamic_pointer_cast
にstd::unique_ptr
のオーバーロードを追加する提案。
std::uniqur_ptr
に対してポインタのキャスト操作を行うことはそのままではできず、現在のベストプラクティスは、一旦.release()
してポインタを取り出した後、そのポインタに対してポインタキャストを適用してから、結果のポインタを再びunique_ptr
でラップする、という手順です。
しかし、この手順は一旦リソースの所有権を手放す必要があるなど、リソース安全な方法とはいえません。
auto GetClient() -> std::unique_ptr<const Client>; // constを外したい voif example() { std::unique_ptr<Client> client; // 生ポインタが露出する client.reset(const_cast<Client*>(GetClient().release())); .. } // dynamic_castを適用したい void UseV2Client(std::unique_ptr<Client>&& client) { std::unique_ptr<ClientV2> v2; ... // dynamic_castが失敗するとリソースリークする v2.reset(dynamic_cast<ClientV2*>(client.release())); ... }
一方で、std::shared_ptr
にはそのキャストのためのフリー関数であるstd::const_pointer_cast
とstd::dynamic_pointer_cast
が用意されており、これを用いるとこのような危険性を回避することができます。また、Boostにはunique_ptr
に対するstd::const_pointer_cast
とstd::dynamic_pointer_cast
オーバーロードが用意されています。
std::uniqur_ptr
のポインタキャストという操作をより安全にするために、std::uniqur_ptr
でもstd::const_pointer_cast
とstd::dynamic_pointer_cast
を使用できるようにする提案です。
方法としては単純に、std::const_pointer_cast
とstd::dynamic_pointer_cast
にstd::uniqur_ptr
を受け取るオーバーロードを追加することでこれを行おうとしていますが、std::unique_ptr
特有の事情により少し設計が異なる部分があります。
1つは提案するオーバーロードはstd::uniqur_ptr
の右辺値に対してのみ作用することで、もう1つはカスタムデリータの考慮です。std::shared_ptr
の場合はカスタムデリータは型消去されて保持されており、キャストの前後でも一貫して元のポインタに対して作用するようにすることで、デリータを考慮する必要はありません。しかし、std::unique_ptr
の場合はデリータは型の一部であり、キャストにあたってデリータを考慮する必要があります。
std::unique_ptr<T, D>
をstd::unique_ptr<U>
にキャストする場合、D
がキャスト後のポインタを削除できるかどうかを考える必要があります。これは単純にデフォルトのデリータを使用している場合にのみその仮定が成り立ちます。一方、std::unique_ptr<T, D>
はD
を通してポインタ型をカスタマイズすることができ、保持するものは実際にはポインタではない場合もあります。この場合、ユーザーが指定したD
はキャスト後のリソースを正しく開放できる場合もあります(これはユーザーが知っています)。
そのため、この提案ではこの2つのユースケースをサポートします。すなわち、キャストは次の2パターンが可能です
std::unique_ptr<T>
->std::unique_ptr<U>
の変換std::unique_ptr<T, D>
->std::unique_ptr<U, D>
の変換
提案されている関数
namespace std { // dynamic_cast template<class T, class U> constexpr unique_ptr<T> dynamic_pointer_cast(unique_ptr<U>&& r) noexcept; template<class T, class D, class U> constexpr unique_ptr<T, D> dynamic_pointer_cast(unique_ptr<U, D>&& r) noexcept; // const_cast template<class T, class U> constexpr unique_ptr<T> const_pointer_cast(unique_ptr<U>&& r) noexcept; template<class T, class D, class U> constexpr unique_ptr<T, D> const_pointer_cast(unique_ptr<U, D>&& r) noexcept; }
この提案のキャスト設計においては、安全であることを重視しています。そのため、この2種類以外のポインタキャスト(static_cast
とreinterpret_cast
)に対応する関数は専門知識が必要であり気軽に使用できるものではないとして提案していません。
また、キャストに当たっては静的にその危険性を検出できるものについてはチェックして弾くようにしています。次の表は、この提案のAPIに追加されているガードレールを表したものです
API | 1の変換 | 2の変換 |
---|---|---|
const_pointer_cast |
T* -> U* へのconst_cast が有効であること |
変換元の::pointer から変換先の::pointer へのconst_cast が有効であり、T, U はどちらも配列型であるかどちらも異なっている |
dynamic_pointer_cast |
T* -> U* へのdynamic_cast が有効であり、U は仮想デストラクタを持つ |
変換元の::pointer から変換先の::pointer へのdynamic_cast が有効であり、T, U はどちらも配列型ではない |
表の右列の「変換元の::pointer
」とはstd::unique_ptr<T, D>::pointer
のことで、「変換先の::pointer
」とはstd::unique_ptr<U, D>::pointer
のことです。
P3149R3 async_scope -- Creating scopes for non-sequential concurrency
P2300のExecutorライブラリについて、並列数が実行時に決まる場合の並行処理のハンドリングを安全に行うための機能を提供する提案。
以前の記事を参照
- P3149R0 async_scope -- Creating scopes for non-sequential concurrency - WG21月次提案文書を眺める(2024年02月)
- P3149R2 async_scope -- Creating scopes for non-sequential concurrency - WG21月次提案文書を眺める(2024年04月)
このリビジョンでの変更は
- サンプルコードを例外安全になるように修正
- async scopeコンセプトをscopeとtokenに分割し、
counting_scope
を更新して一貫させる counting_scope
の名前をsimple_counting_scope
に変更- stop sourceを持つスコープに`conting_scop`という名前を付ける
let_with_async_scope
とconting_scop
を利用して、再帰的に生成された例を追加
などです。
P3154R1 Deprecating signed character types in iostreams
iostreamのストリーム入力/出力演算子のsigned/unsigned char
オーバーロードを非推奨にする提案。
以前の記事を参照
このリビジョンでの変更は
- 影響についての調査の追加
- モチベーションのコード例を変更
- Cの
char8_t
に関する注意を追加 - P0487R1に関する注記を追加
などです。
P3157R1 Generative Extensions for Reflection
静的リフレクションをより有用にするための機能拡張についての提案。
以前の記事を参照
このリビジョンでの変更は明確ではありませんが、おおよそ次の部分が異なっています
- CLOSURE REFLECTIONとCOMPLETE AND ACCURATE REFLECTION OF REPRESENTATIONセクションの削除
- Function Descriptor MetafunctionsとEmbedded Domain Specific Languagesセクションの追加
- 特に、Function Descriptor Metafunctionsセクションでは、関数リフレクションのための具体的なメタ関数が提案されています
- サンプルコードや解説の改善
- 参考文献の更新
などです。
セクションが削除された理由は明確ではありませんが、CLOSURE REFLECTIONに関しては別の提案(P3273)に分離されたようです。この提案は関数リフレクションの強化の方向に特化することにしたようです。
P3175R1 Reconsidering the std::execution::on
algorithm
↓
P3175R2 Reconsidering the std::execution::on
algorithm
P2300のstd::execution::on
アルゴリズムの命名について再考する提案。
以前の記事を参照
R1での変更は
write_env
アダプタを説明専用にしたfinally, unstoppable
アダプタを削除schedule_from
およびlet_*
アルゴリズムへの変更を元に戻す
などです。これらの内容は別の提案(P3284)へ分離されています。
このリビジョンでの変更は
on
アルゴリズムに未規定のタグ型を与えて、カスタマイズを抑制する- これについての説明を行うセクションを追加
on
カスタマイズに厳密な制約を設けて、正しいセマンティクスを持つようにするstart_on, continue_on
の命名に関する説明を追加
などです。
この提案のR3はすでに2024年7月の全体投票をパスしてC++26WDに導入されています。
P3179R1 C++ parallel range algorithms
RangeアルゴリズムをExecutionPolicy
に対応させる提案。
以前の記事を参照
このリビジョンでの変更は
通常のアルゴリズム関数は出力先について出力イテレータのみを要求し、その番兵を必要としません。このリビジョンでは、ExecutionPolicy
を取る並列Rangeアルゴリズムが出力を行う場合、出力先はrange
であるか、番兵の指定を必須にしました。これは、並列アクセスにておいては出力先のサイズが分かっていたほうが効率化できる場合があるためです。
P3183R1 Contract testing support
関数の契約注釈だけを実行してテストを行う特別な関数の提案。
以前の記事を参照
このリビジョンでの変更は
- テスト関数を
constexpr
指定 - コンストラクタ、デストラクタ、及び演算子オーバーロードをテスト可能にするために、P3312R0への参照を追加
void
戻り値型関数の事後条件をテストするための特別なcheck_postconditions_void()
を追加
などです。
P3210R1 A Postcondition is a Pattern Match
事後条件の構文をパターンマッチングの構文に親和させる提案。
以前の記事を参照
このリビジョンでの変更は、Editorialな修正のみのようです。
この提案の方向性はSG21で好まれなかったようで、リジェクトされています。
P3214R0 2024-04 Library Evolution Poll Outcomes
2024年4月に行われた、LEWGにおける投票の結果
次の9つの提案が投票にかけられ、リジェクトされたものはありません
- P2985R0 A type trait for detecting virtual base classes
- P2019R5 Thread Attributes
- P2855R1 Member customization points for Senders and Receivers
- P3168R1 Give std::optional range support
- P2075R5 Philox as an extension of the C++ RNG engines
- P2927R2 Observing exceptions stored in exception_ptr
- P2997R1 Removing the common reference requirement from the indirectly invocable concepts
- P2389R1 dextents Index Type Parameter
- P3201R1 LEWG nodiscard policy
P3201R1とP3201R1以外は、C++26に向けてLWGに転送するための投票です。
また、投票に当たって寄せられたコメントが記載されています。
P3228R1 Contracts for C++: Revisiting contract check elision and duplication
契約条件の評価回数の規定をどうするかについて、ユースケースとソリューションをまとめて比較する文書。
以前の記事を参照
このリビジョンでの変更は
- “Side effects”セクションを追加し、それに応じて他のセクションを再構成
- 最新のSG21の議論を反映するように更新
などです。
“Side effects”セクションでは契約プログラミング機能における副作用についてのこれまでの議論や現在得られている見解がまとめられています。それによってボリュームが増していますが、評価回数の検討などに関してはR0から変化がありません。
P3234R1 Utility to check if a pointer is in a given range
ポインタがあるメモリ範囲の内側にあるかどうかを調べるpointer_in_range()
の提案。
以前の記事を参照
このリビジョンでの変更は、ポインタ範囲をspan
で受け取らない理由の説明(ポインタ2つを受け取るコンパイラの組み込み関数を使うので、ポインタを直接受け取るのが一番最適化を阻害しない)を追加したことです。
P3235R0 std::print
more types faster with less memory
std::print
の効率的な実装をより拡大して適用する提案。
P3107ではstd::print
に対して、std::format()
を使用して一旦std::string
を得てから出力するのではなく、基底のストリームバッファをロックして直接書き込むようにすることでstd::print
の出力動作を効率化しました。ただし、これはフォーマッタ内でロックを取得するようにしている場合にその型の値に対してstd::print
を同じロックの下で呼び出す場合にデッドロックを引き起こす可能性があったため、std::enable_nonlocking_formatter_optimization
変数テンプレートの特殊化をtrue
で定義する場合にのみ有効になるようにされています。
// フォーマットしたい型 struct foo {}; // fooのためのformatter特殊化 template <> struct std::formatter<foo> { ... }; // P3107の効率的な実装を有効化する template <> constexpr bool std::enable_nonlocking_formatter_optimization<foo> = true;
P3107では、このオプトインを基本型(int
等組み込みの型)と文字列型(std::string/std::string_view
)に対してのみ用意しており、他の標準型のフォーマッタに対しては有効化していませんでした。
この提案は、これら以外のフォーマッタ提供済みの標準型に対してもこのオプトインを有効化しようとするものです。
追加を提案しているものは次のものです
<chrono>
関連型- ただし、
std::zoned_time
はデフォルトのTimeZonePtr
に対してのみ提供
- ただし、
std::thread::id
<stack_trace>
関連std::filesystem::path
std::pair/std::tuple
- 要素型のすべてがオプトイン済みである場合に有効化
range
関連std::vector<bool>
とコンテナアダプタを含む- 要素型によらずに有効化
- 反復処理の場合はデッドロック発生の可能性が低いと考えられるため
また追加の提案として、現在のstd::vprint_(non)unicode
をstd::vprint_(non)unicode_buffered
に、std::vprint_(non)unicode_locking
をstd::vprint_(non)unicode
に変更することも提案しています。これは、非ロックオーバーロード(現在のstd::vprint_(non)unicode
)が最終的な書き込み時にstd::vprint_(non)unicode_locking
を呼び出すため、命名が誤解を招くということで修正を提案するものです。修正後の名前は、この2つの関数の違いが出力をバッファリング(全体を文字列化してから出力を行う)して行うかどうかという点が明確になります。
この提案C++23へのDRとして、2024年6月の全体会議で採択されています。
- P3107R0 Permit an efficient implementation of
std::print
- P3107R5 Permit an efficient implementation of
std::print
- P3235 進行状況
P3236R1 Please reject P2786 and adopt P1144
P2786の提案するtrivial relocatabilityをリジェクトすべきとする提案。
以前の記事を参照
このリビジョンでの変更は、著者(というか賛同者)が追加されたことなどです。追加されたのは、Follyのメンテナの方のようです。
P3238R0 An alternate proposal for naming contract semantics
違反ハンドラを呼び出さずに終了する契約セマンティクス(quick_enforce)を、Erroneous Behaviorとして扱うようにする提案。
ここで提案されているのは次の2つの事です
- 契約違反時に違反ハンドラを呼び出さず即時終了するセマンティクス(quick_enforce)を持つ契約注釈の契約違反時の動作は、Erroneous Behaviorであると指定する
- その安全なフォールバック動作として、違反ハンドラを呼び出す
- 契約セマンティクスの名前を変更する
- ignore -> ignored
- enforce -> enforced
- observe -> observed
- quick_enforce -> erroneous
契約違反時に即時終了するセマンティクスにおいては契約違反は致命的な失敗であり、他の方法でこの動作をフックすることはできません。これをEBとして指定することで、そのまま即時終了することと、安全なフォールバック動作として違反ハンドラを呼び出して簡単な診断を発行して終了することの両方を実装に対して許可することができます。
次にこのことを踏まえたうえでquick_enforceに対する適切な名前を考えます。他の3つの契約セマンティクスの名前は概ね「~ the trueness of a predicate」(「述語の真偽を~する」)の~の部分に当てはまるようになっていますが、quick_enforceはそうではありません。この提案ではそこに当てはまるような名前を考えるのではなく、意味上のerroneousという単語を他の名前になっている動詞の過去形と同じ文法的位置に置くようにしています。すなわち、「A contract violation is treated as ~」(契約違反は、~として扱われる)の~に入るように他の3つの名前を変更し、動詞の過去形にすることを提案しています。
この提案の内容はSG21のレビューにおいて好まれませんでした。
P3239R0 A Relocating Swap
リロケーションを用いたswap
を行う、swap_representations
の提案。
swap
のセマンティクスは2つのオブジェクトのオブジェクト表現を交換することです。その操作は実際には、ムーブコンストラクタとムーブ代入演算子(とデストラクタ)によって実行されます。
オブジェクト表現の交換という部分に注目するとswap
の行うことはリロケーションが行うこととよく似ていることに気づきます。特に、トリビアルリロケーション可能な型ならswap
は単純なmemcpy
によって実行できます。これによって、swap
のパフォーマンスを向上させることができます。
しかし、トリビアルなリロケーションのような操作によって2つのオブジェクトの表現を入れ替えることは、C++の非トリビアルな型のオブジェクトについての生存期間のルールに抵触し、トリビアルリロケーション導入後においても許可されません。
この提案は、リロケーションによるswap
を行う専用の関数をライブラリに追加し、それを通してのみリロケーションによるswap
を合法化することを提案するものです。
提案されているのは、std::swap_representations()
という関数です。同じ型の2つのオブジェクトへの参照を受け取って、それらのオブジェクトの値表現を交換しますが、どちらのオブジェクトの生存期間も終了しません。リロケーション操作を用いるとソースオブジェクトは消えてしまうため通常それは不可能です。そのため、この関数は一種の魔法のように動作する特別な関数です。ただし、安全のため、入力の型T
はトリビアルリロケーション可能(is_trivially_relocatable_v<T>
がtrue
)でなければなりません。
namespace std { // 提案するswap_representationsの宣言 template <bool force, class T> void swap_representations(T& a, T& b); }
テンプレートパラメータのforce
はT
がポリモルフィックな型の場合にのみ意味を持ちます。ここでの引数は参照であるため、T
を基底クラスとした派生クラスオブジェクトへの参照が入力される可能性があります。もし、a
とb
が同じ基底クラスT
から派生する別の派生クラスの参照である場合、これは未定義動作になります(a
とb
の仮想関数テーブルが異なっているため)。
この関数内からは参照で渡されたものしか見えないためそれを静的に検出できませんが、swap_representations
利用者が予めこの危険性がない参照を渡すことを知っている場合(例えば、std::vector
内部で要素型がT
そのものであることが分かっている場合など)に、force
パラメータをtrue
にして呼び出すことでポリモルフィックな型の値表現を交換できます。逆に、is_polymorphic_v<T>
がtrue
の場合にforce
がfalse
と指定されていると、コンパイルエラーになります。
また、std::swap
がプログラムの観測可能な動作を変更することなくこれを用いて実装可能であることを示す型特性std::swap_uses_value_representations_v
を用意することも提案しています。型T
のオブジェクトについてのstd::swap
はstd::swap_uses_value_representations_v<T>
がtrue
となる場合にswap_representations
を用いて最適化することができます。
この提案の内容はコア言語の変更を伴わないライブラリ拡張のみであり、なおかつ他のライブラリ機能に影響を及ぼさないものです。しかし、トリビアルリロケーションと共に導入されれば、そのパフォーマンス上の恩恵を最大化することができます。
P3247R1 Deprecate the notion of trivial types
以前の記事を参照
このリビジョンでの変更は、標準ライブラリ内の"trivial class"の用法を2か所調整し、[meta.type.synop]の見落としを修正した事です。
P3248R0 Require [u]intptr_t
(u)intptr_t
を必須にする提案。
(u)intptr_t
はポインタ値を整数として保持したい場合に使用でき、変換前後でポインタ値が変化しないことが保証されている整数型のエイリアスです。しかし、このエイリアスはオプションであり、実装は必ずしも定義する必要はありません。
(u)intptr_t
が存在しない可能性があるという事実によって、移植性を重視するソフトウェアではこの使用を回避する必要があり、そのために余計な作業と潜在的なバグを埋め込む可能性が発生します。
このことは標準ライブラリでも問題になり、P2835(std::atomic_ref
にポインタ値を取得可能な関数を追加する)やP3125(ポインタの未使用ビット使用のための関数を追加する)などではポインタ値のやり取りのために(u)intptr_t
を使用しようとしているものの、それがオプショナルであることが問題となっています。
この提案は、(u)intptr_t
を必須の型エイリアスにしようとするものです。
オプショナルであるとはいえ実際には主要3コンパイラは全てのターゲット向けにこのエイリアスを提供しており、その標準ライブラリ実装も(u)intptr_t
の存在を仮定して実装されているようです。また、プラットフォームのABIでも(u)intptr_t
相当の整数型の存在がポインタ型の指定と共に仄めかされているようです。
従って、(u)intptr_t
を必須にしたとしてもC++のプラットフォームサポートは低下しない、とこの提案では結論付けています。
提案している規格の変更は、(u)intptr_t
からoptionalであるという指定を取り除くだけです。
P3249R0 A unified syntax for Pattern Matching and Contracts when introducing a new name
パターンマッチングと契約プログラミング機能の間で統一的な構文の提案。
パターンマッチング構文においては、パターンの記述内で(マッチしたものを表す)新しい名前を導入する必要があり、P2688のパターンマッチング提案においてはlet x => expr;
の様に記述します。また、契約プログラミング機能の事後条件においては、戻り値を参照する名前を導入する必要があり、post(r : expr)
の様に記述します。
int f(); // パターンマッチングの例 void g() { int i = f(); i match { 42 let val => print(val); // valを導入(val == 42) let x => print(x); // xを導入(x != 42) } } // 契約プログラミング機能の例 int foo() post(ret : ret > 0) // retを導入
そのコンテキストで必要な変数名を導入するという側面からはこの2つは同じことをしていると見ることができ、同じことをするのに異なる構文を使用しているのは一貫性が無く初学者が覚えづらく、言語を複雑にします。
この提案は、この2つの名前導入の構文を統一することで、習得のしやすさの向上と言語の複雑さの低減を図るものです。
提案では、これらの構文を一貫してlet name => expr
のように統一することを提案しています。
int foo() post( let r => r > 0) // rを導入
すなわち、契約プログラミング機能の構文をパターンマッチングに合わせるものです。
ただし、事後条件(post()
)内でのlet ~
はパターンマッチングではないため、そこでパターンマッチングを書きたい場合は右辺の式内に記述する必要があります。
事後条件として(ret == 1 or ret == 2 or ret > 100)
という条件を指定したい場合の例
// P3210の提案 int foo() post ( 1 => true; 2 => true; let ret => ret > 100 ); // この提案の場合 int foo() post ( let ret => ret match { 1 => true; 2 => true; } or ret > 100 );
これが、先行する同種の提案P3210との違いでもあります。
この提案の問題点は、契約プログラミング機能がC++26を目指しているのに対して、パターンマッチング機能はまだその段階にないことです。おそらく、契約プログラミング機能の方が先に完成し標準に取り込まれるでしょう。つまり、パターンマッチングの構文はまだ流動的で、最悪の場合この提案が採択されたうえでパターンマッチングだけ異なる構文になる可能性があります。
ただ、この提案を採択しなければ同じことを行うのに異なる2つの構文が確実に導入されるのに対して、この提案を採択したおけばそれを異なる2つの構文が導入されるかもしれない、まで緩和することができます。
契約プログラミング機能が先に採択されている場合にこの提案が採択されているとすると、パターンマッチングの構文として別のものが提案される場合にそれに対して余分な制約を追加することになります。その場合この提案では、統一構文から逸脱するにせよ整合性を重視するにせよ、それは契約プログラミング機能とは無関係にパターンマッチングが(を議論するEWGが)決定するべき、としています。
P3250R0 C++ contracts with regards to function pointers
契約注釈を持つ関数の関数ポインタへの暗黙変換を禁止する提案。
現在の契約プログラミング機能の仕様(P2900)では、現時点で契約チェックが関数の呼び出し側でなされるのか、呼び出し先でなされるのかが未解決のままです。とはいえ、関数ポインタの場合を除いてこれはほぼ問題にならないようです。
また現在の契約プログラミング仕様では、関数ポインタに対する契約の指定は行えません。これは、関数ポインタがそのシグネチャだけ合っていれば変換できてしまうためで、異なる契約条件の変換をどうするかという厄介な問題の議論を先送りにするための制限です。ただし、将来的にそれを有効化した場合、関数ポインタを通して関数が呼び出された場合に契約チェックを正しく行うには必ず呼び出し先でチェックを実行しなければならないことを意味して います。
ただし、現在の仕様(P2900)では、関数ポインタに対する契約注釈を禁止しているものの、契約注釈を持つ関数の関数ポインタへの変換は禁止していません。これによって、将来的に関数ポインタに対する契約注釈への拡張が行われた場合に問題が起こります。
// 契約を持つ関数 void fun(A& a) pre (a.ok()); // 関数型エイリアス using F = void(A& a); using G = void(A& a) pre(a.ok()); // 将来可能になったとして // 関数ポインタ変数の宣言 F* f = fun; // ok、危険、現在から可能 G* g = fun; // ok、安全
このように同じ関数に対する関数ポインタでも、契約注釈の情報を保持したものとすべて削除されたものの2種類が共存してしまいます。
この提案は、将来の関数ポインタに対する契約注釈の指定を可能にしておくために、契約を持つ関数からシグネチャがマッチする関数ポインタヘの暗黙変換を禁止するものです。
関数ポインタに契約注釈を行えるようにする場合
- コンパイラは呼び出し先でのチェックを省略して、呼び出し側の(場合によっては省略可能な)契約条件のチェックだけを行える
- コンパイラの証明によって、契約条件のチェックが省略可能な機会が増える
- 異なる契約注釈セット間での関数ポインタの変換
- より強い契約からより弱い契約へ、またはその逆、の変換など
等のメリットがあります。
将来の関数ポインタのために設計を予約しておく場合、これらのメリットを将来的に導入する時のために設計を開いておき、なおかつ、現在の関数ポインタの使用方法を将来の変更から保護する事にもつながります。
一方、関数ポインタの契約注釈のために設計空間を予約しておく場合
- 関数名に対してアドレスを取る操作は、明示的な変換のみが許可される
- 型推論される場所で関数ポインタを使用できない
auto*
やテンプレート
- P2900は契約注釈はリフレクションに対して不可視であることを決定したが、これによってそれをコンパイル時に検出出来うる
等のデメリットがあります。
提案より、サンプルコード
int f(int x) post (r: r != 0); using function_type = int(*)(int); auto *fp2 = f; // ERROR auto *fp3 = (function_type)f; // OK template <typename FT> void g(FT func); g(f); // ERROR g((function_type)f); // OK
契約を持たない関数の関数ポインタへの変換、および(契約有無に関わらず)関数ポインタへの明示的変換は、これまで通りに許可されます。
この提案はSG21のレビューで好まれず、否決されています。
P3251R0 C++ contracts and coroutines
コルーチンに対する契約注釈を許可する提案。
C++26を目指している現在の契約プログラミング機能においては、コルーチンに対する契約はcontract_assert
のみが許可されており、事前・事後条件の指定は禁止されています。これは、コルーチンが中断可能な関数であることから、それらの契約がどのように振舞うべきかのセマンティクスを確立できていないため、その議論と決定を急がないための措置です。
通常の関数に対する事前条件と事後条件は、関数に入る時と関数から戻る時の、パラメータ/戻り値とプログラムの状態についての条件を指定します。
コルーチンはその呼び出しの任意の時点で中断して呼び出し元にリターンすることができ、またその後の任意の時点で再開し再び関数内に入ることができます。C++におけるコルーチンは関数宣言で区別されておらず、その定義を見に行かないとその関数がコルーチンであるかどうかはわかりません。従ってコルーチンに対する事前条件と事後条件で契約可能なのは、コルーチンの開始時点の状態と、中断時点の状態についてのものだけです。
現在の契約プログラミング機能でコルーチンに対する事前・事後条件指定が禁止されているとはいっても、それは実際には簡単に回避できます。例えばコルーチンf()
があるとき、auto g() { return f(); }
のようにすればこのg()
はコルーチンではなくなり、g()
には通常の関数同様の契約注釈を指定できます。このようにした時のg()
になされる事前条件と事後条件とは、ちょうどコルーチンの開始時点の状態と中断時点の状態に対する契約として機能し、それはこのようなラッピングを行うユーザーの意図するところと一致するはずです。
ただし通常の関数と異なるところが一点だけあり、それは事後条件からはコルーチン引数を一切参照できないということです(ムーブされている可能性があるため)。コルーチンに対する事後条件のこのコーナーケースを除外(コルーチン事後条件における引数参照の禁止)してまでコルーチン(の直接の戻り値であるawaitable
型)に対して事後条件を指定可能にすることが、事後条件の一貫性を損なうデメリットを上回るかどうかは要検討です。このコーナーケースを回避して一貫性のためにコルーチンに対する事後条件を禁止したとしても大きな問題はないと思われます。
コルーチンに対して事後条件の指定を許可する場合の事を考えます。前述のように、その場合の事後条件はコルーチンが中断した時点での状態に対するものになります。しかしこの場合、コルーチンはまだ実行を開始してさえいない可能性があり、ユーザーがそのような状態についてアサートできることはほとんどありません。
しかし、そのような物理的なコルーチンのインターフェースではなく、論理的なコルーチンのインターフェースを見た時に、コルーチンに対する事後条件とはコルーチンが生成・または返す値(コルーチンの直接の戻り値ではない)に対するものとするのが、ユーザーの期待と合っており望ましいでしょう。ただしこれは、契約注釈がコルーチンであることをほのめかすことなく、関数インターフェースによって自然に伝播される必要があります。
この提案では、コルーチンに対する事後条件の指定は、コルーチンが直接返す値に対して適用するのではなく、そのコルーチンのawaitable
型とpromise
型に対して適用するようにすることを提案しています。
コルーチンのpromise
型はコルーチンステート上に存在して呼び出し側とコルーチン側をつなぐ役割を持っています。promise
型のyield_value()
(co_yield
に対して呼ばれる)とreturn_value()
(co_return
に対して呼ばれる)は、生成/返される値を受け取って、コルーチンに対する事後条件を事前条件としてその値に対して適用することのできる理想的な場所に位置しています。一方、awaitable
な型のawait_resume
などはコルーチンに対する事後条件をそのまま自身の事後条件として持つことのできる場所に位置しています。また、これらの関数には現在の契約プログラミング機能の仕様においても契約注釈を制限なく行えるため、このために特別な規定を増やす必要はありません。
これを実装する方法は、コルーチンの生成/返す値に対する条件と指定された(と分かっている)コルーチンに対する事後条件の注釈を、コルーチンの返す直接の戻り値型(awaitable
)の一部(そのメンバ関数に対するもの)として扱う事です。例えば何かしラップを行う型を用意するなどの方法が考えられますが、これを許可するために現在の規定を弄る必要はありません(そこは実装詳細なので)。
現在の(そしておそらくC++26の)契約プログラミング機能の仕様に対して変更が必要なのは、コルーチンである関数に対する事前・事後条件の契約注釈を禁止している文言に対してだけです。コルーチンに対する事後条件は上記のように複雑になり他の関数と少し異なってしまうため有用かどうかは不明ですが、事前条件指定に関しては間違いなく役に立つはず、と主張しています。
結局この提案では
- コルーチンに対する事前条件(と事後条件)の契約注釈を許可する
- コルーチンに対する事前条件(と事後条件)は、
promise
型(およびawaitable
型)の 事前条件と事後条件として扱うようにする
の2つの事を提案しています。
P3253R0 Distinguishing between member and free coroutines
同じシグネチャを持つメンバ関数とフリー関数のコルーチンを区別するようにする提案。
コルーチンのプロミス型はstd::coroutine_traits<>
のテンプレートパラメータに、コルーチンの戻り値型及び呼び出し時の引数型を入れて特殊化したもの(std::coroutine_traits<R, Args...>
)が使用され、それに対応するものをあらかじめ用意しておくと(すなわち、コルーチン戻り値型に対して特殊化しておけば)、そのメンバ型(std::coroutine_traits<R, Args...>::promise_type
)を定義することでコルーチンで使用されるプロミス型をカスタマイズできます。
ところでコルーチンはメンバ関数としても定義することが可能であり、メンバ関数は最初の引数に暗黙にthis
引数を受けており、メンバコルーチンの場合std::coroutine_traits<R, Args...>
のArgs
の最初には暗黙のthis
引数に対応する型が入っています。
このため、同じ戻り値型を共有するメンバ関数と非メンバ関数のコルーチンで、非メンバコルーチンの方がメンバ関数コルーチンの属するクラスを最初の引数に取る場合、coroutine_traits
からその区別がつかなくなります。
struct S { // メンバコルーチン R member_coro1(int x, std::string y) const; R member_coro2(const S&, int x, std::string y) const; }; // フリー関数のコルーチン R free_coro(const S&, int x, std::string y);
このmember_coro1()
とfree_coro()
のコルーチンは、プロミス型の導出のためにstd::coroutine_traits
の特殊化を、std::coroutine_traits<R, const S&, int, std::string>
としてクエリします。これはコルーチン戻り値型R
から見るとmember_coro1()
とfree_coro()
を区別することができず、この2つの関数は同じプロミス型を取得します。
このようなR
には例えばstd::generator
があります。
このことが問題となるのは、std::generator
のようにアロケータをカスタマイズ可能にしようとする場合です。
コルーチンはコルーチンフレームの確保のために動的メモリ確保を行いますが、これはまずプロミス型のスコープでoperator new()
が探索されます。すなわち、プロミス型でoperator new()
を定義することで動的メモリ確保方法をカスタマイズでき、std::generator
では3番目のテンプレートパラメータと合わせて任意のアロケータを受け取り、使用することができます。
このoperator new()
の探索は、promise_type::operator new(std::size_t, Args...)
の様なシグネチャで行われ、後半の引数はコルーチン引数がその順番のまま渡されます。
std::generator<T, V, Alloc>
の場合、最も分かりやすいアロケータのカスタマイズとして3番目のテンプレートパラメータにアロケータ型を指定する方法があり、この場合にアロケータを初期化するためにはstd::allocator_arg_t
のタグ値の後にアロケータを渡す必要があり、これをコルーチン引数列の先頭で行う必要があります。
using Generator = std::generator<int, void, std::pmr::polymorphic_allocator<>>; struct S { // メンバコルーチン、カスタムアロケータを受け取ろうとしている Generator member_coro1(std::allocator_arg_t, std::pmr::polymorphic_allocator<> alloc, int x) const; }; // フリー関数のコルーチン1、カスタムアロケータを受け取る Generator free_coro1(std::allocator_arg_t, std::pmr::polymorphic_allocator<> alloc, int x); // フリー関数のコルーチン2、カスタムアロケータを受け取ろうとしている Generator free_coro2(const S&, std::allocator_arg_t, std::pmr::polymorphic_allocator<> alloc, int x); // フリー関数のコルーチン3、カスタムアロケータを受け取ろうとしている Generator free_coro3(const S&, const std::vector<int>&, std::allocator_arg_t, std::pmr::polymorphic_allocator<> alloc, int x);
ぱっと見ると、member_coro1()
とfree_coro1()
は正しくアロケータのカスタマイズを行えており、渡したアロケータが使用されるように見えます。一方で、free_coro2()
とfree_coro3()
は渡す引数順が間違っている(std::allocator_arg_t
が2番目以降の引数で渡されている)ためアロケータのカスタマイズは上手くいかないように見えます。
実際には、member_coro1()
とfree_coro1()
、そしてfree_coro2()
は渡されたアロケータを使用しますが、free_coro3()
では使用されません。これは、member_coro1()
の渡し方をサポートするための副作用です。
前述のように、member_coro1()
とfree_coro2()
では同じプロミス型が使用されます。すなわち、この2つの関数のシグネチャはプロミス型から見ると同じです(std::coroutine_traits<Generator, const S&, std::allocator_arg_t, std::pmr::polymorphic_allocator<>, int>::promise_type
)。したがって、探索されるnew
演算子のシグネチャはpromise_type::operator new(std::size_t, const S&, std::allocator_arg_t, std::pmr::polymorphic_allocator<>, int)
のようになります。これはstd::allocator_arg_t
が第一引数にわたっていないため本来カスタマイズ対象ではない(標準ライブラリ内におけるuses-allocator constructionと一貫していない)のですが、std::generator
がこの問題を考慮して設計されているため、これを特別扱いしてサポートするためにこのシグネチャでもoperator new()
のオーバーロードを用意しており、それによってカスタムのnew
演算子が呼ばれてその中で渡されたカスタムアロケータが使用されます。
この特別扱いはmember_coro1()
をサポートするためのものですが、member_coro1()
とfree_coro2()
では同じプロミス型が使用されているため、free_coro2()
に対しても使用可能になります。これによって、意図せずにfree_coro2()
は正常に動作しています。導出されるプロミス型が同一になるため、member_coro1()
だけを受け入れてfree_coro2()
を弾くということはできません。
この問題のために、std::generator
のようなコルーチン戻り値型の制作者は、free_coro1()
をサポートする(これは全く正当なもの)ためにstd::size_t, std::allocator_arg_t, Alloc
のように3番目の引数でアロケータを受け取るoperator new
オーバーロードと、member_coro1()
をサポートするためにstd::size_t, const S&, std::allocator_arg_t, Alloc
のように4番目の引数でアロケータを受け取るoperator new
オーバーロードの2種類を用意しなければならなくなります。
これによって、アロケータ引数に関してメンバコルーチンとフリー関数のコルーチンの間、またstd::generator
(及び同種のコルーチン戻り値型)と他の標準ライブラリ機能の間で、不必要な矛盾が生じています。
この提案はこの問題の解決のために、最初のプロミス型の探索過程を修正しようとするものです。
この提案では、メンバ関数のコルーチンがstd::coroutine_traits
からメンバ型としてプロミス型を取得する場合、まず最初にstd::coroutine_traits<R, Args...>::promise_type_for_nonstatic_member
が定義されているかをチェックし、定義されているならこれをプロミス型として使用し、定義されていない場合は従来通りstd::coroutine_traits<R, Args...>::promise_type
を使用する、といようにします。
プロミス型の定義方法及びその後の経路に関しては一切変更がありません。この入れ子のpromise_type_for_nonstatic_member
型として指定されているプロミス型からは、そのコルーチンがメンバ関数として定義されていることが分かるため、ここだけで4番目の引数でアロケータを受け取るoperator new
オーバーロードを用意すればメンバコルーチンに対してもアロケータカスタマイズを提供できます。なおかつ、promise_type_for_nonstatic_member
がプロミス型として取得されるのは非静的メンバのコルーチンからだけなので、フリー関数のコルーチンが子のプロミス型を使用することはありません。
これにより、先程の例のmember_coro1()
とfree_coro1()
の渡し方だけが有効で(サポートされ)、free_coro2()
(及びfree_coro3()
)の渡し方は有効ではない(サポート外)という弁別が可能になり、問題になっていた非一貫性が解消されます。
この方法では、ユーザーが明示的にstd::coroutine_traits
の特殊化にpromise_type_for_nonstatic_member
を定義しない限り、現在の動作が損なわれることはなく、下位互換性を担保することができます。
この提案は、EWGの初期レビューで好まれなかったようで、否決されています。
P3254R0 Reserve identifiers preceded by @ for non-ignorable annotation tokens
@
から始まるトークン列を予約しておく提案。
C++23ではP2558の採択によって@
が基本ソース文字集合(言語の構文を記述するために使用される文字の集合)に追加されました。これによって、将来のC++では@
を何かしらの言語機能のための構文に使用することができます。
そのような言語機能は現在存在していませんが、この提案は、@
から始まる文字列を将来的に無視できない注釈に使用するために、実質的にユーザー使用できないようにしておく提案です。
現在のC++の属性は無視できるという性質を持つため、何か注釈を行うのに必ずしも最適ではありません。属性は独立した構文空間を持つためその中で使用する名前は既にユーザーが使用しているものと衝突する心配がないという利点がありますが、何か新しい言語機能を追加する場合に無視可能という性質が望ましくなく、必ずしも新しいキーワードの追加や既存キーワード使いまわしを回避できるわけではありません。
@
を使用したものとして有効なトークンはリテラル以外になく、@~
のように@
から始まる文字列を何かしらの注釈として使用することは他の言語でも前例があります。その具体的な提案の検討はここではしていませんが、将来のそうした機能のためにこの構文空間を予約しておくことがこの提案の目的です。
この提案ではそのような注釈の想定される例としていくつかのものをあげています
- 契約注釈
@pre(x > 0)
や@assert(x > 0)
の様な注釈
- トリビアルリロケーション
- クラスに対する
@trivially_relocatable
のような注釈によって、クラス型がトリビアルリロケーション可能であることを表明する
- クラスに対する
- プロファイル注釈
- P2816で述べられているようなプロファイルの指定を
@enable(ranges)
のように指定する
- P2816で述べられているようなプロファイルの指定を
[[no_unique_address]]
の置き換え[[no_unique_address]]
は実際には無視できる属性ではなかったため、非推奨にして@no_unique_address
に置き換える
この提案では、@~
のトークンを単に予約するという文章によって指定するだけではなく、1つのプリプロセッシングトークンにしておくことも提案しています。これは、@~
のトークンが現在2つのプリプロセッシングトークンとして認識されてしまうためです。これが無いと、例えば@assert(expr)
のような注釈を導入しても、assert(expr)
マクロの展開を妨げることができません。同様に、他の@~
トークンでも先頭の@
を除いた部分の文字列がすでにマクロとして定義されている場合にそのマクロの展開を防止することができず、これだと予約しておく意味がほとんどありません。
そのため、@~
を1つのプリプロセッシングトークンとしてプリプロセス時に扱っておくことで、将来的に@~
を使った注釈を導入する時の問題をあらかじめ取り除いておくことができます。
P3255R0 Expose whether atomic notifying operations are lock-free
std::atomic
の通知・待機系関数がロックフリーとは限らない場合がある問題について修正する提案。
std::atomic
(以下std::atomic_flag
およびstd::atomic_ref
を含む)に対してC++20で追加された通知・待機系関数(.wait()
、.notify_~()
)に対しては、その型そのものに対してロックフリーであると指定される場合に(std::atomic_flag
の場合に常に)当然ロックフリーであり、シグナルセーフであることが期待されます。
しかし実際にはロックフリーではない可能性がありその場合シグナルセーフでもありません。.wait()
関数はスレッドをブロックするため明らかですが、通知関数についてもロックフリーな実装を取ることができるものの、オブジェクトサイズや環境によってはロックフリーとならない場合があるようです。
これらの問題の解決のため、この提案では次の事を提案しています
std::atomic_flag
の通知・待機系関数は、ロックフリーである必要が無いことが明確になるように規定を修正std::atomic
のis_lock_free
、is_always_lock_free
、std::atomic_is_lock_free
のプロパティはアトミックの通知・待機系関数に関してのものではないことを明確になるように規定を修正- これらのプロパティの範囲から、通知・待機系関数を除外する
std::atomic_flag
、std::atomic
、std::atomic_ref
にメンバ関数.notify_is_lock_free()
とフリー関数のstd::atomic_notify_is_lock_free
、およびメンバ定数のnotify_is_always_lock_free
を追加し、アトミックの通知・待機系関数がロックフリーかどうかはそれらのプロパティが表明するようにする
これによって、通知・待機系関数がロックフリー(でありシグナルセーフ)であるかどうかを正しく判定できるようになります。
P3257R0 Make the predicate of contract_assert
more regular
契約注釈から参照される変数の暗黙const
化を、contract_assert
では緩和する提案。
現在の契約プログラミング機能の仕様ではP3071の採択によって、契約注釈内からローカル変数・関数引数を参照すると、それは暗黙的にconst
として扱われます。これはconst
メンバ関数のメンバ変数に対する動作に似ており、オーバーロード解決の結果が変化したり、map[key]
のように一部の式がエラーになったりすることが知られています。
これにより、contract_assert()
の内外で同じ式に対するコンパイル結果が明確に変化します
void f() { int i = 0; if (++i < 5) { ... } // OK contract_assert(++i < 5); // P2900R6: ill-formed、この提案: well-formed }
このように、同じソースコードが近傍に出現していても同じように動作せず、ユーザーの期待に反する可能性があります。ただし、このことは事前条件と事後条件には当てはまりません(宣言で出現し、定義から離れている場合があるため)。
void h(int x, int y) pre(x > 0) // ok pre(y < 0) // ok pre(++x < 42); // ill-formed
別の問題として、契約条件の評価異数の問題があります。現在使用されているassert
マクロによる次のようなコードは、単純にはcontract_assert()
で置き換えられない可能性があります。
void f() { #ifndef NDEBUG int iter = 0; #endif while (/* something */) { assert(++iter < 6); // 6回以上繰り返されたらバグ // ... } }
このiter
変数の宣言を同様にガードする方法が無いのもそうですが、一番の問題はcontract_assert
に指定された述語が無制限に評価される可能性があるためです。
assert
マクロからの移行の問題であるため、これについてもcontract_assert
だけが対象です。
これらの問題の解決のために、この提案は次の事を提案しています
contract_assert
ではP3071の変更を元に戻すcontract_assert
の述語内から参照される変数のconst
性は変化しない
- その契約注釈がignoreセマンティクスを持たない場合、
contract_assert()
の述語(条件式)は一回だけ評価されることを規定
これによって、assert
マクロからcontract_assert
への移行がより簡単になります。
この提案の内容はどちらも好まれなかったようで、SG21のレビューでコンセンサスを得ることができませんでした。
P3258R0 Formatting charN_t
charN_t
文字・文字列をstd::fomart
可能にする提案。
charN_t
とは、char8_t, char16_t, char32_t
型の事です。これらのユニコード文字型は言語サポートされているにもかかわらず、標準ライブラリのサポートに乏しく、微妙に使いにくい部分がありました。
この提案はその改善の第一歩として、これらのユニコード文字型による文字と文字列をstd::fomrat()/std::print()
でサポートする提案です。
提案では、char8_t, char16_t, char32_t
の文字と文字列、およびこれらの文字を使ったstd::string/std::string_view
及びrange
をstd::format()
による文字列化対象にすることを提案しています。ただし、フォーマット文字列としてこれらのユニコード文字型を使用可能にすることは提案していません。出力のエンコーディングは、現在同様にフォーマット文字列の文字型から静的に決定されます。
char
とwchar_t
がフォーマット文字列とフォーマット対象で混在するのは許可されていませんが、この2つの文字型がフォーマット文字列として使用されている場合にcharN_t
をフォーマットすることはサポートされています。
筆者の方は、libc++のstd::format()
実装でこれを実装しても実装上の問題はほとんど発生しなかったと報告しています。
P3259R0 const by default
クラス型のオブジェクト宣言時のデフォルトをconst
にする提案。
この提案は、以前のP3218R0で提案されていアプローチの一部(参照セマンティクスを持つ型はデフォルトでconst
が付くようにエイリアスを作る)を言語機能によって実現しようとするものです。P3218R0については以前の記事を参照
この提案では、クラス宣言への指定としてconst
を付加して宣言できるようにすることを提案しています。
// function_refの宣言例 template<class... S> class function_ref; // not defined template<class R, class... ArgTypes> class function_ref<R(ArgTypes...) cv noexcept(noex)> const // 👈 const by default { // ... };
このように宣言されたクラス型の変数は、デフォルトの宣言が暗黙的にconst
であるように扱われます。const
指定されたクラス型からconst
を取り除きこれまで通りの動作(デフォルト非const)をする型を得るには、型名にmutable
を付加します。
void action() {} void some_other_action() { function_ref<void ()> fr1{ action }; // デフォルトの宣言がconst宣言になる mutable function_ref<void ()> fr2{ action }; // 非const変数の宣言 fr1 = function_ref<void ()>{ action }; // ng fr2 = function_ref<void ()>{ action }; // ok std::vector<function_ref<void ()>> v1{ fr1, fr2 }; // ng std::vector<mutable function_ref<void ()>> v2{ fr1, fr2 }; // ok }
クラスのconst
指定の逆の指定(つまり今まで通り)としてmutable
も追加することを提案しており、さらにどちらにも引数としてbool
値を取れるようにしています。
class a const {}; // デフォルトconst宣言 class b mutable {}; // `class b {};`と同じ class c const(true) {}; // `class c const {};`と同じ class d const(false) {}; // `class c mutable {};`と同じ class e mutable(true) {}; // `class c mutable {};`と同じ class f mutable(false) {}; // `class c const {};`と同じ
function_ref
やoptional<T&>
などの浅いconst
セマンティクスを持つ参照セマンティクス型をデフォルトでconst
にしておくことで、言語組み込みの左辺値参照との一貫性が高まり、安全に使用しやすくなります。
P3263R0 Encoded annotated char
ユーザーが指定したエンコーディングによる文字を表現可能な文字型の提案。
テキストの処理と解釈にはその文字のエンコーディングが重要となります。ユーザーが作成する可能性のあるアプリケーションの範囲を考えると、現状のC++にはソフトウェアでテキストを処理するための単一の方法がありません。
型と文字エンコーディングを対応させることでユーザーが様々なエンコーディングでテキストを識別して管理できるようにするのは望ましい方向性であり、C++でもユニコードに対応する三種類の文字型(char8_t, char16_t, char32_t
)がコア言語に対して追加されています。しかしこの方法はスケールしません。存在する全ての文字エンコーディングに対して対応する文字型を標準に導入するのは現実的ではなく、追加に当たっては文字型そのものだけではなく他のライブラリユーティリティ(std::integral, std::string, std::string_view
など)に対する対応も求められます。
この提案は、標準化委員会の関与を必要とすることなくユーザーが独自の文字エンコーディングサポートを追加できるようにするためのライブラリ機能を追加しようとするものです。
文字型の要件としては
- 既存の型の単なるエイリアスとして扱われない一意の型である
using char_iso2022_t = char8_t;
のようなものではない
- 文字として実行できると期待される一般的な操作を適用可能である
- 文字のコード単位がある値であるかをチェックする(
==
)や、文字があるコード単位の範囲内にある化をチェックする(> >=
など)など
- 文字のコード単位がある値であるかをチェックする(
- ユーザーが独自のエンコーディングを、それに期待されるすべての関連機能と共に、簡単に定義できる
- ユーザーが文字コード単位の幅(文字サイズ)を指定できる
- 変換するのが十分に容易である
ここでは、これらの要件を達成するために、コア言語機能での対応を必要とせずenum class
を利用するものでもない、ライブラリ機能を導入しようとしています。その文字型は、次の2つのものから構成されます
EBCDICをサポートする例
// EBCDICエンコーディングのためのエンコーディング注釈型の定義 struct text_encoding_EBCDIC: public std::text_encoding // エンコーディング注釈型であることを示すための基底クラス型 { using char_t = char8_t; // optional, not required static constexpr std::string_view id{"EBCDIC"}; }; // EBCDIC文字型の定義 using char_EBCDIC_t = std::char_enc_t<text_encoding_EBCDIC>; // EBCDIC文字列型の定義 using string_EBCDIC = std::basic_string<char_EBCDIC_t>; using string_view_EBCDIC = std::basic_string_view<char_EBCDIC_t>;
エンコーディング注釈型はstd::text_encoding
を継承したクラス型として定義し、メンバ型::char_t
にその表現型を指定します。提案では、表現型としてはbool
ではない符号なし整数型であることを要求していますが、ユニコード文字型のみに制限するオプションもあげています。
エンコーディング注釈型に必要なことはこれだけで、あとはそれをstd::char_enc_t<>
のテンプレートパラメータに渡すことでそのエンコーディングの文字型を得ることができます。std::char_enc_t<E>
はエンコーディングE
による文字型として利用可能であるために、E::char_t
の薄いラッパとなる型です。std::char_enc_t<E>
ではE::char_t
のものを利用する形で文字演算(ほぼ整数演算)の演算子オーバーロードが定義されており、これによって文字表現型で利用可能な操作をそのまま再利用しながら、なおかつその型とは完全に別の型として扱われる、という性質を達成しています。
また、std::char_enc_t<E>
は表現型への明示的変換のほか、文字の表現型での値を取得する.value()
等が用意されていることで、変換も容易に行えるようにしています。
これらの機能は、先に挙げた5つの要件に加えて、標準化委員会の関与を必要とせずに新しい文字型を定義できるという要件をすべて満たすソリューションです。
ただし一つ懸念点があり、文字・文字列リテラル('c'や"string"
)がコア言語サポートであるためにこの型のそれを自動で追加できず、ユーザー定義リテラルや変換関数を使用することになりますが、それだと言語のリテラルにある永続的なストレージに配置されるという性質が達成できません。この提案ではこれに対する解決策を見出しておらず、未解決の問題としています。
P3264R0 Double-evaluation of preconditions
↓
P3264R1 Double-evaluation of preconditions
事前条件がチェックされる場合に、それが2回以上チェックされる可能性があるという現在の仕様を維持すべきとする提案。
現在の契約プログラミング機能の仕様では、契約注釈の種類を問わず契約条件がチェックされる場合は2回以上チェックされる可能性があるとされています。これには異論もあり、正確に1回・少なくとも上限を設ける・正確に回数を指定する、などの別のアプローチを望む声もあります。
この提案は、事前条件に的を絞ってその利点を説明し、少なくとも事前条件については現在の仕様を維持すべきとする提案です。
ここでは、事前条件の条件式が2回以上チェックされることを許可するようにしておく利点として次の事を挙げています
- 呼び出し側と呼び出される側の両方に対して契約チェックを行う機会を提供できる
- 翻訳単位の片側/両側で契約チェックを有効・無効を切り替えても、ABIに影響を与えない
- 上記の利点をライブラリのグラフ(ライブラリ内で使用されているライブラリ)に拡大しても、同じ利点が得られる
- チェックが有効化されている場合でも効率的
- 単一評価を保証するための実行時機構の呼び出しが義務付けられない
- これらの利点には説得力があるため、標準が正確に一回だけ評価されるという方向性を採用した場合でも、2回以上評価されるというアプローチは(非標準の拡張として)提供されると思われる
そして、2回以上チェックされる可能性があるとしておくことによって、プログラマは契約条件式内での副作用に頼ったプログラムを書くのを回避するようになる、という副次的効果も得られるはずです。契約条件式の副作用に頼ったプログラムはそれを書いたプログラマ以外がコードを理解することを難しくするとともに、契約注釈を活用する外部ツール(静的解析など)の解析も困難にします。
他の2種類の注釈、事後条件とcontract_assert
の場合、同じように規定しても事前条件程の利点が得られない可能性があるものの、少なくとも事後条件に関しては事前条件の場合と同じ利点がある、としています。
P3265R0 Ship Contracts in a TS
↓
P3265R1 Ship Contracts in a TS
契約プログラミング機能をまずTSとして出荷すべき、とする提案。
契約プログラミング機能は今のところC++26を目指して進行しています。しかし、EWGに転送された後で、EWGのメンバが考える契約プログラミング機能と現在のそれが少し異なるものであることが表面化し、それによって契約プログラミング機能に対して機能リクエストが噴出しつつあります。
この提案は、契約プログラミング機能の提案(P2900)から期限を取り除いて、まずTSという形で出荷してもう少し時間をかけることを提案するものです。
提案ではその動機として次のものを挙げています
- 実装経験を得る
- 現場での使用経験を得る
- P2900の複数の点(とくに争点となっている)に対するWG21全体のコンセンサスを得る
- 仮想関数、コルーチン、関数ポインタに対する契約についてのコンセンサスが無い
- 安全性の向上
- 契約注釈は言語の他の部分同様に未定義動作の影響を受けやすく、このことは契約注釈の実行時チェックにとっても静的解析ツールの契約注釈利用時においても問題となる可能性がある
また、契約プログラミング機能に対する機能要求は現在でもいくつも出てきており、それらには以前にはなかったアプローチも含まれています。期限を重視して最小の機能をC++26に入れるMVPのアプローチでは、これらの提案が間に合わないことでそのような別のアプローチへの道を閉ざしてしまう可能性もあります。
この提案はSG21で確認された後EWGでも確認されていますが、まだ何かしらの決定は下されていません。どうやら、契約プログラミング機能の期限を決める責任はEWGにあるようです。
P3266R0 non referenceable types
型の参照型を取得できないようにするクラスの指定の提案。
この提案は、以前のP3218R0で提案されていアプローチの一部(参照セマンティクスを持つ型はアドレスを取得できない)を発展させたものを言語機能によって実現しようとするものです。P3218R0については以前の記事を参照
この提案では、referenceable(bool)
のようなクラス宣言に対する指定によってそれを行おうとしています
template<class... S> class function_ref; // not defined template<class R, class... ArgTypes> class function_ref<R(ArgTypes...) cv noexcept(noex)> referenceable(false) // 参照できず、コピーのみが可能となる { // ... }; template <class T> class optional<T&> referenceable(false) // 同上 { // ... };
このように指定して宣言されたクラスは参照で保持することが禁止され、コピーのみしかできなくなります
// 参照不可能な型への参照は許可されない function_ref<void ()>& f(function_ref<void ()>&); // ill-formed optional<int&>& f(optional<int&>&); // ill-formed void action() {} int main() { function_ref<void ()> fr1{ action }; // 参照不可能な型への参照は許可されない function_ref<void ()>& fr2 = fr1; // ill-formed int i = 42; optional<int&> oi1{ i }; // const by default // 参照不可能な型への参照は許可されない optional<int&>& oi2 = oi1; // ill-formed return 0; }
デフォルトはreferenceable(true)
であり、これは現在のクラス型の振る舞いです。
class a referenceable(false) {}; // 参照不可能な型 class b referenceable(true) {}; // class b {};と同じ
これは参照セマンティクスを持つ型の扱いをより言語参照へ近づけるもの(参照の参照を作成できない)であり、これによってダングリング参照の発生を抑制できるとしています。
P3267R0 C++ contracts implementation strategies
↓
P3267R1 Approaches to C++ Contracts
C++契約プログラミング機能の実装戦略についてまとめて比較した文書。
ここでは主に事前条件と事後条件の契約注釈のチェックされるタイミングについての実装戦略のリストアップと比較を行っています。
説明に当たっては、次のような契約されている関数と、それを呼び出す関数および関数ポインタを使ってそれを呼び出す関数の3つで例示しています。
// 事前・事後条件を持つ関数f() int f(int arg) pre(arg > 5) pre(arg < 2000) post(rv: rv % 2 == 0) { return arg * 2; } // f()を呼び出すだけの関数g() int g() { return f(25); } using function_pointer = int(*)(int); function_pointer fp = &f; // f()を関数ポインタ経由で呼び出す関数h() int h() { return fp(40); }
また、チェックされるタイミングの例示のために契約条件チェックをcheck()
という関数で表し、条件を最適化に利用するポイント(条件式を仮定として扱うポイント、あるいは契約チェックを省略可能なポイント)をimply()
という関数で表わしています。これはアサートの持つ役割を分離して、どの性質が許可されるのかを明確にするためです。
そして、ここでは契約注釈のセマンティクスに関してはこの実装戦略に影響しないとして同等に扱っており、契約注釈のセマンティクスは翻訳単位間で一致していることを前提としています。
1. 全て呼び出し先でチェックする
事前条件と事後条件に関する全ての事を関数の定義内で行う方法で、これはもっとも簡単なC++契約プログラミング機能の実装方法です。
int f(int arg) { check(arg > 5); imply(arg > 5); check(arg < 2000); imply(arg < 2000); int rv = arg * 2; check(rv % 2 == 0); imply(rv % 2 == 0); return rv; } int g() { return f(25); } int h() { return fp(40); }
2. 呼び出し側でチェックし、呼び出し先では条件を仮定、事後条件を元のABIエントリポイントでチェック
この文書における元のABIエントリポイントとは、ABIにおけるf()
の呼び出しを表すものの事です。後で出てきますが、このエントリポイントを分割することで契約チェックの異なる実装が可能になります。
このアプローチは、チェックをそれらを包含する影響がある場所(事前条件が真 => 事後条件が真という含意を使用する場所)に配置するか、チェックを省略可能な他の情報が利用できる場所に配置するため、呼び出し側での最適化に適している実装です。
int f(int arg) { imply(arg > 5); imply(arg < 2000); int rv = arg * 2; check(rv % 2 == 0); return rv; } int g() { check(25 > 5); check(25 < 2000); int rv = f(25); imply(rv % 2 == 0); return rv; } int h() { return fp(40); }
3. 呼び出し側でチェックし、呼び出し先では条件を仮定、事前条件を元のABIエントリポイントでチェック
2と同様に呼び出し側でチェックを行いますが、f()
のABIエントリポイントを分割し、事前条件を仮定し事後条件をチェックする別のエントリポイントを挿入します。
int f(int arg) { check(arg > 5); check(arg < 2000); int rv = f@post-check(arg); imply(rv % 2 == 0); return rv; } int f@post-check(int arg) { imply(arg > 5); imply(arg < 2000); int rv = arg * 2; check(rv % 2 == 0); return rv; } int g() { check(25 > 5); check(25 < 2000); int rv = f@post-check(25); imply(rv % 2 == 0); return rv; } int h() { return fp(40); }
4. 両側でチェックする
アプローチ1と同様に元のABIエントリポイントを保持し、なおかつ関数の呼び出し側と呼び出し先の両方でチェックを行います。この方法は一見冗長に見えますが、関数の両サイドに最適化ポイントを提供します。
int f(int arg) { check(arg > 5); imply(arg > 5); check(arg < 2000); imply(arg < 2000); int rv = arg * 2; check(rv % 2 == 0); imply(rv % 2 == 0); return rv; } int g() { check(arg > 5); check(arg < 2000); int rv = f(25); check(rv % 2 == 0); imply(rv % 2 == 0); return rv; } int h() { return fp(40); }
5. 遅延チェック
このアプローチと異なり、この方法では契約チェックは通常行われません。契約チェックの実行は外部ユーザー(プロファイラやデバッガなど)が指定する場合に実行時の任意のタイミングで行われます。
template <typename T> struct contract_raii_handle { contract_raii_handle(T&& lambda) : _check(std::move(lambda)) , result(2); {} bool operator()() { if (result == 2) { result = _check(); } return (bool)result; } }; auto add_pending_contract_check(auto lambda) { return contract_raii_handle<decltype(lambda)>(std::move(lambda)); } int f(int arg) { auto pre_handle_1 = add_pending_contract_check([=]{ return arg > 5; }); auto pre_handle_2 = add_pending_contract_check([=]{ return arg < 2000; }); return arg * 2; } int g() { check(arg > 5); check(arg < 2000); int rv = f(25); auto post_handle_1 = add_pending_contract_check([=]{ return rv % 2 == 0; }); return rv; } int h() { return fp(40); }
この実装はC++による近似実装であり、より効率的な実装を行うこともできます。
6. 実行時のセマンティクス選択
int f(int arg) { bool __run_checks = should_check(__func__); if (__run_checks) { check(arg > 5); check(arg < 2000); } int rv = arg * 2; if (__run_checks) { check(rv % 2 == 0); } return rv; } int g() { return f(25); } int h() { return fp(40); }
7. プログラムロード時のセマンティクス選択
6のアプローチにおいて、関数呼び出し時ではなくプログラムロード時に契約チェックを行うかを指定してシンボル解決を行うことで、チェックを行う実装の選択をプログラムロード時に解決するものです。
static int f__check(int arg) { check(arg > 5); check(arg < 2000); int rv = arg * 2; check(rv % 2 == 0); return rv; } static int f__nocheck(int arg) { int rv = arg * 2; return rv; } static void *f__resolver() { // As an example of a condition to select on // This will be checked on the first invocation of f(). return getenv("mustgofaster") == nullptr ? &f__check : &f__nocheck; } int f(int arg) __attribute__((ifunc("f__resolver"))); int g() { return f(25); } int h() { return fp(40); }
C++プログラムの開始後main()
が実行されるまでの間にこれを実行することはオーバーヘッドが大きすぎる可能性がありますが、リンカ/ローダーの機能(GNU ifuncなど)を使用することでプログラム開始前のロード時にシンボル解決を行って、オーバーヘッドを削減できます。
比較
各アプローチの特徴は次のようになります
- 全て呼び出し先でチェックする
- 特徴: 関数の実装内で全ての契約チェックを行う。ABIの変更は発生しない
- 利点: 実装がシンプル
- 欠点: 呼び出し側での最適化の余地がほとんどない
- 呼び出し側でチェックし、呼び出し先では条件を仮定、事後条件を元のABIエントリポイントでチェック
- 特徴: 呼び出し側で契約チェックを行い、呼び出し先側は条件を仮定する。事後チェックが元のABIエントリポイントとなるため、ABIの変更は発生しない
- 利点: 呼び出し側での最適化が可能になる
- 欠点: 関数ポインタを使用する場合に契約がチェックされない。呼び出し側と呼び出し先でコンパイラフラグが異なっているとチェックされない場合がある
- 呼び出し側でチェックし、呼び出し先では条件を仮定、事前条件を元のABIエントリポイントでチェック
- 特徴: 呼び出し側で契約チェックを行い、呼び出し先側は条件を仮定する。事前チェックを元のABIエントリポイントとし、ABIを変更した名前の別のエントリポイントを追加する
- 利点: 呼び出し側での最適化が可能であり、ABIエントリポイントを変更することで関数ポインタの場合でも契約チェックが可能になる。翻訳単位が分かれていてもチェックが欠落することはない
- 欠点: 関数ポインタからの呼び出しでチェックを省略できない
- 両側でチェックする
- 特徴: 元のABIを維持しながら、呼び出し側と呼び出し先側の両方でチェックを行う
- 利点: 呼び出し側でも事後条件に関する最適化ポイントが得られる。翻訳単位が分かれていてもそれぞれの側でオンオフを切り替えられ、どちらかの側でチェックが有効になっていればチェックされる
- 欠点: 契約チェックが重複するため、オーバーヘッドが発生する可能性がある
- 遅延チェック
- 特徴: 実際のチェックは行わず、契約を保留にしておき、プロファイラやデバッガなどの外部ツールからの要求に応じて評価する
- 利点: 高コストな契約チェックを必要な場合にのみ実行できるため、実行時のオーバーヘッドを削減できる
- 欠点: 契約違反の検出が遅れる可能性がある。この機構そのものに避け難いオーバーヘッドがある
- 実行時のセマンティクス選択
- 特徴: グローバルなクエリ関数によって実行時に契約チェックの有効無効を取得し、有効な場合にのみチェックを行う
- 利点: パッケージマネージャ等のビルド済みライブラリを配布する存在が、契約の有効無効で配布するバイナリを分ける必要が無い
- 欠点: 実行時のオーバーヘッドが追加される
- ロード時のセマンティクス選択
- 特徴: 環境変数などを介して、プログラムロード時に契約チェックを行うかどうかを決定する
- 利点: 6のアプローチと同じメリットが得られ、実行時オーバーヘッドを回避できる
- 欠点: 関数のインライン化を阻害する
このような実装を考慮することで、各実装アプローチによる契約条件評価回数を求めることができます。
アプローチ | 評価回数の最小値 | 評価回数の最大値 |
---|---|---|
全て呼び出し先でチェック | 1 | 1 |
呼び出し側チェック(3) | 0 | 1 |
呼び出し側チェック(4) | 1 | 1 |
両側チェック | 2 | 2 |
遅延チェック | 0 | 1(∞) |
実行時セマンティクス選択 | 0 | 1 |
ロード時セマンティクス選択 | 0 | 1 |
この文書はどのアプローチが良いかを提案するものではなく、可能なアプローチを列挙子比較する単一の文書を提供することを目的とするものです。
P3268R0 C++ Contracts Constification Challenges Concerning Current Code
契約注釈内での暗黙const
化の影響を見積もる文書。
現在の契約プログラミング機能の仕様では、契約注釈内から参照される外部のもの(関数引数・ローカル変数)は暗黙にconst
として扱われますが、これに対してコードの他の部分との不一致等の懸念が提起されています。
この文書は、筆者の方の所属する企業のコードベースの調査によって、それがどの程度影響するかを見積もったものです。
結果としては、(このコードベースでは)300個に1個のアサートについて注意が必要(const
化の影響を受ける可能性がある)で、そのような行は100000行に1行程度発生すると報告しています。
そして、結論としては暗黙const
化によるコードベースヘの影響は小さく、その仕様を削除する必要は無いとしています。
P3269R0 Do Not Ship Contracts as a TS
契約プログラミング機能をTSとして出荷することに反対する提案。
契約プログラミング機能の提案(P2900R6)がEWGに転送されて以降、あるベンダが契約プログラミング機能のある側面について継続的に反対している他、いくつかの側面でEWGで合意が取れていないことが分かりました。それを受けてP3265R0では契約プログラミング機能をTSとして出荷することを提案しています。
この提案は、それとは逆に契約プログラミング機能はTSを目指さずにC++26に導入すべき、とするものです。
提案ではTSではなく直接ISを目指すべき理由として次の事を挙げています
また、TSが有用ではない理由として次の事を挙げています
- 時間とリソースの非効率な使用
- TSプロセスは時間と手間がかかる
- 公開だけで1年かかる
- そのためのリソースを、現在のP2900R6に対する意見の相違点や問題の解消に費やすべき
- そもそも、TSに行くべきかを検討するこの時間ももったいない
- TSプロセスは時間と手間がかかる
- 実装経験を得るためにTSは不要
- 妥当な品質の仕様書とそれを実装する時間と資金があれば、実装経験を得ることができる
- 仕様書がTSとして公開されているかは重要ではない
- TSは未解決の問題の解決に役立たない
- TSを公開する前に、TSが回答すべき質問のリストを作成する必要がある
- P2900R6に対するそのようなリストはすでに得られており、C++26サイクルの残りの時間で解決可能であると見積もられている
最後に、現在のContracts MVP(P2900R6)に対する未解決の問題をここでもまとめています
- 仮想関数に対する契約
- まだMVPにマージ前だが成熟した提案がすでにあるため、C++26に間に合うと考えられる
- 関数ポインタと契約
- MVPは既に、関数が関数ポインタから呼ばれる場合でも契約条件がチェックされることを求めている
- 関数ポインタ自体に対する契約注釈は、現在のC++のモデルに適合しないためMVPの範囲外
- コルーチンに対する契約
- コルーチンでは事前条件と事後条件がサポートされていない
- SG21およびEWGのほとんどの人はこれはMVPの範囲外だと考えている
- 評価回数と暗黙
const
について- 最近特に議論されている分野であり、提案もいくつも出揃っている
- 解決策はどれも、どれが間違っているというものではなく、何を優先すべきかが異なるだけ
- この優先すべきものを決めるだけであり、この作業は既に始まっている
- 違反ハンドラからの例外送出
- 違反ハンドラからの例外送出を許可する・しないのトレードオフについては、議論し合意を得る必要がある
- 不可能な作業ではなく、TSは役に立たない
- 標準ライブラリでの契約の使用
- TSを出荷しても、この問題の解決には役に立たない
- 現在のところ、標準ライブラリの仕様に契約注釈を規定することは時期尚早であり、実装を許可するものの必須とはしないコンセンサスがある
- 契約と未定義動作
- 契約注釈は、それが存在する場所よりも後の未定義動作(のタイムトラベル最適化)による影響と、契約条件式内での未定義動作による影響の2種類の影響を受ける
- いずれの問題も、一定の解決を合意済み
- 未定義動作を制限するサブセットの利点は不明であり、TSにしたとしてもこの解決に役立たない
- 契約注釈の問題は、安全ではないことではなく存在しないこと
SG21では契約をTSとしないことに合意が取れているようですが、最終的に決定するのはEWGのようです。
P3270R0 Repetition, Elision, and Constification w.r.t. contract_assert
contract_assert
の現在の問題点について、原則に照らして検討する提案。
この提案ではまず、契約注釈の現在の仕様に関する次の2つの問題点
- 契約条件式内での変数のデフォルト
const
化 - 実行時の契約条件式の評価回数
- 2回以上評価される可能性があり、省略可能でもあること
について、特にcontract_assert
がこの懸念点を解消可能であるか、他の契約注釈とどの程度異なる必要があるかなどについて検討しています。
提示されているソリューションにおいては、次の機能単位毎に取り除くとどうなるかについて検討しています
- EL (Elision Clause): コンパイル時に述語が常に偽と判断できる場合に、その評価を省略できる規定
- RE (Repetition): 述語を複数回評価することを巨kする規定
- IM (Implementation Latitude): 一つの翻訳単位内で、全て同じ契約注釈のセマンティクス(チェックするかしないか)を適用することを許可する
- Cアサートのように、1つのフラグで全ての契約注釈のセマンティクスを一括制御する
提示されているソリューションは次の6つです
- ソリューション A: C++26では何もせず、C++29を待つ
- ソリューション B: P2900を現状(P2900R7)のまま維持
- ソリューション C: 省略条項 (EL) を削除した P2900 (P2900 - EL)
- 利点:
- 述語を複数回繰り返すことで、破壊的な副作用のテストが可能になる
- 静的に0であると証明できない冪等な副作用への依存が可能になる
- デフォルトの動作を、後方互換性を保ちながらソリューション E または F に移行できる
- 欠点: 副作用の扱いに関する問題が残る可能性がある
- 利点:
- ソリューション D: 繰り返し (RE) を削除した P2900 (P2900 - RE)
- ソリューション E: 省略 (EL) と繰り返し (RE) を削除した P2900 (P2900 - EL - RE)
- 利点:
- ソリューションCとDの利点を併せ持つ
- デフォルトの動作を、後方互換性を保ちながらソリューション F に移行できる
- 欠点: 柔軟性がさらに低くなり、適用範囲が限定される
- 利点:
- ソリューション F: 省略 (EL)、繰り返し (RE) を削除し、翻訳単位ごとに1種類のセマンティクス(チェック済みまたはチェックなし)のみを持つ P2900 (P2900 - EL - RE - IM)
そして、この作業の過程で現在のassert
マクロを完全に代替することのできるソリューションが無いという新たな問題が認識されたため、その対策としてP3290で提案されているソリューションを現在のContracts MVPにマージすることを提案しています。
P3290の詳細は後の方で詳しく説明しますが、概ね次の3つのものからなります
- 従来のアサーションシステムが契約違反ハンドラを呼び出せるようにするためのAPIの提供
- C++における
assert
マクロの定義を変更して、cerr
に診断を出力する代わりに契約違反ハンドラを(条件付きで)呼び出せるようにする contract_assert
をミラーする新しいキーワードを提供する
結論としては、先に挙げた6つのソリューションのいずれよりも、現在のContracts MVPにP3290R0をマージしたもの(P2900R7 + P3290R0)が適しているとして、このソリューションを推しています。
P3271R0 Function Usage Types (Contracts for Function Pointers)
契約注釈を指定可能な新しい関数型の提案。
内部で呼び出す操作を引数で受け取る関数fancy_calculation()
があるとします。このfancy_calculation()
は渡される関数に対して次の事を期待しています
- 関数は
int
型の引数を1つだけ取る - 関数は
int
型の結果を返す - 関数に渡す引数は正の値
- 関数の結果は正の値であり、渡した数より大きくはならない
例えば次の関数はこの要件を満たすものです
int identity( const int x ) { return x; } int halve ( const int x ) { return x/2; }
一方で、満たさないものも容易に考えることができます
int twice ( const int x ) { return 2*x; }
fancy_calculation()
のユーザーは関数ポインタや関数参照を使用してfancy_calculation()
に関数を渡すことができます。
int fancy_calculation(int(&f)(int));
fancy_calculation()
は内部で渡された関数を呼び出します。
ユーザーはまた、渡す関数(の候補)に対して契約注釈を指定している場合があります。
int identity( const int x ) post( r: r == x ); int halve ( const int x ) post( r: r == x/2 ); int twice ( const int x ) pre( can_multiply(2,x) ) post( r: r == 2*x );
fancy_calculation()
の提供者は契約注釈を使用してこの関数の契約を明文化してチェックを行おうとします
int fancy_calculation( int(&op)(int), const int start ) pre( start >= 0 ) post( r: r >= 0 ) post( r: r <= start ) post( r: r == op(r) );
しかし、これらのアサーションはfancy_calculation()
の動作についてのもので、fancy_calculation()
が受け取って内部で呼び出す操作(op
)についてのものではありません。そのようなものを書く方法で現在の契約プログラミング機能でサポートされている方法として、contract_assert()
によって実際の呼び出し前後でチェックを行う、という方法があります。
int fancy_calculation( int(&op)(int), int start ) { int current = start; while( true ) { contract_assert( current >= 0 ); // our responsibility int next = op( current ); contract_assert( next >= 0 ); // Not our responsibility: these contract_assert( next <= current ); // fail if op was poorly chosen. if ( current == next ) break; current = next; } return current; }
この方法はあまり良い方法とは言えません。呼び出しが複数回に及ぶ場合冗長でエラーが発生しやすくなり、これらのアサーションのための追加コードが必要になる場合もあります。そして、これらのアサーションは本来op
を渡すユーザー側に対してその要件を通知するものですが、関数を呼び出すコードからは見えなくなっています。
このような用途に適したソリューションが必要であり、この提案はそのソリューションを提供しようとするものです。
この提案では、いくつかのステップを踏んでそのようなソリューションを構成しています。
1. fancy_calculation()
が渡される関数に課す要件を名前の下にまとめる
例えば、次のような関数型likeな記述によってその要件を表現します
int fancy_op( const int x ) usage pre( x >= 0 ) post( r: r >= 0 ) post( r: r <= x );
これはこの提案で関数利用型(function usage type)と呼ばれている、新しい関数型です。この型はint(int)
な関数が特定の呼び出し元によって使用される方法について記述しています。
2. fancy_calculation()
で使用するためにある関数を選んだ時点で、その関数がfancy_op
での使用を意図していることを明示する構文を使用する
void print_fancy_results( int start ) { const std::array ops = { &fancy_op{identity}, // good choice &fancy_op{halve}, // good choice &fancy_op{twice} // bad choice! }; }
fancy_op{identity}
はidentity()
関数を参照するfancy_op
型の左辺値式であり、コンパイルの時点で、fancy_calculation()
に渡す操作の利用契約と選ばれた関数の契約注釈の両方が、それを検証しようとするツールで利用可能になります。ただし、この提案ではこのような検証までも提案はしていません。
3. 型システムによって、使用する関数が選ばれた時点から呼び出される時点までの保管チェーンを作成し、各ステップで保管されている関数が使用方法に適合していることを表明する
int fancy_calculation( fancy_op& op, int start ); // halve/twiceはfancy_op&引数に束縛できないが、fancy_op{halve}/fancy_op{twice}は束縛できる
fancy_op
のポインタ/参照は、単なるint(int)
のポインタ/参照あるいはほかの関数利用型のポインタ/参照とは型レベルで異なります。想定する実装では、fancy_op
のポインタ/参照はint(int)
へのポインタ/参照と同じ実行時表現を持ち、型の区別はコンパイル時とリンク時の処理のために使用されます。
4. fancy_op
のポインタ/参照を介して間接呼び出しが行われる場合、fancy_op
で記述されている要件がその呼び出し時に満たされているかチェックされる
ここでチェックされる条件は参照先にある関数の契約注釈とは無関係にチェックされるもので、fancy_op
の利用側が課す追加条件となる。
int fancy_calculation( fancy_op& op, int start ) { ... int next = op( current ); // 実行は次の順序で行われる: // fancy_opの事前条件チェック // 参照先関数の事前条件チェック // 参照先関数の本体実行 // 参照先関数の事後条件チェック // fancy_opの事後条件チェック ... }
例えばopにtwice
が使用されて、twice()
の事前条件違反とfancy_op
の事後条件違反が起こると、twice()
を選択した誤りがここで検出されます。
この提案はこのように、契約注釈を指定可能な新たな関数型を導入して、それに対する関数参照/ポインタを導入することで、参照される関数の契約とは別に、個別の関数参照/ポインタの用途ごとに特化した契約を持つ関数参照/ポインタを作成できるようにしようとするものです。
この提案では、契約が関数の型を変更すべきではなく、型システムに複雑さを持ち込むべきではなく、既存のコードベースへの統合に過度の変更を要求すべきではないという設計原則を重視しており、これらを達成することでこの提案の変更は、C++プログラムにおける契約と関数ポインタのより堅牢でユーザーフレンドリーな相互作用を目指しています。
この提案はひとまず、MVP後の契約プログラミング機能の拡張案として検討していくことがSG21で確認されています。
P3273R0 Introspection of Closure Types
静的リフレクション機能で、ラムダ式のクロージャ型からの情報取得をサポートする提案。
ラムダ式のクロージャ型はあまり詳細にその型の性質について指定されておらず、特に参照キャプチャした場合にクロージャ型の非静的メンバ変数に対応する何かが存在するかどうかは未規定とされています。これはラムダ式の実装において最適化を行うことを意図したもののようです。
しかし、C++26へ向けて進行中の静的リフレクション機能(P2996)ではこのために、クロージャ型のレイアウトに関するリフレクションの適用をどうするかが問題となっているようです。
クロージャ型は計算とそれに関するデータをひとまとめにパッケージングして扱うための簡単な方法であり、標準アルゴリズムをはじめとしてC++プログラミングの様々な部分で広く使用されています。特に、CUDAにおいては、GPUでアルゴリズムを実行することを重視しており(標準並列アルゴリズムやThrust)、クロージャ型を特別に扱ってホストとデバイスの両側で透過的に使用できるようにしています。
クロージャ型を静的リフレクションで支障なく扱えることは、GPU以外のメモリアドレス空間を跨いでデータや計算をマーシャリングする必要があるコード(ネットワーク、プロセス間通信、シリアライズなど)においても有用となります。
このような機会を閉ざさないようにするために、この提案はクロージャ型が参照キャプチャした場合のレイアウトを予測可能にすることで、リフレクションにおける適用可能性の問題を解消しようとするものです。
この提案では、参照キャプチャしたラムダ式のクロージャ型に関する現在の規定(参照キャプチャに対応する非静的メンバ変数が存在するかは未規定)を変更して、参照キャプチャの場合でも参照キャプチャした物ごとに参照メンバが存在することを規定(無名かつ宣言準は不定)するようにするものです。
この提案では、既存の実装は全て参照キャプチャを参照またはポインタをクロージャ型のメンバとすることによって実現しているため、この変更によって振る舞いの修正が必要となる実装は存在しないか、存在したとしても大きな影響はないとしています。また、クロージャ型の最適化についても、この変更と静的リフレクションの導入後に、静的リフレクションによってクロージャ型の情報を取得されていたとしてもそのラムダ式を最適化することを妨げることはないとしています。
この提案はSG7を全会一致でパスしてEWGに転送されたようです。
P3274R0 A framework for Profiles development
安全性向上のためのプロファイルをC++に導入するための開発フレームワークについて解説する文書。
これは以前にP2687R0等で示されていた、プロファイル(ブロックなどに対するアノテーション)によって部分的に安全性の保証を強化するアイデアのより具体的なものです。関連するものについては以前の記事等を参照
- P2687R0 Design Alternatives for Type-and-Resource Safe C++ - WG21月次提案文書を眺める(2022年10月)
- P2816R0 Safety Profiles:Type-and-resource Safe programming in ISO Standard C++
この文書は、以前のこれらのアイデアを具体的な機能提案とするステップの最初のもので、その設計思想や大枠について解説したものです。
まず、プロファイルを指定する方法は[[Profiles::enable(ranges)]]
のような属性構文とすることを提案しています。プロファイルはソースコードで明示的に要求する必要があるものでありながら、プロファイルの指定に対して単一のコードベースを維持することができる構文として属性構文が選択されています。例えば、あるソースコードにプロファイルを指定した時でも、プロファイルをサポートしないコンパイラとサポートするコンパイラの両方でコンパイルして実行可能な状態を維持することができます。
そして、このようなプロファイルが指定されたコードでは、その存在そのものや他のオプションによって検証をトリガーできるようにして、静的解析と実行時検査を組み合わせた一定の安全性の保証を得られるようにします。
ここでは、そのようなプロファイルの初期のものとして簡単に達成できるものがリストアップされています。ただし、それらはあくまで初期のものであり、これが完全なものというわけではありません。この文書の目的は、委員会の人々が自分の専門分野に注力しながらも協力できる部分では協力して、段階的にプロファイルを含めた機能を成長させていくことにあります。この文書のいうフレームワークとは、そのような独立した作業を取りまとめる一つの土台として作用するものです。
そのフレームワークの一部として、プロファイル仕様のフォーマットが提示されています
- 名前
- (希望する)プロファイル名
- 定義
- 提供される保証の仕様
- 影響
- 保証を提供するために何をする必要があるか
- 初期バージョン
- ツールチェーンで比較的簡単に実装できる初期/限定バージョンの提案
- 備考
- 必要な場合の追加コメント
- 質問
- 設計に関する疑問
- 詳細な仕様
- 保証の詳細な仕様と、保証を実施するために必要なテストがどこにあるか
このフォーマットに従って、ここであげられている初期プロファイルは次のものです
- Type
- 定義: すべてのオブジェクトはその定義に従ってのみ使用される
- 影響
- 静的チェックと実行時チェックを組み合わせを必要とする強力な保証
- Ranges、Invalidation、Algorithms、Casting、RAII、Unionなどのより単純なプロファイルの結合となる可能性がある
- 初期バージョン: Initialization、Pointers、Rangesから開始し、他の初期プロファイルが利用可能になったら追加
- 備考
- 質問
- Arithmetic
- 定義: オーバーフローとアンダーフローはエラーをトリガーし、縮小変換はエラーをトリガーする
- 影響: コンパイラーがチェック済み算術演算を使用する必要がある
- 初期バージョン: チェック済み算術演算ライブラリを使用できる。Arithmeticプロファイルが使用されている場合、コードジェネレーターがそのようなライブラリを使用する可能性がある
- 備考
round()
とtruncate()
が必要。チェック済み算術演算ライブラリは存在するが標準ではない- 式内で符号付きと符号なしを混在させるとエラーの重大な原因となる。特に、標準ライブラリのサイズに符号なし整数型を使用すると困難な問題が発生する
- 質問
- 「Arithmetic」は「Cast」を暗示する必要があるか?
- 推奨される回答:はい
- 「Arithmetic」は「Cast」を暗示する必要があるか?
- Concurrency
- 定義: データ競合がない。デッドロックがない。外部リソース(ファイルなど)の競合もない
- 質問
- ロックの競合が原因で発生する優先順位の逆転や遅延も処理する必要があるか?
- 推奨される初期回答:いいえ
- ロックの競合が原因で発生する優先順位の逆転や遅延も処理する必要があるか?
- 備考:
- このプロファイルは提案されているプロファイルの中で現在最も成熟していない。プロファイルに関連する作業は特に行われていないが、並行処理の問題は他のコンテキスト(コアガイドラインやMISRAC++など)で集中的に調査されているため、初期作業についていくつかの提案を行うことができる
- スレッド:スコープ関連の問題を減らすために、
thread
よりもjthread
を優先する - ダングリングポインタ:
jthread
をコンテナーとみなし、リソースの有効期間(RAII)と無効化に関する通常のルールを適用する - エイリアシング
- 無効化:
unique_ptr
と、無効化操作を行わないコンテナ(gsl::dyn_array
など)を使用してスレッド間で情報をやり取りする - 可変性:
const
ポインタを渡す(および保持する)ことを優先 - 同期
- スレッド:スコープ関連の問題を減らすために、
- ロックフリープログラミングを検討する必要があります
- このプロファイルは提案されているプロファイルの中で現在最も成熟していない。プロファイルに関連する作業は特に行われていないが、並行処理の問題は他のコンテキスト(コアガイドラインやMISRAC++など)で集中的に調査されているため、初期作業についていくつかの提案を行うことができる
- Ranges
- 定義:
[]
を使用した範囲外添字アクセスはエラーをトリガーする - 影響
- 生ポインタの添字アクセスは許可されない
- 実行時チェックが常に実行されるように、UBに基づくタイムトラベル最適化を排除する必要がある
- 初期バージョン: 生ポインタの添字アクセスを禁止し、
vector、span、view、string
の添え字アクセスをチェックする - 質問
std::array
はどうする?- 「Algorithms」は「Ranges」の一部にする必要がある?
- 推奨される回答:はい
- Pointers(§3.5)は Ranges(§3.4)の一部にする必要がある?
- 推奨される回答:いいえ、または部分的に
- 備考: すべての添字アクセスをチェックするだけでは、多くのアプリケーションにとって実行時コストが許容できないものになる。したがって、個々のチェックは操作が範囲内であることを確認するチェック(単一のチェックによる)でサポートする必要がある。これは、範囲ベース
for
ループとrange
アルゴリズムを強く推奨することを意味する
- 定義:
- Pointers
- 定義
- 影響: 信頼できる領域の外(
span
の外側など)でのポインタ演算を排除 - 影響: ダングリングポインタを排除する
- 初期バージョン
- 備考
- ポインタに提供される保証はすべて、組み込みポインタではないがオブジェクトを識別するオブジェクトにも同様に適用される必要がある
- たとえば、ラムダキャプチャや
unique_ptr
など
- たとえば、ラムダキャプチャや
- すべての間接参照操作をチェックするだけでは多くのアプリケーションにとって実行時コストが許容できないものになる。したがって、多くの用途でポインタを1回だけチェックする手法をサポートする必要がある
not_null
はその方法の1つであり、スマートコンパイルは別の方法
- ポインタに提供される保証はすべて、組み込みポインタではないがオブジェクトを識別するオブジェクトにも同様に適用される必要がある
- Algorithms
- 定義
- 影響
- 初期バージョン
- 備考: 無限の範囲(一部の
ostream
など)と自動的に拡張される範囲(back_inserter
への書き込みなど)では、(この抽象化レベルでは)範囲チェックは必要ない - 詳細な仕様: このような仕様の初期段階については、付録を参照
- Initialization
- 定義: すべてのオブジェクトは明示的に初期化される(デフォルトコンストラクタも含めて)
- 影響: すべてのメンバーが(一度だけ)初期化されていることを確認するために、コンストラクターを(再帰的に)チェックする必要がある
- 初期バージョン:
- 初期バージョンでは、コンストラクターがすべてのメンバーを適切に初期化すると単純に信頼する
- 最終バージョンでは、静的解析を使用して、すべてのメンバーが初期化されていることを確認する
- 備考
- この保証は、すべてのプロファイルに不可欠
- 保証された初期化により、UBが大幅に削減される
- 初期化を保証するための、より巧妙で制限の少ないスキームは可能だが、実装とプログラマによる管理が難しくなる
- 暗黙的な初期化による明示的な初期化の回避はそれ自体がエラーの原因となる。これは、デフォルト値が有効であっても間違っている可能性があり、すべてのユーザー定義型に適切なデフォルト値があるとは限らないため
- パフォーマンスが重要なアプリケーションでは初期化の強制が望ましくない場合がある
- チェックの抑制や注釈、または特定の初期化されていない型等の方法によって処理する必要がある
- Casting
- 定義: キャストは、その定義に反する方法でオブジェクトへのアクセスを許可しない。キャストはそのソースと等しくない値を持つ結果をもたらさない(縮小変換の禁止)
- 初期バージョン: すべてのキャストを禁止する。実行時チェックされたキャスト(
gsl::narrow<T>(x)
など)を提供して、縮小変換と符号の問題の発生に対処する。 - 備考:
dynamic_cast
は安全だが、ポインタの結果のチェックを強制する必要がある- チェックされない無条件のキャストが必要な場合は、この保証を抑制する
- 型指定されていない生のメモリから型指定されたオブジェクトに変換するには、キャストが必要
- Invalidation(無効化)
- 定義: 無効化された参照またはイテレータを介したアクセスはない
- 初期バージョン
- コンパイラーは、コンテナの要素への参照が取得されている場合、コンテナに対する非
const
関数の呼び出しを禁止する - 大量の誤検出を避けるために、
[[non-validating]]
属性が必要 - 初期バージョンでは、コンテナ要素の1つだけへの参照を無効化する可能性がある単純で直線的なコードのみを許可する
- コンパイラーは、コンテナの要素への参照が取得されている場合、コンテナに対する非
- 備考:
- 型解析とフロー解析の両方を含む本格的な静的解析が必要
- ここでの参照とはオブジェクトを参照するすべてのものを指し、コンテナとは値を保持できるすべてのものを指す
- このコンテキストでは
jthread
はコンテナとみなされる
- このコンテキストでは
- RAII
- 定義: リソースリークがない
- 初期バージョン: 取得して後で解放する必要があるすべてのリソースは、スコープ付きオブジェクトによって表される必要がある
- 備考: 静的オブジェクトが所有するリソースは明示的に解放されない限り永久に存在する
- リソースの例:メモリ、ロック、シェーダ
- Union
- 定義: 共用体のすべてのフィールドは初期化されている場合にのみ使用可能
- 初期バージョン: 実行時チェックされた共用体(
std::variant
など)を使用する - 備考
- パターンマッチングが必要
- その場合、ルールは「共用体を使用せず
match
を使用する」となる
- その場合、ルールは「共用体を使用せず
- タグを伴わない共用体の重要な使用例がある。たとえば、文字列の長さによって実装を選択するSSOなど
- パターンマッチングが必要
このプロファイルアプローチをより信頼に足るものにするためには、これらのプロファイルのうちいくつかを正確に指定し、実装しテストする必要があり、この文書では次の作業として以下の事を進めていくことを推奨しています
- プロファイルの構文を決定する
- 実行時違反を示す構文を決定する
- Initializationプロファイルを指定し、短期的に実行可能なものを決定する
- Rangesプロファイルを指定し、短期的に実行可能なものを決定する
- Castプロファイルを指定し、短期的に実行可能なものを決定する
- 短期的に実行可能なPointerプロファイルのサブセットを考案する
構文さえ決まれば残りの作業やここに上がっていないプロファイルに関する作業を並行して行うことができるとしています。この提案は緊急性を重視しており、すぐに標準に導入することができて、すぐに使い始められることに重点を置いています。
P3275R0 Replace simd operator[]
with getter and setter functions - or not
std::simd
の添え字演算子の問題点と代替案について検討する文書。
std::simd
(basic_simd
とbasic_simd_mask
)には添え字演算子([]
)が用意されており、これは配列やコンテナ同様に個々の要素の読み書きを行うものです。
std::simd<int> x = 0; x[0] = 1; // OK int y = x[0]; // OK x[0] = y; // OK auto z = x[0]; // OK, but : #1 z = 2; // ill-formed #2
basic_simd
とbasic_simd_mask
型それぞれのvalue_type
の値は保持しているものの通常はそのオブジェクトを保持しておらず、そのために[]
は左辺値参照を返すことができません。そのため、const
オーバーロードはprvalueを返し、非const
オーバーロードはプロクシ参照を返します。このプロクシ参照は指定された要素の値を書き換えるための代入演算子や複合代入演算子のオーバーロードを実装しており、それによって代入や演算が可能になっています。
上記例の#1
では値ではなくプロクシ参照を取得していますが、z
の宣言は参照には見えないため、これへの代入を防止するために#2
のような操作はill-formedとなるようにされています。
このようなことが無くとも、左辺値参照の代わりにプロクシ参照を使用してしまうと、[]
仕様に伴って意図しないエラーが発生しやすくなります([]
の結果を直接関数テンプレートで推論した場合など)。
std::simd
の実装では指定された型による配列を保持することを要求しておらず、実装定義の組み込み型を使用している可能性がある他、SIMDレジスタに直接的にマッピングされることさえも禁止していません。basic_simd_mask
の場合、現在のSIMD命令の実装におけるハードウェアのマスクの実装が基本的にはビットマスクを使用する(1要素=1ビットや、1バイト=1ビットなど)ことから、ほぼ確実にbool
の配列をメンバとして持つ実装にはなりません。
この提案はまず、basic_simd
とbasic_simd_mask
では添え字演算子を削除した方が良いのではないか?と提言しています。そして、プロクシ参照を削除したいが添え字演算子を残したい場合のために代替案について検討しています。
[]
を読み取り専用にするbegin()/end()
によるランダムアクセス範囲としての添え字アクセス- 2と似たアプローチとして、配列へ変換可能にする
- セッター/ゲッター関数を追加する
- element_reference型による値への参照を返すようにする
筆者の方の経験では、basic_simd
とbasic_simd_mask
型での添え字演算子の仕様は直感的かつ自然であり、典型的な添え字演算子の使用の9割は読み取り専用であると報告しており、これをもとに、非const
の添え字演算子のみを削除するようにすることを推奨しています。そして、それが受け入れられない場合はelement_reference型を追加してそれを返すようにするアプローチを押しています。
とはいえ、この提案は問題点の共有と代替案の検討を行っただけで、まだどれかのアプローチを提案するものではありません。
LEWGのレビューでは、[]
を読み取り専用にして非const
の添え字演算子のみを削除するアプローチが好まれたようです。
P3276R0 P2900 Is Superior to a Contracts TS
契約プログラミング機能をTSを経由するのではなく、直接標準へ導入すべきという提案。
P3265R0(上の方にある提案)にて、契約プログラミング機能はC++26を目指すのではなくTSを一旦目指すようにすることが提案されています。この提案は、TSにするメリットとコストを考察したうえで、(C++26かはともかく)直接標準への導入を目指すことを提案するものです。
TSを一旦経由することで得られるメリットとしては次のものがよく挙げられます
- 実装経験が得られる
- 使用経験が得られる
- 設計についてのWG21全体の合意が得られる
- 既存の機能との一貫性と適用性の向上
- 安全性の向上
この提案では、これらのメリットはTSを経由しない形でも得ることができるため、TSをを目指す理由にはならないとしています。
むしろTSとして発行することで、次のようなコストが余分にかかってきます
- ベースとなる標準のリベース作業
- 文書の二重管理
- 議論の優先順位
- EWGにおけるTSの議論時間は、必然的に標準そのものへの変更に関するものよりも優先順位が低くなる
- TSを公開するためには限られた対面会議の時間枠の極々一部の時間だけしか使えないため、コンセンサスを得るには余分な労力がかかる
これらのことから、TSを出荷することで契約プログラミング機能を標準に導入するための問題が解決できるというエビデンスは無く、TSを経由することで全世界のC++ユーザーにとって利益のあるC++プログラムの安全性・正確性を向上させる機能の提供が遅れるだけであり、現在のC++を取り巻く環境を考慮するとこの遅れはC++の安全性向上への取り組みが真剣ではないと受け止められる可能性があるとして、現在の計画通りにTSを経由せずに直接標準へ導入することを目指すことを推奨しています。
提案では、TSを目指す理由として少なくとも次の疑問に応えるべきとしています
- TSをリリースすることで解決しようとしている疑問点
- TSによってそのような疑問点に回答する方法
- P2900をこのまま(標準へ)進めてもそれに回答できない理由
- TSを発行した結果としてC++の契約プログラミング機能が採択された場合、その時の出口戦略はどうなるか
現在の契約プログラミング機能のTS化の提案にはこれらの点が述べられているものはまだ無いようです。
P3278R0 Analysis of interaction between relocation, assignment, and swap
リロケーションの代入やスワップとの関係性を考察する文書。
現在のリロケーションに関する提案は、最適化を目的として標準でリロケーションとトリビアルなリロケーションという操作を定義しようとしています。これによって、標準ライブラリを含めたライブラリの実装で代入の観点から実装されている一部の操作をリロケーションで置き換えることで最適化しようとするものです。
この文書は、主に生存期間の観点からリロケーションと代入・スワップの違いを考察するものです。
代入とリロケーション
vector::erase
やvector::insert
等の操作をリロケーションとみなすことは一般的ですが、標準ではこのような操作がオブジェクト全体を再配置するのか、値だけを移動させるのかについて不透明です。実際には、これらの再配置操作は代入を使用して実装されています。標準ライブラリの仕様では、要件や計算量の規定によってそれを明示していることすらあります。
代入及び構築は値のみを移動させる操作であるためこれまで区別されてはおらず、古いオブジェクトで何が起こるのかについてはあまり考慮されてきませんでした(何でも起こり得たものの、代入という意味さえ達成できていれば何が起きていても良かった)。しかし、リロケーションの場合は明確に古いオブジェクトが破棄されるため、この区別が重要になります。
例えば、IDと値を保持するオブジェクトを考えます。IDは一意である必要があるため、代入操作に伴ってはIDを移動せずに値のみを移動させる必要があります(よって、ユーザー定義が必要です)。オブジェクトのIDが重要であるため、このようなオブジェクトの代入はIDをコピー/ムーブしません。しかし、リロケーションの場合、その操作の後で古いオブジェクトが破棄されることが分かっているためIDは重複せず、この場合はIDを再利用することができます。IDと値が共にリロケーション可能である場合この型全体もリロケーション可能であり、このような型の作成者は型をリロケーション可能としてマークする必要があります。結局、古いオブジェクトの破棄はリロケーションの一部です。
P3236では、再配置の実装の一部として代入を使用する既存実装の問題点と、現在のセマンティクスを維持したいという要望の両方を認識しています。全てのリロケーション操作不必要に悲観的に扱うことなくその機能を提供するには、代入時に起こる事とリロケーション時に起こる事を正しく区別する必要があります。そしてこれは、スワップにも当てはまります。
swapとリロケーション
標準は、スワップは値を交換するものとしています。特定の型の場合、値とはオブジェクト表現のことであり、オブジェクト表現と値が完全に一致している場合、スワップはビット単位の交換操作として実装することができます。このような説明ではどのような型が最適化されたスワップ操作を持つことができるかという事だけを言っています。
最適化スワップのもう一つの重要な事項は、オブジェクトの生存期間を考慮することです。
生存期間の考慮事項を無視すると、スワップをビット単位の交換操作で実装するには次の要件を満たす必要があります
- 型はトリビアルにリロケーション可能である
- 代入の代わりにビットコピーを使用する場合でも、セマンティクスが変更されない
2つ目の要件は、再配置操作に代入を使用している現在の実装(リロケーション以前)を最適化するために必要な要件と同じものです。しかし、現在の議論では、暗黙的なトリビアルリロケーション可能性という概念と、「トリビアルリロケーションは代入に使用できる」という概念が混同され、その結果「トリビアルリロケーションはスワップに使用できる」という概念を混同しています。
P1144の暗黙的なトリビアルリロケーション可能性の定義では上記1と2を満たすことが保証されますが、明示的にトリビアルリロケーション可能とマークされている型ではそのような保証は成り立ちません。
既存再配置操作とスワップの間にはもう一つ重要な違いがある事を認識する必要があります
このため、明示的にトリビアルリロケーション可能とマークされた型がトリビアルリロケーションによってスワップを実行できるセマンティクスを持っているかどうかを知る方法が必要になります。
先程の、IDと値を保持する型の例を思い出します。このような型がリロケーションされると、ソースオブジェクトは破棄されているためIDの再利用が可能になるため、IDは保持されます。一方、このような型の2つのオブジェクトのスワップにおいては、代入によって実装されている(現状のベースライン)場合はオブジェクトのIDに変化はなく、2つのオブジェクトの間でその値だけが交換されます。結果、IDは交換されませんが値は交換されます。
つまり、この型がトリビアルにリロケーション可能とマークされている場合でも、リロケーションが提供する保証(ソースオブジェクトの破棄)が無くなったことによってそのセマンティクスが変化してしまっており、スワップをリロケーションによって最適化することはできません(先の要件2は満たされない)。
従って、ムーブ代入+ムーブ構築で行われているスワップ操作は単純にリロケーションによって置き換えることはできません。それにはP3239R0で提案されているように、リロケーションとスワップを区別して認識可能にする必要があります。この文書では、構築と破棄が代入と同等である型を切り分けることを今後の方向性として推奨しています。
また、そのような切り分けの方法とは無関係に、ビットコピー(memcpy/memmove
)操作自体がオブジェクトの生存期間に影響を与える一方でスワップ操作は生存期間に影響を与えないという事実から、トリビアルリロケーション、またはより具体的にビットコピーによってスワップを実装するには、スワップされるオブジェクトの生存期間を終了しない方法が必要となります。P3239ではそのためにリロケーションを用いてスワップするもののオブジェクトの生存期間を中断しない特別な関数を提案しています。
追加の問題
この文書では、このような考察の仮定で浮かんだ2つの問題点についての報告と推奨事項の提案を行っています。
まず1つ目は、トリビアルなリロケーション可能性の伝播についてです。ユーザーが明示的に型をリロケーション可能であるとマークする方法には次の3つのオプションがあります
- 対象の型のすべてのサブオブジェクトがリロケーション可能かどうかをチェックし、一部でもリロケーション可能ではないならば、その型はリロケーション可能としてマークできない
- 全てのサブオブジェクトが明示的にリロケーション不可能としてマークされていない場合にのみ、その型はリロケーション可能としてマークできる
- 型の作成者はサブオブジェクトの状態に関わらず、型をリロケーション可能としてマークすることができる
2と3は現在のコードからの移行を容易にしますが、1が最も安全なオプションです。1を採用してから2,3へ緩和することはできるものの、逆はできません。また、2,3ではサブオブジェクトのプロパティが時間の経過によって変更される場合があるため、実質的に管理することが不可能になります。
もう一つの問題は構文についてです。P3236ではそのような型に対するマーキングの方法として属性構文を提案しており、属性ならば以前の言語バージョンでも(コンパイラがそれを認識することで)これを有効化できるとしています。ただし、属性の無視可能性についてはEWGで何度も確認されており、属性は無視された場合でもセマンティクスに影響を与えない必要があります。
トリビアルリロケーションは型のコンパイル時のプロパティであり、型特性を通して観察可能であるため、セマンティクスに影響を与えます。属性でありながらセマンティクスに影響を与えてしまうものとしては[[no_unique_address]]
が知られており、このABI非互換の影響によってMSVCはしばらくこれを実装しないことを選択しており、この間違いを繰り返さない必要があります。また、Clangの実装者も今回のリロケーション関連のマーカーとして属性を利用することに反対を表明しています。
EWGではここでもその議論を蒸し返さないことが確認されており、この文書でも属性を使用しないことが推奨されています。
なお、この2つの問題点の推奨事項は、この提案のメインの部分とは別の問題であるため、さらなる検討が必要な場合は提案を分けて議論することが示されています。
この文書で提示されている少なくとも合意が必要なこととしては
- 型をトリビアルにリロケーション可能としてマークするためには属性か別の構文のどちらを使用すべきか?
- 暗黙のトリビアルなリロケーション可能性は代入演算子の影響を受けるべきか?
- トリビアルにリロケーション可能としてマークされた型はに対する制限は何か?
- スワップでリロケーションを使用する場合の生存期間の問題への対処
を挙げています。
この提案はP2786へフィードバックされたようです(何が合意されたのかは不明です)。
P3279R0 CWG2463: What 'trivially fooable' should mean
C++におけるtrivially ~という概念の定義を修正し明確化する提案。
trivially ~という概念(例えばtrivially copyable)については、ライブラリ開発者とコンパイラ開発者、言語設計者の間で三者三様な理解がされています
- ライブラリ開発者
- 型がtrivially copyableである場合、5つのルール(Rule-of-Five)の操作(特殊メンバ関数の事)は全て単純な
memcpy
にまで落とすことができる
- 型がtrivially copyableである場合、5つのルール(Rule-of-Five)の操作(特殊メンバ関数の事)は全て単純な
- コンパイラ開発者
- 全ての型は最初はtrivially copyableであるが、その後に非trivially copyableになる
- 例えば、特殊メンバ関数がユーザー定義であることが判明すると、trivially copyableではなくなる
- また、非trivially copyableなメンバ変数があっても、trivially copyableではなくなる
- 全ての型は最初はtrivially copyableであるが、その後に非trivially copyableになる
- 言語設計者(言語法律家)
- ↑2つは、trivially copyableの物理的な認識、直感によって導かれたものライフライムルールに
- 言語設計者の理解は、標準の規定によって確立されている
- それによって、
T
が代入可能でない場合でもmemcpy
を使用して代入を実行できる
このように、特にライブラリ開発者と言語法律家の間には、trivially copyableの理解について相違点があります。
- ライブラリ開発者は5つの特殊メンバ関数による操作を完全な
memcpy
に落とすことができる場合にのみ、C++が型をtrivially copyableと報告することを望んでいる- 誤検知は致命的
- 言語法律家はC++が型をtrivially copyableではないと報告するのは、それらを
memcpy
することが全く意味をなさない場合にのみであることを望んでいる- trivially copyableでない限り、
memcpy
するのはUB - 基本的に、trivially copyableという性質は、どのような場合でもそのオブジェクトの代わりにバイトをシャッフルすることを許可するもの
- trivially copyableでない限り、
特に、双方が望んでいるものは
- ライブラリ開発者は、誤検知が無く厳密に保守的なtrivially copyableという性質のラベルを求めている
- なぜなら、誤検知は実行時の誤動作を意味するため
- 言語法律家は、誤検知がなるべく少ないtrivially copyableという性質のラベルを求めている
- なぜなら、誤検知は、実際には正しく動作するものの形式的にはUBとなるプログラムを意味する(だけ)なため
そして、現在のtrivially ~という概念とその定義は、ライブラリ開発者の期待するようにはなっていません。
まず、現在のコンパイラはどれも、関数呼び出しでない限りすべての操作をトリビアルなものとして扱い、関数呼び出しの場合はトリビアルな特殊メンバ関数を呼び出す場合にのみトリビアル性が維持されます。特に、memcpy
のようなことを全くしないにもかかわらず、is_trivially_constructible<int, float>
やis_trivially_constructible<bool, int>
のようなものはtrue
になり、集成体初期化もトリビアルとみなされます(関数呼び出しをしないので)。
次に、is_trivially_constructible
とis_trivially_assignable
及び、コピー/ムーブコンストラクタと代入演算子がトリビアルになる条件では、オーバーロード解決の結果をチェックしています。つまり、ここで見られているのはどの特殊メンバ関数が定義されているかではなく、オーバーロード解決によってどの関数が選びだされるかです(これはどちらかと言えばライブラリ開発者の直感に沿っています)。しかし一方で、is_trivially_copyable
及びデフォルトコンストラクタがトリビアルになる条件は特殊メンバ関数の存在によって定義されており(オーバーロード解決の結果は無視される)、これはライブラリ開発者の直感とは異なる部分があります。
さらに、これらのことによって留意すべき点が生じます
- コピーコンストラクタやコピー代入演算子などはテンプレートではないため、一致度の低いトリビアルコピーコンストラクタと一致度の高い非トリビアルコンストラクタテンプレートが共存しうる
- 特殊メンバ関数をトリビアルと呼称するルールは文脈に依存せず、一度トリビアルと判定されればずっとトリビアルになる
これらのことを念頭に、提案ではtrivially ~に関する予期しない振舞の例を3パターン照会しています
1. trivially copyable型は非trivially copyableメンバを持つことができる
struct Hamlet { Hamlet(const Hamlet&) { puts("copied"); } Hamlet(Hamlet&&) = default; Hamlet& operator=(Hamlet&&) = default; }; struct Nutshell { Hamlet h_; Nutshell(Nutshell&&) = default; Nutshell& operator=(Nutshell&&) = default; }; // 標準的にはどちらもパスする(EDGとMSVCはパスする) static_assert(!std::is_trivially_copyable_v<Hamlet>); static_assert(std::is_trivially_copyable_v<Nutshell>);
この例は、Hamlet
はトリビアルコピー不可であるのにNutshell
に包むとトリビアルコピー可能になりmemcpy
出来るようになってしまう例です。
これは、言語法律家の理解からすると間違っているものの、ライブラリ開発者の理解からすると適正な振る舞いであるようです。ライブラリ開発者の立場からはその型で直接提供されている操作のみに着目するため、その観点からは正しい振る舞いとなります。
struct Hamlet2 { Hamlet2(const Hamlet2&) { puts("copied"); } Hamlet2(Hamlet2&&) = default; Hamlet2& operator=(Hamlet2&&); // not defaulted }; struct Nutshell { Hamlet2 h_; Nutshell(Nutshell&&) = default; Nutshell& operator=(Nutshell&&) = default; }; static_assert(!std::is_trivially_copyable_v<Hamlet2>); static_assert(!std::is_trivially_copyable_v<Nutshell>);
この例は、コンパイラ開発者の理解に問題がある(ライブラリ開発者から見て)ことを示す例です。Hamlet2
はムーブ代入演算子が定義されていない(従ってトリビアルではない)点だけがHamlet
とは異なりますが、Nutshell
をトリビアルコピー不可のままにします。
このことは、Nutshell
のtrivially copyable性はメンバ変数型の性質ではなく、コンパイラが感知している他の何かの情報によって決まっていると見ることができます。
2. 型は、trivially copyableであると主張しながら、非トリビアルに動作することができる
struct Plum { int *ptr_; explicit Plum(int& i) : ptr_(&i) {} Plum(const Plum&) = default; // コピー代入演算子(削除済) void operator=(const volatile Plum&) = delete; // テンプレート代入演算子 template<class=void> void operator=(const Plum& rhs) { *ptr_ = *rhs.ptr_; } }; static_assert(std::is_trivially_copyable_v<Plum>); static_assert(std::is_assignable_v<Plum&, const Plum&>); static_assert(!std::is_trivially_assignable_v<Plum&, const Plum&>);
トリビアルコピー可能の定義は、トリビアルな特殊メンバ関数の宣言の存在だけを見ます(ユーザー定義されていないことは必須だが、定義されているかは見ない)。このplam
の場合、ユーザー定義のdelete
コピー代入演算子(もどき)によって暗黙の代入演算子定義が(どちらも)抑制されますが、このdelete
定義代入演算子はシグネチャだけはコピー代入演算子として扱われ、なおかつユーザー定義されていません。このためis_trivially_copyable_v<Plum>
がtrue
になります。
一方、コピー代入操作はテンプレート代入演算子によって行われ、こちらはトリビアルであるとは扱われず、is_trivially_assignable
もfalse
になります。
この例はコンパイラ開発者と言語法律家の両者にとっては想定される振る舞いのようです。一方明らかに、ライブラリ開発者(およびほかのすべての人)にとってはこれは驚くべきことです。
3. トリビアル特殊メンバ関数は、memcpyのように動作しない可能性がある
// 全てがトリビアル struct Cat {}; // 代入演算子はトリビアルではないはず struct Leopard : Cat { int spots_; Leopard& operator=(Leopard&) = delete; using Cat::operator=; }; static_assert(is_trivially_copyable_v<Leopard>); static_assert(is_trivially_assignable_v<Leopard&, const Leopard&>); void test(Leopard& a, const Leopard& b) { a = b; // absolutely no data is copied }
一つ前と同様に、Leopard
はトリビアルコピー可能となります。しかし今度の場合、is_trivially_assignable
もtrue
になってしまいます。なぜなら、Cat
から継承している代入演算子がトリビアルなものかつLeopard
で使用可能として発見されるからであり、この結果としてLeopard
の代入操作はなにも(基底クラスの部分しか)コピーしないものになります。
これはライブラリ開発者にとって非常に問題になります。なぜなら、このような例を正しく検出できないためです。
この提案は、これらのトリビアル性の定義をライブラリ開発者にとって自明なものとなるように修正しようとするものです。具体的には
is_trivially_constructible<T>
は、引数無しでT
を構築する操作をno-opにできる(つまり省略可能)ことを正確に意味する- これは現状通り
is_trivially_constructible<To, From>
は、From
からの完全なTo
の構築をmemcpy
にできることを正確に意味する- 例えば、
is_trivially_constructible<float, int>
はfalse
になる
- 例えば、
is_trivially_constructible<To, Args...>
は、Args
が1より大きいパックの場合常にfalse
になるはずであり、それを非推奨化する- これは物理的に対応するものが無く役に立たない
is_trivially_destructible<T>
は、T
の破棄をno-opにできる(つまり省略可能)ことを正確に意味する- これは現状通り
is_trivially_assignable<To, From>
は、From
からの完全なTo
の代入をmemcpy
にできることを正確に意味する- 例えば、
is_trivially_assignable<float&, int&>
はfalse
になる
- 例えば、
is_trivially_copyable<T>
は、引数のCV/参照修飾に関わらずT
がサポートする特殊メンバ関数の操作をmemcpy
にできることを正確に意味する
最後の提案は、つまり次の事を意味します
// 含意(a => b)を表す constexpr bool implies(bool a, bool b) { return b || !a; } template<class T> inline constexpr bool is_trivially_copyable_v = implies(is_constructible_v<T, T&>, is_trivially_constructible_v<T, T&>) && implies(is_constructible_v<T, T&&>, is_trivially_constructible_v<T, T&&>) && implies(is_constructible_v<T, const T&>, is_trivially_constructible_v<T, const T&>) && implies(is_constructible_v<T, const T&&>, is_trivially_constructible_v<T, const T&&>) && implies(is_assignable_v<T&, T&>, is_trivially_assignable_v<T&, T&>) && implies(is_assignable_v<T&, T&&>, is_trivially_assignable_v<T&, T&&>) && implies(is_assignable_v<T&, const T&>, is_trivially_assignable_v<T&, const T&>) && implies(is_assignable_v<T&, const T&&>, is_trivially_assignable_v<T&, const T&&>) && implies(is_destructible_v<T>, is_trivially_destructible_v<T>);
これらの変更の内容は、ライブラリ開発者の直感に従ったものでもあります。
P3281R0 Contact checks should be regular C++
契約プログラミング機能の現在の仕様を、より一般的な現在のC++にとって自然になるようにする提案。
現在の契約プログラミング機能で選択されている設計は、いくつかの点で一般的なC++コードにおけるものとは異なることがあり、それはおそらく契約プログラミング機能がこのまま導入された場合にそれを使用しようとするC++ユーザーにとって驚くべきものになると思われます。そして、このことはCアサートをはじめとする既存のアサーション/チェック機能から契約プログラミングへの移行を困難にすることが予想されます。
この提案は、契約プログラミングへの移行のしやすさを向上させるために、ユーザーの期待と異なる振る舞いを修正しようとするものです。
この提案であげられている問題点の一つは契約条件の評価回数に関するもので、複数回の評価を認める一方で評価を省略することも認めていることです。
struct X {}; // (どこかにある)辞書(マップ)にxを追加し、それがまだ追加されていなかった場合はtrueを返す bool not_already_processing(X* x); // xを辞書から削除し、削除された場合にtrueを返す bool done_processing(X* x); void f(X* x) { contract_assert(not_already_processing(x)); // f()の再帰呼び出しにつながる可能性がある contract_assert(done_processing(x)); }
このコードのようなチェックは、現在のCアサートならプログラマの期待通りに動作しますが、契約プログラミングの場合は動作しない可能性があります。なぜなら、再帰呼び出しの場合にチェックが省略されたり、再帰呼び出しでない場合にチェックが重複したりする可能性があるためです。
もう一つの問題点は、契約注釈内から参照される自動変数が暗黙にconst
化されることです。
これは契約条件式での副作用の利用を防止することを目的として導入されていますが、これによって、既存のアサートからコードの意味を変えずにcontract_assert
へ移行するのが難しくなるケースがあります。これには、選択されるオーバーロードが変わる可能性も含まれています。さらに、これはあくまで浅いconst
であるので、ポインタの先は変更可能となります。
void f(int* x) pre(x = nullptr) // ng、ポインタそのものはconst pre(*x = 1) // ok、ポインタの参照先は非const { ... }
筆者の方はEDGの人(EDGコンパイラの開発者の一人と思われる)で、EDGコンパイラの実装における各種のアサーションを現在の契約プログラミング機能へ移行することを考えると、これらの点を考慮して既存のチェックがこれらの問題点に引っかかるのかをチェックして回る必要があり、そうすると必然的にマクロのラップなどによって、ある種のカテゴリのチェックはあるフラグによって制御され、残ったチェックはまた別のフラグによって制御される、のようなあまり望ましくないコードを書くことになり、これによってチェックに違反処理メカニズムを共有できなくなる、と述べています。
現在の契約プログラミング機能はC++における契約プログラミング機能の出発点となるものであるため、既存の通常の言語のセマンティクスに基づいたアサーション機能からの移行パスを提供する必要があります。ユーザーが契約プログラミングへ簡単に移行できるようにするためには、そのような通常のC++言語を使用する(注釈内だけ異なる言語動作にならない)契約プログラミング機能が必要です。
この提案ではこのために、現在の契約プログラミング仕様(P2900R6)に対して次の3つの変更を提案しています
- 契約条件の評価の省略をしない
- 契約条件式を評価せずに契約チェックを実行するために、条件式の結果を仮定してチェックを省略する許可を削除
- 契約条件の評価の繰り返しをしない
- 一度評価された契約条件式を、同じまたは異なるセマンティクスを使用して同じ注釈内で再度評価する許可を削除
- 契約注釈内で変数の
const
化をしない- 契約条件式内の自動変数(および構造化束縛、関数の結果値)の名前の式の型に暗黙に
const
が追加されるというルールを削除
- 契約条件式内の自動変数(および構造化束縛、関数の結果値)の名前の式の型に暗黙に
なお、これらの変更は3種類の契約アサーションすべてに対して適用することを提案しています。
提案では、これらの変更はC++26以降の契約プログラミング機能の拡張として扱うべきであり、ラベル指定などの方法によって破壊的変更を伴わずに導入できるはずとしています。
この提案は、SG21のレビューにおいて否決されたようです。
P3282R0 Static Storage for C++ Concurrent bounded_queue
bounded_queue
に、ストレージに使用する領域へのポインタを受け取るコンストラクタを追加する提案。
bounded_queue
とは現在P0260で提案されている並行キューで、可変長ではない(予め最大要素数が決まっている)タイプの並行キューです(P0260にはこれ以外のものは提案されていません)。
bounded_queue
と同じコンセプトのキュー構造はその特性から小規模な組込みシステムでもよく使用されていますが、P0260の場合キューの要素数とアロケータをコンストラクタで渡して実行時にその領域動的確保によって割り当てており、ヒープメモリを使用したくない(できない)環境においては特別なアロケータを実装する必要があるか、それができない場合は全く使用できません。
この提案では、bounded_queue
が静的に割り当てられたメモリ領域を使用できるようにするために、キューの要素数とストレージ領域へのポインタを受け取るコンストラクタを追加することを提案しています。
提案では次のようなコンストラクタを追加しようとしています
bounded_queue(size_t max_elems, void *storage);
このコンストラクタの要件は当然storage
の領域にmax_elems
個の要素を配置可能なことですが、通常そのようなメモリの量は不明(同期用のオーバーヘッドなどがあるため)です。
そのため、そのようなサイズを知ることのできるメンバ関数を追加することを提案しています
static consteval size_t required_size(size_t max_elems); static consteval size_t required_alignment();
required_size(max_elems)
は引数のmax_elems
個の要素数を格納するために必要な領域サイズを返し、required_alignment()
はその時のアライメントについて返すものです。
この新しいインターフェースの使用例
using MyQueueT = std::bounded_queue<uint32_t>; // 静的な領域 alignas (MyQueueT::required_alignment()) std::array<std::byte, MyQueueT::required_size(8)> myQStorage; // 領域ポインタを渡してキューを構築 MyQueueT myQ{8, myQStorage.data()}; int main() { // start tasks and use myQ }
P3283R0 Adding .first() and .last() to strings
std::string
に文字列最初と最後の部分文字列にアクセスするためのAPIを追加する提案。
std::string
(およびstd::string_view
)において、最初のN文字にアクセスするためには.substr(0, N)
とする必要があります。同様に、最後のN文字にアクセスするためには.substr(size() - N, N)
とする必要があります。どちらの操作も単純ではあるものの、N文字を取るのに余計な計算と引数の指定が必要になっており、間違えやすく使いづらい面があります。
この提案は、これらの操作を使いやすくするためにラッパであるメンバ関数.first()
と.last()
を追加しようとするものです。
利点としては
- プログラマの意図がより明確で伝わりやすくなり、可読性が向上する
- 引数は欲しい文字数Nのみなので、エラーの可能性が低減される
- よりシンプルになる
このような関数はstd::span
に既に存在しておりspan
には.subspan()
も同時に存在していることから、この2種の関数をあえて別々に追加するだけの関心があったことが分かるため、現在.substr()
しかないstd::string/std::string_view
でも同様に.first()
と.last()
を追加すべき、としています。
追加する関数の宣言の例
namespace std { template <class charT, class traits = char_traits<charT>, class Allocator = allocator<charT> > class basic_string { ... constexpr basic_string first(size_t count) const &; constexpr basic_string first(size_t count) &&; constexpr basic_string last(size_t count) const &; constexpr basic_string last(size_t count) &&; ... }; }
std::string_view
の場合もほぼ同様の引数を取ります。効果は、上で示した.substr()
のN
に対する結果と同じです(.substr()
を使用してはいません)。
int main() { std::string str = "abcdefghijk"; auto first_substr = str.first(3); // abc auto last_substr = str.last(3); // ijk std::string_view strview = str; auto first_substr_view = strview.first(3); // abc auto last_substr_view = strview.last(3); // ijk }
P3284R0 finally
, write_env
, and unstoppable
Sender Adaptors
新しいsender
アルゴリズムの提案。
この提案は、既に追加されているP2300の非同期処理フレームワークライブラリに対するC++26の拡張を提案するものです。提案されているのは
write_env
unstoppable
finally
の3つです。これらのものは、以前にP3175R0で提案されていたものでしたが、P3175のメインの部分を進めるために分離されました。
write_env
receiver
には実行環境(execution environment)というものが関連付けられており、これは単なるKey/Valueストアです。sender
とreceiver
が接続されたときに、接続されたreceiver
を介してそこから値のクエリ/取得を行って、実行環境に関するもの(例えばアロケータやstop_token
など)を取得できます。sender
によるチェーンでは、そのような実行環境はチェーンの上流から下流に向けて伝播し、何もしなければ上流で設定されたものが下流の処理でも使用されますが、場合によっては途中でそれを変更したいこともあります。write_env
アダプタはそのためのもので、sender
チェーン(の背後にあるreceiver
)の実行環境に値をストアするものです。
これは、write_env(sender, environment) -> sender
のようなアダプタであり、入力のsender
と実行環境environment
を受け取って、これらを格納しているsender
を返します。返されたsender
(sndr
)が(別のsender
アルゴリズムとの接続などによって)receiver
(rcvr
)と接続されると、単にそれらを接続した結果のreceiver
を返します(通常のチャネルへの関与は行わない)。
ただし、そうして返されたreceiver
の実行環境はenvironment
とrcvr
の環境が結合されたものになっており、そのreceiver
へのクエリはまずenvironment
に対して行われ、そこで該当するものが見つからない場合にrcvr
の環境にクエリされます。
提案より、アロケータを変更する例
// クエリキーと対応する値を実行環境のクエリ可能なものに変換するクラス template <class Query, class Value> struct with : Query { Value value; auto query(Query) const { return value; } }; // 指定したアロケータを使用するようにsenderを変化させる struct with_allocator_t { template <std::execution::sender Sndr, class Alloc> auto operator()(Sndr sndr, Alloc alloc) const { return std::execution::write_env(sndr, with{std::get_allocator, alloc}); } }; constexpr with_allocator_t with_allocator{};
このwith_allocator
は次のように使用します
namespace ex = std::execution; ex::sender auto make_async_work_with_alloc() { // サードパーティのライブラリによって作成される非同期作業 ex::sender auto work = third_party::make_async_work(); // その作業で使用されるアロケータをカスタマイズする return with_allocator(std::move(work), custom_allocator()); }
unstoppable
unstoppable
アダプタはwrite_env
同様に実行環境を変化させるもので、これの場合はstop_token
をnever_stop_token
に変更することで、入力のsender
が外部からの停止要求に応じないようにします。unstoppable(sender) -> sender
の様なシグネチャになり、これもメインのチャネルには関与しません。
これはwrite_env
を利用して実装することができます
namespace std::execution { inline constexpr struct unstoppable_t { template <sender Sndr> auto operator()(Sndr sndr) const { return write_env(std::move(sndr), never_stop_token()); } auto operator()() const { return write_env(never_stop_token()); } } unstoppable{}; }
finally
C++言語には非同期破棄の直接のサポートが無く、「この非同期処理の後で最初の処理がどのように終了したかに関係なく、無条件で別の非同処理を開始する」ということを(安全に)行う方法がありません。これによって、非同期RAIIを実現することができません。
finally
アダプタは、sender
の領域で非同期RAIIパターンを実現するものです。finally(sender, sender) -> sender
の様なシグネチャで、2つのsender
(非同期処理)を受け取って、返されたsender
が接続を受けて開始されると、まず1つ目のsender
の処理を開始します。それが完了すると、結果を保存してから2つ目のsender
を開始します。このsender
の結果としては、2つ目のsender
の処理が正常に完了した場合1つ目のsender
の結果が返り、それ以外の場合は2つ目のsender
の結果が返ります。
提案文書より、非同期処理の過程でプログラム不変条件が一時的に破棄された後で復元される例
namespace ex = std::execution; ex::sender auto break_invariants(auto&... values); ex::sender auto restore_invariants(auto&... values); // この関数はsenderアダプタクロージャオブジェクトを返す // senderを入力すると、不変条件を破棄してデータを変更して、その後不変条件を復元する新しいsenderを返す auto safely_munge_data( ) { return ex::let_value( [](auto&... values) { return break_invariants(values...) | ex::then(do_munge) // `do_munge`が例外を送出した場合でも不変条件は復元される | ex::finally(ex::unstoppable(restore_invariants(values...))); } ); } auto sndr = ...; scope.spawn( sndr | safely_munge_data() ); // See `counting_scope` from P3149R2
Java言語等にあるtry-catch
に対するfinally
のようなことをsender
チェーンで行うものです。
P3285R0 Contracts: Protecting The Protector
契約注釈の条件式からUBを起こしうる操作を可能な限り減らす提案。
現在の契約プログラミング機能において、契約条件式はほとんど通常のC++コードの式と異なる振る舞いをしません(契約注釈内から参照される外部の自動変数が暗黙const
化したり、関数引数の参照に制限があったりはします)。それによって、契約条件式は任意の副作用を発生させることができ、未定義動作を起こす可能性もあります。
契約プログラミング機能はC++コードの安全性向上に寄与するはずの機能であるのに、その契約条件式が特に制限されていないことによって安全ではない可能性があるというのは度々議論になっており、過去のバージョンでは副作用禁止が明記されていたこともありました。しかし、そのような制限はC++言語のサブセットを生み出すだけでその実現可能性も低いと考えられたため、特に制限を行わないことになっています(ただし暗黙const
化や評価回数の規定なしなどで間接的に禁止している)。
この提案はそれに対して異を唱え、契約プログラミング機能をより安全にするために契約条件式の副作用や未定義動作を抑制しようとするものです。ここでは、現在の契約プログラミング機能の提案(P2900R6)に対して次の2つの変更を提案しています
- 事前条件と事後条件を、非緩和契約(Non-relaxed contracts)と緩和契約(Relaxed contracts)の2つのグループに分類する
- コンベア関数の概念の導入
緩和契約は、事前条件と事後条件を指定する際にrelaxed
という修飾を行うものです。
int rem(int x, int y) pre relaxed(event_log(y), y != 0) { return x % y; }
この場合、rem()
の呼び出しに伴う事前条件チェックにおいては、y != 0
のチェックの前にevent_log(y)
の実行(すなわち副作用)が許可されます。また、緩和契約ではここで提案されているその他の保証は一切適用されません。すなわちこれは、現在の契約機能の振る舞いです。
非緩和契約とは、現在のP2900R6の構文を使用して、契約条件が評価されたときに副作用がその評価コーンの外側に出ないように設計されているという追加的な制約を持つものです。さらに非緩和契約の条件の評価は特定のクラスの未定義動作の影響を受けないことが保証されます。
非緩和契約では、コンベア関数とコンベア関数で許可されている操作のみが許可され、関数引数が変更されないことを保証するために、非緩和契約のなされている関数の引数は、その内部で別の関数に渡される場合は値で(コピーして)渡すか、const
参照に渡すかのどちらかでなければならず、キャスト等のそのほかの変更を伴う操作は許可されません。
コンベア関数は非緩和契約において未定義動作を抑止するためのキーとなる概念で、呼び出された場合に関数の外部に対して副作用を及ぼさない関数の事です。さらに、コンベア関数内部では可能な操作が制限され、未定義動作を発生させうる操作が禁止されています。
コンベア関数は[[conveyor]]
属性を使用して宣言された関数です。
[[conveyor]] int add(int x, int y) { return x + y; } [[conveyor]] int inc(int& x) { return ++x; }
このように宣言された関数内では、未定義動作を発生しうる特定のクラスの操作が禁止(出現したらコンパイルエラー)されます。
[[conveyor]] int deref(int* p) { return *p; // error: pはオブジェクトアドレスを保持しているか不明 }
これは次のように標準で提供される述語によってチェックすることで許可されるようになります
[[converyor]] int deref(int* p) pre(object_address(p)) { return *p; // OK }
ポインタによるオブジェクトの参照はオブジェクトの生存期間外にアクセスを行う可能性があるため、デフォルトでは禁止されます。object_address()
という述語は標準で提供されるもので、ポインタが生存期間内にあるオブジェクトを指していることを保証するもので、これを用いて事前条件チェックすることでコンベア関数内ではそのポインタの仕様が安全だとみなされます。コンベア関数内では同様に未定義動作を発生させうる多くの操作が禁止されているほか、算術演算オーバーフローでは実行時の挙動を指定して定義された振る舞いしかしないようにすることで未定義動作を回避しようとします。
このような制限により、コンベア関数およびそれを用いた非緩和契約においては未定義動作の発生が抑制され、副作用もその評価の内側に閉じ込められることでその影響を緩和できます。また、緩和契約によって現在のベースラインの振る舞いが維持され、明示的な副作用が許可されます。
P3286R0 Module Metadata Format for Distribution with Pre-Built Libraries
ビルド済みライブラリ配布のためのモジュールメタデータのフォーマットの提案。
ここで提案されているのは、ビルド済みライブラリからそのモジュールのインターフェース(BMI)を得るために必要な情報をまとめたメタデータのフォーマットです。ビルド済みライブラリをモジュールとして配布する際にこのような情報の提供は必須なものとなります。
この提案の作業はP2701R0およびP2577R2に続く作業であり、メタデータのフォーマットのみを提案しています。
libc++で標準ライブラリモジュールの実験的サポートが開始されたことを受けて、このようなメタデータとそのフォーマットが必要となり考案され、ビルドシステムはこれを利用して、標準ライブラリの提供するモジュールのビルド済みモジュールのインターフェースファイル(BMI)を作成することができます(作成できる必要があります)。この提案はこうして考案されたフォーマットを標準ライブラリモジュールだけではなくビルド済みライブラリ全般で使用できるメタデータの標準フォーマットとして提案するものです。
そのため、この提案のフォーマットはlibc++で実装経験があり、CMakeはこれを利用してモジュールのBMIを生成する試験実装が行われています。
このメタデータはP2701R0およびP2577R2で示されている要件に従った上で、libc++における実装時に追加でまとめられた要件に従っています。
- ビルドシステムには、ビルド済みライブラリによって提供されるモジュールを識別する方法が必要
- メタデータファイルの場所
- メタデータには次のものが含まれている
- メタデータには次のものが含まれている場合がある
- その翻訳単位の依存関係であるインポート可能な翻訳単位の論理名
- ベンダー固有の属性
長いのでコピペしませんが、提案にはJSONスキーマによるフォーマットの完全な定義が記されています。
- P2577R2 C++ Modules Discovery in Prebuilt Library Releases - WG21月次提案文書を眺める(2022年05月)
- P2701R0 Translating Linker Input Files to Module Metadata Files - WG21月次提案文書を眺める(2022年11月)
- P3286 進行状況
P3287R0 Exploration of namespaces for std::simd
std::simd
に関するAPIを標準ライブラリ中にどのように配置するのかについてを探る提案。
std::simd
に関しては、permute APIに関する議論の過程で、関連するフリー関数をそのままstd
名前空間に入れてしまうことについて懸念の声が上がったようです。この提案はそれを受けて、std::simd
に関連するものの名前空間名も含めた命名について考察するものです。
あがっているのは次の7つです
- 現状(
std
名前空間直下) - 全ての関数をsimdプレフィックス付きの非メンバ関数にする
- 全ての関数をsimdプレフィックスなしの非メンバ関数にする
- 型以外の全てを名前空間に入れる
- 全ての非メンバ関数を隠蔽フレンドにする
- 全てを単一の名前空間に入れる
- 明らかなオーバーロード以外の全てを単一のネームスペースに入れる
- simdを単一のネームスペースに、SIMDジェネリックインターフェースは別のネームスペースに配置する
この提案ではどれを選択するかを決定していないものの、8つ目(最後)の選択肢を推奨しています。
P3288R0 std::elide
コピーもムーブできないクラス型のprvalueの生成を遅延するライブラリ機能の提案。
コピーもムーブもできないクラス型は標準ライブラリにも存在しており、例えばstd::mutex
やstd::counting_semaphore
などがあります。このようなクラス型のオブジェクトを関数からのreturn
する場合、return
文で直接構築してprvalueを返すようにすれば可能です。
std::counting_semaphore<8> FuncReturnsByValue(unsigned const a, unsigned const b) { return std::counting_semaphore<8>(a + b); // ok } int main() { auto cs = FuncReturnsByValue(1, 2); // ok、コピー省略による直接構築 }
これはコピー省略保証によるprvalueの特別扱いであり、このような関数を利用することでコピーもムーブもできない型の生成を簡単にして、取り扱いの厄介さを軽減することができます。しかしこれを利用してもなお、optional
の.emplace()
のような関数でコピーもムーブもできない型の値を注入することはできません。
int main() { std::optional< std::counting_semaphore<8> > var; var.emplace( FuncReturnsByValue(1,2) ); // compiler error }
これは、.emplace()
の引数がT&&
によって変数を受け取っているためにprvalueが実体化されてしまうことでコピー省略が妨げられ、.emplace()
の内部でそのコピーもムーブもできない型(この例ではcounting_semaphore
)のコンストラクタを呼び出すところで単なるムーブ構築となることでエラーになります。
ごく単純に示すと、.emplace()
は次のように配置new
によって特定領域にオブジェクトを構築しようとします。
template<typename... Params> T &emplace(Params&&... args) { ... ::new(buffer) T( forward<Params>(args)... ); ... }
T
がコピーもムーブできない型であり、先程の例のように関数が返すprvalueから構築しようとする場合、ここでのT
のコンストラクタ呼び出しにおいて渡される引数がprvalueになることはなく、それによって必ずコピーかムーブコンストラクタが探索され、それは見つかることは無いためエラーになります。
組み込みのnew
演算子の場合、その引数列にprvalueがあればコピー省略されるため、これを機能させるには、この場所までprvalueを届ける必要があり、例えば次のような簡単なワークアラウンドによって可能になります
int main() { std::optional< std::counting_semaphore<8> > var; struct Helper { operator std::counting_semaphore<8>() { return FuncReturnsByValue(1,2); } }; var.emplace( Helper() ); // ok }
こうすると、配置new
の引数でこのHelper
から変換して構築するコンストラクタがないことから型変換が考慮され、型変換はcounting_semaphore
のprvalueを返すことから、配置new
の構築においてコピー省略が働き、それによってコピーコンストラクタもムーブコンストラクタも使用せずに構築できるためエラーは出なくなります。
このようなもの例えば、std::vsriant
、std::any
、あるいはコンテナなどがあります。
この提案は、このような働きをする汎用的なクラス型であるstd::elide
の提案です。
int main() { std::optional< std::counting_semaphore<8> > var; var.emplace( std::elide(FuncReturnsByValue,1,2) ); // ok }
提案より、実装例
namespace std { template<typename F_ref, typename... Params_refs> class elide final { using R = invoke_result_t< F_ref, Params_refs... >; static_assert( is_same_v< R, remove_reference_t<R> > ); // F must return by value using F = remove_reference_t<F_ref>; F &&f; // 'f' is always an Rvalue reference tuple< Params_refs... > const args_tuple; // just a tuple full of references public: template<typename F, typename... Params> explicit elide(F &&arg, Params&&... args) noexcept // see explicit deduction guide : f(move(arg)), args_tuple( static_cast<Params&&>(args)... ) {} operator R(void) noexcept(noexcept(apply(static_cast<F_ref>(f),move(args_tuple)))) { return apply( static_cast<F_ref>(f), move(args_tuple) ); } /* -------- Delete all miranda methods -------- */ elide( void ) = delete; elide(elide const & ) = delete; elide(elide &&) = delete; elide &operator=(elide const & ) = delete; elide &operator=(elide &&) = delete; elide const volatile *operator&(void) const volatile = delete; template<typename U> void operator,(U&&) = delete; /* -------------------------------------------- */ }; template<typename F, typename... Params> elide(F&&,Params&&...) -> elide<F&&,Params&&...>; // explicit deduction guide }
P3289R0 Consteval blocks
宣言のコンテキストで任意の定数式を実行するためのブロックの提案。
P2996の静的リフレクションや、P2758R2のコンパイル時のメッセージ出力など、定数式においてその式の評価の外側に出ていく副作用をもたらすような提案がC++26に向けて進行しています。これらの機能が導入されると、必然的に宣言だけが行える領域において特定の定数式を実行させることをしたくなるでしょう。
そのような領域には例えばクラスのメンバ宣言の領域があり、そのような場所で任意の定数式を起動するにはstatic_assert()
を悪用できます
#include <meta> template<typename... Ts> struct Tuple { struct storage; static_assert( is_type(define_class(^storage, {data_member_spec(^Ts)...}))); // ↑の解説 // define_class()は不完全型に対して指定されたメンバで定義を追加する関数 // data_member_spec()は与えられた型のメンバ変数を表すmeta::info()を返す関数 // is_type()は、引数のmeta::infoが型を表しているかを判定する述語 // すなわち、storage型がTs...をメンバとして持つように定義している storage data; };
しかしこれは(式の内容を除いても)何をしているのか分かりづらく、難解です。ここでやりたいことは何かのアサーションではなくて、何か定数式の起動の強制です。
この提案では、これを実現するためのシンプルなconseval{}
ブロックを提案するものです。
これはstatic_assert
の単純なラッパであり、次のようなブロックは
consteval { statement-seq(opt) }
次のように展開されます
static_assert( ( []() -> void consteval { statement-seq(opt) }(), true ) );
これにより、冒頭のコードは次のように書き直されます
#include <meta> template<typename... Ts> struct Tuple { struct storage; consteval { define_class(^storage, {data_member_spec(^Ts)...}); } storage data; };
この例では起動したい式は1つだけですが、より複雑なことをしたい場合のために複数の式を置くこともできます。
P3290R0 Integrating Existing Assertions With Contracts
既存のアサーション機構に契約プログラミング機能を統合する提案。
現在の契約プログラミング機能の一つの特徴として、全ての契約注釈の契約条件のそれぞれの評価が、条件がチェックされる場合に同じ契約条件の以前/将来の評価も含めてその他の契約チェックから独立していることがあげられます。この性質によって、契約注釈のセマンティクスは実装定義となり、実行時にも評価毎に変化することが許可されます。
ただし、標準のassert
マクロをはじめとする既存のアサーション機構や契約チェック機構はこれとは微妙に異なるモデルを持っています。ここのアサーションマクロはそのマクロ自体が定義されるときに参照される、NDEBUG
などの1つ以上のマクロの存在に基づいて、ある翻訳単位内での使用する時はいつでも全ての使用が同じセマンティクスを持ちます。これによって、アサーションのセマンティクスはコンパイル時に認識可能である必要は無く、通常気にされません。
また、契約条件式には通常のC++コードをそのまま書くことができ、これによる契約条件式への副作用の埋め込みを回避するために、自動変数や*this
の暗黙const
化及び契約条件評価回数を不定(0以上の任意の回数)とすることなどのところも、既存のassert
と異なる部分です。
これらのような差異によって、既存のassert
から契約プログラミングへ(特に、assert
からcontract_assert
へ)単純に移行することができない場合があります。
現在の契約プログラミング機能の仕様(P2900R7)がEWGに転送されて以降、それまでは検討されてこなかったこの部分(既存機能からの移行のしやすさ)が問題となってきています。
この提案は、既存のアサーション機構を契約プログラミング機能に簡単に移行できるようにするために、既存のアサーション機構へ契約プログラミング機能を組み込むようなソリューションを提案するものです。
提案しているものは次の3つです
- 契約違反ハンドラを直接呼び出せるようにする
- 既存の契約チェック機構をP2900の契約違反処理機構に統合するためのメカニズムを提供する
- なおかつ、(互換性のない可能性のある)既存のセマンティクスを完全に維持する
- 条件付きで
assert
を契約プログラミング機能に統合する- 現在の
assert
マクロの仕様を拡張し、中断(してメッセージを出力する)前に契約違反ハンドラ(例外送出なし)を呼び出せるようにする - これを条件付きでサポートする
- 現在の
- より使い慣れたセマンティクスを持つ新しい形式の契約アサーションを追加する
この3つの提案はそれぞれ独立しており、異なる方法によって既存のアサーション機構から契約プログラミング機能へ移行できるようにしようとするものです。3つのうちの1つ以上を導入することで、assert
の代替機能不足という懸念の大部分が改善され、P2900を先に進める原動力となります。
1. 違反ハンドラの直接呼出し
契約プログラミング機能を採用すべき大きな利点の一つは、プログラムの各所で任意に出現しうるバグ(契約違反)について、それを検出した後の管理(対処、終了、緩和、など)をユーザーがカスタマイズ可能な違反ハンドラによって一元的に取り扱えることです。
3種類の契約アサーションによって検出された契約違反はプログラムのどこで発生したかに関わらず同じ契約違反ハンドラを呼び出します。これは契約プログラミングが言語機能である事の利点の一つでもあります。
現在類似のアサーション機構や契約チェック機構を使用しているユーザーにはそのようなものはありません(あっても、ライブラリ間で異なるものになってしまう)。この提案1は、契約違反ハンドラを直接呼び出すことのメカニズムと、ハンドラ呼び出し後に終了するセマンティクスと同一の方法でプログラムを終了するメカニズムを提供しようとするものです。
まず、次の関数を<contracts>
に追加します
namespace std::contracts { [[noreturn]] void handle_enforced_contract_violation(const char* comment); void handle_observed_contract_violation(const char* comment); }
この2つの関数は名前の通りそれぞれenforced/observed
セマンティクスの下で呼び出される違反ハンドラと同じ動作をするように違反ハンドラを呼び出します。
違反ハンドラには5つのプロパティが設定されますが、これらの関数ではそれは次のようになります
comment
: 関数引数comment
に指定された内容location
: これらの関数を呼び出した場所のstd::source_location
オブジェクトkind
:std::contracts::assertion_kind::manual
(新規追加する列挙値)detection_mode
:std::contracts::detection_mode::manul
(新規追加する列挙値)evaluation_semantic
: 呼び出される関数に対応するセマンティクスの列挙値handle_enforced_contract_violation()
:enforce
handle_observed_contract_violation()
:observe
これらの関数はこのような値を持つstd::contract_violation
オブジェクトを作成し、その環境にインストールされた違反ハンドラを呼び出します。
そして、その実行が完了した後の動作は次のようになります
handle_enforced_contract_violation()
: 実装定義の方法でプログラム終了handle_observed_contract_violation()
: 違反ハンドラから正常にリターンした場合、そのまま復帰する- 違反ハンドラから例外が送出された場合 : 通常通り伝播する
この2つの関数は、契約違反の場合に違反ハンドラを呼び出す全てのセマンティクスについてカバーしており、提案1としてはこれがメインです。ただし、最近追加されたセマンティクスの中にはユーザーサイドでの再現が難しいものがあり、それを再現する呼び出しを行う関数を検討することもできます
namespace std::contracts { [[noreturn]] void handle_quick_enforced_contract_violation(const char* comment) noexcept; }
これはquick_enforce
セマンティクスによる契約違反時の動作を再現するもので、呼び出されると契約違反ハンドラを呼び出さず、速やかに実装定義の方法でプログラムを終了します。
そして最後に、違反ハンドラを呼び出す最初の2つの関数について、noexcept
指定されたオーバーロードを提供することも検討できます
namespace std::contracts { [[noreturn]] void handle_enforced_contract_violation(const char* comment, const std::nothrow_t&) noexcept; void handle_observe_contract_violation(const char* comment, const std::nothrow_t&) noexcept; }
これらの関数はstd::nothrow_t
を受け取らない同名の関数と同じ効果を持ちますが、違反ハンドラから例外が投げられた場合にstd::terminate()
を呼び出してプログラムを終了します。
2. assert
からの違反ハンドラ呼び出し
標準assert
マクロは現実のC++プログラムの多くの場所で使用されており、それらの利用のほとんどのものにはチェックする述語に副作用は含まれていません。副作用をもつような用法も多くがバグかミスでですが、中には実用的で有用な使用法もあります。
前述のように副作用の扱いに関しては特にassert
マクロと契約アサーションでは互換性が無いので、移行に際しては既存のasset
マクロの使用法をすべてチェックして非互換で問題にならないかを確認し、必要なら修正を行わなければなりませんが、これはコストが高く、新たなバグを埋め込むことになる可能性もあります。
提案1のように既存のアサーション機構に一元的な契約違反ハンドラを組み込むことができたとしても、標準のassert
利用者にはその方法は提供されないため、それぞれの組織・開発者が独自の契約プログラミング対応アサーション機構を作成してassert
から移行するしかなく、そのような未来は望ましくありません。
この提案では、C++のassert
マクロの仕様を拡張して、デフォルト動作のエラーメッセージ出力だけではなく、契約違反ハンドラを呼び出すことができるようにするオプションを追加することを提案しています。ただし、デフォルトではオフにして現在の動作を完全に維持し、利用する場合は明示的なオプトインが必要になります。
具体的なスイッチの方法などは指定されていませんが、次のようなものを提案しています
NDEBUG
が定義されていない場合に、マクロに指定された条件式がfalse
を返したとき、その診断結果をstderrに出力するか、違反ハンドラを起動するかを実装定義とする- コンパイラオプション等でユーザーが指定する
- 違反ハンドラが呼び出される場合、その動作は
handle_enforced_contract_violation(#__VA_ARGS__, std::nothrow)
の呼び出しと等価- ただし、
kind
はstd::contracts::assertion_kind::cassert
(新規追加する列挙値)になり detection_mode
は`std::contracts::detection_mode::predicate_false
(新規追加する列挙値)になる
- ただし、
ただし、一部の環境ではassert
マクロの変更が困難である場合があるため、この変更は実装定義とすることを提案しています。
3. assert
互換の契約アサーション
C++の契約プログラミング機能の設計において中心にある考え方として、契約チェックをプログラムに導入してもプログラムの正しさは変わらないはずで、プログラムが正しいか間違っているかを識別できるようになるだけ、というものがあります。契約注釈のセマンティクスは評価毎に異なる可能性があり、それを変更可能であるため、契約注釈の評価においてはその評価がそのほかの契約注釈のすべての評価とは無関係にプログラムの正しさを変更しないことが不可欠です。
assert
マクロではそれとは対照的に、そのセマンティクスはNDEBUG
マクロの状態に基づいて選択されます。NDEBUG
が定義されていればチェックを強制し、定義されていなければ条件は無視されます。したがって、assert
の条件式の評価が同じ翻訳単位内の他のアサーションの評価や#ifndef NDEBUG
等の保護されたブロックに依存するのは完全に合理的です。
従って、assert
マクロの評価における副作用に依存している等の利用法は契約プログラミングにすぐに移行できません。契約プログラミング機能の拡張としてそれをサポートするようにすることも考えられますが、それは少なくとも最初のMVPの後になります。
この提案では、暫定的な解決策として、assert
マクロとよく似た契約注釈構文を追加することを提案しています。これにより、contract_assert
はそのままにしてほとんどのユースケースをカバーする一方で、contract_assert
を利用できないようなアサーションにはこちらを使用することで契約プログラミングへの移行を容易にします。
提案するのは、partial_contract_assert()
という契約注釈で、これはcontract_assert
をベースとしながらも次の違いがあります
partial_contract_assert
は新しいキーワードとして導入- 翻訳単位内の
partial_contract_assert
は、同じ実装定義のセマンティクスで評価される partial_contract_assert
が常に同じ評価セマンティクスを持つようにODRを拡張- チェックを行うセマンティクス(enforce, quick_enforce, observe)を利用して、
partial_contract_assert
を評価する場合、述語は通常通り評価され、省略されない partial_contract_assert
はcontract-assertion sequencesの一部ではないため、他のすべての契約アサーションに対して常に語彙順で評価されるpartial_contract_assert
の評価は繰り返すことができないpartial_contract_assert
内では自動記憶域期間を持つオブジェクトを参照するid式の型は、他の契約アサーションとは異なり暗黙的にconst
にならない
partial_contract_assert
はほぼ、assert
マクロと同じような動作をします。
SG21におけるレビューでは、提案1と2は継続して議論することに合意されたものの、提案3はあまり好まれず、MVP後の機能として検討していくことにも合意が取れませんでした。
P3292R0 Provenance and Concurrency
C++のオブジェクトライフタイムの開始付近での微妙な問題についての改善策の提案。
この提案では、コンパイラが間違ったコードを生成する2つの例を挙げています。
まず一つは、道徳的にはコンパイラが正しいと思われるものの、実際には正しくない例です。
void some_extern_func(int); int* f(int* q) { int* p = new int; *p = 123; // A *q = 456; // B some_extern_func(*p); // C、`some_extern_func(123)`と書き換えてもいい? return p; }
clangとgccはどちらも、O1以上の最適化レベルでこれを間違ってコンパイルし、Cの個所を定数伝播してsome_extern_func(123)
と書き換えます。しかしこれは間違っており、実際にはp
とq
がライフタイムルールに違反することなく等しくなる場合があります。その場合、BではAにおけるストアを上書きします。
当然シングルスレッドなプログラムにおいてはそれは起こらないのですが、並行プログラムにおいてはオブジェクトへのポインタをその作成前の時点に送ることができます。
int* f(int* q) { int* p = new int; *p = 123; // A *q = 456; // B some_extern_func(*p); // C. Can we rewrite this to `some_extern_func(123)` ? return p; } int dummy; std::atomic<int *> ptr_to_int_1{&dummy}; std::atomic<int *> ptr_to_int_2{&dummy}; void t1() { int* q = ptr_to_int_1.load(relaxed); int* p = f(q); // ptr_to_int_1.load()よりも前にこの行を移動できる ptr_to_int_2.store(p, relaxed); } void t2() { // t1のストアをロンダリングして、t1に送り返す int* p = ptr_to_int_2.load(relaxed); ptr_to_int_1.store(p, relaxed); }
t1(), t2()
がそれぞれ別のスレッドで実行されているとします。t1()
を処理するCPUはptr_to_int_2.store(p, relaxed)
を他のすべてのコードよりも前に並べ替えることができます(relaxed
はあらゆる順序付け保証がない)。 その後t2()
はptr_to_int_2
にストアされた値を読み取ってptr_to_int_1
へストアし、t1()
はその後でptr_to_int_1
をロードしてf
に渡す、ということができます。すると、p
とq
は同じ値になります。
f()
内でnew int
と確保されているint
オブジェクトのライフタイムはそのアクセスが起こるよりも前に開始されており、ユーザーはこのint
オブジェクトのライフタイム開始前にそのポインタを操作しているものの、デリファレンスはしていないので、これはライフタイムのルールに違反していません。
2つ目の例は道徳的にコンパイラが間違っており、実際間違っている例です。
void some_extern_function(); int* f() { int *p = new int; *p = 123; // A some_extern_function(); // B return p; }
コンパイラの最適化はこのコードを次のように並べ替える(AとBを入れ替える)ことができるでしょうか?
int* f() { int *p = new int; some_extern_function(); // B *p = 123; // A return p; }
clangは誤って、これに「はい」と答えることがあります。ただし、このコード単体ではそれは起こらず、コードが追加されるとループ分析パスのバグによりこれが起こることがあります。ただし、コンパイラが並べ替えるかどうかという問題は、並べ替えを許可すべきかという問題とは無関係です。これは許可すべきではありません。
これが間違っている理由は、some_extern_function()
の実装でrelease
フェンスが使用されていてsome_extern_function()
が他の場所からも呼び出されている場合に、フェンスを使用した単純なメッセージパッシングが阻害される可能性があるためです。
atomic<int*> atomic_int_ptr; void some_extern_function() { atomic_thread_fence(memory_order_release); } int* f() { int *p = new int; *p = 123; // A some_extern_function(); // B return p; } void message_pass_f() { int* p = f(); atomic_int_ptr.store(p, relaxed); } void receive_message() { int* p = atomic_int_ptr.load(relaxed); atomic_thread_fence(acquire); if (p != nullptr) { assert(*p == 123); } }
receive_message()
において、p
がnullptr
ではない場合(すなわち、message_pass_f()
が実行されている場合)、*p
の値は123
であることがメモリモデルによって保証されています(message_pass_f()
でatomic_int_ptr.store(p, relaxed)
が実行されている場合、release
フェンスとacquire
フェンスの間に順序が成立し、なおかつrelaxed
なロードストアがフェンスを跨がないことで、*p
に123が書き込まれた後でatomic_int_ptr
にセットされ、p
がnullptr
でない値で読みだされた場合は*p
は確実に123になります)。
この場合にAとBを入れ替えると、このメモリモデルによって保証された動作が壊れることが分かります。したがって、このような最適化は厳密には許可されません。
しかし、このようなコンパイラの間違いは許される側面もあります。p
のメモリの割り当ては関数ローカルなものであり、シングルスレッドのセマンティクスに一致する限りそのスコープから外に出るまでは自由に並べ替えられるはずです。むしろ、並行プログラムにおける同期によってそれが阻害され、アクセスできないはずのメモリのアドレスに影響を与えることができています。
この提案は、ポインターのprovenanceを導入することでこれらの様なコンパイラの間違いを遡及的に正当化しようとしています。ポインタのprovenanceとは、ポインタの出どころ(由来、来歴)を重視したポインタモデルであり、ポインタのアドレスが単なる整数値であるという既存のセマンティクスから脱却しようとするものです。あるポインタの型とアドレスが同一だったとしても、provenanceの異なるポインタは別のポインタとして扱われます(詳しくは「ポインタ provenance」などで検索していただくと色々見つかります)。
この提案で確立した直感的な動作は「あるスレッドがあるポインタのデリファレンス権を持っていない場合、そのポインタを別のスレッドに渡すと渡されたスレッドもそのポインタのデリファレンス権を持たない」というものです。これを実現するために、Cのprovenanceモデルをベースとして、そこに新しい種類のprovenanceモデルを追加することを提案しています。
- full provenance: Cで提案中のprovenanceモデル
- empty provenance: 無効なポインタのprovenance
- specific provenance: その他のポインタのprovenance
- provisional provenance: この提案
この提案の下では、ポインタを自由に使用するためにはfull provenanceが必要となり、provisional provenanceしかないポインタでデリファレンスを行うとUBになります。
このprovenanceモデルの下では、まず1つ目の問題は次のように解決されます。
int* f(int* q) { int* p = new int; *p = 123; *q = 456; // E some_extern_func(*p); return p; } int dummy; std::atomic<int *> ptr_to_int_1{&dummy}; std::atomic<int *> ptr_to_int_2{&dummy}; void t1() { int* q = ptr_to_int_1.load(relaxed); // D int* p = f(q); ptr_to_int_2.store(p, relaxed); // A } void t2() { int* p = ptr_to_int_2.load(relaxed); // B ptr_to_int_1.store(p, relaxed); // C }
t2()
がポインタをロード(B)すると、provisional provenanceを持つポインタが得られます。そのポインタはCでストアされ、Dでロードされるポインタq
もprovisional provenanceを持ちます。E
はprovisional provenanceでデリファレンスされるためUBになります。したがって、q
とp
が同一である可能性を考慮する必要は無くなります(UBは起こらないので、q
は常にfull provenanceを持つはずであり、このローカルでfull provenanceが発生するp
とはprovenanceが明らかに異なる)。
2つ目の問題も解決されます
atomic<int*> atomic_int_ptr; int* f() { int *p = new int; *p = 123; // A some_extern_function(); // Really, a release fence return p; } int* f() { int *p = new int; some_extern_function(); // just a release fence *p = 123; return p; } void message_pass_f() { int* p = f(); atomic_int_ptr.store(p, relaxed); } void receive_message() { int* p = atomic_int_ptr.load(relaxed); atomic_thread_fence(acquire); // P if (p != nullptr) { assert(*p == 123); } }
receive_message()
で取得されるポインタp
はprovisional provenanceを持ちます。acquire
フェンス直後の点Pについて考えると、p
がnullptr
ではない場合、int
のライフタイムの開始はPよりも前に発生しており、Pはデリファレンスよりも前に発生します。これによって、p
はfull provenanceを獲得し、この例にはUBはありません(f()
の定義に関わらず)。その後のアサートがどう発動するかはf()
がどう最適化されているかによります。
提案には追加の例があり、代替の解決手段の検討などもされています。
P3293R0 Splicing a base class subobject
リフレクション機能において、基底クラスのサブオブジェクトへアクセスする簡単な方法を提供する提案。
P2996の静的リフレクション機能においては、ある型(もしくはオブジェクト)のサブオブジェクト(非静的メンバ or 基底クラス)に対して同じ操作を順番に適用していくことが有用な状況がいくつもあります。
P2996ではT
のオブジェクトobj
とT
のサブオブジェクトを表す鏡像(meta::info
)sub
を用いて、obj.[:sub:]
という構文でそれを実現しつつスプライシング(値の展開)まで行うことができるのですが、これは基底クラスのサブオブジェクトをサポートしていないという問題があります
template <class T, class F> void for_each_subobject(T const& obj, F f) { // Tのサブオブジェクトについてループする template for (constexpr auto sub : subobjects_of(^T)) { f(obj.[:sub:]); // 非静的メンバ変数に対しては有効だが、基底クラスのサブオブジェクトに対しては使用できない } }
したがって、このようなコードは次のように場合分けして書き直す必要があります
template <class T, class F> void for_each_subobject(T const& obj, F f) { // 基底クラスのサブオブジェクトのイテレーション template for (constexpr auto base : bases_of(^T)) { f(static_cast<type_of(base) const&>(obj)); } // 非静的メンバ変数のイテレーション template for (constexpr auto sub : nonstatic_data_members_of(^T)) { f(obj.[:sub:]); } }
ただしこのコードにもまだ問題があり、static_cast
を使用しているためにアクセスチェックが入り、private
な基底クラスにアクセスできません。これを回避するにはさらにCキャストを使用する必要があります。
template <class T, class F> void for_each_subobject(T const& obj, F f) { // 基底クラスのサブオブジェクトのイテレーション template for (constexpr auto base : bases_of(^T)) { f((typename [: type_of(base) :]&)obj); } // 非静的メンバ変数のイテレーション template for (constexpr auto sub : nonstatic_data_members_of(^T)) { f(obj.[:sub:]); } }
このコードにもさらに問題があり、const
を書くのを忘れているため意図せずconst
性のキャストも行ってしまっています。そして、これはCキャストなので、実際の継承関係に関係なくキャストが成功します。
また、キャストを使用する場合宛先の型を書かなければならず、さらにCV修飾と値カテゴリを正しく指定もする必要があります。完全なソリューションは関数テンプレートを使用することです
template <std::meta::info M, class T> constexpr auto subobject_cast(T&& arg) -> auto&& { constexpr auto stripped = remove_cvref(^T); if constexpr (is_base(M)) { // 基底クラスのサブオブジェクトのイテレーション static_assert(is_base_of(type_of(M), stripped)); return (typename [: copy_cvref(^T, type_of(M)) :])arg; } else { // 非静的メンバ変数の場合 static_assert(parent_of(M) == stripped); return ((T&&)arg).[:M:]; } } template <class T, class F> void for_each_subobject(T const& obj, F f) { template for (constexpr auto sub : subobjects_of(^T)) { f(subobject_cast<sub>(obj)); } }
これはあまりに冗長で、これを書く必要性を要求するのは間違っています。
この提案はこの解決のために、現在のobj.[:nsdm:]
(nsdm
はT
型の非静的メンバ変数の鏡像)という記法が非静的メンバ変数にしかアクセスできないのを修正して、同じ記法をクラスの任意のサブオブジェクトにアクセスするためのものとして再定義することを提案しています。
さらに追加で、&[:mem:]
(mam
はT
の基底クラスB
の鏡像)という構文は、適切なオフセットを持つB T::*
を生成するようにすることも提案しています。
P2996がこうなっていない理由は、非リフレクションの場合にobj.member_name
の記法によってアクセスできるのはメンバ変数に限定されるためで、基底クラスがこのようにアクセスできないのは基底クラスには通常メンバ変数のような名前が無いためです。
この提案の変更ではリフレクションの文脈(スプライシング)において対応する構文で言語(非リフレクション部分のC++)が実行できないことを実行できるようにしてしまいます。しかし、リフレクションは現在のコア言語がネイティブに実行できないことを可能にするものであるため、これは問題にならないと主張しています。
この提案によって、最初のコードがそのまま意図通りに動作するようになります
template <class T, class F> void for_each_subobject(T const& obj, F f) { // Tのサブオブジェクトについてループする template for (constexpr auto sub : subobjects_of(^T)) { f(obj.[:sub:]); // ok、この提案後 } }
P3294R0 Code Injection with Token Sequences
トークンシーケンスを用いてコード注入によるコンパイル時コード生成機能の提案。
C++26に向けて静的リフレクション機能(P2996)の議論が進行しています。P2996は最初のリフレクション提案として主にコンパイル時にC++エンティティの特性を問い合わせて取得し、それを利活用するところの基盤機能を提案することに主眼を置いています。もちろん、そこにはリフレクション結果を利用したコード生成機能も含まれているのですが、問い合わせ部分と比較すると相対的に小さく、まだまだ力不足です。
この提案は、そのP2996の機能をベースとしたコード生成機能を提案するものです。特に、C++のコード片をリフレクションによって取り扱い、任意の場所に注入することでコードを生成させる機能を提案しています。
P2996の部分については以前の記事を参照
そのようなコード片は、この提案ではトークンシーケンス(Token Sequences
)と呼ばれており、@tokens{ balanced-brace-tokens }
の形で導入します。balanced-brace-tokens
は{}
のペアが対応していることのみを要求するC++の構文要素(C++トークンのシーケンス)を指しており、@tokens
リテラルは指定されたトークンシーケンスを保持したstd::meta::info
型の値を返します。
constexpr auto t1 = @tokens { a + b }; // 3トークンのトークンシーケンス static_assert(std::is_same_v<decltype(t1), const std::meta::info>); constexpr auto t2 = @tokens { a += ( }; // トークン列は意味を持つ必要が無い constexpr auto t3 = @tokens { abc { def }; // ng、{}が対応づいていない
この例からも分からるように、トークンシーケンスはあくまでC++のトークン片であり、指定されたトークン列はC++コードとして構文的・意味的に有効であることは要求されず、また検査されません。それは、トークンシーケンスを実際のコードに注入した後に改めて行われます。
トークンシーケンスは+. +=
によって連結することができます。
constexpr auto t1 = @tokens { c = }; constexpr auto t2 = @tokens { a + b; }; constexpr auto t3 = t1 + t2; static_assert(t3 == @tokens { c = a + b; });
この連結は字句的に行われるのではなく、トークンとして連結されます
constexpr auto t1 = @tokens { abc }; constexpr auto t2 = @tokens { def }; constexpr auto t3 = t1 + t2; // 1トークン + 1トークン = 2トークン static_assert(t3 != @tokens { abcdef }); static_assert(t3 == @tokens { abc def });
トークンシーケンスはプリプロセッシングディレクティブではなくC++コードの機能であるため、マクロはトークンシーケンス上でもこれまで通りに動作しますが、トークンシーケンスの境界を越えて関数マクロ呼び出しが作用しないため少し注意が必要です。
// 文字列リテラルの連結は動作する static_assert(@tokens { "abc" "def" } == @tokens { "abcdef" }); // トークンシーケンスを結合した後では文字列リテラルは連結しない static_assert(@tokens { "abc" } + @tokens { "def" } != @tokens { "abcdef" }); #define PLUS_ONE(x) ((x) + 1) // トークンシーケンス内でもマクロは動作する static_assert(@tokens { PLUS_ONE(x) } == @tokens { ((x) + 1) }); // トークンシーケンスの境界を越えて関数マクロが動作しようとしない static_assert(@tokens { PLUS_ONE(x } + @tokens{ ) } != @tokens { ((x } +tokens{) + 1) }); static_assert(@tokens { PLUS_ONE(x } + @tokens{ ) } == @tokens { PLUS_ONE(x) }); // トークンシーケンスの連結によってマクロ呼び出しが現れても、そこでは呼び出されない constexpr auto tok2 = []{ auto t = @tokens { PLUS_ONE(x }; constexpr_print_str("Logging...\n"); t += @tokens{ ) } return t; }(); static_assert(tok2 != @tokens { PLUS_ONE(x) });
トークンシーケンスは通常、後から別の場所で注入して使用します。しかし、含まれるトークンは単なるトークンでしかなく、注入先のコンテキストで意味を持つことはあっても、トークンシーケンスを作成した場所での意味をキャプチャすることができません。そのため、トークンシーケンスの作成時にその外部のコンテキストにおけるトークンを意味と共にキャプチャする構文が必要になります。そのために、ここでは次の2つの特別なトークンシーケンス専用構文を提案しています
$eval(e)
:std::meta::info
型のe
を受け取り、e
の値を保持した疑似リテラルトークンに置き換える$id(e)
: 文字列または整数型のe
を受け取り、その値に置き換える$id(e, ...)
: 複数の文字列または整数の値を連結して1つの識別子に置き換える
これらのキャプチャ構文がトークンシーケンス中に現れると、その中の式を解析して評価し、トークンシーケンス中でこの説明のように対応するものに置き換えられます。
これらの基本機能によって、ボイラープレートを伴うようなコードを簡潔に生成することができるようになります。
std::tuple
のストレージを定義する例
template<class... Ts> struct Tuple { consteval { // パラメータパックTsから個別のmeta::info型を取得し保存 std::array types{^Ts...}; // Tsの型ごとにメンバ変数を宣言する for (size_t i = 0; i != types.size(); ++i) { inject(@tokens { [[no_unique_address]] [: $eval(types[i]) :] $id("_", i); }); /* Tsの型名をTi(iは数値)とすると [[no_unique_address]] Ti _i; のようなメンバ宣言がTupleクラスの定義に注入される */ } } };
consteval
ブロックは宣言コンテキストで任意の定数式を実行するための構文です。上の方のP3289R0で提案されているものです。std::meta::inject()
はここで提案されている新しいリフレクションメタ関数で、受け取ったstd::meta::info
が保持しているコード片をC++コードとしてそのコンテキストに(副作用として)注入するものです。
上記例では、Tuple<Ts...>
型の直接のメンバ変数として、Ts
の各型に対応するメンバを宣言しています。現在のstd::tuple
の場合、継承を用いた非常に複雑な実装によってこれを実現しているため、かなり簡潔になっていることがわかると思います。
std::enable_if
を定義する例
template <bool B, class T=void> struct enable_if { consteval { // Bがtrueの場合にのみ、`using type = T;`を注入する if (B) { inject(@tokens { using type = T; }); } }; };
std::enable_if<expr, T>
はexpr
がtrue
に評価される場合にのみメンバ型type
としてT
を宣言するものです。この実装は通常テンプレートの部分的特殊化を用いて行われますが、ここでは1つのクラス定義のみで簡潔に定義出来ています。
簡単なプロパティ機能を注入する関数を定義する例
consteval auto property(meta::info type, std::string name) -> void { // メンバ変数名を作成 std::string member_name = "m_" + name; // メンバ変数宣言を注入 // 型名をTとすると // T m_name; // のような宣言が注入される inject(@tokens { [:$eval(type):] $id(member_name); }); // ゲッター関数宣言を注入 // 型名をTとすると // auto get_name() -> T const& { // return m_name; // } // のような宣言が注入される inject(@tokens { auto $id("get_", name)() -> [:$eval(type):] const& { return $id(member_name); } }); // ゲッター関数宣言を注入 // 型名をTとすると // auto set_name(T const& x) -> void { // m_name = x; // } // のような宣言が注入される inject(@tokens { auto $id("set_", name)(typename [:$eval(type):] const& x) -> void { $id(member_name) = x; } }); } // 使用例 struct Book { consteval { property(^std::string, "author"); property(^std::string, "title"); } }; int main() { Book b: b.set_author("太宰治"); b.set_title("人間失格"); std::string author = b.get_author(); std::string title = b.get_title(); }
ここではメンバ変数・ゲッター・セッターを3つのinject()
によって生成していますが、1つにまとめることもできます。
後置インクリメント演算子を生成する関数の例
consteval auto postfix_increment() -> void { // 呼び出されたコンテキストのクラス型を取得 auto T = type_of(std::meta::current()); inject(@tokens { auto operator++(int) -> [:$eval(T):] { auto tmp = *this; ++*this; // 定義済みの前置++を使用して実装 return tmp; } }); } // 使用例 struct C { int i; auto operator++() -> C& { ++i; return *this; } consteval { postfix_increment(); } };
既存のクラス(std::vector
)をラップして、関数呼び出しにログ出力を追加する例
template <typename T> class LoggingVector { std::vector<T> impl; public: LoggingVector(std::vector<T> v) : impl(std::move(v)) { } consteval { for (std::meta::info fun : /* vectorのpublicな非特殊メンバ関数全てのリフレクションを取得 */) { // ラッパ関数からvectorの関数への引数転送トークンを構築する auto argument_list = @tokens { }; // 元関数funの引数毎にループ for (size_t i = 0; i != parameters_of(fun).size(); ++i) { // 2つ目以降の引数の時はまず`,`トークンを入れる if (i > 0) { argument_list += @tokens { , }; } // 引数転送(std::forward相当の処理)のトークンを構築 argument_list += @tokens { static_cast<decltype($id("p", i))&&>($id("p", i)) // static_cast<decltype(p0)&&>(p0) の様なトークンが構築される }; } // ラップした同名同引数メンバ関数の定義を注入する inject(@tokens { declare [: $eval(decl_of(fun, "p")) :] { // 関数名を出力するコード std::println("Calling {}", $eval(name_of(fun))); // vectorの対応する関数を呼び出すコード return impl.[: $eval(fun) :]( $eval(argument_list) ); } }); } } };
ここでは新しい構文が2つ使われています。decl_of(fun, "p")
は関数のリフレクションfun
に対して、その仮引数名をプリフィックスp
から始まる連番名とした関数宣言のリフレクションを返し、declare[:e:]
は宣言スプライスと呼ばれるスプラシング構文のファミリで、関数リフレクションe
を受けてその宣言トークンを展開するものです。
これによって、LoggingVector
は自動的にstd::vector
の全メンバ関数(特殊メンバ関数を除く)と同名動引数の関数を備え、なおかつそれらの呼び出しは関数名をログ出力したうえで内部std::vector
の対応するメンバ関数を呼び出す(引数は適切にforward
される)ようになります。
例えば、次のようなコードが生成されます
template <typename T> class LoggingVector { std::vector<T> impl; public: LoggingVector(std::vector<T> v) : impl(std::move(v)) { } auto clear() -> void { std::println("Calling {}", "clear"); return impl.clear(); } auto push_back(T const& value) -> void { std::println("Calling {}", "push_back"); return impl.push_back(static_cast<T const&>(value)); } auto push_back(T&& value) -> void { std::println("Calling {}", "push_back"); return impl.push_back(static_cast<T&&>(value)); } ... };
一通り強力な(現代の言語では一般な?)コード生成機能が備わっていることが分かりましたが、この提案はここで終わりではなく、さらにこれらのトークンシーケンスとstd::meta::inject
を利用した衛生的マクロ(Hygienic Macros)機能も提案しています。
例えば、std::forward()
の正しい使用方法はフォワーディングリファレンスT&& t
に対して、std::forward<T>(t)
と記述しますが、このコンテキストではT = decltype(t)
が成立しますが、std::forward()
を使う場合はとにかく2つの名前を記述しなければなりません。これは簡単ではあるものの記述が少し面倒であり、以前にはこれを簡略化する言語機能が提案されていました(P0644R1, P1221R1)。
// P1221R1の衛生的マクロによるstd::forwardのラッパマクロ using fwd(using auto x) { return static_cast<decltype(x)&&>(x); } // 新旧転送構文の比較 auto old_f = [](auto&& x) { return std::forward<decltype(x)>(x); }; auto new_f = [](auto&& x) { return fwd(x); };
この提案のトークンシーケンスを用いても、同じような衛生的マクロシステムを導入することができます。
// トークンシーケンスを用いたマクロ定義 consteval auto fwd2(@tokens x) -> info { return @tokens { static_cast<decltype($eval(x))&&>($eval(x)); }; } // 転送構文 auto new_f2 = [](auto&& x) { return fwd2!(x); };
マクロ定義は、トークンシーケンスリテラルを受け取りstd::meta::info
を返すconsteval
関数として定義することができます。呼び出しはRustのマクロ呼び出しを参考にしてマクロ名の後に!
を付けた形の関数呼び出しとしてfwd2!(x)
のように行い、これはここまでで何度も使用しているinject(fwd2(@tokens { x }))
の構文糖衣です。
この新しいマクロはC++のコードとして記述され、トークンシーケンス周りを覗いてC++の関数呼び出しと同じような振る舞いをします。これによってC由来のマクロにあるような問題がいくつか(呼び出し引数内の,
に対する鋭敏さやスコープの問題など)解消されます。例えば、マクロを名前空間に配置することができ、モジュールからエクスポートすることができ、複雑なマクロもCプリプロセッサではなくC++コードで記述することができます。
アサーションマクロの例
consteval auto assert_eq(@tokens a, @tokens b) -> info { return @tokens { do { // aの式文字列と式の評価結果を取得 auto sa = $eval(stringify(a)); auto va = $eval(a); // bの式文字列と式の評価結果を取得 auto sb = $eval(stringify(b)); auto vb = $eval(b); if (not (va == vb)) { // アサートが失敗した場合、それぞれの式の評価結果と場所を出力して終了させる std::println( stderr, "{} ({}) == {} ({}) failed at {}", sa, va, sb, vb, $eval(source_location_of(a))); std::abort(); } } while (false); }; }
// こう書くと assert_eq!(42, factorial(3)); // こう展開される do { auto sa = "42"; auto va = 42; auto sb = "factorial(3)"; auto vb = factorial(3); if (not (va == vb)) { std::println( stderr, "{} ({}) == {} ({}) failed at {}", sa, va, sb, vb, /* some source location */); std::abort(); } } while(false);
このようなコードはCマクロでも記述することはできるのですが、こうして定義した方が圧倒的に読みやすく、書きやすいことが分かると思います。ただし、これにはCマクロにある命名の問題が依然としてあります。例えばassert_eq!(42, sa * 2)
と書くと、マクロ定義内のローカル変数sa
と引数にあるsa
が衝突します。
std::format()
のフォーマット文字列に変数名を指定して、呼び出しコンテキストからそれを持ってくるラッパマクロの例。
// パーサの実装はここでは主題でないため省略するものの // "x={this->x:02} y={this->y:02}" のようなフォーマット文字列に対して // {.format_str="x={:02} y={:02}", .args={"this->x", "this->y"}} のようなものを返すことを想定 struct FormatParts { string_view format_str; vector<string_view> args; }; consteval auto parse_format_string(string_view) -> FormatParts; consteval auto format(string_view str) -> meta::info { auto parts = parse_format_string(str); auto tok = @tokens { // まずはフォーマット文字列を渡すところまで、閉じかっこは後で付加 ::std::format($eval(parts.format_str) }; // 元のフォーマット文字列に指定された変数名のカンマ区切り列を構築する for (string_view arg : parts.args) { // tokenize()は文字列をトークン列に変換するもの tok += @tokens { , $eval(tokenize(arg)) }; } // 閉じかっこを追加 tok += @tokens { ) }; return tok; }
例えばformat!("x={x}")
と呼び出すと、::std::format("x={}", x)
に書き換えて呼び出しを行います。このようなことを、専用の言語機能無しで行うことができます。
ただし制限はあり、このマクロ呼び出しレイヤでは型情報が無いためフォーマット文字列のチェックを完全に行うことはできません。ここでのチェックはせいぜい{}
の対応がきちんととれているかをチェックするまでです。しかしこれは現実のユースケースの9割をカバーでき、std::format
専用の言語機能ではなくユーザーサイドで自由に記述することができます。
最後にまとめると、この提案は次の事を提案しています
- トークンシーケンスを導入する機能:
@token {...}
- トークンシーケンス内に外部コンテキストを導入するインタポーザー:
$id(), $eval()
- 宣言スプライシング:
declare [: fun :]
- トークンシーケンスを注入するメタ関数:
std::meta::inject()
- トークンシーケンスを処理するための新しいメタプログラミング機能
- 衛生的マクロ
なお、それぞれの命名や構文については仮のもので後程変更されるかもしれません。
P3295R0 Freestanding constexpr containers and constexpr exception types
フリースタンディング環境の定数式において、std::vector
等を使用可能にする提案。
P2996の静的リフレクション機能では、members_of()
などvector<meta::info>
を返す関数がいくつかあります。静的リフレクションはフリースタンディングかどうかに関わらず価値があり利用できるべきですが、std::vector
はフリースタンディング環境では必須ではなく利用できない場合があるため、そのせいでフリースタンディング環境で静的リフレクションが利用できなくなる可能性があります。
この提案は、その解消のために、P2996で利用されているフリースタンディング環境で必須ではない機能の最小セットについて、定数式でのみ使用可能となるようにしようとするものです。
フリースタンディング環境では多くの場合、実行時にメモリを確保したり例外を投げたりすることができませんが、コンパイル時にはそのような制限はありません。そのため、フリースタンディング環境ではstd::vector
とstd::allocator
をconsteval
で有効化します。
std::vector
は例外を投げるAPIがあり、そこではout_of_range
とlength_error
が使用されていますがこれもフリースタンディングではないためこれらがコードに出現してしまうと、定数式がそこを通らなくてもコンパイルエラーを起こしてしまいます。そのため、これらの例外型をホスト環境ではconstexpr
、フリースタンディング環境ではconsteval
で有効化します。
さらに、out_of_range
とlength_error
には文字列を受け取るAPIがあり、これもまたフリースタンディング指定されていないstd::string
に依存しています。そのため、std::string
も同様にフリースタンディング環境ではconsteval
で有効化します。最後に、現在std::string_view
の一部の関数はfreestanding-deleted
指定されていますが、これをconsteval
で有効化します。
まとめると
- ホスト環境で
constexpr
を付加exception
logic_error
length_error
out_of_range
- フリースタンディング環境で
consteval
を付加logic_error
length_error
out_of_range
allocator
string_view
freestanding-deleted
メンバを変更
string
vector
となります。
例外関連型をconstexpr
指定しておくことは将来的な定数式における例外機構(リフレクションのエラー処理機構)にとってメリットになる可能性があります。
P3296R0 let_with_async_scope
提案中のcounting_scope
の問題を修正する提案。
counting_scope
はP3149で提案されているもので、並列数や処理の継続有無が実行時に決まる場合にコンパイル時の|
によるチェーンと同様に処理の依存関係にスコープを付けて管理することで、非同期処理が確実に完了するまで関連するリソースを保護しておこうとするものです。
counting_scope
については以前の記事を参照
counting_scope
は.nest()
によって処理のスコープを開き、その内部でさらに.nest()
を呼ぶことで非同期処理のネストを表現し、ネストせずに単に継続することで異なるスコープによる非同期処理の継続を表現します。シングルスレッドのプログラムで言うなら.nest()
は単にブロックスコープ{}
を導入しているのにあたります。
counting_scope
はその非同期処理を待機するために.join()
を明示的に呼び出さなければなりません。これはcounting_scope
オブジェクトが管理しているすべての非同期処理が完了するのを待機しなければ、関連するリソースを破棄するタイミングが分からないためです。
// 保護対象のリソース some_data_type scoped_data = make_scoped_data(); // counting_scopeオブジェクト初期化 ex::counting_scope scope; // 実行時にその構造が決定される非同期処理 ex::sender auto async_work = scope.nest(on(exec, [&] { scope.nest(on(exec, [&] { if (need_more_work(scoped_data)) { scope.nest(on(exec, [&] { do_more_work(scoped_data); })); scope.nest(on(exec, [&] { do_more_other_work(scoped_data); })); } })); scope.nest(on(exec, [&] { do_something_else_with(scoped_data); })); })); // 非同期処理を開始(別スレッドで実行されているとする ex::spawn(std::move(async_work), scope); // 例外を投げると・・・? maybe_throw(); // 非同期処理の完了待機 this_thread::sync_wait(scope.join());
maybe_throw()
関数の呼び出しが無い場合、このコードはscope
に登録された非同期処理が完了するのを最後の行のscope.join()
(が返すsender
を受けているsync_wait()
)で待機します。非同期処理が完了するとそのブロックが解除され、このスコープを抜ける時に、scope
-> scoped_data
の順で破棄されることで、非同期処理が完了したことを確認したうえでそこで使用されていたリソースの解放を安全に行うことができます。
しかし、maybe_throw()
関数が例外を投げている場合その保証は破壊されます。.join()
呼び出し前(すなわち、非同期処理が完了してるかどうかわからないタイミングで)に例外によってこのスコープの変数が破棄されることで、scoped_data
だけでなくscope
に対しても生存期間外にアクセスされるリスクが生じます。
この提案は、この問題の解決のためにcounting_scope
と非同期タスクをラップして管理するlet_async_scope
を提案しています。let_async_scope
の返すsender
は渡された非同期処理内で例外が送出されても、関連する(.nest()
された)非同期処理がすべて完了するまで完了状態にならず、これによって先程の問題は起きなくなります。なお、例外はsender
のエラーチャネルで伝播されます。
auto scope_sender = just(make_scoped_data()) // 非同期リソースをjust()で注入 | let_async_scope([](auto scope_token, // counting_scopeオブジェクト auto& scoped_data) // 非同期リソース { scope_token.nest(on(exec, [scope_token, &scoped_data] { scope_token.nest(on(exec, [scope_token, &scoped_data] { if (need_more_work(scoped_data)) { scope_token.nest(on(exec, [&scoped_data] { do_more_work(scoped_data); })); scope_token.nest(on(exec, [&scoped_data] { do_more_other_work(scoped_data); })); } })); scope_token.nest(on(exec, [&scoped_data] { do_something_else_with(scoped_data); })); })); maybe_throw(); }); // 非同期処理の実行待機 this_thread::sync_wait(scope_sender);
また、let_async_scope
の返すsender
に対する停止要求は、ネストされているすべてのsender
に伝達され、それらのsender
がそれに応答してさらに.nest()
によってクリーンアップ作業をスケジュールすることが許可されます。これによって、ネストしたsender
はlet_async_scope
のスコープ内でそのまま処理のキャンセル作業を行うことができ、その場合でも使用するリソースの破棄は安全になされます。
P3297R0 C++26 Needs Contract Checking
この提案の目的は
- C++で日常的に実用的で安全性が重視されるソフトウェアを書いているプログラマの声を届けること
- SG21とEWGが活動の質と専門家意識を向上させるための行動を呼びかけること
- SG21とEWGに対して、C++26のドラフトに基本的な要件を満たす最小限の実行可能な仕様を盛り込むように呼び掛ける
などにあります。この提案による最小限の実行可能な仕様とは
- 関数に付随する構文
- 事前条件を宣言する
- 静的解析ツールがパースできる
- 実行時チェックの挿入
- 通常のC++コードを実行する
- チェック内でタイムトラベル最適化を禁止する
このために、委員会に対して呼びかけを行っています。
- SG21
- 否定的な態度、あるいは言葉による闘争心は、議論の質を低下させ、貢献者を追い出す。これはもはや容認できない
- 新しい情報なしに同じ議論を繰り返すのは時間の無駄。誰の考えも変えられないのであれば繰り返す必要はない
- 優れた根拠を持つ提案は人々の心を変える。説得力のある提案を発表しないことは、自身の影響力を制限することになる
- SG22
- EWG
この提案の著者の方は、組み込み業界など安全性やセキュリティに関わる部分でC++コードベースを扱ってきた経験をもとに、現在の環境でコードベースへの安全性やセキュリティの要件が重要になっており、現在のC++はそのニーズをほとんど満たしていないことを説明しています。その上で、契約プログラミング機能がC++コードベースの安全性向上に貢献する重要なステップであり、これがC++29に3年間遅れることの機会費用が高すぎる(その間に、Rustをはじめとする他の言語基盤への移行を選択させてしまう)として、MVP(P2900)の全体でなくても最低限の部分をC++26に間に合わせるべきとしています。
P3298R0 Implicit user-defined conversion functions as operator.()
ユーザー定義の変換関数としてのoperator.()
の提案。
C++でスマート参照とはスマートポインタの参照版のようなもので、ポインタの動作をユーザーに公開しないようにした別の場所にあるオブジェクトのプロキシクラスです。このようなものを作成しようとする場合、operator.()
が必須になります。
template<class X> class Ref { public: explicit Ref(int a) :p{new X{a}} {} ~Ref() { delete p; } // operator.()はない X& operator.() { /* maybe some code here */ return *p; } void rebind(X* pp) { delete p; p = pp; } ... private: X* p; }; Ref<X> x{99}; x.f(); // => (x.operator.()).f() => (*x.p).f() x = X{9}; // => x.operator.() = X{9} => (*x.p )=X{9} x.rebind(p); // delete old x.p and make x.p=p(operator.()を使用しない
しかしoperator.()
の適用範囲には問題があります。1つは、代入操作(=
)は.
演算子を明示的に使用していませんがoperator.()
を呼び出すべきか?(同様のことが各演算子に言える)という問題であり、もう一つは、.
によるアクセスでそのオブジェクト自身のメンバ(Ref<T>
型のメンバ)にアクセスするにはどうすればいいか?という問題です。
operator.()
は以前から何度も提案されてきましたが、これらの問題を解決できなかったため、何度か提案されては否決されてきたようです。
この提案は、ユーザー定義.
演算子を暗黙変換の延長線上に実現しようとするものです。
あるオブジェクトobj
に対してobj.member()
のようにアクセスを行う時、現在でも基底クラスへの変換は考慮されています。この提案はこの変換過程にユーザー定義変換演算子を作用させることで.
アクセスをユーザーがオーバーライド可能にしようとしています。具体的には、implicit
指定子をユーザー定義変換演算子に指定することで、その変換演算子を.
による基底クラスへの変換時に候補に入れるようにします。この場合の名前探索のルールは継承と同様なので、予期しない変換が起こる可能性は低くなります。
// プロキシ参照型の例 template<typename T> class Proxy { Proxy(T& object) : m_ptr(&object) {} // この提案によるimplicit変換演算子 implicit operator T&() { return *m_ptr; } implicit operator const T&() const { return *m_ptr; } private: T* m_ptr; }; struct MyClass { using Type = int; int x; void f(); static void s(); }; void g(MyClass& o); void test() { MyClass obj; Proxy<MyClass> p(obj); Proxy<MyClass>* pp = &p; p.f(); // Proxy<T>には.f()が定義されていないので、その基底クラスとimplicit変換演算子をの戻り値型をチェックする p.x = 43; // Proxy<T>には.xが定義されていないので、その基底クラスとimplicit変換演算子をの戻り値型をチェックする g(p); // g()はProxy<T>を受け取らないため、その基底クラスとimplicit変換演算子をの戻り値型をチェックする // All name lookup considers names in bases and ICF return types // 全ての名前探索では、基底クラスとimplicit変換演算子の戻り値型における名前が考慮される Proxy<MyClass>::Type anInt; // operator-> もまた、基底クラスとimplicit変換演算子の戻り値型を考慮する pp->f(); pp->MyClass::f(); // 冗長だが許可される。Proxy型がf()を備えている場合に便利 Proxy<MyClass>::s(); // 名前探索によって、MyClassの静的メンバ関数s()を呼び出す }
この提案は.
だけではなく、メンバアクセスと同様に基底クラスへの変換が考慮される場所でimplicit
変換演算子の結果型を候補として加えます。そのため、関数へ渡す場合などの暗黙変換や、->
演算子でも同様にimplicit
変換演算子が作用します。
この提案の以前の提案からの改善点は
- 仕様の複雑化を回避する
operator.()
をはじめとする特別な構文を導入でず、名前探索は基底クラスへの変換と同じルールを使用する.
アクセスでそのクラス自身に自然にアクセスできる=
等演算子の呼び出しでも、関数呼び出し時の基底クラスへの変換ルールと同様に扱われる.
を使用していないことで、.
を使っていないのに考慮される、という状況にならない
implicit
指定子のみでシンプルに実現可能- 以前の提案にあったような冗長な記述を回避
提案より、汎用lazy
クラスの例
template<typename F> struct lazy { lazy(F f) : func(std::move(f)) {} implicit operator decltype(auto)() { return func(); } F func; };
提案にはこれらのほかにもいくつか利用できるシーンが挙げられています(サンプルコードはあまりありませんが)。
P3299R0 Range constructors for std::simd
std::simd
にrange
コンストラクタを追加する提案。
現在のstd::simd
のコンストラクタにはイテレータを1つだけ受け取って、std::simd
に指定されたサイズでそこからデータを読みだして構築するコンストラクタが主なものとなっています。これは、入力のデータ範囲の終端を知る方法が無く、安全ではありません。
int main() { float input[4] = {...}; // イテレータ1つだけを渡して初期化 std::simd<float, 4> simd1{input}; // メンバ関数による初期化も同様 std::simd<float, 4> simd2{}; simd2.copy_from(input); }
この理由は、std::simd
が主に計算の高速化のためのものであることから、初期化時(すなわち、SIMDレジスタへの値のロード時)の範囲チェックを行わないことをデフォルトとしているためです。しかし、実際の計算中はともかく初期化のタイミングは範囲チェックをきちんとしたい需要もあり、その動作(安全性か、パフォーマンスか)をカスタマイズすることができることを望む向きもありました。
この提案は、std::simd
にrange
コンストラクタを追加することで安全な初期化のための選択肢を用意し、なおかつパフォーマンスを重視する場合に動作をカスタマイズすることができるようにしようとするものです。
提案では、例えば次のような範囲コンストラクタを追加しようとしています
template<class T, class Abi> class basic_simd { ... // 現在あるイテレータ1つしか取らないコンストラクタ template<class It, class... Flags> constexpr basic_simd(It first, simd_flags<Flags...> = {}); // 提案 イテレータペアを取るコンストラクタ constexpr basic_simd(It begin, It end, simd_flags<Flags...> = {}); // 提案 イテレータとサイズを取るコンストラクタ constexpr basic_simd(It begin, size_type count, simd_flags<Flags...> = {}); // 提案 固定サイズspanをとるコンストラクタ constexpr basic_simd(span<T, N>, simd_flags<Flags...> = {}); // 提案 動的サイズの範囲を取るコンストラクタ constexpr basic_simd(Range r, simd_flags<Flags...> = {}); ... };
コンパイル時にそのサイズが既知の配列から値をロードする場合、そのサイズは常にぴったり合わなければならないようにすることを提案しています。すなわち、データの切り捨てや暗黙の値の追加は許可せず、プログラマが明示的にそれを指定する必要があります。
int main() { // 入力とサイズがぴったりの例 std::array<float, 6> ad; std::simd<float, 6> simdA(ad); std::span<int, 8> sd(d.begin(), 8); std::simd<int, 8> simdB(sd); // 入力とサイズが合わない例 std::array<float, 6> ad; std::simd<float 8> simdA(ad); // ng // 明示的にリサイズしてロードする std::simd<float, 6> simdA(ad); auto simdA8 = resize<8>(simdA); // ok }
また、コンパイル時にサイズが既知の場合のための推論補助も提供できます
template <contiguous-range-with-static-extent Rg> basic_simd(Rg) -> basic_simd<ranges::range_value_t<Rg>, deduce-t<ranges::range_value_t<Rg>, static-range-size<Rg>>>;
これを用いると、テンプレートパラメータの指定を省略できます。
std::array<float, 4> data; std::simd v = data; // simd<float>::size()が4の場合のみok std::basic_simd w = data; // ABIタグも推論する
入力範囲のサイズが実行時まで分からない場合、サイズのミスマッチは実行時にハンドルする必要があり、コストのかからないものから次の3つがあります
- 未定義動作にする
- 正常に処理される定義済み動作にする
- 多い場合は切り捨て、少ない場合はデフォルト値
- 例外を送出する
これらの3つに対応させたフラグ(simd_unchecked, simd_default_init, simd_exception
)を用意してユーザーが選択可能にすることを提案していますが、それでもなおデフォルトの動作を選択する必要があります。提案では、1か2のどちらかを選択することになるだろうと述べています。
この提案はまだ、提案というよりは方向性を探る段階です。
P3301R0 inplace_stoppable_base
P2300のinplace_stop_callback
に代わるinplace_stoppable_base
の提案。
キャンセルトークン、特にstd::inplace_stop_token
を使用してキャンセル操作をサポートしたいsender
では、そのoperation_state
(receiver
と接続された後のもの)への参照を含むstd::inplace_stop_callback
メンバをoperation_state
型へ追加する必要があります。
// 自作sender型、キャンセル操作をサポートしたい class my_sender { // my_senderのためのoperation_state型 template<typename R> class op_state { R receiver; // キャンセル受付コールバック型 class cb_t { op_state& op; public: cb_t(op_state& op) : op(op) {} void operator()() const { op.on_stop_requested(); } }; // inplace_stop_callbackオブジェクト std::inplace_stop_callback<cb_t> cb; // サイズは非ゼロ // キャンセル要求を受けた時の処理を実装する void on_stop_requested(); }; };
inplace_stop_callback
はキャンセル要求を受けた場合のコールバックを保持しておくためのものです。ただ、このような実装方法だとoperation_state
型に僅かではあるもののスペースのオーバーヘッドが生じます。
標準ライブラリの実装はこのコストを回避できる場合があるようですが、ユーザー定義のsender
実装ではそれはできません。この提案は、CRTPクラステンプレートinplace_stop_token
を追加してユーザーもこの問題を回避できるようにしようとするものです。
これを利用すると先程の例は大幅に簡略化され、さらにスペースオーバーヘッドを取り除くことができます。
class my_sender { // inplace_stop_tokenを使ったoperation_state実装 template<typename R> class op_state : std::inplace_stoppable_base<op_state<R>> { R receiver; // invoked upon request for cancellation. void on_stop_requested(); }; };
inplace_stoppable_base
の唯一のコンストラクタはprotected
で、inplace_stop_token
型の引数1つを受け取ります。
std::inplace_stoppable_base<D>
をCRTPで派生することでoperation_state
型を構成することができ、この場合にinplace_stop_token
経由でキャンセルが通知されると、on_stop_requested()
が派生クラスオブジェクトで呼び出されます。
P3302R0 SG16: Unicode meeting summaries 2024-03-13 through 2024-05-08
SG16のミーティングの議事録。
2024年3月から5月にかけてオンラインで行われたSG16のミーティングの議事録で、どんな内容の話をしたかや誰がどんな発言をしたかなどが詳細に記録されています。
P3303R0 Fixing Lazy Sender Algorithm Customization
P2999の提案の欠けていた部分を埋める提案。
P2999については以前の記事を参照
P2999では提案中で触れられてはいたものの、connect
およびget_completion_signatures
に対する変更について必要な文言が抜け落ちていたようです。この提案は、それを拾い上げて補完するものです。
この提案の変更は次の2点です
get_completion_signatures(sndr, env)
- まず、
transform_sender(get-domain-late(sndr, env), sndr, env)
を使用して入力のsender
(sndr
)を変換し、次のその結果をsndr
の代わりに使用するようにする
- まず、
connect(sndr, rcvr)
- まず、
transform_sender(get-domain-late(sndr, get_env(rcvr)), sndr, get_env(rcvr))
を使用して入力のsender
(sndr
)を変換し、次のその結果をsndr
の代わりに使用するようにする
- まず、
これはP2999R3ですでに議論され、決定され、LEWGで採択された設計に沿うものです。しかし、P2999R3ではそれを標準(正確にはP2300)に適用するための文言が抜け落ちていました(うっかりミスのようです)。この提案はそれを追加するためのもので、設計変更などは行われていません。
この提案は2024年7月に行われた全体会議で承認され、C++26WDに適用されています。
P3304R0 SG14: Low Latency/Games/Embedded/Financial Trading virtual Meeting Minutes 2024/04/10
2024年4月10日に行われた、SG14のオンラインミーティングの議事録。
どのようなことを議論したのかが簡単に記載されています。
P3305R0 SG19: Machine Learning virtual Meeting Minutes to 2024/04/11-2024/05/09
2024年4月11日と5/9日に行われた、SG19のオンラインミーティングの議事録。
どのようなことを議論したのかが簡単に記載されています。
P3306R0 Atomic Read-Modify-Write Improvements
std::atomic
のRead-Modify-Writeに、数値型の演算についてのAPIを追加する提案。
Read-Modify-Write(RMW)操作とは、1操作で値の読み出し・値の更新・更新値の書き込みを行う操作の事で、整数型・浮動小数点数型のstd::atomic
特殊化に存在するfetch_add
や+=
をはじめとする操作が該当します。
ただし、浮動小数点数型の場合は+= -=
に相当するものしか用意されておらず、整数型の場合でも加減算と最低限のビット演算くらいしか用意されていません。
この提案は、std::atomic
のRMW操作として通常の演算に対応するものを追加しようとするものです。
ここで提案されているのは次のものです
- 整数型
shl
: 左シフトshr
: 右シフトmod
: 剰余nand
: ビット毎のNANDdiv
: 割り算/=
mul
: 掛け算*=
- 浮動小数点数型
div
: 割り算/=
mul
: 掛け算*=
提案されているAPIは、std::atomic
のメンバ関数及びフリー関数の2種類です。
これらの操作は、Kokkosのatomic_fetch_<op>
やRustのアトミックAPIで利用可能であり、これらが無い場合ユーザーはstd::atomic
の上にCASループなどを利用して実装することになりますが、それは最適化や進行保証を妨げることになります。
P3307R0 Floating-Point Maximum/Minimum Function Objects
浮動小数点数型が最大値・最小値を求めるC23準拠の関数オブジェクトを追加する提案。
C++の浮動小数点数型のmax()/min()
に関する問題点については以前の記事を参照
浮動小数点数値の列に対して、最大値や最小値を求める操作は非常に一般的です。それは高性能科学計算でも同様であり、そこでは並列std::reduce
やstd::transform_reduce
を使用してそれが行われ、そのような特定の演算はハードウェアによる最適化が利用可能です。しかし、C++のstd::(ranges)::min/max
はIEE754標準に従っていないものしかなく、ハードウェア命令が従っている結果を返す場合にそのような最適化を利用できなくなります。
#include <cassert> #include <execution> #include <numeric> #include <ranges> int main() { double data[] = {2, 1, 4, 3}; double max = std::reduce( std::execution::par, data, data + 5, 0, // HWアクセラレーションを利用できない std::ranges::max ); assert(max == 4); return 0; }
この提案は、そのような事態を回避するために、IEE754に準拠したC23のAPIをベースとした浮動小数点数型の最大・最小値を得る関数オブジェクトを追加する提案です。
追加しようとしているのは、次の6つです
namespace std { struct fmin_t; struct fmax_t; struct fminimum_t; struct fmaximum_t; struct fminimum_num_t; struct fmaximum_num_t; }
これらの関数呼び出し演算子は対応するC23のAPIを呼び出すだけのものです。
#include <cassert> #include <execution> #include <functional> #include <numeric> int main() { double data[] = {2., 1, 4, 3}; double max = std::reduce( std::execution::par, data, data + 5, 0, // HWアクセラレーションが利用可能になる std::fmaximum_num_t{} ); assert(max == 4); return 0; }
P3308R0 mdarray
design questions and answers
mdarray
の設計への疑問点と回答をまとめた文書。
これは、LEWGにおけるP1684R5(mdarray
)のレビューにおいて指摘された疑問点などに応えるものです。
P1684については以前の記事を参照
ここで報告されている議論とその対応をざっとまとめると次のようになります
- LEWGのレビュー時の要求
- LEWGが要求した改訂
- 機能テストマクロ追加
- 対応予定
mdarray
のフォーマット方法の説明mdspan
のフォーマットに乗っかることで適切に対処可能
- 無効な状態のオブジェクトの使用を回避するための事前条件
- 対応予定
- 機能テストマクロ追加
- その他のトピック
文書では各項目についてより詳細に説明されています。
P3309R0 constexpr
atomic
and atomic_ref
std::atomic/stomic_ref
をconstexpr
化する提案。
この提案のモチベーションはコンパイル時と実行時でより多くのコードを共通化できるようにすることです。そのために、std::atomic/stomic_ref
のvolatile
オーバーロードおよび通知・待機関数を除くほとんどのメンバ関数をconstexpr
指定します。
とはいえ、今のところコンパイル時にはスレッドの概念が無く常にシングルスレッドであるため、コンパイル時にアトミックな動作が必要になるわけではありません。この提案では、if consteval
を用いてコンパイル時には通常の非アトミックな動作をするように分岐するようにしています。
atomic_fetch_add()
の実装例
template<class T> constexpr T atomic_fetch_add(atomic<T>* target, typename atomic<T>::difference_type diff) noexcept { if consteval { const auto previous = target->value; target->value += diff; return previous; } else { __atomic_fetch_add(target->value, diff); } }
提案文書より、サンプルコード
constexpr bool process_first_unprocessed(std::atomic<size_t> & counter, std::span<cell> subject) { // BEFORE: 定数式だとここでコンパイルエラー // AFTER: 定数式でも動作する const size_t current = counter.fetch_add(1); if (current >= subject.size()) { return false; } process(subject[current]); return true; } constexpr void process_all(std::span<cell> subject, unsigned thread_count = 1) { // BEFORE: 定数式でここに到達すると、thread_countに関わらずコンパイルエラー // AFTER: 定数評価コードでも、thread_count == 1の場合はエラーにならない std::atomic<size_t> counter{0}; auto threads = std::vector<std::jthread>{}; assert(thread_count >= 1); for (unsigned i = 1; i < thread_count; ++i) { threads.emplace_back([&]{ while (process_first_unprocessed(counter, subject)); }); } while (process_first_unprocessed(counter, subject)); }
P3310R0 Solving partial ordering issues introduced by P0522R0
P0552R0の影響を緩和するための提案。
P0552R0はC++17に採用されたもので、テンプレートテンプレートと通常のテンプレートをテンプレートの部分特殊化によってマッチングする際の一致のルールを変更するものです。ただしこの提案は副作用が大きく、以前に有効であったコードを無効化してしまっていました。
この提案は、それに対処するために、古いコードの動作を維持するようにそのルールを修正しようとするものです。
提案では2つの個別の問題の例と、それに対する解決策が提案されています。
1つはデフォルトテンプレート引数を持つテンプレートに対する部分特殊のマッチングに関してです。
template<class T1> struct A; template<template<class T2> class TT1, class T3> struct A<TT1<T3>>; // #1 template<template<class T4, class T5> class TT2, class T6, class T7> struct A<TT2<T6, T7>> {}; // #2 template<class T8, class T9 = float> struct B; template struct A<B<int>>;
P0552R0以前は、#2のみが候補として考慮されていました。B
には2つのテンプレートパラメータがあり、#1には1つのテンプレートパラメータでしか特殊化されていなかったためです。
しかし、P0552R0によって#1も考慮されるようになりました。これは、A<TT1<T3>>
でTT1
にB
を問題なく当てはめることができるためです。
ただし、部分特殊化における順序付けでは、B
にアクセスせずに候補自体のみを考慮します。そのため、この例でTT1
が2つのパラメータを持つテンプレートに置き換わったとしても、動作するかは明白ではありません
この提案では、#2を選定するという以前の動作を維持することを提案しています。
2つ目は、
template<template<class ...T1s> class TT1> struct A {}; template<class T2> struct B; template struct A<B>; // #1 template<template<class T3> class TT2> struct C {}; template<class ...T4s> struct D; template struct C<D>; // #2
P0552R0以前は#1は有効で#2は無効でしたが、P0552R0以後では#1は有効なまま#2も有効となりました。これだけだと問題が無いように見えますが、似たような場合の順序付けを見てると
template<class T1> struct A; template<template<class ...T2s> class TT1, class T3> struct A<TT1<T3>>; // #1 template<template<class T4> class TT2, class T5> struct A<TT2<T5>> {}; // #2 template<class T6> struct B; template struct A<B<int>>;
P0552R0以前は#2が選択されていましたが、P0552R0によってこれは曖昧になります。これは破壊的変更であり望ましくなたいめ、再び#2が選択されるように調整することを提案しています。
提案にはもう少し詳しい説明が載っています。
P3311R0 An opt-in approach for integration of traditional assert facilities in C++ contracts
Cassert
を契約プログラミング機能に統合する提案。
現在の契約プログラミング機能では、従来のassert
マクロの役割に相当するものとしてcontract_assert
が用意されています。assert
という名前を使用できなかったため名前が異なりますが、assert
マクロの問題点を修正しています。
contract_assert
に対するCassert
の問題点は次のようなものです
NDEBUG
の定義の有無でODRの結果が変わるcontract_assert
はignoreセマンティクスの場合でもODRの結果が変わらない
- 単なるマクロであるため、任意の場所で再定義されうる
contract_assert
はマクロではなく文でありキーワード
- カンマがあるとバグる場合があった
- これは、C/C++双方でP2264R7によって解消済
contract_assert
はマクロではなく、構文的にも式を受け取る
- 任意の副作用をトリガーできる
contract_assert
の場合、参照するものの暗黙const
化や評価回数の不定性などで対処している
ただ、これらのことによってcontract_assert
とassert
の間には互換性がなく、単純に移行ができない部分があります。特に、最後の副作用の実質禁止のための仕組みに関しては、contract_assert
がCassert
の完全な代替となり得ないと考えられてしまう部分になっています。
従来のアサーションはassert
もしくは似たような仕組みによるアサーション機構によって行われており、これらの既存コードをC++26移行に伴って書き直すことを期待したり要求したりするのは現実的ではありません。機械的に(単純なテキスト置換などによって)移行ができない場合、C++26移行の世界ではアサーション機構としてこの2つが並立することになり、その教育や学習のコスト上昇にもつながります。
この提案では、オプトイン方式によってCassert
を契約プログラミング機能に組み込むことができるようにすることで、contract_assert
の変更を必要とすることなく従来のassert
をほぼそのまま契約プログラミング機能に移行できるようにしようとしています。
提案ではまず、contract_assert
の構文を拡張して、文脈依存キーワードtraditional
を指定できるようにします。
contract_assert traditional (...);
この場合のcontract_assert
は動作が次のように変更されます
- 暗黙の
const
化の無効化 - 評価回数は正確に一回
- 条件式からの例外は伝播される(違反ハンドラは呼び出されない)
- 条件式の結果が
false
となった場合、detection_mode()
がpredicate_false
を返し、kind()
がtraditional_assert
(新規追加)を返し、semantic()
がenforce
を返すstd::contracts::contract_violation
オブジェクトへの参照を使用して違反ハンドラを呼び出す - デフォルトの違反ハンドラ推奨事項を変更し、
kind()
がtraditional_assert
を返す引数で呼ばれた場合に、失敗したアサーションに対してC標準で要求される動作を実装する(ことを推奨する)- エラー情報をエラーストリームに書き込んでから
abort()
を呼び出す、など
- エラー情報をエラーストリームに書き込んでから
すなわち、assert
マクロとほぼ同じ動作をします。
そして、assert.h/cassert
ヘッダをインクルードする時点でNDEBUG
マクロが定義されておらず、 ASSERT_USES_CONTRACTS
マクロが定義されている場合に、assert
マクロの定義を次のように変更します
#define assert(...) \ [&] { contract_assert traditional (__VA_ARGS__); }()
ただし、下位互換性の維持のためにNDEBUG
とASSERT_USES_CONTRACTS
の両方が定義されている場合はNDEBUG
が優先されます(assert
マクロは従来通り引数を握りつぶす形で定義される)。
このような方法によって従来のアサーション機構が新しい契約プログラミング機能のフレームワークに統合されていれば、それを利用した移行を起点として契約プログラミング機能の利用を推進する材料になります。これにより、2つの異なるアサーション機能が並立しているという状況も解消されます。
この提案の方向性は概ねSG21に支持され、類似の提案であるP3290R1に統合されることになりました。
P3312R0 Overload Set Types
関数のオーバーロード集合型を導入する提案。
プレースホルダ型(auto
)で宣言されたものに関数名を渡すと関数ポインタが推論されます。しかし、その関数がオーバーロードされている場合どの関数ポインタを取得するべきか分からないためコンパイルエラーになります。
auto call(auto sin, auto v) { return sin(v); } int main() { call(std::sin, 3.14f); // ng、オーバーロードがあるため曖昧 }
この提案では、このような場合に関数ポインタではなくオーバーロード集合型(overload-set-type)を推論し、その関数名についてその時点で利用可能なすべてのオーバーロードに関する情報をラップして渡します。オーバーロード集合型のオブジェクトに対して関数呼び出しを行うと、その場所でオーバーロード解決が行われ、引数に応じた最適なオーバーロードが選択されます。
// この提案では、第一引数はオーバーロード集合型が推論される auto call(auto sin, auto v) { return sin(v); // vによってオーバーロード解決が行われる } int main() { call(std::sin, 3.14f); // ok、std::sin(float)が呼ばれる call(std::sin, 3.14); // ok、std::sin(double)が呼ばれる }
このようなオーバーロード集合型は実行時状態を持たず、コンパイル時の型生成とオーバーロード解決にのみ依存しています。特に、関数ポインタのように実行時まで呼ばれる関数が分からないということはなく、呼び出される関数はコンパイル時に確定されています。
オーバーロード集合型が推論されるのは、プレースホルダ型で宣言された変数をオーバーロードされた関数名で初期化しようとした場合のみです。指定した関数がオーバーロードされていない場合は現在と同じく関数ポインタ型を推論します。
この型の基本的な考え方は、関数オーバーロード集合型のオブジェクトに対して関数呼び出し演算子を使用する場合に、元の関数オーバーロード集合(関数オーバーロード集合型が作成された地点)のすべての側面が維持されるというものです。すなわち、オーバーロード候補は関数呼び出しが実際に行われる場所ではなく、オーバーロード集合型が推論された場所で有効なものが候補となります。
std::sin
がfloat/double
の2種類のみのオーバーロードを持つ場合、オーバーロード集合型をあえて明示すると次のような型とほぼ同じ振る舞いをします。
struct __std_sin_overload_set_type_1 { decltype(auto) operator()(auto&&... as) { return std::sin(std::forward<decltype(as)>(as)...); } // 関数ポインタ(関数参照)型に変換可能 operator float (&)(float) const { return std::sin; } operator double (&)(double) const { return std::sin; } };
これは例示のためのものであって、実際にはこのような型が生成されるとは限りません。特に、関数呼び出し演算子などは明示的に実装されるのではなく省略し、元の関数で正しいオーバーロードが直接的にわたるような形になる必要があります。また、オーバーロード解決に必要な情報はオーバーロード集合型が推論された地点のものが使用されます。
オーバーロード集合型の扱いはラムダ式のクロージャ型に近いもので、名前はないもののdecltype()
を適用でき、各種コンストラクタ及び代入演算子も定義されます。ただし、異なるオーバーロード集合型の間での代入はできません。
提案するオーバーロード集合型について可能な操作の例
void compose(auto F, auto G, auto value) { return F(G(value)); } double one = compose(std::tan, std::atan, 1); auto s = std::sin; // 変数として取得 using SinOverloads = decltype(s); // 型名を取得 callWihtFloat(SinOverloads()); // デフォルト構築 void cc(float (*f)(float)); cc(s); // 関数ポインタへの変換 auto sptr = declcall(s(2.0f)); // P2825の機能 cc(sptr); auto sptr2 = static_cast<float(*)(float)>(s); cc(sptr2); double x = s(3.14); // decay-copy(オーバーロード集合型に対して) auto(std::sin); auto{std::sin}; decltype(std::sin); // オーバーロード集合型が推論される template<auto F> void myFun() requires requires { F(1.2); } { F(2.3); } myFun<std::sin>(); // NTTPに渡す
関数オーバーロード集合型のオブジェクトに対して関数呼び出し演算子を使用する場合に、元の関数オーバーロード集合(関数オーバーロード集合型が作成された地点)のすべての側面が維持される、という基本的な考え方に基づいてその他の動作も設計されています。例えば、デフォルト引数はオーバーロード解決時に考慮されるものの関数ポインタへの変換時は考慮されず、関数テンプレートがオーバーロード候補に含まれている場合でも同様に動作し(インスタンス化地点はオーバーロード解決が行われた場所)、noexcept/constexpr
なども選択された関数のものが使用されます。
ADLに関してもこの動作を踏襲し、ADLの候補集合もオーバーロード解決が行われる場所ではなくオーバーロード集合型が推論された場所のものになります。すなわち、コンパイラはオーバーロード集合型が推論された地点のシンボルテーブルを保存する必要がありますが、これはジェネリックラムダの場合と同じルールです。
ここまではフリー関数のみを対象として説明してきましたが、この提案のオーバーロード集合型はメンバ関数や関数呼び出し演算子などに対しても動作します。ただし、メンバ関数の場合は&
を付けて渡す必要があります(クラス名C
とメンバ関数memf
に対して&C::memf
)。また、このメンバ関数にはコンストラクタやデストラクタも含まれており、それぞれクラス名CName
に対して&CName::CName
、&CName::~CName
のようになります。メンバ関数の場合は、メンバポインタ呼び出しに関してフリー関数の関数呼び出し演算子と同様の扱いとなります(メンバ関数がオーバーロードされていない場合との一貫性のため)。
演算子についてはoperator@
の形式でオーバーロード可能なものについて、operator@
の形で渡すとオーバーロード集合型が推論されます。呼び出しの際はoperator@(args...)
の形式で呼ばれた時のルールに従ってオーバーロード解決されます。すなわち、メンバ関数非メンバ関数の演算子オーバーロードを1つのオーバーロード集合型に含めることはできません。
提案より、std::transform
で使用する例
std::vector<float> in = getInputValues(); std::vector<float> out; // std::sinを各要素に適用する std::transform(in.begin(), in.end(), std::back_inserter(out), std::sin); // Rangeアダプタでも動作 auto out_r = std::views::all(in) | std::views::transform(std::sin) | std::ranges::to<std::vector>(); // 演算子でも動作(フリー関数の場合のみ auto neg_r = std::views::all(out_r) | std::views::transform(operator-) | std::ranges::to<std::vector>();
P3313R0 Impacts of noexept on ARM table based exception metadata
noexept
が生成コードにどのような影響を与えるかについての調査報告の文書。
この文書の目的は次のような質問へ回答することです
- 関数を
noexcept
指定すると、そのメタデータはどのように変更されるか? - 関数が
noexcept
関数を呼び出すと、関数のメタデータにどのような変更があるか? try
ブロックがnoexcept
関数のみを呼び出すと、どのような影響が生じるか?noexcept
関数との相互作用は、非トリビアルなデストラクタを持つオブジェクトを管理する関数にどのように影響するか?
この文書では、GCCが使用しているARM環境におけるItanium ABIのテーブルベース例外の実装によって様々な実験を行っていますが、ここで得られる見識は他のテーブルベース例外の実装においても一貫している、としています。
文書では、ARM EHABIの実装やGCC C++ LSDAについて解説した後、noexcept
及び非noexcept
関数の様々な呼び出しパターンを実験しています。この実験の目的は次の事柄についての見識を提供することです
- 関数の例外ランク
- LSDAサイズとそのセクションのサイズ
結果と分析を通じてここでの実験の結論は、noexcept
は強力な例外保証が必要な場合に役立つものの、コード生成に関しては必ずしも利点があるとは限らないというものです。一般的に言われているnoexcept
の利点はエッジケースでのみ発生するようです。
例外機構の生成コード改善という観点からはむしろツールチェーンの改善に焦点を当てるべきだと述べており、推奨事項として次の事を挙げています
- データ構造の選択の改善
- 同一の例外エントリを持つ関数のグループ化
- リンカは同一の例外エントリを持つ関数をグループ化することができ、これを利用して同一の例外エントリを1つのエントリにマージしてテーブルサイズを削減することができる
- 関数における
noexcept
の推論- 他の関数を呼び出しているものの、その呼び出しグラフの全体にわたって全ての関数が例外を送出しない場合、その関数は
noexcept
として動作する。このような関数を手動でnoexcept
指定する代わりに、リンカがその関数が例外を送出するかどうかを判定することができる - リンカが完全なアセンブリ情報を持つすべての関数を評価して、どの関数が例外を送出するかを判定し、例外を送出することが無い関数を自動で
noexpcet
指定することができる
- 他の関数を呼び出しているものの、その呼び出しグラフの全体にわたって全ての関数が例外を送出しない場合、その関数は
この文書は何かを提案するものではありません。おそらく、契約プログラミング機能の議論の過程におけるnoexcept
の効果についての議論やLEWGにおけるnoexcept
付加ポリシーに関連したレポートだと思われます。
P3316R0 A more predictable unchecked semantic
条件をチェックしない新しい契約セマンティクスであるPromiseセマンティクスの提案。
現在の契約プログラミングの仕様には、契約注釈が実行時にどう振舞うのかについてのセマンティクスが4種類あります。これにより、静的ライブラリを開発して出荷している場合、契約プログラミング機能に対応するには現在の4倍の数のバリアントを出荷する必要がある事を意味します。
これにはコストがかかるため、単一のバージョンを出荷してリンカがリンク時最適化のタイミングで選択されているセマンティクスに応じて使用されないコードを削除することで対応できないか?と考えます。使用されないコードとは、条件式チェック、違反ハンドラ呼び出し、プログラムの中断の3つです。何も取り除かず全てを残す場合はEnforceセマンティクスになり、ハンドラ呼び出しだけを削除するとquick_enforceになり、プログラム中断だけを削除するとObserveセマンティクスになり、全てを削除するとIgnoreセマンティクスになります。
このような最適化は、チェックが失敗した場合にプログラムが終了することをオプティマイザが認識し、それに基づいて行われるものになります。しかしこれは、ライブラリが最初からIgnoreセマンティクスでコンパイルされていた場合と同じではありません。なぜなら、チェックして契約が破られていた場合のブランチが終了しない場合(Enforce/quick_enforce以外の場合)、継続パスにUBが含まれていると、現在の最適化がそうしているようにUBは起こらないものとして最適化が行われ、結果契約チェックと破られていた場合のハンドラ呼び出しを行うコードが最適化される可能性があるからです。
これは特に、Observeセマンティクスが選択された場合に起こり得ます。例えば次のような簡単なcontract_assert
を含むコードは
bool f(int* p, int* q) { if (q != nullptr) { puts("got null"); } contract_assert(p != nullptr); return *p == *q; }
Enforceセマンティクスの場合最も最適化されたとしても次も様なコードになります
bool f(int* p, int* q) { // qのチェックは起こらないものとして削除される // qのチェックで終了はしないため if (not (p != nullptr)) { invoke_handler(); die(); // プログラム終了点がある事でpのチェックは残る } return *p == *q; }
Observeセマンティクスの場合は最悪次のようになります
bool f(int* p, int* q) { // pのチェックパスのdie()が無くなることでpがnullptrでも*pまで行きつき // UBは起こらないのでPのチェックはハンドラ呼び出し毎消える return *p == *q; }
これはIgnoreセマンティクスの結果と同じです。
同様の事は事前・事後条件チェックにおいても起こり得るほか、純粋にIgnoreセマンティクスによって最適化が行われた場合でも(契約チェックの後のパスで同じ条件で分岐してる場合などに)同じことが起こり得ます。
すなわち、IgnoreセマンティクスはUBに対して脆弱であり本質的に危険なもので、契約注釈を無視できるのは防御的なスタイル(二重に同じ条件をチェックして例外によって脱出するなど)が講じられている場合のみとなります。例えば、リリースプログラムをEnforceセマンティクスでテストしてからIgnoreセマンティクスに切り替えて再度実行する場合、最適化傾向が異なるため信頼できないものとなります。
ここで提案されているのは次の3つです
- 推奨される契約チェックが行われないモードとして、IgnoreセマンティクスをPromiseセマンティクスで置き換える
- 推奨されるObserveセマンティクスとして、Promiseセマンティクスに基づくObserveセマンティクスを使用する
[[establish( expression )]]
を追加する- この属性の出現以降に式が
true
になることを表す(前になっている必要は無い
- この属性の出現以降に式が
提案後のObserveセマンティクスの先程のコードのコンパイル結果は次のようになります
bool f(int* p, int* q) { if (not (p != nullptr)) { invoke_handler(); [[establish(p != nullptr)]]; } return *p == *q; }
[[establish( expression )]]
は一種の最適化バリアであり、その出現以降でその条件が満たされているとコンパイラが仮定するようになることで、チェックし失敗後に終了しない場合でも、UBが起こらないと仮定した最適化によって契約チェックブランチが丸ごと削除されるのを防止します。
Primiseセマンティクスでは契約チェックが完全に無視される場合でもこの[[establish( expression )]]
属性が残されることによって、以降のパスでUBは起こらないものとした最適化を抑止することができ、無視セマンティクスの信頼性が向上します。また、冒頭で述べていたようなリンカによるセマンティクスの選択も適切に動作するようになります。
この提案の内容はSG21であまり賛同を得られなかったようで、リジェクトされています。
P3317R0 Compile time resolved contracts
契約をコンパイル時にチェックしておくことができるように契約プログラミング機能を設計すべきという提案。
アプリケーションによっては不意の終了が致命的である場合があります。しかし、実行時に不正なデータを素通しするのもまた危険であるため、契約チェックはenforceセマンティクスで残されるかもしれません。また、実行時のチェックを行いたいがそのための余分なリソース(CPU/メモリ)が確保できないというケースもあります。
このような環境においては、ほとんどの契約注釈をコンパイル時にチェックして実行時のコストと終了の危険性を削減するようにプログラムを作成するのは労力に見合う価値がある可能性があります。これらほどに要求が厳しくないアプリケーションにおいても、多くの契約注釈がコンパイラによってコンパイル時にチェックされている(契約が破られない)ことが分かれば、大きな安心感を得ることができます。
また、契約プログラミング機能の将来の拡張の一つとして、契約注釈をコードの仮定として扱ってコンパイラが最適化に利用できるようにする方向性が知られています。このアプローチで問題となるのは、契約が破られている場合に未定義動作に陥ることでコードがむしろ危険なものになってしまう事です。この場合に、契約注釈の多くの部分がコンパイル時に解決されていれば、実行時の未定義動作のリスクを軽減して最適化の恩恵だけを受け取ることができます。
これは契約の設計を拡張する時に考慮すべき重要な機能であり、これらのことを将来的に可能なようにしておくためには静的解析が可能なように契約機能を設計しておく必要があります。これはその方針を提案するものです。
ここでは次の3つの事を提案(SG21における投票を推奨)しています
- 契約注釈のコンパイル時診断を標準化する
- コンパイル時の診断は次の4つ
- None : 何もしない
- Known violations
- Unhandled conditions
- Runtime conditions
- 全ての契約セマンティクスはコンパイル時の解決を妨げないように設計されるべき
提案されている4つの診断モードは、コンパイル時にどの程度解決するかを指定するものです。なお、Noneは現在の動作(すべて実行時解決)です。
まずKnown violationsは呼び出された場合に必ず失敗する契約注釈についてコンパイル時に診断するモードです。
void f0() post(false) // 失敗を診断する { } void f1() pre(false) // 呼び出すまでは診断されない { } void f2() { f1(); // 失敗を診断する } void f3() { contract_assert(false) // 失敗を診断する }
これらはあまりにも自明な例ですが、コンパイル時定数やコンパイル時に分析可能な条件式などによってこれらと同等になる場合にコンパイル時に契約注釈の有効性を検出できます。
Unhandled conditionsは、コンパイル時に契約を失敗させる入力が存在することを診断するモードです。
int f0(int x) post(r: r > 5) // 診断: 無効な入力が存在する { return x; }
Runtime conditionsは、コンパイル時にはその契約が破られる可能性があるかどうか分からないものについても診断を提供するモードです。
void f0() post(secret_check()) // 診断: 契約条件の有効性は不明 { }
これらの診断モードのうち、最初の2つ(Known violationsとUnhandled conditions)は間違ったコードを明らかにするものなのでデフォルトで有効にし、最後のものはオプションのモードとしておくことを推奨しています。ただし、これらの問題を検出する能力は必ずしもコンパイラ間で同じではないため診断無しでコンパイルすることもできるようにしておくことを提案しています。
ここでの診断とは、警告とエラーかはっきりしていませんが、おそらく最初の2つの診断はエラーを意図しており、最後のものの診断は警告を意図していると思われます。
P3318R0 Throwing violation handlers, from an application programming perspective
標準ライブラリのnoexcept
ポリシーとして、Lakos Ruleを維持または復活させるべきとする文書。
LEWGにおけるポリシーの策定が行われており、noexcept
に関するポリシーも提案されています(P3085やP3155)。この文書は、その議論に関する第三者意見書として提出されたもので、Lakos Ruleに基づいたポリシーとすることを推奨するものです。
契約違反が起きている場合でも必ずしもプログラムが致命的で続行不可能な状態にあるわけではありません。むしろ、契約違反をハンドリングした状態というのはUBに突入しうる状況の直前でそれを防止したという事であり、その原因はプログラマの些細なミス(ポインタ引数に誤ってnullptr
を渡す、タイプミス、論理条件の反転、etc...)である可能性があります。その場合で、その場所がプログラム全体から見ると重要度の低い箇所である場合、そのバグからプログラムを回復させる選択肢を取ることができ、メインのプログラムを続行することができます。これはあるドメインのビジネスルールでは正しい振る舞いであり、即終了することは望ましい振る舞いではありません。
標準ライブラリの関数がLakos Ruleに従わないnoexcept
ポリシー(狭い契約を持ち"Throws: nothing"とある関数でもnoexcept
指定される)を取ると、契約プログラミング機能がもたらされ標準ライブラリにもそれが適用されている世界において、違反ハンドラにより事前条件違反を検出して(enforceセマンティクスにを選択しているとする)、それが(上記のように)回復可能なバグである場合は例外送出することで復帰する、という手段を取ることができなくなります。
例えば次のようなコードにおいて
void whatever_func() { try { // 事前条件を持つ関数 some_func_with_preconditions(oink, boink); } catch (const MyOutOfRangeContractException& e) { // 復帰処理 log(e); disable_offending_subsystem(); resume_normal_operation(); } }
some_func_with_preconditions()
が標準ライブラリの関数であり、契約プログラミング機能が採用された世界でライブラリ実装によって事前条件がコードによって記述されるようになっているとします。この場合にLakos Ruleに従わないnoexcept
ポリシーに基づく場合、事前条件が破られ例外が送出されると(違反ハンドラから例外によって復帰しようとする場合も含めて)プログラムが終了させられるため、ユーザーサイドではそこから復帰するあらゆる手段が封じられます。
Lakos Ruleに従わないnoexcept
ポリシーを採用すると、このようなユースケースへの扉が閉ざされます。そして、一度閉ざされた扉は後方互換性によって二度とは開くことができなくなり、違反ハンドラからの例外送出によって契約違反後の状態回復という手段は使用できなくなります。これは、そうしたいユーザーコミュニティがそうできないという事であり、WG21の委員会の人の大多数がそうする必要が無い(からそうできないようにしたい)という事よりもずっと悪いことです。
そのようなコミュニティの意見はWG21で十分に代表されていない可能性があり、そのようなコミュニティを軽視してWG21で代表されている意見だけに基づいてそのようなコミュニティのユースケースを禁止してしまえば、そのようなコミュニティはC++から足を遠ざけることを選択してしまうでしょう。
これらの理由から、この文書ではLEWGのnoexcept
ポリシーとしてLakos Ruleを維持することを推奨しています。
- P3085R0 noexcept policy for SD-9 (throws nothing) - WG21月次提案文書を眺める(2024年02月)
- P3155R0 noexcept policy for SD-9 (The Lakos Rule) - WG21月次提案文書を眺める(2024年02月)
- P3318 進行状況
P3319R0 Add an iota object for simd (and more)
std::simd
オブジェクト(SIMDレジスタ)を連番の値で初期化するAPIの提案。
std::simd
のジェネレータコンストラクタ(0~SIMD幅の整数定数を受けて値を生成する呼び出し可能なものを受け取って初期化するコンストラクタ)の9割の用途として、SIMDレジスタを連番(0, 1, 2, 3...
およびその定数倍と手数オフセットを足したもの)で初期化するのにに使用されることが想定されています。
// SIMDレジスタを0始まりの連番で初期化 std::simd<int> a ([](int i) { return i; }); // SIMDレジスタを2, 5, 8, 11...の連番で初期化 std::simd<int> b ([](int i) { return 2 + 3 * i; });
std::simd<int>
とすると、その環境のint
型のSIMD演算が最も効率的となるSIMD幅が自動で選択されます。このラムダ式を渡すコンストラクタ(ジェネレータコンストラクタ)は、そのラムダ式に対して0から始まる定数値を渡して呼び出して得られた値でそのSIMDレジスタを初期化するものです。
このように強く特定の用途で使用されることが分かっている場合、汎化するよりも特化したAPIにした方が分かりやすくなります。
// 上の例と同じ初期化を行う auto a = std::iota_v<std::simd<int>>; auto b = 2 + 3 * std::iota_v<std::simd<int>>
この提案は、このような用途のstd::simd
初期化のためにこのstd::iota_v
を提案するものです。
名前から分かるように、std::iota_v
はstd::simd
に対してstd::iota()
(アルゴリズム)がコンテナに対して行うのと同じ初期化を行います。ただし、std::iota_v
は初期化対象のstd::simd
型をテンプレートパラメータで受け取る変数テンプレートであり、その初期化は必ず定数で初期化され、かつSIMDレジスタ初期化にとってより効率的なものとなります。
// 提案されている宣言の例 template<class T> inline constexpr T iota_v; // 算術型用の特殊化 template<class T> requires (std::is_arithmetic_v<T>) inline constexpr T iota_v<T> = T(); // std::simd用の特殊化 template<detail::simd_type T> inline constexpr T iota_v <T> = T([](auto i) { return static_cast<typename T::value_type>(i); });
さらにこの提案では、std::iota_v<T>
で使用可能なT
として算術型の他
- 静的な要素素を持つ
value_type
メンバを持つ- 静的な要素数
N
に対して、N
個のvalue_type
型オブジェクトのリストによる初期化が可能 value_type() + 1
は定数式であり、value_type
に変換可能
を満たす任意の型を使用可能にできるようにすることを提案しています。
// どちらも可能 auto x = std::iota_v<float[5]>; auto y = std::iota_v<std::array<my_fixed_point, 8>>;
この提案の重要な点は、初期化コードが実際のSIMD幅に合わせて自動でスケーリングすることによって、可変長のリスト初期化をユーザーが手書きする必要を無くすことです。
P3320R0 EWG slides for P3144 "Delete if Incomplete"
P3144の紹介スライド。
P3144については以前の記事を参照
P3144の解決しようとしている問題(不完全型のポインタに対するdelete
演算子の適用)について紹介し、その解決方法についての分析と比較を行っています。
P3144R0では不完全型のポインタに対するdelete
演算子の使用を非推奨としたうえで、それが行われた場合の動作をEB(Erroneous Behavior)としてデストラクタ呼び出しなしでオブジェクトの生存期間を終了させる、というソリューションを提案していましたが、このスライドによる分析では全てのケースにおいてill-formedとするのがAPI破壊が起きるものの一番有効な解決策であることが示されています。
P4000R0 To TS or not to TS: that is the question
契約プログラミング機能がTSを目指す場合に考慮するべき事柄についてまとめたスライド。
内容としては、今月分にあるP3269R0を紹介するもののようです。
おわり
次月分(2024/07)の記事執筆のお手伝いを募集しています: https://github.com/onihusube/blog/issues/39