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

文書の一覧

全部で122本あります。

一部の記事は次の方々に手伝っていただきました、ご協力ありがとうございました!

もくじ

N4946 2024-03 Tokyo meeting information

2024年3月に東京で開催される、WG21全体会議のインフォメーション。

開催期間翌週の月火(2023/03/25-26)には、C++ to Japanというカンファレンスイベントが開催される予定です。

N4947 INCITS C++/WG21 agenda: 12-17 June 2023, Varna, Bulgaria

2023年6月にブルガリアのヴェルナで開催される、WG21全体会議のアジェンダ

ここからは、C++26に向けた作業となります。

N4948 Working Draft, C++ Extensions for Library Fundamentals, Version 3

Library Fundamental TS v3のワーキングドラフト。

N4949 Editor's Report: C++ Extensions for Library Fundamentals, Version 3

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

新しく追加された機能などはなく、編集上の修正のみのようです。

N4950 Working Draft, Standard for Programming Language C++

C++23のワーキングドラフト第10弾。

これはC++23の最後のドラフトであり、おそらくC++23 標準規格文書と同等なものとなります。

N4951 Editors' Report - Programming Languages - C++

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

今回は新しく採択された提案はなく、編集上の修正のみです。

N4953 Concurrency TS2

Concurrency TS v2のワーキングドラフト。

N4954 2023 WG21 admin telecon meetings, rev. 1

2023年(今年)のWG21管理者ミーティングの予定表。

P0342R2 pessimize_hint

<chrono>の時計型のnow()が最適化によって並べ替えられないようにする提案。

以前の記事を参照

このリビジョンではこの問題のより汎用的な解決のために、std::pessimize_hint()という恒等関数を提案しています。

namespace std {
  // pessimize_hintの宣言
  template <typename T> T&       pessimize_hint(T& t      ) noexcept;
  template <typename T> T const& pessimize_hint(T const& t) noexcept;
}

これは値を生成する式に対して使用して、その値の最適化に関して実装が最大限悲観的な仮定を置くように指示するものです。

// 以前のサンプルコードの修正部分のみ抜粋
int main() {
  // fib()の実行にかかる時間を計測する
  auto start = std::chrono::high_resolution_clock::now();

  // std::pessimize_hint()を通して値を消費/生成する
  auto result = std::pessimize_hint(fib(std::pessimize_hint(42)));
  
  auto end = std::chrono::high_resolution_clock::now();

  ...
}

悲観的な仮定というのは、pessimize_hint()の引数の式は適格なC++プログラムが実行可能なことは何でも実行できる(実行する)ということで、つまりはその式中で何が起こらないかを仮定できないということです。この例の場合は、fib()が内部でhigh_resolution_clock::now()を呼び出すかもしれないため、質の良い実装ではコード上に見える2回目のhigh_resolution_clock::now()の呼び出しの前にfib()が評価されることを保証します。

このアプローチはひとまずSG1では反対無しで合意され、EWGへ転送されています。

P0447R22 Introduction of std::hive to the standard library

要素が削除されない限りそのメモリ位置が安定なコンテナであるstd::hive(旧名std::colony)の提案。

以前の記事を参照

このリビジョンでの変更は、Appendixにいくつかの項目(hiveの制約の概要、先行技術情報、代替実装に関する情報)を追加したことなどです。

P0843R6 static_vector

静的な最大キャパシティを持ちヒープ領域を使用しないstd::vectorであるstatic_vectorの提案。

以前の記事を参照

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

  • std::vectorのものと共通するようにpush_back()のセマンティクスを修正
  • std::optionalを返すtry_push_back()を追加
  • 最大キャパシティを超えた時に未定義動作となるpush_back_unchecked()の追加
  • inplace_vectorに名前を変更したい事を追記

などです。

P1000R5 C++ IS schedule

C++26策定までのスケジュールなどを説明した文書。

P1028R5 SG14 status_code and standard error object

現在の<sysytem_error>にあるものを置き換える、エラーコード/ステータス伝搬のためのライブラリ機能の提案。

以前の記事を参照

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

  • string_refは実装定義の型となった
  • status_code_ptrnested_status_codeに変更
  • make_nested_status_code()はアロケータを受け取るように変更
  • erased<T>は削除され、erased_status_code<T>status_codeの適切なタグ付き特殊化へのエイリアスとして追加

などです。

P1061R5 Structured Bindings can introduce a Pack

構造化束縛可能なオブジェクトをパラメータパックに変換可能にする提案。

以前の記事を参照

このリビジョンでの変更は、文言の修正のみです。

P1068R7 Vector API for random number generation

<random>にある既存の分布生成器にイテレータ範囲を乱数で初期化するAPIを追加する提案。

以前の記事を参照

このリビジョンでの変更は、引数順の変更と文言の修正などです。

乱数エンジンをE、分布生成器をD、範囲をRとして、以前の引数順はstd::generate_random(E、D, R)だったのが、他のアルゴリズムに合わせて範囲が先に来るように変更され、std::generate_random(R, E, D)となりました。

std::array<float, N> array; 

std::mt19937 eng(777);  // 乱数エンジン
std::uniform_real_distribution dis(1.f, 2.f); // 分布生成器

// R6
std::ranges::generate_random(eng, dis, array);

// このリビジョン
std::ranges::generate_random(array, eng, dis);

P1112R4 Language support for class layout control

クラスレイアウトを明示的に制御するための構文の提案。

構造体の非静的メンバ変数として異なるサイズやアライメントを持つ型を持たせると、それらの間や構造体末尾にパディングが発生します。そのクラスが多数のメンバ変数を持つ場合や配列の要素として扱われる場合、このようなパディングは無駄なメモリを消費することになります。

// boolのサイズはintのサイズよりも小さい
static_assert(sizeof(bool) < sizeof(int));

struct S1 {
  bool b;
  int n;
};

// S1のサイズはint2つ分に等しい -> sizeof(int) - sizeof(bool)の分パディングが含まれる
static_assert(sizeof(S1) == (sizeof(int) * 2));

struct S2 {
  int n;
  bool b;
};

// S1のサイズはint2つ分に等しい -> sizeof(int) - sizeof(bool)の分パディングが含まれる
static_assert(sizeof(S2) == (sizeof(int) * 2));

P = sizeof(int) - sizeof(bool)とすると、通常S1の場合はbの後にPバイトのパディングが挿入され、S2の場合は構造体末尾(bの後)にPバイトのパディングが挿入されています。

このようなパディングが挿入されるかどうかは型のサイズだけではなく型のアライメントによっても変化し、メンバが増えたりクラス型だったりすると予測が難しくなります。また、環境の間でデータ型のサイズやアライメントが異なる場合があり、パディングのサイズと位置の予測をさらに困難にさせます。

クラスのパディングをなるべく無くそうとする時に取れる方法として、メンバ変数の順序を並べ替えるという方法があります。パディングが挿入される領域にそのパディングサイズ以下の型が来るようにメンバを並べ替えることで、パディングを最小にすることができます。しかし、C++のコード上からのクラスメンバの順序はその初期化順序と結びついており、メンバを並べ替えると初期化順序が変化します。また、可読性向上のためにメンバ変数を何かしらのグループにまとめる形で並べている場合、並べ替えることによってクラスの可読性が低下します。

従って、パディングを減らすために型のメンバの全てのサイズとアライメントを知っていたとしても、そのためにメンバを並べ替えることは望ましくありません。また、標準ライブラリ等ABIに気を使っている実装を除けば、クラス型のサイズは任意のタイミングで変化する可能性があります。そのため、メンバの順序について完全に制御化にあるクラスであってもそのメンバとなるクラス型について制御が及ばない場合、ある時点で最適なメンバ順序はそれ以降常に最適であり続ける保証はありません。

C++高級言語であり、多くの場合クラスのメンバ変数はあるクラスのメンバとしての意味論のもとで使用され、それがメモリ上でどのように配置されているかを細かく気にすることは(このパディングの問題がなければほとんどの場合)気にする必要はありません。double7個とbool8個(8バイトx7 + 1バイトx8 = 64バイト)をメンバに持つクラスが64バイトの領域に収まって動作する場合、それが72バイトあるいは120バイトの領域を占めるとすればそれは完全にメモリの無駄遣いであり、配列要素として使用すると無駄な領域がその要素数で乗算され増大したり、キャッシュの局所性を損ねたりといったパフォーマンス上のデメリットをもたらします。

この提案は、パディングの問題をコンパイラが自動的に最適なものに解決するようにすることで、これらの問題を解決しようとするものです。

この提案はlayout属性を追加し、その引数としてレイアウトに関する指示を与えてクラスの宣言に指定することでレイアウトの自動最適化を行うアプローチを提案しています。この属性は[[...]]のようなものとは異なり、alignasなどに準ずるものです。

レイアウトに関する指示(strategy)は最初に最低1つ追加した後からベンダ拡張も含めて将来的に追加していく予定としていますが、とりあえず次の3つが例としてあげられています

  • layout(smallest)
    • クラスのサイズを最小にするためにメンバ順序を変更する
  • layout(standard)
    • 構造体がstandard-layoutであることを保証する
  • layout(explicit)
    • 実装定義、何もしないことも並べ替えをすることも許可されるほか、PGOや外部ソースからの注入などによるレイアウト変更を許可する

提案文書より、サンプルコード

現在 この提案
// スペース節約のために手作業で最適化
// メンバを追加する場合はやりなおし
struct Dog {
  std::string name;
  std::string bered;
  std::string owner;
  int age;
  bool sex_male;
  bool can_bark;
  bool bark_extra_deep;
  double weight;
  double bark_freq;
};
// クラスサイズが最小になるように自動調整される
struct layout(smallest) Dog {
  std::string name;
  std::string bered;
  int age;
  bool sex_male;
  double weight;
  std::string owner;
  bool can_bark;
  double bark_freq;
  bool bark_extra_deep;
};
現在 この提案
struct cell {
  int idx;
  double fortran_input;
  double fortran_output;
};

// スタンダードレイアウトを保証する
static_assert(std::is_standard_layout_v<cell>);
// スタンダードレイアウトを保証する
struct layout(standard) cell {
  int idx;
  double fortran_input;
  double fortran_output;
};
現在 この提案
// trick to simulate extension
#define CELL_MEMBERS \
  int idx; \
  double fortran_input; \
  double fortran_output;

// 共通のメンバを持つクラスをスタンダードレイアウトにする

struct cell {
  CELL_MEMBERS
};
static_assert(std::is_standard_layout_v<cell>);

struct cell_ex {
  CELL_MEMBERS
  int extra_info;
};
static_assert(std::is_standard_layout_v<cell_ex>);
// どちらもスタンダードレイアウトであることが保証される

struct layout(standard) cell {
  int idx;
  double fortran_input;
  double fortran_output;
};

struct layout(standard) cell_ex : cell {
  int extra_info;
};

この提案によるレイアウト調整は実メモリ上のクラスメンバの配置順序を並べ替えたりしますが、C++コード上からのその順序を並べ替えません。すなわち、クラスメンバの初期化順序はコード上の順序によって行われます。

なお、layoutという属性名やレイアウトに関する指示名等はまだ確定した名前ではなく、今後の議論とともに固めていく予定です。

P1144R8 std::is_trivially_relocatable

オブジェクトの再配置(relocation)という操作を定義し、それをサポートするためのユーティリティを整える提案。

以前の記事を参照

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

  • trivially_relocatableな型の要件から、ムーブ構築可能であるか破棄可能であること、というのを削除
  • is_trivially_relocatable_vの事前条件をis_trivially_copyable_vに一致するように調整
  • is_nothrow_relocatable_vを同期が不十分なため削除
  • std::relocate[[nodiscard]]を付加し、戻り値型をremove_cv_t<T>に変更
  • 重複する可能性のあるサブオブジェクトに関する問題点に関する記述を削除(実装経験から問題ではないことが分かったため)
  • pmr型に関する議論を拡張
  • Design goalsセクションを単純化

などです。

改訂された要約によると、この提案は次の5つのユースケースを満足することを目指しています

// unique_ptrはtrivially relocatableであること
static_assert(std::is_trivially_relocatable_v<std::unique_ptr<int>>); // #1

struct RuleOfZero { std::unique_ptr<int> p_; };

// trivially relocatable型だけをメンバに持つ型はまたtrivially relocatable
static_assert(std::is_trivially_relocatable_v<RuleOfZero>); // #2

// trivially relocatableであることを注釈
struct [[trivially_relocatable]] RuleOf3 {
    RuleOf3(RuleOf3&&);
    RuleOf3& operator=(RuleOf3&&);
    ~RuleOf3();
};

static_assert(std::is_trivially_relocatable_v<RuleOf3>); // #3

// 注釈はメンバのtrivially relocatable性より優先される
struct [[trivially_relocatable]] Wrap0 {
    boost::movelib::unique_ptr<int> p_;
    static_assert(!std::is_trivially_relocatable_v<decltype(p_)>);
        // 注釈はされていないが、実際にはtrivially relocatableであることを知っている
};

static_assert(std::is_trivially_relocatable_v<Wrap0>); // #4

// 同上
struct [[trivially_relocatable]] Wrap3 {
    Wrap3(Wrap3&&);
    Wrap3& operator=(Wrap3&&);
    ~Wrap3();
    int i_;
    boost::interprocess::offset_ptr<int> p_ = &i_;
    static_assert(!std::is_trivially_relocatable_v<decltype(p_)>);
        // trivially relocatableではないが、クラス全体として不変条件を保存する
};

static_assert(std::is_trivially_relocatable_v<Wrap3>); // #5

P1684R5 mdarray: An Owning Multidimensional Array Analog of mdspan

多次元配列クラスmdarrayの提案。

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

  • コンストラクオーバーロードのオプションについて議論を追加
  • コンテナと整数パックを取るコンストラクタを削除
  • コンストラクタ引数順を調整し、extents/mappingはコンテナの前に来るようにする
  • 推論補助が曖昧にならないように修正
  • コンテナサイズが十分大きいことに関する関連機能に事前条件を追加
  • mdarrayからコンテナを移動するextract_container()の追加
  • data()container_data()へ変更し、container_size()を追加
    • data()は非ユニークレイアウトの場合など、size()と相性が悪かった
    • ムーブ後状態のdata()mapping().required_span_size()と一緒に動作しない可能性があった
  • data()pointerはcontiguousコンテナのために必要な要件ではないという事実に対処
  • to_mdspanmdspanへの変換演算子を修正

などです。

P1759R6 Native handles and file streams

標準ファイルストリームに、OSやプラットフォームネイティブのファイルを示すものを取得する方法およびその型エイリアスを追加する提案。

以前の記事を参照

このリビジョンでの変更は、native_handle()constかつnoexceptにしたこと、LWGのフィードバックに伴う文言修正などです。

この提案は、2023年6月に行われた全体会議で投票にかけられ、C++26 WDへの導入が決定しています。

P1885R12 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更は、text_encodingオブジェクトがtext_encoding(text_encoding::other)によって構築されたときに名前が存在しない問題に対処して不変条件を修正した事です。

この提案は、2023年6月に行われた全体会議で投票にかけられ、C++26 WDへの導入が決定しています。

P1901R2 Enabling the Use of weak_ptr as Keys in Unordered Associative Containers

std::weak_ptrを非順序連想コンテナのキーとして使用できるようにする提案。

この提案の目指すところは、std::shared_ptr/std::weak_ptrを所有権ベースで区別し、それを非順序連想コンテナ(特に、std::unordered_set)で管理できるようにすることです。順序付き連想コンテナは比較方法をカスタマイズするだけでそれを達成でき、そのためにstd::owner_lessが用意されています。

現在の標準ライブラリにはそのサポートが無く、自前で用意しようとすると、所有権ベース同値比較はowner_before()で行えてもstd::shared_ptr/std::weak_ptrの(所有権ベースの)ハッシュを求めるポータブルな方法がありませんでした。

この提案は、標準ライブラリにそのためのユーティリティを用意することで、ポータブルかつ簡易にstd::shared_ptr/std::weak_ptrを所有権ベースで非順序連想コンテナのキーとして使用可能にするものです。

この提案では、非順序連想コンテナが使用するstd::hashstd::equal_toに対応するものとして、std::owner_hashstd::owner_equalを標準ライブラリに追加します。これは、既存のstd::owner_lessを参考にしたAPIです。

namespace std {
  struct owner_hash {
    template <class T>
    size_t operator()(const shared_ptr<T>&) const noexcept;

    template <class T>
    size_t operator()(const weak_ptr<T>&) const noexcept;

    using is_transparent = unspecified;
  };

  struct owner_equal {
    template <class T, class U>
    bool operator()(const shared_ptr<T>&, const shared_ptr<U>&) const noexcept;

    template <class T, class U>
    bool operator()(const shared_ptr<T>&, const weak_ptr<U>&) const noexcept;

    template <class T, class U>
    bool operator()(const weak_ptr<T>&, const shared_ptr<U>&) const noexcept;

    template <class T, class U>
    bool operator()(const weak_ptr<T>&, const weak_ptr<U>&) const noexcept;
    
    using is_transparent = unspecified;
  };
}

この2つのクラスはoperator()に渡された型のowner_hash()/owner_equal()メンバ関数を使用して所有権ベースのハッシュ計算/同値比較を行います。そのために、std::shared_ptr/std::weak_ptrメンバ関数としてハッシュを求めるowner_hash()、所有権ベース同値比較を行うowner_equal()を追加します。

非順序連想コンテナのデフォルトの比較関数型/ハッシュ型をこれらによって置き換えることで、std::shared_ptr/std::weak_ptrオブジェクトを所有権ベースで非順序連想コンテナに格納することができるようになります。

template<typename T>
using weak_ptr_hashset = std::unordered_set<std::weak_ptr<T>, std::owner_hash, std::owner_equal>;

int main() {
  weak_ptr_hashset<int> set{};

  auto sp1 = std::make_shared<int>(10);
  set.insert(sp1);

  auto sp2 = std::make_shared<int>(20);
  set.insert(sp2);

  auto sp3 = sp1;
  set.insert(sp3);

  assert(set.size() == 2);
}

この提案は既にLWGのレビューを終えており、C++26に向けて次の全体会議で投票にかけられる予定です。

P1928R4 std::simd - Merge data-parallel types from the Parallelism TS 2

std::simd<T>をParallelism TS v2から標準ライブラリへ移す提案。

以前の記事を参照

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

  • 文言のdiffを削除
  • タイトルにstd::simdを含むように変更
  • rangesへの対応とそれを通したstd::formatサポートについて議論を追加
  • イテレータを値で受け取るように修正
  • 添字演算子に左辺値参照修飾を追加
  • value_typeのオブジェクトに対して対応する演算子が有効であるように、各演算子を制約
  • mask reductionsの名前を変更
  • ABIに関する議論や疑問点の記述を削除
  • simd_maskの最初のテンプレートパラメータに関する疑問点を追記
  • マスク引数を取るロード/ストアのオーバーロードを追加
  • simd_mask引数を使用するようにsimdreductionsを修正
  • simdを返すsimd_mask演算子を追加
  • 条件演算子のhidden friendsオーバーロードsimdsimd_maskに追加
  • simdのためのstd::hashについての議論を追加
  • 比較が必要ないくつかの関数をtotally_orderedで制約
  • 変換ルールの再検討
  • ロード/ストアのフラグ名を変更
  • ロード/ストアのフラグを拡張して、変換を行うフラグを追加
  • hmin/hmax命名に関する議論を追加
  • simdのフリースタンディング化について議論を追加
  • splitconcatについて議論を追加
  • P0788R3のライブラリ指定スタイルを適用

などです。

P2019R3 Thread attributes

std::thread/std::jthreadにおいて、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。

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

  • コンストラクタを追加する代わりにmake_with_attributes()ファクトリ関数を提案
  • スレッドプロパティをstd::threadに設定する際にNTTPで渡すAPIについての議論の追加
  • std::threadにスレッドプロパティのセッター/ゲッターAPIを追加する事についての議論を追加

このリビジョンでは、コンストラクタでスレッドプロパティを渡してから構築するAPIから、ファクトリ関数でスレッドプロパティを指定して構築するものに変わっています

namespace std {

  // 属性定義は変化なし
  ...

  class thread {
    ...

    // デフォルトコンストラクタ(元からある
    thread() noexcept;

    // 処理とその引数を受け取るコンストラクタ(元からある
    template<class F, class... Args>
    explicit thread(F&& f, Args&&... args);

    // 処理とプロパティ指定を受け取るコンストラクタ(R2まで
    //template<class F, class... Attrs>
    //  requires (sizeof...(Attrs) != 0) &&
    //           ((is_base_of_v<thread_attribute, Attrs>) && ...) &&
    //           ...
    //explicit thread(F&& f, Attrs&&... attrs);

    // 処理とプロパティ指定を受け取るファクトリ関数(このリビジョン
    template <class F, class... Attrs>
    static thread make_with_attributes(F && f, Attrs&&... attrs);
    ...
  
  private:
    template <class F, class... Attrs>
    thread(attribute-tag, F && f, Attrs&&... attrs); // 説明専用
  };

  // jthreadも同様
}
void f();

int main() {
  // スレッド名とスタックサイズの指定
  auto thread = std::jthread::make_with_attributes(f, std::thread_name("Worker"),
                                                      std::thread_stack_size(512*1024));
}

P2022R2 Rangified version of lexicographical_compare_three_way

std::lexicographical_compare_three_wayのRange版を追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正のみです。

P2141R1 Aggregates are named tuples

集成体(Aggregate)を名前付きのstd::tupleであるとみなし、標準ライブラリにおけるstd::tupleのサポートを集成体に拡張する提案。

以前の記事を参照

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

  • 設計の決定に関するLEWGへの質問に対するLEWGの解答を追記
  • std::tuple_sizeの特殊化ではなくstd::element_countを使用するように変更
  • libstdc++での試験実装に関して追記

などです。

集成体の要素数取得にstd::tuple_sizeを使用しないようにしたのは、tupleを平坦化する(tupletupletupleにする)ようなコードが在野に存在しており、そこではstd::tuple_sizeの使用可能性によってtupleの要素が平坦化対象であるかを判定している場合があり、その場合にこの提案によって任意の集成体でstd::tuple_sizeが提供されるとその動作が静かに変更されるため、それを回避するためです。

P2300R7 std::execution

P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。

