[C++]WG21月次提案文書を眺める(2024年05月) - 地面を見下ろす少年の足蹴にされる私

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

文書の一覧

全部で139本あります。

もくじ

N4983 WG21 agenda: 24-29 June 2024, St. Louis, MO, USA

2024年6月にセントルイスで行われた会議のアジェンダ

P0260R9 C++ Concurrent Queues

標準ライブラリに並行キューを追加するための設計を練る提案。

以前の記事を参照

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

  • 提案する文言の拡充
  • capacityの修正
  • push()/pop()の文言に"strongly happens before"を使うように変更
  • discussion pointsを追加
  • コンストラクタとデストラクタが同じスレッドに戻ることに関する段落を削除
  • try_*はブロックしなくなった
  • try_*のspurious failureに関する文言を追加
  • キューを事前に埋めるbounded_queueのコンストラクタを削除
  • P3282への参照を追加
  • try_push(T &&, T &)に関する議論をHistoric Contentsに移動

などです。

P0843R12 inplace_vector

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

以前の記事を参照

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

  • 以前のリビジョンを復帰して、条件付きconstexprを有効化
  • R8で削除されたconstexpr対応の議論を再導入

などです。

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

P0963R2 Structured binding declaration as a condition

構造化束縛宣言を条件式を書くところで書けるようにする提案。

以前の記事を参照

このリビジョンでの変更は、分解前に条件をチェックするという評価順序を規定しそれについての議論を追加したことと、提案する文言の改善などです。

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

P1000R6 C++ IS schedule

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

このリビジョンはR5のものとほぼ同じですが、決定済みの会議開催地の地名が記載されるようになっています。

P1083R8 Move resource_adaptor from Library TS to the C++ WP

pmr::resource_adaptorをLibrary Foundermental TSからワーキングドラフトへ移動する提案。

以前の記事を参照

このリビジョンでの変更は、resource-adaptor-impに可変長引数コンストラクタを追加した事などです。

この提案は現在LWGでレビュー中です。

P1112R5 Language support for class layout control

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

以前の記事を参照

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

  • Cでは構造体メンバの宣言順調整でこの問題を解消できるが、C++ではそれは実行可能ではないことについての説明の追加
  • リフレクションではこの問題を解決できないことについてのセクションを追加
  • Design principlesセクションを追加
  • evalストラテジーを追加
  • smallestsmallに変更し、アルゴリズムの説明を追加
  • 対象読者を追加
  • 文言作成に関する戦略を追記

などです。

今回レイアウトに関する指示を行う方法が1つ追加されました

  • layout(eval(consteval_func))
    • 元のレイアウトをエントリーの配列として取得し、オフセットフィールドを変更できるconsteval関数を呼び出す

layout(eval(func))に渡すconsteval関数は、レイアウトを変更する場合にtrueを返し、変更しないならばfalseを返す必要があります。そして、その引数には次のような構造体の配列が渡されます

// クラスの元のレイアウトについての、各メンバ毎の情報を持つ
struct layout_entry {
  enum type_id { base, member, tech };
  type_id type;
  string_view name; // メンバ名
  size_t index;     // 宣言順のインデックス
  size_t size;      // sizeof()の結果の値
  int alignment;    // alignas()の結果の値
  bool noua;        // [[no_unique_address]]の有無
  bool fixed;       // ムーブ可能かどうか
  size_t orig_offset;    // 元のレイアウトにおけるオフセット
  mutable size_t offset; // 変更するオフセット(初期値はorig_offsetの値)
};

// layout(eval(f))に渡す関数の例
consteval bool my_layout_func(span<const layout_entry> entries);

これによって、プログラマはかなり詳細にそのオフセットを制御することができるようになります。

この提案はこれ以上追求しないことがEWGで決定されたようです。

P1144R11 std::is_trivially_relocatable

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

以前の記事を参照

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

  • この提案に準拠済みのライブラリについての参照の追加
  • uninitialized_relocate_nのテンプレートパラメータのためにNoThrowInputIterator要件を追加
  • P3279への相互参照を追加

などです。

P1255R13 A view of 0 or 1 elements: views::nullable And a concept to constrain maybes

任意のオブジェクトやstd::optional等のmaybeモナドな対象を要素数0か1のシーケンスに変換するRangeアダプタviews::maybe/views::nullableの提案。

以前の記事を参照

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

  • views::maybeを削除し、std::optionalrange化を好む
  • std::maybeコンセプトの追加
  • std::optionalやポインタ型などmaybee型のためのフリー関数を追加

などです。

std::maybeコンセプトは次のような単純なものです

template <class T>
concept maybe = requires(const T t) {
  bool(t);
  *(t);
};

これに対して次のフリー関数を追加します

template <class T, class R = optional<decay_t<T>>>
constexpr auto yield_if(bool b, T&& t) -> R {
  return b ? forward<T>(t) : R{};
}

template <maybe T,
          class U,
          class R = common_reference_t<iter_reference_t<T>, U&&>>
constexpr auto reference_or(T&& m, U&& u) -> R {
  static_assert(!reference_constructs_from_temporary_v<R, U>);
  static_assert(!reference_constructs_from_temporary_v<R, T&>);
  return bool(m) ? static_cast<R>(*m) : static_cast<R>((U&&)u);
}

template <maybe T,
          class U,
          class R = common_type_t<iter_reference_t<T>, U&&>>
constexpr auto value_or(T&& m, U&& u) -> R {
  return bool(m) ? static_cast<R>(*m) : static_cast<R>(forward<U>(u))
}

template <maybe T,
          class I,
          class R = common_type_t<iter_reference_t<T>,
          invoke_result_t<I>>>
constexpr auto or_invoke(T&& m, I&& invocable) -> R {
  return bool(m) ? static_cast<R>(*m) : static_cast<R>(invocable());
}

P1306R2 Expansion statements

コンパイル時にステートメントをループ生成することのできる、展開ステートメントの提案。

展開ステートメントとは、通常のrange(配列やstd::vector等コンテナ)に加えてタプルやクラス、波かっこのリストを反復することのできるコンパイル時のループ構文であり、ループの結果としてはループ本体のコード(ステートメント)をループの度にその場に展開していきます。

例えば次のような拡張ステートメント

auto tup = std::make_tuple(0, ‘a’, 3.14);
template for (auto elem : tup) {
  std::cout << elem << std::endl;
}

次のようにコードをべた書きしたのと等価になります

auto tup = std::make_tuple(0, ‘a’, 3.14);
{
  auto elem = std::get<0>(tup);
  std::cout << elem << std::endl;
}
{
  auto elem = std::get<1>(tup);
  std::cout << elem << std::endl;
}
{
  auto elem = std::get<2>(tup);
  std::cout << elem << std::endl;
}

また展開ステートメントでは、波かっこリストが特別扱いされており、上記のコードは次のように書くこともできます

template for (auto elem : {0, 'a', 3.14}) {
  std::cout << elem << std::endl;
}

この波かっこリストは初期化子リストではなく、この波かっこリストは初期化を行わずに各要素に対して直接ループが展開されます。

展開ステートメントは次のものに対して使用できます

  • 構造化束縛可能なクラス型のオブジェクト
    • プレーンな構造体やタプルなど
  • constexprな範囲オブジェクト
    • 主にstd::vector
  • 波かっこで区切られた式のリスト(展開初期化子リスト、"expansion-init-lists")
    • 例えば、パラメータパックも{pack...}とすることでループ対象になる

展開ステートメントはループな風を装っていますが、実際のところコンパイル時にループによってステートメントを生成していくのではなく、予めイテレーション対象の各要素によって生成されたステートメントの列として、範囲for同様に言語組み込みのマクロのような形で、一発で置換されます。とはいえ、結果のコードのインスタンス化順序や実行された時の実行順はあたかもループによって順番にステートメントが生成されていったのと同じように見えます。

この展開ステートメントは特に静的リフレクション機能と非常に相性が良い機能です。P2996R5より、列挙値を列挙値名の文字列に変換するサンプルコード

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  template for (constexpr auto e : std::meta::enumerators_of(^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::identifier_of(e));
    }
  }

  return "<unnamed>";
}

enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

^Eは列挙型Eからそのリフレクション情報を取り出しており、それに対するstd::meta::enumerators_of()は列挙型の各列挙子のリフレクション情報を取り出しています。ここでのイテレーション対象estd::meta::infoの値で、[:e:]スプライシング)によって対応する列挙値の定数値に変換することで、展開ステートメントの各ステートメントは列挙型Eの列挙子1つに対してマッチするif文を生成します。

std::meta::identifier_of()はリフレクション情報からそれに対応する識別子名を文字列で得るもので、ここではeは列挙子を映したものなので、列挙子名がコンパイル時文字列で得られます。

このtemplate for以外のものはP2996の静的リフレクション提案ですべて用意されているものです。

P1494R3 Partial program correctness

因果関係を逆転するような過度な最適化を防止するためのバリアであるstd::observable()の提案。

以前の記事を参照

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

  • フリースタンディングでのサポートを要求
  • 実装可能性を明確化
  • I/O関数を暗黙的なチェックポイントとして扱うようにする代替案についての説明の追加

などです。

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

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

以前の記事を参照

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

  • ベクトル化可能な型として、C++23の拡張浮動小数点数型を追加
  • "selected indices”と“selected elements”の定義を改善
  • ABIタグの意図を紹介する文言の改善
  • sizeを呼び出し可能として一貫して使用
  • 不足していたreducetype_identity_tを追加
  • basic_simd_maskのデフォルトテンプレート引数をnative-abiに修正
  • simd_maskのデフォルトテンプレート引数をsimdと一貫するように修正
  • <simd>ヘッダを追加するのに必要な変更を追加
  • 2つの投票結果を追加
  • simd_size(_v)を説明専用にする
  • reduce_min_indexreduce_max_index事前条件を復元

などです。

この提案は現在、LWGでレビュー中です。

P2019R6 Thread attributes

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

このリビジョンでの変更は、安全性向上のためにスレッド属性引数が左辺値参照にならないように制約を追加したことです。

現在のリビジョンでの使い方は次のようになっています

void f(int);

int main() {
  std::jthread thread{
      std::thread::name_hint{"Worker"},       // スレッド名の指定
      std::thread::stack_size_hint{512*1024}, // スタックサイズの指定
      f, 42 // 実行する処理とその引数
  };

  return 0;
}

P2034R4 Partially Mutable Lambda Captures

ラムダ式の全体をmutableとするのではなく、一部のキャプチャだけをmutable指定できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、Meta-Motivationを追記したことと、いくつかのサンプルを改善したことなどです。

メタモチベーションでは、constな変数は正しく扱うのが簡単で間違って扱うのが難しいはずであり、ラムダキャプチャの文脈でconstをシンプルかつ正確に指定できるようになすることはプログラムの安全性とセキュリティを向上させるものである、としています。

P2079R4 System execution context

ハードウェアの提供するコア数(スレッド数)に合わせた固定サイズのスレッドプールを提供するSchedulerの提案。

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

  • 設計上の考慮事項と目標をさらに追加
  • さまざまな置き換え可能性オプションの比較を追加
  • 置き換え可能性ABI標準化のモチベーションを追加
  • 置き換え用ABIの例を追加
  • 生存期間保証を強化
    • system_contextがその上で起動されたすべての作業よりも長く生存しなければならないことが明記され、破棄時に未完了の作業がある場合は std::terminateが呼び出されるようにした

などです。

置き換え可能性(replaceabiilty)とは、system_contextの実装をユーザーが置き換えられるようにすることをサポートする機能の事です。これは、厳密なシングルスレッドしか利用可能ではない組み込み環境やoneTBBのようなベンダー固有実装の利用など、多様な実行環境に対応するためです。

この方法としては3つの方法が考えられます

  • コンパイル時の置き換え
  • リンク時の置き換え
  • 実行時の置き換え
方法 メリット デメリット
リンク時 * 標準の先行例あり: operator new の置換と類似している
* 予測可能性が高い: アプリケーション全体に適用されることが保証される
* リンク時最適化により、型消去や間接参照をある程度削減できる可能性がある
* ABI の定義が必要となるため、型消去や非効率性が発生する可能性がある
* 共有ライブラリを使用する場合、異なる置換バージョンが混在する可能性があり、正しく動作させることが難しい
* 置換がリンクの順序に依存する可能性がある
実行時 * 標準の先行例あり: std::set_terminate() と類似している
* 共有ライブラリ (例: Windows の DLL における C++ 標準ライブラリの同一バージョン) を使用したアプリケーションで、一貫した動作を容易に実現できる
* 1つのプログラムに system scheduler の複数の実装を含めることができる
* system scheduler の置換と、それを用いた作業の生成との間で競合状態が発生する可能性がある
* ABI を介した処理が必要となるため、リンク時最適化ができない
* 異なる実装が起動時に system scheduler のためのリソースを割り当てた後、main の開始時に実装が置換される可能性がある (主に QOI の問題)
* 安全性を確保するため、およびユーザーが明示的に正しい処理を行うために、厳密なライフタイムと所有権の制御が必要となる
コンパイル * ユーザーは、あらゆる場所で利用できる型定義を使用して置換を行うことができ、簡単に切り替えることができる
* ODR 違反が発生する可能性がある
* 同一プロセス内の異なるバイナリ間で共有できない

提案では、コンパイル時の置き換えはsystem_contextの重要な設計原則の一つである、オーバーサブスクリプションを回避するためにアプリケーション全体で共通な1つの実行コンテキストを持つ(ためのもの)、というのを破ってしまうため有効なオプションではないとしており、実行時かリンク時のどちらかがオプションとして残っています。

P2413R1 Remove unsafe conversions of unique_ptr

std::unique_ptrにおける、危険な暗黙変換を禁止する提案。

以前の記事を参照

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

  • 変換先の型にdestroying operator deleteがある場合でも変換を許可する
  • default_delete::operator()を制約する
  • 生ポインタを取るstd::unique_ptrメンバ関数を制約する
  • 不完全型を処理する
  • 破壊的変更を文書化

などです。

P2434R1 Nondeterministic pointer provenance

現在のC++のポインタ意味論をポインタのprovenanceモデルに対して整合させるための提案。

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

  • P2318R1の分析と提案に関する議論を明確化
  • pointer zapへの影響についての調査を追加

などです。

pointer zapへの影響については、皆無ではないものの小さな追加の変更だけで対応できるとしています。

P2689R3 Atomic Refs Bound to Memory Orderings & Atomic Accessors

mdspanの要素にアトミックにアクセスするためのアクセサである、atomic_accessorの提案。

以前の記事を参照

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

  • atomic-ref-boundを算術型・ポインタ型とそれ以外の型用に分離
    • メンバ型deference_typeの定義の有無のため
  • atomic-ref-boundが説明専用であるかどうかについての議論を追加
  • メモリオーダーの指定に関する変換コンストラクタを持たないことについての議論を追加
  • west constスタイルの使用

などです。

P2719R0 Type-aware allocation and deallocation functions

型を指定する形のnew/delete演算子カスタマイズ方法の提案。

ある型について、new/delete演算子をカスタマイズする方法は、クラスの静的メンバ関数として定義するか、グローバルなnew/delete演算子をオーバーライドするかの2択です。new/delete演算子を型ごとにカスタマイズする場合はその型についての知識が必要となりますが、前者の方法だとそれが得られるものの型に対して侵入的にしか定義できません。一方後者の方法は非侵入的に定義できるものの、グローバルなnew/deleteを丸ごと置き換えることになる(型毎にオーバーロード可能なものの、現実には::operator new(void*)を丸ごと置換しているケースがほとんど)ため影響範囲が大きく、複数の翻訳単位(ライブラリ)が独自にそれを行っているとODR違反の未定義動作で厄介なバグを抱え込むことになります。

この提案では、ある型についてのnew/delete演算子の非侵入的なカスタマイズ方法を提案するものです。

この提案によるカスタム方法は、既存のoperator new/deleteオーバーロードがタグ型を取るようになっていることの延長で、ある型Tについてのnew/delete演算子オーバーロードはタグ型としてstd::type_identity<T>を取るようにするものです。そして、new/delete式が演算子を探索する過程を少し調整して、このようなオーバーロードが発見されるようにします。

この提案の前後で、あるnew演算子呼び出し(new式)に対してコンパイラが考慮するnew演算子シグネチャ候補と順番は次のように変化します

現在 この提案
// ユーザーの書くnew演算子
new (args...) T(...)

// コンパイラが考慮する演算子
T::operator new(sizeof(T), args...)
::operator new(sizeof(T), args...)
// ユーザーの書くnew演算子
new (args...) T(...)

// コンパイラが考慮する演算子
T::operator new(sizeof(T), args...)
operator new(sizeof(T), type_identity<T>{}, args…)  // 👈追加
::operator new(sizeof(T), args...)
現在 この提案
// ユーザーの書くdelete演算子
delete ptr

// コンパイラが考慮する演算子
T::operator delete(void-ptr)
::operator delete(void-ptr)
// ユーザーの書くdelete演算子
delete ptr

// コンパイラが考慮する演算子
T::operator delete(void-ptr)
operator delete(void-ptr, type_identity<T>{})  // 👈追加
::operator delete(void-ptr)

配列版も同様です。

セキュリティ向上のために、型毎に異なるメモリ領域に配置する(同じ型のオブジェクトを同じメモリ領域に配置する)というテクニックがあり、これによってType Confusion攻撃に対する耐性を向上させることができるようです。実際に、Appleでは自社開発のOSカーネルでこれを導入することでType Confusion脆弱性の軽減に大きな効果があったとのことです(筆者の方はAppleの方)。

カーネルに限らず、セキュリティが重要なコードベースではこのような手法を取り入れることが合理的な場合がありますが、現在のC++new/deleteオーバーロードはこのような手法を大規模かつシステム全体で達成することをサポートできていません。この提案の内容はそのような手法を達成するための構成要素であり、間接的にC++プログラムのセキュリティを向上させることに繋がります。

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

namespace lib {
  struct Foo { };

  void* operator new(std::size_t, std::type_identity<Foo>); // (1)

  struct Foo2 { };
}

struct Bar {
  static void* operator new(std::size_t); // (2)
};

void* operator new(std::size_t); // (3)

void f() {
  new lib::Foo();  // calls (1)
  new Bar();       // calls (2)
  new lib::Foo2(); // (1)が見つかるがオーバーロード解決に失敗するため、(3)が呼び出される
  new int();       // calls (3)
}

現在コンパイラnew演算子の探索において、まず対象の型(new T{}T)のクラススコープでT::operator new()を探索し、見つかればそれを使用します。見つからない場合、次にグローバルの::operator new()を探索し、見つかればそれを使用します。どちらかが見つかれば例えばそれがエラーでも探索はそれで終了します。

この提案では、グローバルな::operator new()を探索する前に、フリー関数のoperator new()を考慮するようにします。その際のシグネチャは次のものです

operator new(sizeof(T), std::type_identity<T>{}, placement-args...)

Tに対して第二引数でstd::type_identity<T>を受け取るようにオーバーロードしておくことで、この挿入された2ステップ目の探索でそれを使わせることができるようになるわけです。そして、この探索はADLによって行われるため、名前空間内でnew/delete演算子を定義しておくこともでき、これによってODR違反を緩和することができます。

P2758R3 Emitting messages at compile time

コンパイル時に任意の診断メッセージを出力できるようにする提案。

以前の記事を参照

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

  • 他の提案(P2741R3及びP2738R1)が採択されていることを前提として提案を整理
  • すべてのAPIに追加されるタグについて議論を追記

などです。

このリビジョンで導入されたタグとは、エラーメッセージの種別を表すものです。これは、エラーメッセージを発行するかどうかを制御するためのもので、実装定義の方法で特定のタグを持つコンパイル時メッセージ出力関数の出力を無効化することができます。たとえば

std::constexpr_warning(
  "format-too-many-args", // 👈タグの指定
  "Format string consumed {} arguments but {} were provided.",
  current_arg, total);

(fmtライブラリ中で)このようなコンパイル時出力が使用されている場合、-Wno-format-too-many-argオプションによってこの出力を無効化する、のような使い方を想定しています。ただし、この制御方法は実装定義とされています。また、タグに使用できるのはコンパイル時の文字列かつ。大文字小文字と数字アンダーバーくらいに制限することを提案しています。これは、コンパイラフラグとしてタグ文字列に対応する文字列をCLIから指定して制御を行うことを意図しているためです。

このリビジョンでは提案する関数は基本的にこのタグを受け取るようになりました

namespace std {
  // 定数式中でメッセージを出力
  constexpr void constexpr_print_str(string_view) noexcept;
  constexpr void constexpr_print_str(tag-string, string_view) noexcept;

  // 定数式中で警告メッセージを出力
  constexpr void constexpr_warning_str(tag-string, string_view, string_view) noexcept;
  
  // 呼ばれるとコンパイルエラー、指定されたエラーメッセージを出力
  constexpr void constexpr_error_str(tag-string, string_view) noexcept;
}

P2761R1 Slides: If structured binding (P0963R1 presentation)

P0963R1の紹介スライド。

このリビジョンでの変更は明示的ではありませんが、スライドが3ページ分足されています。

P2786R6 Trivial Relocatability For C++26

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

以前の記事を参照

このリビジョンでの変更はロードマップを追加したことです。

P2822R1 Providing user control of associated entities of class types

ADLにおいて考慮される関連名前空間を制御する言語機能の提案。

以前の記事を参照

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

  • クラスに関連エンティティ指定がなされている場合、そのクラスを囲う最も内側の名前空間を関連名前空間として暗黙的に追加しないようにした
  • friendと宣言された関数を発見するADLの動作が影響を受けないようにした
  • 関連エンティティ指定がなされたクラス型の前方宣言の意図されたセマンティクスと、同等の前方宣言を構成する方法を明確化
  • mp_unitsライブラリにおけるADLに関する問題について追記
  • CWG Issue 2857が解決されたことを示すように更新
  • 検討されたオプション(代替案)の長所と短所を説明するために、構文の選択に関するセクションを追加
  • “Proposed Design”セクションを拡張し、予想されるセマンティクスについて詳細に解説する
  • 設計に関するセクションの追加
    • “Naming a class template as an associated entity”
    • “Naming a template template parameter bound to an alias template”
    • “Class template instantiation”
  • 関連エンティティの計算中にテンプレートのインスタンス化を回避しようとするのをやめた
  • 他のエンティティ(非クラス?)の関連名前空間という言葉を個別に定義し、どの名前空間が関連エンティティであり、したがってADLで考慮されるべきかを明示するように提案する文言を変更
  • 参照型を処理するために[basic.lookup.argdep]の文言を修正していたのを取り消し

などです。

P2830R4 Standardized Constexpr Type Ordering

std::type_info::before()constexprにする提案。

以前の記事を参照

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

  • EWGの議論のフィードバックを適用
  • __PRETTY_FUNCTION__の相違点の例を追加
  • ステータスの更新(EWGの構文アクセプトにより、LEWGレビュー待ち)
  • SORT_KEY(x, y)を適切に定義するための更なる検討の追加

などです。

P2835R4 Expose std::atomic_ref's object address

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

以前の記事を参照

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

  • address()の戻り値型にuintptr_tを使用するようにした
    • 戻り値型をconst void*から、Tの修飾をコピーするaddress_return_tに戻した
  • 戻り値型の変更に伴ってサンプルコードやgodboltの例を修正

などです。

この提案はLWGに転送されてレビュー待ちをしています。

P2841R3 Concept and variable-template template-parameters

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

以前の記事を参照

このリビジョンでの変更は提案する文言の改善のみです。

P2846R2 reserve_hint: Eagerly reserving memory for not-quite-sized lazy ranges

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

以前の記事を参照

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

  • size_hintからreserve_hintヘリネーム
  • take_viewは無条件でreserve_hintを提供するように修正

などです。

P2849R0 async-object - aka async-RAII objects

senderチェーンにRAIIリソースを注入するための、非同期オブジェクトの提案。

sender/receiver自体は非同期関数の実装の一部であり、P2300のフレームワークでは非同期関数は値・エラー・キャンセルのいずれかの結果によって非同期的に完了します。

非同期関数の内部で使用されるリソース(非同期オブジェクト)は、非同期処理が単一のローカルなブロックに収まるものではないため、通常手動でメモリを管理する必要があります。

senderアルゴリズムによるチェーンに対して値を注入するには、let_value(value, func)アルゴリズムを使用します。これは、func(value)の結果を(receiverの値チャネルで)返すsenderを返すもので、そこから得られる値(func(value)の結果)は安定したストレージに保持されることが保証されます。

これと同種なものとして、let_stop_source(sender, sender(inplace_stop_source&))というアルゴリズムを考えることができます。これは、senderチェーンに対してinplace_stop_sourceを注入し、なおかつそれに安定的なストレージを提供するものです。この延長で、senderチェーンに対して多様な非同期リソースを注入するめlet_xxxアルゴリズムが個別に考案されつつあります。このことは、このような非同期リソースをsenderチェーンで扱うための汎用的な設計が欠けていることを表しています。

そのような非同期コードでは多くの場合、shared_ptrを使用してアドホックなガーページコレクションが実装されます。このようなアドホックな非同期オブジェクトの非同期的な構築と破棄はある統一的な汎用設計に従っていない場合、汎用的なアルゴリズムからそのような非同期リソースを複数まとめて扱うことを困難にします。

この提案は、sender/receiverを用いた非同期オブジェクトの構築と破棄のルールをライブラリに実装する方法についてを提案するものです。

