文書の一覧
全部で27本あります。
- N4914 WG21 2022-07 Admin telecon minutes
- N4915 Business Plan and Convener's Report: ISO/IEC JTC1/SC22/WG21 (C++)
- N4916 WG21 2022-07 Virtual Meeting Minutes of Meeting
- P0843R5 static_vector
- P1255R9 A view of 0 or 1 elements: views::maybe
- P2019R1 Usability improvements for std::thread
- P2164R6 views::enumerate
- P2264R4 Make assert() macro user friendly for C and C++
- P2477R3 Allow programmers to control coroutine elision
- P2511R2 Beyond operator(): NTTP callables in type-erased call wrappers
- P2517R1 Add a conditional noexcept specification to std::apply
- P2537R1 Relax va_start Requirements to Match C
- P2581R1 Specifying the Interoperability of Built Module Interface Files
- P2587R2 to_string or not to_string
- P2611R0 2022-07 Library Evolution Poll Outcomes
- P2620R1 Lifting artificial restriction on universal character names
- P2621R1 UB? In my Lexer?
- P2623R1 implicit constant initialization
- P2625R0 Slides: Life without operator() (P2511R1 presentation)
- P2626R0 charN_t incremental adoption: Casting pointers of UTF character types
- P2627R0 WG21 2022-07 Virtual Meeting Record of Discussion
- P2628R0 Extend barrier APIs with memory_order
- P2629R0 barrier token-less split arrive/wait
- P2630R0 Submdspan
- P2633R0 thread_local_inherit: Enhancing thread-local storage
- P2634R0 Allow qualifiers in constructor declarations
- P2635R0 Enhancing the break statement
- おわり
N4914 WG21 2022-07 Admin telecon minutes
WG21の各作業部会の管理者ミーティングの議事録。
前回から今回の会議の間のアクティビティの報告がされています。
N4915 Business Plan and Convener's Report: ISO/IEC JTC1/SC22/WG21 (C++)
ビジネスユーザ向けのC++およびWG21の現状報告書。
N4916 WG21 2022-07 Virtual Meeting Minutes of Meeting
2022年2月7日(北米時間)に行われた、WG21全体会議の議事録。
CWG/LWG/LEWGの投票の様子などが記載されています。
P0843R5 static_vector
静的な最大キャパシティを持ちヒープ領域を使用しないstd::vector
であるstatic_vector
の提案。
static_vector<T, N>
はstd::vector
とstd::array
のキメラのようなコンテナで、N
に指定した値を最大キャパシティとして、スタック領域(グローバル変数として使用する場合は静的ストレージ)を用いてstd::vector
のような可変長配列を実現するものです。
主たる性質は
- 動的メモリ確保を必要としない
- スタックor静的ストレージを使用する
- キャパシティ最大値がコンパイル時に指定される
- 要素は後から挿入/削除可能
- 要素のストレージは
static_vector
オブジェクト内に配置される contiguous_range
コンセプトを満たす- 要素型
T
がトリビアルであれば、全ての操作が定数式で可能
などで、static_vector
は次のような場合に有用です
- 動的メモリ確保を行えない
- 例えば、組み込み環境など
- 動的メモリ確保のコストが高くつく
- 例えば、メモリ確保に伴うレイテンシに敏感なプログラム
- 静的ストレージ上に、変則的な生存期間をもつオブジェクトを構築したい
- デフォルト構築できない型の配列など、
std::array
が選択肢にならない constexpr
関数内で可変長配列を使用したい- これはC++20以降
std::vector
でも可能
- これはC++20以降
static_vector
の要素のストレージはstatic_vector
オブジェクト自体が内包している必要がある- シリアライズのための
memcpy
サポートのためなど
- シリアライズのための
この提案のstatic_vector
は既存実装であるboost::container::static_vector
をベースとして設計されており、インターフェースや性質はstd::vector
との共通化が図られています。
#include <static_vector> // <vector>になるかもしれない int main() { std::static_vector<int, 8> sv = {1, 2, 3, 4}; std::println("{}", sv); // [1, 2, 3, 4] sv.push_back(5); sv.emplace_back(6); std::print("{}", sv); // [1, 2, 3, 4, 5, 6] }
なお、最大キャパシティを超えて要素を挿入しようとした場合、全ての操作において事前条件違反として未定義動作となります。例外を投げたリabort
したりするのかは実装の選択とされます。
- Boost.Container static_vector - Faith and Brave - C++で遊ぼう
- Class template static_vector - Boost
- P0843 進行状況
P1255R9 A view of 0 or 1 elements: views::maybe
std::optional
やポインタ等のmaybeモナドな対象を、その状態によって要素数0か1のシーケンスに変換するRangeアダプタviews::maybe
の提案。
以前の記事を参照
- 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月)
このリビジョンでの変更は
maybe_view
からnullable_view
を分離maybe_view/nullable_view
にT&
(左辺値参照型)の特殊化を追加maybe_view
にモナディックインターフェース追加and_then/or_else/transform
- フリースタンディングライブラリ機能として指定
などです。
このリビジョンで新たに追加されたnullable_view
(views::nullable
)は、nullable_object
をview
に変換するものです。nullable_object
とは文脈的なbool
変換が可能であり間接参照が可能な型のことで、void
ではないポインタ型やstd::optional
、std::expected
などが該当します。イテレータ型は通常単体でbool
変換できないためnullable_object
ではありません。
#include <ranges> using std::ranges::nullable_view; int main() { std::optional o{4}; nullable_view m{o}; for (int k : m) { cout << k; // "4"が出力 } o = std::nullopt; nullable_view m2{o}; for (int k : m2) { cout << k; // 実行されない(ループが回らない) } }
nullable_view
は、nullable_object
専用のmaybe_view
です。
これによって、maybe_view
(views::maybe
)はより一般の型のオブジェクトをview
に変換するものとなり、maybe_view
が空になるのはデフォルト構築された時です。
#include <ranges> using std::ranges::maybe_view; int main() { int i{4}; maybe_view m{i}; for (int k : m) { cout << k; // "4"が出力 } maybe_view<int> m2{}; for (int k : m2) { cout << k; // 実行されない(ループが回らない) } }
maybe_view
もnullable_view
も共に長さ0もしくは1のシーケンスになりますが、maybe_view
は値を渡して構築された時にのみ長さ1になるのに対して、nullable_view
は渡されたnullable_object
の状態によって長さが決まります。
nullable_view
はこの提案の元々のmaybe_view
であり、nullable_object
では無い型についてmaybe_view
を拡張しようとした結果、1つのクラスで実装するのは色々問題があったため、2つのクラス(とさらに2つの部分特殊化)に分離されたようです。
P2019R1 Usability improvements for std::thread
std::thread/std::jthread
において、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。
現在のstd::thread/std::jthread
には、スタックサイズを設定する方法もスレッド名を設定する方法も用意されていません。一方で、この2つのクラスが使用している実行環境のスレッドAPIでは、広くこの2つのスレッドプロパティの設定が可能となっています。
環境 | スタックサイズの設定 | スレッド名の設定 |
---|---|---|
Linux, QNX | pthread_attr_setstacksize() |
pthread_setname_np() |
Windows | CreateThread() |
SetThreadDescription() |
Darwin | pthread_attr_setstacksize() |
pthread_setname_np() |
Fuchsia | zx_thread_create() |
|
Android | pthread_attr_setstacksize() |
JavaVMAttachArgs() |
FreeBSD, OpenBSD, NetBSD | pthread_attr_setstacksize() |
pthread_setname_np() |
RTEMS | pthread_attr_setstacksize() |
pthread_setname_np() |
FreeRTOS | xTaskCreate() |
xTaskCreate() |
VxWorks | taskSpawn() |
taskSpawn() |
eCos | cyg_thread_create() |
cyg_thread_create() |
Plan 9 | threadcreate() |
threadsetname() |
Haiku | spawn_thread() |
|
Keil RTX | osThreadNew() |
|
WebAssembly |
※ 空白はなし、スレッド名の設定は一部事後的にしか行えないものがある
また、他のプログラミング言語やC++ライブラリのスレッドAPIにおいても、これらに対応した機能を提供している場合が多くみられます。
- スタックサイズの設定 : Java, Rust, Python, C#, Haskell, D, Perl, Swift, Boost, Qt
- スレッド名の設定 : Rust, Python, D, C#, Java, Raku, Swift, Qt, Folly
スタックサイズの設定は、次のような場合に必要となります
- 特定のスタックサイズで実行できるように設計されているアプリケーションの移植性と信頼性のために、プラットフォーム間で一貫したスタックサイズを指定する
- プラットフォームのデフォルトよりも小さいスタックサイズを使用する
- 一部のアプリケーションでは、メインスレッドに大きなスタックトレースを設定し、そこから起動されたスレッドにも継承させるものがある。これが望ましくない場合がある
- 有名なゲームや大規模アプリケーションなどでは、デフォルトよりも大きいスタックサイズを使用することがある
スレッド名はデバッガを始めとする外部ツールにおいて有用で、主にデバッグに活用できます
- デバッガーにおける利用
- 各種クラッシュダンプや実行トレースツール
- タスク/プロセスモニタ
- プロファイル/トレース/診断ツール
- Windows Performance Analyzer, ETW tracing
これらのことが欠けている事によって、std::thread
やstd::jthread
を使用することができず、ほぼ同等のクラスを再実装したり、基底のAPIを直接使用したりしなければならないケースがあります。また、筆者の方は、「スタックサイズのサポートがないためにstd::thread
を使うことができず、std::thread
は語彙型として失敗している」という話をゲーム開発者の人々から聞いているようです。
この提案は、それらの既存のプラクティスを標準化することで、現在std::thread/std::jthread
を使用できていない所で使用可能にしようとするものです。
ここで提案されているAPIは、プロパティ指定クラスをstd::thread_attribute
から継承させて定義した上で、そのオブジェクトをstd::thread
のコンストラクタで受け取るようにするものです。
namespace std { // スレッドプロパティ指定識別用基底クラス class thread_attribute {}; // スレッド名を指定するクラス class thread_name : thread_attribute { public: constexpr thread_name(std::string_view name) noexcept; constexpr thread_name(std::u8string_view name) noexcept; private: implementation-defined __name[implementation-defined]; // 説明専用 }; // スタックサイズを指定するクラス class thread_stack_size : thread_attribute { public: constexpr thread_stack_size(std::size_t size) noexcept; private: constexpr std::size_t __size; // 説明専用 }; class thread { ... // デフォルトコンストラクタ(元からある thread() noexcept; // 処理とその引数を受け取るコンストラクタ(元からある template<class F, class... Args> explicit thread(F&& f, Args&&... args); // 処理とプロパティ指定を受け取るコンストラクタ(この提案で追加 template<class F, class... Attrs> requires (sizeof...(Attrs) != 0) && ((is_base_of_v<thread_attribute, Attrs>) && ...) && ... explicit thread(F&& f, Attrs&&... attrs); ... }; // jthreadも同様(略 }
std::thread_name/std::thread_stack_size
が渡された設定値をどのように保持して取り出せるようにするかは実装定義とされています。
これらのものを次のように使用してスレッドに設定します。
void f(); int main() { // スレッド名とスタックサイズの指定 std::jthread thread(f, std::thread_name("Worker"), std::thread_stack_size(512*1024)); return 0; }
指定されたプロパティが設定可能でない場合(例えばWASM環境など)、実装はこの指定を無視することができます。
このプロパティ指定の方法はこの2つのプロパティ以外にも拡張可能で、例えばスレッドのアフィニティや優先度を指定可能とすることも将来的には可能なようにされています。とはいえ、この提案ではスタックサイズとスレッド名の2つのプロパティのみを提案しています。
P2164R6 views::enumerate
元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrate
の提案。
以前の記事を参照
- P2164R0 views::enumerate - [C++]WG21月次提案文書を眺める(2020年5月)
- P2164R1 views::enumerate - [C++]WG21月次提案文書を眺める(2020年6月)
- P2164R2 views::enumerate - [C++]WG21月次提案文書を眺める(2020年9月)
- P2164R3 views::enumerate - [C++]WG21月次提案文書を眺める(2020年11月)
- P2164R4 views::enumerate - [C++]WG21月次提案文書を眺める(2021年02月)
- P2164R5 views::enumerate - [C++]WG21月次提案文書を眺める(2021年06月)
このリビジョンでの変更は、enumerate_result
を説明専用コンセプトtuple-like
を満たす型のリストに追加したこと、enumerate_view::iterator::operator*
は値(prvalue)を返すためCpp17ForwardIteratorコンセプトを満たす事ができず、それに応じてイテレータカテゴリを調整したことなどです。
P2264R4 Make assert() macro user friendly for C and C++
assert
マクロをC++の構文に馴染むように置き換える提案。
- P2264R0 Make assert() macro user friendly for C and C++ - WG21月次提案文書を眺める(2020年12月)
- P2264R2 Make assert() macro user friendly for C and C++ - WG21月次提案文書を眺める(2021年10月)
- P2264R3 Make assert() macro user friendly for C and C++ - WG21月次提案文書を眺める(2022年03月)
このリビジョンでの変更は、Cでの採用に伴う文書の調整などです。
P2477R3 Allow programmers to control coroutine elision
コルーチンの動的メモリ確保を避ける最適化を制御するAPIを追加する提案。
以前の記事を参照
- P2477R0 Allow programmer to control and detect coroutine elision by static constexpr bool should_elide() and - WG21月次提案文書を眺める(2021年10月)
- P2477R2 Allow programmer to control and detect coroutine elision - WG21月次提案文書を眺める(2021年09月)
このリビジョンでの変更は、
promise_type::must_elide(P0, ..., Pn)
が最初に評価されるようにすることを提案- コンパイラが処理(最適化)できない非同期ケースの例を追加
- この提案のAPIが必要であるケース
- 最適化が起きたことを検出するAPIを提案から外した
- ユースケースが限られていたため
- 最適化とメモリ使用量についての議論を追加
- 例を追加
などです。
P2511R2 Beyond operator()
: NTTP callables in type-erased call wrappers
std::move_only_fuction
及びstd::function
を、呼び出し可能なNTTP値を型消去するCallable Wrapperへ拡張する提案。
以前の記事を参照
- P2511R0 Beyond
operator()
: NTTP callables in type-erased call wrappers - WG21月次提案文書を眺める(2022年01月) - P2511R1 Beyond
operator()
: NTTP callables in type-erased call wrappers - WG21月次提案文書を眺める(2022年03月)
このリビジョンでの変更は、機能テストマクロを追加したこと、実装経験を追加したことです。
P2517R1 Add a conditional noexcept specification to std::apply
std::apply
にnoexcept
指定を行う提案。
以前の記事を参照
このリビジョンでの変更は、提案する文言の修正とベースとなるワーキングドラフトの更新です。
この提案は既に、2022年7月の全体会議で承認され、C++23ワーキングドラフト入りしています。
P2537R1 Relax va_start Requirements to Match C
可変長引数関数を0個の引数で宣言できるようにする提案。
以前の記事を参照
このリビジョンでの変更は、C23の提案(N2975、C23に導入済み)に仕様を整合させたことです。
P2581R1 Specifying the Interoperability of Built Module Interface Files
ビルド済みモジュールを扱う際に、ビルドシステムがそのビルド済みモジュールファイルを直接扱うことができるかどうかを調べられるようにする提案。
以前の記事を参照
このリビジョンでの変更は
- Binary Module Interfaceという用語をBuilt Module Interfaceに置き換えた
- モジュールをインポートする翻訳単位と、モジュールのインターフェースの翻訳単位の間で、独立したパースコンテキストを持つことについての調査に関するセクションを追加
読みやすさ向上のための調整
P2587R2 to_string
or not to_string
std::to_string
の浮動小数点数出力を修正する提案。
以前の記事を参照
- P2587R0
to_string
or notto_string
- WG21月次提案文書を眺める(2022年05月) - P2587R1
to_string
or notto_string
- WG21月次提案文書を眺める(2022年07月)
このリビジョンでの変更は、LEWG投票の結果を追記した事、std::format
によって将来的に他の型にstd::to_string
を拡張できることを追記、コードベースの調査結果に関して追記したことなどです。
この提案の変更を受けるのは浮動小数点数用のstf::to_string()
だけですが、筆者の方の調査によればそれはあまり使われておらず、ロケールに関するバグがあるものもあったとのことです。
P2611R0 2022-07 Library Evolution Poll Outcomes
2022年の7月に行われた、LEWGでの全体投票の結果。
次の提案が、LWGに転送されることが承認されました
- C++23
- P0429R9 flat_map
- P1222R4 flat_set
- P0792R10 function_ref
- P2505R4 Monadic Functions For expected
- P2585R0 Improving Default Container Formatting
- P2446R2 views::as_rvalue
- P2278R4 cbegin Should Always Return A Constant Iterator
- P2248R5 Enabling List-Initialization For Algorithms
- P2539R1 Should The Output Of print To A Terminal Be Synchronized With The Underlying Stream?
- P2551R2 Clarify Intent Of Individually Specializable Numeric Traits
- P2599R2 index_type & size_type In mdspan
- P2604R0 mdspan: Rename pointer, data, And contiguous
- P2613R1 Add The Missing empty To mdspan
- C++26
C++23向けの提案の一部は、7月の全体会議でC++23ワーキングドラフト入りが承認されたものが含まれています(全体会議ではこれについてLWGの座長が苦言を呈していたようですが・・・)。
また、投票にあたって寄せられたコメントが記載されています。
P2620R1 Lifting artificial restriction on universal character names
ユニコード文字名によって指定するユニバーサルキャラクタ名(名前付文字エスケープ)を識別子に使用した時の制限を解除する提案。
以前の記事を参照
このリビジョンでの変更は、タイポ修正と提案する文言の改善のみです。
P2621R1 UB? In my Lexer?
字句解析するだけで未定義動作を引き起こすものについて、未定義ではなくする提案。
以前の記事を参照
このリビジョンでの変更は、タイポ修正のみです。
P2623R1 implicit constant initialization
一時オブジェクトへの参照によるダングリング発生を削減する提案。
以前の記事を参照
このリビジョンでの変更は
- 既存資料からの参照の明確化
- 説明の明確化
- Why not beforeセクションを削減
const
を使用していないstd::initializer_list
の例(この提案によって安全になる)を追加
などです。
P2625R0 Slides: Life without operator() (P2511R1 presentation)
P2511(std::nontype
)の解説スライド。
P2511はstd::function, std::move_only_function
(将来的にはstd::function_ref
にも)にオーバーヘッドなく簡易なメンバポインタサポートを入れようとする提案です。詳細は以前の記事参照
- P2511R0 Beyond
operator()
: NTTP callables in type-erased call wrappers - WG21月次提案文書を眺める(2022年01月) - P2511R1 Beyond
operator()
: NTTP callables in type-erased call wrappers - WG21月次提案文書を眺める(2022年03月)
このスライドはLEWGのメンバに向けてP2511のモチベーションや利点、使用感について説明するものです。
P2626R0 charN_t
incremental adoption: Casting pointers of UTF character types
互換性のあるユニコード文字表現を持つ異なる型の間での合法的なポインタキャストを行うライブラリ機能の提案。
char8_t, char16_t, char32_t
(以降、ユニコード文字型)などの登場前、C++では任意のユニコード文字(列)を表すのにchar
を使用してバイト単位でそれらを扱うことがよく行われていました。しかし、char
はそれ自身が文字型であり、しばしばそのエンコーディングはUTF-8ではなく、整数型やバイト型と区別されずに使用されています。char
は文字、特にユニコード文字を表す型としては適切ではありません。
そのような背景もあってC++11でchar16_t, char32_t
が、C++20でchar8_t
が導入され、これらを用いて型によって文字エンコーディングを表現することができるようになりました。
しかし、C++20あるいはC++11以前からあるコードでは前述のchar
を用いたプラクティスが残っており、またwchar_t
等別の方法でユニコード文字(列)を表現するコードが多数存在しています。それらの方法で表現されているユニコード文字列とユニコード文字型のシーケンスによるユニコード文字列との間でコピーや未定義動作を伴うことなく変換する方法がないことから、それらのレガシーなインターフェースにユニコード文字型をアダプトできず、それによってユニコード文字型の使用が妨げられています。
この提案は、ユニコード文字型と既存のchar
文字列やwchar_t
文字列などとの間で、エンコーディングを仮定しそれを保持したまま変換するための関数を標準ライブラリに追加することで、そのような問題を解決しようとするものです。
ここで提案されているのは、次の2つの関数です
std::cast_as_utf_unchecked()
:std::byte, char, wchar_t
あるいはサイズN
の符号なし整数型のポインタから、charN_t
のポインタへ変換するstd::cast_utf_to<T>()
:charN_t
のポインタからT
のポインタへ変換するT
はstd::byte, char, wchar_t
あるいはサイズN
の符号なし整数型std::cast_as_utf_unchecked()
の逆変換
この2つの関数はstd::start_lifetime_as_array()
を適切にその文字列に対して適用するのと似たセマンティクスを持ち、ソース領域のオブジェクトのライフタイムを終了させ、指定された型のオブジェクトのライフタイムをその値を保ったまま開始させます。ただし、単なるconstexpr std::start_lifetime_as_array()
ではなく、適切な文字エンコーディングサポートのために、追加のコンパイラサポートを必要とします。
有効な変換の例
char8_t |
char16_t |
char32_t |
|
---|---|---|---|
char |
✅ | ❌ | ❌ |
unsigned char |
✅ | ❌ | ❌ |
uit_least_16_t |
❌ | ✅ | ❌ |
uit_least_32_t |
❌ | ❌ | ✅ |
wchar_t |
❗️ | ❗️ | ❗️ |
std::byte |
✅ | ❌ | ❌ |
行要素->列要素 : std::cast_as_utf_unchecked()
列要素->行要素 : std::cast_utf_to<T>()
表中の❗️は実装定義であることを表します。
これらの関数はその変換にあたって実際にその変換が有効なのかとか、文字がきちんと変換先のエンコーディングになっているのかなどのチェックを一切行いませんが、これらの関数を使用した変換はその意図をコードに反映させる意味も持ちます。例えば、std::cast_as_utf_unchecked()
によってchar8_t
に変換する場合、変換後の文字列はUTF-8の正しいシーケンスでありそうなっていなければなりません。このように、文字エンコーディングについてのプログラマの暗黙的な仮定をコード上で明確にする役割を担ってもいます。
// 現在よく試みられる方法 void before() { // 入力文字列(wchar_tのエンコーディングをUTF-16とする) const wchar_t* str = L"Hello 🌎"; // 多くのプログラマ: ill-formed const char16_t* u = static_cast<const char16_t*>(str); // 経験10年のプログラマ: UB const char16_t* u = reinterpret_cast<const char16_t*>(str); // Cのアプローチ: UB in C++ const char16_t* u = (const char16_t*)(str); // rangesの利用: O(n)かかる、contiguous_rangeでなくなる auto v = std::wstring_view(str) | std::views::transform(std::bit_cast<char16_t, wchar_t>); // 別のメモリにコピー: O(n)、動的確保 auto v = std::wstring_view(str) | std::views::transform(std::bit_cast<char16_t, char16_t>) | std::to<std::vector>; // launderの利用: まだUB const char16_t* u = std::launder(reinterpret_cast<const char16_t*>(str)); // エキスパート: constexprではない const char16_t* u = std::start_lifetime_as_array(reinterpret_cast<const char16_t*>(str), std::char_traits<wchar_t>::length(str)); } // この提案 void after() { // コピーなし、UBなし、constexpr、明確なセマンティクスを持ち実際には何もしない constexpr std::u16_string_view v = std::cast_as_utf_unchecked(L"Hello"sv); }
利便性向上のため、std::cast_as_utf_unchecked()
とstd::cast_utf_to<T>()
にはそれぞれ、string_view
とspan
を受け取るオーバーロードが用意されます。
P2627R0 WG21 2022-07 Virtual Meeting Record of Discussion
2022/07の全体会議の議事録。
N4916のものよりも、誰がどのような発言をしたかが詳しく記録されています。
P2628R0 Extend barrier APIs with memory_order
std::barrier
の各操作に、メモリオーダーを指定できるようにする提案。
std::barrier
はC++20で追加された、複数スレッドの進行管理に使用することのできる同期プリミティブです。その実体はカウンタであり、.arrive()
によってカウンタ減算を、.wait()
によってカウンタリセットまでの待機を行います。ほかにも、それらの複合操作である.arrive_and_wait()
、同期するスレッドのグループから途中離脱するための.arrive_and_drop()
も用意されています。
これらのstd::barrier
のAPIは全てメモリオーダーを指定するものではなく、.arrive()
よりも前に行われた全てのメモリ操作は、.wait()
による待機解除後に可視になることが(すべてのスレッドに渡って)保証されます。
この保証はかなり強いもので、場合によってはこの保証が望ましくない場合があります
- C++プログラムの外部とやり取りをする場合
- 例えば、バリアに参加するすべてのスレッドがそれぞれファイルを開いて読み書きする時、
.arrive_and_wait(1, memory_order_relaxed)
によってすべてのスレッドがファイルを閉じたことを同期する。- この場合、メモリの可視性(ファイルクローズの可視性)はファイルシステムによって確保される
- 例えば、すべてのスレッドは
volatile
操作によって実行環境(マシン)の設定を行ってから、その中の1つのスレッドが環境をスタートさせるような場合。これは、memory_order_relaxed
な.arrive()/.wait()
操作と、std::barrier
の完了関数によって実現できる。- この場合、
volatile
な書き込みが環境開始時(待機解除後)に可視になっていればよく、それはvolatile
操作によって保証される。
- この場合、
- 例えば、バリアに参加するすべてのスレッドがそれぞれファイルを開いて読み書きする時、
- オブジェクトフェンス(P2535)を利用する場合
- 一部のメモリ操作についてのみ可視になればよく、すべてのメモリ操作にその保証は必要ない
この提案は、これらの場合などに、std::barrier
を用いたより効率的な同期を可能とするために、std::barrier
のAPIがstd::memory_order
を追加で受け取れるように拡張しようとするものです。
上記2のケースのサンプル
現在 | この提案 |
---|---|
// Thread 0: x = 1; atomic_object_fence(memory_order_release, x); bar.arrive(); // release fence // Thread 1 bar.arrive_and_wait(); // acquire fence atomic_object_fence(memory_order_acquire, x); assert(x == 1); |
// Thread 0: x = 1; atomic_object_fence(memory_order_release, x); bar.arrive(1, memory_order_relaxed); // no fence // Thread 1 bar.arrive_and_wait(memory_order_relaxed); // no fence atomic_object_fence(memory_order_acquire, x); assert(x == 1); |
現在の例では、この場合のatomic_object_fence
は意味がありません(std::barrier
がより強い同期を保証しているため)。この提案後、memory_order_relaxed
と共にstd::barrier
を使用することでstd::barrier
の保証がほぼなくなり、atomic_object_fence
によって特定の変数のメモリ可視性のみが保証されるようになります(それによって、同期のコスト低減が可能となりうる)。
この提案の内容はstd::latch
をはじめとした他の同期プリミティブにも適用可能ですが、この提案では現在のところ、std::barrier
にのみ焦点を絞っています。
P2629R0 barrier
token-less split arrive/wait
std::barrier
をデータパイプラインの実装においてより効率的に使用できるように拡張する提案。
データパイプラインはデータ処理のモデルの一つで、データ処理の1連の流れを並列化可能なブロックに区切り、それら処理ブロックをパイプライン化することで高速化を図るものです。データパイプラインによる処理は例えば、ディープラーニングやHPCアプリケーションにおいて一般的に行われいます。
そのようなところでは典型的に、各パイプラインステージにおいてproducer-consumerパターンによるリソース共有が行われます。producerとconsumerの2つのスレッドグループは、バリアのペアを使用して共有リソースへのアクセスを同期します。
consumerスレッドはconsumerバリアによってリソース読み取りを待機(wait
)し、バリアが解除されるとリソースを使用し、使用終了するとproducerバリアに到着(arrive
)して共有リソースの再利用(更新)が安全であることをproducerに通知します。
producerスレッドはproducerバリアによってリソース更新を待機(wait
)し、バリアが解除されるとリソースを更新し、更新終了するとconsumerバリアに到着(arrive
)して共有リソースの使用(読み取り)が安全であることをconsumerに通知します。
この同期パターンでは、consumerスレッドはconsumerバリアに到着することなく待機し、producerバリアには待機することなく到着します(producerスレッドの場合はこの逆)。
このパターンのバリアにstd::barrier
を使用しようとすると、std::barrier
の同期がバリア上で待機する前に到着することを要求するため、同期が非効率になってしまいます。
#include <barrier> // 共有リソース // 1つのオブジェクトを使いまわす int shared_resource = 0; // producerバリア // consumer -> producer への読み込み終了通知 std::barrier producer_barrier{2}; // consumerバリア // producer -> consumer への書き込み終了通知 std::barrier consumer_barrier{2}; // リソースを更新する関数 void produce(int&); // リソースを消費する関数 void consume(const int&); // producer本体 void producer() { // consumerバリアのカウントを1にしておく(デッドロック回避のため) std::ignore = consumer_barrier.arrive(); while(true) { // consumerスレッドの読み込み終了を待機 producer_barrier.arrive_and_wait(); // A // リソース更新 produce(shared_resource); // consumerスレッドへ書き込み終了を通知 std::ignore = consumer_barrier.arrive(); // B } } // consumer本体 void consumer() { while(true) { // producerスレッドの書き込み終了を待機 consumer_barrier.arrive_and_wait(); // A // リソース利用 consume(shared_resource); // producerスレッドへ読み込み終了を通知 std::ignore = producer_barrier.arrive(); // B } } int main() { std::jthread th{producer}; consumer(); }
問題があるのはサンプル中のA
とB
(コメント)のところです。A
のところでは到着(.arrive()
)は必要なく待機(.wait()
)だけが必要であり、B
のところではarrive()
によって発行されるバリアトークンが不用で、[[nodiscard]]
による警告を消すために余計なコードが必要になります。
このA
で起こる不要なarrive()
とB
で起こる不要なバリアトークンの発行がこのようなパターンの実装時のオーバーヘッドとなり、しかも現在のstd::barrier
のAPIでは回避できません。
この提案は、この問題の解決(このようなパターンの効率実装)のために、std::barrier
に「到着しない待機」と「待機しない到着」を行うためのAPIを追加する提案です。提案では、「到着しない待機」のために.wait_parity(bool)
、「待機しない到着」のために.arrive_and_discard()
を追加することを提案しています。これらを用いると上記のサンプルは次のように書くことができます
#include <barrier> // 共有リソース // 1つのオブジェクトを使いまわす int shared_resource = 0; // producerバリア // consumer -> producer への読み込み終了通知 // producerスレッドにおける到着を考慮する必要が無くなり、同期スレッド数は-1される std::barrier producer_barrier{1}; // consumerバリア // producer -> consumer への書き込み終了通知 // consumerスレッドにおける到着を考慮する必要が無くなり、同期スレッド数は-1される std::barrier consumer_barrier{1}; // リソースを更新する関数 void produce(int&); // リソースを消費する関数 void consume(const int&); // producer本体 void producer() { // consumerバリアのカウントを0にしておく(デッドロック回避のため) // ここで最初のバリアフェーズが完了する(0 -> 1) consumer_barrier.arrive_and_discard(); bool phase = false; while(true) { // consumerスレッドの読み込み終了を待機 producer_barrier.wait_parity(phase); // A // バリアフェーズの更新 phase = !phase; // リソース更新 produce(shared_resource); // consumerスレッドへ書き込み終了を通知 consumer_barrier.arrive_and_discard(); // B } } // consumer本体 void consumer() { bool phase = false; while(true) { // producerスレッドの書き込み終了を待機 // 1番最初は、フェーズ1に対してフェーズ0で待機するため、すぐブロック解除 consumer_barrier.wait_parity(phase); // A // バリアフェーズの更新 phase = !phase; // リソース利用 consume(shared_resource); // producerスレッドへ読み込み終了を通知 producer_barrier.arrive_and_discard(); // B } } int main() { std::jthread th{producer}; consumer(); }
A
の点では.wait_parity()
によってバリアのカウントに触ることなく待機し、B
の点では.arrive_and_discard()
によって待機せず(バリアトークンを発行せず)に到着しています。これによって、現在のAPIでは回避できなかった不要な到着と待機(バリアトークン発行)を回避することができており、なおかつそれは実装に通知されるためより効率的な同期となることが期待できます。
また、これによってそれぞれのスレッドにおいての不要な到着を管理するために本来不要な同期数(std::barrier
のコンストラクタに渡している値)の+1が必要なくなってもいます(もっとも、これは実際のスレッド数と一致する以外の恩恵はなさそうですが)。
.wait_parity()
に渡しているbool
値phase
は、ある連続した2回のバリアフェーズ(std::barrier
による1回の同期)を識別するためのもので、true/false
によって異なるバリアフェーズに対する.wait()
を行うものです。これは、.wait_parity()
による待機がどのバリアフェーズに対するものなのかを識別するためのものです。
std::barrier
による同期では、ある同期ポイント(.arrive_and_wait()
)に指定した数のスレッドが到達した後、完了関数を実行してから内部カウンタをリセットし待機しているスレッドグループを再開、という流れを繰り返すものです。この1つの流れ(同期ポイントから同期ポイントの間の処理)のことをバリアフェーズと呼び、std::barrier
の保証によって各バリアフェーズは時間方向に順番に実行されるため最初を0としてインデックスを振ることができます。
そのようなバリアフェーズのインデックスについて、true
の場合に奇数インデックスfalse
の場合に偶数インデックスとしてbool
値を対応づけて、このbool
値のことをパリティと呼びます。
バリアフェーズの進行(インデックス)とパリティの関係 phase : 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> ... parity : false -> true -> false -> true -> false -> true -> ...
.wait_parity()
に渡すbool
値は、待機対象のパリティ(すなわちバリアフェーズ)を指定するもので、指定したパリティ(true/false
)から次のパリティへ変化するフェーズを待機します。例えば、wait_parity(false)
とした場合は偶数インデックスのバリアフェーズを待機し、待機が解除されたら奇数インデックスのバリアフェースが開始しています。もし現在のフェーズのパリティと指定されたパリティが異なる場合、それは1つ前のフェーズに対する待機だとして処理(すなわちすぐブロック解除)されます。
P2630R0 Submdspan
std::mdspan
の部分スライスを取得する関数submdspan()
の提案。
この提案は以前はstd::mdspan
の提案(P0009)に含まれていましたが、std::mdspan
をC++23に間に合わせるための時間の制約から切り離され、個別の提案として議論していくことになりました。
mdspan
とそこでのsubmdspan
については以前の記事を参照
submdspan
によってstd::mdspan
から部分領域をスライスとしてstd::mdspan
で取り出すことができると、std::mdspan
を受け取るように実装されたより小さい問題を効率的に解決する関数を再利用することができるようになります(行列積における、各要素ごとの内積計算など)。
提案文書より、長方体内の3次元格子を表すstd::mdspan
の表面領域を0クリアするサンプル
// 2次元領域をゼロクリアする template<class T, class E, class L, class A> void zero_2d(mdspan<T, E, L, A> grid2d) { // ランク2(2次元)のmdspan static_assert(grid2d.rank() == 2); for(int i = 0; i < grid2d.extent(0); ++i) { for(int j = 0; j < grid2d.extent(1); ++j) { grid2d[i,j] = 0; } } } // 長方体表面をゼロクリアする template<class T, class E, class L, class A> void zero_surface(mdspan<T, E, L, A> grid3d) { // ランク3(3次元)のmdspan static_assert(grid3d.rank() == 3); // 6つの表面毎に、2次元平面をゼロクリアするzero_2d()を再利用 zero_2d(submdspan(grid3d, 0, full_extent, full_extent)); zero_2d(submdspan(grid3d, full_extent, 0, full_extent)); zero_2d(submdspan(grid3d, full_extent, full_extent, 0)); zero_2d(submdspan(grid3d, grid3d.extent(0) - 1, full_extent, full_extent)); zero_2d(submdspan(grid3d, full_extent, grid3d.extent(1) - 1, full_extent)); zero_2d(submdspan(grid3d, full_extent, full_extent, grid3d.extent(2) - 1)); }
submdspan()
はstd
名前空間に定義されるフリー関数です。1つ目の引数にstd::mdspan
を取り、2つ目以降の引数でスライス指定を次元毎に受け取ります。スライス指定は、1つ目の引数をx
、対応する次元の番号をd
として[0, x.extent(d))
の範囲の値のどの要素が返されるstd::mdspan
に含まれているかを指定します(std::pair{4, 10}
のようにして任意の範囲を指定できます)。
この提案ではP0009から切り離したsubmdspan()
を個別提案として再構成するとともに、次の変更を加えています
- スライス指定として指定可能なものに
strided_index_range
型を追加 - ユーザー定義のレイアウトポリシーを使用可能なようにカスタマイゼーションポイントを追加
- 入力
mdspan<T, E, L, A>
のL
(レイアウトマッピングクラス)をカスタムするためにsubmdspan_mapping()
とsubmdspan_offset()
を追加 submdspan()
内部ではこの2つの関数をL
のオブジェクトを用いたADLによって呼ぶことで、この2つの関数をL
毎にカスタム可能にする- これらの結果を用いて、返す
mdspan
のレイアウトポリシーとアクセサ、データハンドル(領域を参照する方法、通常はポインタ)を取得する
- 入力
- スライスをコンパイル時の値として指定する機能を追加
- スライス指定が定数値で指定されたときに、それを返す
mdspan
のエクステントテンプレートパラメータに埋め込む
- スライス指定が定数値で指定されたときに、それを返す
submdspan()
の実装イメージ
template<class T, class E, class L, class A, class ... SliceArgs> constexpr auto submdspan(const mdspan<T, E, L, A>& src, SliceArgs... args) { // 部分mdspanのオフセット取得 size_t sub_offset = submdspan_offset(src.mapping(), args...); // ADLによるカスタマイズポイント // レイアウトマッピングの取得 auto sub_map = submdspan_mapping(src.mapping(), args...); // ADLによるカスタマイズポイント // アクセサの取得 typename A::offset_policy sub_acc(src.accessor()); // A::offset_policy入れ子型によるカスタマイズポイント // データハンドルの取得 typename A::offset_policy::data_handle_type // A::offset_policy::data_handle_type入れ子型によるカスタマイズポイント sub_handle = src.accessor().offset(src.data_handle(), sub_offset); return mdspan(sub_handle, sub_map, sub_acc); }
この提案はわざわざP0009から分離したこともあり、おそらくC++23には間に合わないと思われます。
P2633R0 thread_local_inherit
: Enhancing thread-local storage
呼び出し元でのスレッドローカル変数の値を引き継いで初期化されるスレッドローカル変数を作成するための、thread_local_inherit
指定の提案。
スレッドローカル変数はスレッド生成時に呼び出し元のスレッドの対応する変数が持っている値とは無関係に初期化されます。しかし、呼び出し元スレッドの対応するスレッドローカル変数の値を引き継ぎつつ、後からその値を更新したい場合があるようです。そのような変数はスレッドで実行する関数の引数として渡すこともできますが、スレッド起動時点ではその変数を用意できない場合があるほか、スレッドを起動しているプログラマ(コード)が呼び出すスレッドに関しての情報を持っていない場合もあります。スレッドにデータを渡す他の方法は面倒であり、スレッド起動側/スレッド内処理側のプログラマがそれぞれどのようなデータを受け渡す必要があるかを知っている必要があります。
呼び出されたスレッドに必要なデータを必要な時点で必要な場所に自動的に供給可能な言語機能にはメリットがあり、それを実現可能なthread_local_inherit
という新しいスレッドローカルストレージ指定を導入しようとする提案です。
thread_local_inherit
変数は、初期化周りのこととtrivially copyableな型にしか指定できないこと以外はthread_local
変数と同じ性質を持ちます。
メインスレッドにおけるthread_local_inherit
変数はthread_local
変数と同様に初期化され、子スレッドのthread_local_inherit
変数は呼び出し元スレッドの変数の値を単純にコピーすることによって静的初期化されます。thread_local_inherit
変数に対する動的初期化はその変数がまだ初期化されていない場合にのみ起こり、これはおそらくメインスレッドでの最初の初期化時にのみ起こります。
#include <future> #include <iostream> thread_local int th1 = 0; thread_local_inherit int th2 = 0; void f() { std::cout << "thread_local : " << th1 << "\n"; std::cout << "thread_local_inherit : " << th2 << "\n"; } int main() { std::ignore = std::async(f); th1 = 1; th2 = 1; std::ignore = std::async(f); th1 = 2; th2 = 2; std::ignore = std::async(f); }
thread_local : 0 thread_local_inherit : 0 thread_local : 0 thread_local_inherit : 1 thread_local : 0 thread_local_inherit : 2
P2634R0 Allow qualifiers in constructor declarations
コンストラクタに、const/volatile
および参照修飾を行えるようにする提案。
オブジェクトがconst
で構築される時とそうでない時、あるいは左辺値で構築される時と右辺値で構築される時、これらによって呼び出すコンストラクタを分けると便利な場合があります。メンバ関数では、CV/参照修飾によって呼び出される関数をオブジェクトの状態毎に定義することができますが、コンストラクタにはそれらの修飾が行えないため、現在はこのようなことはできません。
この提案は、コンストラクタにCV/参照修飾できるようにすることでコンストラクタを構築のされ方によって呼び分けることができるようにしようとするものです。
struct S { S() &; // #1、左辺値として構築する際のコンストラクタ S() &&; // #2、右辺地として構築する際のコンストラクタ S() const &; // #3、const左辺値として構築する際のコンストラクタ }; S x; // #1が呼ばれる S(); // #2が呼ばれる const S y; // #3が呼ばれる new const S; // #3が呼ばれる
これは例えば、delete
と共に使用して右辺値としての構築を禁止したり、const
として構築された時にだけコンストラクタの処理を単純化する、などの用法がありそうです。
P2635R0 Enhancing the break statement
break
文を拡張して、ネストしたループにおける利便性を改善する提案。
ネストしたループにおいて内側のループをbreak
で終了させる時、終了直後に何かしたい(特に、外側のループの継続に関して)場合があります。このような場合、典型的にはフラグを使用するかgoto
を使用する必要があります。
void before1(std::ranges::range auto&& range_i) { for (auto&& range_j : range_i) { bool broke = false; for (auto j : range_j) { if (cond(j)) { // ループを継続する処理 ... } else { // ループを中断する broke = true; break; } } if (broke) { continue; } else { // 内側ループが完了した後の処理 ... } } } void before2(std::ranges::range auto&& range_i) { for (auto&& range_j : range_i) { for (auto j : range_j) { if (cond(j)) { // ループを継続する処理 ... } else { // ループを中断する goto broke; } } // 内側ループが完了した後の処理 ... // 内側ループの中断先 broke: ; } }
他にも関数にするとかラムダ式を使うとかありますが、いずれにせよこのような場合にbreak
の効果は不十分でした。
この提案は、break
を拡張してこのような場合に余分なコードを最小にしつつ同じことを達成できるようにするものです。
提案では、break statement;
という構文を許可するようにし、break
直後にstatement
を実行させるようにします。
void after(std::ranges::range auto&& range_i) { for (auto&& range_j : range_i) { for (auto j : range_j) { if (cond(j)) { // ループを継続する処理 ... } else { // ループを中断し、外側ループではcontinueする break continue; } } // 内側ループが完了した後の処理 ... } }
break statement;
のstatement
にはreturn
とかbreak
そのものもおく事ができで、多重ループからの脱出にはbreak break ...;
と書く事ができます
void after(std::ranges::range auto&& range_i) { for (auto&& range_j : range_i) { for (auto j : range_j) { if (cond(j)) { // 条件を満たしたら2重ループから脱出 break break; } } } }