以前の記事を参照

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

  • 修正
    • sender_ofコンセプトはTT&&を区別しない
    • senderファクトリjust/just_errorにC配列を渡した際、減衰されるのではなくエラーになるようにした
  • 機能拡張
    • senderreceiverコンセプトに対しては、enable_senderenable_receiverというオプトイン型特性が用意されるようになった
      • ここには、入れ子is_sender/is_receiver型を探索するデフォルト実装が用意される
    • get_attrsを削除し代わりにget_envを使用するように
    • get_envは対応するtag_invokeオーバーロードが見つからない場合にempty_env{}を返すようにフォールバックする
    • get_envはその引数のCV参照修飾に影響されないようにする
    • get_envempty_envenv_of_tstd名前空間に移動
    • senderの非同期プログラミングモデルを抽象的な用語で説明するセクションを追加(§11.3 Asynchronous operations [async.ops]

などです。

P2447R4 std::span over an initializer list

std::spaninitializer_listを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、P2752への参照を追加したこと、HTMLの修正などです。

P2752はinitializer_listの背後にある配列を静的ストレージに配置することを許可する提案でC++26に向けて採択されています。これはこの提案の懸念の一つであるinitializer_listのダングリングに関する問題を回避しません(initializer_listの生存期間外にそれが参照する配列を参照するのは相変わらず未定義動作)が、組み合わせることで最適化を促進する可能性があるとのことです。

P2752はinitializer_listの背後にある配列を静的ストレージに配置することで不可視の余分なコピーを回避する最適化を促進するもので、この提案はstd::spaninitializer_listから構築できるようにするものです。この2つが組み合わさることで、initializer_listから構築されたstd::spanは静的ストレージの配列を直接参照するものになり、initializer_listのためのスタック消費すら回避することが可能になります。これは少なくとも、先行実装のclangにおいて行われることが確認されています。

P2495R3 Interfacing stringstreams with string_view

std::stringstreamstd::string_viewを受けとれるようにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正とis_convertible_v<const T&, const CharT*> == falseという制約を削除したことです。これによって、const CharT*を取るコンストラクタにアロケータとオープンモードの指定ができるようになります。

この提案は2023年6月の全体会議でC++26に向けて採択されています。

P2500R1 C++ parallel algorithms and P2300

P2300で提案されている実行コンテキストの指定を、C++17の並列アルゴリズムにも適用できるようにすることを目指す提案。

以前の記事を参照(番号が間違って公開されたとのことで提案番号が変更されています)

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

for_eachアルゴリズムで示すと、この提案によって追加されるものは次のものです

namespace std {
  namespace ranges {
    // ポリシーベースAPI(従来の従来の実行ポリシーのみを受け取る)
    template<execution_policy Policy, input_iterator I, sentinel_for<I> S, class Proj = identity,
             indirectly_unary_invocable<projected<I, Proj>> Fun>
    constexpr ranges::for_each_result<I, Fun>
      ranges::for_each(Policy&& policy, I first, S last, Fun f, Proj proj = {});
    
    template<execution_policy Policy, input_range R, class Proj = identity,
             indirectly_unary_invocable<projected<iterator_t<R>, Proj>> Fun>
    constexpr ranges::for_each_result<borrowed_iterator_t<R>, Fun>
      ranges::for_each(Policy&& policy, R&& r, Fun f, Proj proj = {});

    // スケジューラベースAPI(scheduler+実行ポリシーを受け取る)
    template<policy_aware_scheduler Scheduler, input_iterator I, sentinel_for<I> S,
             class Proj = identity, indirectly_unary_invocable<projected<I, Proj>> Fun>
    constexpr ranges::for_each_result<I, Fun>
      ranges::for_each(Scheduler sched, I first, S last, Fun f, Proj proj = {}) /*customizable*/;

    template<policy_aware_scheduler Scheduler, input_range R, class Proj = identity,
             indirectly_unary_invocable<projected<iterator_t<R>, Proj>> Fun>
    constexpr ranges::for_each_result<borrowed_iterator_t<R>, Fun>
      ranges::for_each(Scheduler sched, R&& r, Fun f, Proj proj = {}) /*customizable*/;
  }

  // 現在の並列アルゴリズムに対するschedulerオーバーロード
  template <policy_aware_scheduler Scheduler, typename ForwardIterator, typename Function>
  void for_each(Scheduler&& sched, ForwardIterator first, ForwardIterator last, Function f);
}

P2546R4 Debugging Support

標準ライブラリにデバッグサポートの為のユーティリティを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGでの投票結果を記載したことです。

この提案はLEWGでのレビューを終えて、LWGに転送されています。

P2548R5 copyable_function

std::move_only_functionに対して、コピー可能なCallableラッパであるcopyable_functionの提案。

以前の記事を参照

このリビジョンでの変更は、命名に関するセクションを追加したこと、型消去関数ラッパの二重ラッピングを回避するための推奨プラクティスを追記したことなどです。

この提案は2023年6月の全体会議でC++26に採択されています。

P2552R2 On the ignorability of standard attributes

属性を無視できるという概念について、定義し直す提案。

以前の記事を参照

このリビジョンでの変更は明確ではありませんが、この提案では以前に示していた3つの観点に基づいた3つの属性無視に関するルールを提案しています

  1. 属性の構文的な無視に関するルール
    • 標準属性は構文的に無視できず、パースされなければならない
    • 引数の構文エラーや固有の規則や追加の構文要件は診断されなければならない
    • 属性引数のエンティティはODR-used
  2. 標準属性の意味的な無視に関するルール
    • well-formedなプログラムが与えられた時、特定の標準属性のインスタンスを全て削除すると、プログラムの観測可能な振る舞いを変化させることが許可される
    • ただし、削除後の動作が削除前のプログラムにとって適合した振る舞いである場合に限る
  3. __has_cpp_attributeの振る舞いに関するルール
    • 標準属性の機能テストマクロは、実装がその属性のオプショナルなセマンティクスを実装している場合にのみ正の値を返す
    • 1つ目のルールに要求されるように、単にそれを構文的にパースして構文をチェックするだけの場合には正の値を返してはならない

この提案は次のリビジョン(R3)が2023年6月の全体会議でC++26に採択されています。

P2561R2 A control flow operator

std::expectedなどを返す関数において、エラーの伝播を自動化させる演算子??の提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更と演算子名をe??からe.try?に変更したことです。

名前の変更の理由は、??がnull合体演算子として他の言語で多用されていて、ここで提案しているエラー伝播とはかなり異なることをしており、演算子の意味の混同を回避するためです。

P2621R3 UB? In my Lexer?

字句解析するだけで未定義動作を引き起こすものについて、未定義ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなるWDを更新したことです。

この提案は2023年6月の全体会議でC++26に向けて採択されています。

P2637R2 Member visit

std::visitやなどをメンバ関数として追加する提案。

以前の記事を参照

このリビジョンでの変更は、std::visit_format_argを非推奨にしたことと機能テストマクロを追加したことです。

この提案は2023年6月の全体会議でC++26に向けて採択されています。

P2641R3 Checking if a union alternative is active

定数式において、あるオブジェクトが生存期間内にあるかを調べるためのstd::is_within_lifetime()の提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したことと、この関数が参照ではなくポインタをとる理由を追記したことです。

is_within_lifetime()が参照ではなくポインタを取るのは

  • 一時オブジェクトを考慮しないで良くなる
  • 他の低レベルの機能もポインタを取る(std::construct_at()std::start_lifetime_as()など)
  • 参照の有効性に関して考慮しなくて良くなる

などの理由によります。

この提案は2023年6月の全体会議でC++26に向けて採択されています。

P2643R1 Improving C++ concurrency features

C++20で追加された動機プリミティブ周りの機能を強化・改善する提案。

以前の記事を参照

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

  • pari<T, bool>optional<T>を戻り値として使用する際の利点/欠点について追記
  • <chrono>の一部がフリースタンディングではないことを考慮して、時限待機関数のフリースタンディング指定に関する議論を追記
  • barrier::try_wait_forbarrier::try_wait_untilの提案する文言を追加
  • サンプルコードを追加
  • barrier::try_wait_for/_untilarrival_tokenを受け取らないように修正
  • ヒント付き待機メカニズムに関する議論を削除(別の提案とするため)
  • 時間制限のないtry_waitを削除

などです。

P2654R0 Modules and Macros

標準ライブラリで提供されるマクロを、標準ライブラリモジュール(std.compat)からエクスポートするようにする提案。

C++23からstd/std.compatモジュールが提供されるようになり、標準ライブラリの全体をモジュールとしてインポートできるようになります。ただし、これには標準ライブラリ(特に、C互換ヘッダ)でマクロとして提供される機能が含まれていません。

これはモジュールの仕様に基づくもので、名前付きモジュールからはマクロをエクスポートすることができないためです。この制限がないヘッダユニットと呼ばれる、従来のヘッダファイルをモジュールとしてimportする方法もありこちらはマクロもエクスポートされますが、標準ライブラリのC互換ヘッダはヘッダユニットとしてimport可能であるかは実装定義です。

結局、標準のマクロ機能を使用しようとすると、従来のヘッダファイルのインクルード以外に手段がありません。

この提案は、モジュールにおけるマクロの扱いに変更を加えることなくこの制限を取り除くために、標準ライブラリ中でマクロとして提供される機能の代替提供手段を検討するものです。

この提案では、その対象として次のものを挙げています

  • リテラル値に置換されるマクロ
    • これらは#ifディレクティブで多用されるため、constexpr変数で置換できない
    • テキスト置換を行わない新しいプリプロセッシングディレクティブにより解決(別提案)
  • assert
    • このマクロはC++においては様々な問題を抱えている
    • P2884R0では、assertキーワード化して演算子として使用するようにすることを提案しており、懸念事項が取り上げられている
  • offsetof
    • P2883R0で議論
  • setjmp/longjmp
    • C++オブジェクトモデル及びオブジェクト生存期間の概念と直接関わるもの
    • キーワード化して動作を提供することを提案
  • va_arg
    • 言語の基礎的な機能であり、importで使用可能であるべき
    • キーワード化して動作を提供することを提案
  • errno
    • 現在解決案はない
  • ATOMIC_XXX_LOCK_FREE
    • これらのマクロはサポートされる場合にコンパイラによって定義される(モジュールからエクスポートする必要がない)
  • ATOMIC_FLAG_INIT
    • C23ライブラリで削除されているため、削除すれば解決

この提案では必ずしも個別の解決策全てを提案しておらず、他の提案に委ねている部分があります。

P2662R1 Pack Indexing

以前の記事を参照

パラメータパックにインデックスアクセスできるようにする提案。

以前の記事を参照

このリビジョンでの変更は、EWGのリクエストにより構文の代替案を検討したこと、提案する文言を改善したことなどです。

現在の構文はpack...[index]のような構文ですが、異なる選択肢として次のような構文があげられています

  • pack.[index];
  • pack<index>もしくはpack...<index>
  • std::nth_type<index, pack...>もしくはstd::nth_value<index>(pack...)
  • packexpr(args, I);
  • [index]pack;
  • パックオブジェクト(P2671R0)

この提案では今の所、現在のpack...[index]が最善であるとしています。

この提案はEWGでのレビューを終えて、C++26目指してCWGに転送されています。

P2663R2 Proposal to support interleaved complex values in std::simd

P2663R3 Proposal to support interleaved complex values in std::simd

std::simdstd::complexをサポートできるようにする提案。

以前の記事を参照

R2での変更は、提案の概要を追加したことなどです。

このリビジョンでの変更は、提案する文言を追加したことです。

R2では、std::simd<std::complex<T>>に対するアクセサ(real()/imag())や、数学関数の特殊化を用意するようにしています。

P2664R2 Proposal to extend std::simd with permutation API

P2664R3 Proposal to extend std::simd with permutation API

Parallelism TS v2にあるstd::simdに、permute操作のサポートを追加する提案。

以前の記事を参照

R2での変更は、提案の概要を追加したことなどです。

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

  • 生成されたシーケンスを最適化するコンパイラの機能について追記
  • メモリ操作(gather/scatter)を独自のクラスにした
  • ジェネレータが返す特別なインデックスと、ジェネレータに対するサイズ引数を設計オプションから本文へ移動
    • ジェネレータは入力配列の使用する要素を指定するインデックスを返す関数(ラムダ式等)
    • 特別なインデックスを返すことで要素の初期化を制御したり、サイズ引数を追加で渡すことでインデックス計算を効率化する
  • マスクを用いたcompress/expand操作を行う関数に、空いた場所を埋める値を指定する引数を追加
  • gather/scatterによるメモリの並び替え関数の文言と例を追加
  • コンパイル時/実行時/マスクによる並び替えに関する文言を追加

などです。

P2685R1 Language Support For Scoped Objects

スコープ付アロケータモデル(scoped allocator model)に基づくアロケータのカスタマイズのための言語機能の提案。

以前の記事を参照

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

  • スコープ付オブジェクトモデルをより明確にターゲットにした
  • emダッシュとenダッシュを一貫性をもって使い分ける
  • 多相アロケータのconstexprについて質問を追記

などです。

P2686R1 constexpr structured bindings and references to constexpr variables

構造化束縛にconstexpr指定できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、ローカルスコープの構造化束縛によって参照が使用される場合の問題が指摘されたことを受けて、それに対処した実装の選択肢を追加したことです。

参照を取得することは変数のアドレスを取得することとほぼ等価です。構造化束縛においては、tuple-likeオブジェクトに対しての場合にのみ参照が暗黙的に使用されており、定数式においてこれが問題になります。

// 以前の提案は構造化束縛をそのままconstexpr対応させただけのものだった
void f() {
  // これは
  constexpr auto [a] = std::tuple(1);
  static_assert(a == 1);

  // このように展開される
  constexpr auto __sb = std::tuple(1);  // __sb has automatic storage scenario.
  constexpr const int& a = get<0>(__sb);
}

ローカル変数のアドレスは不定でありその関数の実行の度に変化します。したがって、この場合の参照aが保持するアドレスは関数の実行の度に変化します。一方でaconstexpr変数であるので、コンパイル時に1度初期化された後はどのタイミングで参照しても定数であるはずです。すなわち、ローカル変数へのconstexpr参照は定数になりえず、constexprであることと矛盾します。

これが問題になるのは、自動ストレージのtuple-likeオブジェクトに対して構造化束縛する場合で、それはすなわちローカルスコープでtuple-likeオブジェクトに対して構造化束縛する、ごく一般的なケースです。それ以外の場合(配列やクラス型オブジェクトに対する構造化束縛)では問題にならず、constexpr参照の正しい用法は静的ストレージにあるオブジェクトを参照させることです。

CWGのレビューにおいてこれが問題視され、これを解決するための方向性を検討し1つを選択するためにEWGに差し戻されました。このリビジョンでは、そのための選択肢をいくつか用意して説明しています。

  1. staticであるか非tuple-likeの場合のみconstexpr構造化束縛を許可する
  2. constexpr変数を暗黙staticにする
    • 既存コードを壊すため現実的ではない
  3. get()の呼び出しを常に再評価する
    • tuple-likeオブジェクトの構造化束縛の場合のみ、constexpr参照の発生を受け入れる
  4. 記号的なアドレス指定(Symbolic addressing
    • コンパイル時参照はアドレスによって変数を参照するのではなく、特定のオブジェクトそのものを参照する
    • そして、それを定数評価の間維持する

この提案では最も有望な選択肢として4番目の方法を推しています。記号的なアドレス指定は構造化束縛に特化したものではないため、より一般的なconstexpr参照/ポインタを許可することができます。

記号的なアドレス指定によって許可されるconstexpr参照は、その参照先のオブジェクトがコンパイル時定数であるかとは直行した概念となります。

int main() {
  static int i = 0;
  static constexpr int & r = i; // ok
  
  int j = 0;
  constexpr int & s = j; // ng、記号的なアドレス指定モデルのもとではok
}

参照をコンパイル時定数にできるのは、定数評価中に参照がどのオブジェクトを参照しているのかを(そのオブジェクトが定数であるかに関係なく)追跡可能だからです。

EWGのレビューにおいては、thread_local変数を除いた記号的なアドレス指定の方向性が支持され、文言調整のためにCWGに転送されました。ただし、最終的な承認のためには実装経験や実装者からのフィードバックが必要であるとしています。

P2689R2 atomic_accessor

アトミック操作を適用した参照を返すmdspanのアクセッサである、atomic_accessorの提案。

以前の記事を参照

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

  • atomic-ref-boundedatomic-ref-boundに変更
  • atomic-ref-unboundedatomic-ref-unboundに変更
  • basic-atomic-accessor::offsetbasic-atomic-accessor::accessの文言を修正
  • P2616R3が採択された場合同様の変更をatomic-ref-boundに加える必要があることを確認

などです。

この提案はLEWGにてレビュー中です。

P2717R1 Tool Introspection

C++周辺ツールが、Ecosystem ISにどれほど準拠しているのかを互いに通信する手段を標準化する提案。

以前の記事を参照

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

  • スコープ、機能レベル、ユースケースおよび提案する文言を追加した
  • イントロスペクションの実装を些細なものにし、宣言を素直にするために、イントロスペクションと宣言のインターフェースを簡素化
  • この簡素化によって、境界付きイントロスペクションインターフェースを削除

などです。

P2727R2 std::iterator_interface

イテレータを簡単に書くためのヘルパクラスの提案。

以前の記事を参照

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

  • iterator_categoryを定義する方法(または定義するかどうか)を変更
    • iterator_conceptforward_iterator_tag(の派生)である場合にのみ定義する
  • pointervoidである場合、またはreferenceが参照型ではない場合に->を定義しないようにした
  • <=>のサポート
  • input_iteratorの場合は、後置++の戻り値型をvoidにした

などです。

この提案は次のリビジョンがLEWGでの設計合意に至っており、提案する文言を揃えてからLWGに転送する予定です。

P2728R1 Unicode in the Library, Part 1: UTF Transcoding

P2728R2 Unicode in the Library, Part 1: UTF Transcoding

P2728R3 Unicode in the Library, Part 1: UTF Transcoding

標準ライブラリにユニコード文字列の相互変換サポートを追加する提案。

以前の記事を参照

R1での変更は

  • コードポイントを受けるインターフェースでは、char32_tを使用する
  • コードユニットを受けるインターフェースでは、charN_tを使用する
  • 変換をすぐ行うアルゴリズムを削除し、対応するviewを残しておく
  • 全てのoutput_iteratorの削除
  • utfN_viewのテンプレートパラメータを、viewの実装に使用されるトランスコーディングイテレータの型ではなく、form-rangeの型に変更
  • 全てのmake関数を削除
  • 誤って作成されたas_utfN()関数をas_utfNアダプタに置き換え
  • transcoding_error_handlerコンセプトを追加
  • unpack_iterator_and_sentinelをCPOにする
  • UTFイテレータコンセプトをinput_rangeに格下げ

R2での変更は

  • バッファからの変換例を再導入
  • null_sentinel_tをここ以外のところでも使用できるように一般化
  • 不正な形式のエンコーディングを検索するユーティリティ関数では、イテレータペアの代わりにrangeを受け取る
  • utf{8,16,32}_viewを単一のutf_viewに置き換え

R3での変更は

  • noexceptの付加
  • 必須ではない定数とユーティリティ関数を削除し、残ったものの使用法を詳しく説明する
  • P1629R1で提案されている似たものについて、その違いを追記
  • 例を拡張
  • viewのセマンティクスの説明の誤りを修正し、その使用例を追加

などです。

P2741R2 user-generated static_assert messages

static_assertの診断メッセージ(第二引数)に、コンパイル時に生成した文字列を指定できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、char8_tのサポートを削除したことなどです。

結局、static_assert()でサポートされる文字列の文字型はcharのみとなりました。ただし、この提案としてはchar8_tもサポートするべきという方向性を崩しておらず、導入をスムーズにするための措置であると思われます。

この提案は既に、2023年6月の全体会議においてC++26に向けて採択されています。

P2746R2 Deprecate and Replace Fenv Rounding Modes

浮動小数点環境の丸めモード指定関数std::fesetround()を非推奨化して置き換える提案。

以前の記事を参照

このリビジョンでの変更は、rint()系関数に対応するcr_rint<R>()(指定された丸めモードに従って浮動小数点数値を型Rの整数値に変換する)を追加したことです。

P2748R1 Disallow Binding a Returned Glvalue to a Temporary

glvalueが暗黙変換によって一時オブジェクトとして参照に束縛される場合をコンパイルエラーとする提案。

以前の記事を参照

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

  • 動機付けのための別の例を追加
  • 評価されない文脈についての議論を追加
  • 影響を受けるライブラリの規定について保護する文言を追加

などです。

P2752R2 Static storage for braced initializers

std::initializer_listの暗黙の配列がスタックではなく静的ストレージに配置されるようにする提案。

以前の記事を参照

このリビジョンでの変更は、定数評価中の未規定の振る舞いについて議論を追加したことです。

この提案は2023年6月の全体会議でC++26に向けて承認されています。

P2757R2 Type checking format args

std::format()のフォーマット文字列構文について、幅/精度の動的な指定時の型の検証をコンパイル時に行うようにする提案。

以前の記事を参照

このリビジョンでの変更は機能テストマクロを追加したことです。

この提案は2023年6月の全体会議でC++26に向けて承認されています。

P2767R0 flat_map/flat_set omnibus

flat_map/flat_setの仕様にあるいくつかの問題点とその解決策について報告する提案。

この提案は、libc++におけるflat_map/flat_setとそのファミリを実装する過程で明らかになった問題をまとめ、解決が可能なものはその解決策について報告するものです。

この提案で報告されている大きなものは次のような事項です

  1. 編集上の変更
    • 主に、アロケータを受け取るコンストラクタの調整
  2. 一部のデフォルト引数を持つexplicitコンストラクタの分離
  3. flat_set::insert_range()において、要素をムーブするようにする
  4. flat_set::insert_range()において、要素をムーブするようにする
  5. insert()emplce()を使用しないようにする
    • 挿入位置決めのために、まず最初に挿入予定の要素をスタック上に構築する必要があるが、引数で渡されているオブジェクトを使用することでこれを回避できる
    • 同じ理由から、falt_multisetにおいてヘテロジニアスなinsert()が有用となるため追加する(これは、他のmultiな連想コンテナと異なる性質)
    • emplace()の制約を削除
    • flat_set::insert()に制約を追加し、イテレータペアを渡した時にヘテロジニアスinsert()と曖昧にならないようにする
  6. sorted_uniqueをとるinsert()オーバーロードに、rangeをとるものを追加
    • insert(sorted_unique, args...)は、複数の要素がソート済で一意であることを前提に1操作で挿入するAPI
    • insert(first, last)に対してinsert(sorted_unique, first, last)insert(il)に対してinsert(sorted_unique, il)はあった
    • しかし、insert(range)に対してinsert(sorted_unique, range)が欠けていたため、これを追加する
  7. ソートが必要なコンストラクタの計算量の指定の修正
    • 一部のソート済みを仮定しないコンストラクタにおける計算量がO(N)と指定されている
    • これを達成するのは容易ではなく、そのような規定をranges::sort()と同等になるように修正
  8. replace()が右辺値参照ではなく値で受けるようにする
    • replace(key_container_type&&, mapped_container_type&&)はキーと対応する値の配列を受けて、内部の配列をそれによって置換するAPI
    • 引数としては、内部コンテナ型の右辺値参照を受けていた
    • replace()は常に右辺値を渡さなければならないが、似た他の場所のAPIではこのような用法ではなかった
    • 値で受け取るようにすることで、コピーして渡すことを容易にしつつムーブして渡す場合の使用感を維持する
  9. flat_set::keys()の追加
    • flat_mapには、そのキーと値の配列を参照するためのkeys(), values()が用意されているが、flat_setにはない
    • 利便性向上と一貫性のために、flat_setkeys()(だけ)を追加する

他にも、解決策が提案されていないIssueがいくつか報告されています。

P2769R1 get_element customization point object

tuple-likeなオブジェクトから特定のインデックスの要素を抜き出すCPOの提案。

以前の記事を参照

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

  • 構造化束縛の未使用変数の名付けの問題について、P2169R3の_を適用
  • tuple-likeコンセプトの要件緩和の可能性について追記
  • P2141R1とP2547R1の影響について追記
  • std::ranges::get名のAPI/ABI破壊を最小に抑えるアプローチを採用
  • 機能テストマクロを追加

などです。

P2771R1 Towards memory safety in C++

依存関係を追跡することによる時間的なメモリ安全性保証を言語に導入する提案。

以前の記事を参照

このリビジョンでの変更は、依存関係宣言をより明確にしたこと、コンパイル時と実行時のチェックをより明確に分離したこと、インターフェースについての議論を追加したことです。

この提案はSG23の議論でこれ以上レビューされないことが決定されています。

P2774R0 Scoped thread-local storage

ローカル変数に束縛されたスレッドローカルストレージを簡易に扱うためのクラス、std::tls<T>の提案。

C++17の並列アルゴリズムなどによってFork-Joinモデルのような並列化を行い、各スレッド毎に結果を出力する必要がある場合、その出力先の同期を取る必要があります。スレッド1つにつき1つ(単一のオブジェクト)の出力であればstd::atomic等を用いることで同期を効率化できますが、出力が多数(コンテナなど)の場合、std::mutex等による明示的なロックが必要となります。

そのような場合にスレッドローカルストレージ(thread_local)を使用すると見た目はシンプルになりますが、全てのスレッドに対して隠れたコストが発生するなどローカルな問題をグローバル化してしまう等の欠点があります。

そこで、次のようなローカルオブジェクトに束縛されたスレッドローカルな領域を使用するとスレッドローカルストレージの欠点を回避することができます。ただ、これは多数のライブラリ機能を複合させた複雑なものであり、使用も煩雑になりがちです。

// 入力データ
std::span<Triangle> input = …;
double max_area = …;

// スレッドローカルな領域を提供する
std::mutex m;
std::unordered_map<std::thread::id, std::vector<Triangle>> tmp;

// メインの並行処理
std::for_each(std::execution::par, input.begin(), input.end(),
  [&](const auto & tria) {
    // スレッド固有の領域を初期化し、取得
    // スレッドIDによって隔離されているため、取得して以降はロックなしで使用できる
    auto& ref{[&] -> std::vector<Triangle> & {
      const auto tid{this_thread::get_id()};

      const lock_guard lock{m};
      const auto it{tmp.find(tid)};

      if (it != tmp.end()) return it->second;

      return *tmp.emplace(tid, {}).first;
    }()};

    // 結果(複数)をスレッドローカルな領域へ出力
    for (const auto & t : split(tria, max_area)) {
      ref.emplace_back(t);
    }
  }
);

// 後処理、シングルスレッド
for(const auto & tria : tmp | std::views::join) {
  process(tria);
}

// 以降の処理のために、スレッドローカルな領域をクリア
tmp.clear();

この提案は、このような非thread_localなスレッドローカルストレージのためのラッパークラスを提供することで、このような用途(1スレッドが複数の出力を行う場合)におけるより効率的で使いやすいスレッドローカルストレージを提供しようとするものです。

提案されているstd::tlsはまさに上記のコード例におけるmtmpおよびその初期化部分をラップするようなクラスで、次のようなものです。

namespace std {
  template<typename T, typename Allocator = allocator<T>>
  class tls {
    mutex m;
    unordered_map<thread::id, T, hash<thread::id>, key_equal<thread::id>, Allocator> storage;
    // NOTE: 現在標準ライブラリにはアロケータサポートをもつ関数ラッパは存在しない
    unmovable_function<Allocator, T() const> init_func;
  public:
    // (1) constructors
    tls(Allocator alloc = Allocator{}) noexcept requires is_default_constructible_v<T>;
    tls(T value, Allocator alloc = Allocator{}) requires is_copy_constructible_v<T>;
    tls(auto func, Allocator alloc = Allocator{}) requires is_convertible_v<T, invoke_result_t<decltype(func)>>;

    // (2) not copy- nor moveable
    tls(const tls &) =delete;
    auto operator=(const tls &) -> tls & =delete;
    ~tls() noexcept;
    
    // (3) modifiers
    [[nodiscard]]
    auto local() -> tuple<T &, bool>; //thread-safe!
    void clear() noexcept;
    
    // (4) iteration support
    class iterator { … };
    static_assert(forward_iterator<iterator>);

    auto begin() -> iterator;
    auto end() -> iterator; 
  };
}

init_funcは最初に領域を取得しようとする場合にその領域を初期化するための関数であり、std::mutexは領域の取得時に同期をとるために必要となります。領域の取得はlocal()関数で行いますが、これはメンバで持っているstd::mutexにより保護されたスレッドセーフな関数となります。そして、local()によって取得される領域はスレッドIDによって管理されているため、一度取得してしまえば以降はロックなしで使用することができます。

ただし、この例は単純なものであり、並行ハッシュマップを使用するなどより効率的な実装が考えられます。

std::tlsを使用すると、先程のサンプルコードは次のように単純化されます

// 入力データ
std::span<Triangle> input = …;
double max_area = …;

// スレッドローカルな領域を提供する
std::tls<std::vector<Triangle>> tmp;

// メインの並行処理
std::for_each(std::execution::par, input.begin(), input.end(),
  [&](const auto & tria) {
    // スレッド固有の領域を初期化し、取得
    auto [ref, _] = tmp.local();

    // 結果(複数)をスレッドローカルな領域へ出力
    for (const auto & t : split(tria, max_area)) {
      ref.emplace_back(t);
    }
  }
);

// 後処理、シングルスレッド
for(const auto & tria : tmp | std::views::join) {
  process(tria);
}

// 以降の処理のために、スレッドローカルな領域をクリア
tmp.clear();

std::tlsはこのように、thread_localの利点(見た目の単純さ)と明示的ロックによる利点(thread_localに比べて低コスト)を両立し、なおかつロックの粒度を最小化しようとするクラス型です。

P2775R0 2023-05 Library Evolution Polls

2023年5月にLEWGで行われるLEWG全体投票の予定表。

次の提案が、C++26導入を目指してLWGに転送することを決定するために投票にかけられます。

P2781R1 std::constexpr_v

P2781R2 std::constexpr_v

コンパイル時定数オブジェクトを生成するクラスの提案。

この提案は、以前のP2725とそれをより一般化したP2772をうけてそれらを統合した提案で、整数専用のstd::integral_constantに対してより広いNTTP値のラッパとなるstd::constexpr_vを具体的に提案するものです。

std::constexpr_vは擬似的なconstexpr引数を実現するためのNTTPラッパクラスです。

namespace std {

  // constexpr_vの定義例
  template<auto X, class T/* = remove_cvref_t<decltype(X)>*/>
  struct constexpr_v {
    using value_type = T;
    using type = constexpr_v;

    constexpr operator value_type() const { return X; }
    static constexpr value_type value = X;

    ...
  };
}

提案文書より、使用例

template<typename T>
struct my_complex {
  T re, im;
};

inline constexpr short foo = 2;

template<typename T>
struct X {
  void f(auto c) {
    // cをconstexpr変数として使用したい
  }
};

template<typename T>
void g(X<T> x) {
  // constexpr_vは直接的にはこのように使用できる
  x.f(std::constexpr_v<1>{});
  x.f(std::constexpr_v<2uz>{});
  x.f(std::constexpr_v<3.0>{});
  x.f(std::constexpr_v<4.f>{});
  x.f(std::constexpr_v<foo>{});
  x.f(std::constexpr_v<my_complex(1.f, 1.f)>{});
}

とはいえこれだと長くて使いづらいため、より簡易に生成するユーティリティであるstd::c<value>が用意されます

namespace std {
  template<auto X>
  inline constexpr constexpr_v<X> c_{};
}

これを用いると、先ほどの例は次のようになります

template<typename T>
void g(X<T> x) {
  x.f(std::c_<1>);
  x.f(std::c_<2uz>);
  x.f(std::c_<3.0>);
  x.f(std::c_<4.f>);
  x.f(std::c_<foo>);
  x.f(std::c_<my_complex(1.f, 1.f)>);
}

さらに、std::constexpr_vにはXに応じて使用可能となる各種演算子が定義されます。ただし、値を変更するもの(複合代入演算子やインクリメント演算子)については直接的には無意味であるためまだ提案に含まれてはいません。式テンプレートなどユーザー定義型のオーバーロードを扱う際には必要となることが示されており、LEWGの決定待ちです。

また、std::constexpr_vがNTTPXだけではなくその型Tをわざわざテンプレートパラメータに取っているのは、std::constexpr_v変数を起点とするADLにおいてXの型T名前空間をその対象に含めるためです。

auto f = std::c_<strlit("foo")>; // strlitは別の名前空間に定義されており、<<を備えているとする
std::cout << f << "\n"; // strlitに定義された<<がADLによって発見される

この場合、std::constexpr_v<X, T>std::constexpr_v<X>だけだとXの型T(ここではstrlit)の属する名前空間がADLによる検索対象に含まれないため、NTTP値の型Tのために定義されている演算子オーバーロードを呼び出すことができなくなります。

このstrlitは文字列リテラルをNTTP化するラッパクラスです。文字列リテラルはNTTPで使用できないため、このようなものが必要となります(ただしこれは提案されていません)。

P2786R1 Trivial relocatability options

trivially relocatableをサポートするための提案。

以前の記事を参照

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

  • P1144との比較についての部分を別の提案(P2814R0)に分離したこと
  • リロケート操作関数にconstexprを付加
  • リロケート操作関数をフリースタンディング指定
  • move_and_destroyuninitialized_move_and_destroyに変更するとともに、規定を修正
  • uninitialized_move_and_destroyの設計について追記
  • 重複する範囲を扱うrelocateの完全な仕様を追加
  • swapへリロケーションを適用することに関する懸念を追記し、それに関する作業を別の提案に延期した

などです。

P2811R2 Contract Violation Handlers

P2811R3 Contract Violation Handlers

P2811R4 Contract Violation Handlers

契約プログラミングに、ユーザー提供の違反ハンドラを許可する提案。

以前の記事を参照

R2での変更は

  • contract_semanticの列挙値からignoreを削除(実際使用されていないため)
  • 観測された違反のカウントについての議論を追加
  • 安全な停止の使用例を追加
  • 序文に、この提案による修正案についての明確な説明を追加

R3での変更は

  • Designセクションを追加し、SG21からの疑問に回答
  • invoke_default_contract_violation_handlerの追加
  • contract_violationをポリモルフィックかつコピー不可能にし、その生存期間を明確化
  • 契約違反時にプログラムを終了させる場合契約チェック自体に違反するシグナルハンドラをガードするべき、と指摘
  • contract_kindcontract_violation_detection_modeプロパティの目的について明確化
  • contract_violation_detection_modedetection_modeに変更
  • detection_mode::predicate_exceptionevaluation_exceptionに変更
  • detection_mode::predicate_detected_undefined_behaviorevaluation_undefined_behaviorに変更
  • (これらによって)例外スローの意図が明確になった

R4での変更は

  • contract_violationwill_continue()を追加
  • 例外がどのように動作するかについて、セクション6で明示的に提案
  • 例外に関して提案する文言を追加
  • contract_violationの各操作に対するnoexcept[[noreturn]](オプション)の目的を明確化

などです。

このリビジョン時点では、<contract>ヘッダは次のようになっています

// <contract> ヘッダで定義
namespace std::contracts {

  enum class detection_mode : int {
    predicate_false = 1,
    evaluation_exception  = 2,
    evaluation_undefined_behavior = 3
    // 将来の標準によって追加されうる
    // また、実装定義の値を許可する。実装定義の値は1000以上
  };

  enum class contract_semantic : /int {
    enforce = 1
    // 将来の標準によって追加されうる、例えば以下のもの
    // observe = 2,
    // assume = 3,
    // ignore = 4
    // また、実装定義の値を許可する。実装定義の値は1000以上
  };

  enum class contract_kind : int {
    pre = 1,
    post = 2,
    assert = 3
  };

  class contract_violation {
  public:
    // 仮想関数かどうかは実装定義
    /*virtual*/ ~contract_violation();

    // コピー(及びムーブ)禁止
    contract_violation(const contract_violation&) = delete;
    contract_violation& operator=(const contract_violation&) = delete;

    // 破られた契約の条件式のテキスト表現 
    const char* comment() const noexcept;

    // 契約違反の起こり方
    detection_mode detection_mode() const noexcept;

    // 違反ハンドラが正常にリターンした時、その直後の評価を継続することが期待されているかを返す
    // 現在はfalseを返す(違反後継続モードはまだ組み込まれていない)
    bool will_continue() const noexcept;

    // 破られた契約の種別
    contract_kind kind() const noexcept;

    // 違反を起こした場所の情報
    source_location location() const noexcept;

    // ビルドモードに関する情報
    contract_semantic semantic() const noexcept;
  };

  // デフォルトの違反ハンドラ
  // 受け取ったcontract_violationオブジェクトのプロパティを出力する
  void invoke_default_contract_violation_handler(const contract_violation&);
}

// 置換可能、noxeceptや[[noreturn]]であってもいい
void handle_contract_violation(const std::contracts::contract_violation&);

契約条件チェックに伴って例外がスローされた場合の振る舞いについては次のようにすることを提案しています

  • 事前条件/事後条件の評価中に発生した例外は関数本体内で発生したものとして扱われるべき
  • 契約条件式の評価から脱出する例外は契約違反ハンドラを呼び出すべき
    • この例外を呼び出し元に伝播したい場合、それを行うカスタムハンドラを定義できる
  • 例外は、契約違反ハンドラの呼び出し中にスローされる可能性がある
    • このような例外はすべて、対応する契約条件式の評価中にスローされる例外と同じようにスタック巻き戻しを実行する

すなわちここでは、関数に対する契約条件は全て、その関数が呼び出す他の関数の評価に伴うものと同様に、現在のC++の例外伝播とnoexceptルールに従うようにすることを提案しています。これは、無条件noexcept指定を行う関数に対する基準であるLakos Ruleに従うものでもあります。

この提案の内容(正確にはR6の内容)はSG21でコンセンサスを得たようで、C++26に向けたContratcts MVPにマージされます。

P2814R0 Trivial Relocatability --- Comparing P1144 with P2786

オブジェクトの再配置(relocation)という操作に関する2つの提案を比較する文書。

relocationについての2つの提案については以前の記事を参照

2023年2月のIssaquah会議において、relocationに関する2つの提案(P1146R7とP2786R0)がEWGIにてレビューされました。結果、この2つの提案には重複する部分が多くあることから、EWG/EWGIがC++におけるrelocation操作についてのよりよい方針を決定するために、2つの提案の重複する部分をまとめ、また異なる部分を明確にすることで2つの提案を比較検討する必要性が示されました。

この文書はそれを受けて、P1146R7とP2786R0が提案するrelocation(特に、trivially relocatable)に関しての設計や構文等の差異を比較しまとめるものです。

2つの提案の主要な違いは次のような事項です

事項 P1144R7 P2786R0
relocationについて ムーブ+破棄に相当 ムーブ+破棄とは異なる
ムーブ代入の扱い 考慮する 考慮しない
正しい利用について ユーザーを信頼するアプローチ 間違っている可能性のある用法はエラー
提供するもの ユーザーが利用するための汎用アルゴリズム群を提供する コア言語の変更に焦点を当てている
オプトアウト方法 提供しない 提供する

2つの提案のいうrelocationという操作とその利点等は共通していますが、P2786R0がその中でもtrivially relocatableに特化したものであることによって、これらの差異が生まれています。

P2821R1 span.at()

std::span.at()メンバ関数を追加する提案。

以前の記事を参照

このリビジョンでの変更は、例外をスローするためこの関数がフリースタンディングではないことを追加、機能テストマクロを追加したことなどです。

P2828R1 Copy elision for direct-initialization with a conversion function (Core issue 2327)

型変換時のコピー省略のためのルールを明確化する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言を追加したことと例10を追加したことです。

追加された10個目の例では、コピーコンストラクタでもムーブコンストラクタでも無いものを省略している例です。

#include <type_traits>

template <bool has_copy_constructor>
struct Cat {
  Cat();

  // has_copy_constructorがfalseならdelete
  Cat(const Cat&) requires has_copy_constructor;
  Cat(Cat&&) requires has_copy_constructor;

  // ムーブコンストラクタとはみなされない
  template <class C = Cat>
  Cat(std::type_identity_t<C>&&) = delete;
};

struct Dog {
  operator Cat<false>();
};

Dog d;
Cat<false> c(d);  // clangとNVC++はok
                  // gccとmsvcはng ill-formed in GCC, MSVC, and the current standard

この例では、現在のC++及びGCC/MSVCはill-formedとなります。これは、Cat<false>に対して呼ばれるコンストラクタがテンプレート化されたものしかなく、これは省略可能では無いためです。しかし、clangとEDGはこの呼び出しを省略します。

このリビジョンでの提案は、この10個目の例でコピー省略が行われないように省略可能なコンストラクタを制限した上で、EDGのアプローチを採用するようにするものです。また、同時にクラス型のオブジェクトがそのクラスのオブジェクトを1つだけ要素にもつ初期化子リストからリスト初期化される(非常によく似た)ケースもカバーしようとしています。

struct Cat {
  Cat() = default;
private:
  Cat(const Cat&) = delete;
  Cat(Cat&&) = delete;
};

struct Dog { 
  operator Cat();
};


Dog d;

Cat c1(d);    // ok、Dog::operator Cat()が呼ばれる(コピー省略される)
Cat c2 = {d}; // ok、Dog::operator Cat()が呼ばれる(コピー省略される)
Cat c3{d};    // 同上

これらの例は現在でも変換コンストラクタが呼ばれコピー省略がなされていますが、それは明確な規定に基づくものではありませんでした。この提案ではそれを明確に規定するとともにこれと類似のケースで変換関数が呼ばれる条件をきちんと制限することでこの提案が紹介しているような実装間の振る舞いの差異が生じないようにしています。そして、その制限とはこの提案で説明されているEDGのアプローチをベースとして修正を加えたものです。

P2829R0 Proposal of Contracts Supporting Const-On-Definition Style

契約プログラミングにおける事後条件の条件式から参照される関数引数がconstでなければならないことをサポートする関数宣言・定義スタイルの提案。

契約プログラミングにおける事後条件特有の問題として、事後条件から関数引数が参照される場合に事後条件で評価する値がいつキャプチャされるのかが重要になるというものがあります。

// ユーザーが見る宣言
int generate(int lo, int hi)
  [[pre lo <= hi]]
  [[post r: lo <= r && r <= hi]];

// 開発者が見る定義
int generate(int lo, int hi) {
  int result = lo;
  while (++lo <= hi) // loが更新される
  {
    if (further())
      ++result;      // loよりもゆっくりとインクリメントされる
  }
  return result;
}

この例では、generate()の戻り値は呼び出し時点のloよりも大きくなりますが、関数終了時点のloよりも小さくなる場合があります。呼び出し側からみると、関数宣言はコピー渡しであるので変更されず、また事後条件は渡した時点でのloの値で評価されるように読み取れます。しかし、関数定義からみるとそうではなく、事後条件は関数の終了直後に評価されることから呼び出し側の期待と異なる結果を生じてしまいます。

この問題は、事後条件から参照される関数引数のうち、非参照(参照引数なら呼び出し側から見ても変更されうることがわかる)であり非constconst引数なら関数実行中に変更されない)な関数引数に問題があります。

SG21ではこの問題の対策として、事後条件から参照される関数引数は非参照ならばconstでなければならないことを決定しました。したがって、上記の例のようなコードはコンパイルエラーとなります。

int generate(int lo, int hi)              // error: loとhiはconstでなければならない
  PRE(lo <= hi)
  POST(r: lo <= r && r <= hi);
    
int generate(int& lo, const int& hi)      // ok: loとhiは参照
  PRE(lo <= hi)
  POST(r: lo <= r && r <= hi);
    
int generate(int lo, int hi)              // ok: loとhiは事後条件から参照されていない
  PRE(lo <= hi)
  POST(r: r >= 0);
    
int generate(const int lo, const int hi)  // ok: loとhiはconst
  PRE(lo <= hi)
  POST(r: lo <= r && r <= hi);

一方、C++は関数型を決定する場合、関数宣言の解析後に引数型のトップレベconstを削除してから関数型を形成します。これはすなわち、次のような関数宣言と定義は同じ関数に対する宣言と定義として有効であるということです

void f(int x);

void f(const int x) { /*...*/ }

別の言い方をすると、関数引数をconstにする場合それは定義でだけ行えば良いということで、関数引数のconstは関数のインターフェースの一部ではないということです。

この提案では、このような関数宣言・定義スタイルのことを「const-on-definition style」と呼んでいます。そして、この提案は契約プログラミングにおいて事後条件から関数引数を参照する場合に、このconst-on-definition styleを言語サポートしようとするものです。

具体的には、関数引数がその関数の事後条件内から参照されている場合

  • 定義ではない関数宣言では、コンパイラによってその引数は暗黙constとみなされる
  • 関数定義では、明示的にconstとしなければならない

というようにします。

// 関数宣言
void f(int x) // <-- xは暗黙的にconst
  POST(is_const_v<decltype(x)>); // true

// 関数定義
void f(const int x) // <-- xは明示的にconstでなければならない
{
  /*...*/
}

現状の契約仕様では、事後条件内から関数引数を参照している場合、その関数の全ての宣言におけるその変数に対して明示的にconstを付加する必要がありますが、このconst-on-definition styleのサポートによってそれは関数定義だけでよくなります。

P2831R0 Functions having a narrow contract should not be noexcept

標準ライブラリのnoexcept指定に関する設計について、現在のLakos Ruleを維持すべきとする提案。

Lakos Ruleとは、標準ライブラリの関数にnoexceptを指定する際のルールのことです。Lakos Ruleでは関数に関する契約(Contract)を定義し、その契約に基づいてnoexcept指定がされるかどうかを決めます。

Lakos Ruleにおける契約には2種類あり、事前条件を持たない関数は広い契約(Wide Contracts)がなされており、それ以外の関数(何かしらの事前条件を持つ)は狭い契約(Narrow Contracts)がなされているとします。そして、広い契約がなされている関数で例外をスローしない関数に関しては無条件でnoexceptを指定し、それ以外の関数(特に狭い契約がなされている関数)に関してはnoexceptを指定しない、とします。

現在の標準ライブラリ関数のnoexcept指定はこのLakos Ruleに基づいて行われており、これによって"Throws: Nothing"のように指定されているのにnoexcept指定はない関数が存在しています。

より積極的なnoexcept指定を目的として、このルールを見直そうという動きがLEWGにおいてあるようで、現在のLEWGのガイドラインP2148R0)では、狭い契約がなされている関数であってもLWGで例外を投げないという合意が取れれば無条件でnoexceptを指定することになっています。他にも、P1656R2ではLakos Ruleを標準ライブラリ設計原則から外すべきだと主張されています。