この提案では、非同期オブジェクトのための3つの基本的な要件について定義しています

  • 非同期構築(async constructio
    • オブジェクトの中には構築中に実行される非同期関数がある
      • ファイルを開く、通信を確立する、など
    • スレッドをブロックすることなく、構築中に非同期関数を使用できるようにする必要がある
    • コンストラクタはこれを満たすことができない
  • 非同期破棄(async destruction
    • オブジェクトの中には破棄中に実行される非同期関数がある
      • ファイルをフラッシュする、通信を中断する、など
    • スレッドをブロックすることなく、破棄中に非同期関数を使用できるようにする必要がある
    • デストラクタはこれを満たすことができない
  • 構造化された正しい構築(structured and correct-by-construction
    • ローカルスコープのオブジェクトのルールから派生した、直感的なオブジェクト構築のルール
    • structured concurrencyのコンストラクタ/デストラクタ版

これらの要件の元、この提案ではつぎの3つのものを提案しています

  • 非同期オブジェクト
    • 非同期構築が正常に完了するまで使用できない
    • 非同期構築がエラーで完了する場合がある
    • 非同期構築のキャンセルをサポートする
    • 非同期破棄が失敗せず、停止できないことを保証する
  • async_usingアルゴリズム
    • 常に、内部の非同期関数を呼び出す前に非同期構築を完了する
    • 常に、内部の非同期関数が完了する前に非同期破棄を完了する
    • 常に、内部の非同期関数の完了に伴って非同期破棄を呼び出す
    • 常に、複数の非同期オブジェクトの非同期破棄を、その非同期構築の逆順で呼び出す
    • 常に、非同期構築に成功した非同期オブジェクトに対してだけ、非同期破棄を呼び出す
  • async_tuple
    • 常に、async_tupleそのものの非同期構築完了の前に、内包する全ての非同期オブジェクトの非同期構築を完了する
    • 常に、内包する全ての非同期オブジェクトの非同期破棄を、その非同期構築の逆順で呼び出す
    • 常に、非同期構築に成功した内包する非同期オブジェクトに対してだけ、非同期破棄を呼び出す

非同期オブジェクト同士は組み合わせて使用することができ、その要件は次のものです

  • 構成(composition
    • 複数の非同期オブジェクトは、ネストせずに同時に使用可能になる
    • オブジェクト間の依存関係はネストによって表現される
    • 構成は、複数のオブジェクトの並行非同期構築をサポートする
    • 構成は、複数のオブジェクトの並行非同期破棄をサポートする

この提案が提供するのは、これらの要件を具体化したasync_object<T>をはじめとするコンセプトと、async_usingasync_tuple、およびpackaged_async_object(非同期オブジェクトとその非同期コンストラクタ引数をパッケージングするもの)です。

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

非同期オブジェクト型(Foo)の実装例

// Fooはint値を格納する非同期オブジェクト
struct Foo {
  // 非同期オブジェクトを定義
  // 非同期オブジェクトはムーブ不可である必要がある
  struct object : __immovable {
    object() = delete;
    int v;
  private:
    // only allow Foo to run the constructor
    friend struct Foo;
    explicit object(int v) noexcept : v(v) {}
  };

  // 構築された非同期オブジェクトのハンドル型
  // これは非同期構築(async_construct())によって生成される
  using handle = std::reference_wrapper<object>;

  // 非同期オブジェクト用のストレージを予約する型
  // nothrowデフォルト構築可能 かつ ムーブ不可である必要がある
  using storage = std::optional<object>;
  
  // async_construct()は構築された非同期オブジェクトのハンドルで完了するsenderを返す
  auto async_construct(storage& stg, int v) const noexcept {
    return then( just(std::ref(stg), v),
      [](storage& stg, int v) noexcept {
        auto construct = [&]() noexcept { return object{v}; };
        stg.emplace( __conv{construct});
        printf("foo constructed, %d\n", stg.value().v);

        return std::ref(stg.value());
      }
    );
  }

  // async_destruct() はストレージ内の非同期オブジェクトの破棄を実行し、非同期破棄が完了した後に完了するsenderを返す
  auto async_destruct(storage& stg) const noexcept {
    return then( just(std::ref(stg)),
      [](storage& stg) noexcept {
        printf("foo destructed %d\n", stg.value().v);
        stg.reset();
      }
    );
  }
};

// Fooはasync_object
static_assert(async_object<Foo>);
// Fooはintから非同期構築可能
static_assert(async_object_constructible_from<Foo, int>);

2つのFoo型の非同期オブジェクトを作成し、sender式で状態を変更する

int main() {
  // 2つの非同期オブジェクトを使用するsender
  auto inner = [](Foo::handle o0, Foo::handle o1) noexcept {
    return then( just(o0, o1),
      [](Foo::handle o0, Foo::handle o1) noexcept {
        auto& ro0 = o0.get();
        auto& ro1 = o1.get();
        ro0.v = ro0.v + ro0.v;
        ro1.v = ro1.v + ro1.v;
        printf("foo pack usage, %d, %d\n", ro0.v, ro1.v);
        fflush(stdout);
        return ro0.v + ro1.v;
      }
    );
  };

  // 非同期オブジェクトとコンストラクタ引数をパッケージング
  packaged_async_object foo7{Foo{}, 7};
  packaged_async_object foo12{Foo{}, 12};

  // ストレージを予約し、2つの非同期オブジェクトを非同期構築し
  // それらが完了してからinnerの処理にそれらのハンドルを渡し
  // innerの処理が完了したら非同期オブジェクトを非同期破棄する
  // そして、それら全てが完了すると完了する非同期処理を表すsender
  auto use_s = async_using(inner, foo7, foo12);
  
  // 非同期処理開始と待機、結果取得
  auto [v] = sync_wait(use_s).value();
  printf("foo pack result %d\n\n", v);
}

この提案の内容は純粋なライブラリ機能であり、仮実装が公開されています : https://godbolt.org/z/rrbW6veYd

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

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

以前の記事を参照

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

  • rangeコンストラクタについてを別の提案(P3299)へ分離
  • initialiser-listのようなコンストラクタに関する説明を追記

などです。

P2900R7 Contracts for C++

C++ 契約プログラミング機能の提案。

以前の記事を参照

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

  • quick_enforceセマンティクスの追加
    • P3191R0/P3226R0の内容
  • enforceセマンティクスの終了方法をstd::abort()呼び出しから実装定義の方法に変更
    • P3191R0の内容
  • 契約条件の評価回数に実装定義の上限を追加
    • 重複評価回数を指定し、重複評価なしをデフォルトとすることを推奨
    • P3119R0の内容
  • post()の条件式で配列引数をODR使用するのを禁止
    • P3119R0の内容
  • 全ての契約注釈において、va_startの使用を禁止
    • P3119R0の内容
  • ライブラリパートで提案している列挙型の基底型を未規定にする
    • P3198R0の内容
  • contract_kind列挙型の名前をassertion_kindへ変更
    • P3198R0の内容
  • contract_semantic列挙型の名前をevaluation_semanticへ変更
    • P3198R0の内容
  • 評価セマンティクスについて、checked/uncheckedと呼称していたところをchecking/non-checkingに変更
  • 新しいサブセクション追加
    • “Function Type Aliases”
    • “Constructors and Destructors”
    • “Differences Between Contract Assertions and the assert Macro”
  • “Design Principles”セクションの拡張
  • 様々な細かい説明とコード例を追加

などです。

P2963R2 Ordering of constraints involving fold expressions

コンセプトの制約式として畳み込み式を使用した場合に、意図通りの順序付を行うようにする提案。

以前の記事を参照

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

  • CWGのレビューを受けての文言の改善
    • 特にコンセプトがどう満たされるかの説明を追加
  • 包摂のチェックが短絡されることを明確化
    • 設計の説明を追加
  • 畳み込み式の制約は、両方に同じ畳み込み演算子が使用されている場合にのみ包含関係が成立するように変更
    • 空のパックがある場合に、包含関係の成立とチェックの間に矛盾が生じるのを回避

などです。

提案より、(R1で)追加されたサンプルコード

template <std::ranges::bidirectional_range R>
void f(R&&); // #1

template <std::ranges::random_access_range R>
void f(R&&); // #2


template <std::ranges::bidirectional_range... R>
void g(R&&...); // #3

template <std::ranges::random_access_range... R>
void g(R&&...); // #4


int main() {
  f(std::vector{1, 2, 3}); // ok(C++23及びこの提案)#2が呼ばれる
  g(std::vector{1, 2, 3}); // ng(C++23)、ok(この提案)#4が呼ばれる
}

この提案は2024年6月の全体会議で採択され、C++26WDにマージされています。

P2964R1 Allowing user-defined types in std::simd

std::simdのデータ型としてユーザー定義型を使用できるようにする提案。

以前の記事を参照

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

  • SG1/SG6からのフィードバックの反映
  • 要素型の制限(サイズ等)を追加
  • スカラ演算子から有効なstd::simd演算子を構築するための方法として推論(inferencing)を追加
  • 型変換を追加
  • オプトインではなくオプトアウトに変更
  • 明示的なユーザー定義のストレージを削除
  • 推論の例を追加

このリビジョンで追加されたスカラ演算子からの推論とは、要素型となるユーザー定義型で定義されている演算子(1要素の演算を行うもの)を検出してそれを使用するものです。これは、std::simdによる複数要素並列計算の結果は要素単位に同じ計算を適用した時の結果と一致するはず、という仮定に基づくものです。

例えば、std::simdの二項加算演算子operator+)の実装は次のようになります

constexpr friend basic_simd operator+(const basic_simd& lhs, const basic_simd& rhs)
  requires (details::simd_has_custom_binary_plus || details::element_has_plus)
{
  if constexpr (details::is_standard_simd_type) {        // int, float, complex, etc.
    return details::plus(lhs, rhs);
  }
  else if constexpr (details::simd_has_custom_binary_plus) {  // user customisation point
    return simd_binary_plus(lhs, rhs);
  }
  else {
    return details::element_wise_plus(lhs, rhs);     // 👈 Infer from scalar operator
  }
}

この3つ目のカスタマイズがスカラ演算子からの推論です。この場合、要素型に対して定義済みの演算子を使用してそれを要素毎に適用して計算を行います。ただし、これは最低ラインのサポートであり、必ずしも最適なSIMDコードにならない可能性があります。その場合は、2つ目のようにカスタマイズポイントにアダプトすることでより効率的なコード生成を補助することができます。

これによって、ユーザー定義型Uに対するstd::simd<U>を有効化する場合に個々の操作をアダプトしなければならないようにすると(オプトインだと)過度に不便でありメリットが薄くなるとして、要素型の制限を満たす任意のUに対してstd::simd<U>はデフォルトで全ての操作が有効化されるようになります。それが望ましくない場合(std::atomic<T>など)はオプトアウトのメカニズムによってstd::simd<U>を使用不可能にするようにします。

P2967R1 Relocation Is A Library Interface

リロケーションサポートのためのライブラリ機能の提案。

以前の記事を参照

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

  • アブストラクトの単純化
  • 関数名をrelocateからuninitized_relocateに変更
  • イテレータコンセプトを使用してオーバーロードを追加する
  • 失敗しない関数にはnoexceptを付加
  • contiguous_iteratorではない場合に範囲の重複のサポートを削除
  • 実装プロトタイプを追加
  • std::swapの最適化をP3239に委ねる

などです。

P2971R2 Implication for C++

記号論理における含意記号の振る舞いをする=>演算子の提案。

以前の記事を参照

R1での変更は

  • §3 (Intuition): 拡張し、例を追加
  • §4 (Properties): 追加
  • §6 (Why Core?): 再構成、選択肢を増やして拡張
  • §7 (Design): 再構成、設計選択について更新し、より深く記述する
  • §9 (Implementation): 再構成、構文解析の問題に関する新しい議論を追加
  • §10 (Core Wording): 更新された設計決定を反映、Annex Cを追加

などです。

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

  • §1 (What Is Implication?): Adaの構文についての簡単な説明と、必要十分(条件)の説明を追加
  • §8 (Library Impact): 2023年11月のLEWGのレビューを反映し、投票結果を追記
  • §11 (Library Wording): LEWGレビューを反映

などです。

P2976R1 Freestanding Library: algorithm, numeric, and random

<algorithm>, <numeric>, <random>にある一部の機能をフリースタンディング指定する提案。

以前の記事を参照

R1での変更は

  • 機能テストマクロを追加する代わりに__cpp_lib_freestanding_numericを更新
  • is_execution_policyをフリースタンディング対象として追加
  • 並列アルゴリズムfreestanding-deletedとしてマーク
  • <random>のストリーム演算子の仕様を改善し、戻り値を明確化

などです。

P2988R5 std::optional<T&>

std::optionalが参照を保持することができるようにする提案。

以前の記事を参照

このリビジョンでの変更は、value_or()に関してP3091の変更を取り込んだことです。

template<typename T>
class optional<T&> {
  ...

  // P3091からの宣言の例
  template <class U = remove_cv_t<T>, class... Args>
  U value_or(Args&&...) const;

  template <class U = remove_cv_t<T>, class X, class... Args>
  constexpr U value_or(initializer_list<X>, Args&&...) const;

  ...
}

P2996R3 Reflection for C++26

値ベースの静的リフレクションの提案。

以前の記事を参照

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

  • リフレクション(鏡像)間の等価性と、鏡像によって特殊化されたテンプレートエンティティのリンケージについて詳細に説明
  • TS時代の合意を復元するためにaccessible_members_of()を追加
  • value_of()extract()にリネームし、関数を操作できるように拡張
  • 定数式ではなく、値とオブジェクトのリフレクションサポートを明確化
  • clangの試験実装へのリンクを追加
  • can_substitute, is_value, is_object及びvalue_ofextractとは別)を追加
  • 型特性の命名に関するissueを追記
  • 名前付きタプルの別の実装例を追加
  • meta::infoのデフォルト/値/ゼロ初期化は、nullな鏡像を生成する
  • 実装されていたが以前の提案では省略されていたaddressed splicing(スプライシング結果のアドレス取得)を追加
  • テンプレート引数をサポートするために、reflect_invokeオーバーロードを追加
  • 名前の衝突を回避するために、全ての型特性の名前をtype_で始める
  • より汎用的なis_const, is_final, is_volatileを追加
  • is_noexceptを追加
  • is_explicitメンバ関数テンプレートではなくメンバ関数にのみ作用するように修正
  • handling textセクションを追加
  • ODRに関する懸念事項についてのセクションを追加

などです。

P3045R1 Quantities and units library

物理量と単位を扱うライブラリ機能の提案。

以前の記事を参照

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

  • 依存関係(提案)の表に1つ追加
  • Safe unit conversionsの章をvalue_cast()を使用して拡張
  • The affine spaceの章をほぼ書き直し
  • Magnitudes章を追加
  • qp.quantity_from_zero()はユーザーの指定した原点では動作しなくなった
  • qp.quantity_from()は他の量の点に対しても機能するようになった
  • basic_symbol_textsymbol_textヘリネーム
  • symbol_textから[[nodiscard]]を削除
  • 文字列リテラルを受け取るsymbol_textコンストラクタがconstevalになった
  • symbol_textは常にシンボル文字列をchar8_tcharで保持するようになった
  • UTF-8シンボルが使用される場合、u8リテラルを使用する必要がある
  • Symbols of derived dimensions節を追加し、シンボル生成に関する文章をSymbols for derived entities章にリファクタリング
  • 数量のフォーマットをVictor Zverovichと合意した新しい構文にリファクタリング
  • mag<ratio{N, D}>mag_ratio<{N, D}>に置き換えられ、ratio型がライブラリの公開インターフェースではなく実装詳細になった
  • ライブラリの最新の設計を反映するようにCompiler Explorerのリンクを更新

などです。

P3051R1 Structured Response Files

ツールが他のツールにコマンドラインオプションをファイルで引き渡す方法についての提案。

以前の記事を参照

このリビジョンでの変更は、SG15のフィードバックの適用(optionsargumentsに変更)などです。

P3059R1 Making user-defined constructors of view iterators/sentinels private

<ranges>の内部イテレータ型のコンストラクタを非公開にする提案。

以前の記事を参照

このリビジョンでの変更は、この提案による破壊的変更についての議論を追加したことです。

MSVC STLおよびlibc++の開発者はいずれも、この変更による影響は大きくないと見積もっているようです。libstdc++の場合はそもそも実装が標準のものと異なっているため実質的に影響はないとしています。

P3064R1 How to Avoid OOTA Without Really Trying

C++コンパイラによる実装においては、OOTA問題が発生しないことを解説する文書。

以前の記事を参照

このリビジョンでの変更はよくわかりません(提案ではないので、何かしらの修正のみのはずです)。

P3067R0 Provide predefined simd permute generator functions for common operations

P2664で提案されている、std::simdの汎用permute APIを設計し直す提案。

SIMDにおけるpermute命令とは、SIMDレジスタ内で要素を並び替える命令の1つです。ある計算の際に効率的なデータの並びとその後の別の計算の際に効率的なデータの並びが異なる場合など、SIMDレジスタ内にデータを載せたまま並べ替えが必要になることはよくあり、その際にpermute命令を使用できます。

P2664ではstd::simdに対してpermute命令に対応する並べ替えを行うためのstd::permute()を提案していましたが、P2664のstd::permute()APIはほとんどのユーザーの最も一般的な要求をサポートするように汎用的に設計されていたため、必要なことは何でもできるもののよく使用される特定の並べ替えパターンをシンプルに記述することができず、ユーザーが個別にその操作を行う関数を定義する必要性が頻発することが想定されたようです。

この提案ではそのような並べ替え操作の目的やパターンをまとめて、この浮かび上がった要求を元にしてstd::simdの要素並べ替えを行うフリー関数のセットを特定し提案するものです。

今のところ挙げられているものは次のものです

関数 説明
take<int>(simd) simdの先頭からN要素を含む新しいsimdオブジェクトを返す
grow<int>(simd, value={}) 入力されたsimdよりも大きいサイズで、指定されたvalueで初期化された新しい要素を含んだsimdを返す
stride<int N>(simd) 入力されたsimdN番目の要素のみを含む新しいsimdを返す
chunk<int>(simd) 既存のsimd::splitを名前変更および移動したもの
reverse(simd) 入力されたsimdと同じサイズで、要素を逆順にした新しいsimdを返す
repeat_all<int>(simd) 入力されたsimdN回繰り返した新しいsimdを返す(例: repeat_all<3>([a, b])[a, b, a, b, a, b]になる)
repeat_each<int>(simd) 入力されたsimdの各要素をN回繰り返した新しいsimdを返する(例: repeat_each<3>([a, b])[a, a, a, b, b, b ]になる)
transpose<ROWS, COLS>(simd) 入力されたsimdを、ROWS行とCOLS列の行優先の行列として扱い、転置した結果(つまり列優先にした)を返す
zip(simd...) 入力された複数のsimdの対応する要素を交互に並べて新しいsimdを生成する。(例: zip([a, b, c], [0, 1, 2])[a, 0, b, 1, c, 2]を返す
unzip<N>(simd...) 入力されたsimdN個のsimdに分割する。(例: unzip<2>([a, 0, b, 1, c, 2])make_tuple([a, b, c], [0, 1, 2])を返す
cat(simd...) simd_catの名前変更版
extract<N, M>(simd) [N...M)の範囲内の要素を含む新しいsimdを抽出する
rotate<MIDDLE>(simd) simdの要素を左に回転し、MIDDLE インデックスの要素が最初の要素になるようにする
shift_left<N>(simd)/shift_right<N>(simd) 入力されたsimdと同じサイズで、要素を左または右にシフトし、値初期化された要素を挿入した新しいsimdを返す
align<N>(simd a, simd b) 2つのsimdオブジェクトを受け取り、cat(a, b)からsimdを抽出します

これらの良く使用する並べ替え操作(順列のジェネレーター関数)をあらかじめ用意しておくことで

  • 同じジェネレーター関数を繰り返し何度も重複定義することになるのを回避する
  • よく使用する操作について、分かりやすい名前付き関数を提供することで、コードの可読性を向上させる
  • よく使用する操作について、あらかじめ用意しておくことでバグの混入を回避する

等の利点があるとしています。

P3068R2 Allowing exception throwing in constant-evaluation

定数式においてthrow式による例外送出およびtry-catchによる例外処理を許可する提案。

以前の記事を参照

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

  • ライブラリの文言追加
  • 新しい例を追加
  • 実装の説明を追加
  • 変更に影響について追記

などです。

P3085R2 noexcept policy for SD-9 (throws nothing)

ライブラリ関数にnoexceptを付加する条件についてのポリシーの提案。

以前の記事を参照

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

  • § 12 Appendix: Principled design exerciseに2024年東京会議で使用した原則的な設計例を追加
  • § 4.3 Minimal noexceptに最小限のnoexceptポリシーについて追加
  • § 8.3.4 Vectorizationでnoexceptがベクトル化に与える影響について説明
  • § 8.7.7 Don’t discard useful informationで、有用な情報を捨てないことについて説明
  • § 8.3.2 Other implementationsでP3166R0について議論
  • § 8.4.1.3 Allow contract build modes to change the behavior of noexceptで、P3205R0について議論
  • § 8.4.1.2 Retrieve precondition and post-condition results via reflectionで、Bengt GustafssonのContract testing supportについて議論
  • § 8.9 noexcept as Quality of Implementationで、noexceptを完全にQoIにすることについて説明
  • § 8.4.8 Death testsでDejaGnuについて議論を追加
  • § 8.3.3 Bloatで、"Can’t write it better yourself"について議論

などです。

P3091R2 Better lookups for map and unordered_map

連想コンテナからキーによって要素を検索するより便利な関数の提案。

以前の記事を参照

このリビジョンでの変更は、P2988R4の内容(value_orは常にprvalueを返す)を反映した事です。

などです。

P3094R2 std::basic_fixed_string

NTTPとして使用可能なコンパイル時文字列型であるstd::basic_fixed_stringの提案。

以前の記事を参照

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

  • Design discussion章を拡張
  • Open questions章を更新
  • 実装経験の章にコンパイラエクスプローラのリンク追加
  • Synopsis章の名前をWordingに変更し提案する文言として修正
  • traitsテンプレートパラメータのサポートを追加
  • reverse_iteratorのサポートを追加
  • 文字列リテラルを受け取るコンストラクタをconstevalにした
  • rangeコンストラクタを追加
  • view()メンバ関数を追加
  • コンテナと文字列のインターフェースを追加
  • 文字列リテラルと単一文字で動作する二項+演算子を追加
  • 文字列リテラルとの比較演算子を追加
  • array<charT, N>からの推論補助を追加
  • debug-enabled string typeとしてbasic_fixed_stringが追加されたため、formatter特殊化を削除

などです。

P3096R1 Function Parameter Reflection in Reflection for C++26

C++26に向けた静的リフレクションに対して、関数仮引数に対するリフレクションを追加する提案。

以前の記事を参照

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

  • 提案されたAPIのSynopsisを追加
  • 提案する文言を追加
  • 投票結果の追加
  • 提案するソリューションの明確化
  • 文言の改善

などです。

P3100R0 Undefined and erroneous behaviour are contract violations

UB(及びEB)を契約違反として扱うようにする提案。

C++言語を安全ではなくしている要因は実行時の未定義動作にあります。正しくコンパイルされたWell-definedなC++プログラムでも、実行時に任意の未定義動作に陥る可能性があり、未定義動作を回避できない場合それはプログラムのバグとして安全性/セキュリティ上の問題を引き起こします。特に、無効なメモリアクセスによる未定義動作は特に大きな問題で、GoogleMicrosoftの調査によればセキュリティの脆弱性の7割が無効なメモリアクセスによる未定義動作によって発生しているとされます。

近年の安全性・セキュリティの重要性の高まりを受けて、C++プログラムが未定義動作を起こす可能性を減らすための様々な取り組みがなされています。例えば

  • コードレビュー
  • コーディングガイドラインの適用
  • コードの様々場側面を対象にした自動テスト
  • 静的解析ツールの使用
  • 実行時のサニタイザーの使用

など、これらの手法を正しく適用することで、実行時に発生する可能性のある未定義動作を減らすことができます。

これらのアプローチとは異なり、C++標準化委員会はC++プログラムの未定義動作リスクを低減するために、言語そのものを進化させることができる唯一の立場にあります。C++における未定義動作は基本的に実行時の性質であることから主に2つのアプローチが可能です。

  1. 未定義動作に陥るコードパスを静的に検出できる場合、そのプログラムをill-formedにできる
  2. 未定義動作に陥るコードパスに実行時に到達した場合に取り得る動作の範囲を指定し、実行時にその動作を緩和する

この2つのアプローチは排他的なものではなく、相互に補完し合うことができます。

一方、現在C++26を目指して契約プログラミング機能の提案(P2900)が進行しています。契約プログラミング機能はC++言語の安全性を高める機能ではあるものの、万能薬ではなく、契約プログラミング機能はすべての未定義動作をコンパイル時に静的に検出可能にするようなものではありません。しかし、コンパイル時に検出されなかったすべての未定義動作に対して実行時にそれをハンドリングして処理するための包括的なフレームワークを、契約プログラミング機能は提供します。

この提案は、契約プログラミング機能を拡張して、未定義動作を契約違反としてハンドリングできるようにしようとするものです。

この提案のアプローチは、明示的な契約アサーションだけに契約セマンティクスを指定するのではなく、実行時に未定義動作に陥る可能性のあるコア言語構成要素に対して暗黙的に契約を指定することで、未定義動作に陥る場合を契約違反として扱えるようにし、それによって契約プログラミング機能のフレームワークの中で未定義動作をハンドリングできるようにすることを目指すものです。

例えば符号付整数の加算を考えてみます。2つのint型の値a, bを加算する場合、結果がintの最大値を超える(オーバーフローする)場合は現在未定義動作とされていますが、これを再解釈して、符号付整数型の加算には加算結果がオーバーフローしないという暗黙の事前条件ある、とすることができます。

符号付整数型の加算が組み込みのoperator+(int, int)を呼び出して行われると考えると、その組み込み演算子は次のように宣言されているかのように扱われます

int operator+(int a, int b)
  pre ((b >= 0 && a <= INT_MAX - b) // 暗黙の事前条件アサーション
   || (b < 0 && a >= INT_MIN - b));

この事前条件アサーションを「暗黙的」としているのは、通常の契約注釈がユーザーの手によって明示的に付加されるのに対して、この事前条件アサーションコンパイラが暗黙的に生成して付加するものだからです。

この符号付整数型の加算オーバーフローと同様に、「操作Xについて、Yが偽(false)の場合、動作は未定義」というようなC++標準における未定義動作の出現を、「操作Xは、Yが真(true)であるという事前条件を持つ」というように置き換えることができて、明示的な(通常の)契約注釈同様にこの暗黙の契約注釈もまとめて契約プログラミング機能の上で扱うことができるようになります。

こうして実行時にハンドルされうる未定義動作には2つの種類があり、それは、安全なフォールバック動作を定義できるものとできないものです。例えば、符号付整数型の加算のオーバーフローの安全なフォールバック動作はその結果の値が有効な数値になることです。その具体的な数値の性質(飽和演算、ラップアラウンドなど)を指定する代わりに、規格のレベルで未定義動作を解除するには、有効だが未規定な値になると規定するだけで十分です。この数値を計算で使用するとおそらく間違い(バグ)ではあるものの未定義ではなくなります。整数の加算を上記のように組み込み演算子とみなすと、この安全なフォールバック動作はその本体が実行する動作そのものです。

別の例として、未初期化変数を読み取る場合、未定義動作の代わりの安全なフォールバック動作として代わりに未規定だが有効な値(例えばゼロ)を読み取る、とすることで未定義動作を安全に解除することができます。この話をどこかで聞いたことがある人もいるかもしれません、これはP2795R5によって提案されC++26に既に導入されている、Erroneous Behaviour(EB)そのものです。

EBはUB時の動作として安全なフォールバック動作を実装定義で指定し、同時に診断を推奨することでUBを解消しようとする取り組みです。P2795R5で誤ったコードの結果として動作Xを取る、としているところは、この提案において、暗黙の事前条件アサーションが破られた場合の安全なフォールバック動作がXである、というのと全く同じことです。暗黙の契約アサーションがチェックされたかどうかとは関係なく、どちらの場合も誤った状態にあるプログラムの動作について述べており、なおかつその動作は未定義ではなくWell-definedとなります。

このように、P2795R5の「Erroneous Behaviour」とこの提案の「暗黙の契約アサーション違反」という2つの言葉には互換性があります。

そして、P2795R5によればErroneous Behaviourが発生した場合、実装は次のいずれかを行うことが許可されます

  • 問題の報告
  • プログラムの終了
  • 何もしない
    • ただしUBではない

EB時の可能な動作のこのリストは、契約違反が起きた場合の4(+1)つの契約注釈のセマンティクスのサブセットです(quick_enforce, enforce, observe, ignore, assume)。

  • 診断を発行 -> observe
  • プログラム終了 -> quick_enforce/enforce
  • 何もしない -> ignore
    • 安全なフォールバック動作を実行する

逆に、EBのフレームワークにはこのような欠陥のハンドリングに役立つ機能が欠けています。契約プログラミング機能では、契約違反時に違反ハンドラが呼びされ、なおかつこの違反ハンドラはユーザーがカスタマイズできます。EBでは安全なフォールバック動作のない未定義動作を処理できませんが、契約の場合は可能です。

そして、この提案ではC++23以前(契約もEBもない時代)との完全な下位互換を保つオプションとして、契約注釈のセマンティクスにassumeセマンティクスを追加することを提案しています。これは[[assume]]属性同様に契約注釈をコードの仮定として利用することが許可されており、なおかつ契約条件はチェックされず違反ハンドラは呼び出されません。契約違反が起こると未定義動作になります。

assumeセマンティクスは従来(現在)のC++と同じ挙動であり、パフォーマンスをとにかく重視する場合に選択できるものです。これは契約プログラミング機能のオーバーヘッドを取り除き、動作とパフォーマンスの両面で現在のC++23以前の動作との後方互換性を保つためのものです。EBの場合、過去の動作に戻すためにEB個別にオプトアウトメカニズム(例えば[[indeterminate]])が必要となり、規格の複雑さの面でもユーザー負担の面でもスケールしません。

その概念に互換性があり、動作状態を包含しており、C++23以前との後方互換性の一括確保も備えていることから、この提案はErroneous Behaviourを完全に包含しているといえます。そのためこの提案では、EBもまたUB同様に暗黙の契約アサーションをもつものとして扱うことを提案しています。

2種類の未定義動作のもう一つ、安全なフォールバック動作を定義できないタイプの未定義動作に対して暗黙的に付加された契約の事をこの提案では、無視できない契約アサーション(Non-ignorable implicit contract assertions)と呼んでいます。

例えば、配列の添え字アクセスを先程と同様に組み込み演算子としてみてみると次のようになります

template <typename T, size_t N>
T& subscript(T(&array)[N], size_t index)
  pre(index < N) // 暗黙の事前条件アサーション
{
  return array[index];
}

この場合、この暗黙の契約アサーションが破られたときにその本体の動作を安全なフォールバック動作として扱うことはできません。メモリの安全性や型の安全性に違反する操作はこのカテゴリに分類されます。

無視できない契約アサーションに対する契約注釈のセマンティクスは制限され、ignore/observeセマンティクスで評価することができません。指定可能なセマンティクスはquick_enforce/enforce/assumeのみです。これらの評価セマンティクスは例えば、次のような実装戦略にマッピングできます

  • quick_enforce : 範囲外アクセスを実行時にトラップするclangの-fbounds-safety
  • enforce : アドレスサニタイザーによる実行時の検出と診断発行、およびプログラム中断
  • assume : C++23以前(現在)の安全ではないデフォルトの動作

安全なフォールバック動作を持たないC++の未定義動作に対しても、同様に無視できない契約アサーションを持つものとして再解釈することで、同様のマッピングが存在します。

ここでは、契約セマンティクスのマッピングの1つとしてサニタイザーのような外部ツールがあげられています。C++プログラムのためのサニタイザーはいくつかの実装がすでに提供されていますが、いずれの実装もユーザーコードと対話するためのAPI(コールバック)が非常に貧弱であり、その統一的な指針のようなものも存在していません。

この提案では、サニタイザーも含めた外部ツールのユーザーコードとの対話APIとして、契約プログラミング機能の違反ハンドラを使用するようにするアイデアを挙げています。現時点の契約違反ハンドラは現在のサニタイザーが備えるコールバックAPIよりも充実(ユーザーが置換可能、引数で詳細な情報を取得可能)しており、この包括的なAPIによってサニタイザーをC++標準の範囲内に配置することができます。

また、このAPIのサポートは既存のサニタイザーに対して変更を強いるものではありません(そうするかは完全にオプションです)。現在の契約プログラミング機能の仕様では、デフォルトの契約違反ハンドラの動作は実装定義であり、ユーザーが違反ハンドラをカスタムすることを許可しなくても良いとされているため、既存のサニタイザーはすでにそのようなAPIに準拠していると言えます。

P3103R2 More bitset operations

<bit>にあるビット操作関数に対応するメンバ関数std::bitsetにも追加する提案。

以前の記事を参照

このリビジョンでの変更は、P3104R2への言及を追加、§4 Design considerationsを拡張したことなどです。

P3111R0 Atomic Reduction Operations

std::atomicにアトミックリダクション操作を追加する提案。

アトミックリダクション操作とは、アトミックなRMW操作(fetch_add()のような操作)ではありますが古い値を実際にはフェッチせず、メモリモデルの観点からも読み取りを行わないようなRMW操作の事です。

そのような、フェッチされた古い値を破棄するようなアトミックRMW操作を実行する並行アルゴリズムは高性能コンピューティングでは一般的であり、現代のハードウェアはそのような操作に対して効率的な命令を備えています。

例えば次のコードは並列にヒストグラムを求めるものです

std::span<unsigned> data;

std::array<std::atomic<unsigned>, N> buckets;

constexpr T bucket_sz = std::numeric_limits<T>::max() / (T)N;

unsigned nthreads = std::thread::hardware_concurrency();

std::for_each_n(std::execution::par_unseq, std::views::iota(0).begin(), nthreads, 
 [&](int thread) {
  unsigned data_per_thread = data.size() / nthreads;
  T* data_thread = data.data() + data_per_thread * thread;

  for (auto e : span<T>(data_thread, data_per_thread)) {
    // ここでアトミック値に対して足しこみ(fetch_add())が行われている
    buckets[e / bucket_sz].fetch_add(1, std::memory_order_relaxed);
  }
});

このコードには次の2つの問題点があります

  • 正しさ(UB)
    • アトミック操作が次のいずれも満たしていないため、この並列版for_each_nではexecution::parを使用するべきだった(par_unseqを使用しているためにUB)
      • 潜在的に同時実行され、シーケンス化されていないコンテキストでデータ競合が発生する
      • 他の関数呼び出しと同期するため、ベクトル化セーフ
  • パフォーマンス
    • このプログラムでアトミックリダクション操作を使用するようにするためには、高度なコンパイラの分析が必要

std::atomicでアトミックリダクション操作をサポートすることでこの2つの問題を解決でき、この提案はそれを提案するものです。

この提案では、既存のstd::atomicおよびstd::atomic_refメンバ関数である.fetch_xxx()に対してvoid戻り値型のxxx()メンバ関数を追加することを提案しています。

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

現在 この提案
#include <algorithm>
#include <atomic>
#include <execution>
using namespace std;
using execution::par_unseq;

int main() {
  size_t N = 10000;
  vector<int> v(N, 0);
  atomic<int> atom = 0;
  for_each_n(par_unseq, 
     v.begin(), N,
    [&](auto& e) {
      // UB+SLOW:
      atom.fetch_add(e);
  }); 
  return atom.load();
}
#include <algorithm>
#include <atomic>
#include <execution>
using namespace std;
using execution::par_unseq;

int main() {
  size_t N = 10000;
  vector<int> v(N, 0);
  atomic<int> atom = 0;
  for_each_n(par_unseq, 
     v.begin(), N,
    [&](auto& e) {
      // OK+FAST
      atom.add(e);  // 👈
  }); 
  return atom.load();
}

また、この延長として、順序付けされていないときの(並列実行時の)読み取りでないアトミックメモリ操作を許可し、浮動小数点演算が結合的であると仮定して浮動小数点数型の算術リダクション操作を拡張することも追加で提案しています。

浮動小数点数の演算のように非結合的な演算では、並列に実行される演算の順序が結果に影響を与える可能性があるため、アトミックリダクション操作の効果が制限されます。例えば、x = a + (b + c)という計算を並列に実行する場合、アトミックリダクション操作では(a + b) + ca + (c + b)などの異なる順序で計算される可能性があり、浮動小数点演算が結合的ではないことから結果が一致しない可能性があります。この提案の一般化アトミックリダクション操作では、浮動小数点数型のアトミックリダクション操作において浮動小数点演算が結合的であると仮定して実行することを許可することで、実装はこの制限を受けずに最適化が可能になります。

この提案では、一般化アトミックリダクション操作は浮動小数点数型の特殊化のみのメンバ関数として、上記のアトミックリダクション操作とは別名のメンバ関数として追加することを提案しています

template <floating-point>
class atomic {
  ...

  // アトミックリダクション操作
  void add(floating-point, memory_order);
  // 一般化アトミックリダクション操作
  void add_generalized(floating-point, memory_order);

  ...
};

P3119R1 Tokyo Technical Fixes to Contracts

現在のContarcts仕様の小さな問題を解決する提案。

以前の記事を参照

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

  • 無制限の評価について更新
  • 提案1(配列引数と事後条件)と提案2(Cの可変長関数引数と事前/事後条件)についての投票結果を追加

などです。

P3125R0 Pointer tagging

タグ付きポインタをサポートするためのライブラリ機能の提案。

タグ付きポインタとは、ポインタのビットのうち使用されていない部分に情報を埋め込むこんで利用しようとするものです。主に、メモリ安全性の向上のために使用されていますが、これを利用した特殊なデータ構造もあるようです。

現代のC++の環境の多くはポインタのサイズは64bitですが、現状のメモリ容量の場合そのメモリ空間をすべて表現するのに64bitもの長さは必要ありません。アーキテクチャによるようですが、48bit目くらいまでしか使われていないようです。すると、余った上位16bitを無視するようにしてもらえば、その部分を何かしら利用することができます。x64/arm共にこのためのCPU拡張を備えています。

さらに、アドレスがNバイトにアラインされている時そのアドレスはNの倍数になるため、その分の下位数ビット(log2(N)ビット)も同様に利用できます。例えば、int型が4バイトの場合int型変数のアドレスは通常4の倍数の値になるため、下位2ビットはポインタ値に参加していません。

しかし、C++の規格としては有効なポインタの一部のビットを操作することは未定義動作であり、タグ付きポインタは許可されていません。この提案は、特殊なライブラリ関数を通してのみそのような操作を認めるようにしようとするものです。

提案しているのはまず、ポインタからタグ部分を取り出すビットマスクを返す関数です。

namespace std {
  template <typename Pointee, size_t Alignment = alignof(Pointee)>
  constexpr auto tag_bit_mask() noexcept -> uintptr_t;
}

このtag_bit_mask()はポインタ型PointeeとアライメントAlignmentに対して、そのポインタ型のポインタ値のうちタグとして使用可能な部分のビットが1になったビットマスクを返します。下位のビットはアーキテクチャによらず共通(アライメントで決まる)ですが、上位ビットがどこまで利用可能かはアーキテクチャおよびサニタイザーの存在によって変化します。

次に、タグ付きポインタを表す型std::tagged_pointer<T, Alignment>を用意して、これに対してタグ付け、タグのクリア、タグに保存されている値の取り出しを行う関数を提案しています。

namespace std {
  // ポインタにタグ付けを行う
  template <typename T, size_t Alignment = alignof(T)>
  constexpr auto tag_pointer(T* original, uintptr_t value) noexcept -> tagged_pointer<T, Alignment>;

  // タグ付きポインタを元のポインタに戻す
  template <typename T, size_t Alignment = alignof(T)>
  constexpr auto untag_pointer(tagged_pointer<T, Alignment> ptr) noexcept -> T*;

  // タグ付きポインタからタグの値を取り出す
  template <typename T, size_t Alignment = alignof(T)>
  constexpr auto tag_value(tagged_pointer<T, Alignment> ptr) noexcept -> uintptr_t;
}

この提案のタグ付きポインタサポートは特殊なデータ構造(Hash-Array-Mapped-Trie (HAMT)など)の作成を許可することに重きを置いているため、これらの関数はconstexprが付加されています。

std::tagged_pointer<T, alignment>void*と同じサイズでvoid*に変換できる必要があるという要件は定まっているものの、それを何にするか(void*T*、クラス型など)はまだ決まっていないようです。

P3126R1 Graph Library: Overview

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案の概要をまとめた文書。

以前の記事を参照

このリビジョンでの変更は、報告されて対処中の問題についてまとめるためのIssues Statusセクションを追加したことです。

P3130R1 Graph Library: Graph Container Interface

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、グラフの実体となるコンテナのインターフェースについてまとめた文書。

以前の記事を参照

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

  • num_edges(g)has_edge(g)関数を追加
  • 関数の表が大きくなり過ぎたので、グラフ・頂点・エッジの3種類に分割
  • グラフコンテナインターフェースから、Load Graph Dataセクションとそれに関する関数を削除
    • グラウフデータ構造のコンストラクタとインターフェースの複雑化を回避するため
    • これを補完するため、P3131のグラフコンテナcompressed_graphにコンストラクタが追加された
  • compressed_graphの実装経験を反映して、partition関数を改訂し使用法を反映
    • 他の名前と一致するように、partition_count(g)num_partitions(g)に名前変更
    • partition_id(g,u)partition_id(g,uid)に変更
      • この関数が呼ばれたときに頂点が存在しない場合があるため
    • edges(g,u, pid)を削除
      • ターゲット頂点が異なるパーティションにある可能性がある場合、<ranges>の機能を利用して簡単にフィルタを実装できるため

などです。

P3131R1 Graph Library: Containers

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、グラフの実体となるコンテナ実体についてまとめた文章。

以前の記事を参照

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

  • 典型的CSR実装の先にあるcompressed_graphの機能概要を追加
  • compressed_graphnum_edges(g)has_edge(g)に計算量の指定を追加
  • P3130R0のロード関数削除を補完するために、compressed_graph
    • オプションのpartition_start_idsパラメータを踏む

などです。

P3137R1 views::to_input

入力の範囲をinput_rangeに弱めるRangeアダプタ、views::inputの提案。

以前の記事を参照

このリビジョンでの変更は、sized_sentinel_forである場合にoperator-(二項)を追加したこと、このアダプタの分割に関する議論の追加などです。

P3138R1 views::cache_last

入力範囲の現在の要素をキャッシュするRangeアダプタ、views::cache_lastの提案。

以前の記事を参照

このリビジョンでの変更は、constメンバ関数のスレッドセーフ性保証の例外をこの提案のアダプタに関連するもののみに留めたことです。

P3139R0 Pointer cast for unique_ptr

std::const_pointer_caststd::dynamic_pointer_caststd::unique_ptrオーバーロードを追加する提案。

std::uniqur_ptrに対してポインタのキャスト操作を行うことはそのままではできず、現在のベストプラクティスは、一旦.release()してポインタを取り出した後、そのポインタに対してポインタキャストを適用してから、結果のポインタを再びunique_ptrでラップする、という手順です。

しかし、この手順は一旦リソースの所有権を手放す必要があるなど、リソース安全な方法とはいえません。

auto GetClient() -> std::unique_ptr<const Client>;

// constを外したい
voif example() {
  std::unique_ptr<Client> client;
  // 生ポインタが露出する
  client.reset(const_cast<Client*>(GetClient().release()));

  ..
}

// dynamic_castを適用したい
void UseV2Client(std::unique_ptr<Client>&& client) {
  std::unique_ptr<ClientV2> v2;
  ...
  // dynamic_castが失敗するとリソースリークする
  v2.reset(dynamic_cast<ClientV2*>(client.release()));

  ...
}

一方で、std::shared_ptrにはそのキャストのためのフリー関数であるstd::const_pointer_caststd::dynamic_pointer_castが用意されており、これを用いるとこのような危険性を回避することができます。また、Boostにはunique_ptrに対するstd::const_pointer_caststd::dynamic_pointer_castオーバーロードが用意されています。

std::uniqur_ptrのポインタキャストという操作をより安全にするために、std::uniqur_ptrでもstd::const_pointer_caststd::dynamic_pointer_castを使用できるようにする提案です。

方法としては単純に、std::const_pointer_caststd::dynamic_pointer_caststd::uniqur_ptrを受け取るオーバーロードを追加することでこれを行おうとしていますが、std::unique_ptr特有の事情により少し設計が異なる部分があります。

1つは提案するオーバーロードstd::uniqur_ptrの右辺値に対してのみ作用することで、もう1つはカスタムデリータの考慮です。std::shared_ptrの場合はカスタムデリータは型消去されて保持されており、キャストの前後でも一貫して元のポインタに対して作用するようにすることで、デリータを考慮する必要はありません。しかし、std::unique_ptrの場合はデリータは型の一部であり、キャストにあたってデリータを考慮する必要があります。

std::unique_ptr<T, D>std::unique_ptr<U>にキャストする場合、Dがキャスト後のポインタを削除できるかどうかを考える必要があります。これは単純にデフォルトのデリータを使用している場合にのみその仮定が成り立ちます。一方、std::unique_ptr<T, D>Dを通してポインタ型をカスタマイズすることができ、保持するものは実際にはポインタではない場合もあります。この場合、ユーザーが指定したDはキャスト後のリソースを正しく開放できる場合もあります(これはユーザーが知っています)。

そのため、この提案ではこの2つのユースケースをサポートします。すなわち、キャストは次の2パターンが可能です

  1. std::unique_ptr<T> -> std::unique_ptr<U>の変換
  2. std::unique_ptr<T, D> -> std::unique_ptr<U, D>の変換

提案されている関数

namespace std {
  // dynamic_cast
  template<class T, class U>
  constexpr unique_ptr<T> dynamic_pointer_cast(unique_ptr<U>&& r) noexcept;

  template<class T, class D, class U>
  constexpr unique_ptr<T, D> dynamic_pointer_cast(unique_ptr<U, D>&& r) noexcept;

  // const_cast
  template<class T, class U>
  constexpr unique_ptr<T> const_pointer_cast(unique_ptr<U>&& r) noexcept;

  template<class T, class D, class U>
  constexpr unique_ptr<T, D> const_pointer_cast(unique_ptr<U, D>&& r) noexcept;
}

この提案のキャスト設計においては、安全であることを重視しています。そのため、この2種類以外のポインタキャスト(static_castreinterpret_cast)に対応する関数は専門知識が必要であり気軽に使用できるものではないとして提案していません。

また、キャストに当たっては静的にその危険性を検出できるものについてはチェックして弾くようにしています。次の表は、この提案のAPIに追加されているガードレールを表したものです

API 1の変換 2の変換
const_pointer_cast T* -> U*へのconst_castが有効であること 変換元の::pointerから変換先の::pointerへのconst_castが有効であり、T, Uはどちらも配列型であるかどちらも異なっている
dynamic_pointer_cast T* -> U*へのdynamic_castが有効であり、Uは仮想デストラクタを持つ 変換元の::pointerから変換先の::pointerへのdynamic_castが有効であり、T, Uはどちらも配列型ではない

表の右列の「変換元の::pointer」とはstd::unique_ptr<T, D>::pointerのことで、「変換先の::pointer」とはstd::unique_ptr<U, D>::pointerのことです。

P3149R3 async_scope -- Creating scopes for non-sequential concurrency

P2300のExecutorライブラリについて、並列数が実行時に決まる場合の並行処理のハンドリングを安全に行うための機能を提供する提案。

以前の記事を参照

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

  • サンプルコードを例外安全になるように修正
  • async scopeコンセプトをscopeとtokenに分割し、counting_scopeを更新して一貫させる
  • counting_scopeの名前をsimple_counting_scopeに変更
    - stop sourceを持つスコープに`conting_scop`という名前を付ける
    
  • let_with_async_scopeconting_scopを利用して、再帰的に生成された例を追加

などです。

P3154R1 Deprecating signed character types in iostreams

iostreamのストリーム入力/出力演算子signed/unsigned charオーバーロードを非推奨にする提案。

以前の記事を参照

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

  • 影響についての調査の追加
  • モチベーションのコード例を変更
  • Cのchar8_tに関する注意を追加
  • P0487R1に関する注記を追加

などです。

P3157R1 Generative Extensions for Reflection

静的リフレクションをより有用にするための機能拡張についての提案。

以前の記事を参照

このリビジョンでの変更は明確ではありませんが、おおよそ次の部分が異なっています

  • CLOSURE REFLECTIONとCOMPLETE AND ACCURATE REFLECTION OF REPRESENTATIONセクションの削除
  • Function Descriptor MetafunctionsとEmbedded Domain Specific Languagesセクションの追加
    • 特に、Function Descriptor Metafunctionsセクションでは、関数リフレクションのための具体的なメタ関数が提案されています
  • サンプルコードや解説の改善
  • 参考文献の更新

などです。

セクションが削除された理由は明確ではありませんが、CLOSURE REFLECTIONに関しては別の提案(P3273)に分離されたようです。この提案は関数リフレクションの強化の方向に特化することにしたようです。

P3175R1 Reconsidering the std::execution::on algorithm

P3175R2 Reconsidering the std::execution::on algorithm

P2300のstd::execution::onアルゴリズム命名について再考する提案。

以前の記事を参照

R1での変更は

  • write_envアダプタを説明専用にした
  • finally, unstoppableアダプタを削除
  • schedule_fromおよびlet_*アルゴリズムへの変更を元に戻す

などです。これらの内容は別の提案(P3284)へ分離されています。

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

  • onアルゴリズムに未規定のタグ型を与えて、カスタマイズを抑制する
    • これについての説明を行うセクションを追加
  • onカスタマイズに厳密な制約を設けて、正しいセマンティクスを持つようにする
  • start_on, continue_on命名に関する説明を追加

などです。

この提案のR3はすでに2024年7月の全体投票をパスしてC++26WDに導入されています。

P3179R1 C++ parallel range algorithms

RangeアルゴリズムExecutionPolicyに対応させる提案。

以前の記事を参照

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

  • SG1とSG9のフィードバックを反映
  • イテレータ制約についてより追記
  • アルゴリズムの出力先としてrangeを使用することを提案
  • 出力先範囲をイテレータで指定する場合、番兵を必須にする

通常のアルゴリズム関数は出力先について出力イテレータのみを要求し、その番兵を必要としません。このリビジョンでは、ExecutionPolicyを取る並列Rangeアルゴリズムが出力を行う場合、出力先はrangeであるか、番兵の指定を必須にしました。これは、並列アクセスにておいては出力先のサイズが分かっていたほうが効率化できる場合があるためです。

P3183R1 Contract testing support

関数の契約注釈だけを実行してテストを行う特別な関数の提案。

以前の記事を参照

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

  • テスト関数をconstexpr指定
  • コンストラクタ、デストラクタ、及び演算子オーバーロードをテスト可能にするために、P3312R0への参照を追加
  • void戻り値型関数の事後条件をテストするための特別なcheck_postconditions_void()を追加

などです。

P3210R1 A Postcondition is a Pattern Match

事後条件の構文をパターンマッチングの構文に親和させる提案。

以前の記事を参照

このリビジョンでの変更は、Editorialな修正のみのようです。

この提案の方向性はSG21で好まれなかったようで、リジェクトされています。

P3214R0 2024-04 Library Evolution Poll Outcomes

2024年4月に行われた、LEWGにおける投票の結果

次の9つの提案が投票にかけられ、リジェクトされたものはありません

P3201R1とP3201R1以外は、C++26に向けてLWGに転送するための投票です。

また、投票に当たって寄せられたコメントが記載されています。

P3228R1 Contracts for C++: Revisiting contract check elision and duplication

契約条件の評価回数の規定をどうするかについて、ユースケースとソリューションをまとめて比較する文書。

以前の記事を参照

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

  • “Side effects”セクションを追加し、それに応じて他のセクションを再構成
  • 最新のSG21の議論を反映するように更新

などです。

“Side effects”セクションでは契約プログラミング機能における副作用についてのこれまでの議論や現在得られている見解がまとめられています。それによってボリュームが増していますが、評価回数の検討などに関してはR0から変化がありません。

P3234R1 Utility to check if a pointer is in a given range

ポインタがあるメモリ範囲の内側にあるかどうかを調べるpointer_in_range()の提案。

以前の記事を参照

このリビジョンでの変更は、ポインタ範囲をspanで受け取らない理由の説明(ポインタ2つを受け取るコンパイラの組み込み関数を使うので、ポインタを直接受け取るのが一番最適化を阻害しない)を追加したことです。

P3235R0 std::print more types faster with less memory

std::printの効率的な実装をより拡大して適用する提案。

P3107ではstd::printに対して、std::format()を使用して一旦std::stringを得てから出力するのではなく、基底のストリームバッファをロックして直接書き込むようにすることでstd::printの出力動作を効率化しました。ただし、これはフォーマッタ内でロックを取得するようにしている場合にその型の値に対してstd::printを同じロックの下で呼び出す場合にデッドロックを引き起こす可能性があったため、std::enable_nonlocking_formatter_optimization変数テンプレートの特殊化をtrueで定義する場合にのみ有効になるようにされています。

// フォーマットしたい型
struct foo {};

// fooのためのformatter特殊化
template <>
struct std::formatter<foo> {
  ...
};

// P3107の効率的な実装を有効化する
template <>
constexpr bool std::enable_nonlocking_formatter_optimization<foo> = true;

P3107では、このオプトインを基本型(int等組み込みの型)と文字列型(std::string/std::string_view)に対してのみ用意しており、他の標準型のフォーマッタに対しては有効化していませんでした。

この提案は、これら以外のフォーマッタ提供済みの標準型に対してもこのオプトインを有効化しようとするものです。

追加を提案しているものは次のものです

  • <chrono>関連型
    • ただし、std::zoned_timeはデフォルトのTimeZonePtrに対してのみ提供
  • std::thread::id
  • <stack_trace>関連
  • std::filesystem::path
  • std::pair/std::tuple
    • 要素型のすべてがオプトイン済みである場合に有効化
  • range関連
    • std::vector<bool>とコンテナアダプタを含む
    • 要素型によらずに有効化
      • 反復処理の場合はデッドロック発生の可能性が低いと考えられるため

また追加の提案として、現在のstd::vprint_(non)unicodestd::vprint_(non)unicode_bufferedに、std::vprint_(non)unicode_lockingstd::vprint_(non)unicodeに変更することも提案しています。これは、非ロックオーバーロード(現在のstd::vprint_(non)unicode)が最終的な書き込み時にstd::vprint_(non)unicode_lockingを呼び出すため、命名が誤解を招くということで修正を提案するものです。修正後の名前は、この2つの関数の違いが出力をバッファリング(全体を文字列化してから出力を行う)して行うかどうかという点が明確になります。

この提案C++23へのDRとして、2024年6月の全体会議で採択されています。

P3236R1 Please reject P2786 and adopt P1144

P2786の提案するtrivial relocatabilityをリジェクトすべきとする提案。

以前の記事を参照

このリビジョンでの変更は、著者(というか賛同者)が追加されたことなどです。追加されたのは、Follyのメンテナの方のようです。

P3238R0 An alternate proposal for naming contract semantics

違反ハンドラを呼び出さずに終了する契約セマンティクス(quick_enforce)を、Erroneous Behaviorとして扱うようにする提案。

ここで提案されているのは次の2つの事です

  1. 契約違反時に違反ハンドラを呼び出さず即時終了するセマンティクス(quick_enforce)を持つ契約注釈の契約違反時の動作は、Erroneous Behaviorであると指定する
    • その安全なフォールバック動作として、違反ハンドラを呼び出す
  2. 契約セマンティクスの名前を変更する
    • ignore -> ignored
    • enforce -> enforced
    • observe -> observed
    • quick_enforce -> erroneous

契約違反時に即時終了するセマンティクスにおいては契約違反は致命的な失敗であり、他の方法でこの動作をフックすることはできません。これをEBとして指定することで、そのまま即時終了することと、安全なフォールバック動作として違反ハンドラを呼び出して簡単な診断を発行して終了することの両方を実装に対して許可することができます。

次にこのことを踏まえたうえでquick_enforceに対する適切な名前を考えます。他の3つの契約セマンティクスの名前は概ね「~ the trueness of a predicate」(「述語の真偽を~する」)の~の部分に当てはまるようになっていますが、quick_enforceはそうではありません。この提案ではそこに当てはまるような名前を考えるのではなく、意味上のerroneousという単語を他の名前になっている動詞の過去形と同じ文法的位置に置くようにしています。すなわち、「A contract violation is treated as ~」(契約違反は、~として扱われる)の~に入るように他の3つの名前を変更し、動詞の過去形にすることを提案しています。

この提案の内容はSG21のレビューにおいて好まれませんでした。

P3239R0 A Relocating Swap

リロケーションを用いたswapを行う、swap_representationsの提案。

swapのセマンティクスは2つのオブジェクトのオブジェクト表現を交換することです。その操作は実際には、ムーブコンストラクタとムーブ代入演算子(とデストラクタ)によって実行されます。

オブジェクト表現の交換という部分に注目するとswapの行うことはリロケーションが行うこととよく似ていることに気づきます。特に、トリビアルリロケーション可能な型ならswapは単純なmemcpyによって実行できます。これによって、swapのパフォーマンスを向上させることができます。

しかし、トリビアルなリロケーションのような操作によって2つのオブジェクトの表現を入れ替えることは、C++の非トリビアルな型のオブジェクトについての生存期間のルールに抵触し、トリビアルリロケーション導入後においても許可されません。

この提案は、リロケーションによるswapを行う専用の関数をライブラリに追加し、それを通してのみリロケーションによるswapを合法化することを提案するものです。

提案されているのは、std::swap_representations()という関数です。同じ型の2つのオブジェクトへの参照を受け取って、それらのオブジェクトの値表現を交換しますが、どちらのオブジェクトの生存期間も終了しません。リロケーション操作を用いるとソースオブジェクトは消えてしまうため通常それは不可能です。そのため、この関数は一種の魔法のように動作する特別な関数です。ただし、安全のため、入力の型Tトリビアルリロケーション可能(is_trivially_relocatable_v<T>true)でなければなりません。

namespace std {
  // 提案するswap_representationsの宣言
  template <bool force, class T>
  void swap_representations(T& a, T& b); 
}

テンプレートパラメータのforceTがポリモルフィックな型の場合にのみ意味を持ちます。ここでの引数は参照であるため、Tを基底クラスとした派生クラスオブジェクトへの参照が入力される可能性があります。もし、abが同じ基底クラスTから派生する別の派生クラスの参照である場合、これは未定義動作になります(abの仮想関数テーブルが異なっているため)。

この関数内からは参照で渡されたものしか見えないためそれを静的に検出できませんが、swap_representations利用者が予めこの危険性がない参照を渡すことを知っている場合(例えば、std::vector内部で要素型がTそのものであることが分かっている場合など)に、forceパラメータをtrueにして呼び出すことでポリモルフィックな型の値表現を交換できます。逆に、is_polymorphic_v<T>trueの場合にforcefalseと指定されていると、コンパイルエラーになります。

また、std::swapがプログラムの観測可能な動作を変更することなくこれを用いて実装可能であることを示す型特性std::swap_uses_value_representations_vを用意することも提案しています。型Tのオブジェクトについてのstd::swapstd::swap_uses_value_representations_v<T>trueとなる場合にswap_representationsを用いて最適化することができます。

この提案の内容はコア言語の変更を伴わないライブラリ拡張のみであり、なおかつ他のライブラリ機能に影響を及ぼさないものです。しかし、トリビアルリロケーションと共に導入されれば、そのパフォーマンス上の恩恵を最大化することができます。

P3247R1 Deprecate the notion of trivial types

C++ における「トリビアル型」の概念を非推奨にする提案。

以前の記事を参照

このリビジョンでの変更は、標準ライブラリ内の"trivial class"の用法を2か所調整し、[meta.type.synop]の見落としを修正した事です。

P3248R0 Require [u]intptr_t

(u)intptr_tを必須にする提案。

(u)intptr_tはポインタ値を整数として保持したい場合に使用でき、変換前後でポインタ値が変化しないことが保証されている整数型のエイリアスです。しかし、このエイリアスはオプションであり、実装は必ずしも定義する必要はありません。

(u)intptr_tが存在しない可能性があるという事実によって、移植性を重視するソフトウェアではこの使用を回避する必要があり、そのために余計な作業と潜在的なバグを埋め込む可能性が発生します。

このことは標準ライブラリでも問題になり、P2835(std::atomic_refにポインタ値を取得可能な関数を追加する)やP3125(ポインタの未使用ビット使用のための関数を追加する)などではポインタ値のやり取りのために(u)intptr_tを使用しようとしているものの、それがオプショナルであることが問題となっています。

この提案は、(u)intptr_tを必須の型エイリアスにしようとするものです。

オプショナルであるとはいえ実際には主要3コンパイラは全てのターゲット向けにこのエイリアスを提供しており、その標準ライブラリ実装も(u)intptr_tの存在を仮定して実装されているようです。また、プラットフォームのABIでも(u)intptr_t相当の整数型の存在がポインタ型の指定と共に仄めかされているようです。

従って、(u)intptr_tを必須にしたとしてもC++のプラットフォームサポートは低下しない、とこの提案では結論付けています。

提案している規格の変更は、(u)intptr_tからoptionalであるという指定を取り除くだけです。

P3249R0 A unified syntax for Pattern Matching and Contracts when introducing a new name

パターンマッチングと契約プログラミング機能の間で統一的な構文の提案。

パターンマッチング構文においては、パターンの記述内で(マッチしたものを表す)新しい名前を導入する必要があり、P2688のパターンマッチング提案においてはlet x => expr;の様に記述します。また、契約プログラミング機能の事後条件においては、戻り値を参照する名前を導入する必要があり、post(r : expr)の様に記述します。

int f();

// パターンマッチングの例
void g() {
  int i = f();

  i match {
    42 let val => print(val); // valを導入(val == 42)
    let x => print(x); // xを導入(x != 42)
  }
}

// 契約プログラミング機能の例
int foo()
  post(ret : ret > 0) // retを導入

そのコンテキストで必要な変数名を導入するという側面からはこの2つは同じことをしていると見ることができ、同じことをするのに異なる構文を使用しているのは一貫性が無く初学者が覚えづらく、言語を複雑にします。

この提案は、この2つの名前導入の構文を統一することで、習得のしやすさの向上と言語の複雑さの低減を図るものです。

提案では、これらの構文を一貫してlet name => exprのように統一することを提案しています。

int foo()
  post( let r => r > 0) // rを導入

すなわち、契約プログラミング機能の構文をパターンマッチングに合わせるものです。

ただし、事後条件(post())内でのlet ~はパターンマッチングではないため、そこでパターンマッチングを書きたい場合は右辺の式内に記述する必要があります。

事後条件として(ret == 1 or ret == 2 or ret > 100)という条件を指定したい場合の例

// P3210の提案
int foo()
  post (
    1 => true;
    2 => true;
    let ret => ret > 100
  );

// この提案の場合
int foo()
  post ( let ret =>
    ret match {
      1 => true;
      2 => true;
    } or
    ret > 100
  );

これが、先行する同種の提案P3210との違いでもあります。

この提案の問題点は、契約プログラミング機能がC++26を目指しているのに対して、パターンマッチング機能はまだその段階にないことです。おそらく、契約プログラミング機能の方が先に完成し標準に取り込まれるでしょう。つまり、パターンマッチングの構文はまだ流動的で、最悪の場合この提案が採択されたうえでパターンマッチングだけ異なる構文になる可能性があります。

ただ、この提案を採択しなければ同じことを行うのに異なる2つの構文が確実に導入されるのに対して、この提案を採択したおけばそれを異なる2つの構文が導入されるかもしれない、まで緩和することができます。

契約プログラミング機能が先に採択されている場合にこの提案が採択されているとすると、パターンマッチングの構文として別のものが提案される場合にそれに対して余分な制約を追加することになります。その場合この提案では、統一構文から逸脱するにせよ整合性を重視するにせよ、それは契約プログラミング機能とは無関係にパターンマッチングが(を議論するEWGが)決定するべき、としています。

P3250R0 C++ contracts with regards to function pointers

契約注釈を持つ関数の関数ポインタへの暗黙変換を禁止する提案。

現在の契約プログラミング機能の仕様(P2900)では、現時点で契約チェックが関数の呼び出し側でなされるのか、呼び出し先でなされるのかが未解決のままです。とはいえ、関数ポインタの場合を除いてこれはほぼ問題にならないようです。

また現在の契約プログラミング仕様では、関数ポインタに対する契約の指定は行えません。これは、関数ポインタがそのシグネチャだけ合っていれば変換できてしまうためで、異なる契約条件の変換をどうするかという厄介な問題の議論を先送りにするための制限です。ただし、将来的にそれを有効化した場合、関数ポインタを通して関数が呼び出された場合に契約チェックを正しく行うには必ず呼び出し先でチェックを実行しなければならないことを意味して います。

ただし、現在の仕様(P2900)では、関数ポインタに対する契約注釈を禁止しているものの、契約注釈を持つ関数の関数ポインタへの変換は禁止していません。これによって、将来的に関数ポインタに対する契約注釈への拡張が行われた場合に問題が起こります。

// 契約を持つ関数
void fun(A& a) pre (a.ok());

// 関数型エイリアス
using F = void(A& a);
using G = void(A& a) pre(a.ok()); // 将来可能になったとして

// 関数ポインタ変数の宣言
F* f = fun; // ok、危険、現在から可能
G* g = fun; // ok、安全

このように同じ関数に対する関数ポインタでも、契約注釈の情報を保持したものとすべて削除されたものの2種類が共存してしまいます。

この提案は、将来の関数ポインタに対する契約注釈の指定を可能にしておくために、契約を持つ関数からシグネチャがマッチする関数ポインタヘの暗黙変換を禁止するものです。

関数ポインタに契約注釈を行えるようにする場合

  • コンパイラは呼び出し先でのチェックを省略して、呼び出し側の(場合によっては省略可能な)契約条件のチェックだけを行える
    • コンパイラの証明によって、契約条件のチェックが省略可能な機会が増える
  • 異なる契約注釈セット間での関数ポインタの変換
    • より強い契約からより弱い契約へ、またはその逆、の変換など

等のメリットがあります。

将来の関数ポインタのために設計を予約しておく場合、これらのメリットを将来的に導入する時のために設計を開いておき、なおかつ、現在の関数ポインタの使用方法を将来の変更から保護する事にもつながります。

一方、関数ポインタの契約注釈のために設計空間を予約しておく場合

  • 関数名に対してアドレスを取る操作は、明示的な変換のみが許可される
  • 型推論される場所で関数ポインタを使用できない
    • auto*やテンプレート
  • P2900は契約注釈はリフレクションに対して不可視であることを決定したが、これによってそれをコンパイル時に検出出来うる

等のデメリットがあります。

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

int f(int x) post (r: r != 0);

using function_type = int(*)(int);
auto *fp2 = f; // ERROR
auto *fp3 = (function_type)f; // OK

template <typename FT>
void g(FT func);
g(f); // ERROR
g((function_type)f); // OK

契約を持たない関数の関数ポインタへの変換、および(契約有無に関わらず)関数ポインタへの明示的変換は、これまで通りに許可されます。

この提案はSG21のレビューで好まれず、否決されています。

P3251R0 C++ contracts and coroutines

コルーチンに対する契約注釈を許可する提案。

C++26を目指している現在の契約プログラミング機能においては、コルーチンに対する契約はcontract_assertのみが許可されており、事前・事後条件の指定は禁止されています。これは、コルーチンが中断可能な関数であることから、それらの契約がどのように振舞うべきかのセマンティクスを確立できていないため、その議論と決定を急がないための措置です。

通常の関数に対する事前条件と事後条件は、関数に入る時と関数から戻る時の、パラメータ/戻り値とプログラムの状態についての条件を指定します。

コルーチンはその呼び出しの任意の時点で中断して呼び出し元にリターンすることができ、またその後の任意の時点で再開し再び関数内に入ることができます。C++におけるコルーチンは関数宣言で区別されておらず、その定義を見に行かないとその関数がコルーチンであるかどうかはわかりません。従ってコルーチンに対する事前条件と事後条件で契約可能なのは、コルーチンの開始時点の状態と、中断時点の状態についてのものだけです。

現在の契約プログラミング機能でコルーチンに対する事前・事後条件指定が禁止されているとはいっても、それは実際には簡単に回避できます。例えばコルーチンf()があるとき、auto g() { return f(); }のようにすればこのg()はコルーチンではなくなり、g()には通常の関数同様の契約注釈を指定できます。このようにした時のg()になされる事前条件と事後条件とは、ちょうどコルーチンの開始時点の状態と中断時点の状態に対する契約として機能し、それはこのようなラッピングを行うユーザーの意図するところと一致するはずです。

ただし通常の関数と異なるところが一点だけあり、それは事後条件からはコルーチン引数を一切参照できないということです(ムーブされている可能性があるため)。コルーチンに対する事後条件のこのコーナーケースを除外(コルーチン事後条件における引数参照の禁止)してまでコルーチン(の直接の戻り値であるawaitable型)に対して事後条件を指定可能にすることが、事後条件の一貫性を損なうデメリットを上回るかどうかは要検討です。このコーナーケースを回避して一貫性のためにコルーチンに対する事後条件を禁止したとしても大きな問題はないと思われます。

コルーチンに対して事後条件の指定を許可する場合の事を考えます。前述のように、その場合の事後条件はコルーチンが中断した時点での状態に対するものになります。しかしこの場合、コルーチンはまだ実行を開始してさえいない可能性があり、ユーザーがそのような状態についてアサートできることはほとんどありません。

しかし、そのような物理的なコルーチンのインターフェースではなく、論理的なコルーチンのインターフェースを見た時に、コルーチンに対する事後条件とはコルーチンが生成・または返す値(コルーチンの直接の戻り値ではない)に対するものとするのが、ユーザーの期待と合っており望ましいでしょう。ただしこれは、契約注釈がコルーチンであることをほのめかすことなく、関数インターフェースによって自然に伝播される必要があります。

この提案では、コルーチンに対する事後条件の指定は、コルーチンが直接返す値に対して適用するのではなく、そのコルーチンのawaitable型とpromise型に対して適用するようにすることを提案しています。

コルーチンのpromise型はコルーチンステート上に存在して呼び出し側とコルーチン側をつなぐ役割を持っています。promise型のyield_value()co_yieldに対して呼ばれる)とreturn_value()co_returnに対して呼ばれる)は、生成/返される値を受け取って、コルーチンに対する事後条件を事前条件としてその値に対して適用することのできる理想的な場所に位置しています。一方、awaitableな型のawait_resumeなどはコルーチンに対する事後条件をそのまま自身の事後条件として持つことのできる場所に位置しています。また、これらの関数には現在の契約プログラミング機能の仕様においても契約注釈を制限なく行えるため、このために特別な規定を増やす必要はありません。

これを実装する方法は、コルーチンの生成/返す値に対する条件と指定された(と分かっている)コルーチンに対する事後条件の注釈を、コルーチンの返す直接の戻り値型(awaitable)の一部(そのメンバ関数に対するもの)として扱う事です。例えば何かしラップを行う型を用意するなどの方法が考えられますが、これを許可するために現在の規定を弄る必要はありません(そこは実装詳細なので)。

現在の(そしておそらくC++26の)契約プログラミング機能の仕様に対して変更が必要なのは、コルーチンである関数に対する事前・事後条件の契約注釈を禁止している文言に対してだけです。コルーチンに対する事後条件は上記のように複雑になり他の関数と少し異なってしまうため有用かどうかは不明ですが、事前条件指定に関しては間違いなく役に立つはず、と主張しています。

結局この提案では

  • コルーチンに対する事前条件(と事後条件)の契約注釈を許可する
  • コルーチンに対する事前条件(と事後条件)は、promise型(およびawaitable型)の 事前条件と事後条件として扱うようにする

の2つの事を提案しています。

P3253R0 Distinguishing between member and free coroutines

同じシグネチャを持つメンバ関数とフリー関数のコルーチンを区別するようにする提案。

コルーチンのプロミス型はstd::coroutine_traits<>のテンプレートパラメータに、コルーチンの戻り値型及び呼び出し時の引数型を入れて特殊化したもの(std::coroutine_traits<R, Args...>)が使用され、それに対応するものをあらかじめ用意しておくと(すなわち、コルーチン戻り値型に対して特殊化しておけば)、そのメンバ型(std::coroutine_traits<R, Args...>::promise_type)を定義することでコルーチンで使用されるプロミス型をカスタマイズできます。

ところでコルーチンはメンバ関数としても定義することが可能であり、メンバ関数は最初の引数に暗黙にthis引数を受けており、メンバコルーチンの場合std::coroutine_traits<R, Args...>Argsの最初には暗黙のthis引数に対応する型が入っています。

このため、同じ戻り値型を共有するメンバ関数と非メンバ関数のコルーチンで、非メンバコルーチンの方がメンバ関数コルーチンの属するクラスを最初の引数に取る場合、coroutine_traitsからその区別がつかなくなります。

struct S {
  // メンバコルーチン
  R member_coro1(int x, std::string y) const;
  R member_coro2(const S&, int x, std::string y) const;
};

// フリー関数のコルーチン
R free_coro(const S&, int x, std::string y);

このmember_coro1()free_coro()のコルーチンは、プロミス型の導出のためにstd::coroutine_traitsの特殊化を、std::coroutine_traits<R, const S&, int, std::string>としてクエリします。これはコルーチン戻り値型Rから見るとmember_coro1()free_coro()を区別することができず、この2つの関数は同じプロミス型を取得します。

このようなRには例えばstd::generatorがあります。

このことが問題となるのは、std::generatorのようにアロケータをカスタマイズ可能にしようとする場合です。

コルーチンはコルーチンフレームの確保のために動的メモリ確保を行いますが、これはまずプロミス型のスコープでoperator new()が探索されます。すなわち、プロミス型でoperator new()を定義することで動的メモリ確保方法をカスタマイズでき、std::generatorでは3番目のテンプレートパラメータと合わせて任意のアロケータを受け取り、使用することができます。

このoperator new()の探索は、promise_type::operator new(std::size_t, Args...)の様なシグネチャで行われ、後半の引数はコルーチン引数がその順番のまま渡されます。

std::generator<T, V, Alloc>の場合、最も分かりやすいアロケータのカスタマイズとして3番目のテンプレートパラメータにアロケータ型を指定する方法があり、この場合にアロケータを初期化するためにはstd::allocator_arg_tのタグ値の後にアロケータを渡す必要があり、これをコルーチン引数列の先頭で行う必要があります。

using Generator = std::generator<int, void, std::pmr::polymorphic_allocator<>>;

struct S {
  // メンバコルーチン、カスタムアロケータを受け取ろうとしている
  Generator member_coro1(std::allocator_arg_t,
                         std::pmr::polymorphic_allocator<> alloc,
                         int x) const;
};

// フリー関数のコルーチン1、カスタムアロケータを受け取る
Generator free_coro1(std::allocator_arg_t,
                     std::pmr::polymorphic_allocator<> alloc,
                     int x);

// フリー関数のコルーチン2、カスタムアロケータを受け取ろうとしている
Generator free_coro2(const S&,
                     std::allocator_arg_t,
                     std::pmr::polymorphic_allocator<> alloc,
                     int x);
                     
// フリー関数のコルーチン3、カスタムアロケータを受け取ろうとしている
Generator free_coro3(const S&,
                     const std::vector<int>&,
                     std::allocator_arg_t,
                     std::pmr::polymorphic_allocator<> alloc,
                     int x);

ぱっと見ると、member_coro1()free_coro1()は正しくアロケータのカスタマイズを行えており、渡したアロケータが使用されるように見えます。一方で、free_coro2()free_coro3()は渡す引数順が間違っている(std::allocator_arg_tが2番目以降の引数で渡されている)ためアロケータのカスタマイズは上手くいかないように見えます。

実際には、member_coro1()free_coro1()、そしてfree_coro2()は渡されたアロケータを使用しますが、free_coro3()では使用されません。これは、member_coro1()の渡し方をサポートするための副作用です。

前述のように、member_coro1()free_coro2()では同じプロミス型が使用されます。すなわち、この2つの関数のシグネチャはプロミス型から見ると同じです(std::coroutine_traits<Generator, const S&, std::allocator_arg_t, std::pmr::polymorphic_allocator<>, int>::promise_type)。したがって、探索されるnew演算子シグネチャpromise_type::operator new(std::size_t, const S&, std::allocator_arg_t, std::pmr::polymorphic_allocator<>, int)のようになります。これはstd::allocator_arg_tが第一引数にわたっていないため本来カスタマイズ対象ではない(標準ライブラリ内におけるuses-allocator constructionと一貫していない)のですが、std::generatorがこの問題を考慮して設計されているため、これを特別扱いしてサポートするためにこのシグネチャでもoperator new()オーバーロードを用意しており、それによってカスタムのnew演算子が呼ばれてその中で渡されたカスタムアロケータが使用されます。

この特別扱いはmember_coro1()をサポートするためのものですが、member_coro1()free_coro2()では同じプロミス型が使用されているため、free_coro2()に対しても使用可能になります。これによって、意図せずにfree_coro2()は正常に動作しています。導出されるプロミス型が同一になるため、member_coro1()だけを受け入れてfree_coro2()を弾くということはできません。

この問題のために、std::generatorのようなコルーチン戻り値型の制作者は、free_coro1()をサポートする(これは全く正当なもの)ためにstd::size_t, std::allocator_arg_t, Allocのように3番目の引数でアロケータを受け取るoperator newオーバーロードと、member_coro1()をサポートするためにstd::size_t, const S&, std::allocator_arg_t, Allocのように4番目の引数でアロケータを受け取るoperator newオーバーロードの2種類を用意しなければならなくなります。

これによって、アロケータ引数に関してメンバコルーチンとフリー関数のコルーチンの間、またstd::generator(及び同種のコルーチン戻り値型)と他の標準ライブラリ機能の間で、不必要な矛盾が生じています。

この提案はこの問題の解決のために、最初のプロミス型の探索過程を修正しようとするものです。

この提案では、メンバ関数のコルーチンがstd::coroutine_traitsからメンバ型としてプロミス型を取得する場合、まず最初にstd::coroutine_traits<R, Args...>::promise_type_for_nonstatic_memberが定義されているかをチェックし、定義されているならこれをプロミス型として使用し、定義されていない場合は従来通りstd::coroutine_traits<R, Args...>::promise_typeを使用する、といようにします。

プロミス型の定義方法及びその後の経路に関しては一切変更がありません。この入れ子promise_type_for_nonstatic_member型として指定されているプロミス型からは、そのコルーチンがメンバ関数として定義されていることが分かるため、ここだけで4番目の引数でアロケータを受け取るoperator newオーバーロードを用意すればメンバコルーチンに対してもアロケータカスタマイズを提供できます。なおかつ、promise_type_for_nonstatic_memberがプロミス型として取得されるのは非静的メンバのコルーチンからだけなので、フリー関数のコルーチンが子のプロミス型を使用することはありません。

これにより、先程の例のmember_coro1()free_coro1()の渡し方だけが有効で(サポートされ)、free_coro2()(及びfree_coro3())の渡し方は有効ではない(サポート外)という弁別が可能になり、問題になっていた非一貫性が解消されます。

この方法では、ユーザーが明示的にstd::coroutine_traitsの特殊化にpromise_type_for_nonstatic_memberを定義しない限り、現在の動作が損なわれることはなく、下位互換性を担保することができます。

この提案は、EWGの初期レビューで好まれなかったようで、否決されています。

P3254R0 Reserve identifiers preceded by @ for non-ignorable annotation tokens

@から始まるトークン列を予約しておく提案。

C++23ではP2558の採択によって@が基本ソース文字集合(言語の構文を記述するために使用される文字の集合)に追加されました。これによって、将来のC++では@を何かしらの言語機能のための構文に使用することができます。

そのような言語機能は現在存在していませんが、この提案は、@から始まる文字列を将来的に無視できない注釈に使用するために、実質的にユーザー使用できないようにしておく提案です。

現在のC++の属性は無視できるという性質を持つため、何か注釈を行うのに必ずしも最適ではありません。属性は独立した構文空間を持つためその中で使用する名前は既にユーザーが使用しているものと衝突する心配がないという利点がありますが、何か新しい言語機能を追加する場合に無視可能という性質が望ましくなく、必ずしも新しいキーワードの追加や既存キーワード使いまわしを回避できるわけではありません。

@を使用したものとして有効なトークンはリテラル以外になく、@~のように@から始まる文字列を何かしらの注釈として使用することは他の言語でも前例があります。その具体的な提案の検討はここではしていませんが、将来のそうした機能のためにこの構文空間を予約しておくことがこの提案の目的です。

この提案ではそのような注釈の想定される例としていくつかのものをあげています

  • 契約注釈
    • @pre(x > 0)@assert(x > 0)の様な注釈
  • トリビアルリロケーション
    • クラスに対する@trivially_relocatableのような注釈によって、クラス型がトリビアルリロケーション可能であることを表明する
  • プロファイル注釈
    • P2816で述べられているようなプロファイルの指定を@enable(ranges)のように指定する
  • [[no_unique_address]]の置き換え
    • [[no_unique_address]]は実際には無視できる属性ではなかったため、非推奨にして@no_unique_addressに置き換える

この提案では、@~トークンを単に予約するという文章によって指定するだけではなく、1つのプリプロセッシングトークンにしておくことも提案しています。これは、@~トークンが現在2つのプリプロセッシングトークンとして認識されてしまうためです。これが無いと、例えば@assert(expr)のような注釈を導入しても、assert(expr)マクロの展開を妨げることができません。同様に、他の@~トークンでも先頭の@を除いた部分の文字列がすでにマクロとして定義されている場合にそのマクロの展開を防止することができず、これだと予約しておく意味がほとんどありません。

そのため、@~を1つのプリプロセッシングトークンとしてプリプロセス時に扱っておくことで、将来的に@~を使った注釈を導入する時の問題をあらかじめ取り除いておくことができます。

P3255R0 Expose whether atomic notifying operations are lock-free

std::atomicの通知・待機系関数がロックフリーとは限らない場合がある問題について修正する提案。

std::atomic(以下std::atomic_flagおよびstd::atomic_refを含む)に対してC++20で追加された通知・待機系関数(.wait().notify_~())に対しては、その型そのものに対してロックフリーであると指定される場合に(std::atomic_flagの場合に常に)当然ロックフリーであり、シグナルセーフであることが期待されます。

しかし実際にはロックフリーではない可能性がありその場合シグナルセーフでもありません。.wait()関数はスレッドをブロックするため明らかですが、通知関数についてもロックフリーな実装を取ることができるものの、オブジェクトサイズや環境によってはロックフリーとならない場合があるようです。

これらの問題の解決のため、この提案では次の事を提案しています

  • std::atomic_flagの通知・待機系関数は、ロックフリーである必要が無いことが明確になるように規定を修正
  • std::atomicis_lock_freeis_always_lock_freestd::atomic_is_lock_freeのプロパティはアトミックの通知・待機系関数に関してのものではないことを明確になるように規定を修正
    • これらのプロパティの範囲から、通知・待機系関数を除外する
  • std::atomic_flagstd::atomicstd::atomic_refメンバ関数.notify_is_lock_free()とフリー関数のstd::atomic_notify_is_lock_free、およびメンバ定数のnotify_is_always_lock_freeを追加し、アトミックの通知・待機系関数がロックフリーかどうかはそれらのプロパティが表明するようにする

これによって、通知・待機系関数がロックフリー(でありシグナルセーフ)であるかどうかを正しく判定できるようになります。

P3257R0 Make the predicate of contract_assert more regular

契約注釈から参照される変数の暗黙const化を、contract_assertでは緩和する提案。

現在の契約プログラミング機能の仕様ではP3071の採択によって、契約注釈内からローカル変数・関数引数を参照すると、それは暗黙的にconstとして扱われます。これはconstメンバ関数のメンバ変数に対する動作に似ており、オーバーロード解決の結果が変化したり、map[key]のように一部の式がエラーになったりすることが知られています。

これにより、contract_assert()の内外で同じ式に対するコンパイル結果が明確に変化します

void f() {
  int i = 0;
  if (++i < 5) { ... }        // OK
  contract_assert(++i < 5);   // P2900R6: ill-formed、この提案: well-formed
}

このように、同じソースコードが近傍に出現していても同じように動作せず、ユーザーの期待に反する可能性があります。ただし、このことは事前条件と事後条件には当てはまりません(宣言で出現し、定義から離れている場合があるため)。

void h(int x, int y)
  pre(x > 0)       // ok
  pre(y < 0)       // ok
  pre(++x < 42);   // ill-formed

別の問題として、契約条件の評価異数の問題があります。現在使用されているassertマクロによる次のようなコードは、単純にはcontract_assert()で置き換えられない可能性があります。

  void f() {
#ifndef NDEBUG
    int iter = 0;
#endif
    while (/* something */) {
      assert(++iter < 6);   // 6回以上繰り返されたらバグ
      // ...         
    }
  }

このiter変数の宣言を同様にガードする方法が無いのもそうですが、一番の問題はcontract_assertに指定された述語が無制限に評価される可能性があるためです。

assertマクロからの移行の問題であるため、これについてもcontract_assertだけが対象です。

これらの問題の解決のために、この提案は次の事を提案しています

  • contract_assertではP3071の変更を元に戻す
    • contract_assertの述語内から参照される変数のconst性は変化しない
  • その契約注釈がignoreセマンティクスを持たない場合、contract_assert()の述語(条件式)は一回だけ評価されることを規定

これによって、assertマクロからcontract_assertへの移行がより簡単になります。

この提案の内容はどちらも好まれなかったようで、SG21のレビューでコンセンサスを得ることができませんでした。

P3258R0 Formatting charN_t

charN_t文字・文字列をstd::fomart可能にする提案。

charN_tとは、char8_t, char16_t, char32_t型の事です。これらのユニコード文字型は言語サポートされているにもかかわらず、標準ライブラリのサポートに乏しく、微妙に使いにくい部分がありました。

この提案はその改善の第一歩として、これらのユニコード文字型による文字と文字列をstd::fomrat()/std::print()でサポートする提案です。

提案では、char8_t, char16_t, char32_tの文字と文字列、およびこれらの文字を使ったstd::string/std::string_view及びrangestd::format()による文字列化対象にすることを提案しています。ただし、フォーマット文字列としてこれらのユニコード文字型を使用可能にすることは提案していません。出力のエンコーディングは、現在同様にフォーマット文字列の文字型から静的に決定されます。

charwchar_tがフォーマット文字列とフォーマット対象で混在するのは許可されていませんが、この2つの文字型がフォーマット文字列として使用されている場合にcharN_tをフォーマットすることはサポートされています。

筆者の方は、libc++のstd::format()実装でこれを実装しても実装上の問題はほとんど発生しなかったと報告しています。

P3259R0 const by default

クラス型のオブジェクト宣言時のデフォルトをconstにする提案。

この提案は、以前のP3218R0で提案されていアプローチの一部(参照セマンティクスを持つ型はデフォルトでconstが付くようにエイリアスを作る)を言語機能によって実現しようとするものです。P3218R0については以前の記事を参照

この提案では、クラス宣言への指定としてconstを付加して宣言できるようにすることを提案しています。

// function_refの宣言例

template<class... S>
class function_ref;    // not defined

template<class R, class... ArgTypes>
class function_ref<R(ArgTypes...) cv noexcept(noex)> const  // 👈 const by default
{
  // ...
};

このように宣言されたクラス型の変数は、デフォルトの宣言が暗黙的にconstであるように扱われます。const指定されたクラス型からconstを取り除きこれまで通りの動作(デフォルト非const)をする型を得るには、型名にmutableを付加します。

void action() {}

void some_other_action() {
  function_ref<void ()> fr1{ action };          // デフォルトの宣言がconst宣言になる
  mutable function_ref<void ()> fr2{ action };  // 非const変数の宣言

  fr1 = function_ref<void ()>{ action };  // ng
  fr2 = function_ref<void ()>{ action };  // ok
  
  std::vector<function_ref<void ()>> v1{ fr1, fr2 };          // ng
  std::vector<mutable function_ref<void ()>> v2{ fr1, fr2 };  // ok
}

クラスのconst指定の逆の指定(つまり今まで通り)としてmutableも追加することを提案しており、さらにどちらにも引数としてbool値を取れるようにしています。

class a const {};     // デフォルトconst宣言
class b mutable {};   // `class b {};`と同じ
class c const(true) {};   // `class c const {};`と同じ
class d const(false) {};  // `class c mutable {};`と同じ
class e mutable(true) {};   // `class c mutable {};`と同じ
class f mutable(false) {};  // `class c const {};`と同じ

function_refoptional<T&>などの浅いconstセマンティクスを持つ参照セマンティクス型をデフォルトでconstにしておくことで、言語組み込みの左辺値参照との一貫性が高まり、安全に使用しやすくなります。

P3263R0 Encoded annotated char

ユーザーが指定したエンコーディングによる文字を表現可能な文字型の提案。

テキストの処理と解釈にはその文字のエンコーディングが重要となります。ユーザーが作成する可能性のあるアプリケーションの範囲を考えると、現状のC++にはソフトウェアでテキストを処理するための単一の方法がありません。

型と文字エンコーディングを対応させることでユーザーが様々なエンコーディングでテキストを識別して管理できるようにするのは望ましい方向性であり、C++でもユニコードに対応する三種類の文字型(char8_t, char16_t, char32_t)がコア言語に対して追加されています。しかしこの方法はスケールしません。存在する全ての文字エンコーディングに対して対応する文字型を標準に導入するのは現実的ではなく、追加に当たっては文字型そのものだけではなく他のライブラリユーティリティ(std::integral, std::string, std::string_viewなど)に対する対応も求められます。

この提案は、標準化委員会の関与を必要とすることなくユーザーが独自の文字エンコーディングサポートを追加できるようにするためのライブラリ機能を追加しようとするものです。

文字型の要件としては

  • 既存の型の単なるエイリアスとして扱われない一意の型である
    • using char_iso2022_t = char8_t;のようなものではない
  • 文字として実行できると期待される一般的な操作を適用可能である
    • 文字のコード単位がある値であるかをチェックする(==)や、文字があるコード単位の範囲内にある化をチェックする(> >=など)など
  • ユーザーが独自のエンコーディングを、それに期待されるすべての関連機能と共に、簡単に定義できる
  • ユーザーが文字コード単位の幅(文字サイズ)を指定できる
  • 変換するのが十分に容易である

ここでは、これらの要件を達成するために、コア言語機能での対応を必要とせずenum classを利用するものでもない、ライブラリ機能を導入しようとしています。その文字型は、次の2つのものから構成されます

EBCDICをサポートする例

// EBCDICエンコーディングのためのエンコーディング注釈型の定義
struct text_encoding_EBCDIC: public std::text_encoding // エンコーディング注釈型であることを示すための基底クラス型
{
    using char_t = char8_t;

  // optional, not required
    static constexpr std::string_view id{"EBCDIC"};
};

// EBCDIC文字型の定義
using char_EBCDIC_t = std::char_enc_t<text_encoding_EBCDIC>;


// EBCDIC文字列型の定義
using string_EBCDIC = std::basic_string<char_EBCDIC_t>;
using string_view_EBCDIC = std::basic_string_view<char_EBCDIC_t>;

エンコーディング注釈型はstd::text_encodingを継承したクラス型として定義し、メンバ型::char_tにその表現型を指定します。提案では、表現型としてはboolではない符号なし整数型であることを要求していますが、ユニコード文字型のみに制限するオプションもあげています。

エンコーディング注釈型に必要なことはこれだけで、あとはそれをstd::char_enc_t<>のテンプレートパラメータに渡すことでそのエンコーディングの文字型を得ることができます。std::char_enc_t<E>エンコーディングEによる文字型として利用可能であるために、E::char_tの薄いラッパとなる型です。std::char_enc_t<E>ではE::char_tのものを利用する形で文字演算(ほぼ整数演算)の演算子オーバーロードが定義されており、これによって文字表現型で利用可能な操作をそのまま再利用しながら、なおかつその型とは完全に別の型として扱われる、という性質を達成しています。

また、std::char_enc_t<E>は表現型への明示的変換のほか、文字の表現型での値を取得する.value()等が用意されていることで、変換も容易に行えるようにしています。

これらの機能は、先に挙げた5つの要件に加えて、標準化委員会の関与を必要とせずに新しい文字型を定義できるという要件をすべて満たすソリューションです。

ただし一つ懸念点があり、文字・文字列リテラル'c'や"string")がコア言語サポートであるためにこの型のそれを自動で追加できず、ユーザー定義リテラルや変換関数を使用することになりますが、それだと言語のリテラルにある永続的なストレージに配置されるという性質が達成できません。この提案ではこれに対する解決策を見出しておらず、未解決の問題としています。

P3264R0 Double-evaluation of preconditions

P3264R1 Double-evaluation of preconditions

事前条件がチェックされる場合に、それが2回以上チェックされる可能性があるという現在の仕様を維持すべきとする提案。

現在の契約プログラミング機能の仕様では、契約注釈の種類を問わず契約条件がチェックされる場合は2回以上チェックされる可能性があるとされています。これには異論もあり、正確に1回・少なくとも上限を設ける・正確に回数を指定する、などの別のアプローチを望む声もあります。

この提案は、事前条件に的を絞ってその利点を説明し、少なくとも事前条件については現在の仕様を維持すべきとする提案です。

ここでは、事前条件の条件式が2回以上チェックされることを許可するようにしておく利点として次の事を挙げています

  • 呼び出し側と呼び出される側の両方に対して契約チェックを行う機会を提供できる
    • ビルド済みのライブラリでチェックが有効化されていない場合でも、ライブラリの再コンパイルを必要とせずに呼び出し側でチェックを有効化できる
    • アプリケーションで契約チェックが有効化されていない場合でも、アプリケーションの再コンパイルを必要とせずに呼び出し先でチェックを有効化できる
    • 両側で契約チェックが有効化されていれば、2回チェックされる
  • 翻訳単位の片側/両側で契約チェックを有効・無効を切り替えても、ABIに影響を与えない
  • 上記の利点をライブラリのグラフ(ライブラリ内で使用されているライブラリ)に拡大しても、同じ利点が得られる
  • チェックが有効化されている場合でも効率的
    • 単一評価を保証するための実行時機構の呼び出しが義務付けられない
  • これらの利点には説得力があるため、標準が正確に一回だけ評価されるという方向性を採用した場合でも、2回以上評価されるというアプローチは(非標準の拡張として)提供されると思われる

そして、2回以上チェックされる可能性があるとしておくことによって、プログラマは契約条件式内での副作用に頼ったプログラムを書くのを回避するようになる、という副次的効果も得られるはずです。契約条件式の副作用に頼ったプログラムはそれを書いたプログラマ以外がコードを理解することを難しくするとともに、契約注釈を活用する外部ツール(静的解析など)の解析も困難にします。

他の2種類の注釈、事後条件とcontract_assertの場合、同じように規定しても事前条件程の利点が得られない可能性があるものの、少なくとも事後条件に関しては事前条件の場合と同じ利点がある、としています。

P3265R0 Ship Contracts in a TS

P3265R1 Ship Contracts in a TS

契約プログラミング機能をまずTSとして出荷すべき、とする提案。

契約プログラミング機能は今のところC++26を目指して進行しています。しかし、EWGに転送された後で、EWGのメンバが考える契約プログラミング機能と現在のそれが少し異なるものであることが表面化し、それによって契約プログラミング機能に対して機能リクエストが噴出しつつあります。

この提案は、契約プログラミング機能の提案(P2900)から期限を取り除いて、まずTSという形で出荷してもう少し時間をかけることを提案するものです。

提案ではその動機として次のものを挙げています

  • 実装経験を得る
  • 現場での使用経験を得る
  • P2900の複数の点(とくに争点となっている)に対するWG21全体のコンセンサスを得る
  • 仮想関数、コルーチン、関数ポインタに対する契約についてのコンセンサスが無い
  • 安全性の向上
    • 契約注釈は言語の他の部分同様に未定義動作の影響を受けやすく、このことは契約注釈の実行時チェックにとっても静的解析ツールの契約注釈利用時においても問題となる可能性がある

また、契約プログラミング機能に対する機能要求は現在でもいくつも出てきており、それらには以前にはなかったアプローチも含まれています。期限を重視して最小の機能をC++26に入れるMVPのアプローチでは、これらの提案が間に合わないことでそのような別のアプローチへの道を閉ざしてしまう可能性もあります。

この提案はSG21で確認された後EWGでも確認されていますが、まだ何かしらの決定は下されていません。どうやら、契約プログラミング機能の期限を決める責任はEWGにあるようです。

P3266R0 non referenceable types

型の参照型を取得できないようにするクラスの指定の提案。

この提案は、以前のP3218R0で提案されていアプローチの一部(参照セマンティクスを持つ型はアドレスを取得できない)を発展させたものを言語機能によって実現しようとするものです。P3218R0については以前の記事を参照

この提案では、referenceable(bool)のようなクラス宣言に対する指定によってそれを行おうとしています

template<class... S>
class function_ref;    // not defined

template<class R, class... ArgTypes>
class function_ref<R(ArgTypes...) cv noexcept(noex)> referenceable(false) // 参照できず、コピーのみが可能となる
{
  // ...
};

template <class T>
class optional<T&> referenceable(false) // 同上
{
  // ...
};

このように指定して宣言されたクラスは参照で保持することが禁止され、コピーのみしかできなくなります

// 参照不可能な型への参照は許可されない
function_ref<void ()>& f(function_ref<void ()>&); // ill-formed
optional<int&>& f(optional<int&>&); // ill-formed

void action() {}

int main()
{
  function_ref<void ()> fr1{ action };

  // 参照不可能な型への参照は許可されない
  function_ref<void ()>& fr2 = fr1; // ill-formed

  int i = 42;
  optional<int&> oi1{ i };  // const by default

  // 参照不可能な型への参照は許可されない
  optional<int&>& oi2 = oi1;  // ill-formed

  return 0;
}

デフォルトはreferenceable(true)であり、これは現在のクラス型の振る舞いです。

class a referenceable(false) {}; // 参照不可能な型
class b referenceable(true) {};  // class b {};と同じ

これは参照セマンティクスを持つ型の扱いをより言語参照へ近づけるもの(参照の参照を作成できない)であり、これによってダングリング参照の発生を抑制できるとしています。

P3267R0 C++ contracts implementation strategies

P3267R1 Approaches to C++ Contracts

C++契約プログラミング機能の実装戦略についてまとめて比較した文書。

ここでは主に事前条件と事後条件の契約注釈のチェックされるタイミングについての実装戦略のリストアップと比較を行っています。

説明に当たっては、次のような契約されている関数と、それを呼び出す関数および関数ポインタを使ってそれを呼び出す関数の3つで例示しています。

// 事前・事後条件を持つ関数f()
int f(int arg)
  pre(arg > 5)
  pre(arg < 2000)
  post(rv: rv % 2 == 0)
{ 
  return arg * 2; 
}

// f()を呼び出すだけの関数g()
int g() {
  return f(25);
}

using function_pointer = int(*)(int);
function_pointer fp = &f;

// f()を関数ポインタ経由で呼び出す関数h()
int h() {
  return fp(40);
}

また、チェックされるタイミングの例示のために契約条件チェックをcheck()という関数で表し、条件を最適化に利用するポイント(条件式を仮定として扱うポイント、あるいは契約チェックを省略可能なポイント)をimply()という関数で表わしています。これはアサートの持つ役割を分離して、どの性質が許可されるのかを明確にするためです。

そして、ここでは契約注釈のセマンティクスに関してはこの実装戦略に影響しないとして同等に扱っており、契約注釈のセマンティクスは翻訳単位間で一致していることを前提としています。

1. 全て呼び出し先でチェックする

事前条件と事後条件に関する全ての事を関数の定義内で行う方法で、これはもっとも簡単なC++契約プログラミング機能の実装方法です。

int f(int arg)
{ 
  check(arg > 5);
  imply(arg > 5);
  check(arg < 2000);
  imply(arg < 2000);

  int rv = arg * 2;
  
  check(rv % 2 == 0);
  imply(rv % 2 == 0);
  
  return rv;
}

int g() {
  return f(25);
}

int h() {
  return fp(40);
}

2. 呼び出し側でチェックし、呼び出し先では条件を仮定、事後条件を元のABIエントリポイントでチェック

この文書における元のABIエントリポイントとは、ABIにおけるf()の呼び出しを表すものの事です。後で出てきますが、このエントリポイントを分割することで契約チェックの異なる実装が可能になります。

このアプローチは、チェックをそれらを包含する影響がある場所(事前条件が真 => 事後条件が真という含意を使用する場所)に配置するか、チェックを省略可能な他の情報が利用できる場所に配置するため、呼び出し側での最適化に適している実装です。

int f(int arg)
{ 
  imply(arg > 5);
  imply(arg < 2000);

  int rv = arg * 2;
  
  check(rv % 2 == 0);
  return rv;
}

int g() {
  check(25 > 5);
  check(25 < 2000);

  int rv = f(25);
  
  imply(rv % 2 == 0);
  return rv;
}

int h() {
  return fp(40);
}

3. 呼び出し側でチェックし、呼び出し先では条件を仮定、事前条件を元のABIエントリポイントでチェック

2と同様に呼び出し側でチェックを行いますが、f()のABIエントリポイントを分割し、事前条件を仮定し事後条件をチェックする別のエントリポイントを挿入します。

int f(int arg) 
{
  check(arg > 5);
  check(arg < 2000);

  int rv = f@post-check(arg);
  
  imply(rv % 2 == 0);
  return rv;
}

int f@post-check(int arg)
{ 
  imply(arg > 5);
  imply(arg < 2000);

  int rv = arg * 2;
  
  check(rv % 2 == 0);
  return rv;
}

int g() {
  check(25 > 5);
  check(25 < 2000);

  int rv = f@post-check(25);
  
  imply(rv % 2 == 0);
  return rv;
}

int h() {
  return fp(40);
}

4. 両側でチェックする

アプローチ1と同様に元のABIエントリポイントを保持し、なおかつ関数の呼び出し側と呼び出し先の両方でチェックを行います。この方法は一見冗長に見えますが、関数の両サイドに最適化ポイントを提供します。

int f(int arg)
{ 
  check(arg > 5);
  imply(arg > 5);
  check(arg < 2000);
  imply(arg < 2000);

  int rv = arg * 2;
  
  check(rv % 2 == 0);
  imply(rv % 2 == 0);
  
  return rv;
}

int g() {
  check(arg > 5);
  check(arg < 2000);

  int rv = f(25);
  
  check(rv % 2 == 0);
  imply(rv % 2 == 0);
  
  return rv;
}

int h() {
  return fp(40);
}

5. 遅延チェック

このアプローチと異なり、この方法では契約チェックは通常行われません。契約チェックの実行は外部ユーザー(プロファイラやデバッガなど)が指定する場合に実行時の任意のタイミングで行われます。

template <typename T>
struct contract_raii_handle {
  contract_raii_handle(T&& lambda)
  : _check(std::move(lambda))
  , result(2);
  {}
  bool operator()() {
    if (result == 2) {
      result = _check();
    }
    return (bool)result;
  }
};
auto add_pending_contract_check(auto lambda) { return contract_raii_handle<decltype(lambda)>(std::move(lambda)); }

int f(int arg)
{
  auto pre_handle_1 = add_pending_contract_check([=]{ return arg > 5; });
  auto pre_handle_2 = add_pending_contract_check([=]{ return arg < 2000; });
  return arg * 2;
}

int g() {
  check(arg > 5);
  check(arg < 2000);

  int rv = f(25);
  
  auto post_handle_1 = add_pending_contract_check([=]{ return rv % 2 == 0; });
  
  return rv;
}

int h() {
  return fp(40);
}

この実装はC++による近似実装であり、より効率的な実装を行うこともできます。

6. 実行時のセマンティクス選択

int f(int arg)
{ 
  bool __run_checks = should_check(__func__);
  if (__run_checks) {
    check(arg > 5);
    check(arg < 2000);
  }

  int rv = arg * 2;
  
  if (__run_checks) {
    check(rv % 2 == 0);
  }
  
  return rv;
}

int g() {
  return f(25);
}

int h() {
  return fp(40);
}

7. プログラムロード時のセマンティクス選択

6のアプローチにおいて、関数呼び出し時ではなくプログラムロード時に契約チェックを行うかを指定してシンボル解決を行うことで、チェックを行う実装の選択をプログラムロード時に解決するものです。

static int f__check(int arg)
{ 
  check(arg > 5);
  check(arg < 2000);

  int rv = arg * 2;
  
  check(rv % 2 == 0);
  
  return rv;
}

static int f__nocheck(int arg)
{ 
  int rv = arg * 2;
  return rv;
}

static void *f__resolver() {
  // As an example of a condition to select on
  // This will be checked on the first invocation of f().
  return getenv("mustgofaster") == nullptr ? &f__check : &f__nocheck;
}

int f(int arg) __attribute__((ifunc("f__resolver")));

int g() {
  return f(25);
}

int h() {
  return fp(40);
}

C++プログラムの開始後main()が実行されるまでの間にこれを実行することはオーバーヘッドが大きすぎる可能性がありますが、リンカ/ローダーの機能(GNU ifuncなど)を使用することでプログラム開始前のロード時にシンボル解決を行って、オーバーヘッドを削減できます。

比較

各アプローチの特徴は次のようになります

  1. 全て呼び出し先でチェックする
    • 特徴: 関数の実装内で全ての契約チェックを行う。ABIの変更は発生しない
    • 利点: 実装がシンプル
    • 欠点: 呼び出し側での最適化の余地がほとんどない
  2. 呼び出し側でチェックし、呼び出し先では条件を仮定、事後条件を元のABIエントリポイントでチェック
    • 特徴: 呼び出し側で契約チェックを行い、呼び出し先側は条件を仮定する。事後チェックが元のABIエントリポイントとなるため、ABIの変更は発生しない
    • 利点: 呼び出し側での最適化が可能になる
    • 欠点: 関数ポインタを使用する場合に契約がチェックされない。呼び出し側と呼び出し先でコンパイラフラグが異なっているとチェックされない場合がある
  3. 呼び出し側でチェックし、呼び出し先では条件を仮定、事前条件を元のABIエントリポイントでチェック
    • 特徴: 呼び出し側で契約チェックを行い、呼び出し先側は条件を仮定する。事前チェックを元のABIエントリポイントとし、ABIを変更した名前の別のエントリポイントを追加する
    • 利点: 呼び出し側での最適化が可能であり、ABIエントリポイントを変更することで関数ポインタの場合でも契約チェックが可能になる。翻訳単位が分かれていてもチェックが欠落することはない
    • 欠点: 関数ポインタからの呼び出しでチェックを省略できない
  4. 両側でチェックする
    • 特徴: 元のABIを維持しながら、呼び出し側と呼び出し先側の両方でチェックを行う
    • 利点: 呼び出し側でも事後条件に関する最適化ポイントが得られる。翻訳単位が分かれていてもそれぞれの側でオンオフを切り替えられ、どちらかの側でチェックが有効になっていればチェックされる
    • 欠点: 契約チェックが重複するため、オーバーヘッドが発生する可能性がある
  5. 遅延チェック
    • 特徴: 実際のチェックは行わず、契約を保留にしておき、プロファイラやデバッガなどの外部ツールからの要求に応じて評価する
    • 利点: 高コストな契約チェックを必要な場合にのみ実行できるため、実行時のオーバーヘッドを削減できる
    • 欠点: 契約違反の検出が遅れる可能性がある。この機構そのものに避け難いオーバーヘッドがある
  6. 実行時のセマンティクス選択
    • 特徴: グローバルなクエリ関数によって実行時に契約チェックの有効無効を取得し、有効な場合にのみチェックを行う
    • 利点: パッケージマネージャ等のビルド済みライブラリを配布する存在が、契約の有効無効で配布するバイナリを分ける必要が無い
    • 欠点: 実行時のオーバーヘッドが追加される
  7. ロード時のセマンティクス選択
    • 特徴: 環境変数などを介して、プログラムロード時に契約チェックを行うかどうかを決定する
    • 利点: 6のアプローチと同じメリットが得られ、実行時オーバーヘッドを回避できる
    • 欠点: 関数のインライン化を阻害する

このような実装を考慮することで、各実装アプローチによる契約条件評価回数を求めることができます。

アプローチ 評価回数の最小値 評価回数の最大値
全て呼び出し先でチェック 1 1
呼び出し側チェック(3) 0 1
呼び出し側チェック(4) 1 1
両側チェック 2 2
遅延チェック 0 1(∞)
実行時セマンティクス選択 0 1
ロード時セマンティクス選択 0 1

この文書はどのアプローチが良いかを提案するものではなく、可能なアプローチを列挙子比較する単一の文書を提供することを目的とするものです。

P3268R0 C++ Contracts Constification Challenges Concerning Current Code

契約注釈内での暗黙const化の影響を見積もる文書。

現在の契約プログラミング機能の仕様では、契約注釈内から参照される外部のもの(関数引数・ローカル変数)は暗黙にconstとして扱われますが、これに対してコードの他の部分との不一致等の懸念が提起されています。

この文書は、筆者の方の所属する企業のコードベースの調査によって、それがどの程度影響するかを見積もったものです。

結果としては、(このコードベースでは)300個に1個のアサートについて注意が必要(const化の影響を受ける可能性がある)で、そのような行は100000行に1行程度発生すると報告しています。

そして、結論としては暗黙const化によるコードベースヘの影響は小さく、その仕様を削除する必要は無いとしています。

P3269R0 Do Not Ship Contracts as a TS

契約プログラミング機能をTSとして出荷することに反対する提案。

契約プログラミング機能の提案(P2900R6)がEWGに転送されて以降、あるベンダが契約プログラミング機能のある側面について継続的に反対している他、いくつかの側面でEWGで合意が取れていないことが分かりました。それを受けてP3265R0では契約プログラミング機能をTSとして出荷することを提案しています。

この提案は、それとは逆に契約プログラミング機能はTSを目指さずにC++26に導入すべき、とするものです。

提案ではTSではなく直接ISを目指すべき理由として次の事を挙げています

  • 契約プログラミング機能はC++の安全性向上に役立つ
    • 安全性の向上は急務
  • 将来の拡張のための基礎
    • 現在の契約プログラミング機能は完全なものを目指しておらず、将来拡張可能な現在合意できる最小のもの
    • TSで達成できることは、C++26に入れても達成できる
  • Contracts MVPは品質の妥協ではない
    • MVPは機能セットの妥協であり、これはWG21が合意可能な最小のものであって、品質の妥協ではない
    • C++26に間に合わない場合はTSではなくC++29を目指すべき

また、TSが有用ではない理由として次の事を挙げています

  • 時間とリソースの非効率な使用
    • TSプロセスは時間と手間がかかる
      • 公開だけで1年かかる
    • そのためのリソースを、現在のP2900R6に対する意見の相違点や問題の解消に費やすべき
    • そもそも、TSに行くべきかを検討するこの時間ももったいない
  • 実装経験を得るためにTSは不要
    • 妥当な品質の仕様書とそれを実装する時間と資金があれば、実装経験を得ることができる
    • 仕様書がTSとして公開されているかは重要ではない
  • TSは未解決の問題の解決に役立たない
    • TSを公開する前に、TSが回答すべき質問のリストを作成する必要がある
    • P2900R6に対するそのようなリストはすでに得られており、C++26サイクルの残りの時間で解決可能であると見積もられている

最後に、現在のContracts MVP(P2900R6)に対する未解決の問題をここでもまとめています

  • 仮想関数に対する契約
    • まだMVPにマージ前だが成熟した提案がすでにあるため、C++26に間に合うと考えられる
  • 関数ポインタと契約
    • MVPは既に、関数が関数ポインタから呼ばれる場合でも契約条件がチェックされることを求めている
    • 関数ポインタ自体に対する契約注釈は、現在のC++のモデルに適合しないためMVPの範囲外
  • コルーチンに対する契約
    • コルーチンでは事前条件と事後条件がサポートされていない
    • SG21およびEWGのほとんどの人はこれはMVPの範囲外だと考えている
  • 評価回数と暗黙constについて
    • 最近特に議論されている分野であり、提案もいくつも出揃っている
    • 解決策はどれも、どれが間違っているというものではなく、何を優先すべきかが異なるだけ
    • この優先すべきものを決めるだけであり、この作業は既に始まっている
  • 違反ハンドラからの例外送出
    • 違反ハンドラからの例外送出を許可する・しないのトレードオフについては、議論し合意を得る必要がある
    • 不可能な作業ではなく、TSは役に立たない
  • 標準ライブラリでの契約の使用
    • TSを出荷しても、この問題の解決には役に立たない
    • 現在のところ、標準ライブラリの仕様に契約注釈を規定することは時期尚早であり、実装を許可するものの必須とはしないコンセンサスがある
  • 契約と未定義動作
    • 契約注釈は、それが存在する場所よりも後の未定義動作(のタイムトラベル最適化)による影響と、契約条件式内での未定義動作による影響の2種類の影響を受ける
    • いずれの問題も、一定の解決を合意済み
    • 未定義動作を制限するサブセットの利点は不明であり、TSにしたとしてもこの解決に役立たない
    • 契約注釈の問題は、安全ではないことではなく存在しないこと

SG21では契約をTSとしないことに合意が取れているようですが、最終的に決定するのはEWGのようです。

P3270R0 Repetition, Elision, and Constification w.r.t. contract_assert

contract_assertの現在の問題点について、原則に照らして検討する提案。

この提案ではまず、契約注釈の現在の仕様に関する次の2つの問題点

  1. 契約条件式内での変数のデフォルトconst
  2. 実行時の契約条件式の評価回数
    • 2回以上評価される可能性があり、省略可能でもあること

について、特にcontract_assertがこの懸念点を解消可能であるか、他の契約注釈とどの程度異なる必要があるかなどについて検討しています。

提示されているソリューションにおいては、次の機能単位毎に取り除くとどうなるかについて検討しています

  • EL (Elision Clause): コンパイル時に述語が常に偽と判断できる場合に、その評価を省略できる規定
  • RE (Repetition): 述語を複数回評価することを巨kする規定
  • IM (Implementation Latitude): 一つの翻訳単位内で、全て同じ契約注釈のセマンティクス(チェックするかしないか)を適用することを許可する
    • Cアサートのように、1つのフラグで全ての契約注釈のセマンティクスを一括制御する

提示されているソリューションは次の6つです

  • ソリューション A: C++26では何もせず、C++29を待つ
    • 利点: C++29に向けて、あらゆる選択肢を残せる
    • 欠点: C++26では現状の問題が解決されない
  • ソリューション B: P2900を現状(P2900R7)のまま維持
    • 利点:
      • contract_assertの仕様が、prepostと一貫性を保てる
      • ライブラリ側とアプリケーション側で独立したチェックが可能になる
      • 教える内容が少なく、副作用についても深く教えなくて済む
      • const 化との整合性が取れる
      • 破壊的な副作用を含む単体テストベータテストが容易になる
      • デフォルトで副作用の使用を抑止できる
      • 実装の選択肢が最も広範囲に渡る
      • デフォルトの動作を、後方互換性を保ちながらソリューション C、D、E、または F に移行できる
    • 欠点: 現状の問題が解決されない可能性がある
  • ソリューション C: 省略条項 (EL) を削除した P2900 (P2900 - EL)
    • 利点:
      • 述語を複数回繰り返すことで、破壊的な副作用のテストが可能になる
      • 静的に0であると証明できない冪等な副作用への依存が可能になる
      • デフォルトの動作を、後方互換性を保ちながらソリューション E または F に移行できる
    • 欠点: 副作用の扱いに関する問題が残る可能性がある
  • ソリューション D: 繰り返し (RE) を削除した P2900 (P2900 - RE)
    • 利点:
      • 表明述語の評価コストが増加することがない
      • 既存の開発者にとって馴染みやすい方法で教えられる
      • デフォルトの動作を、後方互換性を保ちながらソリューション E または F に移行できる
    • 欠点: 柔軟性が低く、いくつかのユースケースに対応できない可能性がある
  • ソリューション E: 省略 (EL) と繰り返し (RE) を削除した P2900 (P2900 - EL - RE)
    • 利点:
      • ソリューションCとDの利点を併せ持つ
      • デフォルトの動作を、後方互換性を保ちながらソリューション F に移行できる
    • 欠点: 柔軟性がさらに低くなり、適用範囲が限定される
  • ソリューション F: 省略 (EL)、繰り返し (RE) を削除し、翻訳単位ごとに1種類のセマンティクス(チェック済みまたはチェックなし)のみを持つ P2900 (P2900 - EL - RE - IM)
    • 利点:
      • ソリューションC, D, Eの利点を併せ持つ
      • C の assert と同様に動作し、翻訳単位ごとに契約アサーションがすべてチェックされるか、すべてチェックされないかのいずれかになる
    • 欠点:
      • 後方互換性を保つ移行パスがない
      • 柔軟性が最も低く、prepostとの整合性が低い

そして、この作業の過程で現在のassertマクロを完全に代替することのできるソリューションが無いという新たな問題が認識されたため、その対策としてP3290で提案されているソリューションを現在のContracts MVPにマージすることを提案しています。

P3290の詳細は後の方で詳しく説明しますが、概ね次の3つのものからなります

  1. 従来のアサーションシステムが契約違反ハンドラを呼び出せるようにするためのAPIの提供
  2. C++におけるassertマクロの定義を変更して、cerrに診断を出力する代わりに契約違反ハンドラを(条件付きで)呼び出せるようにする
    • これにより、ソースコードの変更なしで(場合によっては再コンパイルなしで)既存のコードは契約違反ハンドラを利用できる
  3. contract_assertをミラーする新しいキーワードを提供する
    • このキーワードによるアサーションは、デフォルトでC++標準のassertマクロとフラグ互換の動作をする
    • 既存のCassertからの移行を容易にチェックできる

結論としては、先に挙げた6つのソリューションのいずれよりも、現在のContracts MVPにP3290R0をマージしたもの(P2900R7 + P3290R0)が適しているとして、このソリューションを推しています。

P3271R0 Function Usage Types (Contracts for Function Pointers)

契約注釈を指定可能な新しい関数型の提案。

内部で呼び出す操作を引数で受け取る関数fancy_calculation()があるとします。このfancy_calculation()は渡される関数に対して次の事を期待しています

  • 関数はint型の引数を1つだけ取る
  • 関数はint型の結果を返す
  • 関数に渡す引数は正の値
  • 関数の結果は正の値であり、渡した数より大きくはならない

例えば次の関数はこの要件を満たすものです

int identity( const int x ) { return x; }
int halve ( const int x ) { return x/2; }

一方で、満たさないものも容易に考えることができます

int twice ( const int x ) { return 2*x; }

fancy_calculation()のユーザーは関数ポインタや関数参照を使用してfancy_calculation()に関数を渡すことができます。

int fancy_calculation(int(&f)(int));

fancy_calculation()は内部で渡された関数を呼び出します。

ユーザーはまた、渡す関数(の候補)に対して契約注釈を指定している場合があります。

int identity( const int x ) post( r: r == x );
int halve ( const int x ) post( r: r == x/2 );
int twice ( const int x ) pre( can_multiply(2,x) ) post( r: r == 2*x );

fancy_calculation()の提供者は契約注釈を使用してこの関数の契約を明文化してチェックを行おうとします

int fancy_calculation( int(&op)(int), const int start )
  pre( start >= 0 )
  post( r: r >= 0 )
  post( r: r <= start )
  post( r: r == op(r) );

しかし、これらのアサーションfancy_calculation()の動作についてのもので、fancy_calculation()が受け取って内部で呼び出す操作(op)についてのものではありません。そのようなものを書く方法で現在の契約プログラミング機能でサポートされている方法として、contract_assert()によって実際の呼び出し前後でチェックを行う、という方法があります。

int fancy_calculation( int(&op)(int), int start ) {
  int current = start;

  while( true )
  {
    contract_assert( current >= 0 ); // our responsibility
    
    int next = op( current );
    
    contract_assert( next >= 0 ); // Not our responsibility: these
    contract_assert( next <= current ); // fail if op was poorly chosen.
    
    if ( current == next )
      break;

    current = next;
  }
  
  return current;
}

この方法はあまり良い方法とは言えません。呼び出しが複数回に及ぶ場合冗長でエラーが発生しやすくなり、これらのアサーションのための追加コードが必要になる場合もあります。そして、これらのアサーションは本来opを渡すユーザー側に対してその要件を通知するものですが、関数を呼び出すコードからは見えなくなっています。

このような用途に適したソリューションが必要であり、この提案はそのソリューションを提供しようとするものです。

この提案では、いくつかのステップを踏んでそのようなソリューションを構成しています。

1. fancy_calculation()が渡される関数に課す要件を名前の下にまとめる

例えば、次のような関数型likeな記述によってその要件を表現します

int fancy_op( const int x ) usage
  pre( x >= 0 )
  post( r: r >= 0 )
  post( r: r <= x );

これはこの提案で関数利用型(function usage type)と呼ばれている、新しい関数型です。この型はint(int)な関数が特定の呼び出し元によって使用される方法について記述しています。

2. fancy_calculation()で使用するためにある関数を選んだ時点で、その関数がfancy_opでの使用を意図していることを明示する構文を使用する

void print_fancy_results( int start ) {
  const std::array ops = {
    &fancy_op{identity},  // good choice
    &fancy_op{halve},     // good choice
    &fancy_op{twice}      // bad choice!
  };

}

fancy_op{identity}identity()関数を参照するfancy_op型の左辺値式であり、コンパイルの時点で、fancy_calculation()に渡す操作の利用契約と選ばれた関数の契約注釈の両方が、それを検証しようとするツールで利用可能になります。ただし、この提案ではこのような検証までも提案はしていません。

3. 型システムによって、使用する関数が選ばれた時点から呼び出される時点までの保管チェーンを作成し、各ステップで保管されている関数が使用方法に適合していることを表明する

int fancy_calculation( fancy_op& op, int start );
  // halve/twiceはfancy_op&引数に束縛できないが、fancy_op{halve}/fancy_op{twice}は束縛できる

fancy_opのポインタ/参照は、単なるint(int)のポインタ/参照あるいはほかの関数利用型のポインタ/参照とは型レベルで異なります。想定する実装では、fancy_opのポインタ/参照はint(int)へのポインタ/参照と同じ実行時表現を持ち、型の区別はコンパイル時とリンク時の処理のために使用されます。

4. fancy_opのポインタ/参照を介して間接呼び出しが行われる場合、fancy_opで記述されている要件がその呼び出し時に満たされているかチェックされる

ここでチェックされる条件は参照先にある関数の契約注釈とは無関係にチェックされるもので、fancy_opの利用側が課す追加条件となる。

int fancy_calculation( fancy_op& op, int start ) {
  
  ...
  
  int next = op( current );
  // 実行は次の順序で行われる:
  // fancy_opの事前条件チェック
  // 参照先関数の事前条件チェック
  // 参照先関数の本体実行
  // 参照先関数の事後条件チェック
  // fancy_opの事後条件チェック
  
  ...
}

例えばopにtwiceが使用されて、twice()の事前条件違反とfancy_opの事後条件違反が起こると、twice()を選択した誤りがここで検出されます。

この提案はこのように、契約注釈を指定可能な新たな関数型を導入して、それに対する関数参照/ポインタを導入することで、参照される関数の契約とは別に、個別の関数参照/ポインタの用途ごとに特化した契約を持つ関数参照/ポインタを作成できるようにしようとするものです。

この提案では、契約が関数の型を変更すべきではなく、型システムに複雑さを持ち込むべきではなく、既存のコードベースへの統合に過度の変更を要求すべきではないという設計原則を重視しており、これらを達成することでこの提案の変更は、C++プログラムにおける契約と関数ポインタのより堅牢でユーザーフレンドリーな相互作用を目指しています。

この提案はひとまず、MVP後の契約プログラミング機能の拡張案として検討していくことがSG21で確認されています。

P3273R0 Introspection of Closure Types

静的リフレクション機能で、ラムダ式クロージャ型からの情報取得をサポートする提案。

ラムダ式クロージャ型はあまり詳細にその型の性質について指定されておらず、特に参照キャプチャした場合にクロージャ型の非静的メンバ変数に対応する何かが存在するかどうかは未規定とされています。これはラムダ式の実装において最適化を行うことを意図したもののようです。

しかし、C++26へ向けて進行中の静的リフレクション機能(P2996)ではこのために、クロージャ型のレイアウトに関するリフレクションの適用をどうするかが問題となっているようです。

クロージャ型は計算とそれに関するデータをひとまとめにパッケージングして扱うための簡単な方法であり、標準アルゴリズムをはじめとしてC++プログラミングの様々な部分で広く使用されています。特に、CUDAにおいては、GPUアルゴリズムを実行することを重視しており(標準並列アルゴリズムやThrust)、クロージャ型を特別に扱ってホストとデバイスの両側で透過的に使用できるようにしています。

クロージャ型を静的リフレクションで支障なく扱えることは、GPU以外のメモリアドレス空間を跨いでデータや計算をマーシャリングする必要があるコード(ネットワーク、プロセス間通信、シリアライズなど)においても有用となります。

このような機会を閉ざさないようにするために、この提案はクロージャ型が参照キャプチャした場合のレイアウトを予測可能にすることで、リフレクションにおける適用可能性の問題を解消しようとするものです。

この提案では、参照キャプチャしたラムダ式クロージャ型に関する現在の規定(参照キャプチャに対応する非静的メンバ変数が存在するかは未規定)を変更して、参照キャプチャの場合でも参照キャプチャした物ごとに参照メンバが存在することを規定(無名かつ宣言準は不定)するようにするものです。

この提案では、既存の実装は全て参照キャプチャを参照またはポインタをクロージャ型のメンバとすることによって実現しているため、この変更によって振る舞いの修正が必要となる実装は存在しないか、存在したとしても大きな影響はないとしています。また、クロージャ型の最適化についても、この変更と静的リフレクションの導入後に、静的リフレクションによってクロージャ型の情報を取得されていたとしてもそのラムダ式を最適化することを妨げることはないとしています。

この提案はSG7を全会一致でパスしてEWGに転送されたようです。

P3274R0 A framework for Profiles development

安全性向上のためのプロファイルをC++に導入するための開発フレームワークについて解説する文書。

これは以前にP2687R0等で示されていた、プロファイル(ブロックなどに対するアノテーション)によって部分的に安全性の保証を強化するアイデアのより具体的なものです。関連するものについては以前の記事等を参照

この文書は、以前のこれらのアイデアを具体的な機能提案とするステップの最初のもので、その設計思想や大枠について解説したものです。

まず、プロファイルを指定する方法は[[Profiles::enable(ranges)]]のような属性構文とすることを提案しています。プロファイルはソースコードで明示的に要求する必要があるものでありながら、プロファイルの指定に対して単一のコードベースを維持することができる構文として属性構文が選択されています。例えば、あるソースコードにプロファイルを指定した時でも、プロファイルをサポートしないコンパイラとサポートするコンパイラの両方でコンパイルして実行可能な状態を維持することができます。

そして、このようなプロファイルが指定されたコードでは、その存在そのものや他のオプションによって検証をトリガーできるようにして、静的解析と実行時検査を組み合わせた一定の安全性の保証を得られるようにします。

ここでは、そのようなプロファイルの初期のものとして簡単に達成できるものがリストアップされています。ただし、それらはあくまで初期のものであり、これが完全なものというわけではありません。この文書の目的は、委員会の人々が自分の専門分野に注力しながらも協力できる部分では協力して、段階的にプロファイルを含めた機能を成長させていくことにあります。この文書のいうフレームワークとは、そのような独立した作業を取りまとめる一つの土台として作用するものです。

そのフレームワークの一部として、プロファイル仕様のフォーマットが提示されています

  • 名前
    • (希望する)プロファイル名
  • 定義
    • 提供される保証の仕様
  • 影響
    • 保証を提供するために何をする必要があるか
  • 初期バージョン
    • ツールチェーンで比較的簡単に実装できる初期/限定バージョンの提案
  • 備考
    • 必要な場合の追加コメント
  • 質問
    • 設計に関する疑問
  • 詳細な仕様
    • 保証の詳細な仕様と、保証を実施するために必要なテストがどこにあるか

このフォーマットに従って、ここであげられている初期プロファイルは次のものです

  • Type
    • 定義: すべてのオブジェクトはその定義に従ってのみ使用される
    • 影響
      • 静的チェックと実行時チェックを組み合わせを必要とする強力な保証
      • Ranges、Invalidation、Algorithms、Casting、RAII、Unionなどのより単純なプロファイルの結合となる可能性がある
    • 初期バージョン: Initialization、Pointers、Rangesから開始し、他の初期プロファイルが利用可能になったら追加
    • 備考
      • 完全な型安全性を達成するためには、spanなどの抽象化の実装外部での生ポインタの添字アクセス、nullポインタの逆参照、無効化されたポインタの使用、ダングリングポインタ、メモリリーク(リソースの枯渇につながる)を排除する必要がある
        • これによって、従来のCスタイルのコードは排除されるが、C++11以降の推奨事項に従った多くのコードは排除されない
      • 完全な従来のC++ と 完全な型とリソースの安全性 の両方を同時に実現することはできない
    • 質問
      • ヒープの枯渇は型エラーか?
        • 終了が選択肢にない一部のアプリケーションでは、ヒープの枯渇を処理できるため、処理される場合は型エラーではない
      • リソースリークは型エラーか?
  • Arithmetic
    • 定義: オーバーフローとアンダーフローはエラーをトリガーし、縮小変換はエラーをトリガーする
    • 影響: コンパイラーがチェック済み算術演算を使用する必要がある
    • 初期バージョン: チェック済み算術演算ライブラリを使用できる。Arithmeticプロファイルが使用されている場合、コードジェネレーターがそのようなライブラリを使用する可能性がある
    • 備考
      • round()truncate()が必要。チェック済み算術演算ライブラリは存在するが標準ではない
      • 式内で符号付きと符号なしを混在させるとエラーの重大な原因となる。特に、標準ライブラリのサイズに符号なし整数型を使用すると困難な問題が発生する
    • 質問
      • 「Arithmetic」は「Cast」を暗示する必要があるか?
        • 推奨される回答:はい
  • Concurrency
    • 定義: データ競合がない。デッドロックがない。外部リソース(ファイルなど)の競合もない
    • 質問
      • ロックの競合が原因で発生する優先順位の逆転や遅延も処理する必要があるか?
        • 推奨される初期回答:いいえ
    • 備考:
      • このプロファイルは提案されているプロファイルの中で現在最も成熟していない。プロファイルに関連する作業は特に行われていないが、並行処理の問題は他のコンテキスト(コアガイドラインやMISRAC++など)で集中的に調査されているため、初期作業についていくつかの提案を行うことができる
        • スレッド:スコープ関連の問題を減らすために、threadよりもjthreadを優先する
        • ダングリングポインタ:jthreadをコンテナーとみなし、リソースの有効期間(RAII)と無効化に関する通常のルールを適用する
        • エイリアシング
          • ポインタが別のスレッドに渡されたかどうかを静的に検出する
          • 初期バージョンでは非自明な制御フローにおけるポインタ操作に制限が必要になる
          • 一般に、すべてのエイリアシングを静的に検出できるわけではないため複雑すぎるコードは拒否する必要があり、「複雑すぎる」の定義が不可欠となる
            • そうしないと、コンパイラーの非互換性のために移植性の問題が発生する
        • 無効化:unique_ptrと、無効化操作を行わないコンテナ(gsl::dyn_arrayなど)を使用してスレッド間で情報をやり取りする
        • 可変性:constポインタを渡す(および保持する)ことを優先
        • 同期
          • scoped_lockを使用してデッドロックの可能性を減らす
          • 可変データの複数のスレッドにおけるエイリアスを静的に検出し、それらを介したアクセス時に同期の使用を強制する可能性を検討する
          • スレッド間でのエイリアシングを防ぐために、unique_ptrを使用する
      • ロックフリープログラミングを検討する必要があります
  • Ranges
    • 定義: []を使用した範囲外添字アクセスはエラーをトリガーする
    • 影響
      • 生ポインタの添字アクセスは許可されない
      • 実行時チェックが常に実行されるように、UBに基づくタイムトラベル最適化を排除する必要がある
    • 初期バージョン: 生ポインタの添字アクセスを禁止し、vector、span、view、stringの添え字アクセスをチェックする
    • 質問
      • std::arrayはどうする?
      • 「Algorithms」は「Ranges」の一部にする必要がある?
        • 推奨される回答:はい
      • Pointers(§3.5)は Ranges(§3.4)の一部にする必要がある?
        • 推奨される回答:いいえ、または部分的に
    • 備考: すべての添字アクセスをチェックするだけでは、多くのアプリケーションにとって実行時コストが許容できないものになる。したがって、個々のチェックは操作が範囲内であることを確認するチェック(単一のチェックによる)でサポートする必要がある。これは、範囲ベースforループとrangeアルゴリズムを強く推奨することを意味する
  • Pointers
    • 定義
      • すべてのポインタはオブジェクトを指しているかnullptrのどちらか
      • すべてのイテレータは要素または範囲の終わりを指している
      • ポインタまたはイテレータを介したすべてのアクセスは、nullptrや範囲の終わりを指すポインタを介したものではない
    • 影響: 信頼できる領域の外(spanの外側など)でのポインタ演算を排除
    • 影響: ダングリングポインタを排除する
    • 初期バージョン
      • not_nullを必須にする
      • 範囲バージョンのアルゴリズムの使用を必須にする
      • 範囲を使用する場合、範囲チェックされたspan、範囲ベースfor、またはアルゴリズムを必須にする
    • 備考
      • ポインタに提供される保証はすべて、組み込みポインタではないがオブジェクトを識別するオブジェクトにも同様に適用される必要がある
        • たとえば、ラムダキャプチャやunique_ptrなど
      • すべての間接参照操作をチェックするだけでは多くのアプリケーションにとって実行時コストが許容できないものになる。したがって、多くの用途でポインタを1回だけチェックする手法をサポートする必要がある
        • not_null はその方法の1つであり、スマートコンパイルは別の方法
  • Algorithms
    • 定義
      • 誤って指定された範囲(イテレータのペアやポインタとサイズなど)による範囲エラーはない
      • 無効なイテレータの逆参照はない
      • 範囲の終端イテレータの逆参照はない
    • 影響
      • シーケンスを識別するためにイテレータのペアではなく範囲を一貫して使用し、出力ターゲットを示すために単一のイテレータを使用する
      • 範囲へのアクセスは範囲チェックされている必要がある
    • 初期バージョン
      • すべての標準ライブラリアルゴリズムの範囲バージョンを提供する
      • すべてのコンテナー(c)について、pcの終わりを1つ過ぎたものであるかどうか(つまり、p != end(c) であるかどうか)をチェックするnot_end(c,p)チェックを提供して、静的解析が戻り値として使用されるイテレータが逆参照される前にチェックされることを確認できるようにする
    • 備考: 無限の範囲(一部のostreamなど)と自動的に拡張される範囲(back_inserterへの書き込みなど)では、(この抽象化レベルでは)範囲チェックは必要ない
    • 詳細な仕様: このような仕様の初期段階については、付録を参照
  • Initialization
    • 定義: すべてのオブジェクトは明示的に初期化される(デフォルトコンストラクタも含めて)
    • 影響: すべてのメンバーが(一度だけ)初期化されていることを確認するために、コンストラクターを(再帰的に)チェックする必要がある
    • 初期バージョン:
      • 初期バージョンでは、コンストラクターがすべてのメンバーを適切に初期化すると単純に信頼する
      • 最終バージョンでは、静的解析を使用して、すべてのメンバーが初期化されていることを確認する
    • 備考
      • この保証は、すべてのプロファイルに不可欠
      • 保証された初期化により、UBが大幅に削減される
      • 初期化を保証するための、より巧妙で制限の少ないスキームは可能だが、実装とプログラマによる管理が難しくなる
      • 暗黙的な初期化による明示的な初期化の回避はそれ自体がエラーの原因となる。これは、デフォルト値が有効であっても間違っている可能性があり、すべてのユーザー定義型に適切なデフォルト値があるとは限らないため
      • パフォーマンスが重要なアプリケーションでは初期化の強制が望ましくない場合がある
        • チェックの抑制や注釈、または特定の初期化されていない型等の方法によって処理する必要がある
  • Casting
    • 定義: キャストは、その定義に反する方法でオブジェクトへのアクセスを許可しない。キャストはそのソースと等しくない値を持つ結果をもたらさない(縮小変換の禁止)
    • 初期バージョン: すべてのキャストを禁止する。実行時チェックされたキャスト(gsl::narrow<T>(x) など)を提供して、縮小変換と符号の問題の発生に対処する。
    • 備考:
      • dynamic_castは安全だが、ポインタの結果のチェックを強制する必要がある
      • チェックされない無条件のキャストが必要な場合は、この保証を抑制する
      • 型指定されていない生のメモリから型指定されたオブジェクトに変換するには、キャストが必要
  • Invalidation(無効化)
    • 定義: 無効化された参照またはイテレータを介したアクセスはない
    • 初期バージョン
      • コンパイラーは、コンテナの要素への参照が取得されている場合、コンテナに対する非const関数の呼び出しを禁止する
      • 大量の誤検出を避けるために、[[non-validating]]属性が必要
      • 初期バージョンでは、コンテナ要素の1つだけへの参照を無効化する可能性がある単純で直線的なコードのみを許可する
    • 備考:
      • 型解析とフロー解析の両方を含む本格的な静的解析が必要
      • ここでの参照とはオブジェクトを参照するすべてのものを指し、コンテナとは値を保持できるすべてのものを指す
        • このコンテキストではjthreadはコンテナとみなされる
  • RAII
    • 定義: リソースリークがない
    • 初期バージョン: 取得して後で解放する必要があるすべてのリソースは、スコープ付きオブジェクトによって表される必要がある
    • 備考: 静的オブジェクトが所有するリソースは明示的に解放されない限り永久に存在する
      • リソースの例:メモリ、ロック、シェーダ
  • Union
    • 定義: 共用体のすべてのフィールドは初期化されている場合にのみ使用可能
    • 初期バージョン: 実行時チェックされた共用体(std::variant など)を使用する
    • 備考
      • パターンマッチングが必要
        • その場合、ルールは「共用体を使用せずmatch を使用する」となる
      • タグを伴わない共用体の重要な使用例がある。たとえば、文字列の長さによって実装を選択するSSOなど

このプロファイルアプローチをより信頼に足るものにするためには、これらのプロファイルのうちいくつかを正確に指定し、実装しテストする必要があり、この文書では次の作業として以下の事を進めていくことを推奨しています

  • プロファイルの構文を決定する
  • 実行時違反を示す構文を決定する
  • Initializationプロファイルを指定し、短期的に実行可能なものを決定する
  • Rangesプロファイルを指定し、短期的に実行可能なものを決定する
  • Castプロファイルを指定し、短期的に実行可能なものを決定する
  • 短期的に実行可能なPointerプロファイルのサブセットを考案する

構文さえ決まれば残りの作業やここに上がっていないプロファイルに関する作業を並行して行うことができるとしています。この提案は緊急性を重視しており、すぐに標準に導入することができて、すぐに使い始められることに重点を置いています。

P3275R0 Replace simd operator[] with getter and setter functions - or not

std::simdの添え字演算子の問題点と代替案について検討する文書。

std::simdbasic_simdbasic_simd_mask)には添え字演算子[])が用意されており、これは配列やコンテナ同様に個々の要素の読み書きを行うものです。