この提案はそのような動きに反対し、Lakos Ruleが現在でも必要かつ有用であり標準ライブラリ設計原則として維持されるべき理由を解説するものです。

主にライブラリ関数のテスト(特にネガティブテスト)における有用性が主張されている他、noexceptを指定することがコンパイラの最適化にとって有利であることを示した報告はなく(むしろ低下させることを示した報告はある)、パフォーマンス向上を目的としてライブラリ関数に片っ端からnoexceptをつけて回ることは間違っているとも述べられています。

P2834R0 Semantic Stability Across Contract-Checking Build Modes

契約プログラミングにおいて、契約述語の存在がビルドモードによって異なる影響を他の言語機能に及ぼさないようにする提案。

契約条件がその評価に伴って例外を投げる場合、その契約がなされている関数がnoexcept指定されているとすると、その関数に対するnoexcept演算子はどのように振る舞えばいいのかが問題になります。あるいは、契約条件が満たされない場合に例外を投げるような場合(ビルドモードもしくは例外ハンドラによる)にも同様の問題が発生します。

まだこの問題の結論は出てはいませんが、1つの方針として、契約(事前条件)の評価は関数の呼び出し前に行われるため、契約条件が例外を投げるかどうかはその関数の例外仕様の一部では無い、とするものがあります。その場合、関数のnoexcept指定は契約の有無や内容によらず常に有効であり、その関数に対するnoexcept演算子trueを返すことになります。

void my_func(int i) [[pre: i >= 0]];
void your_func(int i) noexcept [[pre: i >= 0]];

int x; // Value is not used.
static_assert( false == noexcept(my_func(x)) );   // 常に成り立つ
static_assert( true == noexcept(your_func(x)) );  // 常に成り立つ?

しかし、契約条件(事前条件)の評価に伴う例外(契約条件式からのものであれ、契約が破られた時のものであれ)は全て、関数が呼び出される前にスローされます。例外が発生するのが関数の呼び出し前なのか後なのかを判断する仕組みはなく(そしておそらくそのような仕組みは意味がなく)、上記のyour_func()は契約条件を評価するビルドモードでは常に例外をスローする可能性があります。そのため、事前条件のチェックが関数の例外仕様の外側にある場合に、契約条件を評価するビルドモードではnoexcept演算子は契約がなされている関数に対してfalseを返す以外の選択肢がありません。

int x; // Value is not used.
static_assert( true == noexcept(your_func(x)) );  // 契約条件をチェックしないビルドモードでは成り立つ
static_assert( false == noexcept(your_func(x)) ); // 契約条件をチェックするビルドモードでは成り立つ

すなわち、noexcept演算子の振る舞いはビルドモードによって変化してしまいます。

このようなビルドモードによる例外仕様の意図しない変化は、noexcept演算子によって関数の例外仕様をチェックしそれによって処理を分岐させている(これはnoexceptの正しい用法です)コードに対して、静かにバグを埋め込んでしまう可能性があります(例は提案を参照)。

結局、契約条件を評価するビルドモードにおける関数の例外仕様の問題を回避するためだけに、事前条件を関数呼び出し前(または事後条件を呼び出し後)に評価するという戦略は、有効性が疑わしく実行可能ではありません。実行可能な唯一の選択肢は、関数の宣言から観測可能な例外仕様がnoexcept演算子やその他のコンパイル時クエリの動作を、全ての契約チェックビルドモードで同じになるように制御することです。

この提案ではまず、次のような原則を提示しています

  1. ビルドモードの独立性
    • 契約がなされている関数は、noexcept演算子をはじめとするコンパイル時のクエリについて、コンパイル時のセマンティクスが契約チェックのビルドモードによって変化することはない
    • 契約がなされている関数においてその契約条件がコンパイルされwell-formedだったならば、noexcept演算子は全てのビルドモードで(すなわち契約が評価されるかどうかに関わらず)同じ動作をする
  2. Lakos Rule
    • noexcept指定された関数の例外仕様と狭い契約は、本質的に互換性がなく、矛盾している
    • つまり、何かしらの契約がなされている関数は狭い契約を持つ(引数等に関して事前条件を持つ)ため、noexcept指定されるべきではない
  3. 無視される契約条件のオーバーヘッドをゼロにする
    • 契約条件が無視された(ビルドモードによって)場合、その契約がなされている関数等付近のコードは、あたかもその契約条件がコメントアウトされたかのように振る舞う
    • ただし、ビルドモードに関わらず、契約条件から参照されているものはODR-useされる

その上で、Lakos Ruleを言語機能として組み込み強制させること(つまり、noexcept指定されている関数に対する契約の指定をコンパイルエラーとすること)は回避します。テストのためなど、noexcept指定と契約チェックを両立したいユースケースは想定され、また、嘘のnoexcept指定(実際は例外を投げうるが開発者が追加の情報からそれを考慮しなくて良いと判断している場合など)にも有効なユースケースがあります(例外を投げうるムーブコンストラクタを持つ型をラップして、ムーブコンストラクタをnoexceptにするなど)。そのように、関数の持つプロパティの一部をコンパイラが強制することはC++プログラマに利益をもたらしません。

これらのことをベースに、この提案では契約プログラミング導入後のnoexceptに関して次のことを提案しています

  • 関数の引数を初期化した後、未処理の例外をスローするnoexcept関数のそれ以降のステップは、 [except.spec]/5に従ってstd::terminate()を呼び出す

すなわち、noexcept指定されている関数に契約を付与することができ、noexcept関数ではその契約は評価及び破られた時にも例外を投げないとみなされます。もしその仮定が裏切られ、その契約が評価中に例外を投げるか、契約が満たされなかった時に例外を投げた場合、現在のnoexcept関数から例外を投げた時と同様にstd::terminate()を呼び出してプログラムを終了させます。

この提案は、契約条件が例外を投げるかどうかはその関数の例外仕様の一部では無いとする方針の特別なケースであり、この方針によって示された利点(契約が評価されるか否かを翻訳単位の外で決定できるなど)を享受しつつ、ビルドモードによるコンパイル時プロパティの変化という欠点を回避することができます。

P2835R0 Expose std::atomic_ref's object address

std::atomic_refが参照しているオブジェクトのアドレスを取得できるようにする提案。

一部のハードウェアには、同じコア上で実行され同じプログラムステップを実行している、同じプログラムの異なるスレッドを検出するための命令が備わっています。

そのようなハードウェアではその命令を使用して、複数のスレッドで実行されるアトミック操作を1つのスレッドでだけ実行される単一の操作に集約することができます。そのようなパターンを用いると、複数スレッド間での同期のコストを削減し、パフォーマンスを向上させられる可能性があります。

単純なコードで記述すると、次のようなコードパターンになります

// この関数は複数のスレッドで同時実行される
void unsynchronized_aggregated_faa(atomic<int>& acc, int upd) {
  // `acc`と`upd`の同じ値を使用して実行している空間的に近いスレッドを特定する
  auto thread_mask = __discover_threads_with_same(acc, upd);
  auto thread_count = popcount(thread_mask);
  
  // それらスレッドグループのリーダーを選出し、更新操作を集約する
  // スレッドごとに1つではなくこのスレッドでだけ、アトミックRMW操作を実行する
  if(__pick_one(thread_mask))
     acc.fetch_add(thread_count * upd, memory_order_relaxed);
}

そのようなハードウェアにはたとえばNVIDIAGPUが該当し、同じWarpに所属しているスレッドが空間的に近いスレッドとなります。NVIDIAGPU(CUDA)では、そのような命令として__match_any_sync()(と__activemask())が提供されています。

そのような組み込みの命令(上記例の__discover_threads_with_same())では、スレッドグループが共有している変数のポインタを受け取って、同じポインタを渡してきたスレッドを同じスレッドグループだと判定するものがあります。複数のスレッドで共有する変数なのでstd::atomicを使用するのは自然で、そのような命令にはstd::atomic変数のアドレスを渡すことになります。

この時に、std::atomic_refを用いているとそのような命令を使用することができなくなります。なぜなら、std::atomic_refはそもそも参照セマンティクスを持つ型なので関数には値渡しをするはずで、そうすると、各スレッドが持っているstd::atomic_refオブジェクトはローカルのものになり、そのアドレスはおそらく一致の保証がありません。

// atomic_refを使用する場合の宣言
void unsynchronized_aggregated_faa(atomic_ref<int> acc, int upd) {
  ...
}

int main() {
  int n = 0;
  std::atomic_ref<int> ar{n};

  unsynchronized_aggregated_faa(ar, 0); // 例えばこのように呼ばれる
}

std::atomic_refを参照渡しすれば解決できるかもしれませんが、それは無意味な二重参照であり、ともすれば間接参照のコストがかかってきます。あるいは、std::atomicの参照/ポインタを用いても解決できますが、提案によるとそれが必ずしもできない場合があるとのことです。

std::atomic_refを使用している時でも、複数のスレッドで同じ1つのオブジェクトをアトミックに共有しているということは変わっておらず、この場合に欲しいのはstd::atomic_refが参照しているオブジェクトのアドレスです。しかし、現在のstd::atomic_refはそれを完全に隠蔽しており、取得する方法がありません。

この提案は、このような目的のためにstd::atomic_ref.data()を追加して、その参照先のアドレスを取得できるようにしようとするものです。

namespace std {
  template<class T>
  struct atomic_ref {
    ...
    
    // 追加するdate()関数
    T const* data() const noexcept;
    
    ...
  };
}

P2837R0 Planning to Revisit the Lakos Rule

Lakos Ruleの見直しを、契約プログラミング機能が固まるまで延期する事を推奨する提案。

Lakos Ruleは標準ライブラリの関数にnoexceptを付加する際の基本的なルールです。C++11で導入されて以来10年以上経過しており、最近のライブラリ設計者はこのルールを改定することを頻繁に提案しているようです。

一方、現在契約プログラミング機能のC++26への導入に向けて活発な作業が続いています。もしそれが標準入りした場合、標準ライブラリ実装に対して契約を適用する事を許可するかどうかという事が議論され、それを許可する場合は現在文書で指定されている契約条件がどのように契約コードにエンコードされるべきかのガイドライン(ルール)を策定する必要があります。

契約プログラミングの事前・事後条件が必要になる関数というのは、Lakos Ruleでいうところの狭い契約を持つ関数であり、そのような契約プログラミングに関するガイドラインにはLakos Ruleが密接にかかわってくることは明らかです。

Lakos Ruleは標準ライブラリの上に構築されるプログラムが外的要因などによって標準ライブラリ機能の使用を制限されることが無いように、意図的に保守的なルールになっています。そのため、Lakos Ruleを順守するライブラリの上にLakos Ruleに従わないプログラムを書くことができる一方で、Lakos Ruleを順守しないライブラリの上にLakos Ruleに従うプログラムを書くことはできません。

契約プログラミングの機能がまだあまり固まっていないこともあり、契約プログラミングを標準ライブラリに適用する際のルールや原則がどのようになるかはまだ明らかではありません。しかし、そこにはLakos Ruleが関わってくることは明らかです。

そのため、この提案は、C++標準ライブラリの基礎的な設計指針としてのLakos Ruleを少なくとも契約プログラミングの準備が整うまでは現状を維持する(改訂を延期する)ことを提案するものです。

P2839R0 Nontrivial relocation via a new "owning reference" type

リロケーション(relocation)の言語サポートのための、新しい参照型の提案。

リロケーションについての2つの提案については以前の記事を参照

リロケーション、特にトリビアルなリロケーション操作は、ムーブを効率化(オブジェクト全体のmemcpy)しムーブ後オブジェクトの問題を解決することができます。

この提案は、トリビアルなリロケーション操作の背景を解説することを目的とし、上の2つの提案のようなライブラリサポートではなくムーブとよく似た機構による言語サポートを提案するものです。

この提案では、型Tに対するowning referenceT~をまず導入します。これは、リロケーションされようとしているリロケーション元のオブジェクトを指す参照であり、そのような状態のオブジェクトの値カテゴリはrlvalueとなります。

owning reference型の値はengageddisengagedのどちらかの状態にあり、engaged状態のowning referenceはオブジェクトを所有しています。engaged状態のowning referenceがその生存期間を終えると、所有している(参照している)オブジェクトは破棄されます。なお、プログラムの特定の地点でowning referenceengagedであるか否かは後述するルールに従って静的に決定されます。

この提案ではリロケーション(rlvalueあるいはT~へのキャスト)はreloc演算子によって行います。左辺値のオブジェクトははreloc演算子によってrlvalueに変換され、その後元のオブジェクトを参照しようとする式は全てコンパイルエラーとなります。なお、T~型の変数名自体は左辺値です(右辺値参照型の変数が左辺値なのと同様)。

struct T {
  int m;
};

void g(T& x);

void f(T~ ref) {  // `ref`はengaged状態、何かオブジェクトを所有している
  g(ref);  // OK; `ref`は左辺値

  T~ ref2 = reloc ref;
   // `ref`はdisengaged状態
   // `ref2`はengaged状態、以前に`ref`が所有していたオブジェクトを所有している

  g(ref);   // ill formed; `ref`はdisengaged状態
  ++ref.m;  // 同様にエラー
  g(ref2);  // OK

  if (rand() % 2) {
    {
      T~ ref3 = reloc ref2;
      // `ref3`はengaged状態、`ref2`はdisengaged状態
      // `ref3`の生存期間が終了し、`ref3.~T()`が呼ばれる
    }
    g(ref2);  // error
  } else {
    g(ref2);  // OK
    // `ref2`は暗黙的にはdisengaged状態へ移行、`ref2.~T()`が呼ばれる
  }

  g(ref2);  // error
}

このように、制御フローが分岐する場合にその分岐の一端でowning referencedisengaged状態になった場合、その制御フローが合流する地点(disengagedになってない分岐パスの終了地点)で同じowning referencedisengaged状態に移行します。

このような関数と同様に、T~を引数にとるコンストラクタを定義することができます。それはリロケーションコンストラクタ(relocation constructor)と呼ばれ、上記のT~及びrlvalueの性質からムーブコンストラクタよりも強く所有権を引き取るコンストラクタです。コンストラクタから値を返すことは(例外を除けば)できないので、リロケーションコンストラクタに渡したowning reference(及びその参照元オブジェクト)の寿命は、そのコンストラクタが終了する時に終了することになります。

ある特定の型では、リロケーションコンストラクタが暗黙定義されます。暗黙定義されたリロケーションコンストラクタは既存の特殊メンバ関数と同様のルールに従いますが、常に無条件noexceptである点だけが異なります。あるいは、明治的にdefault定義しておくこともでき、その場合も暗黙定義された時と同じ性質を持ちます。そのようなデフォルトリロケーションコンストラクタはクラス型によって次のように動作します

  • トリビアルにリロケーション可能な型では、memcpyによってオブジェクト表現をコピーしたかのようにオブジェクトを初期化する、トリビアルリロケーションコンストラクタが定義される
    • トリビアルリロケーションコンストラクタでは、ソースオブジェクトの生存期間を終了させるもののそのデストラクタを呼び出さない
  • そうではなく、自身のxvalueから直接初期化が可能で有効なデストラクタを持つような型(Cとする)の場合
    • CxvalueからCを直接初期化するために選択されたコンストラクタとCのデストラクタの両方がdefault宣言されているならば
      • Cの基底クラス及びメンバ変数の再起的なリロケーションを行う
    • それ以外の場合、Cのムーブコンストラクタに委譲する
      • C(C~ source) : C(static_cast<C&&>(source)) {}
        • ソースオブジェクトはこのコンストラクタの完了後に破棄される
  • それ以外の場合、リロケーションコンストラクタはdeleteされる

Trlvalueは例えば次のような変換が可能です

  • rlvalueT~)が.もしくは.*の左辺のオペランドである時、rvalueT&&)に暗黙変換される
  • prvalueT)はrlvalueT~)に暗黙変換できる
    • オーバーロード解決において、この変換は右辺値参照またはconst左辺値参照(T&&/const T&)への束縛よりも良い変換とみなされる(優先順位が上になる)
  • glvalueT&/T&&)はstatic_castによって明治的にrlvalueT~)へ変換できる
    • この場合でも、owning reference型は参照先オブジェクトの所有権を引き取り、その生存期間の終わりにengaged状態だったらそのオブジェクトを破棄する

オブジェクトの一部(サブオブジェクト)だけをリロケーションするのは危険なため、継承関係にある型(基底クラスBと派生クラスD)の間でB~ -> D~のような変換は禁止されています。

この提案ではまた、reloc演算子を使用して自動変数をリロケーションできるようにするために、このowning referenceの観点から自動変数のモデルと定義し直します。owning referenceの導入後、自動変数xに対して暗黙的に所有参照__x~が定義されます。__x~xを所有しているため、reloc演算子によって他の関数やコンストラクタ、owning referenceに所有権が移されない場合、そのスコープの終了時に__x~xを破棄することになります。その後、xを指名するid式はill-formedとなります。

struct T {
  int m;
};

int main() {
  T x = {0};
  T y;
  T~ r = reloc x;  // `__x~`はdisengaged状態になり、`r`が`x`の所有権を引き取る
  ++x.m;  // ill formed `__x~`はdisengaged状態
  ++r.m;  // OK `rは左辺値

  // `r`のスコープ終端、`x`を破棄
  // `__y~`のスコープ終端、`y`を破棄
  // `y`のスコープ終端、`~T()`は呼ばれない
  // `__x~`のスコープ終端、disengaged状態のため何もしない
  // `x`のスコープ終端、`~T()`は呼ばれない
}

reloc演算子の振る舞いは、このようなモデルをベースとして定義されます。reloc演算子はid式(変数名)に対して適用できて、結果としてその変数名に結び付けられているオブジェクトのowning referenceを取得し、値カテゴリはrlvalueの式となります。

reloc演算子は次のように動作します

  • オペランドが、直接囲んでいる関数定義に関連するブロックスコープまたは関数パラメータスコープに属するT~型の自動変数xである場合
    • reloc xの結果はxが参照しているオブジェクトを指すrlvalueであり、xはそれによってdisengagedとなる
  • オペランドが、直接囲んでいる関数定義に関連するブロックスコープに属するオブジェクト型の自動変数xである場合
    • reloc xの結果はreloc __x~

ABIによっては、関数引数のオブジェクト型の破棄責任が呼び出し先(関数内)ではなく呼び出し側にあるものがあり、それを考慮するとT~型ではない関数引数をrelocすることはできないため、2番目の動作ではそれを除いています。ただし、コピー/ムーブコンストラクタを持たずリロケーションコンストラクタだけを持つようなリロケーション専用の型ではこれを認めることも提案しています。

提案では、これらのこと以外にも既存のムーブや右辺値/転送参照周りの仕様を参考にしながら、owning referenceとリロケーションサポートのための言語機能について解説されています。提案は大きく4つのパートに分かれており、それぞれのパートはそれ以前のパートに依存するようになっているため、この提案の内容は全てを一気に導入するのではなく一部を少しづつ導入していくことができます。

P2841R0 Concept Template Parameters

コンセプトを受け取るためのテンプレートテンプレートパラメータ構文の提案。

現在テンプレートテンプレートパラメータで渡すことができるのは型のみで、変数テンプレートやコンセプトを渡すことはできません。

この提案は、より高レベルの構成を可能とするために、テンプレートテンプレートパラメータ構文を拡張してコンセプト(と変数テンプレート)を渡せるようにしようとするものです。

これによって例えば、コンセプトアダプタのようなものが可能になったり

// 参照型はregularではない
// 実質、右辺値を渡した時にしか制約が満たされない
template<std::regular T>
void f1(T&&);

// decayしてからコンセプトに渡すようにする
template<typename T>
  requires std::regular<std::decay_t<T>>
void f2(T&&);

// TをdecayしてCに通す、コンセプトアダプタ
template<typename T, template <typename concept C>>
concept decay_to = C<std::decay_t<T>>;

template<std::decay_to<std::regular> T>
void f3(T&&);

int main() {
  int n;

  f1(n);  // ng
  f2(n);  // ok
  f3(n);  // ok
}

あるいは複数のコンセプトを用いる制約を1つにまとめることができたり

// range型Rの要素はCである
template<typename R, template <typename> concept C>
concept range_of = std::ranges::range<R> && C<std::ranges::range_value_t<R>>;

// 整数範囲を受けとりたい
auto f(range_of<std::integral> auro&& r);
// 浮動小数点数範囲を受けとりたい
auto f(range_of<std::floating_point> auro&& r);


// Tが全てのCを満たす
template<typename T, template<typename>... concept Cs>
concept all_of = (Cs<T> && ...);

// regularかつintへ変換可能
auto g(all_of<std::regular, std::convertible_to<int>> auto v);

// viewかつforward_range
auto g(all_of<std::ranges::view, std::ranges::forward_range> auto v);

現在の標準ライブラリにも見られるコンセプト内の制約の重複を共通化して括り出せたり

// 現在イテレータヘッダにもある間接的に呼び出し可能系のコンセプトの例
// 共通する制約が多く含まれている
namespace now {
  template<class F, class I>
  concept IndirectUnaryInvocable =
    Readable<I> &&
    CopyConstructible<F> &&
    Invocable<F&, iter_value_t<I>&> &&
    Invocable<F&, iter_reference_t<I>> &&
    Invocable<F&, iter_common_reference_t<I>> &&
    CommonReference<
      invoke_result_t<F&, iter_value_t<I>&>,
      invoke_result_t<F&, iter_reference_t<I>>>;

  template<class F, class I>
  concept IndirectRegularUnaryInvocable =
    Readable<I> &&
    CopyConstructible<F> &&
    RegularInvocable<F&, iter_value_t<I>&> &&
    RegularInvocable<F&, iter_reference_t<I>> &&
    RegularInvocable<F&, iter_common_reference_t<I>> &&
    CommonReference<
      invoke_result_t<F&, iter_value_t<I>&>,
      invoke_result_t<F&, iter_reference_t<I>>>;

  template<class F, class I>
  concept IndirectUnaryPredicate =
    Readable<I> &&
    CopyConstructible<F> &&
    Predicate<F&, iter_value_t<I>&> &&
    Predicate<F&, iter_reference_t<I>> &&
    Predicate<F&, iter_common_reference_t<I>>;

  template<class F, class I1, class I2 = I1>
  concept IndirectRelation =
    Readable<I1> && Readable<I2> &&
    CopyConstructible<F> &&
    Relation<F&, iter_value_t<I1>&, iter_value_t<I2>&> &&
    Relation<F&, iter_value_t<I1>&, iter_reference_t<I2>> &&
    Relation<F&, iter_reference_t<I1>, iter_value_t<I2>&> &&
    Relation<F&, iter_reference_t<I1>, iter_reference_t<I2>> &&
    Relation<F&, iter_common_reference_t<I1>,
      iter_common_reference_t<I2>>;

  template<class F, class I1, class I2 = I1>
  concept IndirectStrictWeakOrder =
    Readable<I1> && Readable<I2> &&
    CopyConstructible<F> &&
    StrictWeakOrder<F&, iter_value_t<I1>&, iter_value_t<I2>&> &&
    StrictWeakOrder<F&, iter_value_t<I1>&, iter_reference_t<I2>> &&
    StrictWeakOrder<F&, iter_reference_t<I1>, iter_value_t<I2>&> &&
    StrictWeakOrder<F&, iter_reference_t<I1>,
      iter_reference_t<I2>> &&
    StrictWeakOrder<F&, iter_common_reference_t<I1>,
      iter_common_reference_t<I2>>;
}

// コンセプトテンプレートパラメータによって、共通部分を括り出す
namespace future {

  template <template <typename...> concept Direct,
      typename F, typename... Is>
  concept Indirect = 
    (Readable<Is> && ...) &&
    CopyConstructible<F> &&
    Direct<F&, iter_value_t<Is>&...> &&
    Direct<F&, iter_reference_t<Is>...> &&
    Direct<F&, iter_common_reference_t<Is>...> &&
    CommonReference<
      invoke_result_t<F&, iter_value_t<I>&...>,
      invoke_result_t<F&, iter_reference_t<Is>...>>;
  
  template<class F, class I>
  concept IndirectUnaryInvocable =
    Indirect<Invocable, F, I>;

  template<class F, class I>
  concept IndirectRegularUnaryInvocable =
    Indirect<RegularInvocable, F, I>;

  template<class F, class I>
  concept IndirectUnaryPredicate =
    Indirect<Predicate, F, I>;

  template<class F, class I1, class I2 = I1>
  concept IndirectRelation =
    Indirect<Relation, F, I1, I2>;

  template<class F, class I1, class I2 = I1>
  concept IndirectStrictWeakOrder =
    Indirect<StrictWeakOrder, F, I1, I2>;
}

などの利点があります。

この例を見ればわかるように、コンセプトテンプレートパラメータは通常のテンプレートテンプレートの構文(template<template<typename> typename T>)をベースに、最後のtypename(もしくはclass)のところをconcept(コンセプト)もしくはauto(変数テンプレート)で置き換えることで記述します。

template<
  typename T, // テンプレートパラメータの宣言
  auto V,     // NTTPの宣言
  template<typename> typename TT, // テンプレートテンプレートパラメータの宣言
  template<typename> auto VT,     // 非型テンプレートテンプレート パラメータの宣言
  template<typename> concept C,   // コンセプトテンプレートパラメータの宣言
>
void f();

この提案の内容はclangのフォークにて実装されており、Compiler Explorerで試すことができます。実装にあたっては特に困ったことは起こらなかったようです。

P2842R0 Destructor Semantics Do Not Affect Constructible Traits

コンストラクタに関する型特性がデストラクタのセマンティクスに影響を受けないようにする提案。

例えば、is_nothrow_copy_constructible型特性はnoexcept演算子を用いて次のようにコンパイラマジックなしで実装でき(そうに思え)ます。

template<typename T>
struct is_nothrow_copy_constructible<T> {
  static constexpr bool value = noexcept(T{declval<T const &>()});
};

この時問題になるのはnoexcept演算子の内部にはTのデストラクタの実行(一時オブジェクトの破棄)も含まれてしまっていることで、Tのコピーコンストラクタがnoexceptな時でも、デストラクタがそうではない場合にこれはfalseになってしまいます。

C++11規格完成前にはこの問題は把握されており、標準の文言はこの問題を回避するために巧妙な言葉遣いをしています。is_nothrow_copy_constructibleの場合はまずis_nothrow_constructible<T, const T&>に委譲したうえで、is_nothrow_constructibleではその結果がどうなるかは次のように指定されています([meta.unary.prop]/4

is_­constructible_­v<T, Args...> is true and the variable definition for is_­constructible, as defined below, is known not to throw any exceptions ([expr.unary.noexcept]).

ここで重要なのは「variable definition (for is_­constructible)」という言葉であり、これはこのチェックのために変数を定義した場合にその定義に当たって例外を投げないということが言いたいらしく、その際例外を投げないとはどういうことかについてコア言語のnoexcept演算子に投げています。

この変数定義については直接書かれていませんが、この文言を導入したN3142によると次のようなものです

// こういう関数があったとして
template <class T>
typename add_rvalue_reference<T>::type create();

// これがチェックのための変数定義
T t(create<Args>()...);

この変数定義において、変数tは一時オブジェクトではなく左辺値であり、この変数定義そのものにはデストラクタの実行は含まれていません。従って、この変数定義が有効であり例外を投げない場合にis_nothrow_constructibletrueになるという事です。

このような文言の意図は同様の問題(判定時にデストラクタの実行が混じってしまう)がある他の型特性についても同じ意図で導入されており、この文言が暗に示しているのはコンパイラマジックによってこれらの型特性を実装する(ただし、個別の式に分解した後でそれぞれの式の例外判定にnoexcept演算子を用いることを許可する)べき、ということです。

このような巧妙な言いまわしはしかし、標準ライブラリ実装者には伝わらなかったようで、C++11および現在に至るまで標準ライブラリ実装はこの文章(上記変数定義)のことを直接チェックするべき式だと思って実装しているようです。つまり結局、冒頭のサンプルコードのような実装になってしまっているようです。

#include <type_traits>

struct Test {
  Test() = default;
  Test(Test const&) = default;
  ~Test() noexcept(false) {} // non-trivial, potentially throwing
};

static_assert(std::is_trivially_copy_constructible<Test>::value, "non-trivial");
static_assert(std::is_nothrow_copy_constructible<Test>::value, "may throw");

このコードは、現在の主要な標準ライブラリ実装において失敗します(godbolt)。

この提案は、この問題の解決のために、標準の規定のオリジナルの意図と実際の実装のどちらを重視するのかを決定し、それによって標準文書とライブラリ実装のどちらかを修正することを迫るものです。

この提案としては、問題があるのは実装の方だとして、現在標準ライブラリに報告されている関連Issueを欠陥ではない(NAD)として全てクローズし、既存標準ライブラリ実装に対してバグレポートを提出することを推奨しています。また、そのうえで上記のようなチェックすべき変数定義について明確化する事を提案しています。

一方で、既存のライブラリ実装を重視する場合についても考慮されており、いくつかオプションがあるもののその特性の判定にデストラクタの実行が関与することを明確にするように推奨しています。

P2843R0 Preprocessing is never undefined

プリプロセッサに存在する未定義動作を取り除く提案。

未定義動作というのはWell-formedなプログラムの実行時の振る舞いに関する指定であって、コンパイル時に起こるものではありません。現在のプリプロセッサ仕様 には未定義動作がいくつか含まれていますがそれは正しい指定ではなく、それをill-formed, no diagnostic requiredに変更しようとするものです。

この提案の対象は次のものです

  • #ifディレクティブの条件式内のマクロを置換したときにdefinedが現れた場合
  • #ifディレクティブの条件式のdefinedの使用がおかしい場合
  • #includeディレクティブのヘッダ名部分のマクロ置換の結果が"header-name"<header-name>のどちらでもない場合
  • 関数マクロ呼び出し時の引数内にプリプロセッシングディレクティブが存在する場合
  • #による文字列化の結果が有効な文字列リテラルにならない場合
  • ##の結果がユニバーサル文字名を形成する場合
  • ##の結果が有効なプリプロセッシングトークンとならない場合
  • 現在の行数を変更する#lineディレクティブの行数指定に、0もしくは2147483647以上の数が指定されている場合
  • #lineに続くトークンをマクロ置換した結果が、#lineディレクティブとして有効ではない場合
  • 事前定義マクロ名もしくはdefinedが、#define#undefの対象となる場合

これらのケースは現在未定義動作とされています。この提案でもこれらの場合の挙動が変わるわけではありませんが、それはIFNDR(不適格だが診断不用)と指定されるようになります。

P2845R0 Formatting of std::filesystem::path

std::filesystem::pathstd::format()でフォーマット可能にする提案。

std::filesystem::pathに対するstd::formatter特殊化は以前に提案(P1636)されていました。その出力は、std::quotedをラップしたostream出力演算子の観点からのフォーマットとして提案され、つまりはpathオブジェクトを<<で出力した時と同じフォーマットによって文字列化するものでした。

std::cout << std::format("{}", std::filesystem::path("/usr/bin"));
//"/usr/bin"

ただこれには多くの問題がありました。

まず、std::quoted"\のみをエスケープします。そのため、パスに改行等の他の制御文字が含まれていると出力はパス文字列として使用できないものになります。

std::cout << std::format("{}", std::filesystem::path("multi\nline"));
//"multi
//line"

この出力はC++やシェル言語等における有効な文字列ではなく、この出力はパス文字列として使用可能ではありません。

もう一つの問題はエンコーディングで、path::native()はシステムの文字コードとしてbasic_string<value_type>を返し、value_typeは実行環境のOSによって決定されます。それは通常、POSIX環境ではcharWindowsではwchar_tになります。

<<による出力ではpath::string<CharT, Traits>()からパス文字列を取得するため、CharTによってはその際に内部で保持しているパス文字列の文字コードからの変換が行われる場合があります。std::coutの場合はPOSIX環境では変換が起きませんがWindows環境では変換が行われ、それによって文字化けが発生します。

例えば次のパスをベラルーシ語で出力しようとすると

std::print("{}\n", std::filesystem::path(L"Шчучыншчына"));

全てのコードページと地域設定がベラルーシ語に設定され、ソースエンコーディングリテラルエンコーディングUTF-8である場合でも、Windowsでは次のような出力が得られます

"�����������"

std::printpathも両方ともユニコードをサポートしているにもかかわらず、path::string()内部の中間の変換でchar(CP1251)を経由することによって文字間の対応関係が切られ、文字化けが発生します。

これらと同種の問題は、C++23のstd::print(P2093)と<ranges>のフォーマット(P2286)で議論され解決されています。

そこでこの提案は、それらの経験を踏まえたエスケープとWindows上でのユニコード変換を行うstd::formatterpath特殊化を追加することで、以前の問題を解決しpathをフォーマット可能にすることを提案しています。

実装としては、path::native()から変換なしで文字列を取得して、それを文字列範囲としてRangeのフォーマットに移譲します。その際、エスケープに関してはC++23で追加されたフォーマット指定子?と同様の処理を行い、これは入力文字列と等価な文字列を生成可能なC++文字列リテラルを出力とするようにエスケープ処理がなされるものです。

path::native()から変換なしで文字列を取得しているので、std::printによる出力は他の文字列と同様にその内部で適切なユニコード変換によって(入力がユニコードならユニコードtoユニコードの直接変換によって)出力されます。

これによって、先ほどの問題があった例の出力は次のように修正されます

std::cout << std::format("{}", std::filesystem::path("multi\nline"));
//"multi\nline"

std::print("{}\n", std::filesystem::path(L"Шчучыншчына"));
//"Шчучыншчына"

P2846R0 size_hint: Eagerly reserving memory for not-quite-sized lazy ranges

※この部分は@Reputelessさんに執筆していただきました

遅延評価のため要素数が確定しない range の ranges::to を行う際に、推定の要素数をヒントとして知らせる ranges::size_hint CPO を追加する提案。

文字列内の小文字を大文字に変換する次のような uppercase_view を仮定します。"ß" を大文字にすると "SS" と 2 文字になることに注意します。

U"In C++ ist es schwieriger, sich selbst in den fuß zu schießen."sv
| views::uppercase
| ranges::to<std::u32string>();
IN C++ IST ES SCHWIERIGER, SICH SELBST IN DEN FUSS ZU SCHIESSEN

ここで uppercase_viewsized_range でも random_access_range でもありません。そのため、ranges::to が結果を構築する際に事前に要素数がわかりません。結果、push_back のような操作でアロケーションが繰り返し起こり、実行時性能にネガティブな影響を及ぼす可能性があります。

しかし、例えば長さ L の文字列を大文字に変換するときは、結果の長さが少なくとも L であることは推定できるはずです。こうしたサイズのヒントを知らせることができれば、同様のケースで、ranges::to や、range から構築するコンストラクタにおけるアロケーションの回数を抑制できます。

この提案では、そうした機能の実現のために、ranges::size_hint CPO と、approximately_sized_range コンセプトを導入し、既存の各種 views に size_hint への対応を実装します。

ranges::size_hint CPO は、sized_range に対しては ranges::size を呼び、それ以外には size_hint メンバ関数、それが無ければ ADL 経由で見つかった size_hint を呼びます。また、approximately_sized_range コンセプトは、range に対して ranges::size_hint を使って償却定数時間で推定要素数を得られることを示します。

この提案を踏まえると、先ほどの uppercase_view は次のように実装されるでしょう。ここでは、変換結果が基底の range と同じ長さであると仮定しています。"ß" が含まれるなど特殊なケースでは実際にはもう少し長くなる可能性もあります。この view は要素数を確定できないため size メンバ関数は持ちません。

template <input_range V>
class uppercase_view {
  constexpr const V & base() const;
  constexpr auto begin() const;
  constexpr auto end() const;
  constexpr auto size_hint() requires approximately_sized_range<View> {
    return ranges::size_hint(base());
  }
  constexpr auto size_hint() const requires approximately_sized_range<const View> {
    return ranges::size_hint(base());
  }
};

P2848R0 std::is_uniqued

※この部分は@Reputelessさんに執筆していただきました

範囲内に重複する隣接要素がないかを調べる std::is_uniqued, std::ranges::is_uniqued<algorithm> に追加する提案。

現在の標準ライブラリのアルゴリズムを整理すると、次のような対応表を作ることができます。

イテレータを返す bool を返す 等価な実装
is_sorted_until is_sorted is_sorted_until == end()
is_heap_until is_heap is_heap_until == end()
mismatch equal mismatch == {end(), end()}
find_if none_of find_if == end()
find contains find != end()
search contains_subrange search != end()
adjacent_find (該当なし) adjacent_find == end()


範囲を操作する関数 範囲を調べる関数
sort is_sorted
make_heap is_heap
unique (該当なし)

この提案では、上記の表の空白を埋める is_uniqued を標準ライブラリに追加します。

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
  std::vector<int> v1 = { 1, 1, 2, 2, 3, 3 };
  std::vector<int> v2 = { 1, 2, 3, 1, 2, 3 };

  std::cout << std::boolalpha;
  std::cout << std::is_uniqued(v1.begin(), v1.end()) << '\n'; // false
  std::cout << std::is_uniqued(v2.begin(), v2.end()) << '\n'; // true
}

この提案の著者は、すでに libc++ への is_uniqued の実験的な実装 を行っています。

P2850R0 Minimal Compiler Preserved Dependencies

並行処理における処理の進行順序認識のために、コーナーケースにおける順序導出を例示する文書。

並行処理プログラムにおいて何もない場所から値を読み出ししてしまう問題をThin-air(read)問題といいます。Thin-airは現在のC++ではメモリモデルと注釈によって強く禁止されていますが、それでも完全に禁止できない場合があり、あるいはそれを許可したい場合があるようです。

そのようなThin-air問題の解決策として検討されているのが、semantic dependency(sdep)と呼ばれるもので、これはプログラム中のデータや制御フロー、アドレスの依存関係に暗黙的に含まれる順序のことで、それをコンパイラが認識できるようにしようとするものです。sdepは、コンパイラが最適化した後にも残らなければならない依存関係でもあり、最適化を禁止する側面と許可する側面を持っています。

この文書は、Thin-airが起こりうるような並行プログラムのコーナーケースのような場合にsemantic dependencyがどのように構築されるかを示すことによって、semantic dependencyがどのような最適化(Thin-air)を許可し、あるいは最適化(Thin-air)を禁止するのかを示すものです。

この文書の目的は、sdepが無ければコンパイラが最適化を行えないケースとコンパイラが自由に最適化できなければsdepを求められないケースを定義することで、semantic dependencyを標準に導入するための技術報告書のようなものにつなげようとするものです。

P2852R0 Contract violation handling semantics for the contracts MVP

契約プログラミングの違反ハンドラとそれに伴うセマンティクスに関する提案。

現在C++26に向けて、SG21では契約プログラミングの最小限の設計を確立するための作業が進められています。

最近そこで議論されているのは、契約条件のチェックに伴って起こりうることについて、そのセマンティクス(意味論)をどのように指定するか?ということです。

特に、P2811R1では、ユーザー定義の違反ハンドラを許可することで契約違反が起きた場合の振る舞いをカスタマイズできるようにすることを提案しており、それによって契約違反時のセマンティクスをコア言語とビルドモードによる指定からユーザーによるカスタマイズによる指定に、設計を変化させようとしています。

この提案はP2811の方向性を支持し、ユーザー定義の違反ハンドラの振る舞いを確かなものにするために、違反ハンドラが呼ばれる場合と違反ハンドラが呼ばれた後の振る舞いに関するセマンティクスを規定しようとするものです。

この提案は、契約違反と違反ハンドラの振る舞いについて、次のようなことを提案しています

  1. P2811R1で提案されている違反ハンドラを必要なら修正を加えて採用する
  2. 実装が提供するデフォルトの違反ハンドラは、実装定義のアクション(エラーメッセージ表示など)の後でstd::abort()を呼び出す実装とする
  3. ユーザー定義の違反ハンドラはデフォルトの違反ハンドラを呼び出すことができる
    • これによって、ユーザー定義違反ハンドラで何かした後で、エラーメッセージ表示などデフォルトの動作をデフォルトのハンドラに委任できる
  4. 契約条件のチェックおよび違反ハンドラの呼び出しのセマンティクスは次のように指定される
動作 指定されるセマンティクス
契約条件はチェックされなかった well-defined; 実行継続
契約条件がチェックされ、trueを返した well-defined; 実行継続
契約条件がチェックされ、falseを返した well-defined; 違反ハンドラが呼び出され、それが正常にリターンした場合は実行継続
契約条件がチェックされ、未定義動作に遭遇した 未定義動作
契約条件がチェックされ、例外が送出された 未規定の動作
契約条件がチェックされ、std::longjmp()が呼ばれた well-defined; 呼ばれたstd::longjmp()に従って実行継続
違反ハンドラが異常終了した well-defined; プログラムは呼び出されたハンドラの指定に従って終了する
違反ハンドラが正常にリターンした well-defined; 実行継続
違反ハンドラが例外を送出した 未規定の動作
違反ハンドラがstd::longjmp()を呼んだ well-defined; 呼ばれたstd::longjmp()に従って実行継続

この提案による設計では、C++標準は契約のビルドモード(契約条件を評価するかしないか)を認識する必要が無くなります。実装は、違反ハンドラがリターンしないことや例外を投げない等の仮定を置いた最適化を実行することができます。

この提案はこれをC++26最終仕様とすることを意図しておらず、コンセンサスが得られている事項については動作と意味論を定義し、そうでない事項については未規定や未定義として別の提案によって詰めていくことを意図しています。

ただし、SG21では別の提案によって検討されている方向性を議論しておくことになったらしく、この提案の追求はストップされています。

P2853R0 Proposal of std::contract_violation

契約プログラミングにおける、ユーザー定義可能な違反ハンドラのAPIの提案。

C++26の契約プログラミング導入に向けて、SG21ではP2811の方向性を採用し違反ハンドラをカスタマイズ可能とすることを決定したようです。

この提案は、P2811で提案されいてるAPIをベースに修正を加えるものです。

この提案の修正は次のような点です

  • ヘッダは<contract_violation>
  • std::contract_violation(契約違反が起きた条件に関する情報を保持するクラス)はstd::exceptionの派生型
  • std::contract_violationsemiregularな型
    • デフォルト構築可能
    • コピー/ムーブ構築・代入可能
  • std::contract_violationはABI安定(インラインpimplイディオムによる)
    • 将来の拡張時にABIを気にせずに済むようにする
  • std::contract_violationはヒープを使用せず、大きな固定サイズバッファーを持つ
    • そのバッファ先頭にプライベートメンバが配置され、残りはメッセージ(.what()で取得するもの)の保持に使用する
  • デフォルトの違反ハンドラは、contract_violation::what()のメッセージをstderrに出力し、std::contract_resolution::abort_program列挙値を返す
// <contract_violation> ヘッダで定義(以前は<contract> ヘッダ
namespace std/*::contracts*/ {