std::simd<int> x = 0;
x[0] = 1;       // OK
int y = x[0];   // OK
x[0] = y;       // OK
auto z = x[0];  // OK, but :  #1
z = 2;          // ill-formed #2

basic_simdbasic_simd_mask型それぞれのvalue_typeの値は保持しているものの通常はそのオブジェクトを保持しておらず、そのために[]は左辺値参照を返すことができません。そのため、constオーバーロードはprvalueを返し、非constオーバーロードはプロクシ参照を返します。このプロクシ参照は指定された要素の値を書き換えるための代入演算子や複合代入演算子オーバーロードを実装しており、それによって代入や演算が可能になっています。

上記例の#1では値ではなくプロクシ参照を取得していますが、zの宣言は参照には見えないため、これへの代入を防止するために#2のような操作はill-formedとなるようにされています。

このようなことが無くとも、左辺値参照の代わりにプロクシ参照を使用してしまうと、[]仕様に伴って意図しないエラーが発生しやすくなります([]の結果を直接関数テンプレートで推論した場合など)。

std::simdの実装では指定された型による配列を保持することを要求しておらず、実装定義の組み込み型を使用している可能性がある他、SIMDレジスタに直接的にマッピングされることさえも禁止していません。basic_simd_maskの場合、現在のSIMD命令の実装におけるハードウェアのマスクの実装が基本的にはビットマスクを使用する(1要素=1ビットや、1バイト=1ビットなど)ことから、ほぼ確実にboolの配列をメンバとして持つ実装にはなりません。

この提案はまず、basic_simdbasic_simd_maskでは添え字演算子を削除した方が良いのではないか?と提言しています。そして、プロクシ参照を削除したいが添え字演算子を残したい場合のために代替案について検討しています。

  1. []を読み取り専用にする
  2. begin()/end()によるランダムアクセス範囲としての添え字アクセス
  3. 2と似たアプローチとして、配列へ変換可能にする
  4. セッター/ゲッター関数を追加する
  5. element_reference型による値への参照を返すようにする

筆者の方の経験では、basic_simdbasic_simd_mask型での添え字演算子の仕様は直感的かつ自然であり、典型的な添え字演算子の使用の9割は読み取り専用であると報告しており、これをもとに、非constの添え字演算子のみを削除するようにすることを推奨しています。そして、それが受け入れられない場合はelement_reference型を追加してそれを返すようにするアプローチを押しています。

とはいえ、この提案は問題点の共有と代替案の検討を行っただけで、まだどれかのアプローチを提案するものではありません。

LEWGのレビューでは、[]を読み取り専用にして非constの添え字演算子のみを削除するアプローチが好まれたようです。

P3276R0 P2900 Is Superior to a Contracts TS

契約プログラミング機能をTSを経由するのではなく、直接標準へ導入すべきという提案。

P3265R0(上の方にある提案)にて、契約プログラミング機能はC++26を目指すのではなくTSを一旦目指すようにすることが提案されています。この提案は、TSにするメリットとコストを考察したうえで、(C++26かはともかく)直接標準への導入を目指すことを提案するものです。