  // 削除
  // enum class contract_violation_detection_mode : /*unspecified*/ {
  //   unknown,
  //   predicate_false,
  //   predicate_exception,
  //   predicate_undefined_behavior
  // };

  // enum class contract_semantic : /*unspecified*/ {
  //   ignore,
  //   enforce
  // };

  // 追加
  enum class contract_resolution { 
    abort_program
  };

  enum class contract_kind : /*unspecified*/ {
    empty, // 追加
    pre,
    post,
    assert
  };

  class contract_violation : public std::exception {
  public:
    // 追加
    contract_violation() noexcept;
    contract_violation(const contract_violation &) noexcept;
    contract_violation(contract_violation &&) noexcept;
    ~contract_violation();
    contract_violation &operator=(const contract_violation &) noexcept;
    contract_violation &operator=(contract_violation &&) noexcept;

    // 追加
    const char *what() const noexcept override;

    // 破られた契約の条件式のテキスト表現 
    //const char* comment() const noexcept;
    const char* source_code() const noexcept;

    // 契約違反の起こり方
    //contract_violation_detection_mode detection_mode() const noexcept;

    // 破られた契約の種別
    contract_kind kind() const noexcept;

    // 違反を起こした場所の情報
    //source_location location() const noexcept;
    const source_location& source_location() const noexcept;

    // ビルドモードに関する情報
    //contract_semantic semantic() const noexcept;
  private:
    // 説明専用メンバ
    static constexpr size_t size = 512;
    alignas(std::max_align_t) mutable char storage[size];
  };
}

そして、違反ハンドラはcontract_resolution列挙値を返すように変更されます

// 違反ハンドラの宣言
//void handle_contract_violation(const std::contracts::contract_violation&);
std::contract_resolution handle_contract_violation(const std::contract_violation &);

戻り値型が変更されているのは将来的な違反後継続モードなどをサポートすることを目したもので、将来的な後方互換のためです。現在はabort_program列挙値しかないためプログラム中断のみがサポートされており、将来的に別のモードをサポートする場合は列挙値を追加したうえで、ユーザーが自身の違反ハンドラの戻り値を変更することで行います。これによって、後から別のモードを追加したときにもその時点で使用されている違反ハンドラの振る舞いに影響を与えないようにしています。

このAPIを使って、Eval_and_abortモード(つまりデフォルトの違反ハンドラ)は次のように実装でき

std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
  std::cerr << v.what() << std::endl;
  return std::contract_resolution::abort_program;
}

Eval_and_throwモード(P2698R0)は次のように実装できます

std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
  throw v;
}

std::contract_violationstd::exceptionの派生クラスになっていることから、このような単純な実装によってEval_and_throwモードを実装可能です。

その他の例。

// 例外再送出の検出
std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
  if (std::exception_ptr e = std::current_exception())
    std::rethrow_exception(e);
  else
    /*...*/;
}

// Eval_and_spinモードとカスタムエラーメッセージ
std::contract_resolution handle_contract_violation(const std::contract_violation& v) {
  std::contract_kind kind = v.kind();
  const char* code = v.source_code();
  std::source_location location = v.source_location();
  
  // カスタムエラーメッセージ作成
  ErrorMessage msg = FormatErrorMessage(kind, code, location);
  
  // エラーメッセージを表示して、スレッドを停止
  DisplayErrorMessageAndWait(msg);
  
  return std::contract_resolution::abort_program;
}

contract_violation_detection_modeが削除されたのは1つ目の例のようにstd::current_exceptionで検出することができるため(かつunknownundefined_behaviorの使い分けが不明瞭だったため)です。

P2855R0 Member customization points for Senders and Receivers

P2300で使用されるtag_invokeにアダプトするために、非メンバ関数ではなくメンバ関数と専用タグ型を使用するようにする提案。

P2300ではCPOの実装のためにtag_invokeと呼ばれるユーティリティを使用しています。tag_invoketag_invokeという名前の関数にCPO毎のタグ型(CPOそのものが使用される)と追加の引数を渡して、ADLによって非メンバ関数(Hidden friendがよく使用されている)のユーザー定義tag_invokeを探して呼び出します。

従来のCPOではそこにアダプトするために同名の関数(非メンバ/メンバ)を定義する必要があり、その呼び出しはコンセプトでチェックされるものの完全に区別されるわけではなかったため、実質的に名前を占有していました。これによって、ユーザーは標準CPOで使用されている名前の使用を控えざるを得なくなったり、CPOの呼び出しに伴う探索範囲が広く候補関数が増大しやすかったりと言った問題がありました。

tag_invokeを使用すると、あるCPOにアダプトするための関数名は全てtag_invokeという名前の関数になり、tag_invokeはADLオンリーかつタグによって関数を識別するようになるため、それらの課題が解決されます。

// 自作スケジューラ実装
struct my_scheduler {

  // schedule CPOにアダプトする例
  friend std::execution::sender auto tag_invoke(std::execution::schedule_t, auto&& self) {
    // schedulerにアクセスするためのsenderを返す
    ...
  }

};

とはいえ、tag_invokeという名前の関数がどのCPOにアダプトしているのかが視認しづらいことや、CPOにアダプトするための関数定義が複雑になりがち、CPOの型名を露出しなければならないなどの問題があります。

そのため、P2300も含めた将来的なカスタマイゼーションポイントを備えたライブラリを見据えて、C++20のCPOやtag_invokeが持つ問題を解決した関数カスタマイゼーションのための仕組みを言語機能で備えようとする動きがあります。

この提案は、(非メンバ関数ではなく)メンバ関数とCPO個別のタグ型を用いることによって、そのような言語機能を必要とせずにtag_invokeの持つ問題を改善できる、とするものです。

この提案の利点は次の2点です

  • ADLを用いない
  • カスタマイゼーションポイントの定義がかなりシンプルになる

この提案前後のstd::execution::schduleCPOの実装は簡単には次のようになります

struct schedule_t {
  
  auto operator()(auto&& s) const {
    // 現在
    return tag_invoke(auto(*this), s);

    // この提案
    return s.schedule(auto(*this));
  }
};

新しい定義によるstd::execution::schduleCPOにアダプトするためには、次のようにメンバ関数schduleを実装します

// 自作スケジューラ実装
struct my_scheduler {

  // schedule CPOにアダプトする例
  std::execution::sender auto schedule(std::execution::schedule_t) {
    // schedulerにアクセスするためのsenderを返す
    ...
  }

};

メンバ関数ではなくメンバ関数を使用するようにすることで、探索にADLを使用しなくなるため名前が占有される空間をクラススコープに限定することができ、その上でタグ型のチェックを行うことでCPOにアダプトしている関数を区別します。これによって、CPOを定義する側とそれを利用する側のコードが単純化されます。

その他の例

struct my_op_state {
  // start CPOへのアダプト宣言
  friend void tag_invoke(std::execution::start_t, recv_op& self) noexcept;

  // この提案
  void start(std::execution::start_t) noexcept;
};

struct my_sender {

  // connect CPOへのアダプト宣言
  template <typename _Self, receiver _Receiver>
    requires sender_to<__copy_cvref_t<_Self, _Sender>, __receiver<_Receiver>>
  friend auto tag_invoke(std::execution::connect_t, _Self&& __self, _Receiver __rcvr);

  // この提案
  template <typename _Self, receiver _Receiver>
    requires sender_to<__copy_cvref_t<_Self, _Sender>, __receiver<_Receiver>>
  auto connect(this _Self&& __self, std::execution::connect_t, _Receiver __rcvr);
}

このconnect関数を呼び出すには次のようにします

struct S {

  template<typename Sender, typename Receiver>
  auto operator()(Sender&& s, Receiver r) const {
    // 現在
    return tag_invoke(std::execution::connect, std::forward<Snd>(s), r);

    // この提案
    return std::forward<Snd>(s).connect(std::execution::connect, r);
  }

};

ただし、クエリを行うCPOに関してはこのようにせず、tag_invokeの代わりにtag_queryという統一的な名前使用するtag_invokeのアプローチ(ただし、非メンバではなくメンバ関数のみを探索)を使用することを提案しています。

struct S {
  // get_stop_token CPOへのアダプト宣言
  friend in_place_stop_token tag_invoke(std::execution::get_stop_token_t, const S& __self) noexcept;

  // この提案
  in_place_stop_token tag_query(std::execution::get_stop_token_t) const noexcept;
};

クエリの場合は、クエリ呼び出しそのものを転送する場合があるため、このようになっているとのことです(よくわからなかった)。

P2857R0 P2596R0 Critique

P2596(std::hiveの容量モデルを修正する提案)への反対を表明する提案。

P2596はstd::hiveの容量モデルが複雑で意図しない振る舞いをするとして、それを単純化することを提案するものです。P2596に関しては以前の記事を参照

この文書はP2596を添削する形でその間違いを指摘し、主張のまとめを行うものです。

提案による、現状維持する(容量モデルを変更しない)ことを指示するポイント

  1. ライブラリ/実装はキャッシュラインサイズを知らないため、ユーザーは参照局所性向上のために要素ブロックをキャッシュラインサイズの倍数にしたい場合がある
  2. ライブラリ/実装はユーザーがどのようにデータを消去/挿入をするかのパターンを知らず、std::hiveのイテレートの効率とメモリ消費はブロック容量に影響される
    • 定量で挿入/削除を行うユーザーはそのサイズに一致するブロックサイズを設定するだろう
  3. 要素の削除があるため、最大ブロックサイズが大きいことは良いことでばない
    • ブロックはその全ての要素が削除されるまでは有効であり続けるため、要素の削除が行われブロックの空室率が高まると要素間のギャップが大きくなる
    • これは統計的にみて、ブロックサイズが大きいほど無駄なメモリが増えることになる
    • 要素間のギャップは参照局所性を低下させ、メモリの浪費は組み込みや性能が求められる環境で問題となる可能性がある
  4. キャッシュ制限のため、最大ブロックサイズが大きいことは良いことでばない
    • ブロックサイズがキャッシュラインサイズより大きくなっても参照局所性が向上することはなく、挿入/削除時のメモリ確保/解放が少なくなるだけ
  5. 過剰なメモリ確保を防ぐために最大ブロックサイズが必要
  6. アロケータによっては、その内部の特定のチャンクで割り当てを行うものがあり、std::hiveの容量モデルをはそれを支援するもの
  7. 必ずしも全てのユーザーのニーズを予測できるわけではないため、合理的な範囲で柔軟な使い方を目指すべき
  8. std::hiveは特定条件下でSIMD処理で使用できる
  9. ブロックの制限はユーザーが指定するかどうかに関係なく実装に存在する
    • 最小値側では、最初のブロックのメタデータのサイズよりも大きな妥当な最小値を持つことが理にかなっている
    • 最大値側では、ジャンプカウントのスキップフィールドのビット深度によって決定される。例えば16ビットのスキップフィールドではブロック内で最大65535要素のジャンプが可能なので、ブロックの最大サイズ制限は65536になる
      • スキップフィールドのビット深度が大きいほどメモリを浪費し性能向上につながるとは限らない。そのため、ユーザーのブロックサイズ制限をサポートするためのコードはほとんど付随的なもの
  10. この容量モデルとそのAPIは、実際のユーザーから好評だった機能であり、個人的な美学を理由に削除を選択する人がいるのは奇妙なこと

一方で、変更を指示するポイントは次のような点です

  1. 他のコンテナに同様のものがない
    • dequevectorにはcapacity()があるが、listにはない。mapsetにはキーがあるがdequevectorlistにはない。
    • std::hiveには容量制限があり、他のコンテナにはない
  2. 制限があるとコンストラクタが増加する
    • 追加されたコンストラクタは全て委譲によって実装できるため、コードが肥大化することはない
  3. 実装負荷の増加
    • 前述(現状維持ポイント9)したように、これによって実装負荷は増加しない。実際の作業は全て、コンテナの仕様と3つの中核的な設計面に費やされる。
    • 2つの主要ベンダーがリファレンス実装をフォークすると表明しており、もう1つのベンダーも参考にする可能性があるため、最小限の追加負荷は既に完了している

この提案の著者はstd::hive実装者かつ提案者の方です。

P2858R0 Noexcept vs contract violations

事前/事後条件をもつnoexcept関数における、契約違反時の例外送出に関する設計上の問題点を指摘する文書。

現在の契約プログラミング議論では、契約違反時の振る舞いの一つとしてEval_or_throwモードが提案されたことでnoexcept関数の事前/事後条件の評価に伴って例外が送出されうる場合のnoexceptプロパティの扱いが問題となっています。

// noexceptだが、事前条件違反が起こる
void fun() noexcept [[pre: false]];

constexpr bool mystery = noexcept(fun());  // この値は何になる?

using nothrow_callback_t = void(*)() noexcept;
nothrow_callback_t p = fun;                // コンパイルが通る?

void overload(void(*)());                  // #1
void overload(void(*)() noexcept);         // #2

overload(&fun);                            // どちらのオーバーロードが呼ばれる?

この問題解決のために、すでにいくつかの実装論や意味論に関する提案が提出されています。この提案はそれらの議論を踏まえつつ、noexceptの考え方などを説明し、それらの提案にある問題点について報告するものです。

  • 契約チェックから例外を投げられるようにするには、noexceptに関する全ての静的なプロパティについて明確な意味論を定義する必要がある
    • そのために、noexceptとは何かを明確に説明する必要がある
    • オーバーロードを制御するための表明と、事前条件を充足することによる失敗しない保証を混同することは有益でない場合がある
  • あるいは、違反ハンドラからの例外送出をひとまず禁止しておき、noexcept関連の議論に時間をかける
  • 契約違反時に停止することを回避するアプリケーションにおいて、契約違反を検知してから例外によってそれを報告することは間違っている
    • 例外それ自体が早期終了の原因となる
    • noexcept関数からの例外送出は無条件終了となり、例外は契約と関係の無い場所からでも投げられるため

直接的には主張されて位はいませんが、雰囲気的には契約違反時に例外を投げること(Eval_or_throwモード)に反対しているようです。

P2861R0 The Lakos Rule: Narrow Contracts And noexcept Are Inherently Incompatible

標準ライブラリの関数にnoexceptを付加する基準であるLakos Ruleを維持すべき理由を解説する文書。

この文書は非常に長いですが、次のような構成になっています

  1. 契約について議論する前に用語を定義する
    • ライブラリUBと言語UBを区別する
    • 例外を投げないという例外指定を持つ関数は、狭い契約(Narrow Contracts)を持つことができない
  2. ある特定の狭い契約をを持つライブラリ関数を考え、それを複数のバージョンにわたって拡張していくことの価値をC++の側面から検討する
    • 最初のバージョンでnoexceptを追加していたらどう(ひどいことに)なっていたかを調べる
  3. 狭い契約と契約チェック(言語機能)がどのように相互作用するかを見る
    • 契約違反ハンドラが例外をスローすることの必要性を、いくつか正当化する
    • 特に、完全な回復ではないにせよ一時的な継続、およびネガティブテストの手段として、契約違反時に例外をスローすることを検討する
  4. noexcept指定の自由な使用が例外を使用して稀なエラーや予期しないエラーを伝達するソフトウェア設計に及ぼす悪影響について検討する
    • noexcept演算子を広く利用することでコードサイズを削減することができるが、実行時のパフォーマンスが大幅に改善されることやそれが測定可能であることを示す理論や経験則は存在しない
  5. Lakos Ruleの再検討
    • Lakos Ruleの例外とはどのようなものなのかについて、仮説を立ててそれが4つの基準を満たすかを調べる
    • 最後に、Lakos Ruleの唯一の例外を紹介する
  6. 標準ライブラリの仕様、その具体的な実装、サードパーティライブラリ、エンドユーザーライブラリに対して、noexcept指定を有効に活用するための推奨事項とその正当性を示す

最終的にこの文書の主張するところは、「技術的にやむを得ない正当な理由がない限り、Lakos Ruleから外れることは常に非常に悪いアイデアである。特に、標準ライブラリの仕様内でそうすることは絶対に避けるべき」というものです。

なお、この文書の筆者の方はLakos Ruleを提唱した方です。

P2862R0 text_encoding::name() should never return null values

※この部分は@Reputelessさんに執筆していただきました

std::text_encoding の提案(P1885R12)に含まれる text_encoding::name() の仕様を変更する提案。

具体的には、text_encoding::name() が名前を返すことができない場合、ヌルポインタではなく空の文字列を返すように仕様を変更することを提案しています。

P1885R12 の設計では、ICU や iconv のような広く導入されているライブラリと互換性を持たせることを目指しています。それらのライブラリでは、エンコード名をヌルポインタとして扱うことをサポートしていない場合があります。次のコードはその一例で、セグメンテーション違反を引き起こします。

iconv_open(nullptr, "utf-8"); // NG

同様に、次のようなシンプルな C++ コードでも、text_encoding::name() がヌルポインタを返す場合、容易に未定義動作となります。

std::cout << te.name();             // Violates [ostream.inserters.character] p3
std::format("Name: {}", te.name()); // Violates [format.arg] p5
""sv == te.name();                  // Violates [string.view.cons] p2 since traits::length doesn't accept null values

text_encoding::name() がヌルポインタを返すべき強い理由が見あたらなかったため、戻り値の型が const char* である source_location::file_name() が常に null 終端文字列を返す、という既存事例にならい、text_encoding::name() についても空の文字列を返すようにすることを提案しています。

P2863R0 Review Annex D for C++26

現在非推奨とマークされている機能について、C++26で削除/復帰を検討する提案。

C++23までの間に非推奨とされたコア言語/ライブラリの機能は、必ずしも削除されずに残されており、規格書のAnnex Dセクションにまとめられています。

現在そのような機能は29個(コア言語9, ライブラリ20)あり、この提案はそれらの機能を取り巻く環境や非推奨化の背景を検討したうえで、非推奨のままにしておくのか、非推奨を取り消すのか、削除するのか、を決定しようとするものです。

ただし、この提案はそれらのまとめとインデックスのような文書で、個々の機能それぞれについては個別の提案で詳しく検討されます。C++23では1つの提案にまとめて同じことを行おうとしていましたが、複数のフィードバックが寄せられた結果処理がパンクし提案の改訂が間に合わず、結局C++23設計サイクル中にほとんど議論できなかったためのようです。

現在非推奨となっているコア言語機能の一覧

機能 導入時期 非推奨時期
Arithmetic conversion on enumerations C++98 C++20
Implicit capture of *this by reference C++11 C++20
Array comparisons C++98 C++20
Deprecated use of volatile C++98 C++20
Redeclare static constexpr members C++11 C++17
Non-local use of TU-local entities C++98 C++20
Implicit special members C++98 C++11
Some literal operator declarations C++11 C++23
template keyword before qualified names C++98 C++23

現在非推奨となっているライブラリ機能の一覧

機能 導入時期 非推奨時期
Requires: clauses C++98 C++20
has_denorm members in numeric_limits C++98 C++23
Deprecated C macros C++98 C++23
relops C++98 C++20
char * streams C++98 C++98
Deprecated error numbers C++11 C++23
The default allocator C++17 C++23
polymorphic_allocator::destroy C++17 C++23
Deprecated type traits C++11 C++20
volatile tuple API C++11 C++20
volatile variant API C++17 C++20
std::iterator C++98 C++17
move_iterator::operator-> C++11 C++20
C API to use shared_ptr atomically C++11 C++20
basic_string::reserve() C++98 C++20
<codecvt> C++11 C++17
wstring_convert et al. C++11 C++17
Deprecated locale category facets C++11 C++20
filesystem::u8path C++17 C++20
atomic operations C++11 C++20

P2864R0 Remove Deprecated Arithmetic Conversion on Enumerations From C++26

C++20の一貫比較仕様に伴って非推奨とされた、列挙値から算術型への暗黙変換を削除する提案。

C++20の宇宙船演算子ではenum値と浮動小数点数型や異なる列挙型間の比較を禁止していますが、従来の比較演算子ではそれは暗黙変換によって可能となっており、異なる列挙型の間では算術演算すら可能です。それらの挙動はバグであると思われるため、C++20で非推奨とされました。

ただ、列挙型から浮動小数点数型への暗黙変換は、比較以外の場所では元々禁止されていました。

この提案は、C++26にてそれらの非推奨化されている暗黙変換を削除しようとするものです。この提案が削除しようとしているのは次の2つのものです

  • 列挙型から浮動小数点数型への暗黙変換
  • 異なる列挙型の値から同じ整数型への暗黙変換
int main() {
  enum E1 { e };
  enum E2 { f };

  bool b = e <= 3.7;  // C++20で非推奨、削除を提案
  int k = f - e;      // C++20で非推奨、削除を提案
}

この仕様はC++98にて導入されたもののようで、削除する事は破壊的変更となります。しかし、このような変換はそのメリットよりも意図せず起こしてしまう場合のデメリットの方が大きいため、削除することを提案しています。また、C++26で削除するとするとC++20で非推奨とされてから6年経過しており、その間に主要な実装はこの変換に警告を出すようになっています。

なお、列挙型と整数型の間の演算(列挙型から整数型への暗黙変換)は非推奨とされていないためC++20以降も影響を受けておらず、異なる列挙型間の演算や比較については単項+演算子を使用して片方を整数昇格させることで回避することができたりします。

int main() {
  enum E1 { e };
  enum E2 { f };

  int k =  f - e; // C++20で非推奨
  int x = +f - e; // OK
}

P2865R0 Remove Deprecated Array Comparisons from C++26

C++20の一貫比較仕様に伴って非推奨とされた、配列間の比較を削除する提案。

従来の比較演算子では配列型と配列型の比較が可能で、それは配列の先頭ポインタの比較をおこなっていました。C++20の宇宙船演算子はそれを禁止しており、それに倣って従来の比較演算子では非推奨とされました。

すなわち、配列と配列の等価比較を行っていてもそれは要素ごとの比較ではなく配列の先頭アドレスの比較になっており、trueとなるのは同じ配列同士を比較した時だけです。順序付比較(<など)はより悪く、特定の条件下を除いてほとんどの場合結果は未規定です。

オブジェクトの同一性チェックはそのアドレスを明示的に取得して行うのが一般的かつ最良です。このような暗黙的な変換に頼ることは非常に稀であると思われ、その意味を知らないプログラマがそのコードを見てもその意図を見抜くことはできないでしょう。

したがって、この提案はC++20で非推奨とされた配列同士の比較をC++26で削除することを提案するものです。

より正確には、比較演算子オペランドの型変換の際に、片方のオペランドが配列の場合は他方のオペランドがポインタの場合にのみ配列からポインタへの変換を適用する、というように修正します。

そのため、C++20及びこの提案採択後でも、配列とポインタの間の比較(配列オペランドのポインタへの減衰)は非推奨ではなく合法的な動作です。

int main() {
  int arr1[5];
  int arr2[5];

  bool same = arr1 ==  arr2; // C++20で非推奨、削除を提案
  bool idem = arr1 == +arr2; // OK、アドレスの比較、ただし結果は未規定
}

P2866R0 Remove Deprecated Volatile Features From C++26

C++20で非推奨とされたvolatile関連の機能を削除する提案。

C++20では、無意味だったり危険なvolatile関連の使用法がコア言語・ライブラリ両方において非推奨とされ、その後C++23にて、このうち複合代入演算子の非推奨化は解除されました。

この提案は、C++20で非推奨化されC++23で残っているすべてのvolatileの用法について、削除しようとするものです。

複合代入演算子を除いては、非推奨化に反対するフィードバックは寄せられていないようで、これらの用法を削除することで誤解を招くようなコードを書けないようにすることができます。

P2867R0 Remove Deprecated strstreams From C++26

長く非推奨となっていた、std::strstreamを削除する提案。

std::strstreamは生配列をラップする文字列ストリームであり、std::stringstreamstd::stringをラップする文字列ストリームであるのと比較すると、こちらはchar[N]をラップするストリームです。

std::strstreamメンバ関数.str()char*を返しますが、こうして返された領域をユーザーが解放すべきなのか気にしなくていいのか、どのように管理すべきかが不透明でした。コンストラクタでの構築時はユーザーがその領域を指定することもstd::strstreamに確保させることもでき、std::strstreamに確保させた場合はその領域がどのように確保されたのかはどこにも記載がありません。

正解は、構築時に領域を渡していない場合に.str()で文字列を取得した場合はデストラクタ実行までの間に.freeze(false)を呼び出すことでstd::strstreamのデストラクタがその領域を解放してくれます。しかし、この挙動は分かりづらく、実際あまり周知されていなかったようで、簡単に間違って使うことができてしまっていました。

#include <strstream>

int main() {
  {
    // 動的なバッファを確保してもらう
    std::strstream s1;
    s1 << "dynamic buffer";
    s1 << std::ends;  // 手動null終端

    std::cout << "Contents : " << s1.str() << '\n';
    
    s1.freeze(false); // 忘れるとメモリリーク
  }
  
  {
    // 静的なバッファを渡す
    char buffer[20];
    std::strstream s2{buffer, std::size(buffer)};
    s2 << "static buffer";
    s2 << std::ends;  // 手動null終端

    std::cout << "Contents : " << s2.str() << '\n';
    // freeze()はいらない
  }
}

他にも、.str()のnull終端のためにはユーザーがそれを(std::endsを利用するなどして)ストリームに入力しなければならないなどやはり使いづらいところがあり、これらの問題からC++98で非推奨とされました。

代替としてはstd::stringstreamを用いることができるのですが、こちらはこちらで内部文字列をいつもコピーして返すなどの問題があり、std::strstreamのようにあらかじめ用意した静的な領域を渡すことで動的確保を避けたいような用途としては代替機能がなく、削除されずに残されていました。

C++20では、std::stringstream.view()が追加されたり、.str()がムーブして返すことができるようになるなど、文字列の取得に伴うコピー回避の手段が提供されたほか、std::spansstreamstd::strstreamの完全かつ安全な代替機能として提供されました。

この提案は、std::strstreamを削除する準備が整ったとして、C++26でstd::strstreamと関連する機能を削除しようとするものです。

P2868R0 Remove Deprecated std::allocator Typedef From C++26

std::allocatorにある非推奨化された入れ子型定義を削除する提案。

std::allocatorsize_typepointer等の入れ子型を持っていましたが、これはstd::allocator_traitsによって自動で導出可能であったため、C++17で非推奨とされました。これらについてはC++20で削除されています。

その後、C++23にてis_always_equalも非推奨とされました。この提案は、これはC++26で削除しようとするものです。

std::allocator::is_always_equalは、アロケータがステートレスであるかを調べる入れ子型で、デフォルトではstd::true_typeが使用されます。std::allocatorから派生して独自のアロケータを実装しようとする時にそのアロケータがステートレスではない場合、is_always_equalを上書きしない場合デフォルトのis_always_equal(ステートレスであると表明)が使用されてしまい、静かなバグを埋め込むことになります。

このような誤用を防止し、またわざわざその必要性などを説明する必要をなくすために、この提案ではstd::allocator::is_always_equalを削除しようとしています。

P2869R0 Remove Deprecated shared_ptr Atomic Access APIs From C++26

C++20で非推奨とされた、std::shared_ptrのアトミックフリー関数を削除する提案。

std::shared_ptrには、そのポインタ値そのもの(not参照先)にアトミックアクセスするためのフリー関数が用意されていました。しかし、これらはフリー関数であるため、アトミックアクセスしたい対象のstd::shared_ptrオブジェクトはプログラマが区別する必要がありました。そのため、直接対象のstd::shared_ptrオブジェクトにアクセスすれば非アトミックアクセスとなり、それが複数スレッド間で同時に起きればデータ競合として未定義動作となります。

// アトミックにアクセスしたいshared_ptrオブジェクト
std::shared_ptr<int> atomic_ptr{};

void thread_f() {
  // アトミックにshared_ptrを更新
  std::atomic_store(&atomic_ptr, std::make_shared<int>(20));

  // ポインタへのアクセスがアトミックになっていない
  auto n = *atomic_ptr;

  // こうすると、ポインタへのアクセスがアトミックになる
  auto ptr = std::atomic_load(&atomic_ptr);
  auto m = *ptr;
  // 一行で書いても良い
  // auto m = *std::atomic_load(&atomic_ptr);
}