TSを一旦経由することで得られるメリットとしては次のものがよく挙げられます

  1. 実装経験が得られる
  2. 使用経験が得られる
  3. 設計についてのWG21全体の合意が得られる
  4. 既存の機能との一貫性と適用性の向上
  5. 安全性の向上

この提案では、これらのメリットはTSを経由しない形でも得ることができるため、TSをを目指す理由にはならないとしています。

むしろTSとして発行することで、次のようなコストが余分にかかってきます

  • ベースとなる標準のリベース作業
    • TSとして発行するにはその時点で公開済みの標準バージョンをベースとしなければならないが、現時点ではそれはC++20である
    • そのため、まず現在の(C++26ドラフトをベースとしている)P2900をC++20ベースになるように書き換えなければならない
    • おそらく、そのすぐ後でC++23ベースになるようにリベースしなければならない
      • C++23は未発行でありいつ発行されるか不透明
    • C++23~26との一貫性や適用性を向上させるにはC++26標準を待たなければならない可能性があり、それは2027年以降になる
  • 文書の二重管理
    • TSが採択された場合に標準にマージできる状態を保つには、ベースとなっている標準規格(C++20 or C++23)と、最新の規格ドラフトの差分を考慮してマージに支障が出ないことを確認し続ける必要がある
    • ベースとなる標準規格が古いほどこの作業は煩雑になる
  • 議論の優先順位
    • EWGにおけるTSの議論時間は、必然的に標準そのものへの変更に関するものよりも優先順位が低くなる
    • TSを公開するためには限られた対面会議の時間枠の極々一部の時間だけしか使えないため、コンセンサスを得るには余分な労力がかかる

これらのことから、TSを出荷することで契約プログラミング機能を標準に導入するための問題が解決できるというエビデンスは無く、TSを経由することで全世界のC++ユーザーにとって利益のあるC++プログラムの安全性・正確性を向上させる機能の提供が遅れるだけであり、現在のC++を取り巻く環境を考慮するとこの遅れはC++の安全性向上への取り組みが真剣ではないと受け止められる可能性があるとして、現在の計画通りにTSを経由せずに直接標準へ導入することを目指すことを推奨しています。

提案では、TSを目指す理由として少なくとも次の疑問に応えるべきとしています

  • TSをリリースすることで解決しようとしている疑問点
  • TSによってそのような疑問点に回答する方法
  • P2900をこのまま(標準へ)進めてもそれに回答できない理由
  • TSを発行した結果としてC++契約プログラミング機能が採択された場合、その時の出口戦略はどうなるか

現在の契約プログラミング機能のTS化の提案にはこれらの点が述べられているものはまだ無いようです。

P3278R0 Analysis of interaction between relocation, assignment, and swap

リロケーションの代入やスワップとの関係性を考察する文書。

現在のリロケーションに関する提案は、最適化を目的として標準でリロケーションとトリビアルなリロケーションという操作を定義しようとしています。これによって、標準ライブラリを含めたライブラリの実装で代入の観点から実装されている一部の操作をリロケーションで置き換えることで最適化しようとするものです。

この文書は、主に生存期間の観点からリロケーションと代入・スワップの違いを考察するものです。

代入とリロケーション

vector::erasevector::insert等の操作をリロケーションとみなすことは一般的ですが、標準ではこのような操作がオブジェクト全体を再配置するのか、値だけを移動させるのかについて不透明です。実際には、これらの再配置操作は代入を使用して実装されています。標準ライブラリの仕様では、要件や計算量の規定によってそれを明示していることすらあります。

代入及び構築は値のみを移動させる操作であるためこれまで区別されてはおらず、古いオブジェクトで何が起こるのかについてはあまり考慮されてきませんでした(何でも起こり得たものの、代入という意味さえ達成できていれば何が起きていても良かった)。しかし、リロケーションの場合は明確に古いオブジェクトが破棄されるため、この区別が重要になります。

例えば、IDと値を保持するオブジェクトを考えます。IDは一意である必要があるため、代入操作に伴ってはIDを移動せずに値のみを移動させる必要があります(よって、ユーザー定義が必要です)。オブジェクトのIDが重要であるため、このようなオブジェクトの代入はIDをコピー/ムーブしません。しかし、リロケーションの場合、その操作の後で古いオブジェクトが破棄されることが分かっているためIDは重複せず、この場合はIDを再利用することができます。IDと値が共にリロケーション可能である場合この型全体もリロケーション可能であり、このような型の作成者は型をリロケーション可能としてマークする必要があります。結局、古いオブジェクトの破棄はリロケーションの一部です。

P3236では、再配置の実装の一部として代入を使用する既存実装の問題点と、現在のセマンティクスを維持したいという要望の両方を認識しています。全てのリロケーション操作不必要に悲観的に扱うことなくその機能を提供するには、代入時に起こる事とリロケーション時に起こる事を正しく区別する必要があります。そしてこれは、スワップにも当てはまります。

swapとリロケーション

標準は、スワップは値を交換するものとしています。特定の型の場合、値とはオブジェクト表現のことであり、オブジェクト表現と値が完全に一致している場合、スワップはビット単位の交換操作として実装することができます。このような説明ではどのような型が最適化されたスワップ操作を持つことができるかという事だけを言っています。

最適化スワップのもう一つの重要な事項は、オブジェクトの生存期間を考慮することです。

生存期間の考慮事項を無視すると、スワップをビット単位の交換操作で実装するには次の要件を満たす必要があります

  1. 型はトリビアルにリロケーション可能である
  2. 代入の代わりにビットコピーを使用する場合でも、セマンティクスが変更されない

2つ目の要件は、再配置操作に代入を使用している現在の実装(リロケーション以前)を最適化するために必要な要件と同じものです。しかし、現在の議論では、暗黙的なトリビアルリロケーション可能性という概念と、「トリビアルリロケーションは代入に使用できる」という概念が混同され、その結果「トリビアルリロケーションはスワップに使用できる」という概念を混同しています。

P1144の暗黙的なトリビアルリロケーション可能性の定義では上記1と2を満たすことが保証されますが、明示的にトリビアルリロケーション可能とマークされている型ではそのような保証は成り立ちません。

既存再配置操作とスワップの間にはもう一つ重要な違いがある事を認識する必要があります

  • 再配置操作では、ソースオブジェクトは再配置操作の後で破棄される
  • スワップでは、スワップの後も両方のオブジェクトが存続する

このため、明示的にトリビアルリロケーション可能とマークされた型がトリビアルリロケーションによってスワップを実行できるセマンティクスを持っているかどうかを知る方法が必要になります。

先程の、IDと値を保持する型の例を思い出します。このような型がリロケーションされると、ソースオブジェクトは破棄されているためIDの再利用が可能になるため、IDは保持されます。一方、このような型の2つのオブジェクトのスワップにおいては、代入によって実装されている(現状のベースライン)場合はオブジェクトのIDに変化はなく、2つのオブジェクトの間でその値だけが交換されます。結果、IDは交換されませんが値は交換されます。

つまり、この型がトリビアルにリロケーション可能とマークされている場合でも、リロケーションが提供する保証(ソースオブジェクトの破棄)が無くなったことによってそのセマンティクスが変化してしまっており、スワップをリロケーションによって最適化することはできません(先の要件2は満たされない)。

従って、ムーブ代入+ムーブ構築で行われているスワップ操作は単純にリロケーションによって置き換えることはできません。それにはP3239R0で提案されているように、リロケーションとスワップを区別して認識可能にする必要があります。この文書では、構築と破棄が代入と同等である型を切り分けることを今後の方向性として推奨しています。

また、そのような切り分けの方法とは無関係に、ビットコピー(memcpy/memmove)操作自体がオブジェクトの生存期間に影響を与える一方でスワップ操作は生存期間に影響を与えないという事実から、トリビアルリロケーション、またはより具体的にビットコピーによってスワップを実装するには、スワップされるオブジェクトの生存期間を終了しない方法が必要となります。P3239ではそのためにリロケーションを用いてスワップするもののオブジェクトの生存期間を中断しない特別な関数を提案しています。

追加の問題

この文書では、このような考察の仮定で浮かんだ2つの問題点についての報告と推奨事項の提案を行っています。

まず1つ目は、トリビアルなリロケーション可能性の伝播についてです。ユーザーが明示的に型をリロケーション可能であるとマークする方法には次の3つのオプションがあります

  1. 対象の型のすべてのサブオブジェクトがリロケーション可能かどうかをチェックし、一部でもリロケーション可能ではないならば、その型はリロケーション可能としてマークできない
  2. 全てのサブオブジェクトが明示的にリロケーション不可能としてマークされていない場合にのみ、その型はリロケーション可能としてマークできる
  3. 型の作成者はサブオブジェクトの状態に関わらず、型をリロケーション可能としてマークすることができる

2と3は現在のコードからの移行を容易にしますが、1が最も安全なオプションです。1を採用してから2,3へ緩和することはできるものの、逆はできません。また、2,3ではサブオブジェクトのプロパティが時間の経過によって変更される場合があるため、実質的に管理することが不可能になります。

もう一つの問題は構文についてです。P3236ではそのような型に対するマーキングの方法として属性構文を提案しており、属性ならば以前の言語バージョンでも(コンパイラがそれを認識することで)これを有効化できるとしています。ただし、属性の無視可能性についてはEWGで何度も確認されており、属性は無視された場合でもセマンティクスに影響を与えない必要があります。

トリビアルリロケーションは型のコンパイル時のプロパティであり、型特性を通して観察可能であるため、セマンティクスに影響を与えます。属性でありながらセマンティクスに影響を与えてしまうものとしては[[no_unique_address]]が知られており、このABI非互換の影響によってMSVCはしばらくこれを実装しないことを選択しており、この間違いを繰り返さない必要があります。また、Clangの実装者も今回のリロケーション関連のマーカーとして属性を利用することに反対を表明しています。

EWGではここでもその議論を蒸し返さないことが確認されており、この文書でも属性を使用しないことが推奨されています。

なお、この2つの問題点の推奨事項は、この提案のメインの部分とは別の問題であるため、さらなる検討が必要な場合は提案を分けて議論することが示されています。

この文書で提示されている少なくとも合意が必要なこととしては

  • 型をトリビアルにリロケーション可能としてマークするためには属性か別の構文のどちらを使用すべきか?
  • 暗黙のトリビアルなリロケーション可能性は代入演算子の影響を受けるべきか?
  • トリビアルにリロケーション可能としてマークされた型はに対する制限は何か?
  • スワップでリロケーションを使用する場合の生存期間の問題への対処

を挙げています。

この提案はP2786へフィードバックされたようです(何が合意されたのかは不明です)。

P3279R0 CWG2463: What 'trivially fooable' should mean

C++におけるtrivially ~という概念の定義を修正し明確化する提案。

trivially ~という概念(例えばtrivially copyable)については、ライブラリ開発者とコンパイラ開発者、言語設計者の間で三者三様な理解がされています

  • ライブラリ開発者
    • 型がtrivially copyableである場合、5つのルール(Rule-of-Five)の操作(特殊メンバ関数の事)は全て単純なmemcpyにまで落とすことができる
  • コンパイラ開発者
    • 全ての型は最初はtrivially copyableであるが、その後に非trivially copyableになる
      • 例えば、特殊メンバ関数がユーザー定義であることが判明すると、trivially copyableではなくなる
      • また、非trivially copyableなメンバ変数があっても、trivially copyableではなくなる
  • 言語設計者(言語法律家)
    • ↑2つは、trivially copyableの物理的な認識、直感によって導かれたものライフライムルールに
    • 言語設計者の理解は、標準の規定によって確立されている
    • それによって、Tが代入可能でない場合でもmemcpyを使用して代入を実行できる

このように、特にライブラリ開発者と言語法律家の間には、trivially copyableの理解について相違点があります。

  • ライブラリ開発者は5つの特殊メンバ関数による操作を完全なmemcpyに落とすことができる場合にのみ、C++が型をtrivially copyableと報告することを望んでいる
    • 誤検知は致命的
  • 言語法律家はC++が型をtrivially copyableではないと報告するのは、それらをmemcpyすることが全く意味をなさない場合にのみであることを望んでいる
    • trivially copyableでない限り、memcpyするのはUB
    • 基本的に、trivially copyableという性質は、どのような場合でもそのオブジェクトの代わりにバイトをシャッフルすることを許可するもの

特に、双方が望んでいるものは

  • ライブラリ開発者は、誤検知が無く厳密に保守的なtrivially copyableという性質のラベルを求めている
    • なぜなら、誤検知は実行時の誤動作を意味するため
  • 言語法律家は、誤検知がなるべく少ないtrivially copyableという性質のラベルを求めている
    • なぜなら、誤検知は、実際には正しく動作するものの形式的にはUBとなるプログラムを意味する(だけ)なため

そして、現在のtrivially ~という概念とその定義は、ライブラリ開発者の期待するようにはなっていません。

まず、現在のコンパイラはどれも、関数呼び出しでない限りすべての操作をトリビアルなものとして扱い、関数呼び出しの場合はトリビアルな特殊メンバ関数を呼び出す場合にのみトリビアル性が維持されます。特に、memcpyのようなことを全くしないにもかかわらず、is_trivially_constructible<int, float>is_trivially_constructible<bool, int>のようなものはtrueになり、集成体初期化もトリビアルとみなされます(関数呼び出しをしないので)。

次に、is_trivially_constructibleis_trivially_assignable及び、コピー/ムーブコンストラクタと代入演算子トリビアルになる条件では、オーバーロード解決の結果をチェックしています。つまり、ここで見られているのはどの特殊メンバ関数が定義されているかではなく、オーバーロード解決によってどの関数が選びだされるかです(これはどちらかと言えばライブラリ開発者の直感に沿っています)。しかし一方で、is_trivially_copyable及びデフォルトコンストラクタがトリビアルになる条件は特殊メンバ関数の存在によって定義されており(オーバーロード解決の結果は無視される)、これはライブラリ開発者の直感とは異なる部分があります。

さらに、これらのことによって留意すべき点が生じます

これらのことを念頭に、提案ではtrivially ~に関する予期しない振舞の例を3パターン照会しています

1. trivially copyable型は非trivially copyableメンバを持つことができる

struct Hamlet {
  Hamlet(const Hamlet&) { puts("copied"); }
  Hamlet(Hamlet&&) = default;
  Hamlet& operator=(Hamlet&&) = default;
};

struct Nutshell {
  Hamlet h_;
  Nutshell(Nutshell&&) = default;
  Nutshell& operator=(Nutshell&&) = default;
};

// 標準的にはどちらもパスする(EDGとMSVCはパスする)
static_assert(!std::is_trivially_copyable_v<Hamlet>);
static_assert(std::is_trivially_copyable_v<Nutshell>);

この例は、Hamletトリビアルコピー不可であるのにNutshellに包むとトリビアルコピー可能になりmemcpy出来るようになってしまう例です。

これは、言語法律家の理解からすると間違っているものの、ライブラリ開発者の理解からすると適正な振る舞いであるようです。ライブラリ開発者の立場からはその型で直接提供されている操作のみに着目するため、その観点からは正しい振る舞いとなります。

struct Hamlet2 {
  Hamlet2(const Hamlet2&) { puts("copied"); }
  Hamlet2(Hamlet2&&) = default;
  Hamlet2& operator=(Hamlet2&&); // not defaulted
};
struct Nutshell {
  Hamlet2 h_;
  Nutshell(Nutshell&&) = default;
  Nutshell& operator=(Nutshell&&) = default;
};

static_assert(!std::is_trivially_copyable_v<Hamlet2>);
static_assert(!std::is_trivially_copyable_v<Nutshell>);

この例は、コンパイラ開発者の理解に問題がある(ライブラリ開発者から見て)ことを示す例です。Hamlet2はムーブ代入演算子が定義されていない(従ってトリビアルではない)点だけがHamletとは異なりますが、Nutshellトリビアルコピー不可のままにします。

このことは、Nutshellのtrivially copyable性はメンバ変数型の性質ではなく、コンパイラが感知している他の何かの情報によって決まっていると見ることができます。

2. 型は、trivially copyableであると主張しながら、非トリビアルに動作することができる

struct Plum {
  int *ptr_;
  explicit Plum(int& i) : ptr_(&i) {}
  Plum(const Plum&) = default;

  // コピー代入演算子(削除済)
  void operator=(const volatile Plum&) = delete;

  // テンプレート代入演算子
  template<class=void>
  void operator=(const Plum& rhs) {
    *ptr_ = *rhs.ptr_;
  }
};


static_assert(std::is_trivially_copyable_v<Plum>);
static_assert(std::is_assignable_v<Plum&, const Plum&>);
static_assert(!std::is_trivially_assignable_v<Plum&, const Plum&>);

トリビアルコピー可能の定義は、トリビアルな特殊メンバ関数の宣言の存在だけを見ます(ユーザー定義されていないことは必須だが、定義されているかは見ない)。このplamの場合、ユーザー定義のdeleteコピー代入演算子(もどき)によって暗黙の代入演算子定義が(どちらも)抑制されますが、このdelete定義代入演算子シグネチャだけはコピー代入演算子として扱われ、なおかつユーザー定義されていません。このためis_trivially_copyable_v<Plum>trueになります。

一方、コピー代入操作はテンプレート代入演算子によって行われ、こちらはトリビアルであるとは扱われず、is_trivially_assignablefalseになります。

この例はコンパイラ開発者と言語法律家の両者にとっては想定される振る舞いのようです。一方明らかに、ライブラリ開発者(およびほかのすべての人)にとってはこれは驚くべきことです。

3. トリビアル特殊メンバ関数は、memcpyのように動作しない可能性がある

// 全てがトリビアル
struct Cat {};

// 代入演算子はトリビアルではないはず
struct Leopard : Cat {
    int spots_;
    Leopard& operator=(Leopard&) = delete;
    using Cat::operator=;
};

static_assert(is_trivially_copyable_v<Leopard>);
static_assert(is_trivially_assignable_v<Leopard&, const Leopard&>);

void test(Leopard& a, const Leopard& b) {
  a = b; // absolutely no data is copied
}

一つ前と同様に、Leopardトリビアルコピー可能となります。しかし今度の場合、is_trivially_assignabletrueになってしまいます。なぜなら、Catから継承している代入演算子トリビアルなものかつLeopardで使用可能として発見されるからであり、この結果としてLeopardの代入操作はなにも(基底クラスの部分しか)コピーしないものになります。

これはライブラリ開発者にとって非常に問題になります。なぜなら、このような例を正しく検出できないためです。

この提案は、これらのトリビアル性の定義をライブラリ開発者にとって自明なものとなるように修正しようとするものです。具体的には

  • is_trivially_constructible<T>は、引数無しでTを構築する操作をno-opにできる(つまり省略可能)ことを正確に意味する
    • これは現状通り
  • is_trivially_constructible<To, From>は、Fromからの完全なToの構築をmemcpyにできることを正確に意味する
    • 例えば、is_trivially_constructible<float, int>falseになる
  • is_trivially_constructible<To, Args...>は、Argsが1より大きいパックの場合常にfalseになるはずであり、それを非推奨化する
    • これは物理的に対応するものが無く役に立たない
  • is_trivially_destructible<T>は、Tの破棄をno-opにできる(つまり省略可能)ことを正確に意味する
    • これは現状通り
  • is_trivially_assignable<To, From>は、Fromからの完全なToの代入をmemcpyにできることを正確に意味する
    • 例えば、is_trivially_assignable<float&, int&>falseになる
  • is_trivially_copyable<T>は、引数のCV/参照修飾に関わらずTがサポートする特殊メンバ関数の操作をmemcpyにできることを正確に意味する

最後の提案は、つまり次の事を意味します

// 含意(a => b)を表す
constexpr bool implies(bool a, bool b) { return b || !a; }

template<class T>
inline constexpr bool is_trivially_copyable_v = 
  implies(is_constructible_v<T, T&>,        is_trivially_constructible_v<T, T&>) &&
  implies(is_constructible_v<T, T&&>,       is_trivially_constructible_v<T, T&&>) &&
  implies(is_constructible_v<T, const T&>,  is_trivially_constructible_v<T, const T&>) &&
  implies(is_constructible_v<T, const T&&>, is_trivially_constructible_v<T, const T&&>) &&
  implies(is_assignable_v<T&, T&>,          is_trivially_assignable_v<T&, T&>) &&
  implies(is_assignable_v<T&, T&&>,         is_trivially_assignable_v<T&, T&&>) &&
  implies(is_assignable_v<T&, const T&>,    is_trivially_assignable_v<T&, const T&>) &&
  implies(is_assignable_v<T&, const T&&>,   is_trivially_assignable_v<T&, const T&&>) &&
  implies(is_destructible_v<T>,             is_trivially_destructible_v<T>);

これらの変更の内容は、ライブラリ開発者の直感に従ったものでもあります。

P3281R0 Contact checks should be regular C++

契約プログラミング機能の現在の仕様を、より一般的な現在のC++にとって自然になるようにする提案。

現在の契約プログラミング機能で選択されている設計は、いくつかの点で一般的なC++コードにおけるものとは異なることがあり、それはおそらく契約プログラミング機能がこのまま導入された場合にそれを使用しようとするC++ユーザーにとって驚くべきものになると思われます。そして、このことはCアサートをはじめとする既存のアサーション/チェック機能から契約プログラミングへの移行を困難にすることが予想されます。

この提案は、契約プログラミングへの移行のしやすさを向上させるために、ユーザーの期待と異なる振る舞いを修正しようとするものです。

この提案であげられている問題点の一つは契約条件の評価回数に関するもので、複数回の評価を認める一方で評価を省略することも認めていることです。

struct X {};

// (どこかにある)辞書(マップ)にxを追加し、それがまだ追加されていなかった場合はtrueを返す
bool not_already_processing(X* x);

// xを辞書から削除し、削除された場合にtrueを返す
bool done_processing(X* x);

void f(X* x) {
  contract_assert(not_already_processing(x));
  
  // f()の再帰呼び出しにつながる可能性がある
  contract_assert(done_processing(x));
}

このコードのようなチェックは、現在のCアサートならプログラマの期待通りに動作しますが、契約プログラミングの場合は動作しない可能性があります。なぜなら、再帰呼び出しの場合にチェックが省略されたり、再帰呼び出しでない場合にチェックが重複したりする可能性があるためです。

もう一つの問題点は、契約注釈内から参照される自動変数が暗黙にconst化されることです。

これは契約条件式での副作用の利用を防止することを目的として導入されていますが、これによって、既存のアサートからコードの意味を変えずにcontract_assertへ移行するのが難しくなるケースがあります。これには、選択されるオーバーロードが変わる可能性も含まれています。さらに、これはあくまで浅いconstであるので、ポインタの先は変更可能となります。

void f(int* x)
  pre(x = nullptr)  // ng、ポインタそのものはconst
  pre(*x = 1)       // ok、ポインタの参照先は非const
{
  ...
}

筆者の方はEDGの人(EDGコンパイラの開発者の一人と思われる)で、EDGコンパイラの実装における各種のアサーションを現在の契約プログラミング機能へ移行することを考えると、これらの点を考慮して既存のチェックがこれらの問題点に引っかかるのかをチェックして回る必要があり、そうすると必然的にマクロのラップなどによって、ある種のカテゴリのチェックはあるフラグによって制御され、残ったチェックはまた別のフラグによって制御される、のようなあまり望ましくないコードを書くことになり、これによってチェックに違反処理メカニズムを共有できなくなる、と述べています。

現在の契約プログラミング機能はC++における契約プログラミング機能の出発点となるものであるため、既存の通常の言語のセマンティクスに基づいたアサーション機能からの移行パスを提供する必要があります。ユーザーが契約プログラミングへ簡単に移行できるようにするためには、そのような通常のC++言語を使用する(注釈内だけ異なる言語動作にならない)契約プログラミング機能が必要です。

この提案ではこのために、現在の契約プログラミング仕様(P2900R6)に対して次の3つの変更を提案しています

  1. 契約条件の評価の省略をしない
    • 契約条件式を評価せずに契約チェックを実行するために、条件式の結果を仮定してチェックを省略する許可を削除
  2. 契約条件の評価の繰り返しをしない
    • 一度評価された契約条件式を、同じまたは異なるセマンティクスを使用して同じ注釈内で再度評価する許可を削除
  3. 契約注釈内で変数のconst化をしない
    • 契約条件式内の自動変数(および構造化束縛、関数の結果値)の名前の式の型に暗黙にconstが追加されるというルールを削除

なお、これらの変更は3種類の契約アサーションすべてに対して適用することを提案しています。

提案では、これらの変更はC++26以降の契約プログラミング機能の拡張として扱うべきであり、ラベル指定などの方法によって破壊的変更を伴わずに導入できるはずとしています。

この提案は、SG21のレビューにおいて否決されたようです。

P3282R0 Static Storage for C++ Concurrent bounded_queue

bounded_queueに、ストレージに使用する領域へのポインタを受け取るコンストラクタを追加する提案。

bounded_queueとは現在P0260で提案されている並行キューで、可変長ではない(予め最大要素数が決まっている)タイプの並行キューです(P0260にはこれ以外のものは提案されていません)。

bounded_queueと同じコンセプトのキュー構造はその特性から小規模な組込みシステムでもよく使用されていますが、P0260の場合キューの要素数とアロケータをコンストラクタで渡して実行時にその領域動的確保によって割り当てており、ヒープメモリを使用したくない(できない)環境においては特別なアロケータを実装する必要があるか、それができない場合は全く使用できません。

この提案では、bounded_queueが静的に割り当てられたメモリ領域を使用できるようにするために、キューの要素数とストレージ領域へのポインタを受け取るコンストラクタを追加することを提案しています。

提案では次のようなコンストラクタを追加しようとしています

bounded_queue(size_t max_elems, void *storage);

このコンストラクタの要件は当然storageの領域にmax_elems個の要素を配置可能なことですが、通常そのようなメモリの量は不明(同期用のオーバーヘッドなどがあるため)です。

そのため、そのようなサイズを知ることのできるメンバ関数を追加することを提案しています

static consteval size_t required_size(size_t max_elems);

static consteval size_t required_alignment();

required_size(max_elems)は引数のmax_elems個の要素数を格納するために必要な領域サイズを返し、required_alignment()はその時のアライメントについて返すものです。

この新しいインターフェースの使用例

using MyQueueT = std::bounded_queue<uint32_t>;

// 静的な領域
alignas (MyQueueT::required_alignment())
std::array<std::byte, MyQueueT::required_size(8)> myQStorage;

// 領域ポインタを渡してキューを構築
MyQueueT myQ{8, myQStorage.data()};

int main()
{
  // start tasks and use myQ
}

P3283R0 Adding .first() and .last() to strings

std::stringに文字列最初と最後の部分文字列にアクセスするためのAPIを追加する提案。

std::string(およびstd::string_view)において、最初のN文字にアクセスするためには.substr(0, N)とする必要があります。同様に、最後のN文字にアクセスするためには.substr(size() - N, N)とする必要があります。どちらの操作も単純ではあるものの、N文字を取るのに余計な計算と引数の指定が必要になっており、間違えやすく使いづらい面があります。

この提案は、これらの操作を使いやすくするためにラッパであるメンバ関数.first().last()を追加しようとするものです。

利点としては

  • プログラマの意図がより明確で伝わりやすくなり、可読性が向上する
  • 引数は欲しい文字数Nのみなので、エラーの可能性が低減される
  • よりシンプルになる

このような関数はstd::spanに既に存在しておりspanには.subspan()も同時に存在していることから、この2種の関数をあえて別々に追加するだけの関心があったことが分かるため、現在.substr()しかないstd::string/std::string_viewでも同様に.first().last()を追加すべき、としています。

追加する関数の宣言の例

namespace std {

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

    constexpr basic_string first(size_t count) const &;
    constexpr basic_string first(size_t count) &&;

    constexpr basic_string last(size_t count) const &;
    constexpr basic_string last(size_t count) &&;

    ...
  };
}

std::string_viewの場合もほぼ同様の引数を取ります。効果は、上で示した.substr()Nに対する結果と同じです(.substr()を使用してはいません)。

int main() {
  std::string str = "abcdefghijk";

  auto first_substr = str.first(3); // abc
  auto last_substr = str.last(3);   // ijk

  std::string_view strview = str;

  auto first_substr_view = strview.first(3); // abc
  auto last_substr_view = strview.last(3);   // ijk
}

P3284R0 finally, write_env, and unstoppable Sender Adaptors

新しいsenderアルゴリズムの提案。

この提案は、既に追加されているP2300の非同期処理フレームワークライブラリに対するC++26の拡張を提案するものです。提案されているのは

  • write_env
  • unstoppable
  • finally

の3つです。これらのものは、以前にP3175R0で提案されていたものでしたが、P3175のメインの部分を進めるために分離されました。

write_env

receiverには実行環境(execution environment)というものが関連付けられており、これは単なるKey/Valueストアです。senderreceiverが接続されたときに、接続されたreceiverを介してそこから値のクエリ/取得を行って、実行環境に関するもの(例えばアロケータやstop_tokenなど)を取得できます。senderによるチェーンでは、そのような実行環境はチェーンの上流から下流に向けて伝播し、何もしなければ上流で設定されたものが下流の処理でも使用されますが、場合によっては途中でそれを変更したいこともあります。write_envアダプタはそのためのもので、senderチェーン(の背後にあるreceiver)の実行環境に値をストアするものです。

これは、write_env(sender, environment) -> senderのようなアダプタであり、入力のsenderと実行環境environmentを受け取って、これらを格納しているsenderを返します。返されたsendersndr)が(別のsenderアルゴリズムとの接続などによって)receiverrcvr)と接続されると、単にそれらを接続した結果のreceiverを返します(通常のチャネルへの関与は行わない)。

ただし、そうして返されたreceiverの実行環境はenvironmentrcvrの環境が結合されたものになっており、そのreceiverへのクエリはまずenvironmentに対して行われ、そこで該当するものが見つからない場合にrcvrの環境にクエリされます。

提案より、アロケータを変更する例

// クエリキーと対応する値を実行環境のクエリ可能なものに変換するクラス
template <class Query, class Value>
struct with : Query {
  Value value;
  auto query(Query) const { return value; }
};

// 指定したアロケータを使用するようにsenderを変化させる
struct with_allocator_t {
  template <std::execution::sender Sndr, class Alloc>
  auto operator()(Sndr sndr, Alloc alloc) const {
    return std::execution::write_env(sndr, with{std::get_allocator, alloc});
  }
};

constexpr with_allocator_t with_allocator{};

このwith_allocatorは次のように使用します

namespace ex = std::execution;

ex::sender auto make_async_work_with_alloc() {
  // サードパーティのライブラリによって作成される非同期作業
  ex::sender auto work = third_party::make_async_work();

  // その作業で使用されるアロケータをカスタマイズする
  return with_allocator(std::move(work), custom_allocator());
}

unstoppable

unstoppableアダプタはwrite_env同様に実行環境を変化させるもので、これの場合はstop_tokennever_stop_tokenに変更することで、入力のsenderが外部からの停止要求に応じないようにします。unstoppable(sender) -> senderの様なシグネチャになり、これもメインのチャネルには関与しません。

これはwrite_envを利用して実装することができます

namespace std::execution {

  inline constexpr struct unstoppable_t {
    template <sender Sndr>
    auto operator()(Sndr sndr) const {
      return write_env(std::move(sndr), never_stop_token());
    }

    auto operator()() const {
      return write_env(never_stop_token());
    }
  } unstoppable{};
}

finally

C++言語には非同期破棄の直接のサポートが無く、「この非同期処理の後で最初の処理がどのように終了したかに関係なく、無条件で別の非同処理を開始する」ということを(安全に)行う方法がありません。これによって、非同期RAIIを実現することができません。

finallyアダプタは、senderの領域で非同期RAIIパターンを実現するものです。finally(sender, sender) -> senderの様なシグネチャで、2つのsender(非同期処理)を受け取って、返されたsenderが接続を受けて開始されると、まず1つ目のsenderの処理を開始します。それが完了すると、結果を保存してから2つ目のsenderを開始します。このsenderの結果としては、2つ目のsenderの処理が正常に完了した場合1つ目のsenderの結果が返り、それ以外の場合は2つ目のsenderの結果が返ります。

提案文書より、非同期処理の過程でプログラム不変条件が一時的に破棄された後で復元される例

namespace ex = std::execution;

ex::sender auto break_invariants(auto&... values);
ex::sender auto restore_invariants(auto&... values);

// この関数はsenderアダプタクロージャオブジェクトを返す
// senderを入力すると、不変条件を破棄してデータを変更して、その後不変条件を復元する新しいsenderを返す
auto safely_munge_data( ) {
  return ex::let_value( [](auto&... values) {
      return break_invariants(values...)
        | ex::then(do_munge) // `do_munge`が例外を送出した場合でも不変条件は復元される
        | ex::finally(ex::unstoppable(restore_invariants(values...)));
  } );
}

auto sndr = ...;
scope.spawn( sndr | safely_munge_data() ); // See `counting_scope` from P3149R2

Java言語等にあるtry-catchに対するfinallyのようなことをsenderチェーンで行うものです。

P3285R0 Contracts: Protecting The Protector

契約注釈の条件式からUBを起こしうる操作を可能な限り減らす提案。

現在の契約プログラミング機能において、契約条件式はほとんど通常のC++コードの式と異なる振る舞いをしません(契約注釈内から参照される外部の自動変数が暗黙const化したり、関数引数の参照に制限があったりはします)。それによって、契約条件式は任意の副作用を発生させることができ、未定義動作を起こす可能性もあります。

契約プログラミング機能はC++コードの安全性向上に寄与するはずの機能であるのに、その契約条件式が特に制限されていないことによって安全ではない可能性があるというのは度々議論になっており、過去のバージョンでは副作用禁止が明記されていたこともありました。しかし、そのような制限はC++言語のサブセットを生み出すだけでその実現可能性も低いと考えられたため、特に制限を行わないことになっています(ただし暗黙const化や評価回数の規定なしなどで間接的に禁止している)。

この提案はそれに対して異を唱え、契約プログラミング機能をより安全にするために契約条件式の副作用や未定義動作を抑制しようとするものです。ここでは、現在の契約プログラミング機能の提案(P2900R6)に対して次の2つの変更を提案しています

  1. 事前条件と事後条件を、非緩和契約(Non-relaxed contracts)と緩和契約(Relaxed contracts)の2つのグループに分類する
  2. コンベア関数の概念の導入

緩和契約は、事前条件と事後条件を指定する際にrelaxedという修飾を行うものです。

int rem(int x, int y)
  pre relaxed(event_log(y), y != 0)
{
  return x % y;
}

この場合、rem()の呼び出しに伴う事前条件チェックにおいては、y != 0のチェックの前にevent_log(y)の実行(すなわち副作用)が許可されます。また、緩和契約ではここで提案されているその他の保証は一切適用されません。すなわちこれは、現在の契約機能の振る舞いです。

非緩和契約とは、現在のP2900R6の構文を使用して、契約条件が評価されたときに副作用がその評価コーンの外側に出ないように設計されているという追加的な制約を持つものです。さらに非緩和契約の条件の評価は特定のクラスの未定義動作の影響を受けないことが保証されます。

非緩和契約では、コンベア関数とコンベア関数で許可されている操作のみが許可され、関数引数が変更されないことを保証するために、非緩和契約のなされている関数の引数は、その内部で別の関数に渡される場合は値で(コピーして)渡すか、const参照に渡すかのどちらかでなければならず、キャスト等のそのほかの変更を伴う操作は許可されません。

コンベア関数は非緩和契約において未定義動作を抑止するためのキーとなる概念で、呼び出された場合に関数の外部に対して副作用を及ぼさない関数の事です。さらに、コンベア関数内部では可能な操作が制限され、未定義動作を発生させうる操作が禁止されています。

コンベア関数は[[conveyor]]属性を使用して宣言された関数です。

[[conveyor]]
int add(int x, int y) { return x + y; }

[[conveyor]]
int inc(int& x) { return ++x; }

このように宣言された関数内では、未定義動作を発生しうる特定のクラスの操作が禁止(出現したらコンパイルエラー)されます。

[[conveyor]]
int deref(int* p) 
{ 
  return *p; // error: pはオブジェクトアドレスを保持しているか不明
}

これは次のように標準で提供される述語によってチェックすることで許可されるようになります

[[converyor]]
int deref(int* p) pre(object_address(p))
{
  return *p; // OK
}

ポインタによるオブジェクトの参照はオブジェクトの生存期間外にアクセスを行う可能性があるため、デフォルトでは禁止されます。object_address()という述語は標準で提供されるもので、ポインタが生存期間内にあるオブジェクトを指していることを保証するもので、これを用いて事前条件チェックすることでコンベア関数内ではそのポインタの仕様が安全だとみなされます。コンベア関数内では同様に未定義動作を発生させうる多くの操作が禁止されているほか、算術演算オーバーフローでは実行時の挙動を指定して定義された振る舞いしかしないようにすることで未定義動作を回避しようとします。

このような制限により、コンベア関数およびそれを用いた非緩和契約においては未定義動作の発生が抑制され、副作用もその評価の内側に閉じ込められることでその影響を緩和できます。また、緩和契約によって現在のベースラインの振る舞いが維持され、明示的な副作用が許可されます。

P3286R0 Module Metadata Format for Distribution with Pre-Built Libraries

ビルド済みライブラリ配布のためのモジュールメタデータのフォーマットの提案。

ここで提案されているのは、ビルド済みライブラリからそのモジュールのインターフェース(BMI)を得るために必要な情報をまとめたメタデータのフォーマットです。ビルド済みライブラリをモジュールとして配布する際にこのような情報の提供は必須なものとなります。

この提案の作業はP2701R0およびP2577R2に続く作業であり、メタデータのフォーマットのみを提案しています。

libc++で標準ライブラリモジュールの実験的サポートが開始されたことを受けて、このようなメタデータとそのフォーマットが必要となり考案され、ビルドシステムはこれを利用して、標準ライブラリの提供するモジュールのビルド済みモジュールのインターフェースファイル(BMI)を作成することができます(作成できる必要があります)。この提案はこうして考案されたフォーマットを標準ライブラリモジュールだけではなくビルド済みライブラリ全般で使用できるメタデータの標準フォーマットとして提案するものです。

そのため、この提案のフォーマットはlibc++で実装経験があり、CMakeはこれを利用してモジュールのBMIを生成する試験実装が行われています。

このメタデータはP2701R0およびP2577R2で示されている要件に従った上で、libc++における実装時に追加でまとめられた要件に従っています。

  • ビルドシステムには、ビルド済みライブラリによって提供されるモジュールを識別する方法が必要
  • メタデータファイルの場所
    • 標準ライブラリの場合
      • ビルドシステムはツールチェーン(コンパイラもしくはパッケージマネージャ)に対してそのメタデータファイルの場所を問い合わせることができる
    • それ以外の場合
      • 強力なパッケージマネージャが存在しない場合、(それが実行可能な環境では)ビルドシステムはリンカ引数からそのメタデータの場所を推測できる
      • パッケージマネージャが存在する場合、その方法は実装定義の(そのパッケージマネージャの提供する)方法で収集できる
    • メタデータファイルへのパスはリンカに渡される入力ファイルと関連付けられている必要がある
      • ライブラリのビルド毎にメタデータファイルが異なることも予想される
  • メタデータには次のものが含まれている
    • 提供されるインポート可能な翻訳単位の論理名
    • インポート可能な翻訳単位のプライマリソースコードへのパス
    • 特定のインポート可能な翻訳単位を翻訳するために必要な追加のインクルードパス
    • 特定のインポート可能な翻訳単位を翻訳するために必要な追加のコンパイラ定義
    • モジュールが標準ライブラリモジュールであるかどうか(名前が予約されているため)
  • メタデータには次のものが含まれている場合がある
    • その翻訳単位の依存関係であるインポート可能な翻訳単位の論理名
    • ベンダー固有の属性

長いのでコピペしませんが、提案にはJSONスキーマによるフォーマットの完全な定義が記されています。

P3287R0 Exploration of namespaces for std::simd

std::simdに関するAPIを標準ライブラリ中にどのように配置するのかについてを探る提案。

std::simdに関しては、permute APIに関する議論の過程で、関連するフリー関数をそのままstd名前空間に入れてしまうことについて懸念の声が上がったようです。この提案はそれを受けて、std::simdに関連するものの名前空間名も含めた命名について考察するものです。

あがっているのは次の7つです

  1. 現状(std名前空間直下)
  2. 全ての関数をsimdプレフィックス付きの非メンバ関数にする
  3. 全ての関数をsimdプレフィックスなしの非メンバ関数にする
    • 利点
      • 一貫性があり、覚えやすい
      • SIMDジェネリックインターフェースを簡単に提供できる
    • 欠点
      • auto x = std::copy_from(data.begin())のようなコードを見ても、basic_simdオブジェクトが作成されることがわからない
      • 同じ名前の非SIMDオーバーロードは、機能が同等でない場合、問題となる(名前の競合)
      • 一貫性のないオーバーロードされた用語を明確にする必要がある場合、simd_プレフィックスが必要になる
    • 評価: 名前空間の競合や、混乱を招く可能性のあるオーバーロードがあるため、受け入れられない
  4. 型以外の全てを名前空間に入れる
    • 利点
      • 新しい名前空間から自由に名前を取得できる
    • 欠点
      • 型と関数が異なる名前空間にあるのはぎこちない
      • 機能(std::simd)と名前空間の間に必要な不一致は、不満を感じる
      • SIMDジェネリックプログラミングは、constexpr-if分岐が多すぎるため、ほとんど不可能である
    • 評価: SIMDジェネリックプログラミングができない、ADLが機能しないため、受け入れられない
  5. 全ての非メンバ関数を隠蔽フレンドにする
    • 欠点
      • generateやcopy_fromなどを呼び出す方法がなく、機能しない
      • 無条件に呼び出す必要があるのは奇妙である
      • SIMDジェネリックプログラミングが非常に難しくなる
    • 評価: 使い物にならない
  6. 全てを単一の名前空間に入れる
    • 利点
      • 新しい名前空間から自由に名前を取得できる
      • ADLが機能する
      • 一貫性があるため、ユーザーは「std::simd名前空間にあるものはsimdで動作する。simd用の関数を探すときは、std::simd名前空間を探す」ということを学ぶだけでよい
    • 欠点
      • SIMDジェネリックプログラミングが難しくなる
      • クラステンプレート名std::simd::simdが少しぎこちない
    • 評価: SIMDジェネリックプログラミングができないため、受け入れられない。ただし、constexpr-ifをすぐに使用しなくても済むようにすれば興味深い
  7. 明らかなオーバーロード以外の全てを単一のネームスペースに入れる
    • 利点
      • 新しい名前空間から自由に名前を取得できる
      • ADLが機能する
      • 比較的一貫性がある
      • SIMDジェネリックプログラミングの提供と使用が簡単である
    • 欠点
      • クラステンプレート名std::simd::simdが少しぎこちない
      • stdstd::simdに非メンバ関数が混在している
    • 評価: 受け入れられるが、現状とあまり変わらないため、実際に良くなっているかどうかはわからない
  8. simdを単一のネームスペースに、SIMDジェネリックインターフェースは別のネームスペースに配置する
    • 利点
      • 新しい名前空間から自由に名前を取得できる
      • ADLが機能する
      • 一貫性があるため、ユーザーは「std::simd名前空間にあるものはsimdで動作する。simd用の関数を探すときは、std::simd名前空間を探す
        • simdとスカラの両方で汎用的に動作させる必要がある場合は、std::simd_genericに切り替える」ということを学ぶだけでよい
      • 誤って間違ったオーバーロードを呼び出してしまうことに対して、比較的「安全」なオプトインSIMDジェネリックプログラミングである
    • 欠点
      • クラステンプレート名std::simd::simdがまだ少しぎこちない
      • std::simd_genericは長すぎるため、異なるコードベースで異なる名前空間エイリアスに省略される
    • 評価: スカラ/SIMD/SIMDジェネリックが明確に分かれているため、好感が持てる。名前空間エイリアスによる簡潔なコード表現も好ましい

この提案ではどれを選択するかを決定していないものの、8つ目(最後)の選択肢を推奨しています。

P3288R0 std::elide

コピーもムーブできないクラス型のprvalueの生成を遅延するライブラリ機能の提案。

コピーもムーブもできないクラス型は標準ライブラリにも存在しており、例えばstd::mutexstd::counting_semaphoreなどがあります。このようなクラス型のオブジェクトを関数からのreturnする場合、return文で直接構築してprvalueを返すようにすれば可能です。

std::counting_semaphore<8> FuncReturnsByValue(unsigned const a, unsigned const b) {
  return std::counting_semaphore<8>(a + b); // ok
}

int main() {
  auto cs = FuncReturnsByValue(1, 2); // ok、コピー省略による直接構築
}

これはコピー省略保証によるprvalueの特別扱いであり、このような関数を利用することでコピーもムーブもできない型の生成を簡単にして、取り扱いの厄介さを軽減することができます。しかしこれを利用してもなお、optional.emplace()のような関数でコピーもムーブもできない型の値を注入することはできません。

int main() {
  std::optional< std::counting_semaphore<8> > var;

  var.emplace( FuncReturnsByValue(1,2) );  // compiler error
}

これは、.emplace()の引数がT&&によって変数を受け取っているためにprvalueが実体化されてしまうことでコピー省略が妨げられ、.emplace()の内部でそのコピーもムーブもできない型(この例ではcounting_semaphore)のコンストラクタを呼び出すところで単なるムーブ構築となることでエラーになります。

ごく単純に示すと、.emplace()は次のように配置newによって特定領域にオブジェクトを構築しようとします。

template<typename... Params>
T &emplace(Params&&... args) {
  ...

  ::new(buffer) T( forward<Params>(args)... );

  ...
}

Tがコピーもムーブできない型であり、先程の例のように関数が返すprvalueから構築しようとする場合、ここでのTのコンストラクタ呼び出しにおいて渡される引数がprvalueになることはなく、それによって必ずコピーかムーブコンストラクタが探索され、それは見つかることは無いためエラーになります。

組み込みのnew演算子の場合、その引数列にprvalueがあればコピー省略されるため、これを機能させるには、この場所までprvalueを届ける必要があり、例えば次のような簡単なワークアラウンドによって可能になります

int main() {
  std::optional< std::counting_semaphore<8> > var;

  struct Helper {
    operator std::counting_semaphore<8>() {
      return FuncReturnsByValue(1,2);
    }
  };

  var.emplace( Helper() );  // ok
}

こうすると、配置newの引数でこのHelperから変換して構築するコンストラクタがないことから型変換が考慮され、型変換はcounting_semaphoreのprvalueを返すことから、配置newの構築においてコピー省略が働き、それによってコピーコンストラクタもムーブコンストラクタも使用せずに構築できるためエラーは出なくなります。

このようなもの例えば、std::vsriantstd::any、あるいはコンテナなどがあります。

この提案は、このような働きをする汎用的なクラス型であるstd::elideの提案です。

int main() {
  std::optional< std::counting_semaphore<8> > var;

  var.emplace( std::elide(FuncReturnsByValue,1,2) );  // ok
}

提案より、実装例

namespace std {
  template<typename F_ref, typename... Params_refs>
  class elide final {
    using R = invoke_result_t< F_ref, Params_refs... >;
    static_assert( is_same_v< R, remove_reference_t<R> > );  // F must return by value
    using F = remove_reference_t<F_ref>;
    F &&f;  // 'f' is always an Rvalue reference
    tuple< Params_refs... > const args_tuple;  // just a tuple full of references
  
  public:
    template<typename F, typename... Params>
    explicit elide(F &&arg, Params&&... args) noexcept  // see explicit deduction guide
      : f(move(arg)), args_tuple( static_cast<Params&&>(args)... ) {}

    operator R(void) noexcept(noexcept(apply(static_cast<F_ref>(f),move(args_tuple))))
    {
        return apply( static_cast<F_ref>(f), move(args_tuple) );
    }

    /* -------- Delete all miranda methods -------- */
    elide(     void     ) = delete;
    elide(elide const & ) = delete;
    elide(elide       &&) = delete;
    elide &operator=(elide const & ) = delete;
    elide &operator=(elide       &&) = delete;
    elide const volatile *operator&(void) const volatile = delete;
    template<typename U> void operator,(U&&) = delete;
    /* -------------------------------------------- */
  };

  template<typename F, typename... Params>  
  elide(F&&,Params&&...) -> elide<F&&,Params&&...>;  // explicit deduction guide
}

P3289R0 Consteval blocks

宣言のコンテキストで任意の定数式を実行するためのブロックの提案。

P2996の静的リフレクションや、P2758R2のコンパイル時のメッセージ出力など、定数式においてその式の評価の外側に出ていく副作用をもたらすような提案がC++26に向けて進行しています。これらの機能が導入されると、必然的に宣言だけが行える領域において特定の定数式を実行させることをしたくなるでしょう。

そのような領域には例えばクラスのメンバ宣言の領域があり、そのような場所で任意の定数式を起動するにはstatic_assert()を悪用できます

#include <meta>

template<typename... Ts>
struct Tuple {
  struct storage;

  static_assert(
    is_type(define_class(^storage,
                         {data_member_spec(^Ts)...})));
  // ↑の解説
  // define_class()は不完全型に対して指定されたメンバで定義を追加する関数
  // data_member_spec()は与えられた型のメンバ変数を表すmeta::info()を返す関数
  // is_type()は、引数のmeta::infoが型を表しているかを判定する述語
  // すなわち、storage型がTs...をメンバとして持つように定義している
  
  storage data;
};

しかしこれは(式の内容を除いても)何をしているのか分かりづらく、難解です。ここでやりたいことは何かのアサーションではなくて、何か定数式の起動の強制です。

この提案では、これを実現するためのシンプルなconseval{}ブロックを提案するものです。

これはstatic_assertの単純なラッパであり、次のようなブロックは

consteval {
  statement-seq(opt)
}

次のように展開されます

static_assert(
  (
    []() -> void consteval {
      statement-seq(opt)
    }(),
    true
  )
);

これにより、冒頭のコードは次のように書き直されます

#include <meta>

template<typename... Ts> struct Tuple {
  struct storage;
  
  consteval {
    define_class(^storage,
                 {data_member_spec(^Ts)...});
  }

  storage data;
};

この例では起動したい式は1つだけですが、より複雑なことをしたい場合のために複数の式を置くこともできます。

P3290R0 Integrating Existing Assertions With Contracts

既存のアサーション機構に契約プログラミング機能を統合する提案。

現在の契約プログラミング機能の一つの特徴として、全ての契約注釈の契約条件のそれぞれの評価が、条件がチェックされる場合に同じ契約条件の以前/将来の評価も含めてその他の契約チェックから独立していることがあげられます。この性質によって、契約注釈のセマンティクスは実装定義となり、実行時にも評価毎に変化することが許可されます。

ただし、標準のassertマクロをはじめとする既存のアサーション機構や契約チェック機構はこれとは微妙に異なるモデルを持っています。ここのアサーションマクロはそのマクロ自体が定義されるときに参照される、NDEBUGなどの1つ以上のマクロの存在に基づいて、ある翻訳単位内での使用する時はいつでも全ての使用が同じセマンティクスを持ちます。これによって、アサーションのセマンティクスはコンパイル時に認識可能である必要は無く、通常気にされません。

また、契約条件式には通常のC++コードをそのまま書くことができ、これによる契約条件式への副作用の埋め込みを回避するために、自動変数や*thisの暗黙const化及び契約条件評価回数を不定(0以上の任意の回数)とすることなどのところも、既存のassertと異なる部分です。

これらのような差異によって、既存のassertから契約プログラミングへ(特に、assertからcontract_assertへ)単純に移行することができない場合があります。

現在の契約プログラミング機能の仕様(P2900R7)がEWGに転送されて以降、それまでは検討されてこなかったこの部分(既存機能からの移行のしやすさ)が問題となってきています。

この提案は、既存のアサーション機構を契約プログラミング機能に簡単に移行できるようにするために、既存のアサーション機構へ契約プログラミング機能を組み込むようなソリューションを提案するものです。

提案しているものは次の3つです

  1. 契約違反ハンドラを直接呼び出せるようにする
    • 既存の契約チェック機構をP2900の契約違反処理機構に統合するためのメカニズムを提供する
    • なおかつ、(互換性のない可能性のある)既存のセマンティクスを完全に維持する
  2. 条件付きでassert契約プログラミング機能に統合する
    • 現在のassertマクロの仕様を拡張し、中断(してメッセージを出力する)前に契約違反ハンドラ(例外送出なし)を呼び出せるようにする
    • これを条件付きでサポートする
  3. より使い慣れたセマンティクスを持つ新しい形式の契約アサーションを追加する
    • contract_assertが提供していない、既存のassertマクロのユースケースに対応することを目的としている
    • このアサーションは副作用を許容し、暗黙const化を行わない

この3つの提案はそれぞれ独立しており、異なる方法によって既存のアサーション機構から契約プログラミング機能へ移行できるようにしようとするものです。3つのうちの1つ以上を導入することで、assertの代替機能不足という懸念の大部分が改善され、P2900を先に進める原動力となります。

1. 違反ハンドラの直接呼出し

契約プログラミング機能を採用すべき大きな利点の一つは、プログラムの各所で任意に出現しうるバグ(契約違反)について、それを検出した後の管理(対処、終了、緩和、など)をユーザーがカスタマイズ可能な違反ハンドラによって一元的に取り扱えることです。

3種類の契約アサーションによって検出された契約違反はプログラムのどこで発生したかに関わらず同じ契約違反ハンドラを呼び出します。これは契約プログラミングが言語機能である事の利点の一つでもあります。

現在類似のアサーション機構や契約チェック機構を使用しているユーザーにはそのようなものはありません(あっても、ライブラリ間で異なるものになってしまう)。この提案1は、契約違反ハンドラを直接呼び出すことのメカニズムと、ハンドラ呼び出し後に終了するセマンティクスと同一の方法でプログラムを終了するメカニズムを提供しようとするものです。

まず、次の関数を<contracts>に追加します

namespace std::contracts {
  [[noreturn]]
  void handle_enforced_contract_violation(const char* comment);

  void handle_observed_contract_violation(const char* comment);
}

この2つの関数は名前の通りそれぞれenforced/observedセマンティクスの下で呼び出される違反ハンドラと同じ動作をするように違反ハンドラを呼び出します。

違反ハンドラには5つのプロパティが設定されますが、これらの関数ではそれは次のようになります

  • comment : 関数引数commentに指定された内容
  • location : これらの関数を呼び出した場所のstd::source_locationオブジェクト
  • kind : std::contracts::assertion_kind::manual(新規追加する列挙値)
  • detection_mode : std::contracts::detection_mode::manul(新規追加する列挙値)
  • evaluation_semantic : 呼び出される関数に対応するセマンティクスの列挙値
    • handle_enforced_contract_violation() : enforce
    • handle_observed_contract_violation() : observe

これらの関数はこのような値を持つstd::contract_violationオブジェクトを作成し、その環境にインストールされた違反ハンドラを呼び出します。

そして、その実行が完了した後の動作は次のようになります

  • handle_enforced_contract_violation() : 実装定義の方法でプログラム終了
  • handle_observed_contract_violation() : 違反ハンドラから正常にリターンした場合、そのまま復帰する
  • 違反ハンドラから例外が送出された場合 : 通常通り伝播する

この2つの関数は、契約違反の場合に違反ハンドラを呼び出す全てのセマンティクスについてカバーしており、提案1としてはこれがメインです。ただし、最近追加されたセマンティクスの中にはユーザーサイドでの再現が難しいものがあり、それを再現する呼び出しを行う関数を検討することもできます

namespace std::contracts {
  [[noreturn]]
  void handle_quick_enforced_contract_violation(const char* comment) noexcept;
}

これはquick_enforceセマンティクスによる契約違反時の動作を再現するもので、呼び出されると契約違反ハンドラを呼び出さず、速やかに実装定義の方法でプログラムを終了します。

そして最後に、違反ハンドラを呼び出す最初の2つの関数について、noexcept指定されたオーバーロードを提供することも検討できます

namespace std::contracts {

  [[noreturn]]
  void handle_enforced_contract_violation(const char* comment,
                                          const std::nothrow_t&) noexcept;

  void handle_observe_contract_violation(const char* comment,
                                         const std::nothrow_t&) noexcept;
}

これらの関数はstd::nothrow_tを受け取らない同名の関数と同じ効果を持ちますが、違反ハンドラから例外が投げられた場合にstd::terminate()を呼び出してプログラムを終了します。

2. assertからの違反ハンドラ呼び出し

標準assertマクロは現実のC++プログラムの多くの場所で使用されており、それらの利用のほとんどのものにはチェックする述語に副作用は含まれていません。副作用をもつような用法も多くがバグかミスでですが、中には実用的で有用な使用法もあります。

前述のように副作用の扱いに関しては特にassertマクロと契約アサーションでは互換性が無いので、移行に際しては既存のassetマクロの使用法をすべてチェックして非互換で問題にならないかを確認し、必要なら修正を行わなければなりませんが、これはコストが高く、新たなバグを埋め込むことになる可能性もあります。

提案1のように既存のアサーション機構に一元的な契約違反ハンドラを組み込むことができたとしても、標準のassert利用者にはその方法は提供されないため、それぞれの組織・開発者が独自の契約プログラミング対応アサーション機構を作成してassertから移行するしかなく、そのような未来は望ましくありません。

この提案では、C++assertマクロの仕様を拡張して、デフォルト動作のエラーメッセージ出力だけではなく、契約違反ハンドラを呼び出すことができるようにするオプションを追加することを提案しています。ただし、デフォルトではオフにして現在の動作を完全に維持し、利用する場合は明示的なオプトインが必要になります。