void f() {
  // アトミック操作関数を経由しなければ非アトミックアクセス
  atomic_ptr = std::make_shared<int>(20);
  auto n = *atomic_ptr;
}

このように、std::shared_ptrのアトミックアクセス用フリー関数は簡単に誤って使用することができ危険だったためC++20で非推奨とされ、代わりにstd::shared_ptr(とstd::weak_ptr)のstd::atomic特殊化が追加されました。こちらを用いると、どこからアクセスした時でもstd::shared_ptrのポインタ値にアトミックにアクセスすることができます。

安全かつ完全に代替できる機能がすでに追加されており、削除することで危険な利用をコンパイルエラーとして報告することができるようになります。また、コードの変更も対象のstd::shared_ptrstd::atomic<std::shared_ptr>に書き換えるだけで済みます。この提案はこれらの理由からstd::shared_ptrのアトミックアクセス用フリー関数を削除しようとする提案です。

ただし、以前にこれらの関数を使用していたコードはstd::shared_ptrstd::atomic<std::shared_ptr>に書き換えた後でstd::atomic*を引数に取るフリー関数を呼び出すようになります。これそのものに問題はないのですが、これは<atomic>ヘッダで定義されているためヘッダ依存関係が変更されます。これを回避するために、削除対象の関数と同名のstd::atomic*を引数に取るフリー関数を<memory>ヘッダで宣言しておくことも提案されています。

P2870R0 Remove basic_string::reserve() From C++26

C++20で非推奨とされたstd::string::reserve()C++26に向けて削除する提案。

std::string::reserve()は元々、キャパシティを増大させるだけではなく減少させることもサポートしていました。これはstd::vector::reserve()の挙動とは異なっており、引数の値によってはパフォーマンス低下を引き起こすなどの問題がありました。

このため、C++20にてstd::string::reserve()はキャパシティを減少させないことが規定され、それに伴ってデフォルト引数(0)を取っていたオーバーロードが非推奨とされました(このオーバーロードは減少しかしないため)。

この提案は、std::string::reserve()のデフォルト引数を持つオーバーロードを削除する提案です。

元々C++20での非推奨時に、その後のLEWGのレビューまでの間に重大な懸念が明らかにならなければこのオーバーロードを削除することに合意されていました。削除を急ぐ理由は特にないようですが、この提案ではその以前の合意に従って削除することを推奨しています。

P2871R0 Remove Deprecated Unicode Conversion Facets From C++26

C++17で非推奨とされた<codecvt>ヘッダをC++26で削除する提案。

<codecvt>ヘッダではユニコードとの間で文字コードの変換を行う機能が提供されていましたが、不正なユニコード文字列を入力されるような攻撃を受けた際にそれをエラーとして安全にハンドルする方法がなく、細かい仕様も曖昧だったりで、その必要性に反して文字コード変換のための機能としては不適当なものでした。

そのため、<codecvt>はヘッダごとC++17で非推奨とされ、SG16はこの議論を契機としてC++により適切なユニコードサポートをもたらすための作業を開始しました。そこでは<codecvt>ヘッダの機能に代わる文字コード変換機能も目標に入っていますが、C++23時点ではまだそのようなものは利用可能ではありません。

SG16がこれを改善する計画や余裕を持たないこと、C++26出荷時点で非推奨期間の方が長くなることなどの理由から、この提案では、<codecvt>C++26でヘッダごと削除することを提案しています。ただし、その名前(codecvt_utf8など)を規格書のゾンビ名セクションに追加しておくことで、標準ライブラリ実装がC++26以降もそれを提供し続けることを許可する(ゾンビ名セクションはこのためにあるようです)ようにしておくことを提案しています。

P2872R0 Remove wstring_convert From C++26

C++17で非推奨とされたwstring_convertC++26で削除する提案。

1つ前の<codecvt>と同様の理由によって、wstring_convertC++26で削除しようとする提案です。

wstring_convert<codecvt>にある機能を使用する窓口のようなもので、C++17でそれらと一緒に非推奨とされました。<codecvt>を削除したとしてもユーザーが代替のものを提供して使い続けることは可能であり、また標準でそれを提供することができる可能性もあるとして、C++23サイクル中のSG16での議論では削除に慎重な意見が聴かれていたようですが、一方で削除に反対する意見はなかったようです。

ここでは削除を提案していますが、<codecvt>とは異なりそれをゾンビ名セクションに入れとくことは提案されていないようです。

P2873R0 Remove Deprecated locale category facets for Unicode from C++26

C++20で非推奨とされたロケールカテゴリファセットをC++26で削除する提案。

ロケールカテゴリファセットとはstd::codecvt/std::codecvt_bynameの特殊化のことで、この提案の対象となっているのは次の4つのものです

codecvt<char16_t, char, mbstate_t>
codecvt<char32_t, char, mbstate_t>
codecvt_byname<char16_t, char, mbstate_t>
codecvt_byname<char32_t, char, mbstate_t>

削除の理由や経緯に関しては、前の<codecvt>wstring_convertと同じです。

P2874R0 Mandating Annex D

Annex Dセクションにある機能の規定について、標準の他の部分と記法を合わせる提案。

Annex Dには過去に非推奨とされまだ削除されていない機能が移動されています。C++20で事前条件や適格要件の書き方が変更(P0788R3)された際、議論時間の都合からAnnex Dの内容はその変更が適用されず、古い記法のまま記述されました。その後、変更が適用された機能が非推奨とされて移動されたことで新旧の記述が入り混じっています。

この提案はAnnex DセクションにもP0788R3を適用し、標準の書き方を完全に統一しようとするものです。

この提案はすでに、2023年6月の全体会議でC++26に適用されることが決まっています。

P2875R0 Undeprecate polymorphic_allocator::destroy For C++26

C++20で非推奨とされたpolymorphic_allocator::destroyの非推奨化を解除する提案。

polymorphic_allocator::destroy()は与えられた領域にあるオブジェクトのデストラクタ呼び出しを行う関数です。これは、std::allocator_traits::destroy()が提供するデフォルト実装と全く同じであり、polymorphic_allocatorがアロケータとして使用されることを考えると冗長なものです。そのため、C++20で非推奨とされました。

しかし、polymorphic_allocatorは語彙型として設計されており、必ずしも従来のアロケータのようにコンテナでstd::allocator_traitsを介して使用されるだけのものではありません。そのような場合、std::allocator_traitsが提供している関数を単体で提供する必要があります。

この提案は、それらの理由とconstruct()との対称性を確保するためにもpolymorphic_allocator::destroy()を非推奨としないようにする提案です。

P2876R0 Proposal to extend std::simd with more constructors and accessors

std::simdに対して、利便性向上のために標準ライブラリにあるデータ並列型等のサポートを追加する提案。

std::simdクラスは、SIMDレジスタとそれに対する演算・操作をラップするようなクラス型で、std::simdのオブジェクトに対してC++コードとして記述した計算をそのまま(自動で)SIMD演算に落とし込むことを目的とするものです。

std::simdは現在、C++26導入を目指して作業が進められています(P1928)。

この提案は、std::simdオブジェクトの入出力の利便性を向上させるために、P1928のstd::simdクラスに欠けている標準ライブラリのクラス型との相互変換を追加しようとするものです。対象となるものは次のものです

  • std::bitset
    • simd_mask型にstd::bitsetを受け取るコンストラクタを追加
    • simd_mask型にstd::bitsetへの変換(変換演算子or明示的な変換関数)を追加
  • 整数値のビット表現の利用
    • simd_mask型に符号なし整数値を受け取るコンストラクタを追加
      • constexpr simd_mask(auto std::unsigned_integral bits) noexcept;
    • simd_mask型に、マスクを整数値のビット表現として取得する.to_ullong()を追加
  • std::initializer_list
    • std::simdstd::initializer_listを受け取るコンストラクタを追加
  • contiguous_range
    • std::simdcontiguous_rangeを受け取って初期化するコンストラクタを追加
      • constexpr simd(std::ranges::contiguous_range auto x);
    • std::arraystd::spanに対しては推論補助も追加

これによって、std::simd及びstd::simd_maskはそれと意味的に同一視できるものから変換する形で構築したり、逆に変換することで値をストアすることができるようになります。

P2878R0 Reference checking

P2878R1 Reference checking

プログラマが明示的に関数の戻り値に関するライフタイム注釈を行えるようにする提案。

ここで提案されているのは、Rustのexplicit lifetimeと呼ばれる機能に近いものです

// 戻り値の生存期間(参照の有効期間)は、引数であるa, bの生存期間を超えないという注釈
fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
  x
}

このような機能は静的解析のような外部のツールによっても達成できるかもしれませんが、それはユーザーがかなりの手間をかけて導入し使用した場合にのみ機能するものでしかありません。このような機能を限定的であったとしても言語機能として持つことで、ライフタイムにまつわる問題を警告ではなくコンパイル時のエラーとして検出できるようになり、より効果的に言語の安全性を高めることができます。

この提案では、コンパイル時に参照に対して4つの生存期間に関するプロパティを付加します。

  1. (生存期間が)グローバル
  2. ローカル
  3. 一時的(一時オブジェクト)
  4. その他(不明)

参照は初期化が必須であるため、これらのプロパティは初期化時に確定する性質です。

const int GLOBAL = 42;

void f(int* ip, int& ir/*生存期間不明*/) {
  int local = 42;

  int& r1 = *ip;    // 生存期間不明
  int& r2 = ir;     // 生存期間不明
  int& r3 = GLOBAL; // 生存期間はグローバル
  int& r4 = local;  // 生存期間はローカル
}

そして、参照がコピーされるときはこのプロパティも同時にコピーされます。

const int GLOBAL = 42;

void f(int* ip, int& ir/*生存期間不明*/) {
  int local = 42;

  int& r1 = *ip;    // 生存期間不明
  int& r2 = ir;     // 生存期間不明
  int& r3 = GLOBAL; // 生存期間はグローバル
  int& r4 = local;  // 生存期間はローカル

  // 参照のコピー
  int& r5 = r1;     // 生存期間不明
  int& r6 = r2;     // 生存期間不明
  int& r7 = r3;     // 生存期間はグローバル
  int& r8 = r4;     // 生存期間はローカル
}

このプロパティだけでも、ローカル変数の参照をreturnする関数をエラーにすることができます

const int GLOBAL = 42;

int& f(int* ip, int& ir/* unknown lifetime */) {
  int local = 42;

  int& r1 = *ip;    // 生存期間不明
  int& r2 = ir;     // 生存期間不明
  int& r3 = GLOBAL; // 生存期間はグローバル
  int& r4 = local;  // 生存期間はローカル

  // 参照のコピー
  int& r5 = r1;     // 生存期間不明
  int& r6 = r2;     // 生存期間不明
  int& r7 = r3;     // 生存期間はグローバル
  int& r8 = r4;     // 生存期間はローカル

  return r8;  // error! ローカル参照を返している
}

たとえば次のようにして、ある参照の生存期間を別の参照の生存期間に関連付けることができるようにします

const int GLOBAL = 42;

// 戻り値の参照の有効期間は引数left/right(のより短い方)の有効期間より長くない
[[dependson(left, right)]]
const int& f1(const int& left/* unknown lifetime */, const int& right/* unknown lifetime */) {
  if (randomBool()) {
    return left;
  } else {
    return right;
  }
}

この時、先程のプロパティはtemporary < local < globalの順で生存期間が短いとされます。この順序によって、この関数の戻り値の参照の生存期間は引数left/rightの生存期間のより短いものに制限されます。

これによってさらに次のチェックが可能となります

  • 一時オブジェクトへの参照を返すとエラー
  • 一時的な生存期間を持つ参照(一時オブジェクトへの参照)を初期化した後、別の行(別の完全式)で使用するとエラー
int& f2() {
  int local = 42;

  const int& r1 = f1(local, local);   // local
  const int& r2 = f1(GLOBAL, GLOBAL); // global
  const int& r3 = f1(42, 42);         // temporary
  const int& r4 = f1(local, GLOBAL);  // local
  const int& r5 = f1(local, 42);      // temporary
  const int& r6 = f1(GLOBAL, 42);     // temporary
  
  if (randomBool()) {
    return r1;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r2;  // OK、r2はglobal参照
  }
  if (randomBool()) {
    return r3;  // error: temporary参照を返せない
  }
  if (randomBool()) {
    return r4;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r5;  // error: temporary参照を返せない
  }
  if (randomBool()) {
    return r6;  // error: temporary参照を返せない
  }

  int x1 = r3 + 43; // error: temporary参照を使用できない
  int x2 = r5 + 44; // error: temporary参照を使用できない
  int x3 = r6 + 45; // error: temporary参照を使用できない
  return f1(f1(GLOBAL, 4), f1(local, 2)); // error: temporary参照を返せない
}

これによって、ローカル変数の間接的なダングリングだけでなく、一時オブジェクトの間接的なダングリングも修正されます。

さらに、クラス型が参照をpublicメンバとして持っている場合も、同様の事を行うことができます。

// 参照をpublicメンバとして含むクラス型
struct S {
  int& first;
  const int& second;
};

int& f2() {
  int local = 42;

  S s1{GLOBAL, local};
  S s2{local, f1(GLOBAL, 24)};
  
  const int& r1 = s1.first;   // global
  const int& r2 = s1.second;  // local
  const int& r3 = s2.first;   // local
  const int& r4 = s2.second;  // temporary
  
  if (randomBool()) {
    return r1;  // OK: r2はglobal参照local参照を返せない
  }
  if (randomBool()) {
    return r2;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r3;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r4;  // error: temporary参照を返せない
  }

  int x = r4 + 43;  // error: temporary参照を使用できない

  return 42;  // error: temporary参照を返せない
}

S f3() {
  int local = 42;

  S s1{GLOBAL, local};
  S s2{local, f1(GLOBAL, 24)};
  
  if (randomBool()) {
    return s1;  // error: local参照を含んでいる
  }

  return s2;  // error: localとtemporary参照を含んでいる
}

また、言語組み込み機能であればpublicではなくても同様の事を検出できます

// local/temporaryなものをキャプチャもしくは`return`するラムダを返せないようにする
auto lambda() {
  int local = 42;
  
  const int& ref_temporary = f1(GLOBAL, 24);

  return [&local, &ref_temporary]() -> const int& {
      if(randomBool()) {
        return local; // error: local参照を返せない
      }

      return ref_temporary; // error: temporary参照を返せない
    };
  // error: localとtemporary参照を含んでいる
}

// local/temporaryなものをキャプチャもしくは`return`するコルーチンを返せないようにする
auto coroutine() {
  int local = 42;
  
  const int& ref_temporary = f1(GLOBAL, 24);

  return [&local, &ref_temporary]() -> generator<const int&> {
      if(randomBool()) {
        co_return local;  // error: local参照を返せない
      }

      co_return ref_temporary;// error: temporary参照を返せない
    };
  // error: localとtemporary参照を含んでいる
}

クラス型が非publicな形で参照を内部に含む場合、それを取得しようとするメンバ関数に対してそのオブジェクトそのものに依存するライフタイムを注釈する追加の構文によって同様の検出を行います

namespace std {
  template <class charT,
            class traits = char_traits<charT>,
            class Allocator = allocator<charT> >
  class basic_string {
    ...

    // 戻り値の参照(likeなオブジェクト)はthisに依存する
    constexpr std::string::operator [[dependson(this)]] std::basic_string_view<CharT, Traits>() const noexcept;
  }
}

int main() {
  std::string_view sv = "hello world"s; // temporary

  sv.size();  // error: temporary参照を使用できない
}

これはstd::spanstd::function_refなどの他の参照セマンティクスを持つ型でも使用できます。ただし、reference_wrapperのように再束縛できる(あとから参照先を切り替えられる)ものについてはこれを適用できません。他にもポインタ型やstd::unique_ptr当のポインタセマンティクスを持つ型が該当します。

ただし、そのような型でもconstであれば初期化時に非nullで初期化されていると推定でき、また後から参照先が変化しないため同様のことが行えます。

[[dependson(left, right)]]
const std::reference_wrapper<const int> f(const int& left/* unknown lifetime */, const int& right/* unknown lifetime */) {
  if(randomBool()) {
    return std::cref(left);
  } else {
    return std::cref(right);
  }
}

最後に、これらのチェックはnew式による初期化時にも適用できます

struct S { int mi; const std::pair<int,int>& mp; };

S a { 1, {2,3} };
S* p = new S{ 1, {2,3} }; // error: オブジェクトはtemporaryに依存する

もし参照がpublicではない場合は、そのコンストラクタでその依存関係を指定する追加の構文が必要になります

class S {
  int mi;
  const std::pair<int,int>& mp;

public:
  [[parameter_dependency(dependent{"this"}, providers{"mp"})]]
  S(int mi, const std::pair<int,int>& mp);
};

S a { 1, {2,3} };         // error: オブジェクトはtemporaryに依存する
S* p = new S{ 1, {2,3} }; // error: オブジェクトはtemporaryに依存する

この提案では構文の説明のために属性構文が使用されていますが、属性として採用すべきか別の言語機能として採用すべきかは提案しておらず、むしろその能力を獲得することを目的としています。

この提案はSG23でレビューされ、引き続き議論されないことが決定しています。

P2880R0 Algorithm-like vs std::simd based RNG API

複数の乱数の効率的な生成のためのAPIとして、提案中のベクターAPIstd::simdによるAPIを比較する提案。

P1068では大量の乱数を効率的に生成するための高レベルなベクターAPIを提案しており、そこでは範囲に対して乱数を充填するAPIによって実装が効率的な乱数生成方法を選択できるようにしています。その実装には例えばSIMD演算によるものが想定されています。

一方で、P1928で議論されているstd::simdSIMDレジスタと命令のラッパクラスであり、std::simdそのものの操作あるいはAPIによって直接的にそのような大量乱数生成の効率実装を行うことができます。そのため、std::simd導入を見据えた場合にP1068の高レベルAPIによる複数乱数生成は必要なのか?あるいはstd::simdに乱数生成のためにどのようなAPIを持たせるべきか?と言ったことが疑問として浮かんできます。

この提案はそのような疑問に答えるために、両者のAPIによるコードを比較することで利点欠点を洗い出し、std::simdの乱数生成APIのいくつかの可能性を示すものです。

この提案では、"European options pricing"というベンチマーク中から複数の乱数を生成しているコードを抽出し、それをP1068とstd::simdを用いて実装してみるとどうなるかを示すことで比較を行なっています。

// "European options pricing"中の複数の乱数を生成し利用するコード

std::mt19937 engine(777); // 乱数エンジン
std::normal_distribution distribution(0., 1.);  // 分布生成器

double v0 = 0, v1 = 0;

// ループ(npath)の分乱数を生成する
for (std::size_t p = 0; p < npath; ++p) { //e.g., npath=1,000,000
  // 乱数の生成
  double rand = distribution(engine);

  // 乱数の利用
  double res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
  v0 += res;
  v1 += res * res;
}

// 結果出力
result     = v0 / npath;
confidence = v1 / npath;

P1068R7の高レベルAPI

まずP1068R7ではstd::ranges::generate_randomという関数(正確にはCPO)を用いて、範囲に対して指定されたエンジンと分布によって生成した乱数を充填します。それによって、先ほどのコードは次のようになります

std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);

// 生成した乱数を受けるための範囲
std::array<double, npath> rand; // npath=1,000,000 -> sizeof(rand)=8 MB

// 複数の乱数を一括生成し範囲に詰め込む
std::ranges::generate_random(rand, engine, distribution);

double v0 = 0, v1 = 0;

// 乱数の利用
for(std::size_t p = 0; p < npath; ++p) {
  double res = std::max(0., S * exp(sqrt(T) * vol * rand[p] + T * mu) - X);
  v0 += res;
  v1 += res * res;
}

result     = v0 / npath;
confidence = v1 / npath;

std::ranges::generate_randomによってループ前に使用予定の乱数を全て生成しているため、ループの中は乱数利用コードのみになります。前述のように、これはSIMD命令等を用いて効率的に乱数生成を行う実装がなされるはずなので、ループで生成しながら利用するコードよりも効率的になることが期待されます。

ただし、この場合生成した乱数を受けるためにそこそこ巨大な配列を最初に用意しなければなりません。これはキャッシュヒット率を低下させることが予想されるため、最適なコードとは言えません。そこで効率化のために、バッファリングを行います

std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);

// 乱数を受けるバッファ
std::array<double, nbuffer> rand; // e.g., nbuffer=128

double v0 = 0, v1 = 0;

// nbuffer分づつ乱数を生成して利用するループ
for(std::size_t p = 0; p < npath; p += nbuffer) {
  // 末尾の調整(npathはnbufferの倍数とは限らない)
  std::size_t local_size = (p + nbuffer <= npath) ? nbuffer : (npath - p); // dealing with tail
  // nbuffer分乱数を生成
  std::ranges::generate_random(std::span(rand.begin(), local_size), engine, distribution);

  // 乱数の利用
  for(std::size_t b = 0; b < local_size; ++b) {
    double res = std::max(0., S * exp(sqrt(T) * vol * rand[p] + T * mu) - X);
    v0 += res;
    v1 += res * res;
  }
}

result     = v0 / npath;
confidence = v1 / npath;

nbuffer分づつ処理を分けることで、1度に生成する乱数とそれを保存するためのストレージサイズをnbufferに減らします。その代わり乱数生成を一括で行う単位も減ってしまいますが、キャッシュミスによるペナルティよりも一度の乱数生成オーバーヘッドの方が小さい間はこちらの方が効率的になります。

std::simdAPI

次に、std::simdでも同じことを考えます。ただし、std::simdはまだそのようなAPIを持たないため、その可能な設計として幾つかのパターンが考えられます。この設計で重要なことは、ユーザーが要求するstd::simd型に関する情報をどのレベルで取得するか?ということです

1. エンジンのテンプレートパラメータ

1つ目の例は、エンジンと分布生成器の両方がstd::simd型に関する情報を持って構築されるものです。

std::mt19937<std::fixed_size_simd<std::uint_fast32_t, 16>> E(777);    // 乱数エンジン
std::normal_distribution<std::fixed_size_simd<double, 16>> D(0., 1.); // 分布生成器

// 乱数の生成
auto rand = D(E);

エンジンと分布生成器の数値型が異なっており、既存の実装はこのような場合normal_distributiondouble値1つに対してエンジンのuint_fast32_t値を2つ消費します(全体では、乱数16個に対してエンジン出力32個を消費する)。そのため、実装によっては次のように定義した方が効率的である場合があります

// 32 SIMD size passed to engine
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, 32>> E(777);
// 16 SIMD size passed to distribution
std::normal_distribution<std::fixed_size_simd<double, 16>> D(0., 1.);

auto rand = D(E);

より洗練された分布生成器では結果値ごとに異なった個数の入力エンジン値を消費する分布の実装が一般的となるため、分布生成器とSIMD幅が与えられた時にそれに最適なエンジンのSIMD幅の適切なサイズの普遍的な解答はありません。

そのような分布生成器において、エンジンが生成した固定幅の乱数配列の一部しか消費しない場合、使用しなかった残りの部分について選択肢が生まれます

  1. 残りの部分は分布オブジェクトが内部に保存する
    • 保存する領域のオーバーヘッドやどう使用されるかが問題となる
    • 残りの部分が次の生成に使用されるとすると、分布のランダム性に残った値の利用という要素が追加される
  2. 残りの部分は廃棄する

このことを念頭に置いて、最初のコードをこのAPIで書き直したのが次のコードです

// SIMD幅
constexpr std::size_t size = 16;

// エンジンと分布生成器
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, size>> E(777);
std::normal_distribution<std::fixed_size_simd<double, size>> D(0., 1.);

double v0 = 0, v1 = 0;
std::size_t p = 0;

for(; p + size <= npath; p += size) {
  // 複数乱数(size個分)の一括生成
  auto rand = D(E); // std::fixed_size_simd<double, size>

  // 乱数の利用(自動SIMD化)
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
  v0 += std::reduce(res);
  v1 += std::reduce(res * res);
}

// 処理数(npath)が16(size)の倍数ではない場合の端数の処理
if (p != npath) {
  // 複数乱数(size個分)の一括生成
  auto rand_tail = D(E);

  // 乱数の利用
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
  for(std::size_t i = 0; p + i < npath; ++i) {
      v0 += res[i];
      v1 += res[i] * res[i];
  }
  // resには使用されない部分があり、捨てられる
}

result     = v0 / npath;
confidence = v1 / npath;

このコードでは、npathsize(16)の倍数ではない場合に必要以上の数の乱数を生成してしまうため最初のコードど完全に同じことをしているわけではありませんが近いコードではあります。そして、端数の処理の際に生成した乱数(rand_tail)の一部を捨ててしまうことになりますが、その捨て方が問題となる可能性があります(エンジンや分布生成器を再利用する場合など)。

2. エンジンのテンプレートパラメータ + 再バインドコンストラク

それらの問題を念頭に置いて次のAPI設計案では、異なるSIMD幅やスカラ型を持つエンジンの再バインド構築によって未使用部分の問題を回避します。再バインドによって以前のエンジンの内部状態を引き継ぐことで、余分な乱数の生成を防止し、捨てられる値や目に見えない状態を回避します。

constexpr std::size_t size = 16;

std::mt19937<std::fixed_size_simd<std::uint_fast32_t, size>> E(777);
std::normal_distribution<std::fixed_size_simd<double, size>> D(0., 1.);

double v0 = 0, v1 = 0;
std::size_t p = 0;

for (; p + size <= npath; p += size) {
  // 複数乱数(size個分)の一括生成
  auto rand = D(E);

  // 乱数の利用
  auto res = std::max(0., S*exp(sqrt(T) * vol * rand + T * mu)-X);
  v0 += std::reduce(res);
  v1 += std::reduce(res * res);
}

// 処理数(npath)が16(size)の倍数ではない場合の端数の処理
if (p != npath) {
  // エンジンの再バインド
  std::mt19937 E_tail(E); // rebinding to scalar type
  std::normal_distribution D_tail(0., 1.); // getting scalar distribution

  for (; p < npath; ++p) {
    // 乱数の生成(スカラAPI)
    auto rand_tail = D_tail(E_tail);

    // 乱数の利用
    auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
    v0 += res;
    v1 += res * res;
  }

  // 元のエンジンに状態を返す
  E = E_tail;
}

result     = v0 / npath;
confidence = v1 / npath;

乱数エンジンの再バインド機構によって、乱数エンジンの生成値の型を変更しつつエンジンの内部状態を引き継ぐことができるようにしています。

この場合、ユーザーレベルの柔軟性が得られる一方で余計なコピーが追加されており、状態の大きなエンジンではそのオーバーヘッドが問題となる可能性があります。

3. アルゴリズムlikeな関数のテンプレートパラメータ

更なる代替案として、std::simd型の情報を受け取る点をエンジン/分布生成器からその使用地点に移すAPIが考えられます。これによって、エンジンはその使用モードを意識することなく、内部のレイアウトは実装によってスカラ/ベクターどちらの生成も可能とするバランスの取れた形に選択されます。

std::mt19937             E(777);
std::normal_distribution D(0., 1.);

// 乱数生成
auto rand = std::generate_random_simd<std::fixed_size_simd<double, 16>>(E, D);

エンジンレイアウトの厳密な(環境のSIMD幅に合わせた)最適化が制限される代わりに、標準ライブラリに実装の自由が与えられ、エンジンからベース乱数を消費する方法はプラットフォームによって異なる可能性があリます。

constexpr std::size_t size = 16;

std::mt19937 E(777);
std::normal_distribution D(0., 1.);

double v0 = 0, v1 = 0;
std::size_t p = 0;

for (; p+size <= npath; p += size) {
  // 複数乱数(size個分)の一括生成
  auto rand = std::get_random_simd<std::fixed_size_simd<double, size>>(E, D);

  // 乱数の利用
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
  v0 += std::reduce(res);
  v1 += std::reduce(res * res);
}