具体的なスイッチの方法などは指定されていませんが、次のようなものを提案しています

  • NDEBUGが定義されていない場合に、マクロに指定された条件式がfalseを返したとき、その診断結果をstderrに出力するか、違反ハンドラを起動するかを実装定義とする
  • 違反ハンドラが呼び出される場合、その動作はhandle_enforced_contract_violation(#__VA_ARGS__, std::nothrow)の呼び出しと等価
    • ただし、kindstd::contracts::assertion_kind::cassert(新規追加する列挙値)になり
    • detection_mode`std::contracts::detection_mode::predicate_false(新規追加する列挙値)になる

ただし、一部の環境ではassertマクロの変更が困難である場合があるため、この変更は実装定義とすることを提案しています。

3. assert互換の契約アサーション

C++契約プログラミング機能の設計において中心にある考え方として、契約チェックをプログラムに導入してもプログラムの正しさは変わらないはずで、プログラムが正しいか間違っているかを識別できるようになるだけ、というものがあります。契約注釈のセマンティクスは評価毎に異なる可能性があり、それを変更可能であるため、契約注釈の評価においてはその評価がそのほかの契約注釈のすべての評価とは無関係にプログラムの正しさを変更しないことが不可欠です。

assertマクロではそれとは対照的に、そのセマンティクスはNDEBUGマクロの状態に基づいて選択されます。NDEBUGが定義されていればチェックを強制し、定義されていなければ条件は無視されます。したがって、assertの条件式の評価が同じ翻訳単位内の他のアサーションの評価や#ifndef NDEBUG等の保護されたブロックに依存するのは完全に合理的です。

従って、assertマクロの評価における副作用に依存している等の利用法は契約プログラミングにすぐに移行できません。契約プログラミング機能の拡張としてそれをサポートするようにすることも考えられますが、それは少なくとも最初のMVPの後になります。

この提案では、暫定的な解決策として、assertマクロとよく似た契約注釈構文を追加することを提案しています。これにより、contract_assertはそのままにしてほとんどのユースケースをカバーする一方で、contract_assertを利用できないようなアサーションにはこちらを使用することで契約プログラミングへの移行を容易にします。

提案するのは、partial_contract_assert()という契約注釈で、これはcontract_assertをベースとしながらも次の違いがあります

  • partial_contract_assertは新しいキーワードとして導入
  • 翻訳単位内のpartial_contract_assertは、同じ実装定義のセマンティクスで評価される
  • partial_contract_assertが常に同じ評価セマンティクスを持つようにODRを拡張
  • チェックを行うセマンティクス(enforce, quick_enforce, observe)を利用して、partial_contract_assertを評価する場合、述語は通常通り評価され、省略されない
  • partial_contract_assertはcontract-assertion sequencesの一部ではないため、他のすべての契約アサーションに対して常に語彙順で評価される
  • partial_contract_assertの評価は繰り返すことができない
  • partial_contract_assert内では自動記憶域期間を持つオブジェクトを参照するid式の型は、他の契約アサーションとは異なり暗黙的にconstにならない

partial_contract_assertはほぼ、assertマクロと同じような動作をします。

SG21におけるレビューでは、提案1と2は継続して議論することに合意されたものの、提案3はあまり好まれず、MVP後の機能として検討していくことにも合意が取れませんでした。

P3292R0 Provenance and Concurrency

C++のオブジェクトライフタイムの開始付近での微妙な問題についての改善策の提案。

この提案では、コンパイラが間違ったコードを生成する2つの例を挙げています。

まず一つは、道徳的にはコンパイラが正しいと思われるものの、実際には正しくない例です。

void some_extern_func(int);

int* f(int* q) {
  int* p = new int;
  *p = 123; // A
  *q = 456; // B
  some_extern_func(*p); // C、`some_extern_func(123)`と書き換えてもいい?
  return p;
}

clangとgccはどちらも、O1以上の最適化レベルでこれを間違ってコンパイルし、Cの個所を定数伝播してsome_extern_func(123)と書き換えます。しかしこれは間違っており、実際にはpqがライフタイムルールに違反することなく等しくなる場合があります。その場合、BではAにおけるストアを上書きします。

当然シングルスレッドなプログラムにおいてはそれは起こらないのですが、並行プログラムにおいてはオブジェクトへのポインタをその作成前の時点に送ることができます。

int* f(int* q) {
  int* p = new int;
  *p = 123; // A
  *q = 456; // B
  some_extern_func(*p); // C. Can we rewrite this to `some_extern_func(123)` ?
  return p;
}

int dummy;
std::atomic<int *> ptr_to_int_1{&dummy};
std::atomic<int *> ptr_to_int_2{&dummy};

void t1() {
  int* q = ptr_to_int_1.load(relaxed);
  int* p = f(q);
  // ptr_to_int_1.load()よりも前にこの行を移動できる
  ptr_to_int_2.store(p, relaxed);
}

void t2() {
  // t1のストアをロンダリングして、t1に送り返す
  int* p = ptr_to_int_2.load(relaxed);
  ptr_to_int_1.store(p, relaxed);
}

t1(), t2()がそれぞれ別のスレッドで実行されているとします。t1()を処理するCPUはptr_to_int_2.store(p, relaxed)を他のすべてのコードよりも前に並べ替えることができます(relaxedはあらゆる順序付け保証がない)。 その後t2()ptr_to_int_2にストアされた値を読み取ってptr_to_int_1へストアし、t1()はその後でptr_to_int_1をロードしてfに渡す、ということができます。すると、pqは同じ値になります。

f()内でnew intと確保されているintオブジェクトのライフタイムはそのアクセスが起こるよりも前に開始されており、ユーザーはこのintオブジェクトのライフタイム開始前にそのポインタを操作しているものの、デリファレンスはしていないので、これはライフタイムのルールに違反していません。

2つ目の例は道徳的にコンパイラが間違っており、実際間違っている例です。

void some_extern_function();

int* f() {
  int *p = new int;
  *p = 123; // A
  some_extern_function(); // B
  return p;
}

コンパイラの最適化はこのコードを次のように並べ替える(AとBを入れ替える)ことができるでしょうか?

int* f() {
  int *p = new int;
  some_extern_function(); // B
  *p = 123; // A
  return p;
}

clangは誤って、これに「はい」と答えることがあります。ただし、このコード単体ではそれは起こらず、コードが追加されるとループ分析パスのバグによりこれが起こることがあります。ただし、コンパイラが並べ替えるかどうかという問題は、並べ替えを許可すべきかという問題とは無関係です。これは許可すべきではありません。

これが間違っている理由は、some_extern_function()の実装でreleaseフェンスが使用されていてsome_extern_function()が他の場所からも呼び出されている場合に、フェンスを使用した単純なメッセージパッシングが阻害される可能性があるためです。

atomic<int*> atomic_int_ptr;

void some_extern_function() {
  atomic_thread_fence(memory_order_release);
}

int* f() {
  int *p = new int;
  *p = 123; // A
  some_extern_function(); // B
  return p;
}

void message_pass_f() {
  int* p = f();
  atomic_int_ptr.store(p, relaxed);
}

void receive_message() {
  int* p = atomic_int_ptr.load(relaxed);
  atomic_thread_fence(acquire);
  if (p != nullptr) {
    assert(*p == 123);
  }
}

receive_message()において、pnullptrではない場合(すなわち、message_pass_f()が実行されている場合)、*pの値は123であることがメモリモデルによって保証されています(message_pass_f()atomic_int_ptr.store(p, relaxed)が実行されている場合、releaseフェンスとacquireフェンスの間に順序が成立し、なおかつrelaxedなロードストアがフェンスを跨がないことで、*pに123が書き込まれた後でatomic_int_ptrにセットされ、pnullptrでない値で読みだされた場合は*pは確実に123になります)。

この場合にAとBを入れ替えると、このメモリモデルによって保証された動作が壊れることが分かります。したがって、このような最適化は厳密には許可されません。

しかし、このようなコンパイラの間違いは許される側面もあります。pのメモリの割り当ては関数ローカルなものであり、シングルスレッドのセマンティクスに一致する限りそのスコープから外に出るまでは自由に並べ替えられるはずです。むしろ、並行プログラムにおける同期によってそれが阻害され、アクセスできないはずのメモリのアドレスに影響を与えることができています。

この提案は、ポインターのprovenanceを導入することでこれらの様なコンパイラの間違いを遡及的に正当化しようとしています。ポインタのprovenanceとは、ポインタの出どころ(由来、来歴)を重視したポインタモデルであり、ポインタのアドレスが単なる整数値であるという既存のセマンティクスから脱却しようとするものです。あるポインタの型とアドレスが同一だったとしても、provenanceの異なるポインタは別のポインタとして扱われます(詳しくは「ポインタ provenance」などで検索していただくと色々見つかります)。

この提案で確立した直感的な動作は「あるスレッドがあるポインタのデリファレンス権を持っていない場合、そのポインタを別のスレッドに渡すと渡されたスレッドもそのポインタのデリファレンス権を持たない」というものです。これを実現するために、Cのprovenanceモデルをベースとして、そこに新しい種類のprovenanceモデルを追加することを提案しています。

  • full provenance: Cで提案中のprovenanceモデル
    • empty provenance: 無効なポインタのprovenance
    • specific provenance: その他のポインタのprovenance
  • provisional provenance: この提案
    • 別のスレッドから受け取ったポインタのprovenance
    • 何らかの同期が確立されるまでは、デリファレンス権を得られない
    • ポインタが同期されない複数のスレッド間でやり取りされる場合、再帰的に適用される

この提案の下では、ポインタを自由に使用するためにはfull provenanceが必要となり、provisional provenanceしかないポインタでデリファレンスを行うとUBになります。

このprovenanceモデルの下では、まず1つ目の問題は次のように解決されます。

int* f(int* q) {
  int* p = new int;
  *p = 123;
  *q = 456; // E
  some_extern_func(*p);
  return p;
}

int dummy;
std::atomic<int *> ptr_to_int_1{&dummy};
std::atomic<int *> ptr_to_int_2{&dummy};

void t1() {
  int* q = ptr_to_int_1.load(relaxed); // D
  int* p = f(q);
  ptr_to_int_2.store(p, relaxed); // A
}

void t2() {
  int* p = ptr_to_int_2.load(relaxed); // B
  ptr_to_int_1.store(p, relaxed); // C
}

t2()がポインタをロード(B)すると、provisional provenanceを持つポインタが得られます。そのポインタはCでストアされ、Dでロードされるポインタqもprovisional provenanceを持ちます。Eはprovisional provenanceでデリファレンスされるためUBになります。したがって、qpが同一である可能性を考慮する必要は無くなります(UBは起こらないので、qは常にfull provenanceを持つはずであり、このローカルでfull provenanceが発生するpとはprovenanceが明らかに異なる)。

2つ目の問題も解決されます

atomic<int*> atomic_int_ptr;

int* f() {
  int *p = new int;
  *p = 123; // A
  some_extern_function(); // Really, a release fence
  return p;
}

int* f() {
  int *p = new int;
  some_extern_function(); // just a release fence
  *p = 123;
  return p;
}

void message_pass_f() {
  int* p = f();
  atomic_int_ptr.store(p, relaxed);  
}

void receive_message() {
  int* p = atomic_int_ptr.load(relaxed);
  atomic_thread_fence(acquire);
  // P
  if (p != nullptr) {
    assert(*p == 123);
  }
}

receive_message()で取得されるポインタpはprovisional provenanceを持ちます。acquireフェンス直後の点Pについて考えると、pnullptrではない場合、intのライフタイムの開始はPよりも前に発生しており、Pはデリファレンスよりも前に発生します。これによって、pはfull provenanceを獲得し、この例にはUBはありません(f()の定義に関わらず)。その後のアサートがどう発動するかはf()がどう最適化されているかによります。

提案には追加の例があり、代替の解決手段の検討などもされています。

P3293R0 Splicing a base class subobject

リフレクション機能において、基底クラスのサブオブジェクトへアクセスする簡単な方法を提供する提案。

P2996の静的リフレクション機能においては、ある型(もしくはオブジェクト)のサブオブジェクト(非静的メンバ or 基底クラス)に対して同じ操作を順番に適用していくことが有用な状況がいくつもあります。

P2996ではTのオブジェクトobjTのサブオブジェクトを表す鏡像(meta::infosubを用いて、obj.[:sub:]という構文でそれを実現しつつスプライシング(値の展開)まで行うことができるのですが、これは基底クラスのサブオブジェクトをサポートしていないという問題があります

template <class T, class F>
void for_each_subobject(T const& obj, F f) {
  // Tのサブオブジェクトについてループする
  template for (constexpr auto sub : subobjects_of(^T)) {
    f(obj.[:sub:]); // 非静的メンバ変数に対しては有効だが、基底クラスのサブオブジェクトに対しては使用できない
  }
}

したがって、このようなコードは次のように場合分けして書き直す必要があります

template <class T, class F>
void for_each_subobject(T const& obj, F f) {
  // 基底クラスのサブオブジェクトのイテレーション
  template for (constexpr auto base : bases_of(^T)) {
    f(static_cast<type_of(base) const&>(obj));
  }

  // 非静的メンバ変数のイテレーション
  template for (constexpr auto sub : nonstatic_data_members_of(^T)) {
    f(obj.[:sub:]);
  }
}

ただしこのコードにもまだ問題があり、static_castを使用しているためにアクセスチェックが入り、privateな基底クラスにアクセスできません。これを回避するにはさらにCキャストを使用する必要があります。

template <class T, class F>
void for_each_subobject(T const& obj, F f) {
  // 基底クラスのサブオブジェクトのイテレーション
  template for (constexpr auto base : bases_of(^T)) {
    f((typename [: type_of(base) :]&)obj);
  }

  // 非静的メンバ変数のイテレーション
  template for (constexpr auto sub : nonstatic_data_members_of(^T)) {
    f(obj.[:sub:]);
  }
}

このコードにもさらに問題があり、constを書くのを忘れているため意図せずconst性のキャストも行ってしまっています。そして、これはCキャストなので、実際の継承関係に関係なくキャストが成功します。

また、キャストを使用する場合宛先の型を書かなければならず、さらにCV修飾と値カテゴリを正しく指定もする必要があります。完全なソリューションは関数テンプレートを使用することです

template <std::meta::info M, class T>
constexpr auto subobject_cast(T&& arg) -> auto&& {
    constexpr auto stripped = remove_cvref(^T);
    if constexpr (is_base(M)) {
      // 基底クラスのサブオブジェクトのイテレーション
      static_assert(is_base_of(type_of(M), stripped));
      return (typename [: copy_cvref(^T, type_of(M)) :])arg;
    } else {
      // 非静的メンバ変数の場合
      static_assert(parent_of(M) == stripped);
      return ((T&&)arg).[:M:];
    }
}

template <class T, class F>
void for_each_subobject(T const& obj, F f) {
  template for (constexpr auto sub : subobjects_of(^T)) {
    f(subobject_cast<sub>(obj));
  }
}

これはあまりに冗長で、これを書く必要性を要求するのは間違っています。

この提案はこの解決のために、現在のobj.[:nsdm:]nsdmT型の非静的メンバ変数の鏡像)という記法が非静的メンバ変数にしかアクセスできないのを修正して、同じ記法をクラスの任意のサブオブジェクトにアクセスするためのものとして再定義することを提案しています。

さらに追加で、&[:mem:]mamTの基底クラスBの鏡像)という構文は、適切なオフセットを持つB T::*を生成するようにすることも提案しています。

P2996がこうなっていない理由は、非リフレクションの場合にobj.member_nameの記法によってアクセスできるのはメンバ変数に限定されるためで、基底クラスがこのようにアクセスできないのは基底クラスには通常メンバ変数のような名前が無いためです。

この提案の変更ではリフレクションの文脈(スプライシング)において対応する構文で言語(非リフレクション部分のC++)が実行できないことを実行できるようにしてしまいます。しかし、リフレクションは現在のコア言語がネイティブに実行できないことを可能にするものであるため、これは問題にならないと主張しています。

この提案によって、最初のコードがそのまま意図通りに動作するようになります

template <class T, class F>
void for_each_subobject(T const& obj, F f) {
  // Tのサブオブジェクトについてループする
  template for (constexpr auto sub : subobjects_of(^T)) {
    f(obj.[:sub:]); // ok、この提案後
  }
}

P3294R0 Code Injection with Token Sequences

トークンシーケンスを用いてコード注入によるコンパイル時コード生成機能の提案。

C++26に向けて静的リフレクション機能(P2996)の議論が進行しています。P2996は最初のリフレクション提案として主にコンパイル時にC++エンティティの特性を問い合わせて取得し、それを利活用するところの基盤機能を提案することに主眼を置いています。もちろん、そこにはリフレクション結果を利用したコード生成機能も含まれているのですが、問い合わせ部分と比較すると相対的に小さく、まだまだ力不足です。

この提案は、そのP2996の機能をベースとしたコード生成機能を提案するものです。特に、C++のコード片をリフレクションによって取り扱い、任意の場所に注入することでコードを生成させる機能を提案しています。

P2996の部分については以前の記事を参照

そのようなコード片は、この提案ではトークンシーケンス(Token Sequences)と呼ばれており、@tokens{ balanced-brace-tokens }の形で導入します。balanced-brace-tokens{}のペアが対応していることのみを要求するC++の構文要素(C++トークンのシーケンス)を指しており、@tokensリテラルは指定されたトークンシーケンスを保持したstd::meta::info型の値を返します。

constexpr auto t1 = @tokens { a + b };      // 3トークンのトークンシーケンス
static_assert(std::is_same_v<decltype(t1), const std::meta::info>);

constexpr auto t2 = @tokens { a += ( };     // トークン列は意味を持つ必要が無い
constexpr auto t3 = @tokens { abc { def };  // ng、{}が対応づいていない

この例からも分からるように、トークンシーケンスはあくまでC++トークン片であり、指定されたトークン列はC++コードとして構文的・意味的に有効であることは要求されず、また検査されません。それは、トークンシーケンスを実際のコードに注入した後に改めて行われます。

トークンシーケンスは+. +=によって連結することができます。

constexpr auto t1 = @tokens { c =  };
constexpr auto t2 = @tokens { a + b; };
constexpr auto t3 = t1 + t2;
static_assert(t3 == @tokens { c = a + b; });

この連結は字句的に行われるのではなく、トークンとして連結されます

constexpr auto t1 = @tokens { abc };
constexpr auto t2 = @tokens { def };
constexpr auto t3 = t1 + t2;  // 1トークン + 1トークン = 2トークン

static_assert(t3 != @tokens { abcdef });
static_assert(t3 == @tokens { abc def });

トークンシーケンスはプリプロセッシングディレクティブではなくC++コードの機能であるため、マクロはトークンシーケンス上でもこれまで通りに動作しますが、トークンシーケンスの境界を越えて関数マクロ呼び出しが作用しないため少し注意が必要です。

// 文字列リテラルの連結は動作する
static_assert(@tokens { "abc" "def" } == @tokens { "abcdef" });

// トークンシーケンスを結合した後では文字列リテラルは連結しない
static_assert(@tokens { "abc" } + @tokens { "def" } != @tokens { "abcdef" });


#define PLUS_ONE(x) ((x) + 1)

// トークンシーケンス内でもマクロは動作する
static_assert(@tokens { PLUS_ONE(x) } == @tokens { ((x) + 1) });

// トークンシーケンスの境界を越えて関数マクロが動作しようとしない
static_assert(@tokens { PLUS_ONE(x } + @tokens{ ) } != @tokens { ((x } +tokens{) + 1) });
static_assert(@tokens { PLUS_ONE(x } + @tokens{ ) } == @tokens { PLUS_ONE(x) });

// トークンシーケンスの連結によってマクロ呼び出しが現れても、そこでは呼び出されない
constexpr auto tok2 = []{
    auto t = @tokens { PLUS_ONE(x };
    constexpr_print_str("Logging...\n");
    t += @tokens{ ) }
    return t;
}();
static_assert(tok2 != @tokens { PLUS_ONE(x) });

トークンシーケンスは通常、後から別の場所で注入して使用します。しかし、含まれるトークンは単なるトークンでしかなく、注入先のコンテキストで意味を持つことはあっても、トークンシーケンスを作成した場所での意味をキャプチャすることができません。そのため、トークンシーケンスの作成時にその外部のコンテキストにおけるトークンを意味と共にキャプチャする構文が必要になります。そのために、ここでは次の2つの特別なトークンシーケンス専用構文を提案しています

  • $eval(e) : std::meta::info型のeを受け取り、eの値を保持した疑似リテラルトークンに置き換える
    • eトークンシーケンスである場合、eトークンシーケンスをそこに展開し連結する
  • $id(e) : 文字列または整数型のeを受け取り、その値に置き換える
    • $id(e, ...) : 複数の文字列または整数の値を連結して1つの識別子に置き換える

これらのキャプチャ構文がトークンシーケンス中に現れると、その中の式を解析して評価し、トークンシーケンス中でこの説明のように対応するものに置き換えられます。

これらの基本機能によって、ボイラープレートを伴うようなコードを簡潔に生成することができるようになります。

std::tupleのストレージを定義する例

template<class... Ts>
struct Tuple {
    consteval {
      // パラメータパックTsから個別のmeta::info型を取得し保存
      std::array types{^Ts...};

      // Tsの型ごとにメンバ変数を宣言する
      for (size_t i = 0; i != types.size(); ++i) {
        inject(@tokens {
            [[no_unique_address]]
            [: $eval(types[i]) :] $id("_", i);
        });
        /* Tsの型名をTi(iは数値)とすると
        [[no_unique_address]]
        Ti _i;
        のようなメンバ宣言がTupleクラスの定義に注入される
        */
      }
    }
};

constevalブロックは宣言コンテキストで任意の定数式を実行するための構文です。上の方のP3289R0で提案されているものです。std::meta::inject()はここで提案されている新しいリフレクションメタ関数で、受け取ったstd::meta::infoが保持しているコード片をC++コードとしてそのコンテキストに(副作用として)注入するものです。

上記例では、Tuple<Ts...>型の直接のメンバ変数として、Tsの各型に対応するメンバを宣言しています。現在のstd::tupleの場合、継承を用いた非常に複雑な実装によってこれを実現しているため、かなり簡潔になっていることがわかると思います。

std::enable_ifを定義する例

template <bool B, class T=void>
struct enable_if {
    consteval {
      // Bがtrueの場合にのみ、`using type = T;`を注入する
      if (B) {
        inject(@tokens { using type = T; });
      }
    };
};

std::enable_if<expr, T>exprtrueに評価される場合にのみメンバ型typeとしてTを宣言するものです。この実装は通常テンプレートの部分的特殊化を用いて行われますが、ここでは1つのクラス定義のみで簡潔に定義出来ています。

簡単なプロパティ機能を注入する関数を定義する例

consteval auto property(meta::info type, std::string name) -> void {
  // メンバ変数名を作成
  std::string member_name = "m_" + name;

  // メンバ変数宣言を注入
  // 型名をTとすると
  // T m_name;
  // のような宣言が注入される
  inject(@tokens {
      [:$eval(type):] $id(member_name);
  });

  // ゲッター関数宣言を注入
  // 型名をTとすると
  // auto get_name() -> T const& {
  //   return m_name;
  // }
  // のような宣言が注入される
  inject(@tokens {
      auto $id("get_", name)() -> [:$eval(type):] const& {
          return $id(member_name);
      }
  });

  // ゲッター関数宣言を注入
  // 型名をTとすると
  // auto set_name(T const& x) -> void {
  //   m_name = x;
  // }
  // のような宣言が注入される
  inject(@tokens {
      auto $id("set_", name)(typename [:$eval(type):] const& x)
          -> void {
          $id(member_name) = x;
      }
  });
}


// 使用例
struct Book {
  consteval {
    property(^std::string, "author");
    property(^std::string, "title");
  }
};

int main() {
  Book b:

  b.set_author("太宰治");
  b.set_title("人間失格");

  std::string author = b.get_author();
  std::string title = b.get_title();
}

ここではメンバ変数・ゲッター・セッターを3つのinject()によって生成していますが、1つにまとめることもできます。

後置インクリメント演算子を生成する関数の例

consteval auto postfix_increment() -> void {
  // 呼び出されたコンテキストのクラス型を取得
  auto T = type_of(std::meta::current());

  inject(@tokens {
    auto operator++(int) -> [:$eval(T):] {
        auto tmp = *this;
        ++*this;  // 定義済みの前置++を使用して実装
        return tmp;
    }
  });
}

// 使用例
struct C {
  int i;

  auto operator++() -> C& {
      ++i;
      return *this;
  }

  consteval { postfix_increment(); }
};

既存のクラス(std::vector)をラップして、関数呼び出しにログ出力を追加する例

template <typename T>
class LoggingVector {
  std::vector<T> impl;

public:
  LoggingVector(std::vector<T> v) : impl(std::move(v)) { }

  consteval {
    for (std::meta::info fun : /* vectorのpublicな非特殊メンバ関数全てのリフレクションを取得 */) {
      // ラッパ関数からvectorの関数への引数転送トークンを構築する
      auto argument_list = @tokens { };
      // 元関数funの引数毎にループ
      for (size_t i = 0; i != parameters_of(fun).size(); ++i) {
        // 2つ目以降の引数の時はまず`,`トークンを入れる
        if (i > 0) {
          argument_list += @tokens { , };
        }

        // 引数転送(std::forward相当の処理)のトークンを構築
        argument_list += @tokens {
          static_cast<decltype($id("p", i))&&>($id("p", i))
          // static_cast<decltype(p0)&&>(p0) の様なトークンが構築される
        };
      }

      // ラップした同名同引数メンバ関数の定義を注入する
      inject(@tokens {
        declare [: $eval(decl_of(fun, "p")) :] {
          // 関数名を出力するコード
          std::println("Calling {}", $eval(name_of(fun)));
          // vectorの対応する関数を呼び出すコード
          return impl.[: $eval(fun) :]( $eval(argument_list) );
        }
      });
    }
  }
};

ここでは新しい構文が2つ使われています。decl_of(fun, "p")は関数のリフレクションfunに対して、その仮引数名をプリフィックスpから始まる連番名とした関数宣言のリフレクションを返し、declare[:e:]は宣言スプライスと呼ばれるスプラシング構文のファミリで、関数リフレクションeを受けてその宣言トークンを展開するものです。

これによって、LoggingVectorは自動的にstd::vectorの全メンバ関数(特殊メンバ関数を除く)と同名動引数の関数を備え、なおかつそれらの呼び出しは関数名をログ出力したうえで内部std::vectorの対応するメンバ関数を呼び出す(引数は適切にforwardされる)ようになります。

例えば、次のようなコードが生成されます

template <typename T>
class LoggingVector {
  std::vector<T> impl;

public:
  LoggingVector(std::vector<T> v) : impl(std::move(v)) { }
  
  auto clear() -> void {
    std::println("Calling {}", "clear");
    return impl.clear();
  }

  auto push_back(T const& value) -> void {
    std::println("Calling {}", "push_back");
    return impl.push_back(static_cast<T const&>(value));
  }

  auto push_back(T&& value) -> void {
    std::println("Calling {}", "push_back");
    return impl.push_back(static_cast<T&&>(value));
  }

  ...
};

一通り強力な(現代の言語では一般な?)コード生成機能が備わっていることが分かりましたが、この提案はここで終わりではなく、さらにこれらのトークンシーケンスとstd::meta::injectを利用した衛生的マクロ(Hygienic Macros)機能も提案しています。

例えば、std::forward()の正しい使用方法はフォワーディングリファレンスT&& tに対して、std::forward<T>(t)と記述しますが、このコンテキストではT = decltype(t)が成立しますが、std::forward()を使う場合はとにかく2つの名前を記述しなければなりません。これは簡単ではあるものの記述が少し面倒であり、以前にはこれを簡略化する言語機能が提案されていました(P0644R1, P1221R1)。

// P1221R1の衛生的マクロによるstd::forwardのラッパマクロ
using fwd(using auto x) {
  return static_cast<decltype(x)&&>(x);
}

// 新旧転送構文の比較
auto old_f = [](auto&& x) { return std::forward<decltype(x)>(x); };
auto new_f = [](auto&& x) { return fwd(x); };

この提案のトークンシーケンスを用いても、同じような衛生的マクロシステムを導入することができます。

// トークンシーケンスを用いたマクロ定義
consteval auto fwd2(@tokens x) -> info {
  return @tokens {
    static_cast<decltype($eval(x))&&>($eval(x));
  };
}

// 転送構文
auto new_f2 = [](auto&& x) { return fwd2!(x); };

マクロ定義は、トークンシーケンスリテラルを受け取りstd::meta::infoを返すconsteval関数として定義することができます。呼び出しはRustのマクロ呼び出しを参考にしてマクロ名の後に!を付けた形の関数呼び出しとしてfwd2!(x)のように行い、これはここまでで何度も使用しているinject(fwd2(@tokens { x }))の構文糖衣です。

この新しいマクロはC++のコードとして記述され、トークンシーケンス周りを覗いてC++の関数呼び出しと同じような振る舞いをします。これによってC由来のマクロにあるような問題がいくつか(呼び出し引数内の,に対する鋭敏さやスコープの問題など)解消されます。例えば、マクロを名前空間に配置することができ、モジュールからエクスポートすることができ、複雑なマクロもCプリプロセッサではなくC++コードで記述することができます。

アサーションマクロの例

consteval auto assert_eq(@tokens a,
                         @tokens b) -> info {
  return @tokens {
    do {
      // aの式文字列と式の評価結果を取得
      auto sa = $eval(stringify(a));
      auto va = $eval(a);

      // bの式文字列と式の評価結果を取得
      auto sb = $eval(stringify(b));
      auto vb = $eval(b);

      if (not (va == vb)) {
        // アサートが失敗した場合、それぞれの式の評価結果と場所を出力して終了させる
        std::println(
            stderr,
            "{} ({}) == {} ({}) failed at {}",
            sa, va,
            sb, vb,
            $eval(source_location_of(a)));

        std::abort();
      }
    } while (false);
  };
}
// こう書くと
assert_eq!(42, factorial(3));

// こう展開される
do {
  auto sa = "42";
  auto va = 42;

  auto sb = "factorial(3)";
  auto vb = factorial(3);

  if (not (va == vb)) {
    std::println(
        stderr,
        "{} ({}) == {} ({}) failed at {}",
        sa, va,
        sb, vb,
        /* some source location */);

    std::abort();
  }
} while(false);

このようなコードはCマクロでも記述することはできるのですが、こうして定義した方が圧倒的に読みやすく、書きやすいことが分かると思います。ただし、これにはCマクロにある命名の問題が依然としてあります。例えばassert_eq!(42, sa * 2)と書くと、マクロ定義内のローカル変数saと引数にあるsaが衝突します。

std::format()のフォーマット文字列に変数名を指定して、呼び出しコンテキストからそれを持ってくるラッパマクロの例。

// パーサの実装はここでは主題でないため省略するものの
// "x={this->x:02} y={this->y:02}" のようなフォーマット文字列に対して
// {.format_str="x={:02} y={:02}", .args={"this->x", "this->y"}} のようなものを返すことを想定
struct FormatParts {
  string_view format_str;
  vector<string_view> args;
};
consteval auto parse_format_string(string_view) -> FormatParts;

consteval auto format(string_view str) -> meta::info {
  auto parts = parse_format_string(str);

  auto tok = @tokens {
    // まずはフォーマット文字列を渡すところまで、閉じかっこは後で付加
    ::std::format($eval(parts.format_str)
  };

  // 元のフォーマット文字列に指定された変数名のカンマ区切り列を構築する
  for (string_view arg : parts.args) {
    // tokenize()は文字列をトークン列に変換するもの
    tok += @tokens { , $eval(tokenize(arg)) };
  }

  // 閉じかっこを追加
  tok += @tokens { ) };
  return tok;
}

例えばformat!("x={x}")と呼び出すと、::std::format("x={}", x)に書き換えて呼び出しを行います。このようなことを、専用の言語機能無しで行うことができます。

ただし制限はあり、このマクロ呼び出しレイヤでは型情報が無いためフォーマット文字列のチェックを完全に行うことはできません。ここでのチェックはせいぜい{}の対応がきちんととれているかをチェックするまでです。しかしこれは現実のユースケースの9割をカバーでき、std::format専用の言語機能ではなくユーザーサイドで自由に記述することができます。

最後にまとめると、この提案は次の事を提案しています

  • トークンシーケンスを導入する機能: @token {...}
  • トークンシーケンス内に外部コンテキストを導入するインタポーザー: $id(), $eval()
  • 宣言スプライシング: declare [: fun :]
  • トークンシーケンスを注入するメタ関数: std::meta::inject()
  • トークンシーケンスを処理するための新しいメタプログラミング機能
    • 連結: + +=
    • トークンシーケンスと文字列の相互変換機能: stringify(), tokenize()
    • トークンシーケンスをトークンの範囲に分割する機能
  • 衛生的マクロ
    • トークンシーケンスを関数引数として受け取る機能
    • 関数から返されるトークンシーケンスを直接挿入する機能: 関数名末尾の!

なお、それぞれの命名や構文については仮のもので後程変更されるかもしれません。

P3295R0 Freestanding constexpr containers and constexpr exception types

フリースタンディング環境の定数式において、std::vector等を使用可能にする提案。

P2996の静的リフレクション機能では、members_of()などvector<meta::info>を返す関数がいくつかあります。静的リフレクションはフリースタンディングかどうかに関わらず価値があり利用できるべきですが、std::vectorはフリースタンディング環境では必須ではなく利用できない場合があるため、そのせいでフリースタンディング環境で静的リフレクションが利用できなくなる可能性があります。

この提案は、その解消のために、P2996で利用されているフリースタンディング環境で必須ではない機能の最小セットについて、定数式でのみ使用可能となるようにしようとするものです。

フリースタンディング環境では多くの場合、実行時にメモリを確保したり例外を投げたりすることができませんが、コンパイル時にはそのような制限はありません。そのため、フリースタンディング環境ではstd::vectorstd::allocatorconstevalで有効化します。

std::vectorは例外を投げるAPIがあり、そこではout_of_rangelength_errorが使用されていますがこれもフリースタンディングではないためこれらがコードに出現してしまうと、定数式がそこを通らなくてもコンパイルエラーを起こしてしまいます。そのため、これらの例外型をホスト環境ではconstexpr、フリースタンディング環境ではconstevalで有効化します。

さらに、out_of_rangelength_errorには文字列を受け取るAPIがあり、これもまたフリースタンディング指定されていないstd::stringに依存しています。そのため、std::stringも同様にフリースタンディング環境ではconstevalで有効化します。最後に、現在std::string_viewの一部の関数はfreestanding-deleted指定されていますが、これをconstevalで有効化します。

まとめると

  • ホスト環境でconstexprを付加
    • exception
    • logic_error
    • length_error
    • out_of_range
  • フリースタンディング環境でconstevalを付加
    • logic_error
    • length_error
    • out_of_range
    • allocator
    • string_view
      • freestanding-deletedメンバを変更
    • string
    • vector

となります。

例外関連型をconstexpr指定しておくことは将来的な定数式における例外機構(リフレクションのエラー処理機構)にとってメリットになる可能性があります。

P3296R0 let_with_async_scope

提案中のcounting_scopeの問題を修正する提案。

counting_scopeはP3149で提案されているもので、並列数や処理の継続有無が実行時に決まる場合にコンパイル時の|によるチェーンと同様に処理の依存関係にスコープを付けて管理することで、非同期処理が確実に完了するまで関連するリソースを保護しておこうとするものです。

counting_scopeについては以前の記事を参照

counting_scope.nest()によって処理のスコープを開き、その内部でさらに.nest()を呼ぶことで非同期処理のネストを表現し、ネストせずに単に継続することで異なるスコープによる非同期処理の継続を表現します。シングルスレッドのプログラムで言うなら.nest()は単にブロックスコープ{}を導入しているのにあたります。

counting_scopeはその非同期処理を待機するために.join()を明示的に呼び出さなければなりません。これはcounting_scopeオブジェクトが管理しているすべての非同期処理が完了するのを待機しなければ、関連するリソースを破棄するタイミングが分からないためです。

// 保護対象のリソース
some_data_type scoped_data = make_scoped_data();

// counting_scopeオブジェクト初期化
ex::counting_scope scope;

// 実行時にその構造が決定される非同期処理
ex::sender auto async_work = scope.nest(on(exec, [&] {
    scope.nest(on(exec, [&] {
        if (need_more_work(scoped_data)) {
            scope.nest(on(exec, [&] { do_more_work(scoped_data); }));
            scope.nest(on(exec, [&] { do_more_other_work(scoped_data); }));
        }
    }));
    scope.nest(on(exec, [&] { do_something_else_with(scoped_data); }));
}));

// 非同期処理を開始(別スレッドで実行されているとする
ex::spawn(std::move(async_work), scope);

// 例外を投げると・・・?
maybe_throw();

// 非同期処理の完了待機
this_thread::sync_wait(scope.join());

maybe_throw()関数の呼び出しが無い場合、このコードはscopeに登録された非同期処理が完了するのを最後の行のscope.join()(が返すsenderを受けているsync_wait())で待機します。非同期処理が完了するとそのブロックが解除され、このスコープを抜ける時に、scope -> scoped_dataの順で破棄されることで、非同期処理が完了したことを確認したうえでそこで使用されていたリソースの解放を安全に行うことができます。

しかし、maybe_throw()関数が例外を投げている場合その保証は破壊されます。.join()呼び出し前(すなわち、非同期処理が完了してるかどうかわからないタイミングで)に例外によってこのスコープの変数が破棄されることで、scoped_dataだけでなくscopeに対しても生存期間外にアクセスされるリスクが生じます。

この提案は、この問題の解決のためにcounting_scopeと非同期タスクをラップして管理するlet_async_scopeを提案しています。let_async_scopeの返すsenderは渡された非同期処理内で例外が送出されても、関連する(.nest()された)非同期処理がすべて完了するまで完了状態にならず、これによって先程の問題は起きなくなります。なお、例外はsenderのエラーチャネルで伝播されます。

auto scope_sender = just(make_scoped_data())  // 非同期リソースをjust()で注入
                  | let_async_scope([](auto scope_token,  // counting_scopeオブジェクト
                                       auto& scoped_data) // 非同期リソース
  {
    scope_token.nest(on(exec, [scope_token, &scoped_data] {
        scope_token.nest(on(exec, [scope_token, &scoped_data] {
            if (need_more_work(scoped_data)) {
                scope_token.nest(on(exec, [&scoped_data] { do_more_work(scoped_data); }));
                scope_token.nest(on(exec, [&scoped_data] { do_more_other_work(scoped_data); }));
            }
        }));
        scope_token.nest(on(exec, [&scoped_data] { do_something_else_with(scoped_data); }));
    }));

    maybe_throw();
  });

// 非同期処理の実行待機
this_thread::sync_wait(scope_sender);

また、let_async_scopeの返すsenderに対する停止要求は、ネストされているすべてのsenderに伝達され、それらのsenderがそれに応答してさらに.nest()によってクリーンアップ作業をスケジュールすることが許可されます。これによって、ネストしたsenderlet_async_scopeのスコープ内でそのまま処理のキャンセル作業を行うことができ、その場合でも使用するリソースの破棄は安全になされます。

P3297R0 C++26 Needs Contract Checking

C++26で契約プログラミング機能を出荷すべきとする提案。

この提案の目的は

  1. C++で日常的に実用的で安全性が重視されるソフトウェアを書いているプログラマの声を届けること
  2. SG21とEWGが活動の質と専門家意識を向上させるための行動を呼びかけること
  3. SG21とEWGに対して、C++26のドラフトに基本的な要件を満たす最小限の実行可能な仕様を盛り込むように呼び掛ける

などにあります。この提案による最小限の実行可能な仕様とは

  1. 関数に付随する構文
    • 事前条件を宣言する
    • 静的解析ツールがパースできる
  2. 実行時チェックの挿入
    • 通常のC++コードを実行する
    • チェック内でタイムトラベル最適化を禁止する

このために、委員会に対して呼びかけを行っています。

  • SG21
    1. 否定的な態度、あるいは言葉による闘争心は、議論の質を低下させ、貢献者を追い出す。これはもはや容認できない
    2. 新しい情報なしに同じ議論を繰り返すのは時間の無駄。誰の考えも変えられないのであれば繰り返す必要はない
    3. 優れた根拠を持つ提案は人々の心を変える。説得力のある提案を発表しないことは、自身の影響力を制限することになる
  • SG22
    1. C++の安全性とは何かについて、強い姿勢を示す
    2. EWGがC++の安全性を支援するために何ができるかを定義し、その目標を設定する
  • EWG
    1. 現在満たされていない安全性・セキュリティに関するニーズは、C++の存立に関わる問題であることを認識する
    2. 少なくとも次の機能を実現することを優先する
      • 関数に付随する構文
      • 実行時チェックの挿入
    3. 理想的な機能を追求するための機会費用を理解する
      • 現在のC++コードは、単純な契約チェック機能が無いまま、何年もコードが書かれ、実行され、精査されている

この提案の著者の方は、組み込み業界など安全性やセキュリティに関わる部分でC++コードベースを扱ってきた経験をもとに、現在の環境でコードベースへの安全性やセキュリティの要件が重要になっており、現在のC++はそのニーズをほとんど満たしていないことを説明しています。その上で、契約プログラミング機能がC++コードベースの安全性向上に貢献する重要なステップであり、これがC++29に3年間遅れることの機会費用が高すぎる(その間に、Rustをはじめとする他の言語基盤への移行を選択させてしまう)として、MVP(P2900)の全体でなくても最低限の部分をC++26に間に合わせるべきとしています。

P3298R0 Implicit user-defined conversion functions as operator.()

ユーザー定義の変換関数としてのoperator.()の提案。

C++でスマート参照とはスマートポインタの参照版のようなもので、ポインタの動作をユーザーに公開しないようにした別の場所にあるオブジェクトのプロキシクラスです。このようなものを作成しようとする場合、operator.()が必須になります。

template<class X>
class Ref {
public:
  explicit Ref(int a) :p{new X{a}} {}
  ~Ref() { delete p; }

  // operator.()はない
  X& operator.() {
    /* maybe some code here */
    return *p;
  }

  void rebind(X* pp) {
    delete p;
    p = pp;
  }
  
  ...

private:
  X* p;
};

Ref<X> x{99};
x.f();    // => (x.operator.()).f()  => (*x.p).f()
x = X{9}; // => x.operator.() = X{9} => (*x.p )=X{9}

x.rebind(p); // delete old x.p and make x.p=p(operator.()を使用しない

しかしoperator.()の適用範囲には問題があります。1つは、代入操作(=)は.演算子を明示的に使用していませんがoperator.()を呼び出すべきか?(同様のことが各演算子に言える)という問題であり、もう一つは、.によるアクセスでそのオブジェクト自身のメンバ(Ref<T>型のメンバ)にアクセスするにはどうすればいいか?という問題です。

operator.()は以前から何度も提案されてきましたが、これらの問題を解決できなかったため、何度か提案されては否決されてきたようです。

この提案は、ユーザー定義.演算子を暗黙変換の延長線上に実現しようとするものです。

あるオブジェクトobjに対してobj.member()のようにアクセスを行う時、現在でも基底クラスへの変換は考慮されています。この提案はこの変換過程にユーザー定義変換演算子を作用させることで.アクセスをユーザーがオーバーライド可能にしようとしています。具体的には、implicit指定子をユーザー定義変換演算子に指定することで、その変換演算子.による基底クラスへの変換時に候補に入れるようにします。この場合の名前探索のルールは継承と同様なので、予期しない変換が起こる可能性は低くなります。

// プロキシ参照型の例
template<typename T>
class Proxy {
  Proxy(T& object) : m_ptr(&object) {}

  // この提案によるimplicit変換演算子
  implicit operator T&() { return *m_ptr; }
  implicit operator const T&() const { return *m_ptr; }
private:
  T* m_ptr;
};

struct MyClass {
  using Type = int;
  int x;
  void f();
  static void s();
};

void g(MyClass& o);

void test() {
  MyClass obj;

  Proxy<MyClass> p(obj);
  Proxy<MyClass>* pp = &p;
  
  p.f();    // Proxy<T>には.f()が定義されていないので、その基底クラスとimplicit変換演算子をの戻り値型をチェックする
  p.x = 43; // Proxy<T>には.xが定義されていないので、その基底クラスとimplicit変換演算子をの戻り値型をチェックする
  g(p);     // g()はProxy<T>を受け取らないため、その基底クラスとimplicit変換演算子をの戻り値型をチェックする

  // All name lookup considers names in bases and ICF return types
  // 全ての名前探索では、基底クラスとimplicit変換演算子の戻り値型における名前が考慮される
  Proxy<MyClass>::Type anInt;

  // operator-> もまた、基底クラスとimplicit変換演算子の戻り値型を考慮する
  pp->f();
  pp->MyClass::f();     // 冗長だが許可される。Proxy型がf()を備えている場合に便利
  Proxy<MyClass>::s();  // 名前探索によって、MyClassの静的メンバ関数s()を呼び出す
}

この提案は.だけではなく、メンバアクセスと同様に基底クラスへの変換が考慮される場所でimplicit変換演算子の結果型を候補として加えます。そのため、関数へ渡す場合などの暗黙変換や、->演算子でも同様にimplicit変換演算子が作用します。

この提案の以前の提案からの改善点は

  • 仕様の複雑化を回避する
    • operator.()をはじめとする特別な構文を導入でず、名前探索は基底クラスへの変換と同じルールを使用する
      • .アクセスでそのクラス自身に自然にアクセスできる
      • =演算子の呼び出しでも、関数呼び出し時の基底クラスへの変換ルールと同様に扱われる
        • .を使用していないことで、.を使っていないのに考慮される、という状況にならない
  • implicit指定子のみでシンプルに実現可能
    • 以前の提案にあったような冗長な記述を回避

提案より、汎用lazyクラスの例

template<typename F>
struct lazy {
  lazy(F f) : func(std::move(f)) {}
  implicit operator decltype(auto)() { return func(); }
  F func;
};

提案にはこれらのほかにもいくつか利用できるシーンが挙げられています(サンプルコードはあまりありませんが)。

P3299R0 Range constructors for std::simd

std::simdrangeコンストラクタを追加する提案。

現在のstd::simdのコンストラクタにはイテレータを1つだけ受け取って、std::simdに指定されたサイズでそこからデータを読みだして構築するコンストラクタが主なものとなっています。これは、入力のデータ範囲の終端を知る方法が無く、安全ではありません。

int main() {
  float input[4] = {...};

  // イテレータ1つだけを渡して初期化
  std::simd<float, 4> simd1{input};

  // メンバ関数による初期化も同様
  std::simd<float, 4> simd2{};
  simd2.copy_from(input);
}

この理由は、std::simdが主に計算の高速化のためのものであることから、初期化時(すなわち、SIMDレジスタへの値のロード時)の範囲チェックを行わないことをデフォルトとしているためです。しかし、実際の計算中はともかく初期化のタイミングは範囲チェックをきちんとしたい需要もあり、その動作(安全性か、パフォーマンスか)をカスタマイズすることができることを望む向きもありました。

この提案は、std::simdrangeコンストラクタを追加することで安全な初期化のための選択肢を用意し、なおかつパフォーマンスを重視する場合に動作をカスタマイズすることができるようにしようとするものです。

提案では、例えば次のような範囲コンストラクタを追加しようとしています

template<class T, class Abi>
class basic_simd {
  ...

  // 現在あるイテレータ1つしか取らないコンストラクタ
  template<class It, class... Flags>
  constexpr basic_simd(It first, simd_flags<Flags...> = {});

  // 提案 イテレータペアを取るコンストラクタ
  constexpr basic_simd(It begin, It end, simd_flags<Flags...> = {});

  // 提案 イテレータとサイズを取るコンストラクタ
  constexpr basic_simd(It begin, size_type count, simd_flags<Flags...> = {});

  // 提案 固定サイズspanをとるコンストラクタ
  constexpr basic_simd(span<T, N>, simd_flags<Flags...> = {});

  // 提案 動的サイズの範囲を取るコンストラクタ
  constexpr basic_simd(Range r, simd_flags<Flags...> = {});

  ...
};

コンパイル時にそのサイズが既知の配列から値をロードする場合、そのサイズは常にぴったり合わなければならないようにすることを提案しています。すなわち、データの切り捨てや暗黙の値の追加は許可せず、プログラマが明示的にそれを指定する必要があります。

int main() {
  // 入力とサイズがぴったりの例
  std::array<float, 6> ad;
  std::simd<float, 6> simdA(ad);

  std::span<int, 8> sd(d.begin(), 8);
  std::simd<int, 8> simdB(sd);
  
  // 入力とサイズが合わない例
  std::array<float, 6> ad;
  std::simd<float 8> simdA(ad); // ng

  // 明示的にリサイズしてロードする
  std::simd<float, 6> simdA(ad);
  auto simdA8 = resize<8>(simdA); // ok
}

また、コンパイル時にサイズが既知の場合のための推論補助も提供できます

template <contiguous-range-with-static-extent Rg>
basic_simd(Rg)
  -> basic_simd<ranges::range_value_t<Rg>,
                deduce-t<ranges::range_value_t<Rg>, static-range-size<Rg>>>;

これを用いると、テンプレートパラメータの指定を省略できます。

std::array<float, 4> data;

std::simd v = data;       // simd<float>::size()が4の場合のみok
std::basic_simd w = data; // ABIタグも推論する

入力範囲のサイズが実行時まで分からない場合、サイズのミスマッチは実行時にハンドルする必要があり、コストのかからないものから次の3つがあります

  1. 未定義動作にする
  2. 正常に処理される定義済み動作にする
    • 多い場合は切り捨て、少ない場合はデフォルト値
  3. 例外を送出する

これらの3つに対応させたフラグ(simd_unchecked, simd_default_init, simd_exception)を用意してユーザーが選択可能にすることを提案していますが、それでもなおデフォルトの動作を選択する必要があります。提案では、1か2のどちらかを選択することになるだろうと述べています。

この提案はまだ、提案というよりは方向性を探る段階です。

P3301R0 inplace_stoppable_base

P2300のinplace_stop_callbackに代わるinplace_stoppable_baseの提案。

キャンセルトークン、特にstd::inplace_stop_tokenを使用してキャンセル操作をサポートしたいsenderでは、そのoperation_statereceiverと接続された後のもの)への参照を含むstd::inplace_stop_callbackメンバをoperation_state型へ追加する必要があります。

// 自作sender型、キャンセル操作をサポートしたい
class my_sender {

  // my_senderのためのoperation_state型
  template<typename R>
  class op_state {
    R receiver;

    // キャンセル受付コールバック型
    class cb_t {
      op_state& op;
    public:
      cb_t(op_state& op)
        : op(op) {}
      void operator()() const {
        op.on_stop_requested();
      }
    };

    // inplace_stop_callbackオブジェクト
    std::inplace_stop_callback<cb_t> cb;  // サイズは非ゼロ

    // キャンセル要求を受けた時の処理を実装する
    void on_stop_requested();
  };
};

inplace_stop_callbackはキャンセル要求を受けた場合のコールバックを保持しておくためのものです。ただ、このような実装方法だとoperation_state型に僅かではあるもののスペースのオーバーヘッドが生じます。

標準ライブラリの実装はこのコストを回避できる場合があるようですが、ユーザー定義のsender実装ではそれはできません。この提案は、CRTPクラステンプレートinplace_stop_tokenを追加してユーザーもこの問題を回避できるようにしようとするものです。

これを利用すると先程の例は大幅に簡略化され、さらにスペースオーバーヘッドを取り除くことができます。

class my_sender {

  // inplace_stop_tokenを使ったoperation_state実装
  template<typename R>
  class op_state : std::inplace_stoppable_base<op_state<R>> {
    R receiver;

    // invoked upon request for cancellation.
    void on_stop_requested();
  };
};

inplace_stoppable_baseの唯一のコンストラクタはprotectedで、inplace_stop_token型の引数1つを受け取ります。

std::inplace_stoppable_base<D>をCRTPで派生することでoperation_state型を構成することができ、この場合にinplace_stop_token経由でキャンセルが通知されると、on_stop_requested()が派生クラスオブジェクトで呼び出されます。

P3302R0 SG16: Unicode meeting summaries 2024-03-13 through 2024-05-08

SG16のミーティングの議事録。

2024年3月から5月にかけてオンラインで行われたSG16のミーティングの議事録で、どんな内容の話をしたかや誰がどんな発言をしたかなどが詳細に記録されています。

P3303R0 Fixing Lazy Sender Algorithm Customization

P2999の提案の欠けていた部分を埋める提案。

P2999については以前の記事を参照

P2999では提案中で触れられてはいたものの、connectおよびget_completion_signaturesに対する変更について必要な文言が抜け落ちていたようです。この提案は、それを拾い上げて補完するものです。

この提案の変更は次の2点です

  • get_completion_signatures(sndr, env)
    • まず、transform_sender(get-domain-late(sndr, env), sndr, env)を使用して入力のsendersndr)を変換し、次のその結果をsndrの代わりに使用するようにする
  • connect(sndr, rcvr)
    • まず、transform_sender(get-domain-late(sndr, get_env(rcvr)), sndr, get_env(rcvr))を使用して入力のsendersndr)を変換し、次のその結果をsndrの代わりに使用するようにする

これはP2999R3ですでに議論され、決定され、LEWGで採択された設計に沿うものです。しかし、P2999R3ではそれを標準(正確にはP2300)に適用するための文言が抜け落ちていました(うっかりミスのようです)。この提案はそれを追加するためのもので、設計変更などは行われていません。

この提案は2024年7月に行われた全体会議で承認され、C++26WDに適用されています。

P3304R0 SG14: Low Latency/Games/Embedded/Financial Trading virtual Meeting Minutes 2024/04/10

2024年4月10日に行われた、SG14のオンラインミーティングの議事録。

どのようなことを議論したのかが簡単に記載されています。

P3305R0 SG19: Machine Learning virtual Meeting Minutes to 2024/04/11-2024/05/09

2024年4月11日と5/9日に行われた、SG19のオンラインミーティングの議事録。

どのようなことを議論したのかが簡単に記載されています。

P3306R0 Atomic Read-Modify-Write Improvements

std::atomicのRead-Modify-Writeに、数値型の演算についてのAPIを追加する提案。

Read-Modify-Write(RMW)操作とは、1操作で値の読み出し・値の更新・更新値の書き込みを行う操作の事で、整数型・浮動小数点数型のstd::atomic特殊化に存在するfetch_add+=をはじめとする操作が該当します。

ただし、浮動小数点数型の場合は+= -=に相当するものしか用意されておらず、整数型の場合でも加減算と最低限のビット演算くらいしか用意されていません。

この提案は、std::atomicのRMW操作として通常の演算に対応するものを追加しようとするものです。

ここで提案されているのは次のものです

  • 整数型
    • shl : 左シフト
    • shr : 右シフト
    • mod : 剰余
    • nand : ビット毎のNAND
    • div : 割り算 /=
    • mul : 掛け算 *=
  • 浮動小数点数
    • div : 割り算 /=
    • mul : 掛け算 *=

提案されているAPIは、std::atomicメンバ関数及びフリー関数の2種類です。

これらの操作は、Kokkosのatomic_fetch_<op>やRustのアトミックAPIで利用可能であり、これらが無い場合ユーザーはstd::atomicの上にCASループなどを利用して実装することになりますが、それは最適化や進行保証を妨げることになります。

P3307R0 Floating-Point Maximum/Minimum Function Objects

浮動小数点数型が最大値・最小値を求めるC23準拠の関数オブジェクトを追加する提案。

C++浮動小数点数型のmax()/min()に関する問題点については以前の記事を参照

浮動小数点数値の列に対して、最大値や最小値を求める操作は非常に一般的です。それは高性能科学計算でも同様であり、そこでは並列std::reducestd::transform_reduceを使用してそれが行われ、そのような特定の演算はハードウェアによる最適化が利用可能です。しかし、C++std::(ranges)::min/maxはIEE754標準に従っていないものしかなく、ハードウェア命令が従っている結果を返す場合にそのような最適化を利用できなくなります。

#include <cassert>
#include <execution>
#include <numeric>
#include <ranges>

int main() {
  double data[] = {2, 1, 4, 3};
  double max = std::reduce(
    std::execution::par,
    data, data + 5,
    0,
    // HWアクセラレーションを利用できない
    std::ranges::max
  );
  assert(max == 4);
  return 0;
}

この提案は、そのような事態を回避するために、IEE754に準拠したC23のAPIをベースとした浮動小数点数型の最大・最小値を得る関数オブジェクトを追加する提案です。

追加しようとしているのは、次の6つです

namespace std {
  struct fmin_t;
  struct fmax_t;
  struct fminimum_t;
  struct fmaximum_t; 
  struct fminimum_num_t;
  struct fmaximum_num_t; 
}

これらの関数呼び出し演算子は対応するC23のAPIを呼び出すだけのものです。

#include <cassert>
#include <execution>
#include <functional>
#include <numeric>

int main() {
  double data[] = {2., 1, 4, 3};
  double max = std::reduce(
    std::execution::par,
    data, data + 5,
    0,
    // HWアクセラレーションが利用可能になる
    std::fmaximum_num_t{}
  );
  assert(max == 4);
  return 0;
}

P3308R0 mdarray design questions and answers

mdarrayの設計への疑問点と回答をまとめた文書。

これは、LEWGにおけるP1684R5(mdarray)のレビューにおいて指摘された疑問点などに応えるものです。

P1684については以前の記事を参照

ここで報告されている議論とその対応をざっとまとめると次のようになります

  • LEWGのレビュー時の要求
    • (多次元かもしれない)C配列からのコンストラクタの追加を検討する
      • コンストラクタと推論補助を追加する
    • in_place_tコンストラクタ(引数を内部コンテナのコンストラクタへ転送する)の追加の検討
      • コンストラクタと推論補助を追加する
    • 初期化子リストからの構築について議論する
      • 多次元ネストした初期化子リストからの構築と推論補助の追加を検討する
  • LEWGが要求した改訂
    • 機能テストマクロ追加
      • 対応予定
    • mdarrayのフォーマット方法の説明
      • mdspanのフォーマットに乗っかることで適切に対処可能
    • 無効な状態のオブジェクトの使用を回避するための事前条件
      • 対応予定
  • その他のトピック
    • mdspanを受け取るコンストラクタで多次元コピーを並列化するために、ExecutionPolicy&&を取るオーバーロードを追加する
      • 一方、in_place_tコンストラクタを追加すると、ユーザーがmdspanから効率的に構築できるコンテナを持っている場合に、そのコンテナを用いてmdspanからmdarrayの構築を行うことでもパフォーマンスの問題は解消される
    • コンテナアダプタからコンテナへの設計変更
      • 変更の理由は現状を上回らない(mdarrayはコンテナアダプタのままであるべき)

文書では各項目についてより詳細に説明されています。

P3309R0 constexpr atomic and atomic_ref

std::atomic/stomic_refconstexpr化する提案。

この提案のモチベーションはコンパイル時と実行時でより多くのコードを共通化できるようにすることです。そのために、std::atomic/stomic_refvolatileオーバーロードおよび通知・待機関数を除くほとんどのメンバ関数constexpr指定します。

とはいえ、今のところコンパイル時にはスレッドの概念が無く常にシングルスレッドであるため、コンパイル時にアトミックな動作が必要になるわけではありません。この提案では、if constevalを用いてコンパイル時には通常の非アトミックな動作をするように分岐するようにしています。

atomic_fetch_add()の実装例

template<class T>
constexpr T atomic_fetch_add(atomic<T>* target, typename atomic<T>::difference_type diff) noexcept {
    if consteval {
        const auto previous = target->value;
        target->value += diff;
        return previous;
    } else {
        __atomic_fetch_add(target->value, diff);
    }
}

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

constexpr bool process_first_unprocessed(std::atomic<size_t> & counter, std::span<cell> subject) {
    // BEFORE: 定数式だとここでコンパイルエラー
    // AFTER: 定数式でも動作する
    const size_t current = counter.fetch_add(1); 
    
    if (current >= subject.size()) {
        return false;
    }
    
    process(subject[current]);
    return true;
}

constexpr void process_all(std::span<cell> subject, unsigned thread_count = 1) {
    // BEFORE: 定数式でここに到達すると、thread_countに関わらずコンパイルエラー
    // AFTER: 定数評価コードでも、thread_count == 1の場合はエラーにならない
    std::atomic<size_t> counter{0};
    auto threads = std::vector<std::jthread>{};
    
    assert(thread_count >= 1);
    
    for (unsigned i = 1; i < thread_count; ++i) {
        threads.emplace_back([&]{
            while (process_first_unprocessed(counter, subject));
        });
    }
    
    while (process_first_unprocessed(counter, subject));
}

P3310R0 Solving partial ordering issues introduced by P0522R0

P0552R0の影響を緩和するための提案。

P0552R0はC++17に採用されたもので、テンプレートテンプレートと通常のテンプレートをテンプレートの部分特殊化によってマッチングする際の一致のルールを変更するものです。ただしこの提案は副作用が大きく、以前に有効であったコードを無効化してしまっていました。

この提案は、それに対処するために、古いコードの動作を維持するようにそのルールを修正しようとするものです。

提案では2つの個別の問題の例と、それに対する解決策が提案されています。

1つはデフォルトテンプレート引数を持つテンプレートに対する部分特殊のマッチングに関してです。

template<class T1>
struct A;

template<template<class T2> class TT1, class T3>
struct A<TT1<T3>>; // #1

template<template<class T4, class T5> class TT2, class T6, class T7>
struct A<TT2<T6, T7>> {}; // #2

template<class T8, class T9 = float>
struct B;

template struct A<B<int>>;

P0552R0以前は、#2のみが候補として考慮されていました。Bには2つのテンプレートパラメータがあり、#1には1つのテンプレートパラメータでしか特殊化されていなかったためです。

しかし、P0552R0によって#1も考慮されるようになりました。これは、A<TT1<T3>>TT1Bを問題なく当てはめることができるためです。 ただし、部分特殊化における順序付けでは、Bにアクセスせずに候補自体のみを考慮します。そのため、この例でTT1が2つのパラメータを持つテンプレートに置き換わったとしても、動作するかは明白ではありません

この提案では、#2を選定するという以前の動作を維持することを提案しています。

2つ目は、

template<template<class ...T1s> class TT1>
struct A {};

template<class T2>
struct B;

template struct A<B>; // #1

template<template<class T3> class TT2>
struct C {};

template<class ...T4s>
struct D;

template struct C<D>; // #2

P0552R0以前は#1は有効で#2は無効でしたが、P0552R0以後では#1は有効なまま#2も有効となりました。これだけだと問題が無いように見えますが、似たような場合の順序付けを見てると

template<class T1>
struct A;

template<template<class ...T2s> class TT1, class T3>
struct A<TT1<T3>>; // #1

template<template<class T4> class TT2, class T5>
struct A<TT2<T5>> {}; // #2

template<class T6> struct B;
template struct A<B<int>>;

P0552R0以前は#2が選択されていましたが、P0552R0によってこれは曖昧になります。これは破壊的変更であり望ましくなたいめ、再び#2が選択されるように調整することを提案しています。

提案にはもう少し詳しい説明が載っています。

P3311R0 An opt-in approach for integration of traditional assert facilities in C++ contracts

Cassert契約プログラミング機能に統合する提案。

現在の契約プログラミング機能では、従来のassertマクロの役割に相当するものとしてcontract_assertが用意されています。assertという名前を使用できなかったため名前が異なりますが、assertマクロの問題点を修正しています。

contract_assertに対するCassertの問題点は次のようなものです

  • NDEBUGの定義の有無でODRの結果が変わる
    • contract_assertはignoreセマンティクスの場合でもODRの結果が変わらない
  • 単なるマクロであるため、任意の場所で再定義されうる
    • contract_assertはマクロではなく文でありキーワード
  • カンマがあるとバグる場合があった
    • これは、C/C++双方でP2264R7によって解消済
    • contract_assertはマクロではなく、構文的にも式を受け取る
  • 任意の副作用をトリガーできる
    • contract_assertの場合、参照するものの暗黙const化や評価回数の不定性などで対処している

ただ、これらのことによってcontract_assertassertの間には互換性がなく、単純に移行ができない部分があります。特に、最後の副作用の実質禁止のための仕組みに関しては、contract_assertがCassertの完全な代替となり得ないと考えられてしまう部分になっています。

従来のアサーションassertもしくは似たような仕組みによるアサーション機構によって行われており、これらの既存コードをC++26移行に伴って書き直すことを期待したり要求したりするのは現実的ではありません。機械的に(単純なテキスト置換などによって)移行ができない場合、C++26移行の世界ではアサーション機構としてこの2つが並立することになり、その教育や学習のコスト上昇にもつながります。

この提案では、オプトイン方式によってCassert契約プログラミング機能に組み込むことができるようにすることで、contract_assertの変更を必要とすることなく従来のassertをほぼそのまま契約プログラミング機能に移行できるようにしようとしています。

提案ではまず、contract_assertの構文を拡張して、文脈依存キーワードtraditionalを指定できるようにします。

contract_assert traditional (...);

この場合のcontract_assertは動作が次のように変更されます

  • 暗黙のconst化の無効化
  • 評価回数は正確に一回
  • 条件式からの例外は伝播される(違反ハンドラは呼び出されない)
  • 条件式の結果がfalseとなった場合、detection_mode()predicate_falseを返し、kind()traditional_assert(新規追加)を返し、semantic()enforceを返すstd::contracts::contract_violationオブジェクトへの参照を使用して違反ハンドラを呼び出す
  • デフォルトの違反ハンドラ推奨事項を変更し、kind()traditional_assertを返す引数で呼ばれた場合に、失敗したアサーションに対してC標準で要求される動作を実装する(ことを推奨する)
    • エラー情報をエラーストリームに書き込んでからabort()を呼び出す、など

すなわち、assertマクロとほぼ同じ動作をします。

そして、assert.h/cassertヘッダをインクルードする時点でNDEBUGマクロが定義されておらず、 ASSERT_USES_CONTRACTSマクロが定義されている場合に、assertマクロの定義を次のように変更します

#define assert(...) \
  [&] { contract_assert traditional (__VA_ARGS__); }()

ただし、下位互換性の維持のためにNDEBUGASSERT_USES_CONTRACTSの両方が定義されている場合はNDEBUGが優先されます(assertマクロは従来通り引数を握りつぶす形で定義される)。

このような方法によって従来のアサーション機構が新しい契約プログラミング機能のフレームワークに統合されていれば、それを利用した移行を起点として契約プログラミング機能の利用を推進する材料になります。これにより、2つの異なるアサーション機能が並立しているという状況も解消されます。

この提案の方向性は概ねSG21に支持され、類似の提案であるP3290R1に統合されることになりました。

P3312R0 Overload Set Types

関数のオーバーロード集合型を導入する提案。

プレースホルダ型(auto)で宣言されたものに関数名を渡すと関数ポインタが推論されます。しかし、その関数がオーバーロードされている場合どの関数ポインタを取得するべきか分からないためコンパイルエラーになります。

auto call(auto sin, auto v) {
  return sin(v);
}

int main() {
  call(std::sin, 3.14f);  // ng、オーバーロードがあるため曖昧
}

この提案では、このような場合に関数ポインタではなくオーバーロード集合型(overload-set-type)を推論し、その関数名についてその時点で利用可能なすべてのオーバーロードに関する情報をラップして渡します。オーバーロード集合型のオブジェクトに対して関数呼び出しを行うと、その場所でオーバーロード解決が行われ、引数に応じた最適なオーバーロードが選択されます。

// この提案では、第一引数はオーバーロード集合型が推論される
auto call(auto sin, auto v) {
  return sin(v);  // vによってオーバーロード解決が行われる
}

int main() {
  call(std::sin, 3.14f);  // ok、std::sin(float)が呼ばれる
  call(std::sin, 3.14);   // ok、std::sin(double)が呼ばれる
}

このようなオーバーロード集合型は実行時状態を持たず、コンパイル時の型生成とオーバーロード解決にのみ依存しています。特に、関数ポインタのように実行時まで呼ばれる関数が分からないということはなく、呼び出される関数はコンパイル時に確定されています。

オーバーロード集合型が推論されるのは、プレースホルダ型で宣言された変数をオーバーロードされた関数名で初期化しようとした場合のみです。指定した関数がオーバーロードされていない場合は現在と同じく関数ポインタ型を推論します。

この型の基本的な考え方は、関数オーバーロード集合型のオブジェクトに対して関数呼び出し演算子を使用する場合に、元の関数オーバーロード集合(関数オーバーロード集合型が作成された地点)のすべての側面が維持されるというものです。すなわち、オーバーロード候補は関数呼び出しが実際に行われる場所ではなく、オーバーロード集合型が推論された場所で有効なものが候補となります。

std::sinfloat/doubleの2種類のみのオーバーロードを持つ場合、オーバーロード集合型をあえて明示すると次のような型とほぼ同じ振る舞いをします。

struct __std_sin_overload_set_type_1 {
  decltype(auto) operator()(auto&&... as) {
    return std::sin(std::forward<decltype(as)>(as)...);
  }

  // 関数ポインタ(関数参照)型に変換可能
  operator float (&)(float) const { return std::sin; }
  operator double (&)(double) const { return std::sin; }
};

これは例示のためのものであって、実際にはこのような型が生成されるとは限りません。特に、関数呼び出し演算子などは明示的に実装されるのではなく省略し、元の関数で正しいオーバーロードが直接的にわたるような形になる必要があります。また、オーバーロード解決に必要な情報はオーバーロード集合型が推論された地点のものが使用されます。

オーバーロード集合型の扱いはラムダ式クロージャ型に近いもので、名前はないもののdecltype()を適用でき、各種コンストラクタ及び代入演算子も定義されます。ただし、異なるオーバーロード集合型の間での代入はできません。

提案するオーバーロード集合型について可能な操作の例

void compose(auto F, auto G, auto value) { return F(G(value)); }

double one = compose(std::tan, std::atan, 1);

auto s = std::sin;  // 変数として取得 

using SinOverloads = decltype(s); // 型名を取得

callWihtFloat(SinOverloads());  // デフォルト構築

void cc(float (*f)(float));

cc(s);  // 関数ポインタへの変換

auto sptr = declcall(s(2.0f));  // P2825の機能

cc(sptr);

auto sptr2 = static_cast<float(*)(float)>(s);

cc(sptr2);

double x = s(3.14);

// decay-copy(オーバーロード集合型に対して)
auto(std::sin);
auto{std::sin};

decltype(std::sin); // オーバーロード集合型が推論される

template<auto F>
void myFun()
  requires requires { F(1.2); }
{
  F(2.3);
}

myFun<std::sin>();  // NTTPに渡す

関数オーバーロード集合型のオブジェクトに対して関数呼び出し演算子を使用する場合に、元の関数オーバーロード集合(関数オーバーロード集合型が作成された地点)のすべての側面が維持される、という基本的な考え方に基づいてその他の動作も設計されています。例えば、デフォルト引数はオーバーロード解決時に考慮されるものの関数ポインタへの変換時は考慮されず、関数テンプレートがオーバーロード候補に含まれている場合でも同様に動作し(インスタンス化地点はオーバーロード解決が行われた場所)、noexcept/constexprなども選択された関数のものが使用されます。

ADLに関してもこの動作を踏襲し、ADLの候補集合もオーバーロード解決が行われる場所ではなくオーバーロード集合型が推論された場所のものになります。すなわち、コンパイラオーバーロード集合型が推論された地点のシンボルテーブルを保存する必要がありますが、これはジェネリックラムダの場合と同じルールです。

ここまではフリー関数のみを対象として説明してきましたが、この提案のオーバーロード集合型はメンバ関数や関数呼び出し演算子などに対しても動作します。ただし、メンバ関数の場合は&を付けて渡す必要があります(クラス名Cメンバ関数memfに対して&C::memf)。また、このメンバ関数にはコンストラクタやデストラクタも含まれており、それぞれクラス名CNameに対して&CName::CName&CName::~CNameのようになります。メンバ関数の場合は、メンバポインタ呼び出しに関してフリー関数の関数呼び出し演算子と同様の扱いとなります(メンバ関数オーバーロードされていない場合との一貫性のため)。

演算子についてはoperator@の形式でオーバーロード可能なものについて、operator@の形で渡すとオーバーロード集合型が推論されます。呼び出しの際はoperator@(args...)の形式で呼ばれた時のルールに従ってオーバーロード解決されます。すなわち、メンバ関数メンバ関数演算子オーバーロードを1つのオーバーロード集合型に含めることはできません。

提案より、std::transformで使用する例

std::vector<float> in = getInputValues();
std::vector<float> out;

// std::sinを各要素に適用する
std::transform(in.begin(), in.end(), std::back_inserter(out), std::sin);

// Rangeアダプタでも動作
auto out_r = std::views::all(in) |
             std::views::transform(std::sin) |
             std::ranges::to<std::vector>();

// 演算子でも動作(フリー関数の場合のみ
auto neg_r = std::views::all(out_r) |
             std::views::transform(operator-) |
             std::ranges::to<std::vector>();

P3313R0 Impacts of noexept on ARM table based exception metadata

noexeptが生成コードにどのような影響を与えるかについての調査報告の文書。

この文書の目的は次のような質問へ回答することです

  • 関数をnoexcept指定すると、そのメタデータはどのように変更されるか?
  • 関数がnoexcept関数を呼び出すと、関数のメタデータにどのような変更があるか?
  • tryブロックがnoexcept関数のみを呼び出すと、どのような影響が生じるか?
  • noexcept関数との相互作用は、非トリビアルなデストラクタを持つオブジェクトを管理する関数にどのように影響するか?

この文書では、GCCが使用しているARM環境におけるItanium ABIのテーブルベース例外の実装によって様々な実験を行っていますが、ここで得られる見識は他のテーブルベース例外の実装においても一貫している、としています。

文書では、ARM EHABIの実装やGCC C++ LSDAについて解説した後、noexcept及び非noexcept関数の様々な呼び出しパターンを実験しています。この実験の目的は次の事柄についての見識を提供することです

  • 関数の例外ランク
  • LSDAサイズとそのセクションのサイズ

結果と分析を通じてここでの実験の結論は、noexceptは強力な例外保証が必要な場合に役立つものの、コード生成に関しては必ずしも利点があるとは限らないというものです。一般的に言われているnoexceptの利点はエッジケースでのみ発生するようです。

例外機構の生成コード改善という観点からはむしろツールチェーンの改善に焦点を当てるべきだと述べており、推奨事項として次の事を挙げています

  • データ構造の選択の改善
    • リーフ関数の場合: インデックスエントリを省略する
    • noexcept関数のみを呼び出す場合: インデックスエントリを省略する
    • noexcept関数でtryブロックを持たない場合: インラインnoexceptフラグを使用する
    • noexcept関数で非トリビアルなデストラクタを持つオブジェクトが存在する場合: インラインnoexceptフラグを使用する
  • 同一の例外エントリを持つ関数のグループ化
    • リンカは同一の例外エントリを持つ関数をグループ化することができ、これを利用して同一の例外エントリを1つのエントリにマージしてテーブルサイズを削減することができる
  • 関数におけるnoexceptの推論
    • 他の関数を呼び出しているものの、その呼び出しグラフの全体にわたって全ての関数が例外を送出しない場合、その関数はnoexceptとして動作する。このような関数を手動でnoexcept指定する代わりに、リンカがその関数が例外を送出するかどうかを判定することができる
    • リンカが完全なアセンブリ情報を持つすべての関数を評価して、どの関数が例外を送出するかを判定し、例外を送出することが無い関数を自動でnoexpcet指定することができる

この文書は何かを提案するものではありません。おそらく、契約プログラミング機能の議論の過程におけるnoexceptの効果についての議論やLEWGにおけるnoexcept付加ポリシーに関連したレポートだと思われます。

P3316R0 A more predictable unchecked semantic

条件をチェックしない新しい契約セマンティクスであるPromiseセマンティクスの提案。

現在の契約プログラミングの仕様には、契約注釈が実行時にどう振舞うのかについてのセマンティクスが4種類あります。これにより、静的ライブラリを開発して出荷している場合、契約プログラミング機能に対応するには現在の4倍の数のバリアントを出荷する必要がある事を意味します。

これにはコストがかかるため、単一のバージョンを出荷してリンカがリンク時最適化のタイミングで選択されているセマンティクスに応じて使用されないコードを削除することで対応できないか?と考えます。使用されないコードとは、条件式チェック、違反ハンドラ呼び出し、プログラムの中断の3つです。何も取り除かず全てを残す場合はEnforceセマンティクスになり、ハンドラ呼び出しだけを削除するとquick_enforceになり、プログラム中断だけを削除するとObserveセマンティクスになり、全てを削除するとIgnoreセマンティクスになります。

このような最適化は、チェックが失敗した場合にプログラムが終了することをオプティマイザが認識し、それに基づいて行われるものになります。しかしこれは、ライブラリが最初からIgnoreセマンティクスでコンパイルされていた場合と同じではありません。なぜなら、チェックして契約が破られていた場合のブランチが終了しない場合(Enforce/quick_enforce以外の場合)、継続パスにUBが含まれていると、現在の最適化がそうしているようにUBは起こらないものとして最適化が行われ、結果契約チェックと破られていた場合のハンドラ呼び出しを行うコードが最適化される可能性があるからです。

これは特に、Observeセマンティクスが選択された場合に起こり得ます。例えば次のような簡単なcontract_assertを含むコードは

bool f(int* p, int* q) {
  if (q != nullptr) {
    puts("got null");
  }

  contract_assert(p != nullptr);
  
  return *p == *q;
}

Enforceセマンティクスの場合最も最適化されたとしても次も様なコードになります

bool f(int* p, int* q) {

  // qのチェックは起こらないものとして削除される
  // qのチェックで終了はしないため

  if (not (p != nullptr)) {
    invoke_handler();
    die();  // プログラム終了点がある事でpのチェックは残る
  }
  
  return *p == *q;
}

Observeセマンティクスの場合は最悪次のようになります

bool f(int* p, int* q) {

  // pのチェックパスのdie()が無くなることでpがnullptrでも*pまで行きつき
  // UBは起こらないのでPのチェックはハンドラ呼び出し毎消える
  
  return *p == *q;
}

これはIgnoreセマンティクスの結果と同じです。

同様の事は事前・事後条件チェックにおいても起こり得るほか、純粋にIgnoreセマンティクスによって最適化が行われた場合でも(契約チェックの後のパスで同じ条件で分岐してる場合などに)同じことが起こり得ます。

すなわち、IgnoreセマンティクスはUBに対して脆弱であり本質的に危険なもので、契約注釈を無視できるのは防御的なスタイル(二重に同じ条件をチェックして例外によって脱出するなど)が講じられている場合のみとなります。例えば、リリースプログラムをEnforceセマンティクスでテストしてからIgnoreセマンティクスに切り替えて再度実行する場合、最適化傾向が異なるため信頼できないものとなります。

ここで提案されているのは次の3つです

  1. 推奨される契約チェックが行われないモードとして、IgnoreセマンティクスをPromiseセマンティクスで置き換える
  2. 推奨されるObserveセマンティクスとして、Promiseセマンティクスに基づくObserveセマンティクスを使用する
  3. [[establish( expression )]]を追加する
    • この属性の出現以降に式がtrueになることを表す(前になっている必要は無い

提案後のObserveセマンティクスの先程のコードのコンパイル結果は次のようになります

bool f(int* p, int* q) {


  if (not (p != nullptr)) {
    invoke_handler();
    [[establish(p != nullptr)]];
  }
  
  return *p == *q;
}

[[establish( expression )]]は一種の最適化バリアであり、その出現以降でその条件が満たされているとコンパイラが仮定するようになることで、チェックし失敗後に終了しない場合でも、UBが起こらないと仮定した最適化によって契約チェックブランチが丸ごと削除されるのを防止します。

Primiseセマンティクスでは契約チェックが完全に無視される場合でもこの[[establish( expression )]]属性が残されることによって、以降のパスでUBは起こらないものとした最適化を抑止することができ、無視セマンティクスの信頼性が向上します。また、冒頭で述べていたようなリンカによるセマンティクスの選択も適切に動作するようになります。

この提案の内容はSG21であまり賛同を得られなかったようで、リジェクトされています。

P3317R0 Compile time resolved contracts

契約をコンパイル時にチェックしておくことができるように契約プログラミング機能を設計すべきという提案。

アプリケーションによっては不意の終了が致命的である場合があります。しかし、実行時に不正なデータを素通しするのもまた危険であるため、契約チェックはenforceセマンティクスで残されるかもしれません。また、実行時のチェックを行いたいがそのための余分なリソース(CPU/メモリ)が確保できないというケースもあります。

このような環境においては、ほとんどの契約注釈をコンパイル時にチェックして実行時のコストと終了の危険性を削減するようにプログラムを作成するのは労力に見合う価値がある可能性があります。これらほどに要求が厳しくないアプリケーションにおいても、多くの契約注釈がコンパイラによってコンパイル時にチェックされている(契約が破られない)ことが分かれば、大きな安心感を得ることができます。

また、契約プログラミング機能の将来の拡張の一つとして、契約注釈をコードの仮定として扱ってコンパイラが最適化に利用できるようにする方向性が知られています。このアプローチで問題となるのは、契約が破られている場合に未定義動作に陥ることでコードがむしろ危険なものになってしまう事です。この場合に、契約注釈の多くの部分がコンパイル時に解決されていれば、実行時の未定義動作のリスクを軽減して最適化の恩恵だけを受け取ることができます。

これは契約の設計を拡張する時に考慮すべき重要な機能であり、これらのことを将来的に可能なようにしておくためには静的解析が可能なように契約機能を設計しておく必要があります。これはその方針を提案するものです。

ここでは次の3つの事を提案(SG21における投票を推奨)しています

  • 契約注釈のコンパイル時診断を標準化する
  • コンパイル時の診断は次の4つ
    • None : 何もしない
    • Known violations
    • Unhandled conditions
    • Runtime conditions
  • 全ての契約セマンティクスはコンパイル時の解決を妨げないように設計されるべき

提案されている4つの診断モードは、コンパイル時にどの程度解決するかを指定するものです。なお、Noneは現在の動作(すべて実行時解決)です。

まずKnown violationsは呼び出された場合に必ず失敗する契約注釈についてコンパイル時に診断するモードです。

void f0()
  post(false) // 失敗を診断する
{
}

void f1()
  pre(false) // 呼び出すまでは診断されない
{
}

void f2()
{
  f1(); // 失敗を診断する
}

void f3()
{
  contract_assert(false) // 失敗を診断する
}

これらはあまりにも自明な例ですが、コンパイル時定数やコンパイル時に分析可能な条件式などによってこれらと同等になる場合にコンパイル時に契約注釈の有効性を検出できます。

Unhandled conditionsは、コンパイル時に契約を失敗させる入力が存在することを診断するモードです。

int f0(int x)
  post(r: r > 5) // 診断: 無効な入力が存在する
{
  return x;
}

Runtime conditionsは、コンパイル時にはその契約が破られる可能性があるかどうか分からないものについても診断を提供するモードです。

void f0()
  post(secret_check()) // 診断: 契約条件の有効性は不明
{
}

これらの診断モードのうち、最初の2つ(Known violationsUnhandled conditions)は間違ったコードを明らかにするものなのでデフォルトで有効にし、最後のものはオプションのモードとしておくことを推奨しています。ただし、これらの問題を検出する能力は必ずしもコンパイラ間で同じではないため診断無しでコンパイルすることもできるようにしておくことを提案しています。

ここでの診断とは、警告とエラーかはっきりしていませんが、おそらく最初の2つの診断はエラーを意図しており、最後のものの診断は警告を意図していると思われます。

P3318R0 Throwing violation handlers, from an application programming perspective

標準ライブラリのnoexceptポリシーとして、Lakos Ruleを維持または復活させるべきとする文書。

LEWGにおけるポリシーの策定が行われており、noexceptに関するポリシーも提案されています(P3085やP3155)。この文書は、その議論に関する第三者意見書として提出されたもので、Lakos Ruleに基づいたポリシーとすることを推奨するものです。

契約違反が起きている場合でも必ずしもプログラムが致命的で続行不可能な状態にあるわけではありません。むしろ、契約違反をハンドリングした状態というのはUBに突入しうる状況の直前でそれを防止したという事であり、その原因はプログラマの些細なミス(ポインタ引数に誤ってnullptrを渡す、タイプミス、論理条件の反転、etc...)である可能性があります。その場合で、その場所がプログラム全体から見ると重要度の低い箇所である場合、そのバグからプログラムを回復させる選択肢を取ることができ、メインのプログラムを続行することができます。これはあるドメインのビジネスルールでは正しい振る舞いであり、即終了することは望ましい振る舞いではありません。

標準ライブラリの関数がLakos Ruleに従わないnoexceptポリシー(狭い契約を持ち"Throws: nothing"とある関数でもnoexcept指定される)を取ると、契約プログラミング機能がもたらされ標準ライブラリにもそれが適用されている世界において、違反ハンドラにより事前条件違反を検出して(enforceセマンティクスにを選択しているとする)、それが(上記のように)回復可能なバグである場合は例外送出することで復帰する、という手段を取ることができなくなります。

例えば次のようなコードにおいて

void whatever_func() {
  try {
    // 事前条件を持つ関数
    some_func_with_preconditions(oink, boink);
  } catch (const MyOutOfRangeContractException& e) {
    // 復帰処理
    log(e);
      disable_offending_subsystem();
      resume_normal_operation();
  }
}

some_func_with_preconditions()が標準ライブラリの関数であり、契約プログラミング機能が採用された世界でライブラリ実装によって事前条件がコードによって記述されるようになっているとします。この場合にLakos Ruleに従わないnoexceptポリシーに基づく場合、事前条件が破られ例外が送出されると(違反ハンドラから例外によって復帰しようとする場合も含めて)プログラムが終了させられるため、ユーザーサイドではそこから復帰するあらゆる手段が封じられます。

Lakos Ruleに従わないnoexceptポリシーを採用すると、このようなユースケースへの扉が閉ざされます。そして、一度閉ざされた扉は後方互換性によって二度とは開くことができなくなり、違反ハンドラからの例外送出によって契約違反後の状態回復という手段は使用できなくなります。これは、そうしたいユーザーコミュニティがそうできないという事であり、WG21の委員会の人の大多数がそうする必要が無い(からそうできないようにしたい)という事よりもずっと悪いことです。

そのようなコミュニティの意見はWG21で十分に代表されていない可能性があり、そのようなコミュニティを軽視してWG21で代表されている意見だけに基づいてそのようなコミュニティのユースケースを禁止してしまえば、そのようなコミュニティはC++から足を遠ざけることを選択してしまうでしょう。

これらの理由から、この文書ではLEWGのnoexceptポリシーとしてLakos Ruleを維持することを推奨しています。

P3319R0 Add an iota object for simd (and more)

std::simdオブジェクト(SIMDレジスタ)を連番の値で初期化するAPIの提案。

std::simdのジェネレータコンストラクタ(0~SIMD幅の整数定数を受けて値を生成する呼び出し可能なものを受け取って初期化するコンストラクタ)の9割の用途として、SIMDレジスタを連番(0, 1, 2, 3...およびその定数倍と手数オフセットを足したもの)で初期化するのにに使用されることが想定されています。

// SIMDレジスタを0始まりの連番で初期化
std::simd<int> a ([](int i) { return i; }); 

// SIMDレジスタを2, 5, 8, 11...の連番で初期化
std::simd<int> b ([](int i) { return 2 + 3 * i; });

std::simd<int>とすると、その環境のint型のSIMD演算が最も効率的となるSIMD幅が自動で選択されます。このラムダ式を渡すコンストラクタ(ジェネレータコンストラクタ)は、そのラムダ式に対して0から始まる定数値を渡して呼び出して得られた値でそのSIMDレジスタを初期化するものです。

このように強く特定の用途で使用されることが分かっている場合、汎化するよりも特化したAPIにした方が分かりやすくなります。

// 上の例と同じ初期化を行う
auto a = std::iota_v<std::simd<int>>;
auto b = 2 + 3 * std::iota_v<std::simd<int>>

この提案は、このような用途のstd::simd初期化のためにこのstd::iota_vを提案するものです。

名前から分かるように、std::iota_vstd::simdに対してstd::iota()アルゴリズム)がコンテナに対して行うのと同じ初期化を行います。ただし、std::iota_vは初期化対象のstd::simd型をテンプレートパラメータで受け取る変数テンプレートであり、その初期化は必ず定数で初期化され、かつSIMDレジスタ初期化にとってより効率的なものとなります。

// 提案されている宣言の例

template<class T>
inline constexpr T iota_v;

// 算術型用の特殊化
template<class T>
  requires (std::is_arithmetic_v<T>)
inline constexpr T iota_v<T> = T();

// std::simd用の特殊化
template<detail::simd_type T>
inline constexpr T iota_v <T> = T([](auto i) { return static_cast<typename T::value_type>(i); });

さらにこの提案では、std::iota_v<T>で使用可能なTとして算術型の他

  • 静的な要素素を持つ
  • value_typeメンバを持つ
  • 静的な要素数Nに対して、N個のvalue_type型オブジェクトのリストによる初期化が可能
  • value_type() + 1は定数式であり、value_typeに変換可能

を満たす任意の型を使用可能にできるようにすることを提案しています。

// どちらも可能
auto x = std::iota_v<float[5]>;
auto y = std::iota_v<std::array<my_fixed_point, 8>>;

この提案の重要な点は、初期化コードが実際のSIMD幅に合わせて自動でスケーリングすることによって、可変長のリスト初期化をユーザーが手書きする必要を無くすことです。

P3320R0 EWG slides for P3144 "Delete if Incomplete"

P3144の紹介スライド。

P3144については以前の記事を参照

P3144の解決しようとしている問題(不完全型のポインタに対するdelete演算子の適用)について紹介し、その解決方法についての分析と比較を行っています。

P3144R0では不完全型のポインタに対するdelete演算子の使用を非推奨としたうえで、それが行われた場合の動作をEB(Erroneous Behavior)としてデストラクタ呼び出しなしでオブジェクトの生存期間を終了させる、というソリューションを提案していましたが、このスライドによる分析では全てのケースにおいてill-formedとするのがAPI破壊が起きるものの一番有効な解決策であることが示されています。

P4000R0 To TS or not to TS: that is the question

契約プログラミング機能がTSを目指す場合に考慮するべき事柄についてまとめたスライド。

内容としては、今月分にあるP3269R0を紹介するもののようです。

おわり

この記事のMarkdownソース

次月分(2024/07)の記事執筆のお手伝いを募集しています: https://github.com/onihusube/blog/issues/39