// 処理数(npath)が16(size)の倍数ではない場合の端数の処理
for (; p < npath; ++p) {
  // 乱数の生成(スカラAPI)
  auto rand_tail = D(E);

  // 乱数の利用
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
  v0 += res;
  v1 += res * res;
}

result     = v0 / npath;
confidence = v1 / npath;

この例は最初のコードと完全に一致しており、余分な乱数を生成してエンジン状態が不明になったり、それを回避するためにエンジン状態をコピーするなどの問題を回避しています。

これらの比較と観察から得られる結論は次のようなものです

  • 高レベルのAPIは通常のC++開発者が作成する乱数利用アプリケーションの大部分をサポートすることを目的としている
    • APIを実装するベンダがHWアクセラレータを有効にする実装をとれば、そのようなアプリケーションのパフォーマンスを向上できる
    • SIMDを利用した実装はその一種であり、同じAPIを使用して利用可能となる
  • 低レベルの(std::simdによる)APIC++の慣習に則った上でよりHWに近いコーディングを必要とする上級開発者を対象としている
    • ただし、std::simdによるAPIではエンジンと分布生成器の概念に基づいた乱数生成アルゴリズムSIMD実装の詳細を開発者に理解させることは避けるべき

従って、P1068の高レベルのAPIstd::simdによるAPIはターゲットが異なるため排他的なものではなく、両方を標準ライブラリに持つことは合理的であると考えられます。

P2881R0 Generator-based for loop

範囲for文に新しいループカスタマイゼーションポイントを追加する提案。

<ranges>のRangeアダプタなどに見られるようにイテレータの定義は複雑で、何か処理を範囲for文でループさせるように書き直そうとするとその対象の状態をoperator*operator++に分割してエンコードしてやる必要があり、これによって処理のイテレータrange)への移行は非常に難しくなっています。

C++20のコルーチンとC++23std::generatorの利用によってそれは劇的に簡単になります。例えばstd::generatorを使用するとviews::filterviews::joinviews::concatは次のように簡単に定義できます

template <typename Rng, typename Predicate>
auto filter(Rng&& rng, Predicate predicate) -> std::generator<…> {
  for (auto&& elem : rng) {
    if (predicate(elem)) {
      co_yield std::forward<decltype(elem)>(elem);
    }
  }
}

template <typename Rng>
auto join(Rng&& rng_of_rng) -> std::generator<…> {
  for (auto&& rng : rng_of_rng) {
    co_yield std::ranges::elements_of(std::forward<decltype(rng)>(rng));
  }
}

template <typename ... Rng>
auto concat(Rng&& ... rng ) -> std::generator<…> {
  ((co_yield std::ranges::elements_of(std::forward<decltype(rng)>(rng))), ...);
}

ただしstd::generatorもいいとこづくめではなく、いくつかデメリットがあります

  1. パフォーマンスでイテレータに劣る
    • コルーチンステート保存のためのメモリ確保や、関数の中断のサポート、例外機構などによるオーバーヘッドが回避できない
  2. ネストした文脈でco_yieldを使用できない
    • スタックレスコルーチンはその内部で呼び出した関数内などのスタックフレームの異なる場所で中断できない

2つ目の問題は、ツリー構造のようなネストした構造に対してstd::generatorを直接適用できない場合がある問題で、次のようなものです

struct tree {
  using leaf = int;
  // ノードの途中か末端かのどちらか
  std::variant<leaf, std::vector<tree>> impl;
};

// ツリー構造を辿って末端の値を出力していく
std::generator<int> tree_data(const tree& t) {
  std::visit(
    overloaded(
      [&](int data) {
        co_yield data; // error
      },
      [&](const std::vector<tree>& children) {
        for (auto& child : children)
        co_yield std::ranges::elements_of(tree_data(child)); // error
      }),
    t.impl);
}

これは、コルーチンがその内部で呼び出した関数で中断(co_yieldco_await)できないというスタックレスコルーチンの特性によるものです。ネストした部分を別のコルーチンにすることで回避はできます

std::generator<int> tree_data(const tree& t) {
  auto sub =
      std::visit(
        overloaded(
          [&](int data) -> std::generator<int> {
            co_yield data;
          },
          [&](const std::vector<tree>& children) -> std::generator<int> {
            for (auto& child : children) {
              co_yield std::ranges::elements_of(tree_data(child));
            }
          }),
        t.impl);

  co_yield std::ranges::elements_of(sub);
}

この提案は、コルーチンによるgeneratorの利点を享受しつつこれらのデメリットを回避するような、generatorにちかい記述によって処理をループに落とし込むための仕組みを提案し、それを範囲for文のカスタマイゼーションポイントとして追加しようとするものです。

この提案によるgeneratorgenerator ranges(ジェネレータ範囲)と呼ばれており、それは、範囲for文の処理本体をラムダ式として受け取ってそれを適宜呼び出しながら処理を実行する関数オブジェクト的な何かです。ジェネレータ範囲による範囲for文は、begin()/end()を使用したイテレータループを行う代わりに、そのループ本文をラムダ式としてジェネレータ範囲のオブジェクトの関数呼び出し演算子として渡して、ジェネレータ範囲のオブジェクトの関数呼び出し演算子では、そうして受け取ったループ本文に適宜各要素を渡して呼び出すことでループを実行します。

// 1, 2, 3を生成するジェネレータ範囲
struct generator123 {

  // sinkには、呼ばれた範囲forの本体処理をcallableとして受ける
  auto operator()(auto&& sink) const {
    // 範囲forの本体がループを継続しているかを判定する
    std::control_flow flow;

    // 範囲forの本体処理に1を入力し(xに1が代入され)て実行
    flow = sink(1);
    if (!flow) return flow;

    // 範囲forの本体処理に1を入力し(xに2が代入され)て実行
    flow = sink(2);
    if (!flow) return flow; // この例ではここで終わる

    // 範囲forの本体処理に1を入力し(xに3が代入され)て実行
    return sink(3);
  }
};

for (int x : generator123{}) {
  std::print("{}\n", x);
  if (x == 2) {
    break;
  }
}
// 1
// 2

std::control_flowはジェネレータ範囲による範囲for文の本体内のbreak/continueの結果を表現するクラス型です。これは基本的に強く型付けされたbool型で、次のように定義されます

namespace std {

  /// `continue`に対応するタグ型とそのオブジェクト
  struct continue_t {
    // Empty.

    constexpr operator std::true_type() const noexcept {
      return {};
    }

    constexpr std::false_type operator!() const noexcept {
      return {};
    }

    friend std::strong_ordering operator<=>(continue_t, continue_t) noexcept = default;
  };
  inline constexpr continue_t continue_;

  /// `break`に対応するタグ型とそのオブジェクト
  struct [[nodiscard("need to forward break")]] break_t {
    // Empty.

    constexpr operator std::false_type() const noexcept {
      return {};
    }

    constexpr std::true_type operator!() const noexcept {
      return {};
    }

    friend std::strong_ordering operator<=>(break_t, break_t) noexcept = default;
  };
  inline constexpr break_t break_;


  /// `continue/break`または実装定義の`break`に似た状態を表す制御フローオブジェクト
  class [[nodiscard("need to forward control flow")]] control_flow {
  public:
      /// `continue`状態で構築
      constexpr control_flow(continue_t) noexcept;
      constexpr control_flow() noexcept : control_flow(continue_) {}

      /// `break`状態で構築
      constexpr control_flow(break_t) noexcept;

      /// Trivially copyable.

      /// `continue`の時true, それ以外の場合は`false`を返す
      constexpr explicit operator bool() const noexcept;

      constexpr friend bool operator==(control_flow, control_flow) noexcept;
      constexpr friend std::strong_ordering operator<=>(control_flow, control_flow) noexcept;
  };
}

std::continue_std::break_std::true_type/std::false_typeへの定数変換演算子を持つ個別のタグ型として定義されており、これによって常に継続や常に中断といった一般的なケースを型システムにエンコードして最適化を保証することができます。

ジェネレータ範囲による範囲for文は通常の範囲forと同様に言語組み込みマクロのようなもので、次のように展開されます

for (T binding : object)
{
  body
}

{
  auto __body = [&](T&& __element) -> see-below {
    T binding = std::forward<T>(__element);
    body
    return std::continue_;
  };

  auto __flow = object(__body);
  
  see-below // body内のreturn/gotoがここに配置される
}

関数本体であるbodyで特に終了(returnbreak)をしなければ、デフォルトでstd::continue_が返されることで処理を継続させます。bodyに何か制御文を書くとそれはstd::control_flowの値を返すように変換されます

  • continue;
    • return std::continue_;
  • break;
    • return std::break_;
  • return;
    • return implementation-defined
    • std::break_;と同じ効果となるが、コンパイラはループの後ろにreturnを配置する
  • return expr;
    • exprを実行してその結果をどこかに保存してから、return implementation-defined
    • std::break_;と同じ効果となるが、コンパイラはループの後ろにreturnを配置し戻り値を返す
  • goto
    • return implementation-defined
    • std::break_;と同じ効果となるが、コンパイラはループの後ろにgotoを配置する
  • throw
    • そのまま
  • co_await/co_yield/co_return
    • ill-formed

これによって、ジェネレータ範囲による範囲for文は利用者から見ると通常のfor文とほとんど透過的に使用することができます。

// このジェネレータ範囲によるループは
for (int x : generator123{}) {
  if (x == 0)
    continue;
  
  if (x == 2)
    break;
  
  std::printf("%d\n", x);
}

// こう展開される
{
  auto __body = [&](int&& __element) -> std::control_flow {
    int x = __element;
    
    if (x == 0)
      return std::continue_;
    
    if (x == 2)
      return std::break_;

    std::printf("%d\n", x);
    
    return std::continue_;
  };

  auto __flow = generator123{}(__body);
  (void)__flow;
}

この新しい範囲for文では、co_yieldの代わりに関数呼び出し演算子を通してループ本体に要素(値)を提供することで擬似的な関数の中断と値の生成を実現し、ループの処理が終わると自動的に再開されます。それはコルーチンのような複雑な仕組みを全く用いておらず、そのために導入されていたオーバーヘッドも全くありません。そのため、パフォーマンスではイテレータと同等かそれ以上のものを達成でき、コルーチンあるいはstd::generatorの制約によるデメリットも回避されます。

views::filterの例

template <typename Rng, typename Predicate>
auto filter(Rng&& rng, Predicate predicate) {
  // ループ処理本体を受け取るジェネレータ範囲を返す
  return [=](auto sink) {
    for (auto&& elem : rng) {
      if (predicate(elem)) {
        auto result = sink(std::forward<decltype(elem)>(elem));
        if (result == tc::break_) {
          return result;
        }
      }
    }

    return tc::continue_;
  };
}

ネストした構造に対しても、ほぼそのまま適用できます

auto tree_data(const tree& t) {
  return [&](auto sink) {
    auto flow =
      std::visit(
        overloaded(
          [&](int data) {
            // Forward break/exit.
            return sink(data);
          },
          [&](const std::vector<tree>& children) {
            for (auto& child : children) {
              auto flow = tree_data(child)(sink);
              if (flow == tc::break_) {
                // Forward early break and do actually break.
                return flow;
              }
            }
            return tc::continue_;
          }),
        t.impl);

    return flow;
  }
}

P2882R0 An Event Model for C++ Executors

実行コンテキスト間でやりとりするための標準的な方法を提供するための設計や問題点について探るための文書。

P2300では実行コンテキストをschedulerコンセプトによって抽象化しており、そこではschedulerを提供する以外のことを要求していません。そのため、異なる実行コンテキストから制御を移すためのインターフェースが欠けており、それを行うためのtransfer()アルゴリズムのようなものの実装が難しくなっています。

例えば、次のようなネットワークから音源をダウンロードしてきて、それをデコードし再生(再生デバイスへ転送)するような処理を考えます

// 音源のダウンロード
void receive() {
  SnapClient srv{srvAddr, srvPort};

  while (true) {
    // データを受信し
    std::span<uint8_t> buf = srv.receiveWireChunk(); // blocks

    // キューに入れる
    opusQueue.wait_push(buf);
  }
}

// PCMへのデコード
void decode() {
  while (true) {
    std::span<uint8_t> inBuf;
    // 受信データをキューから取り出し
    opusQueue.wait_pop(inBuf);

    // デコードし
    int samples = opus_decode(decoder,
                              inBuf.data(), inBuf.size(),
                              decodeBuf.data(), maxFrameSamples,
                              0);

    std::span outBuf(decodeBuf.data(), samples);
    // キューに入れる
    pcmQueue.wait_push(outBuf);
  }
}

// PCMの再生(デバイスへの転送)
void play() {
  while (true) {
    std::span<uint8_t> inBuf;
    // PCMデータをキューから取り出し
    pcmQueue.wait_pop(inBuf);
    
    uint32_t const *start = inBuf.data();
    size_t offset = 0;

    // 再生デバイスへ転送する
    while (offset < size) {
      size_t bytesDone;

      // i2s_channel_write blocks
      i2s_channel_write(tx,
                        start + offset,
                        size - offset,
                        &bytesDone,
                        noTimeout);
      
      offset += bytesDone;
    }
  }
}

この3つの関数はそれぞれ別のスレッドで実行されます。この時、各スレッド(実行コンテキスト)間のやりとりにはキューが使用されています。

これをP2300のsender/receiverによって実装すると例えば次のようになります

void network_speaker() {
  // デコードと再生を行うための実行コンテキストのschedulerを取得
  exec::scheduler auto sched0 = cppos::ContextCore0::LoopScheduler();
  exec::scheduler auto sched1 = cppos::ContextCore1::LoopScheduler();

  AudioServer srv(sched0, srvAddr, audioPort);

  srv.readPacket()                              // データ受信
      | bufferedTransfer<netBufferSize>(sched1) // 結果をバッファにつめて実行コンテキスト遷移
      | then(soundDecode)                       // 受信データのデコード
      | bufferedTransfer<pcmBufferSize>(sched0) // 結果をバッファにつめて実行コンテキスト遷移
      | then(sendI2sChunk())                    // PCMを再生デバイスへ転送
      | runForever();                           // これらの一連の処理を繰り返す
}

このbufferedTransferは内部的にキューを使用しているものとすると、この場合も実行コンテキスト間のやりとりにはキューが使用されています。

また、コルーチンを使用しても実装できます

CoHandle receive() {
  while (true) {
    std::span<uint8_t> buf = co_await srv.coReceiveWireChunk(); // blocks
    co_await opusQueuePush.wait_push(buf);
  }
}

CoHandle decode() {
  while (true) {
    std::span<uint8_t> inBuf;
    co_await opusQueuePull.wait_pop(inBuf);

    int samples = opus_decode(decoder,
                              inBuf.data(), inBuf.size(),
                              decodeBuf.data(), maxFrameSamples,
                              0);
    std::span outBuf(decodeBuf.data(), samples);

    co_await pcmQueuePush.wait_push(outBuf);
  }
}

CoHandle play() {
  while (true) {
    std::span<uint8_t> inBuf;
    co_await pcmQueuePull.wait_pop(inBuf);

    uint32_t const *start = inBuf.data();
    size_t offset = 0;
    
    while (offset < size) {
      size_t bytesDone;

      co_await co_i2s_channel_write(tx,
                                    start + offset,
                                    size - offset,
                                    &bytesDone,
                                    noTimeout);

      offset += bytesDone;
    }
  }
}

// 例えばこのように実行
void network_speaker() {
  while (true) {
    co_await receive();
    co_await decode();
    co_await pray();
  }
}

他にもファイバーによる実装も考えることができ、それはスレッド版とほぼ同じようなコードになります。

現在のC++はこれらの実装方法のうち最初のスレッド版だけをサポートしています。ただし、スレッド間で共有可能な並行キューはありません。とはいえ、コルーチンはすでに利用可能であり、P2300やファイバーも程なく利用可能になる予定なので、残りのコードも近いうちにサポートされます。すると、そこに欠けているのは実行コンテキスト間同期に使用している並行キューのようなものです。

この文書は、このキューのような実行コンテキスト間で通信(同期)を取るための標準的なメカニズムに必要な要件や設計について検討し、それに伴って浮かんだいくつかの疑問についてSG1に問うものです。まだ何かを提案しているわけではありません。

実行コンテキストに特化した同期メカニズムを提供するのは簡単ですが、全ての実行コンテキストでうまく動作するものを提供するのは困難です。例えば、上記例のキューはスレッド版とコルーチン版で異なるメンバ関数を提供しなければならないほか、動機を取る方法についても実行コンテキストによって最適なものが変わるでしょう。

それぞれの実装に特化したものを用意しても、複数の実行コンテキストを組み合わせて使用する場合にうまくいかなくなります。例えば、コルーチンによる実行においてその処理の一部(デコード)を別のスレッドで行おうとすると、コルーチンとスレッドという異なる実行方法にまたがって動作する同期方法(例ではキュー)が必要になります。

P0073R2ではそのような同期のためのキューではなくeventというクラス型を提案しています。

class event {
  // イベント通知
  void signal();
}

// イベント通知を待機
void block(event until);

このような抽象化は有用であると思われますが、異なる実行コンテキスト間で動作しようとするとき、signal()はイベントを受信する(送信先の)実行コンテキストを、block()は現在の実行コンテキストを知っていなければならないようです。そのために、eventクラスはそのオブジェクトでブロック中の全てのタスクのリストを保持する必要があります。実行コンテキストが静的にわかっていれば問題はないですが、動的にしかわからない場合が想定されるようで、その場合には実行時に現在の実行コンテキストを知る手段が必要となります

並行キューについては、キュー本体とキューのインターフェースを分離することで実装を効率化できる可能性が見出されました。その場合、キューは実行コンテキストによってテンプレート化され、実行コンテキストはそのブロッキングと通知のためのAPI(上記eventクラスのインターフェースと同様のもの)を提供する必要があります。

すなわち、eventクラスのようなAPIの設計はそれそのものやキューに限らず、同様の目的の同期メカニズムに対して一般化することができるはずです。すると、それらのものは実行コンテキストを知っている必要がありますが、これは必ずしも静的に検出可能ではない場合があります。

また、そのようなメカニズムではsignal()に相当するイベント通知処理がブロック中のタスクとその実行コンテキストを覚えておく必要があります。そのリストを保持するために追加のメモリ確保は許容されるのか、あるいは回避されるべきでしょうか。また、そのようなリストはおそらく型消去されることになります。すると型消去そのもののために動的確保が必要となる可能性があります。

この文書では、このようなまず浮かび上がった疑問をSG1に投げかけています。

  1. SG1はこのような汎用同期APIにより力を入れるべきか?
  2. P0073R2のeventは使用可能な抽象であるか?
  3. あるスレッドで実行中の処理内から、それを包含する最上位の実行コンテキスト(スレッドに対するスレッドプールなど)を検出するような実装を要求できるか?
  4. 実行コンテキストの完全なチェーンは必要か?
  5. 標準ライブラリにあるブロックする可能性がある関数のリストは必要か?
  6. 実行エージェントのペアのためのカスタマイズを許可するか?

この文書はこれらの解答を受けて、さらなる検討や作業を進めるつもりのようです。

P2883R0 offsetof Should Be A Keyword In C++26

offsetofキーワード化し、offsetofマクロの機能を言語機能とする提案。

この提案には、上の方のP2654R0の項も関連しています。

offsetofはマクロなのでモジュールからエクスポートすることができず、stdモジュールからもエクスポートされません。そのため、使用する場合は引き続き#include <cstddef>が必要となりますが、このヘッダはインポート可能であると指定されてはいないため、ヘッダユニットとしてインポートすることもできません。

この提案は、主にこの問題の解消のために、offsetofをキーワードとしてその機能を言語機能に昇格することでヘッダのインクルードによらず使用可能にしようとするものです。

これによって、offsetofの他の問題点の解決を図ることができます

  • マクロ引数に,が含まれていると展開がバグる
  • offsetofの第一引数に標準レイアウトクラス型以外を指定した場合、もしくは第二引数にデータメンバ以外を指定した場合のUB
  • 結果をポインタ演算に使用するとUBとなる

この提案によって解消される問題の例

import std.compat;  // offsetofはエクスポートされない
import <cstddef>;   // ポータブルではない
#include <cstddef>  // ok、これが最善

template <typename A, typename B>
struct Test {
  int data;
};

using TestInts = Test<int, int>;
static_assert(offsetof( TestInts, data) == 0); // ok

static_assert(offsetof( Test<int, int> , data) == 0); // error、型名にカンマが含まれる
static_assert(offsetof((Test<int, int>), data) == 0); // error、()で括ることをサポートしていない

class S1 {
  int data;
public:
  S2(int n) : data{n} {}
};

struct S2 {
  int data;
  void f();
};

static_assert(offsetof(S1, data) == 0); // UB
static_assert(offsetof(S2, f) == 0);    // UB

struct T {
  int i;
  double j;
  short k;
  void *p;
};

int main() {
  using namespace std;

  T x = {};
  size_t y = offsetof(T, k);
  short *p = (short*)((byte*)&x + y); // このx + yのポインタ演算はUB

  *p =123;
  printf("%d", x.k);
}

この提案では、キーワード化されたoffsetof演算子となり、上記のUBをエラーにしたり結果をポインタ演算に使用できるようにするなどの意味論が整備されます。

P2884R0 assert Should Be A Keyword In C++26

assertキーワード化し、assertマクロの機能を言語機能とする提案。

この提案には、1つ前のP2883及び上の方のP2654R0の項も関連しています。

assertはマクロなのでモジュールからエクスポートすることができず、stdモジュールからもエクスポートされません。そのため、使用する場合は引き続き#include <cassert>が必要となりますが、このヘッダはインポート可能であると指定されてはいないため、ヘッダユニットとしてインポートすることもできません。

この提案は、主にこの問題の解消のために、assertをキーワードとしてその機能を言語機能に昇格することでヘッダのインクルードによらず使用可能にしようとするものです。

1つ上のoffsetofと同様にキーワード化されたassert演算子となり、それによって式に,が含まれている場合に展開がバグる問題が解消されます。

この提案によって解消される問題の例

import std.compat;  // assertはエクスポートされない
import <cstddef>;   // ポータブルではない
#include <cassert>  // ok、これが最善

int main() {
  assert( std::is_same_v<int, int> );         // ng、マクロ引数が多い(カンマが含まれている)
  assert((std::is_same_v<int, int>));         // ok
  assert( std::vector{1, 2, 3}.size() == 3 ); // ng、マクロ引数が多い(カンマが含まれている)
  assert((std::vector{1, 2, 3}.size() == 3)); // ok

  int x = 0;
  int y = 0;

  assert( [x, y]{ return test(x, y);}() ); // ng、マクロ引数が多い(カンマが含まれている)
  assert(([x, y]{ return test(x, y);}())); // ok
}

このassertとよく似た機能は、関数中の不変条件の表現とチェックのための構文としてC++26予定の契約プログラミング機能においても議論されています。ただし、契約プログラミングにおいては契約の構文をどうするかまだ決定されていないため、その検討の一環としてもassertキーワード化をここで議論してSG21(契約プログラミング作業グループ)にフィードバックすることもこの提案の目的の一つです。

また、assert演算子化した場合には、その有効無効の切り替え(NDEBUGマクロによるなど)の方法の可否や、ビルドモードのよる構文評価の有無などの問題と向き合う必要がありますが、それらの細かい議論はSG21で提案されている実行時アサーションの機能の議論と合流させる(ここでは取り扱わない)ことを推奨しています。

P2886R0 Concurrency TS2 Editor's report

Concurrency TS v2の最新のドラフト(N4953)の変更点をまとめた文書。

次の提案と編集上の修正が適用されているようです。

P2887R0 SG14: Low Latency/Games/Embedded/Finance/Simulation virtual meeting minutes to 2023/05/11

2023年5月11日に行われたSG13の議事録。

P2888R0 SG19: Machine Learning Virtual Meeting Minutes to 2023/05/12

2023年3月9日と4月13日に行われたSG19の議事録。

機械学習に関連のある提案のレビューが行われているようです。

P2889R0 Distributed Arrays

複数の翻訳単位で分散している配列を1つの配列として扱う機能の提案。

C++のリンカには、複数の翻訳単位からシンボルを取り出してそれを1つの配列にまとめる機能があります。それは例えば、ある同じ名前の外部リンケージを持つ配列に対して各翻訳単位でそれぞれ初期化した後、リンカが最終的なプログラムを出力する際にそれぞれの翻訳単位(オブジェクトファイル)で初期化されている配列要素を何かしらの方法でマージします。

この機能はC++単体テストフレームワークにおいて活用されており、各翻訳単位で定義されている単体テストを集めて管理するグローバルなシングルトンの実装に使用されます。このようなグローバルな分散配列の初期化は、コンパイル時(リンク時)にはその初期化子は判明していますが、言語サポートなどはないのでその初期化は実行時に行われます。

registerというキーワードでこの分散配列を指定することにすると、例えば次のコードのような雰囲気のことが行われています

/// test_framework.h
using test_func = bool (*)();

// 分散配列g_testsの宣言
extern const test_func g_tests[register];
/// always_pass.cpp
#include "test_framework.h"

bool always_pass() {return true;}

// 分散配列に要素を追加
const test_func g_tests[register] = {always_pass};
/// always_fail.cpp
#include "test_framework.h"

bool fail1() {return false;}
bool fail2() {return false;}

// 分散配列に要素を追加
const test_func g_tests[register] = {fail1, fail2};
/// main.cpp
#include "test_framework.h"

int main() {
  // 分散配列g_testsは次の要素を含む : {always_pass, fail1, fail2}
  // ただし、その順序は不定
  for (test_func f : g_tests) {
    if(!f()) return 1;
  }

  return 0;
}

この機能は他のところでは、C++の実装(コンパイラ)が静的初期化子やスレッドローカルストレージ、例外処理テーブルなどを実装する際に使用されるほか、動的リフレクションの実装にも使用されているようです。

単体テストフレームワークにおける利用例はこの機能が有効に利用されている大きな例であり、その他の利用例もこの機能の有用性を物語っています。また、この機能をポータブルかつ簡易に利用できるようにすることで、static initialization order fiascoとして知られる問題をプログラマの希望に沿った初期化を確実に行われるように解決することができます。例えば、初期化関数と優先順位のペアからなる分散配列を用意しておき、プログラムの任意のタイミングでそれを優先順位でソートして対応する初期化を実行する、などの方法が可能となります。さらに、この方法は動的ライブラリの初期化にも活用できます。

既存の幅広い利用例や想定される利用法など、分散配列を言語サポートしてポータブルかつ簡易に利用可能にすることにはかなりの価値があり、この提案はそれを提案するものです。

この提案ではまだ具体的な構文が固まっていないようですが__distributed_arrayに分散配列の名前を渡して配列宣言に指定することで、分散配列であることを明示し、その初期化子は1つの配列にリンク時に統合されます。

/// テストフレームワークのヘッダ
using test_case_callback = void();

// テストケースを登録する分散配列
extern test_case_callback* const test_cases[];
/// テストフレームワークを使用するユーザーコード

// 分散配列への単一要素追加
__distributed_array(test_cases)
test_case_callback* const my_test_case = my_test_case_function_1;

// 分散配列への複数要素追加
__distributed_array(test_cases)
test_case_callback* const my_test_case_array[] = {
  my_test_case_function_2,
  my_test_case_function_3,
};
/// テストフレームワークのソース
void run_unit_tests() {
  test_case_callback* const* const test_data = ::test_cases;

  // 分散配列の要素数取得
  size_t const test_count = std::distributed_array_size(test_data);

  // 分散配列をspanで参照
  std::span const tests = std::span(test_data, test_count);

  // 分散配列のイテレーション
  for (test_case_callback* const test : tests) {
    test();
  }
}

分散配列そのものの宣言はexternによる要素数不明の配列であり、これは新しい構文やキーワードを追加する負担を避けたものです。別の提案(P2268)で提案されていたように(最初の例のように)registerキーワードを使用するなど、別の構文を妨げるものではありません。

分散配列の型に関しては、普通の配列型と区別しておくとコンパイル時の診断が行いやすくなったり、既存の範囲のためのユーティリティを直接分散配列で利用できるようになるなどのメリットがあるため、分散配列専用の配列型を追加することを提案しています。具体的には決まっていませんが、registerキーワードを用いる場合はT[register]T(&)[register]のようなものが考えられます。

静的な変数(配列)の定義に対して__distributed_array(A)のように指定すると、その配列は分散配列Aの部分定義として機能します。最終的なプログラムでは、それら部分定義は全てマージされてAの定義となります。分散配列がextern cv T A[];のように定義されている時、その部分定義はcv T型の変数もしくはcv T型の要素を持つ配列宣言であり、かつ名前空間スコープもしくは静的メンバ変数である必要があります。

分散配列のサイズを取得するためには、ここではstd::distributed_array_size()というライブラリ関数を利用していますが、sizeofや専用言語機能による取得方法も考慮されており、まだ決定していません。

分散配列の要素の順序はほぼ不定ですが、1つの翻訳単位で定義された(非inline)部分定義はその定義の順番で順序づけされます。

有効な利用例を見出せないため現在のところはthread_localな分散配列は提案されていません。

動的ライブラリにおいて実行時にそれらに含まれる分散配列を統合することはおそらくほぼ不可能であり、動的リンクはC++標準の範囲外のことであるため提案されていません(Windowsでは問題とならないとのことです)。ここではあくまで、静的なリンク時点で定義される分散配列を提案しており、静的ライブラリでは同じ分散配列定義は統合されますが、動的ライブラリにおいてはそれらシンボルは競合するとみなすことにすることを提案しています。言い換えると、動的リンカは分散配列を扱う必要はありません。これは既存のリンカの動作と一致しているようです。

この提案は、EWGIの初期レビューで好意的に受け止められており、さらなる作業が続行される予定です。

P2891R0 SG16: Unicode meeting summaries 2023-01-11 through 2023-05-10

SG16(ユニコード関連の作業部会)の2023年1月11日から5月10日の間のミーティングの議事録

9回分のミーティングの参加者や議題、発言、投票行動などが記録されています。

P2892R0 std::simd Types Should be Regular

提案中のstd::simdregularであるようにする提案。

C++26に向けて提案中のstd::simd型はoperator==が要素ごとの比較を行った結果をboolではなく要素ごとの比較結果を1/0で保存したsimd_mask型として返します。そのため、regularコンセプトを満たしません。

これは例えば、std::simd型をメンバとして保持するクラスにoperator==をデフォルト実装しようとした時に問題となります。

using uint32_4v = std::fixed_size_simd<std::uint32_t, 4>;

class Color {
public:
  bool operator==(const Color &) const = default;
private:
  uint32_4v data_;
};

void f() {
  Color a, b;
  
  ...
  
  if ( a == b ) // ERROR: operator==は削除されている
}

std::simdは値セマンティクスを持つ値型として設計されており、その演算をSIMD演算にエンコードする以外の部分では組み込み型と同様に動作することを意図しています。regularコンセプトはそのような値型が満たすべき基本的な性質です。また、regularな型には次のような関係があることが期待されます

  1. T a = b; assert(a == b);
  2. T a; a = b;T a = b;は同値
  3. T a = c; T b = c; a = d; assert(b == c);
  4. T a = c; T b = c; zap(a); assert(b == c && a != b);
    • zapはつねに引数の値を変更する

これらの性質は例えばstd::findのように内部で比較を行うような処理が暗黙的に要求していることでもあり、標準ライブラリの中でも一般のライブラリにおいても広く活用されています。

また、在野のライブラリでは多くのSIMD型が提供されており、そこではSIMDレジスタをラップするものから、より大きな数学的なベクトル・行列を表現するもの(DSL)まであります。そこでもoperator==による比較が提供されており、多くの場合は要素ごとの比較を行いその結果を(非boolで)返します。

そのようなライブラリのうち、DSLを目的とするライブラリはstd::simdとは目的が異なっているため、そこからstd::simdへ移行することはまずないと思われるためstd::simdの比較がどう選択されてもそのユーザーにはあまり関係ないでしょう。それ以外のライブラリのユーザーやDSLの実装者はstd::simdへ移行することにはかなりの利点があり、その場合のstd::simdの比較は他のC++の場合と同じ意味論が自然に要求されるでしょう。

C++の標準ライブラリにはstd::simdのようなデータ並列型は存在していませんが、値のシーケンスを表す型はいくつかあり(bitset, vector, array, valarrayなど)、std::valarrayを除いて全ての型がboolを返すoperator==を持っています。

この提案はこれらの背景から、std::simdoperator==をマスクではなくboolを結果として返すように変更し、結果をマスクで取得するのは別のフリー関数によって行うようにすることを提案するものです。

P2893R0 Variadic Friends

friend宣言でのパック展開を許可する提案。

クラステンプレート定義内でテンプレートパラメータに対してfriend指定を行う際、そこにパラメータパックを指定することはできません。

template <typename T>
class Foo1 {
  friend T; // ok
public:
  // ...
};

template <typename... Ts>
class Foo2 {
  friend Ts...; // ng
public:
  // ...
}

この提案はfriend宣言でのパック展開を許可し、パラメータパックに含まれるそれぞれのクラス型に対してfriendを適用するようにするものです。

この提案のモチベーションの一つとして、Passkeyイディオムというテクニックのサポートが挙げられています。

あるクラスCの定義内で他のクラスDfrined指定すると、DCの全てのプライベートメンバへアクセスすることができます。この時、アクセスしたいのがある1つのプライベートメンバだけだったとしても、その公開範囲を制限する方法はありません。

class D;

class C {
  friend D; // DはCの全てのメンバにアクセスできる

  ...
};

Passkeyイディオムは、この時に別のクラスPasskeyを間に挟むことでプライベートメンバの公開範囲を制限するものです。

class D;

class Passkey {
  friend D;
  Passkey() = default;  // PasskeyはDからしか生成できない
};

class C {
  ...

public:
  // Passkeyを生成できないとこの関数を呼べない
  auto do_something(Passkey) {
    // 必要なメンバにだけアクセスする
    ...
  } 
};

プライベートアクセスは関数経由にはなりますが、これによって必要なプライベートメンバだけにプライベートアクセス範囲を絞ることができます。

PasskeyイディオムにおけるPasskeyクラスはテンプレートにすることで使いまわすことができ、そうするとPasskeyを使用する場所でアクセス可能なクラス名が明示されるようになります。

class D;

template<typename T>
class Passkey {
  friend T;
  Passkey() = default;  // PasskeyはTからしか生成できない
};

class C {
  ...

public:
  // Dからのみ呼べる
  auto do_something(Passkey<D>) {
    // 必要なメンバにだけアクセスする
    ...
  } 
};

この場合でもdo_something()にはDからしかアクセスできません。

さらに、複数のクラスに対してアクセスを許可したい場合のためにPasskeyクラスのテンプレートパラメータを可変にすることが考えられます。

class D;
class E;
class F;

template<typename... Ts>
class Passkey {
  friend Ts...; // 現在これができない
  Passkey() = default;  // PasskeyはTs...からしか生成できない
};

class C {
  ...

public:
  // D, E, Fからのみ呼べる
  auto do_something(Passkey<D, E, F>) {
    // 必要なメンバにだけアクセスする
    ...
  } 
};

ただし、現在はfriend宣言におけるパック展開が不可能であるため、これはできません。

提案では他にも、CRTPにおいて基底クラスから派生クラスのプライベートメンバアクセスを許可する場合に派生クラスで可変長テンプレートを使用する場合の例を挙げています。

P2895R0 noncopyable and nonmoveable utility classes

※この部分は@Reputelessさんに執筆していただきました

派生クラスをコピー不可 / ムーブ不可にするユーティリティクラス std::noncopyable, std::nonmovable<utility> に追加する提案。

リソースを複製しない RAII クラスの実装のために、クラスをムーブオンリーに、あるいはムーブもコピーも禁止にしたいことがあります。Boost ライブラリが提供する noncopyable のようなクラスを継承すると、そうした実装を簡単に記述でき、意図も明確になります。この方法はコード検索でも多数ヒットするため、一般的なイディオムであると考えられます。

ただし、boost::noncopyable は、ムーブセマンティクスが導入される C++11 以前に設計されたもので、現在の C++ では、その派生クラスはコピー不可かつムーブ不可になります。これは標準に導入されたコンセプト std::copyable, std::movable と並べたときに違和感があります。C++ のクラスは std::copyable を満たさない場合でも、std::movable を満たすことはできるからです。

Boost の noncopyable の実装概略:

class noncopyable {
protected:
    noncopyable() = default;
    ~noncopyable() = default;
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
};

struct ObjectNonCopyable : noncopyable {};

int main()
{
    std::cout << std::boolalpha;
    std::cout << std::copyable<ObjectNonCopyable> << '\n'; // false
    std::cout << std::movable<ObjectNonCopyable> << '\n'; // false
}

この提案では、コピーを禁止させるためのクラス std::noncopyable と、ムーブおよびコピーを禁止させるためのクラス std::nonmovable をそれぞれ標準ライブラリで提供し、std::noncopyable ではムーブを禁止しないことで、標準のコンセプト std::copyable, std::movable と一貫させることを目指します。

提案の実装概略は次の通りです。

struct noncopyable {
    noncopyable() = default;
    noncopyable(noncopyable&&) = default;
    noncopyable& operator=(noncopyable&&) = default;
};

struct nonmovable {
    nonmovable() = default;
    nonmovable(nonmovable const&) = delete;
    nonmovable& operator=(nonmovable const&) = delete;
};

struct ObjectNonCopyable : noncopyable {};
struct ObjectNonMovable : nonmovable {};

int main()
{
    std::cout << std::boolalpha;    
    std::cout << std::copyable<ObjectNonCopyable> << '\n'; // false
    std::cout << std::movable<ObjectNonCopyable> << '\n'; // true

    std::cout << std::copyable<ObjectNonMovable> << '\n'; // false
    std::cout << std::movable<ObjectNonMovable> << '\n'; // false
}

いずれも Empty base optimization がはたらくため、派生クラスに余分なオーバーヘッドは生じません。

P2897R0 aligned_accessor: An mdspan accessor expressing pointer overalignment

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

mdspanのアクセサポリシーとは、領域へのポインタとインデックスを受け取ってどのようにその領域へアクセスするか(要素を引き当てるか)を指定するポリシークラスです。デフォルトのアクセサ(std::default_accessor<T>)は要素型Tのポインタptrとインデックスidxに対してptr[idx]のようにアクセスしてその結果を返します。

この提案は、この場合に領域ポインタptrstd::assume_alignedに通してからアクセスすることで、mdspanに対してアライメント要件を宣言しつつコンパイラの最適化を適用する能力を与えるものです。

提案しているのはaligned_accessor<T, N>というクラスで、次のようなものです

namespace std {

  // aligned_accessor
  template<class ElementType, size_t the_byte_alignment>
  struct aligned_accessor {
    // オフセット結果に対するアクセサ
    // 領域先頭がアライメントされていても、オフセット結果までそうであるとは限らない
    using offset_policy = default_accessor<ElementType>;
    // 要素型
    using element_type = ElementType;
    // アクセス結果の型
    using reference = ElementType&;
    // データハンドル(参照領域を指定するもの)の型、ほとんどの場合ポインタのこと
    using data_handle_type = ElementType*;

    // 要求(仮定)するアライメント
    static constexpr size_t byte_alignment = the_byte_alignment;

    constexpr aligned_accessor() noexcept = default;

    // 非const ElementTypeからconst ElementTypeへの変換と
    // より大きなアライメントから小さいアライメントへの変換を行うコンストラクタ
    template<class OtherElementType, size_t other_byte_alignment>
    constexpr aligned_accessor(aligned_accessor<OtherElementType, other_byte_alignment>) noexcept;

    constexpr operator default_accessor<element_type>() const {
      return {};
    }

    // 指定したインデックスで要素を引き当てる
    constexpr reference access(data_handle_type p, size_t i) const noexcept {
      // assume_alignedを通して要素アクセス
      return assume_aligned<byte_alignment>(p)[i];
    }

    // 指定したインデックスでオフセットしたデータハンドル(ポインタ)を得る
    constexpr typename offset_policy::data_handle_type
      offset(data_handle_type p, size_t i) const noexcept {
        // pの指す領域はbyte_alignmentでアラインされている(はず)だが
        // その要素p + iの領域はそうとは限らない
        return p + i;
      }

    // 少なくともbyte_alignmentでアラインされているかを取得する
    constexpr static bool is_sufficiently_aligned(data_handle_type p);
  };
}

アクセサクラスはmdspanに指定すると内部で勝手によしなにしてくれるので、通常これを直接扱う必要はないはずです(is_sufficiently_aligned()はアライメントチェックのために使うことがあるかもしれません)。

簡単な使用例

#include <mdspan>
#include <ranges>

// nx4行列でアライメント要求をとるmdspan
template<typename T, size_t byte_alignment>
using aligned_mdspan_Nx4 = std::mdspan<T, std::extents<size_t, std::dynamic_extent, 4>, std::layout_right, std::aligned_accessor<T, byte_alignment>>;

int main() {
  using namespace std::views;

  // float配列を16バイトアライメントにアラインする
  alignas(16) float array1[] = {...};
  alignas(16) float array2[] = {...};

  // 4x4行列として参照
  aligned_mdspan_Nx4<float, 16> mat44_1{array1, 4};
  aligned_mdspan_Nx4<const float, 16> mat44_2{array2, 4};

  // assume_alignedを通していることで、このような計算は最適化されやすくなる
  for (auto [y, x] : cartesian_product(iota(0, 4), iota(0, 4))) {
    mat44_1[y, x] *= mat44_2[y, x];
  }
}

提案文書より、サンプルコード

// float要素の1次元動的mdspan
template<size_t byte_alignment>
using aligned_mdspan =
  std::mdspan<float, std::dextents<int, 1>, std::layout_right, std::aligned_accessor<float, byte_alignment>>;

// 32バイトアライメントを要求するインターフェース
// 例えば、floatで幅8のSIMDを使用するなど
extern void vectorized_axpy(aligned_mdspan<32> y, float alpha, aligned_mdspan<32> x);
extern float vectorized_norm(aligned_mdspan<32> y);

// 16バイトアライメントを要求するインターフェース
// 例えば、floatで幅4のSIMDを使用するなど
extern void fill_x(aligned_mdspan<16> x);
extern void fill_y(aligned_mdspan<16> y);

// Helper functions for making overaligned array allocations.

template<class ElementType>
struct delete_raw {
  void operator()(ElementType* p) const {
    std::free(p);
  }
};

template<class ElementType>
using allocation = std::unique_ptr<ElementType[], delete_raw<ElementType>>;

template<class ElementType, std::size_t byte_alignment>
allocation<ElementType> allocate_raw(const std::size_t num_elements) {
  const std::size_t num_bytes = num_elements * sizeof(ElementType);
  void* ptr = std::aligned_alloc(byte_alignment, num_bytes);
  return {ptr, delete_raw<ElementType>{}};
}

float user_function(size_t num_elements, float alpha) {
  // 32バイトアライメントでメモリを確保
  constexpr size_t max_byte_alignment = 32;
  auto x_alloc = allocate_raw<float, max_byte_alignment>(num_elements);
  auto y_alloc = allocate_raw<float, max_byte_alignment>(num_elements);

  // 32バイトアライメントで領域を参照
  aligned_mdspan<max_byte_alignment> x(x_alloc.get(), num_elements);
  aligned_mdspan<max_byte_alignment> y(y_alloc.get(), num_elements);

  fill_x(x); // 32バイトアライメントから16バイトアライメントへの変換
  fill_y(y); // 32バイトアライメントから16バイトアライメントへの変換

  vectorized_axpy(y, alpha, x);
  return vectorized_norm(y);
}

P2898R0 Importable Headers are Not Universally Implementable

モジュールにおけるインポート可能なヘッダ(importable header)というものを修正する提案。

インポート可能なヘッダ(importable header)とは、非モジュールのヘッダファイルのうち、ヘッダユニットとしてインポートすることができる種類のヘッダのことです。インポート可能なヘッダの#includeは実装によってimportに置換される可能性があります。

ただし、標準ライブラリのC互換ではないヘッダを除いて、C++標準はインポート可能なヘッダが何かを規定しておらず、それは実装定義とされています。

ヘッダユニットはモジュールの一種ではありますが、モジュール宣言によって作成される名前付きモジュールとは異なりあくまでヘッダファイルです。そのため、インポート可能なヘッダと名前付きモジュールには根本的な違いがあります

  1. 名前付きモジュールはその識別と探索のために以前には存在しなかったモジュール名という探索空間を提供するが、ヘッダユニット名はヘッダ名と同じ探索空間を共有する
  2. ヘッダユニットのインポートは、インポートされた先の翻訳単位にプリプロセッサの状態を漏洩する

この違いにより、いくつかの問題が生じています

ヘッダユニットの問題点

1. インポート可能なヘッダとインクルードされるヘッダの識別

現在のC++にはヘッダ名を正確に識別するための仕様やメカニズムがありません。そのため、インポート可能なヘッダの#includeimportに置換した場合にそれが同じヘッダファイルを指しているかすら保証できません。ともすれば異なるファイルを処理してしまったり、あるコンパイラでは意図通りになっていても別のコンパイラでは異なる結果が生じたりする可能性があります。

このことは、#pragma onceが有用であり実質ポータブルでありながら標準化されない(できない)理由にも通じます。#pragma onceでは、何が一度だけインクルードされるべきなのかを指定する方法がなかったため、標準化に至りませんでした。実際、コンパイラによって同じヘッダを区別する方法が異なっています。

2. 依存関係スキャンの依存関係

モジュールのimportプリプロセッサとして処理されるため、あるソースファイルの依存関係をスキャンする場合には少なくともそのファイルのプリプロセスを完了させる必要があります。

その時、ヘッダユニットのインポートはそのプリプロセス状態(マクロ)もエクスポートするため、ヘッダユニットのインポートはその内容によって依存関係スキャンの結果に影響を与える可能性があります。そのため、依存関係スキャン処理は次のいずれかの対応をしなければなりません

  • 依存関係スキャンへの入力としてヘッダユニットのビルド済モジュールインターフェースを受け入れる
  • 一貫した処理のため、ヘッダユニットからのコマンドライン引数を受け入れて現在のプリプロセッサの状態を更新し、その後にヘッダユニットのプリプロセッサ状態をマージする

clang/MSVCの初期の依存関係スキャン実装は、あたかもヘッダのインクルードを行なっているかのようにヘッダユニットのインポートを処理しているようです(つまり、どちらの方法でもない)。そのアプローチはモノリシックリポジトリのような環境ではうまく動きますが、ビルド済バイナリが依存関係として入ってくるようなより複雑なビルドではインポート可能なヘッダが上手く扱われることを保証できません。

そのような場合は、次のように依存関係スキャンプロセスへの入力を補うことで解決が図れます

  • 既知のインポート可能なヘッダの全てのリストを依存関係スキャン処理への入力とする
  • インポート可能なヘッダのローカルプリプロセッサの引数を依存関係スキャン処理への入力とする
  • 現在の翻訳単位のローカルプリプロセッサの引数を依存関係スキャン処理への入力とする

しかし、この代償としてある翻訳単位の依存関係に大きなボトルネックが生じます。つまり、これらの入力(インポート可能なヘッダのリストやプリプロセッサ引数)のどれかを少しでも変更すると、そこ以降の依存関係スキャン結果は全て無効になり、影響を受ける翻訳単位は全て依存関係スキャンのやり直しとその結果を受けてのリビルドが必要となります。これはC++プロジェクトのビルドに大きな追加のコストを導入します。

3. プリプロセッサ状態についての推論

名前付きモジュールのimportはマクロをエクスポートしないため、インポート先の翻訳単位のプリプロセッサ状態に何ら影響を与えません。

一方でヘッダユニットはそうではなく、ヘッダユニットのインポートを使用している場合にそこからエクスポートされるマクロについて知るには、そのヘッダユニットがどのようにコンパイルされるか(コンパイルオプション)を調べて、その状態がどのようにインポート先のプリプロセッサ状態にマージされるかを調べるためにビルドシステム(コンパイラ/依存関係スキャナ)の仕様を知りに行く必要があります。

この時、#includeimportに置換される場合、プログラマはどのヘッダがその対象なのかを知らなければプリプロセッサの状態を推測することができません。コンパイラによってその方法・基準が異なるという状況では、それはより困難となります。

このことは特にC++の教育時に問題となります。このような質問に対してどう答えるかは使用するビルドシステムにも依存することを考えると、この問題はより深刻です。

ヘッダユニットの目的

ヘッダユニットという仕様の目標は次のようなものでした

  • 事前コンパイル済ヘッダの経験を教訓として、共通の一貫した仕様を策定する
  • Clang Header Moduleの経験から、一貫した仕様を策定する
  • モジュールへの移行を容易にする

これらの目標そのものには価値があり重要な目標ではありますが、現在のヘッダユニットの仕様ではこれらのことを達成できていません。

事前コンパイル済ヘッダにあった制限が受け継がれていない

事前コンパイル済ヘッダの実装はコンパイラによってまちまちでしたが、共通のサブセット要件が存在していました。それは、事前コンパイル済ヘッダの内容はそれを使用する翻訳単位内のコードの影響を受けないというものです。

すなわち、あるヘッダファイルの#includeは事前コンパイル済ヘッダが利用可能であるかどうかでその動作が変わることがありません。

Clang Header Moduleの制限

Clang Header Moduleはそのヘッダがそれを使用する場所のプリプロセッサ状態の影響を受けないことを前提として実装されてており、それに違反するとエラーとなりこれはユーザーの責任とされていました。言い換えるとこれは、C++言語のサブセットを採用するコードベースに対して適用されます。

C++モジュール仕様におけるヘッダユニットにおいてはそのように適用可能な場所に言語サブセットを要求することは当然回避されるため、Clang Header Moduleのこの側面の経験をC++エコシステム全体にどのように適用するかという問題には対処していません。

ヘッダユニットが移行の役に立たない

事前コンパイル済ヘッダとClang Header Moduleの両方を経験したコードベースにおいては、どちらの場合の解釈もあくまでヘッダのインクルードであり、ツールが行うことはインクルードの最適化だと理解されています。

そのため、それら及び#includeimportに切り替えられた時の解釈は、インポート時点でのプリプロセッサの状態に影響されることなくヘッダをインクルードするセマンティクスに依存することになります。

しかし、よく使用するヘッダ(ソース)ファイルで新しい構文を使用するとそのファイルの使用可能性は、その構文をサポートするコンパイラとビルドシステムのみに制限されます。この時点で、インポート可能なヘッダに移行するか名前付きモジュールに移行するかの労力の差はそれほど変わりません。

実際、モジュールセマンティクスを必要とするコードベースにとっては与えられたヘッダのラッパモジュールを自動的に生成して、その名前付きモジュールでヘッダのエンティティをエクスポートすることはそれほど難しくないでしょう。それだけだとマクロをサポートできませんが、マクロは専用のヘッダにまとめて配置しそれをインクルードするようにすることは難しくなく、これらを組み合わせることは名前付きモジュールのための良い移行経路となるはずです。

ヘッダユニットのパフォーマンス上の利点は、ボトムアップに採用した場合に限られる

ヘッダユニットの初期の経験では、ヘッダユニットの採用が最も下位から(ネストした依存関係の内側から)開始されない場合にコンパイラのパフォーマンスが低下することがわかっています。

これは、同じヘッダのインクルードを含むようなより大きなヘッダ単位でインポートしてしまうと、それぞれのヘッダユニットのビルドにおいて同じヘッダを複数回処理することになってしまうためです。インクルードの場合、インクルードガードの処理が効率化されたことでそのスキップが効果的となるようです。

またそのような大きなヘッダをインポートした側では、同じヘッダをインクルードしていることで重複するエンティティ情報の統合を行う必要も出てきます。

これらのことを受けてこの提案では、インポート可能なヘッダの意味論を別に指定せず、インポート可能なヘッダを実装がインクルード処理を最適化する方法として指定することで、現在起きている問題を解決し元々の目標を達成することを提案しています。

このアプローチの重要な点は、ヘッダファイルの取り込みに関してはインクルードのセマンティクスが依然として標準的かつ期待される動作であり、実装に対して、そうすることが有利な場合に異なることを行う許可を与えて実装がその最適化に関する独自の注意事項を定義できるようにすることにあります。

この提案は、SG15の議論において方向性に同意が得られれば、このための文言を提供する改訂版を提出する予定です。

P2901R0 Extending linear algebra support to batched operations

P1673で提案中のBLASベース線形代数ライブラリに、バッチ操作のサポートを追加する提案。

BLASAPIは基本的に1関数につき1つの計算を行います。例えば、倍精度実数行列の行列積を計算するcblas_dgemm()という関数は、入力として3つの行列A, B, Cとその係数alpha, betaを受け取って、C = alpha * AB + beta * Cのような計算を行うものですが、これは3つの行列を使った1つの行列積を計算するものです。行列のサイズ(行数/列数)はA, B, Cで一貫している必要はありますが任意であり、問題を1つの行列に落とし込めればこれだけでも並列計算が可能ではあります。

対して、BLASにおけるバッチ操作(batched operation)では1つの関数呼び出しで複数の行列を使った複数の計算を行うものです。1つの行列に問題を落とし込むことができず独立した複数の行列で独立に同じ計算を行う必要がある場合、バッチAPIを用いると個別の関数呼び出しを(並列に)繰り返すよりも効率的に行列計算を行うことができます。

例えば、cblas_dgemm()に対応するバッチ操作はcblas_dgemm_batch()という関数で、行う計算自体は同じですが、パラメータは全て配列で受け取り複数の行列積を1つの関数呼び出しで並列実行します。実際どのように実行されるかは実装によるのですが、個別の関数を独立して呼び出すよりも効率的に実行されることが期待でき、実際そのように実装されます。

現在P1673で提案され作業中のBLASベース線形代数ライブラリはBLASの基本部分APIに対応するもので、バッチ操作は含まれていません。この提案はそこにバッチ操作も入れようとするものです。

バッチ操作の利点は次のようなものです

  • 同じ処理による大量の問題を1度に解くというユーザーの意図を明らかにすることで、単一の小さな問題を解く処理がもつものよりもはるかに大きな並列化・ベクトル化の機会を得られる
  • 個別の問題をBLAS APIに引き渡すために関数呼び出しの引数として表現することによるオーバーヘッドを償却できる
    • 関数呼び出しごとの引数チェックの償却など
  • バッチ引数に関する制約や仮定をインターフェースによって表現し、それに応じた計算の効率化を行える
    • メモリアクセスパターンの改善
    • 共通のデータの読み込みの再利用
      • 全て同じ係数を使用するcblas_dgemm_batch()における係数alpha, betaの再利用(ブロードキャスト)など
    • 潜在的に共通する計算の再利用
  • バッチ操作はBLASが利用されるさまざまな分野で計算効率化の役に立つ
  • NVIDIA, AMD, intelなどのハードウェアベンダはバッチ操作を実装したBLAS実装とそれを高速に実行するハードウェアを提供している
  • MAGMAやKokkosなどのオープンソースライブラリはクロスプラットフォームでバッチ操作を提供している

この提案では、std::mdspanで入力を表現することによってcblas_dgemm()に対するcblas_dgemm_batch()のようにAPI名を分岐する必要はなく、関数に対する要求事項の変更のみによってP1673のAPIを拡張できるとしています。

また、バッチ操作の実装では基本的に使用する行列のサイズやストライドは行列ごとに可変とすることができるようですがそれを考慮すると効率化が妨げられるため、この提案では基本のケースである全ての行列のサイズとストライドは等しい場合のみをサポートすることにし、その効率化に焦点を当てています。

さらに、バッチ操作の入力行列(複数)の表現についても自由度があり(配列の配列、ストライドによる分離、インターリーブ)、この提案では効率性の観点からストライドとインターリーブのみをサポートすることにしています。それはまたstd::mdspanのカスタムレイアウトによって表現可能であり、これによってインターフェースを1つにまとめることができます。

おわり

この記事のMarkdownソース