[C++]名前を必要としない変数のための変数名

C++26より、使用しない値に対する共通した変数名として_(U+005F、アンダースコア/アンダーバー)を言語サポート付きで使用できるようになります。

[[nodiscard]]
auto f() -> int;

auto g() -> std::tuple<int, double, std::string>;

int main() {
  auto _ = f(); // ok、警告なし
  auto [n, _, str] = g();   // ok

  std::cout << _;   // ng
}

概要

ローカル変数でその変数名が_であるものは、暗黙的に[[maybe_unused]]が指定されたように振る舞います。

[[nodiscard]]
auto f() -> int;

int main() {
  // この宣言は
  auto _ = f();
  
  // このように宣言されているのと同等
  [[maybe_unused]]
  auto _ = f();
}

これによって、この変数_は以降使用されていなくてもコンパイラは警告しません。また、[[nodiscard]]指定された関数では、戻り値を捨てている警告も抑制されます。

変数名_はまた、ローカルの構造化束縛の変数名としても使用でき、同じことが適用されます。

auto g() -> std::tuple<int, double, std::string>;

int main() {
  auto [_, d, str] = g(); // ok
  auto [n, _, _] = g();   // ok
  auto [_, _, _] = g();   // ok
}

構造化束縛の例からもわかるかもしれませんが、変数名_は同じスコープで何度でも使用することができます。

int main() {
  auto _ = 0;   // ok
  auto _ = f(); // ok
  auto [_, _, str] = g(); // ok
  double _ = 1.0; // ok
}

そして、_が2回以上宣言されている場合、そのコンテキストにおける_の利用はコンパイルエラーとなります。

void h(auto);

int main() {
  auto _ = 0; // ok

  std::cout << _ << '\n'; // ok、この時点では使用可能

  auto _ = 1.0; // ok

  std::cout << _ << '\n'; // ng
  _ = f();    // ng
  int n = _;  // ng
  h(_);       // ng
}

この場合、_は変数名として宣言することにのみ使用できます。

このように、C++26における変数名_は初期化後使用しないために名前を必要としない変数に対する共通の名前として使用できるようになります。

そして、この機能はまた、将来のパターンマッチング構文で_を使用するための前準備でもあります。

name-independent declaration

ここでは細かい規定の話を掘り下げます。興味がなければ読み飛ばしても大丈夫です。

自動記憶域期間(automatic storage duration)を持つ変数の宣言で、その名前が_であるものは 名前に依存しない宣言(name-independent declaration として扱われます。また同様に、その名前が_である次の宣言も名前に依存しない宣言となります

  • 名前空間スコープ以外の構造化束縛
  • 初期化キャプチャ
  • staticメンバ変数

名前に依存しない宣言に対する実装への推奨事項として、実装は名前に依存しない宣言(の変数名)が使用されていること、あるいは使用されていないことに関する警告を発しない、ことが推奨されています。これは強制ではなく推奨事項なので、もしかしたらこれに従わずに_変数の未使用について警告するコンパイラが存在する可能性はあります。この記事執筆時点でこの機能を実装しているのはClangのみですが、Clangはこの推奨にしたがっています。

そして、名前に依存しない宣言は後の宣言で同じ名前によって宣言されている時でも、名前が衝突しているとはみなされません。現在、名前に依存しない宣言で許可されている名前は_のみなので、_を使用した名前に依存しない宣言は何度でも再宣言可能となります。

[[nodiscard]]
constexpr auto f() -> int;

// 名前空間スコープでは、名前に依存しない宣言は考慮されない
auto _ = f(); // ok、名前に依存しない宣言ではない
auto _ = f(); // ng、同名変数の再宣言

// 上記宣言がなかったとしても
auto [_, _] = std::make_pair(10, 1.0);  // ng、名前が重複している


// メンバ変数
struct S {
  int _ = 0;    // ok、名前に依存しない宣言
  int _ = f();  // ok、名前に依存しない宣言

  auto _ = f(); // ng、メンバ変数宣言ではautoを使用不可

  static int _ = 0; // ng、同名変数の再宣言
};

int main() {
  // グローバルの_を隠蔽する
  int _ = 0;    // ok、名前に依存しない宣言
  auto _ = f(); // ok、名前に依存しない宣言

  // 構造化束縛
  auto [_, _] = std::make_pair(10, 1.0);  // ok、名前に依存しない宣言

  // ラムダ式の初期化キャプチャ
  auto _ = [_ = 0, _ = f()](auto) {}; // ok、キャプチャはどちらも名前に依存しない宣言

  static int _ = 0; // ng、同名変数の再宣言
}

ただし、名前に依存しない宣言の衝突が許容されるのは再宣言のみであり、その名前(_)が2つ以上の異なるエンティティ(変数宣言)に対応する(つまり_が2回以上宣言されている)場合、_の名前解決は失敗しコンパイルエラーとなります。

[[nodiscard]]
constexpr auto f() -> int;

int main() {
  int _;  // ok、名前に依存しない宣言

  // _の名前解決先は1つに定まる
  _ = 0;      // ok
  int n = _;  // ok

  auto _ = f(); // ok、名前に依存しない宣言
  
  // 名前候補に2つの宣言が該当し、名前解決に失敗する
  ++_;  // ng
}

なお、名前に依存しない宣言は宣言のコンテキストとその名前が_であるかによって決まるため、型の指定は自由であり必ずしもautoである必要はありません。ただし、クラススコープではauto変数宣言は使用できません。

上記の4種類に該当しない宣言は_をその名前に使用していても名前に依存しない宣言ではないため、これらの特別扱いはされません。それは上記例のように名前空間スコープの変数やstatic変数などが該当しますが、それ以外のもので名前空間スコープに無いながらも該当しないものとして、関数引数とNTTPがあります。

void f(int _,     // ok、_という名前の仮引数宣言(名前に依存しない宣言ではない)
       double _   // ng、仮引数名が衝突している
      );

template<auto _,  // ok、_という名前のNTTPの宣言(名前に依存しない宣言ではない)
         int _    // ng、NTTP名が衝突している
        >
struct S{};

これらの名前については、それを使用しない場合はその意図として省略する(無名にする)ことができます。関数引数の場合はマクロの使用や関数テンプレート内の分岐などによって使用するかどうかが変化する場合がありますがその場合でもそのために_は使えないため、仮引数宣言に[[maybe_unused]]を指定することで警告を抑えるしかありません。

名前空間スコープの_名はそれが変数名であっても名前に依存しない宣言ではないため、その名前をusing宣言によってローカルスコープに導入する場合は、そのコンテキストで名前に依存しない宣言が存在していてはなりません。逆に、そのコンテキストでグローバルの_usingされている場合でも、名前に依存しない宣言を使用することができます。

int _;  // ok、グローバル変数

void f() {
  int _;  // ok、名前に依存しない宣言(B)
  _ = 0;  // ok

  using ::_; // ng、このusing宣言はBより後に来られない
}

void g() {
  using ::_; // ok、名前に依存しない宣言よりも前にあれば良い
  _ = 0;  // ok、::_を更新

  int _ = 10; // ok、名前に依存しない宣言

  _ = -1; // ng
}

破棄のタイミング

C#やRustなどの_変数名と異なる点として、C++26の_変数名はその値の破棄(デストラクタ呼び出し)を意味していません。あくまで、その変数(オブジェクト)の利用に興味がないことを意味しています。

したがって、_変数の破棄のタイミングは他の変数と同じになり、そのスコープの終わりで宣言と逆順に破棄されます。

struct check_dtor {
  int n;

  check_dtor(int a) : n(a) {}

  ~check_dtor() {
    std::println("Destructor called {:d}.", n);
  }
};

int main() {
  auto _ = check_dtor{1};
  auto _ = check_dtor{2};

  {
    auto _ = check_dtor{3};
  }

  auto _ = check_dtor{4};
}
Destructor called 3.
Destructor called 4.
Destructor called 2.
Destructor called 1.

デストラクタがトリビアルな型の場合は最適化によってスタック上から消し去られる可能性はありますが、C++の意味論としては_変数に束縛されたオブジェクトが即座に破棄されることはありません。

これはまた、[[nodiscard]]な戻り値を破棄するテクニックとの違いでもあります。

[[nodiscard]]
auto f() -> int;

int main() {
  // いずれも警告されない
  auto _ = f();       // 戻り値は破棄されていない
  (void)f();          // 戻り値はこの行で破棄される
  std::ignore = f();  // 戻り値はこの行で破棄される
}

このようになっているのは、RAII以外の役割を持たない型のオブジェクトに対する変数名として_を使用できるようにすることを意図しているためです。

std::mutex mtx{};

auto f() {
  // lock_guardのオブジェクトはRAIIのためだけに必要
  std::lock_guard _{mtx};

  ...
}

auto g() {
  using namespace std::experimental;

  // scope_exitのオブジェクトもRAIIのためだけに必要
  scope_exit _{[]() { ... }};

  ...
}

std::lock_guard等によるstd::mutexのロックが分かりやすいと思いますが、これらのオブジェクトはRAIIのためだけに宣言が必要であり、初期化後にアクセスする必要が全くありません。従来はこのような変数に対しても名前を付ける必要があり、そのようなものが同じスコープに複数ある場合は特に命名に少し面倒さがありました。そこに_を使用することで、このようなRAIIオブジェクトがいくつあったとしても1つの共通の意味を持つ名前を使用できるようになります。

このようなRAIIオブジェクトの変数名のために_を使用するようにする場合、_の初期化の行でそのオブジェクトが即破棄されると意図通りになりません。そのため、_変数名で初期化されているときでも、そのオブジェクト実体の破棄順序は通常と同じになっています。

後方互換について

C++23以前から_そのものはソース文字集合の要素の1つであり、任意のC++エンティティの名前(変数名や関数名など)として使用することができました。ただし、_自身も含めた_から始まるグローバルスコープの任意の名前は標準によって予約されており、その使用は未定義動作となります(多くのコンパイラは別にそれについて警告を発しないようですが)。したがって、_プログラマが自由に使用できていたのはローカルスコープのみとなります。

そのため、_変数名が一度しか宣言されていない場合は従来意図して_を使用していたコードと区別ができません。一方で、変数名として使用した時にはあるスコープに一度しかその名前は宣言できません。そのため、従来のコードの動作を変えないために_が一度だけ宣言されている場合は通常の変数名とほぼ変わりなく扱われており、複数回宣言された場合にのみその使用がコンパイルエラーとなるようになっています。

// C++23以前のコードとする

double _ = 1.0; // UB、予約語

int main() {
  int _ = 0;  // ok

  // 再代入や値の読み取りもできる
  _ = 10;     // ok
  int n = _;  // ok

  // 他の変数同様に再宣言できない
  int _ = 20; // ng、C++26からok
}

規定における名前に依存しない宣言への推奨事項である「実装は名前に依存しない宣言(の変数名)が使用されていること、あるいは使用されていないことに関する警告を発しない」というのの使用されていることについての警告も発しないという指定は、_変数名が一度しか宣言されていない場合の使用を念頭に置いたものです。

それでも一応、この機能の導入に当たって、_という名前が実際に使用されているのかどうかが調査されたところ、2つの例が見つかりました。まず1つはGoogle Mockというライブラリで、グローバル変数名として使用されていました。

// Google Mockにおける_使用例
namespace testing {
  
  const internal::AnythingMatcher _ = {};

}

おそらくこのような使用例との衝突を回避するために、C++26の_変数名の特別扱いはローカルスコープ(関数スコープ)とクラススコープのみに制限されており、_変数名は必ず何かしらの変数の宣言として導入されます。そのため、他の名前に使用されていてもその名前を隠蔽するだけで、この利用例に対して過度に影響を与えることはないはずです。

2つ目は、Gettextというライブラリを使用するプロジェクトにおいてのもので、そこでは_はGettextのラッパの関数マクロとしてよく使用されています。

#define _(msgid) gettext (msgid)

ただしこれは、Gettextライブラリヘッダ自体が提供するものではなく、あくまでその利用側での慣習として良く定義されているものです。また、この場合は関数マクロであるので、この機能の_とはあまり衝突しません。

constexpr const char* gettext(int) { return nullptr;}
#define _(msgid) gettext (msgid)

int main() {
  constexpr auto _ = _(42); // ok
  auto _ = 42;  // ok
  static_assert(_ == nullptr);  // ng
}

この2つの名前空間スコープにおける例はいずれも厳密に言えば未定義動作にあたっていますが。C++26のこの機能はどちらに対する影響も抑えたものになっています。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で40本あります。

もくじ

P0843R9 inplace_vector

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

以前の記事を参照

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

  • sz < capacityの事前条件は、unchecked_系関数を除いてstd::bad_alloc例外をスローするようにした
  • try_系の挿入関数は、コンテナ容量が満杯の場合に入力引数の右辺値を消費しないようにした
  • Tのコピー/ムーブコンストラクタがトリビアルなら、コンテナの対応するコンストラクタもトリビアルになるようにした
  • メンバswap()は、サイズが0か要素型のムーブコンストラクタがnoexceptならnoexceptになるようにした
  • リサイズの計算量を線形にした
  • 文言の範囲外の計算(以下と未満)を修正
  • emplace系関数に対する制約を修正
  • デフォルト挿入可能性を要求するサイズのみを引数にとる単項コンストラクタを修正
  • <inplace_vector>で山かっこが抜けていたのを修正し、アルファベット順にソート
  • シーケンスコンテナの用件でカバーされている事前条件の重複を削除
  • メンバswap()と特殊化されたアルゴリズムの不要な指定を削除
  • 幾つかの記述スタイルの修正

などです。

この提案はLEWGのレビューを通過し、LWGへ転送されています。

P1068R9 Vector API for random number generation

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

以前の記事を参照

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

  • ベクトル化で安全ではない要素アクセス操作の使用に関する注釈を追記
  • 機能テストマクロ
  • タイポの修正

などです。

この提案はLEWGのレビューを通過し、LWGへ転送されています。

P1255R10 A view of 0 or 1 elements: views::maybe

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

以前の記事を参照

このリビジョンでの変更は、views::maybe/views::nullableに分割した後のborrowed_range性を修正、安全上の懸念を明確化したことなどです。

P2264R5 Make assert() macro user friendly for C and C++

assertマクロをC++の構文に馴染むように置き換える提案。

このリビジョンでの変更は、渡された式を暗黙的にstatic_cast<bool>することについての議論を追加したことです。

P2542R4 views::concat

P2542R5 views::concat

同じ要素型を持つ異なる型の範囲を連結するRangeファクトリ、views::concatの提案。

R4での変更はconcat_expertを追加したことです。

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

  • concat_expertの削除(R3へ戻す)
  • static_cast<difference_type>を適切に使用
  • cartesian_product_viewを再利用して、concat-is-bidirectionalを定義

などです。

P2686R2 constexpr structured bindings and references to constexpr variables

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

以前の記事を参照

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

  • 記号的なアドレス指定(Symbolic addressing)のための文言を追加
  • constinit構造化束縛を許可

などです。

P2748R2 Disallow Binding a Returned Glvalue to a Temporary

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

以前の記事を参照

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

  • ライブラリの文言を削除し、コア言語の文言を追加
  • 実装経験について追記

などです。

筆者の方は、この提案の変更をパッチしたclang16.0.6をビルドし、clangそのものをビルドすることに成功したとのことです。その際、13件のテストが失敗しましたが、この変更の意図に反したものではなかったようです。その後、BloombergのBDEやchromium等をビルドすることもできたとのことです。この結果は、この提案の変更後に既存の出荷済みのコードにおいてエラーが発生する可能性が低いことを示しています。

P2755R0 A Bold Plan for a Complete Contracts Facility

C++契約プログラミング機能について、将来的な完成形のための計画を練る提案。

現在、C++26に向けて契約プログラミング機能を導入すべく作業が続けられていますが、それは本来の契約プログラミング機能の合意がとれたサブセットです。本来想定される契約プログラミング機能はより広いものですが、そこにはまだ合意がとれていないものや議論が紛糾しているもの、実装経験が乏しいものなどが含まれており、それらを全て導入しようとすればさらに長い時間が必要となります。

この提案は、そのような最終的なC++契約プログラミング機能がどのようなニーズの元に構成され、何を備えているべきかを説明し、提案するものです。必然的に、C++26に導入されようとしている機能を超えた部分についての提案になっています。

1章を除いた残りの部分は

  • 2章 : 契約機能のC++におけるユースケースについての説明
  • 3章 : 提案する個々の機能についての解説や提案
  • 4章 : 3章を踏まえた、より大規模で現実的なサンプルの提示
  • 5章 : 最終的に対処する必要がある、言語と契約機能についての他の(3章に含まれない)側面についての議論

のような構成となっています。

P2760R0 A Plan for C++26 Ranges

C++26に向けての、<ranges>ライブラリ関連作業の予定表。

C++23では<ranges>ライブラリ関連作業のまとめをP2214で行っていました。最終的に、そこで提案されていた優先度1のほとんどのものと優先度2の一部のものをC++23に導入することができました。

この提案は、C++26でも同様に作業を優先付けして分類し、追加の考慮が必要なものについてまとめておくためのものです。

この提案で優先度1とされているものは次のものです

  • Rangeアダプタ
    • views::concat
    • take/dropのファミリ
      • views::drop_lastviews::take_last
      • views::drop_last_whileviews::take_last_while
      • views::drop_exactlyviews::take_exactly
      • views::slice
    • 単純な合成アダプタ
      • views::transform_join
      • views::replaceviews::replace_if
      • views::removeviews::remove_if
      • views::upto
    • views::as_input
    • views::cache_last
    • views::chunk_on
    • views::cycle
    • views::delimitviews::c_str
    • より複雑な状況における、より多くのアダプタのborrowed_range対応
    • ジェネレータ
      • views::scan
      • views::generateviews::generate_n
  • Rangeアルゴリズム
    • ranges::reduce
    • ranges::sum
    • ranges::product

他にも優先度2と3の分類がありますが、C++26に向けてはこの優先度1のものについての作業を優先的に行う予定です。

P2762R1 Sender/Receiver Interface For Networking

現在のNetworking TSにP2300のsender/receieverサポートを入れ込む提案。

以前の記事を参照

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

  • 決定を求めるために投票する項目をまとめたセクションを追加
  • Networking TSをターゲットとする理由を明確化
  • receiverからschedulerを取得することが完全に明白な選択ではない理由を追加
  • 提起されたトピックを把握し、それに対する回答や変更を纏めたセクションを追加
  • ネットワークアルゴリズムに関するセクションを追加
  • Networking sendersenderアダプタである必要がある理由を説明するセクションを追加
  • 提案するNetworking senderとしてasync_waitを追加

などです。

P2833R2 Freestanding Library: inout expected span

C++23のライブラリ機能の一部をFreestanding指定する提案。

以前の記事を参照

このリビジョンでの変更は、shared_ptrの前方宣言を削除した事です。

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

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

以前の記事を参照

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

  • 動機の説明の修正
  • ベンチマークの追加
  • sized_rangeapproximately_sized_rangeを包摂するように変更
  • 命名に関するセクションを追加

などです。

P2865R3 Remove Deprecated Array Comparisons from C++26

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

以前の記事を参照

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

  • nullptr定数と配列の同値比較についての問題を追記
  • CWGのレビューを受けての文言の修正
  • 提案する文言の変更

などです。

P2866R1 Remove Deprecated Volatile Features From C++26

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

以前の記事を参照

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

  • 対象のグループとしてSG22を追加
  • 欠けていたライブラリについての分析を追加
  • ライブラリ内に残っている構造化束縛のvolatile依存についての分析を追加
  • 提案する文言の更新

などです。

P2867R1 Remove Deprecated strstreams From C++26

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

以前の記事を参照

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

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

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の更新などです。

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

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

以前の記事を参照

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

  • std::atomic<std::shared_ptr>に切り替えた場合のヘッダの問題についての新しいオプションを追加
  • 提案する文言の更新

などです。

アトミックフリー関数を使っていたstd::shared_ptrstd::atomic<std::shared_ptr>に変更すると、自動的に<atomic>の同名フリー関数が使用されますが、それは<memory>ではなく<atomic>にあるため、追加のヘッダインクルードが必要となります。この提案ではその問題のために

  1. ユーザー任せ(何も対処しない)
  2. std::atomic<std::shared_ptr>のためのフリー関数を<memory>に追加
  3. 最小のstd::atomic<T>フリー関数を<memory>に追加
  4. 全てのstd::atomic<T>フリー関数を<memory>に追加
  5. <memory><atomic>をインクルードするようにする

の5つのオプションを提示しています(まだ未決定)。

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

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

以前の記事を参照

このリビジョンでの変更は提案する文言の更新などです。

P2871R2 Remove Deprecated Unicode Conversion Facets From C++26

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

以前の記事を参照

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

この機能の代替が無いまま削除することへの懸念がありますが、LEWGのレビューと投票の際にはそれを認識しながらも削除することは正しい選択だと考えている人が多数だったようです。

P2872R2 Remove wstring_convert From C++26

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

以前の記事を参照

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

  • 提案する文言の更新
  • P2874R2に関連する文言衝突の懸念についてを削除
  • /W3を使用してMSVCを再テスト

などです。

P2875R2 Undeprecate polymorphic_allocator::destroy For C++26

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

以前の記事を参照

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

  • allocator_traitsのメンバ型を削除した場合の影響との比較を追加
  • 提案する文言の更新

などです。

P2885R2 Requirements for a Contracts syntax

C++契約プログラミング機能の構文の選択のための評価基準や要件をまとめる文書。

以前の記事を参照

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

  • 投票結果と議論の追記
  • 投票結果を受けてintroductionとsummaryを変更
  • セクションの相互参照の修正

などです。

P2909R1 Fix formatting of code units as integers (Dude, where's my char?)

P2909R2 Fix formatting of code units as integers (Dude, where's my char?)

std::format()charを整数値としてフォーマットする際の挙動を改善する提案。

以前の記事を参照

R1での変更は

  • タイトルの変更
  • 影響を受けるフォーマットオプションを明確化
  • printfとの比較を追加
  • SG16の投票結果を追加
  • charwchar_tとしてフォーマットする場合の処理を修正

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

  • __cpp_lib_formatバンプするようにした
  • charwchar_tとしてフォーマットする場合に欠けていたキャストを追加

などです。

この提案はすでにLEWGのレビューをパスしてLWGに転送されています。

P2932R0 A Principled Approach to Open Design Questions for Contracts

契約機能に関する未解決の問題についての設計原則に基づく解決策の提案。

C++26に向けて契約プログラミング機能を導入するべく、SG21での作業は順調に進行しています。今年に入ってから、主に契約注釈の意味論についての議論が交わされ、合意の下でいくつかの提案がMVPにマージされました。

MVP仕様(P2521R4)に対してこれらをマージすれば、C++の言語機能としての契約プログラミング機能がすぐにでも得られますが、いくつかのエッジケースや設計ポイントにはまだ対処すべき問題が残っています。

契約プログラミングに関する議論が紆余曲折を経ていることもあって、C++における契約プログラミング機能の設計に関するドキュメントは存在していません。この提案は、C++26の契約プログラミング機能のための残りの作業の完遂のために、C++における契約プログラミング機能の設計意図を体系化する基本原則をまとめ、それらをもとに現在残っているいくつかの問題への回答を示すものです。

提案されている原則とは次の4つです

  1. 契約注釈は平易な言語(非プログラミング言語)による契約をチェックする
    • 関数の宣言に付加された契約注釈は、それが評価されたときにその関数の平易な言語による契約の違反を特定しなければならない
  2. プログラムのセマンティクスは(実装によって)選択された契約注釈のセマンティクスから独立している
    • 契約注釈のセマンティクスは、それがなされている地点を取り巻く近接した部分のコンパイル時のセマンティクスに影響を与えてはならない
  3. 無視される契約述語(つまり、条件が評価されない場合)はゼロオーバーヘッドである
    • 無視される契約注釈付近のコードの動作は、その注釈がコメントアウトされているかのようになる
    • ただし、無視される場合でも構文チェックは継続される(エンティティがODR-usedであるかは変わらない)
  4. 未定義動作はill-formed(コンパイルエラー)とする
    • 現在の契約機能ではサポートされていないユースケースに対応するために、未定義(安全でない)や実装定義(ポータブルでない)ではなく、未解決の振る舞いをill-formedとすることで拡張性を柔軟に保つことを好む

これらの原則をベースとして、この提案では次の問題についての解決が提案されています

  1. トリビアルな特殊メンバ関数と契約
    • 契約注釈は関数のトリビアル性に影響を与えない
    • そのような契約注釈は評価されない可能性がある
  2. 暗黙のラムダキャプチャ
    1. ラムダ式における契約注釈を許可する
    2. 契約注釈中のODR-usedは暗黙的にキャプチャしない
      • 契約注釈(assertを含む)はラムダ式がキャプチャしていないものをキャプチャしない
      • それがなされている場合はill-formed
  3. 定数式における契約チェック
    1. コンパイル時の契約違反は診断を発行する
      • 契約違反プロセスがコンパイル時に評価されると(observeもしくはenforceセマンティクスでコンパイル時に評価される場合に契約違反が発生すると)診断が発行される
        • enforceセマンティクスによって評価されている場合、プログラムはill-formed
    2. セマンティクスはコンパイル時に選択される可能性がある
      • 契約注釈を評価する際の実装定義の評価セマンティクスの選択は、コア定数式の一部として評価される可能性がある
      • つまり、契約注釈の評価の一部としてそのセマンティクスを選択しても、それを含む式はコア定数式ではなくならない
    3. コンパイル時に契約条件が評価される場合に、定数式で実行不可能な式を使用するのはill-formed
      • コア定数式として有効な式において、コア定数式として有効ではない契約条件を評価すると診断が発行される
        • enforceセマンティクスによって評価されている場合、プログラムはill-formed
    4. コンパイル時定数ではないものの、定数式として有効な初期化子内の契約注釈は、実行時に評価される
      • 潜在的コンパイル時定数である非コンパイル時定数変数の初期化式が、その全ての契約注釈がignoreセマンティクスで評価されたときにコア定数式とならない場合、その初期化式はコア定数式ではない
      • 従って、異なるセマンティクスを持つ可能性のある契約条件を使用してその式を再度定数評価しようとすることは行われない
    5. 定数式における契約注釈の評価について、まとめ
      • 式が定数式として有効であるかを判断するときは、まずignoreセマンティクスで契約注釈とその式を評価することで、式が定数式として有効であるかを調べる
        • 式は定数式として有効である、もしくは、定数式として有効ではないがmanifestly constant-evaluatedなコンテキストにある場合
          • 実装定義の方法で選択されたセマンティクスに基づいて、契約注釈を評価しながら式を再評価する
            • observeセマンティクスの元で契約違反が発生した場合、診断を発行し実行(コンパイル)を継続
            • enforceセマンティクスの元で契約違反が発生した場合、プログラムはill-formed、診断を発行する
            • 式が定数式で実行できない場合、プログラムはill-formed
        • それ以外の場合、式は定数式ではない
  4. 仮想関数 : 次のどちらか
    1. 契約注釈は継承されない
      • 宣言に事前条件・事後条件を持たない仮想関数は、事前条件・事後条件を持たない
      • オーバーライドされる関数の契約条件を継承しない
    2. 仮想関数には契約注釈を行えない
      • 事前条件・事後条件をvirtualとマークされた関数もしくはそれをオーバライドする関数に行うのはill-formed
  5. コルーチン
    • コルーチンには契約注釈を行えない
      • コルーチンである関数に事前条件・事後条件を指定することはill-formed
      • コルーチン本体内でのassertは許可
  6. 最初の宣言の契約
    1. 契約注釈は最初の宣言でのみ行える
      • 関数の宣言がその最初の宣言ではない場合、事前条件・事後条件を持つことはできない
    2. 関数の契約注釈のリストは一貫している必要がある
      • ある関数について、全ての翻訳単位における最初の宣言における契約注釈リストは同じである必要がある。ただし、診断は不要

この提案が採択されることで、P2896R0で収集されている全ての未解決の問題は解決されます。そして、それによって(構文の選択は必要ですが)契約機能の提案はC++言語に統合するための準備ができている状態になります。

P2935R1 An Attribute-Like Syntax for Contracts

P2935R2 An Attribute-Like Syntax for Contracts

C++契約プログラミングのための構文として属性構文を推奨する提案。

以前の記事を参照

R1での変更は、文書の改善などです。

R2での変更は

  • 大体区切り文字(非属性構文)に関する投票結果を追記、それを受けてそれを使用する提案を削除
  • 宣言終了構文、構文拡張およびその議論についてのアンケート結果の追記
  • この提案としての具体的な構文の提案を追加

などです。

P2944R2 Comparisons for reference_wrapper

reference_wrapperに比較演算子を追加する提案。

以前の記事を参照

このリビジョンでの変更は、曖昧さに関するセクションを追加し、それに応じた提案する文言の更新です。

R1では、reference_wrapperに次のような比較演算子を追加することを提案していました

template<class T>
class reference_wrapper {
  friend constexpr bool operator==(reference_wrapper, reference_wrapper);
}

しかし、これだと現在有効な一部の比較が曖昧になるケースがありました。

auto check(int i, std::reference_wrapper<int> r) -> bool {
  return i == r;  // 現在はok、このR1の内容だと曖昧になる
}

現在、この比較はrint&に暗黙変換(reference_wrapper::operator int&による)されることで組み込みの演算子によって比較が行われます。R1の変更後、追加されたreference_wrapperoperator==(上記のもの)も有効な候補として認識され、その結果2つの候補の間でオーバーロード解決に失敗するようになります。これはint以外の型でも同様に起こります。

このことのチェックは、非constTに対して次のようなコンセプトを用いて行うことができます。

template <class T>
concept ref_equality_comparable = requires (T a, T const ca, Ref<T> r, Ref<T const> cr) {
  // the usual T is equality-comparable with itself
  a == a;
  a == ca;
  ca == ca;

  // Ref<T> is equality-comparable with itself
  r == r;
  r == cr;
  cr == cr;

  // T and Ref<T> are equality-comparable
  a == r;
  a == cr;
  ca == r;
  ca == cr;
};

R1の提案では、全ての型でこの比較は失敗します。

前述のように、最後のグループに問題があり、ca == r以外の比較が失敗します。const T&reference_wrapper<T>に変換できないため曖昧になっておらず、それ以外の3つについて対処する演算子を追加する必要があります。

また、このテストによってr == crにも問題があることがわかるため、これを行う演算子も追加する必要があります。

それによって、追加を提案する比較演算子は3つになります。

template<class T>
class reference_wrapper {
  friend constexpr bool operator==(reference_wrapper, reference_wrapper);
  friend constexpr bool operator==(reference_wrapper, T const&);
  friend constexpr bool operator==(reference_wrapper, reference_wrapper<T const>); // 非constなTについてのみ有効
};

これにより、前述のテストを任意の型Tについてパスすることができるようになります。

P2951R3 Shadowing is good for safety

変数のシャドウィングを活用した、安全性向上のための言語機能の提案。

以前の記事を参照

このリビジョンでの変更は、内容の軽微な修正などです。

P2955R1 Safer Range Access

std::vector等コンテナ向けの安全な要素アクセス関数の提案。

以前の記事を参照

このリビジョンでの変更は、内容の軽微な修正のみです。

P2961R0 A natural syntax for Contracts

契約プログラミング機能のための構文の提案。

C++26に向けた契約プログラミングの議論において、あとは構文の選択が大きな議題として残っています。現在のところ構文の候補として有力なのは属性like構文と呼ばれるもので、これはC++20の頃の構文を踏襲し新しい文法やキーワードを発明する必要がなく、属性の無視という性質を自然に利用でき、さらにGCCによる実装経験があります。

属性like構文にもいくつか欠点があります

  1. 契約注釈の区切りのトーク[[ ... ]]が構文として重い
    • 一部のユーザーからは醜いと認識されている
  2. 契約構文は属性と同様の記法を利用するが属性ではないため、混乱が生じる
    • 契約構文は違反ハンドラを通じるなどして、関数から新しいコードパスを作成できるが、標準属性はこのようなことを行うように設計されていない
  3. 契約注釈を置ける構文上の位置は関数宣言の自然な読み取り順序に反している
    • 属性の置ける位置を再利用するため、後置戻り値の前(overriderequires節の前)に事前条件と事後条件がくる
  4. assertは式ではないため、Cのassertの完全な代替となり得ない
  5. 3と4を属性構文のまま解決しようとすると、属性構文の利点が失われる
    • 現在それらが可能なように標準属性はできていない、そのため実装経験もない
  6. 属性構文では、その内部の述語の前に:がくる場合に、それより前の内容に区切りを導入しない
    • 視覚的な情報の区別(契約種別や戻り値の名前、ラベルなどの見分け)が難しくなり、将来的に構文解析の曖昧さを生じさせる
  7. 契約注釈自体に属性を付加する場合、属性内の属性という文法を導入させなければならない

属性like構文の代替としていくつか構文の提案があります

P2373は契約注釈のためにcontract-kind ( predicate )という新しい文法を発明しました。そしてそれに加えて幾つかの設計上の選択を提案しました

  • assertionをinconditionにリネーム
  • pre, post, assertの代わりに、precond, postcond, incondを使用
  • precond, postcond, incondを文脈依存ではない完全なキーワードとして追加
  • 事後条件では、戻り値をresultという固定的な名前で参照する

これらのことがSG21では受け入れられなかったようで、この提案の方向性も問題を抱えています。

この提案は、P2461R1とP2373R0で提示されたアイデアを流用しながら、それら2つと属性like構文にある問題を解決しつつP2885で示された契約構文に対する要件を満たすような、契約プログラミング機能のためのより自然な構文を提案するものです。

この提案の目指す設計ゴールは次のようなものです

  • 構文は既存のC++に自然に馴染む。
    • 契約機能に慣れていないユーザーでも混乱を招くことなく直感的に理解できるものである必要がある
  • 契約注釈構文は、属性やラムダ式など既存のC++の構成要素に似ていてはならない
    • ぱっと見で認識可能な独自の設計空間に置かれているべき
  • 構文はエレガントかつ軽量である
    • 必要以上にトークンや文字を使用するべきではない
  • 読みやすくするために、一次情報とニ次情報を構文的に分離する
    • 一次情報(条件種別、契約条件式、戻り値名、キャプチャなど)をそれ以外のニ次情報(ラベルなど)よりも視覚的に強調する

これらの目標は、現在の構文候補が満たしていないものでもあります。

この提案による構文は、P2461R1とP2373R0で提案されたアイデアをベースとしており、関数の一番最後の位置にpre(...)post(...)によって事前条件と事後条件を指定します。

float sqrt(float x)
  pre (x >= 0);     // 事前条件

int f(int x)
  post (r: r > x);  // 事後条件

pre(...)post(...)のように()の内部に述語(契約条件)を指定することは、if(expr)while(expr)などの既存のC++構文と一貫しており、非常に自然なものです。また、事後条件で戻り値を使用する場合は、条件式の前に:を置いて、その前で任意の名前を使用できます。

pre, postはどちらも文脈依存なキーワードであり、関数の一番最後(requiresの後)にくることもあって、関数の他の部分でpre, postを任意のエンティティの名前として使用することができます。

template <typename T>
auto f(T x) -> bool
  requires std::integral<T>
  post (x > 0);

文法の詳細は提案の4.1 Grammarに記載されています。少し大きめなのと差分をわかりやすく表示できないためここには転記しません。

assertも事前・事後条件と同様の構文によるのですが、この場合既存のCのassertマクロと衝突してしまうので、それを回避する必要があります。そのために、キーワードを少し変更しています

void f() {
  int i = get_i();

  // C assertと衝突する
  assert(i >= 0);

  // この提案によるassert
  assrt(i >= 0);
}

ここではassrtとしていますが、他の選択肢(ass, assertion, co_assertなど)でも良いとしています。コルーチンのキーワードであるco_yieldなどと同様に、ユーザーはすぐに慣れるとしています。

assrt(expr)は式であるため、現在のassertマクロと同様に式として使用できます。

class X {
  int* _p;
public:
  X(int* p)
   : _p((assrt(p), p))  // ok
  {}
};

これによって、単純な文字列置換によってassertマクロからassrt式へ移行することができます。

この提案ではさらに、P2885で示されているC++26契約の後の機能拡張のアイデアを考慮しています。

例えばキャプチャ

void vector::push_back(const T& v)
    post [old_size = size()] ( size() == old_size + 1 ); // 初期化キャプチャ、関数の実行によって変更される前の値を保存する

ここでは、キャプチャ関連に限ってのみラムダ式のキャプチャ構文を流用しますが、契約種別のワード(post)が先行し契約条件が()内で記述されることでラムダ式との混同を回避しています。

他の例として、契約注釈に対するrequires

template <typename T>
void f(T x)
  pre (x > 0) requires std::integral<T>;

// もしくは
template <typename T>
void f(T x)
  pre requires std::integral<T> (x > 0);

契約注釈に対するrequires節が述語の前後どちらに来るべきかはまだ議論されていませんが、この提案による構文はどちらの場合でも拡張可能です。

契約注釈に対する属性指定

template <typename T>
void f(T x)
  pre (x > 0) [[deprecated]];

これも属性がどこに来るかはまだ決まっていませんが、やはりどちらも受け止めることができます。属性like構文の場合のように属性内属性を考慮する必要もありません。

ラベルの指定

ラベルとは、契約注釈に対するメタ注釈となるものです。現在想定されているのは、契約注釈のセマンティクスを指定するものです。

void f(int x)
  pre (x > 0) [audit]; // or <audit>, or {audit}, or [{audit}], or @audit ...

前のものと同様にこれも位置について異論がありますが、どちらでも受け止めることができます。

戻り値の分解

構造化束縛に近い構文によって、事後条件で参照する戻り値を分解して参照する拡張が考えられます

std::tuple<int, int, int> f()
  post ([x, y, z] : x != y && y != z);

これらの構文はまた、P2885で示された契約構文に対する要件の多く(将来の拡張も含めて)を考慮し、満たすように設計されています。

P2461(closure-based syntax)の著者の方はこの提案がP2461のアイデアを包含し改善するものであるとしてこの提案を支持し、P2461の追及を停止することにしたそうです。そのため、契約機能の構文提案としては、属性like構文とP2737R0の条件中心構文、及びこの提案の3つがアクティブとなっています。

P2963R0 Ordering of constraints involving fold expressions

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

現在のコンセプトの制約の半順序ルールでは、畳み込み式はそれ全体で1つの原子制約式と扱われます。それによって、プログラマの意図と異なる制約順序付けが行われ、特に可変長テンプレートにおける適切な制約を難しくしています。

template <class T>
concept A = std::is_move_constructible_v<T>;

template <class T>
concept B = std::is_copy_constructible_v<T>;

template <class T>
concept C = A<T> && B<T>;


template <class... Ts>
  requires (A<Ts> && ...)
void g(T...);

template <class... Ts>
  requires (C<Ts> && ...)
void g(T...);

この例のコンセプトCは型に対してコピー構築可能性とムーブ構築可能性を要求しています。同じ型のシーケンスTsに対して、C<Ts> && ...A<Ts> && ...を包含しているため、制約の順序づけもそれを反映したものとなることが期待されます。しかし、現在は畳み込み式はその全体で1つの原子制約式として扱われて順序づけが行われてしまうため、同じ型のシーケンスTsに対してC<Ts> && ...A<Ts> && ...の間には包摂関係が成立せず順序づけ不可能となります。

その結果、上記の例ではABを満たす型Tを1つ以上g()に渡すと、2つのオーバーロードの間で優先順位が付かないため呼び出しは曖昧になり、コンパイルエラーとなります。

この提案は、畳み込み式においてはその全体ではなく、含まれる個別の制約式を原子制約式として扱うようにして、この問題を解決しようとするものです。

提案では次のような手順によって畳み込み式の半順序を規定しようとしています

  1. 畳み込み式を正規化して、二項畳み込み式を単行畳み込み式に変換する
    • (init && ... && Pack)もしくは(Pack && ... && init)を、(Pack && ...)initを正規化したものを&&で繋げた形に変換
    • ||も同様
  2. (... op Pack)(Pack op ...)に変換し、以降区別しない
    • op&& ||のどちらか
  3. 残った(Pack && ...)もしくは(Pack || ...)について、比較する2つの畳み込み式のパラメータパックが同じサイズならば、通常の原子制約式の包摂のルールに従って包摂関係が判定される

この提案の一部はclangにおいて実装されているようで、そこでは実装においてもコンパイル時間においても大きな影響はなかったとのことです。また、この提案は現在曖昧になるオーバーロードを意図通りに順序づけするだけで、既存のコードの動作を変えるものではありません。

P2966R0 Making C++ Better for Game Developers -- Progress Report

P2966R1 Making C++ Better for Game Developers -- Progress Report

ゲーム開発者にとってより使いやすいものへC++を進化させるための作業についての報告書。

この文書は、SG14のゲーム開発の経験を有するメンバーが中心となってゲーム開発者の視点からC++を改善するために行っている作業のついての進捗方向をおこなうものです。

取り組みは2019年12月頃から開始され、最初は主に情報(要望当)の収集を行っていたようです。そこで得られた情報はSG14に持ち込まれ、議論・分類・選択が行われました。

この議論及びプロセスの目的はゲーム開発にとって最も効果的な機能のサブセットを特定し、個別の提案を提出することでさらに議論を進めていくためのものでした。この文書はそのような取り組みの現時点での成果報告を行うものです。

そのプロセスにおいては、次のような原則の下で議論が行われました

  • C++をよりシンプルにすることは良い事
  • C++をより教えやすくすることは良い事
  • パフォーマンスへの悪影響を避ける
  • デバッグは重要

この作業の結果として追求しようとする要求(機能)は必ずしもこの原則のすべてを満たすものではありませんが、すくなくともこの原則に違反しないものとなっています。

作業においては要求を次のようなカテゴリで分類しています

  • Compile-Time Computing
  • Memory Allocation and Deterministic Behavior
  • Attributes
  • Move Semantics
  • Handling Disappointment
  • Pattern Matching
  • Tooling and Ease-of-Coding
  • Networking
  • Parallel and Concurrent Computing
  • Logging and I/O
  • Numeric Computing
  • Miscellaneous

そのうえで、それぞれの要求についてSG14として次のような基準でガイダンスを行っています

  • この機能はSG14が望むものか?
  • この機能を提案として追求する場合、SG14単独で追求すべきか、関連グループと強調すべきか?
  • この機能は既存の言語機能で代替できるものか?その場合、この機能を追求する価値はあるか?
  • 望ましい代替のアプローチは存在するか?

現在のリビジョン(R1)では、これらの事をベースとして現在補足されている要求がリストアップされています。

SG14のメンバーは、ここで挙げられているいくつかのものについて個別の提案を書く予定のようです。

P2968R0 Make std.ignore a first-class object

std::ignoreは主にstd::tieでtuple-likeなオブジェクトを分解しながら受ける際に、一部の要素を無視するためのものです。これは純粋なライブラリオブジェクトであり、あらゆる型を受けながら何もしない代入演算子を定義することで実装されています。

// std::ignoreの実装例
struct ignore_type {

  template<typename T>
  void operator=(T&&) const {}
};

inline constexpr ignore_type ignore;


auto f() -> int {
  return 10;
}

auto g() -> std::string {
  return "str";
}

int main() {
  ignore = 1.0; // ok
  ignore = f(); // ok
  ignore = g(); // ok
}

このため、関数の実行結果を捨てていることを明示する、とりわけ[[nodiscard]]関数の結果を明示的に捨てる(ことで警告を抑制する)ためにもよく使用されています。

[[nodiscard]]
int f() {
  return 10;
}

int main() {
  f();                    // 警告が発せられる
  std::ignore = f();      // 警告されない、厳密には標準で保証された動作ではない
  (void)f();              // 警告されない、合法
  static_cast<void>(f()); // 警告されない、合法
}

このことはC++コアガイドラインなどでも推奨されていますが、std::ignoreの規定はstd::tieで使用することしか想定しておらず、その実装について何も指定していない(実装例のような代入演算子を持つかどうか指定されていない)ため、実際にはこの振る舞いは標準が保証するものではありません。とはいえ、主要なC++標準ライブラリ実装は全てテンプレート代入演算子によってこれを実装しています。

現在のところ完全に合法的に同じことを行う方法は上記例の下2つの明示的キャストによるものだけですが、CスタイルキャストC++では推奨されない場合が多く教育可能性の問題があり、static_castは冗長です。コアガイドラインやcppreferenceのサンプル、在野のライブラリドキュメントなど、多くの場所でこのイディオムは好んで使用されており、この使用方法をきちんと標準化する必要があります。

この提案は、std::ignoreの実装がテンプレートであらゆる型のオブジェクトを取り何もしない代入演算子によるものに(上記例のように)なるように指定することで、このイディオムをwell-definedにしようとするものです。

この提案によるメリットは次のようなものです

  • コードの意図を伝えるより適切な自己文書化コードを促進
  • 安全性が重要な環境におけるC++初心者プログラマへの教えやすさの向上

また、同時に、std::ignoreの型がconstexprコンストラクタを持つことを指定することで、LWG Issue 2933を解決することも含んでいます。

P2971R0 Implication for C++

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

ここで提案されている=>演算子記号論理における含意記号と同じ振る舞いをするもので、p => q!p || qと同じ結果となります。

p q p => q !p || q
true true true true
true false false false
false true true true
false false true true

含意p => qは文章の中や日常的な使用において、通常の文として表現されることが一般的です。その場合の表現は通常、「pならばq」や「もしpが成り立つならばqが成り立つ」のように言われます。

英語においては、p => qは、pを前件(antecedent)、qを後件(consequent)として、“If antecedent, then consequent.”のように綴られます。

このため、プログラミングの初学者が、この含意の表現としてのifプログラミング言語の条件付き制御構造としてのifを混同してしまうケースが多々あるようです。

if (cond) {...}においてのifwhenin the event thatの意味で使用され、そこに含意は全く関係がなく、その流れは条件付き制御構造でしかありません。含意は条件付き制御構造とは無関係ですが、if文の流れを含意だと思ってしまう場合があるようです。

ifと含意が相互作用することがあるのは、if (p => q) {...}のようにif文の条件式(の一部)を含意が構成する場合です。この場合、含意の表現とそのコードの表現が一致し、読みやすさや教えやすさが向上します。

また、含意の表現による条件の指定は、規格書中(特にライブラリ機能の規定)においてよく使用されているようです。

  • [unique.ptr.single.general]/2:
    • If the deleter’s type D is not a reference type, D shall meet the Cpp17Destructible requirements.

    • static_assert( nonreference_type<D> => destructible_type<D> );
  • [optional.relops]/2:
    • Returns: If x.has_value() != y.has_value(), false; otherwise if x.has_value() == false, true; otherwise *x == *y.

    • return (x.has_value() == y.has_value()) and (x.has_value() => *x == *y);
  • [out.ptr.t]/3:
    • If Smart is a specialization of shared_ptr and sizeof...(Args) == 0, the program is ill-formed.

    • static_assert( shared_ptr_type<Smart> => sizeof...(Args) > 0uz ) );

提案にはこれ以外にも3つの例が示されています。

このような含意は、演算子オーバーロードも含めたユーザ定義の関数によって書くことができません。なぜなら、短絡評価を実現できないからです。短絡評価も含めて実現しようとするとマクロに頼るほかありません。

また、言語組み込みの=>演算子requires式やコンセプト定義等制約の文脈でも条件の表現や読みやすさの改善に役立つ可能性があります。

これらの理由から、この提案は含意記号と同等の意味論を持つ論理演算子=>を組み込みの新しい演算子として追加することを提案しています。

提案では、p => q!p || qと同じセマンティクスを持つように設計されています

  • =>演算子の優先順位は||と同じ
  • =>演算子は左結合
  • =>演算子は短絡評価される
    • p => qにおいて、pfalseならばqを評価しない

この提案ではまた、標準ライブラリの条件指定をこの演算子によって表現するようにすることも提案しています。

P2972R0 2023-09 Library Evolution Polls

2023年9月に行われる予定のLEWGの投票の予定表。

投票にかけられる予定の提案は次のものです

全てC++26に向てけのライブラリ機能の提案ですが、一部C++23のDRなものがあります。

P2973R0 Erroneous behaviour for missing return from assignment

代入演算子の定義時にreturnを忘れた場合を、UBからEB(erroneous behaviour)として*thisを返すように変更する提案。

クラス型の代入演算子を変なオーバーロードではなく適切に定義しようとする場合、代入操作を記述するので満足してreturn *this;を書き忘れるのは比較的よくあることです。

struct Foo {
  Foo& operator=(const Foo& rhs) {
    x = rhs.x;
    y = rhs.y;
    // error: forgot "return *this;"!
  }

  int x, y;
};

残念ながら、通常の関数同様に非void戻り値型関数でreturnを忘れてもエラーにはならず警告止まりです。これを修正せずに実行してしまうと、未定義動作となります。

P2795では、未初期変数の読み取りに伴う未定義動作を修正するために、erroneous behaviour(EB)という新しい動作状態を定義しようとしています。EBはその名の通り間違った動作(エラー)ではありますがUBではなくWell-definedな動作であり、その動作は未定義ではなく規定された、あるいは実装定義の特定の動作をすることが求められます。たとえば、P2795では未初期化変数を特定の値(通常は0、コンパイルオプションで任意の値を指定可能)に初期化しておくことで、少なくとも読み取りを未定義ではなくすとともに読んでしまった場合の動作を推測可能(デバッグで気付きやすい)なようにしようとしています。

EBはP2795で導入されようとしている新しい概念でありまだ標準には導入されていませんが、これは未初期化変数読み取り以外の現在UBとされているものに対して適用可能である可能性があります。この代入演算子return忘れという問題はそこで提示されている原則を満たしています

  • 危害の可能性
    • 一般的な最適化では、returnが欠落した代入演算子を実行すると、予期しないコードが継続して実行される可能性がある
  • 検出可能性
    • returnの欠落はコンパイル時に検出可能であるべき。現在のほとんどのコンパイラはこれについて警告を発することができている
  • オプトアウト可能性
    • std::unreachable()を挿入することで、実行されてないことがユーザーにわかっているブランチなどで現状を復帰(UBに)することができる

これらのことに基づいて、この提案はreturn文が無いコピー/ムーブ代入演算子の末尾での動作を、UBではなくEBとしてreturn *this;と等価である、と規定しようとするものです。

P2976R0 Freestanding Library: algorithm, numeric, and random

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

この提案は、ヒープ割り当てやシステムコールを使用せず、例外を必要としない標準ライブラリをフリースタンディング指定していく一連の作業の一環です。フリースタンディング指定された機能は、OSの無い環境やそのサポートの受けられない環境においても使用可能であることが規定されます。

この提案では、<algorithm>, <numeric>, <random>にあるフリースタンディングとなりうるライブラリ機能の内、浮動小数点数ExecutionPolicyに依存しないものについてフリースタンディングとすることを目指すものです。

浮動小数点数型については、OSのカーネル環境において浮動小数点数を使用してしまうと、ユーザーモード浮動小数点数状態を変化させ壊してしまうため、フリースタンディング機能として指定することを避けています。このため、<random>の乱数生成周りの機能は整数の計算のみで実装可能なものだけをフリースタンディングとして提案しています。

おわり

この記事のMarkdownソース

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

文書の一覧

全部で44本あります。

もくじ

N4956 Concurrency TS2 PDTS

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

N4958 Working Draft, Programming Languages -- C++

C++26のワーキングドラフト第1弾

N4959 Editors' Report, Programming Languages -- C++

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

N4960 Business Plan and Convener's Report: ISO/IEC JTC1/SC22/WG21 (C++)

ビジネスユーザ向けのC++およびWG21の現状報告書。

P0124R7 Linux-Kernel Memory Model

P0124R8 Linux-Kernel Memory Model

Linuxカーネルにおけるメモリモデルについて説明する文書。

この文書は、C/C++標準化委員会がメモリモデルに関する既存の慣行・実装としてLinuxカーネルにおけるものを参照する際に役立てることを意図したものです。

P0963R1 Structured binding declaration as a condition

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

構造化束縛宣言は変数宣言の変種であり、ほぼ変数宣言をかけるところなら同様に書くことができます。しかし、if文等の条件式はその例外であり、条件式に直接構造化束縛宣言を書くことはできません。

例えば次のような何かパースを行う関数があったとき

struct parse_window {
  char const *first;
  char const *last;
};

auto parse(std::contiguous_iterator auto begin, std::contiguous_iterator auto end) -> parse_window;

この関数の実行結果を取得し、パースの成否をチェックして継続のアクションを記述するには、例えば次のように書くことができます

if (auto [first, last] = parse(begin(), end()); first != last) {
  // interpret [first, last) into a value
}

この時、構造化束縛宣言を条件式として使用できるとより記述が単純化されます。

// 現在できない
if (auto [first, last] = parse(begin(), end())) {
  // interpret [first, last) into a value
}

この提案は、これをできるようにしようとするものです。

この提案による条件式における構造化束縛はオブジェクトの分解のみを担っており、条件判定に使用されるのは構文上からは隠蔽されている右辺の結果オブジェクトそのものです。従って、先ほどの例ではパース結果を表すparse_window型がパース成否を表現できるようにしておく必要があります。

struct parse_window {
  char const *first;
  char const *last;

  // パース成否テストを追加
  explicit operator bool() const noexcept { return first != last; }
};
// この提案の後、次の2つのif文は同じ意味になる

if (auto [first, last] = parse(begin(), end()); first != last) {
  // interpret [first, last) into a value
}

if (auto [first, last] = parse(begin(), end())) {
  // interpret [first, last) into a value
}

提案より、その他の例。

C++26のstd::to_charsの例

// この提案がない場合の書き方
if (auto result = std::to_chars(p, last, 42)) {
  auto [to, _] = result;
  auto s = std::string(p, to);  // 変換した数字文字列
  ...
} else {
  auto [_, ec] = result;
  std::cout << ec << '\n';
}
// この提案の場合の書き方
if (auto [to, ec] = std::to_chars(p, last, 42)) {
  auto s = std::string(p, to);  // 変換した数字文字列
  ...
} else {
  std::cout << ec << '\n';
}

C++26では、to_chars_resultbool変換演算子が追加されることにより、先ほどの例と同様にこの提案の恩恵を受けることができます。これはstd::from_charsも同様です。

if (int v; auto [ptr, ec] = std::from_chars(p, last, v)) {
  auto s = std::string(ptr, last);  // 変換に使用されなかった残りの部分の文字列
  ...
} else {
  std::cout << ec << '\n';
}

何か数学的な反復ソルバの例。

反復ソルバは主要な処理ステップをループを回して実行し、特定の条件が満たされるまでこのループを継続することで問題を解こうとします。

while (true) {
  auto [Ap, bp, x, y] = solve(...);

  // 最適解が得られていたらループを抜ける
  if (is_optimal(x))  // scan the x vector
  {
    break;
  }

  ...
}

この時、is_optimal(x)は線形かそれより悪いアルゴリズムが使用される(あるいは使用せざるを得ない)場合があります。一方で、ソルバステップ(solve())においては答えが最適かどうかを認識しその情報をキャッシュすることができる場合があります。その場合に戻り値からそれを直接取得できればコードが簡潔かつ効率的になります。

while (true) {
  // 最適解が得られていたらループを抜ける
  if (auto [Ap, bp, x, y] = solve(...))
  {
    break;
  }

  ...
}

これらの例に共通しているのは、ある処理の成否を保持しているのはその戻り値の一部または全部のコンポーネントであり、そのチェックの方法は型によって様々かつ自明ではないということです。この場合に、処理の成否をその処理の結果型そのものに焼き付けることでそのような煩雑なチェックコードを削減することができます。しかし、処理の結果として必要なのが結果オブジェクトのサブコンポーネントである場合、それを行おうとすると今度は余分な中間オブジェクトを導入しなければなりません(上記to_chars()の例のように)。

現状の構文では結果の分解と結果の成否チェックを同時に実行することができませんが、この提案による条件式における構造化束縛宣言を用いるとそれを同時に記述することができるようになります。またこれによって、何か処理結果を表す型を書く際に、その処理の成否のテスト方法を型に埋め込みユーザーはその方法について無知なままで結果を簡易に利用できるようにする(上記例のような)コーディングスタイルを促進します。

P1068R8 Vector API for random number generation

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

以前の記事を参照

このリビジョンでの変更は、std::spanベースのカスタマイズの許可、generate_randomイテレータ版を追加、design-considerationsセクションの拡充などです。

P1967R11 #embed - a simple, scannable preprocessor-based resource acquisition method

コンパイル時(プリプロセス時)にバイナリデータをインクルードするためのプリプロセッシングディレクティブ#embedの提案。

以前の記事を参照

このリビジョンでの変更は、EWGでの投票結果の追記、CWGレビューに伴う提案する文言の修正などです。

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

P2407R5 Freestanding Library: Partial Classes

一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。

以前の記事を参照

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

  • 名前空間スコープで定義されないものについて何がフリースタンディング指定されるのかを明確化
  • freestanding.itemの番号をふり直した
  • クラスメンバの全てがフリースタンディングとなる必要がないように要件を修正
  • 言葉として、関数の定義よりもエンティティを使用する
  • フリースタンディングのコメント文法を簡素化
  • フリースタンディングからホストへの移行に関するメモを調整
  • 文言の根拠となるメモなどの追加
  • std::variantの指定において、get_ifの代わりに説明専用のGET関数を使用する
  • this以外のオブジェクトにアクセスする際に、string_viewの説明用インターフェースではなくpublicインターフェースを使用する

などです。

この提案はLWGのレビューを通過し、次の全体会議で投票にかけられることが決まっています。

P2521R5 Contract support -- Record of SG21 consensus

C++26の契約プログラミングのMVP仕様に対する決定などを追跡するための文書。

以前の記事を参照

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

  • オーバーライドする関数は事前条件と事後条件を持てないが、オーバライドされる関数の事前条件と事後条件が適用される
  • 構文の選択に関する議論を短縮
  • 未解決問題のリストを更新

などです。

P2728R6 Unicode in the Library, Part 1: UTF Transcoding

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

以前の記事を参照

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

  • null_sentinel_tの比較演算子の複雑な制約を単純化
  • ranges::project_viewを導入し、それによってcharN_viewsを実装するように変更
  • utfN_viewsを個別クラスではなくエイリアスに変更

などです。

P2746R3 Deprecate and Replace Fenv Rounding Modes

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

以前の記事を参照

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

  • Varna会議でのコメントを追記
  • Cの予約名との衝突を避けるため、cr_xxx()という関数名をcr::xxx()のようにcr名前空間に配置
  • flush-to-zero問題への対処/回避
  • cr::cast()の指定を修正
  • 実装が複雑になるのを回避するため、一部の関数でconstexprを避ける
  • cr::make()に、欠落していた丸モード引数を追加
  • constevalからconstexprに変更

などです。

P2795R3 Erroneous behaviour for uninitialized reads

未初期化変数の読み取りに関して、Erroneous Behaviourという振る舞いの規定を追加する提案。

以前の記事を参照

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

  • 振る舞いを修正する対象を“object with automatic storage duration”から“non-static local variable”に変更
    • 一時オブジェクトと関数引数を除外することを意図している
  • オプトアウトのための[[indeterminate]]属性を追加

などです。

[[indeterminate]]属性は変数の定義に対して指定するもので、これが指定されたローカル変数が初期化子を持たない場合、現在と同様に初期化されません。

void f(int);
void g() {
  int x [[indeterminate]], y;
  // xは未初期化、yは実装定義の値で初期化

  f(y); // OK、yの読み取りはerroneous behaviour
  f(x); // UB、xの読み取りはundefined behavior
}

P2821R4 span.at()

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

以前の記事を参照

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

この提案はLEWGのレビューを終えて、LWGに転送するためのLEWGでの投票待ちをしています。

P2833R1 Freestanding Library: inout expected span

C++23のライブラリ機能の一部をFreestanding指定する提案。

以前の記事を参照

このリビジョンでの変更は、P2407R5の変更を適用、機能テストマクロをフリースタンディング指定、P2821とのコンフリクトについて追記、などです。

この提案はLEWGのレビューを終えて、LWGに転送するためのLEWGでの投票待ちをしています。どうやら、C++23へのDRとなるようです。

P2845R2 Formatting of std::filesystem::path

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

以前の記事を参照

このリビジョンでの変更は、デバッグ出力(?オプション)を提供するようにしたことです。

P2863R1 Review Annex D for C++26

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

以前の記事を参照

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

  • D.29(filesystem::u8path)に何もしないことの根拠を追記
  • Issueへのリンクを修正
  • ベースとなるドラフトを更新
  • 進捗状況について追記

などです。

個別の提案に分かれていないものとして、static constexprなクラスのメンバ変数に対する定義の再宣言の非推奨を解除することにEWGで合意がなされ、別の提案で進められることになりました。

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

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

以前の記事を参照

このリビジョンでの変更は、文言の修正や改善などです。

この提案の担当範囲については、EWGのレビューで削除することに合意が取れなかったため作業は中止されるようです。

P2865R2 Remove Deprecated Array Comparisons from C++26

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

以前の記事を参照

このリビジョンでの変更は、文言の修正やCWGレビューについて追記したことなどです。

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

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

以前の記事を参照

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

  • LEWGのレビューで明らかになった懸念について追記
  • ゾンビ名セクションを変更しないことを確認
  • Annex Cの文言を提供

などです。

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

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

以前の記事を参照

このリビジョンでの変更は、SG1やLEWGのレビューを受けての文言の修正などです。

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

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

以前の記事を参照

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

  • 異なるバージョン間でこの関数を使用するリスクについて追記
  • 古いコードからの移行について追記
  • ゾンビ名セクションを変更しないことを確認
  • Annex Cの文言を提供

などです。

P2871R1 Remove Deprecated Unicode Conversion Facets From C++26

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

以前の記事を参照

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

P2875R1 Undeprecate polymorphic_allocator::destroy For C++26

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

以前の記事を参照

このリビジョンでの変更は、ベースとなるWDの更新と文言の調整などです。

P2878R5 Reference checking

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

以前の記事を参照

このリビジョンでの変更は、do式とパターンマッチングにおけるダングリングの問題に対する解決策の修正、std::thread/std::jthreadに対する6種目のチェックの提案、などです。

P2885R1 Requirements for a Contracts syntax

C++契約プログラミング機能の構文の選択のための評価基準や要件をまとめる文書。

以前の記事を参照

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

  • [func.mix]と[future.abbrev]に新しい要件を追加
  • "Accessibility" [basic.access]を"Teachability" [basic.teach]へ変更
  • [basic.lang]と[compat.cpp]の要件を単一の[basic.cpp]にマージ
  • [func.retval]を満たすために提案された2つのアプローチを、個別のより具体的な要件である[func.retval.predef]と[func.retval.userdef]の2つに分解
  • [func.pos.prepost]と[func.pos.assert]から、位置と名前探索に関する基本要件を抽出し[func.pos]にまとめた
  • 要件[func.pos.prepost]と[func.pos.assert]をより具体的にした
    • 事前/事後条件はパラメータ宣言の後に置く必要があり、アサーションはCのアサートがかける場所ならどこでも書けるようにする
  • [compat.back], [compat.impl]と[future.reuse]にそれぞれ根拠を追記
  • よりバランスの取れた表現となるように、[basic.aesthetic]と[basic.brief]を修正
  • [compat.c]の意図を明確化
  • [func.pos]において、属性構文の事前/事後条件の位置についての間違いを修正
  • [func.pos.prepost]に、配列ポインタを返す関数の曖昧な位置と名前探索の例を追加
  • [func.pos.prepost]と[func.mix]に違反する構文のアイデアとして、ピアノ構文についての言及を追記
  • [future.prim]に違反する属性構文の例を追加
  • [future.captures]を少し書き直して、仮引数名のキャプチャとそれ以外のもののキャプチャの違い、及び初期化キャプチャの許可とラムダのようなあらゆる種類のキャプチャ許可の違いを説明
  • P2935R0で説明されている属性構文の様々な拡張および設計の代替案について、文書全体に追記
  • 一次情報と二次情報について説明する段落を概要に追加
  • 将来の進化に対する要求の妥当性の違いについて言及
  • 要求の優先順位を決定し、それらを客観的/主観的なものとして分類するために、SG21で実施予定の電子投票に言及した方法論のセクションを書き直し、個々の要件の説明からそのような優先順位や分類を削除した

などです。

P2890R0 Contracts on lambdas

ラムダ式に対する契約条件指定ができるようにする提案。

現在進行中の契約プログラミングに関する議論においては、通常の関数に対して契約を行うことを主眼に議論されています。同様にラムダ式に対しても行えることが期待されますが、現在のところラムダでに対してどのように動作するのかはあまり考慮されていないようです。

この提案は、契約条件指定がラムダ式においても通常の関数と同じように動作し、なおかつキャプチャをどのように処理するかを提案するものです。

現在のContracts MVP仕様(P2388R4)においてはラムダ式における契約条件指定はその名前解決の問題から未解決の問題とされ、P2036にその解決を委ねMVPとしてはそれを待ってはいません。しかし、P2036はR3が既にC++23に採択されているため、その問題は解決済みでした。

この提案では、ラムダ式における契約条件式内の名前探索はP2036で提案されている後置戻り値型におけるそれと同様にすることを提案しています。すなわち、そのコンテキストで表れている名前はまずキャプチャ名が考慮されます。

int i = 0;
double j = 42.0;

...

auto counter = [j=i]() mutable [[pre: j >= 0]] {
//                                    ^
  return j++;
};

(ここでが契約構文として属性構文を使用していますが、これは仮のもので、この提案の内容は構文とは独立しています)

この例における事前条件式内のjは外側で宣言されているdouble型の変数jではなく、そのラムダにおいてキャプチャされている変数jint型)として扱われます。

この名前解決以外のところでは通常関数に対する契約条件指定と同じ振る舞いとなります。特に、現れる名前が常にODR-usedとなるという点も現在の契約仕様及び言語の他の部分([[assume]]など)と同様とすることを提案しています。

P2894R0 Constant evaluation of Contracts

定数式においても契約チェックを有効化する提案。

現在進行中の契約プログラミングに関する議論においては、実行時の関数に対して契約を行うことを主眼に議論されています。そのため、定数式において契約条件チェックするかどうか、それがどのようなセマンティクスを持つのか、などはあまり議論されておらず、方向性や一致した見解は確立されていません。

現在のC++コンパイル時プログラミングがかなり強力かつ一般的なものとなっており、実行時に動作するコードの多くはコンパイル時にも動作し、契約プログラミングに関しても同様であることが自然と期待されます。この提案はそのために現在の契約プログラミング仕様の下で定数式における契約条件チェック周りのセマンティクスについて議論し提案するものです。

現在の契約プログラミング仕様は、P2877R0の採択によってビルドモードの概念が削除され、契約アノテーションのセマンティクスは実装定義となりコンパイル時には認識できないようにされています。そのため、C++のコードの一部がill-formedであるかを契約プログラミングのセマンティクス(実行時にチェックされるかどうか)に依存させることはできなくなっています。したがって、定数評価中はP2877R0で示されている3つのセマンティクス(ignore, observe, enforce)のいずれかによって実行されているかを選択することができません。

契約アノテーションの定数評価時のセマンティクスについて指定が必要なのは次のどちらかの場合についてです

  1. 契約条件式はコア定数式ではない
  2. 契約条件式はコア定数式だが、falseに評価された(契約が破られた)

この2つの場合についてそれぞれ、セマンティクスを指定する必要があります。

1つ目のケースについてはまず、P2448R2がC++23に適用されたため、constexpr/consteval関数がコンパイル時に呼び出されなければそこに指定されている契約条件について何かをする必要はありません。

int pred(); // constexprではない述語

constexpr int f() [[ pre: pred() ]]; // OK; コンパイル時に呼ばれない
consteval int g() [[ pre: pred() ]]; // OK: 使用されない

int main() {
  return f(); // f()はコンパイル時に呼び出されない
}

考慮すべきは、この場合にf()/g()が定数式で呼ばれ、契約条件式を評価しようとしてpred()の呼び出しで何が起こるかです。

int pred(); // constexprではない述語

int f() constexpr [[ pre: pred() ]]; // OK; コンパイル時に呼ばれない

int main() {
  std::array<int, f()> a; // f()が定数式で呼び出される
  
  ...
}

この場合の振る舞いには次の4つのオプションが考えられます

  1. ill-formed
  2. ill-formed、ただし診断不要
  3. 契約条件式は無視される(定数評価中のみ)
  4. 契約条件式は無視するが、警告を発することを推奨する

さらに、このうちどのオプションを選択するかは実装定義とすることもできます。

2つ目のケースの場合については、例えば次のプログラムはどのように動作すべきでしょうか?

int f(int i) constexpr [[ pre: i > 0 ]];

int main() {
  std::array<int, f(0)> a; // 契約条件を満たさない定数式中の呼び出し
  ...
}

実行時の場合は契約条件がtrueに評価されない場合とは多岐にわたります

  • falseに評価された
  • 例外をスローした
  • longjmpを呼び出した
  • プログラムを終了させた
  • 未定義動作に陥った

実行時において契約違反となるのは最初の2つの場合のみです。また、そのセマンティクスもignore, observe, enforceの3つのいずれかが実装定義で選択(指定)され、それによって契約条件は無視されるか(ignore)、破られて違反ハンドラが呼ばれた後どうなるか(observe, enforce)、が変化します。

定数評価中はこの点が大きく異なり、定数評価中は契約アノテーションのセマンティクスを選択できず(どれが選択されているかはわからず)observe, enforceを区別する意味がありません。無視されずにtrueと評価されなかった場合はfalseに評価される以外に選択肢がありません。定数評価中は例外を投げたりlongjmpを呼び出したりできず、それが行われる場合はコア定数式ではないためそのセマンティクスの規定はケース1の領分となります。

そのため、このケース2のセマンティクスの指定は、契約条件式がコア定数式でありfalseに評価された場合についてのみ考慮するだけですみます。ケース1と同様に、ここでも4つの選択肢があります

  1. ill-formed
  2. ill-formed、ただし診断不要
  3. 契約条件式は無視される(定数評価中のみ)
  4. 契約条件式は無視するが、警告を発することを推奨する

ここでも、このうちどのオプションを選択するかは実装定義とすることもできます。

これらのことと設計原則(正しさ、柔軟さ、教えやすさ、コンパイル時間)を前提に、提案では3つのオプションを提示しています

  • オプションA : どちらもill-formedとする
    • 正しさを最も重視
    • チェックできない条件式は許可せず(ケース1)、契約違反を実行時よりもコンパイル時に補足できた方が望ましい(ケース2)
    • 教えやすさにも優れているが、柔軟性とコンパイル時間を犠牲にしている
    • 全ての契約条件が常にチェックされなければならず、それによるコンパイル時間の増大が許容できないものになる可能性がある
  • オプションB : どちらも契約条件式を無視する
    • コンパイル時間を最も重視
    • コンパイラは全ての契約アノテーションを定数評価中に無視し、契約条件は実行時にのみ評価される
    • 教えやすさにも優れているが、柔軟性と正しさを犠牲にしている
  • オプションC : どちらの場合のセマンティクスも実装定義とする
    • 柔軟性を最も重視
    • 正しさとコンパイル時間の間の妥協点を探るもので、コンパイラ(もしくはユーザー)がどちらを優先するかを選択できる
    • 教えやすさを犠牲にしており、ルールを複雑化するとともに契約の一部分を標準からコンパイラベンダの手に委ねてしまっている
    • コンパイラはその引数か独自のビルドモードによって、契約条件をコンパイル時にチェックするかしないか(正しさを優先するかコンパイル時間を優先するか)を選択できるようになる
      • また、コンパイル時と実行時でその指定を共通させることもさせないことも選択できるようになる

契約プログラミングの場合は正しさが最も重要な設計目標であるため、オプションAが最も望ましい選択肢です。しかし、それによってコンパイル時間の増大を許容できなくなったユーザーがマクロにラップしてその有効性をスイッチするようなことをし始めると、契約機能の採用に影響を及ぼす可能性があります。なぜなら、オプションAでは契約条件がコンパイル時に評価されないが実行時には評価される、ようなことを許可しないためです。

オプションBも正しさの観点から適切ではありません。コンパイル時間のために正しさを犠牲にし、結果得られるプログラムが未定義の振る舞いをするようになってしまったら意味がありません。標準は少なくともこのようなケースを診断できるべきです。

したがって、最も推奨されるのはオプションCとなります。コンパイラ引数等のスイッチが増えるのはやや残念な点ですが、これによって契約プログラミングを使用する全てのユーザーに契約を有効に使用するための選択肢を与えることができます。例えば、このオプションでは契約条件がコンパイル時に評価されないが実行時には評価される、といったことが許容されます。

P2896R0 Outstanding design questions for the Contracts MVP

契約プログラミングに関する未解決の問題について収集し、解決を促すための提案。

C++における現在の契約プログラミングに関する議論は、MVPと呼ばれる最小仕様を確立し、それをC++26に導入することを目指しています。そのタスクの残りの主要な論点は契約構文に関するものですが、それ以外にも小さいながらも解決すべき問題がいくつか残されています。

この提案は、それらの未解決の問題(設計上の疑問点)を収集してリスト化し、MVP仕様が確立される前に何らかの回答(SG21における投票や個別の提案の採択など)を求めるものです。

  1. 異なる翻訳単位の最初の宣言における契約について
    • 未解決、提案が必要
    • 現在のMVPでは、fが異なる翻訳単位で宣言されている場合、その契約は同一(identical)でなけれならない(そうでない場合診断不用のill-formed)とされているが、同一(identical)の意味が定義されていない。この定義が必要
    • 選択肢
      1. 同一(identical)の意味を定義し、これが実装可能であることを確認する
      2. 異なる翻訳単位の同じ関数の2つの宣言について、両方に契約がなされている場合をill-formed(診断不用)として問題を回避する。ただしこれは実用的ではない
  2. オーバーライドする関数とされる関数の契約について
    • 未解決、提案が必要
    • 現在のMVPでは、オーバーライドする関数はされる関数の契約を継承し、追加の契約を行えない。これについて異論があり、どうするかを選択する必要がある。
    • 選択肢
      1. なにもしない(現在のMVPのまま)
      2. MVPの制限を強め、オーバーライドする関数もされる関数も契約を行えないようにする
      3. 継承された契約をオーバーライドする機能などの仮想関数に対するより柔軟なソリューションを考案し、MVPを緩和する
  3. ラムダ式に対する契約と暗黙キャプチャについて
    • 未解決、P2890R0とP2834R1で提案済
    • 契約機能はラムダ式においても機能しなければならない。その際、ラムダの本体で使用されていないが契約指定で使用されている名前はキャプチャされるかどうか(契約に表れている名前がODR-usedであるかどうか)が未解決
    • 選択肢
      1. ラムダにおける契約は他の所と同じルールに従う。すなわち、契約条件式でのみ使用されている名前はキャプチャされる
        • P2890R0が提案している
      2. ill-formedとする。ラムダにおける契約条件式は、他の方法でキャプチャされない名前をキャプチャできない
        • P2834R1が提案している
      3. ラムダ式における契約機能を無効にする
  4. コルーチンにおける契約
    • 未解決、P2957R0で提案済
    • コルーチンに対する契約は通常の関数と同様に動作するのかが未解決
    • 選択肢
      1. コルーチンで事前・事後条件とアサーションを許可し、事前条件と事後条件のセマンティクスを指定する
        • P2957R0が提案し、セマンティクスについても提供している
      2. コルーチンではアサーションのみ許可する
      3. コルーチンでは契約機能は無効とする
  5. 定数式における契約について
    • 未解決、P2894R0で提案済
    • 定数評価中に契約条件は評価されるのか、どういうセマンティクスを持つのかが未解決。特に、契約条件式はコア定数式ではない場合と、契約条件式はコア定数式だがfalseに評価された場合にどうなるのかが問題。
    • 選択肢(2つの場合のどちらについても)
      1. ill-formed
      2. ill-formed、ただし診断不要
      3. 契約条件式は無視される(定数評価中のみ)
      4. 契約条件式は無視するが、警告を発することを推奨する
  6. トリビアルな特殊メンバ関数に対する契約について
    • 未解決、P2834R1で提案済
    • トリビアルな関数に契約がなされている場合、そのトリビアル性に影響するかどうかが未解決
    • 選択肢
      1. 契約が指定されていてもトリビアルのまま
      2. 契約が指定されていてもトリビアルのままだが、その結果として事前・事後条件がチェックされない可能性がある
        • P2834R1が提案している

この提案は、これらの問題の解決を2024年春の東京における全体会議までに解決するべきとしています。

P2905R2 Runtime format strings

std::make_format_args()が左辺値でフォーマット対象の値を受け取るようにする提案。

以前の記事を参照

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

P2909R0 Dude, where's my char?

std::format()charを整数値としてフォーマットする際の挙動を改善する提案。

std::format()ではcharの値をdxオプションによって整数値として出力することができます。ただ、この出力はintに暗黙変換して出力する形になっており、charの符号が実装定義であることを考慮していませんでした。

import std;

int main() {
  for (char c : std::string("🤷")) {
    std::print("\\x{:02x}", c);
  }
}

リテラルエンコーディングUTF-8だとすると、この出力はcharが符号付きか否かによって次のどちらかになります

\xf0\x9f\xa4\xb7
\x-10\x-61\x-5c\x-49

例えば、ARM環境は多くの場合charが符号付きとなります。

std::format()の暗黙的な設計意図として、同じ整数型と同じIEEE754浮動小数点数型についてプラットフォーム間で一貫した出力をする、というものがあります。しかし、現在の仕様だとcharがそのプロパティを破っています。

また、charstd::format()で出力するときは、元のテキストの部分テキストとして出力するか、そのビットパターンを出力するために使用されます。後者の場合、それは符号なし整数を通常意図しているはずで、プラットフォームによっては符号付き整数値として出力されることは意図しないはずであり、10進以外で出力している場合は特にそうでしょう。

この問題は{fmt}ライブラリにおいて報告され、既に修正済みです。この提案は、この修正をstd::format()にも適用しようとするものです。

提案では、charの値をb, B, d, o, x, Xオプションによって出力する際は対応する符号なし整数型(つまり、unsigned char)にキャストしてからフォーマットする、のように指定することでこの問題の解決を図っています。

この修正は実行時の振る舞いを静かに変化させる破壊的変更となりますが、単一のchar値をb, B, d, o, x, Xオプションによって出力する場合、かつcharが符号付き整数型であるプラットフォームにおいてのみ影響があります。変更が{fmt}ライブラリに適用され出荷されて以降この修正が問題となった報告はなく、std::format()の利用率はそれよりさらに低いため破壊的変更の影響はかなり小さいと考えられます。

LEWGにおける投票では、この修正をC++23に対するDRとすることに合意がとれています。

P2933R0 std::simd overloads for <bit> header

<bit>にあるビット演算を行う関数について、std::simd向けのオーバーロードを追加する提案。

std::simdはデータ並列型として、通常のスカラ型とほぼ同様に使用してハードウェアのSIMD演算を活用できるようにすることを意図したクラス型です。そのため、演算子オーバーロードや関数オーバーロードによって通常の四則演算や数学関数などを使用可能なようになっています。

この提案は、現在そこに欠けている<bit>のビット演算系関数のオーバーロードを追加しようとするものです。

この提案の対象は、C++23時点で<bit>ヘッダにあるもののうちstd::endianstd::bit_castを除いた残りの関数です。追加されるオーバーロードの制約は元の関数のものをstd::simdの要素型に対して要求し、効果はstd::simdの値の要素ごとに適用され、結果は入力と同じstd::simd<T, ABI>型の値として返されます。

ただし、countl_zero()bit_width()などのようにビットの状態をクエリする系の関数の戻り値型は通常のものはintとなっていますが、std::simdで同じようにすると結果の値のビット幅とストレージサイズが変化しパフォーマンスに影響を与える可能性があるため、これらの関数については入力の要素型と同じ幅の符号付き整数型を要素型とするstd::simd値を返すようにされています。

P2935R0 An Attribute-Like Syntax for Contracts

C++契約プログラミングのための構文として属性構文を推奨する提案。

C++における現在の契約プログラミングに関する議論は、MVPと呼ばれる最小仕様を確立し、それをC++26に導入することを目指しています。意味論面に関しては小さい問題がいくつか残されていますがそれは本質的な問題ではなく、残った大きな決定事項は契約構文に関するものです。

現在の契約プログラミングのための構文としては、C++20で一旦採用された際の属性構文の他、ラムダ式likeな構文、専用の区切り記号を利用するものなどの候補があります。この提案は、そのうちC++20で採用されたものとほぼ同様の属性構文によるものを提案しています。

提案されている構文の文法は次のようなものです

contract-checking-annotation :
  precondition-specifier
  postcondition-specifier
  assertion-specifier

precondition-specifier :
  [ [ pre : conditional-expression ] ]
postcondition-specifier :
  [ [ post postcondition-return-value-specifier(opt) : conditional-expression ] ]
postcondition-return-value-specifier :
  identifier
  ( identifier )
assertion-specifier :
  [ [ assert : conditional-expression ] ]

attribute-specifier :
  [ [ attribute-using-prefix(opt) attribute-list ] ]
  alignment-specifier
  precondition-specifier
  postcondition-specifier

assertion-statement :
  assertion-specifier ;

よりまとめると、契約アノテーションの構文は次のような形式をしています

contract-checking-annotation
  [ [ contract-kind metadata-sequence : evaluable-code ] ]

contract-kindにはpre post assertのいずれかが指定でき、metadata-sequenceには事後条件における戻り値のキャプチャのみが置けます。evaluable-codeは文脈に応じてboolに変換される通常のC++の式、すなわちconditional-expressionを置くことができます。

この構文の利用例

int f(const int x, int y)
  [[ pre : y > 0 ]]
  [[ post : fstate() == x ]]  // 事後条件から参照される仮引数名はconstでなければならない
  [[ post r : r > x ]]        // postの後に変数名を指定することで戻り値を参照する
  [[ post (r) : r > x ]]      // 戻り値名は()で括っても良い
{
  [[ assert : x > 0 ]]; // Assertions form a complete statement.
  return x;
}

事前条件と事後条件は関数ではなく関数の型に関連する属性が配置されるのと同じ場所に配置され、noexceptの後かつ後置戻り値型の前に配置されます。

struct S1 {
  auto f() const & noexcept [[ pre : true ]] -> int;
  
  virtual void g() [[ pre : true ]] final = 0;

  template <typename T>
  void h() [[ pre : true ]] requires true;
};

ラムダ式の場合も同様になります。

auto w = []         [[pre: true]]        { return 6; };
auto x = [] (int x) [[pre: true]]        { return 7; };
auto y = [] (int x) [[pre: true]] -> int { return 8; };

事前条件と事後条件の数や順序に制限はなく、好きな順番で好きな数追加できます。

std::pair<double,double> clamp2d(double x, double y,
                                 const double minx, const double maxx,
                                 const double miny, const double maxy)
  // Check the x-dimension range.
  [[ pre : minx <= maxx ]]
  [[ post r : r.first >= minx && r.second <= maxx ]]
  // Check the y-dimension range.
  [[ pre : miny <= maxy ]]
  [[ post r : r.second >= miny && r.second <= maxy ]];

assertion-statementはそれ単体で1つの文を形成し、ブロックスコープの内側でのみ使用可能となります。ただし、assertion-specifierそのものにはそうした制限はなく、将来的にvoid型の式として式に付随させることを許可することを妨げません。

struct S2 {
  int d_x;
  S2(int x)
    : d-x( [[ assert : x > 0 ]], x ) // error、ただし将来的に許可する事は可能
  {}
};

P2944R1 Comparisons for reference_wrapper

reference_wrapperに比較演算子を追加する提案。

以前の記事を参照

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

P2951R2 Shadowing is good for safety

変数のシャドウィングを活用した、安全性向上のための言語機能の提案。

以前の記事を参照

このリビジョンでの変更は、5つ目の提案を追加したことです。

5つ目の提案は、2つ目の提案を範囲forで特化させたもので、イテレート対象の範囲をその内部で暗黙的にconstシャドウィングを行うものです。

#include <string>
#include <vector>
#include <optional>

using namespace std;

int main() {
  vector<string> vs{"1", "2", "3"};

  [[checked]]
  for (auto &s : vs) {
    // 暗黙的にconstシャドウィングされる
    //const vector<string>& vs = vs;
  }
  
  // 代替構文
  cfor (auto &s : vs) {
    //const vector<string>& vs = vs;
  }

  return 0;
}

この例のvsのようにイテレート対象の式が単純な変数名であればこの提案の実装には問題がありませんが、ここにはより複雑な式が来る可能性があります。その時に同じことをしようとすると、より複雑な組み合わせのパターンマッチングが必要になります。そのため、実装の選択肢は3段階のチェックに分かれます。

  1. vsのように単純な式(変数名)のみ自動シャドウィングする
  2. vs.member.function().memberのような複雑な式が、その範囲forのスコープ内で非constとして使用されないようにする
  3. 式の組み合わせがその範囲forのスコープ内で非constとして使用されないようにする

2と3の例

// 2の例
[[checked]]
for (auto &s : vs.member.function().member) {
  // 非constで`vs.member.function().member`を使用できない
  vs.member.function().member.non_const_method(); // error
}

// 3の例
cfor (auto &s : vs.member.function().member) {
  auto first2 = vs.member;
  auto remainder = first2.function().member;
  
  // 非constで`vs.member.function().memberと同等のものを使用できない
  remainder.non_const_method(); // error
}

P2952R0 auto& operator=(X&&) = default

defaultな特殊メンバ関数の戻り値型をauto&で宣言できるようにする提案。

現在のC++では、= defaultは特定のシグネチャと戻り値型を持つ関数にのみ使用できます。そのような関数の戻り値型は変更することができ、そのコンテキストから特定可能な型ですが、現在の規定では<=>を除いて戻り値型に対してautoを使用できません。

これにより、例えば代入演算子の実装において冗長な繰り返しが避けられません。

struct ForwardDiffView {
  ...
  
  ForwardDiffView(ForwardDiffView&&) = default;
  ForwardDiffView(const ForwardDiffView&) = default;
  ForwardDiffView& operator=(ForwardDiffView&&) = default;
  ForwardDiffView& operator=(const ForwardDiffView&) = default;
  
  ...
};

また、比較演算子<=>とこの点において一貫していません。

auto operator<=>(const MyClass& rhs) const = default; // well-formed
auto operator==(const MyClass& rhs) const = default;  // ill-formed、boolでなければならない
auto operator<(const MyClass& rhs) const = default;   // ill-formed、boolでなければならない

これらのことはまた、defaultではない場合とも一貫していません。

auto& operator=(const MyClass& rhs) { i = rhs.i; return *this; } // well-formed
auto& operator=(const MyClass& rhs) = default; // ill-formed、'MyClass&'でなければならない

auto operator==(const MyClass& rhs) const { return i == rhs.i; } // well-formed
auto operator==(const MyClass& rhs) const = default; // ill-formed、'bool'でなければならない

この提案は、これらの例のように現在行えない特殊メンバ関数の戻り値型におけるautoの使用を許可しようとするものです。

この場合のautoの示す型名は、特定の許可される型のセットを定義してそこから選択するのではなく、仮想のretrun文を返す関数としての通常の戻り値型推論によって推定されます。

auto戻り値型を持つdefaultな関数宣言は、その関数の種別によって次のような値を返す架空のreturn文から推論された型を持ちます

  • bool型のprvalue : operator==operator!=operator<operator>operator<=operator>=
  • 共通の比較カテゴリ型Q型のprvalue : operator<=>
  • Cの左辺値 : クラス型/共用体Coperator=

その後、このように取得された戻り値型が標準で許可されている戻り値型と一致するかを調べ、一致するならwell-defined、一致しないならill-formedとなります。

コピー代入演算子の例

struct MyClass {
  auto& operator=(const MyClass&) = default;          // Proposed OK: deduces MyClass&
  decltype(auto) operator=(const MyClass&) = default; // Proposed OK: deduces MyClass&
  auto&& operator=(const MyClass&) = default;         // Proposed OK: deduces MyClass&
  const auto& operator=(const MyClass&) = default;    // Still ill-formed: deduces const MyClass&
  auto operator=(const MyClass&) = default;           // Still ill-formed: deduces MyClass
  auto* operator=(const MyClass&) = default;          // Still ill-formed: deduction fails
};

等値比較演算子の例

struct MyClass {
  auto operator==(const MyClass&) const = default;           // Proposed OK: deduces bool
  decltype(auto) operator==(const MyClass&) const = default; // Proposed OK: deduces bool
  auto&& operator==(const MyClass&) const = default;         // Still ill-formed: deduces bool&&
  auto& operator==(const MyClass&) const = default;          // Still ill-formed: deduction fails
};

また、提案ではこの変更に伴って発見された既存の特殊メンバ関数(特に== <=>)の宣言の微妙な差異によるバグやベンダ間の差異について明確となるようにを修正することも提案しています。

P2953R0 Forbid defaulting operator=(X&&) &&

右辺値修飾されたdefault代入演算子を禁止する提案。

明示的にdefaultで宣言される代入演算子は、その参照修飾(CV修飾は禁止されている)や戻り値型等の微妙な違いによる宣言が許可されており、それによって暗黙のdefault宣言で導入されるものとは異なったシグネチャdefault代入演算子を宣言可能になっています。

その中でも最も意味がないと思われるのが、右辺値参照修飾されたdefault代入演算子の宣言です。

struct S {
  // どちらも、今日可能なdefault宣言
  S& operator=(const S&) && = default;
  S& operator=(S&&) && = default;
};

これには次のような問題があります

  • このような無意味な宣言の可能性は、C++の理解を困難にさせる
  • このような宣言を許可するための標準の文言は、禁止する場合に比べて複雑になる

この提案では、この右辺値参照修飾されたdefault代入演算子の宣言を禁止することを提案しています。

この提案による恩恵はまた、明示的オブジェクトパラメータとP2952(1つ前の節)を採用した場合のコーナーケースを潰すのにも役に立ちます

struct C {
  auto&& operator=(this C&& self, const C&) { return self; }
    // 現在: OK, 戻り値型はC&&が推論される
    // P2952採択後: OK, 戻り値型はC&&が推論される(変わらない)
    // この提案: OK, 戻り値型はC&&が推論される(変わらない)

  auto&& operator=(this C&& self, const C&) = default;
    // 現在: Ill-formed, 戻り値型にプレースホルダを使用できない
    // P2952採択後: OK, 戻り値型はC&が推論される
    // この提案: 削除される、オブジェクトパラメータはC&ではない
};

この提案がない場合、この2つの同じ意味と思われる宣言はP2952の採択後に異なる型を推論するようになり、一見するとこの推論は矛盾しているように思えます。P2952とこの提案を同時に採択することによってこのコーナーケースを軽減することができます。

なお、この提案では該当する代入演算子をill-formedとするのではなく、既存の振る舞いに従って暗黙deleteとすることを提案しています。

struct S {
  // この提案の後、どちらもdeleteされる
  S& operator=(const S&) && = default;
  S& operator=(S&&) && = default;
};

P2954R0 Contracts and virtual functions for the Contracts MVP

現在のC++契約プログラミング仕様に対して、仮想関数に関する契約についてを規定する提案。

仮想関数のオーバーライドに伴う契約の指定に関しては、EiffelにおいてAssertion Redeclaration ruleと呼ばれる規則が確立されています。それは

  1. 事前条件は、継承元と同じかより弱い(緩い)ものである必要がある
  2. 事後条件は、継承元と同じかより強い(厳しい)ものである必要がある

というものです。

C++20の契約仕様においてもこれをベースとして、事前条件と事後条件はオーバーライド元と先で同一となることを指定していました。

  • オーバーライドする関数に契約を指定する場合、オーバーライドされる関数の契約指定と同じものを指定する
  • 関数が複数の関数をオーバーライドする場合、オーバーライドされる関数は全て同じ契約指定を持つ必要がある

現在C++26へ向けて進行中の契約仕様(MVP)においては

  • オーバーライドする関数には契約指定を行うことはできず、オーバーライドされる関数の契約指定を継承する
  • (関数が複数の関数をオーバーライドする場合は同一)

となっており、少し厳しくなっています。

この点に関してはさまざまな意見があるようで、特に、契約指定の同一性(あるいは契約の強弱や広さの定義)をどう判断するのかや、複数の関数をオーバーライドする場合に基底の関数全てに同じ契約指定を要求することが正しいのかについて合意がとれてはいないようです。

この提案は、現在のMVP仕様をC++26に間に合わせるためにも、この点について次のようにすることを提案しています

  • 関数をオーバーライドする仮想関数には契約指定を行えず、オーバーライドされる関数の契約指定を継承する
  • 関数が複数の関数をオーバーライドする場合、オーバーライドされる関数は契約指定を持ってはならない

これは、適切であり将来の互換性に影響がない部分(オーバーライド後関数は元の関数と同一の契約を持つこと)を許可し、現在異論があり意見の一致を見ていない部分(契約条件の同一性の定義、複数の基底関数への契約の強制について)を不許可(ill-formed、要診断)にしようとするものです。これによって、C++26契約仕様としては契約を持つ仮想関数のオーバーライドを可能にしつつ、それはAssertion Redeclaration ruleに従ったものになります(契約条件は基底関数のコンテキストにおいて同一)。現在異論がある部分については、実装経験などを見ながら将来的に決定することを目指します。

例えば、オーバーライドする関数は基底クラスの関数とは全く異なるコンテキスト及びスコープで定義される可能性があるため、トークンの同一性は契約条件の同一性を意味しません。ODRの同一性に関しは実装経験が乏しく(一応、GCCの契約実装がこれをおこなっているらしい)、それがユーザーにとってどのような影響をもたらすのかは不透明です。これを診断不要のill-formedにしてしまうと、契約仕様の理解に混乱をもたらします。そのため、明確にill-formedとしておくことで問題を分離し、単純化した振る舞いをC++26に間に合わせることができます。

GCCの契約実装はODRベースで契約指定の同一性を判断するようですが、これは複数の関数をオーバーライドしている場合には行われないようで、その点に関しても合意や実装経験はありません。そのため、これもill-formedとしておくことで振る舞いを単純化し、将来のために設計スペースを空けておくことができます。

P2955R0 Safer Range Access

std::vector等コンテナ向けの安全な要素アクセス関数の提案。

std::vectorを始めとするコンテナ(std::dequestd::stringstd::spanなどなど)は要素アクセスが安全であるとはいえません。.at()以外のアクセス方法は範囲外アクセスをチェックしておらず、.at()の利用率は低いため要素アクセスの大部分は範囲外参照チェックなしで行われています。

また、.at()も戻り値が要素への直接の参照であるため、参照無効化・ダングリング参照の危険性を孕んでいます。

この提案は、値セマンティクスに基づく関数を提供することで、範囲外アクセスやダングリング参照によるエラーを低減させることを目指すものです。

現在のC++の要素アクセスに共通する課題として、次のようなものがあります

  1. C++の添字演算子は参照セマンティクスを持つ傾向にある
    • 値の取得と設定それぞれに特化した専用の演算子がないためと考えられる
  2. C++の添字演算子はインデックスの一部ではない追加の引数をサポートしない
    • 例えばstd::source_location
  3. std::optional, std::variant, std::expectedは参照をサポートしない

この課題を回避するために、この提案では演算子やプロクシオブジェクトを使用する代わりにメンバ関数を使用しています。

まず追加するのは、与えられたインデックスが有効化どうかを判断する.test()関数です。

[[nodiscard]]
[[safe]]
constexpr bool test( std::vector<T>::size_type pos ) const;

この関数は安全に実装できます([[safe]]は新規属性ではなく、この提案内において安全/安全ではないことを示すマーカーです)。

次に追加する2つの関数は安全なものではありませんが、この提案のメインである安全な関数群を提供するためのベースとなるものです

[[nodiscard]]
[[unsafe(reason=["range", "dangling_reference", "reference_invalidation"])]]
constexpr reference get_reference( size_type pos ) noexcept;

[[nodiscard]]
[[unsafe(reason=["range", "dangling_reference", "reference_invalidation"])]]
constexpr const_reference get_reference( size_type pos ) const noexcept;

ここでの[[unsafe]][[safe]]同様に説明のためのマーカーです。これらの関数は指定されたインデックスに対応する要素の参照を取得するものです。

これらをベースとした安全な関数群は、次のような設計理念によって分類されます

  1. [], at(), front(), back()等範囲アクセス関数に対しては、ゲッター(値を受け取る)とセッター(値を返す)とに分離する
  2. 分離されたゲッターとセッター関数ごとに、エラーを処理する方法に応じたバリアントが追加される
    • 例外送出
    • デフォルト値を返す
    • std::optionalを返す
    • 終了する
    • 何もしない(つまり、無効)
    • 追加する

front()/back()に対応する関数は例えば

取得/設定 エラー処理の方法 front() back()
get get_value get_front_value get_back_value
get get_optional get_front_optional get_back_optional
get get_or_terminate get_front_or_terminate get_back_or_terminate
set set_value set_front_value set_back_value
set set_or_terminate set_front_or_terminate set_back_or_terminate
set set_and_crop set_front_and_crop set_back_and_crop
set set_and_grow set_front_and_grow set_back_and_grow

ゲッター関数は何も受け取らず値を返し、セッター関数は値を受け取り何も返しません。

要素を引き当てその参照に対して何かをしたい(値の更新やメンバ呼び出しなど)場合のために、ゲッター/セッターの分離はtransform/visitに置き換えられ、対応する関数が追加されます

取得/設定 エラー処理の方法 front() back()
transform transform_value transform_front_value transform_back_value
transform transform_optional transform_front_optional transform_back_optional
transform transform_or_terminate transform_front_or_terminate transform_back_or_terminate
visit visit_value visit_front_value visit_back_value
visit visit_or_terminate visit_front_or_terminate visit_back_or_terminate
visit visit_and_crop visit_front_and_crop visit_back_and_crop
visit visit_and_grow visit_front_and_grow visit_back_and_grow

transform系関数はインデックス値とそれによって引き当てた要素に対して何かする関数を受け取り、その関数の戻り値を(非参照で)返します。visit系関数は新規追加する値と追加後の要素に対して何かする関数を受け取り、何も返しません。

これらの関数は多岐に渡りますが、実際にはコンテナ種別によってやることは変わらないため(最初に追加した2種類の関数をベースとするため)、実際にはメンバ関数ではなくフリー関数として追加することが望ましいです。それによって、std::vector以外のコンテナでも同じように利用できるようになります。

提案には、このように追加される関数のstd::vectorに追加する場合の宣言例と実装例があります(長いのでコピペしません)。

この提案のメリットとしては

  • 範囲アクセス関数のエラーを減らす
  • 実行時のダングリング参照の発生を減らす
  • 実行時の参照無効化エラーの発生を減らす
  • プログラマが例外を扱う際に不必要な動的確保を減らす

などがあります。

P2956R0 Add saturating library support to std::simd

P0543で提案されている整数型の飽和演算関数群にstd::simdオーバーロードを追加する提案。

std::simdはデータ並列型として、通常のスカラ型とほぼ同様に使用してハードウェアのSIMD演算を活用できるようにすることを意図したクラス型です。そのため、演算子オーバーロードや関数オーバーロードによって通常の四則演算や数学関数などを使用可能なようになっています。

P0543は整数型向けの飽和演算のためのライブラリ関数を提案するもので、C++26に採用される予定です(まだ承認前)。そこでは整数型のためのオーバーロードのみを提供しています。

この提案は、そこにstd::simd向けのオーバーロードを追加しようとするものです。

提案されているオーバーロードでは、対象となる飽和演算は整数型を要素型とするstd::simdの要素ごとに実行されます。

P2957R0 Contracts and coroutines

コルーチンに対して契約を有効化した場合に、各種の契約がどのように動作するのかについての提案。

コルーチンは、その関数定義内にコルーチン関連の3種類のキーワード(co_return, co_yield, co_await)のいずれかが含まれている場合にその関数はコルーチンとなります。そのため、宣言だけからあるいは呼び出しだけからその関数がコルーチンであるかどうかを知ることはできません。

// コルーチンかもしれないし普通の関数かもしれない
awaitable<int> session(int id);

// 普通の関数
awaitable<int> default_session() { 
  awaitable<int> s = session(0); // コルーチン呼び出しかもしれないし普通の関数呼び出しかもしれない
  return s;
}

default_session()は定義があるため普通の関数であることが分かりますが、session()は宣言のみであるためどちらかは分かりません。

コルーチンを呼び出したとき、呼び出し側から見るとその呼び出しはコルーチンを起動しその戻り値オブジェクトを返すためのファクトリ関数(ramp関数と呼ぶらしい)を呼び出しいています。そして、このramp関数は契約を持つことができます(例えば上記例ではsession()idが正であることなど)。

通常の関数とコルーチンの大きな違いはこの後、呼び出し側に戻った時に関数本体の実行が完了し終了しているかどうかです。通常の関数は確実に実行は終了しており、引数も含めた関数内部の状態は開放済みです。一方で、コルーチンではまだ終了しておらず単に中断しているだけの可能性があり、その場合関数内部状態はコルーチンステート内部に保存されています。そのため、コルーチンの観測可能な副作用はその最初の呼び出しがreturnした後、任意のタイミングで変化し得ます。

したがって、ramp関数がreturnした後その戻り値オブジェクトのいくつかの状態について何か期待を持っていたとしても、それは通常の関数に対するものよりも制限されるでしょう。

このように、コルーチンは通常の関数を一般化したもので、契約注釈が通常の関数と同様に動作するのかどうか(つまりその意味論について)はいくつかの疑問があります。この提案は、3種類の契約注釈がコルーチンにおいてどのように振舞うべきかを提案するものです。

とはいえ、アサーションに関してはあまり疑問は無いでしょう。アサーションが配置されるのはコルーチンの本体であり、それはコルーチンの実装詳細です。アサーションは現在のassertマクロと同様に、実行がそこに到達したときに評価されることが期待されます。

残った2つ事前条件と事後条件はそれぞれ、関数が呼び出された時に期待すること、関数が正常にreturnしたときに関数が保証すること、を表しています。これらがチェックされるべき非コルーチン通常関数における適切な場所は次の場所です

  1. 事前条件 : 関数の呼び出し時、関数引数が初期化された直後
  2. 事後条件 : 関数が正常にreturnする時、戻り値が初期化され自動変数が破棄された後、関数引数が破棄される前

事前条件の実行時チェックによって関数の引数の予期しない組み合わせが関数本体で使用されないことが保証され、事後条件の実行時チェックによって呼び出し元が後続の処理を実行するにあたって望ましい状態のオブジェクト(またはプログラムの他の部分の状態)を確実に保証できるようになります。

awaitable<int> cancelable_session(int id) 
  [[post r: is_cancelable(r)]];
  
template <typename T>
void manage(awaitable<T> session)
  [[pre: is_cancelable(session)]];
  
void test()
{
  awaitable<int> session = cancelable_session(1);
  // 契約を順守してmanage()を呼び出していることを確認するには
  // is_cancelable(session)がtrueであることを保証する必要がある
  manage(session);
}

したがって、事前条件と事後条件は関数の呼び出し側と呼び出される側の関係性であり、呼び出し側は呼び出す関数のシグネチャのみを認識しその本体を認識しません。呼び出し側は呼び出しているものがコルーチンなのか関数なのかを知りません。

このようなことを前提とすると、コルーチンにおける事前条件のセマンティクスは、関数引数が初期化された直後、その引数が使用される可能性のあるコルーチンの初期アクションが実行される前、にチェックされることが最も適切です。

事後条件は呼び出されたコルーチンが呼び出し元に戻るタイミングが2種類あるために、より複雑になります。

通常の関数では、事後条件は戻り値に関して要求されることが多く、戻り値は関数の最後に作成されるため、事後条件が関数の最後に確立されると期待するのは自然です。そこでは、関数の終了時に何が起こるか?と、関数が呼び出し元にreturnしたときに何が起こるか?は同じ意味です。しかし、コルーチンの場合はその2つを区別する必要がある場合があり、その場合は後者のみが呼び出し側に関係します。

コルーチンとよく関連する非同期処理の観点から見ると、非同期処理Aを呼び出す呼び出し元は、呼び出したAの実行結果には関心が無い場合がほとんどで、Aから戻り値の形で処理の成果を取得するわけではありません。

Aの呼び出し元はAが成功したかどうか及びAの処理成果を何らかのグローバル状態やコールバックなどの形でのみ受け取ります。そのため、Aの処理が成功しておりその成果が得られていれば、Aがどのように終了したか(Aの終了時に何が起こったか)は呼び出し側には関係がありません。この場合にもやはり、関数(A)の終了時に何が起こるか?は呼び出し元には関係の無い性質です。

コルーチンによって非同期処理を行う場合、コルーチンの呼び出しにはタスクを非同期機構に投入するramp関数を呼び出す小さな初期の同期部分が存在します。呼び出し側は、この部分の成否には関心がある場合があります。つまり、ramp関数が処理を非同期機構に投入することに成功したかどうかを知りたい場合があります。

awaitable<int> cancelable_session(int id) 
  [[post r: is_cancelable(r)]];
  
void caller()
{
  awaitable<int> s = cancelable_session(1);
  [[assert: is_cancelable(s)]];
  global_cancelable_sessions.push(std::move(s));
}

これは単純な例ですが、コルーチンがどのように終了したかよりも、コルーチンによって非同期処理が開始されたかどうか(ramp関数が成功したかどうか)に関心がある場合のものです。

ただ、コルーチンはその呼び出し時点の状態によっては、初期サスペンドポイントで中断せずに続行しそのまま終了することもできます。つまり、コルーチンは完全に同期的に(通常の関数と同じように)終了することができます。その内部状態や観測可能な副作用が何らかの方法で既知であるとは考えられないため、このことはコルーチンの事後条件がその直接の戻り値オブジェクトによって表される状態に限定されることを示唆しています。

そうはいっても、コルーチンの最後に何が起きたのかを知りたい場合もあるでしょう。しかし、コルーチンの終了はco_return文の直接の到達によるものだけではなく(中断や例外送出など)、そのような終了に対する何らかの条件は呼び出し側と呼び出された側の合意を表すものではなく、別の種類の契約になります。

呼び出し側はコルーチン内部状態を推定できないため、呼び出し側がコルーチンの終了点の状態についてを実行時チェックで検証できることを保証できません。従って、コルーチンの終了点(中断点)における事後条件(コルーチン状態に関する仮想的な保証)はアサーションの方法でコルーチン内部でチェックされるべきです。

事後条件は呼び出し側に関連するものであり、呼び出し側は関数の宣言と呼び出し式そのものしか見えないため、呼び出しているのがコルーチンなのかどうかを知ることはできません。また、現在の契約プログラミング仕様では、関数の事後条件は呼び出し側でもその関数の終了直後に契約条件を評価することによって行える必要があります。

awaitable<int> cancelable_session(int id) 
  [[post r: is_cancelable(r)]];
  
void caller() {
  awaitable<int> s = cancelable_session(1);
  // ここで事後条件を検証できるはず
  ... 
}

これらのことからこの提案では、コルーチンに対する事後条件を許可し、それはコルーチンのramp関数を呼び出した直後に評価され、ramp関数の呼び出しが終了した時点のプログラムの望ましい状態を記述するものとすることを提案しています。コルーチンに対する事後条件の評価は、コルーチン呼び出しの結果オブジェクトの初期化の直後に順序付けられ、コルーチンローカルの状態の破棄に対しては順序付けされません。事後条件から参照される関数引数は、コルーチンフレームにコピーされたものではなく、元の関数引数を参照します。

言い換えると、コルーチンに対する事前条件と事後条件の指定は、そのコルーチンの呼び出しが類似の契約条件指定を持つファクトリ転送関数にラップされた場合と同じように動作する必要があります。

awaitable<int> f1(int i)  // coroutine
  [[pre: p(i)]]
  [[post r: q(r)]];
  
awaitable<int> f2(int i); // coroutine

awaitable<int> ff2(int i)
  [[pre: p(i)]]
  [[post r: q(r)]];
{
  return f2(i);
}
  
void caller() {
  // これら2つの関数呼び出しは契約条件評価に関して類似したセマンティクスを持つ
  f1(1);
  ff2(1);
}

P2958R0 typeof and typeof_unqual

C23に導入されたtypeoftypeof_unqualC++でも有効化する提案。

C言語の次期バージョンであるC23には、長らく実装の拡張として存在してきたtypeof演算子が導入され、そこから修飾情報を取り除いた型を得るtypeof_unqualも同時に導入されました。

typeof演算子は既にCで幅広く使用されており、C++とのコード共有部分においても使用されています。C++は標準としてはtypeof演算子を持たず、typeof_unqualは実装の拡張としても存在しなかったため、その存在がC23とのコード共有部分において問題となります。

typeof演算子のやることはdecltypeとほぼ同じであるためマクロによるラップで単純には置換できますが、Cの式には値カテゴリの概念が無いことから結果が異なる場合がある他、typeof/typeof_unqualには式だけではなく型名も渡せるという微妙な違いがあります。

そのため、C23とのコード共有部分においては、C++のコードとしてtypeofを完全には近似できません。

この提案は、C23との親和性向上のために、C++でもC23の意味論に沿ったtypeof/typeof_unqual演算子を追加しようとするものです。

P2960R0 Concurrency TS Editor's report for N4956

Concurrency TS v2の最新のワーキングドラフト(N4956)の変更点をまとめた文書。

おわり

この記事のMarkdownソース

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

文書の一覧

全部で106本あります。

もくじ

N4955 WG21 2023-06 Admin telecon minutes

2023年6月に行われた、WG21管理者ミーティングの議事録

N4957 WG21 February 2023 Minutes of Meeting

2023年6月に行われた、WG21全体会議の議事録

P0260R6 C++ Concurrent Queues

P0260R7 C++ Concurrent Queues

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

以前の記事を参照

R6での変更は

  • ターゲットとなるConcurrency TSのリビジョンなどについて追記
  • TSに入れると仮定すると浮かぶ疑問について追記
  • 非同期インターフェースを追加

などです。

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

  • LEWGのフィードバックを反映
    • system_errorからconqueue_errcを導出する
    • Rangeコンストラクタの追加
  • capacity()の追加
  • 既存の実装例としてTBBのconcurrent_bounded_queueを追加
  • pop()APIに関する議論を別の提案に分離

などです。

R6で追加された非同期インターフェースはsenderを返す次のようなものです

sender auto queue::async_push(T x);
sender auto queue::async_pop();

これらは要素をpush/popする操作を表すsenderを返し、そのsenderが実行されるまでは実際にpush/popは行われません。また、このsenderはキャンセルをサポートしています。

P0543R3 Saturation arithmetic

整数の飽和演算を行うライブラリ機能の提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けて、事前条件に違反した場合に定数式ではないという指定に関するコメントをdiv_satに追加したことです。

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

P0843R7 inplace_vector

P0843R8 inplace_vector

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

以前の記事を参照

R7での変更は

  • 名前をstatic_vectorからinplace_vectorへ変更
  • try_push_back()T*を返すように変更
  • push_back()が条件付きでstd::bad_allocをスローするように変更
  • value_typeがtrivially-copyableであればinplace_vectorも trivially-copyableとなることを明記
  • inplace_vectorをどのヘッダに配置すべきかについてのLEWGの投票をリクエス
  • push_back()が参照を返すように変更

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

  • <inplace_vector>に配置することにLEWGの合意が取れた事を追記
  • 機能テストマクロの追加
  • try_push_back()/unchecked_push_back()を提案する文言に追加
  • Rangeコンストラクタと代入演算子を追加
  • reserve()capacity()を超えた場合に例外をスローするように変更
  • shrink_to_fit()(なにもしない)を追加
  • insert_range()を追加
  • ムーブコンストラクタとデストラクタのトリビアル性について追記(Tトリビアルならトリビアルになる)
  • capacity()を推定できないため推論補助を削除
  • erase()/erase_if()を追加
  • 設計選択時のLEWGにおける投票結果を追記
  • operator==/operator<=>をHidden friendsに変更
  • <inplace_vector>のフリースタンディング指定を解除(それに関しては別の提案で議論する)

などです。

P0901R11 Size feedback in operator new

::operator newが実際に確保したメモリのサイズを知ることができるオーバーロードを追加する提案。

以前の記事を参照

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

  • new式のサポートを削除
  • return_size_tvoid*メンバをもつ非テンプレートクラスに変更
  • この機能がstd::allocatorの詳細やフリー関数として提供するのではなく、::operator newとして提供する必要がある理由について追記
  • 実装と導入の経験、および動的確保への影響にかんするレポートを追加

などです。

このリビジョンでの変更によって、new式から直接return_size_tを取るオーバーロードを呼び出せなくなりました。

#include <new>

int main() {
  auto [ptr, size] = new(std::return_size) int[5];                        // ng
  auto [ptr, size] = ::operator new(decltype(int[5]), std::return_size);  // ok
}

new式はメモリを確保した後その領域に指定された型のオブジェクトを構築し、対応するdelete式はオブジェクトを破棄した後メモリを開放しますが、それらの仕組みはreturn_size_tを取るオーバーロードによって得られる余剰サイズの領域にあるものについて感知しません。

メモリの解放は自動で行われますが、オブジェクトの構築と破棄の責任はプログラマにありそれは手動で行わなければならなくなります。

// 以前のリビジョンにおけるnew式の使用例

// メモリの確保
// T[5]の領域にはオブジェクトが構築される
auto [p, sz] = new (std::return_size) T[5];

// 余剰領域のオブジェクトを構築
for (int i = 5; i < sz / sizeof(T); i++) {
  new (p[i]) T;
}

// 確保した領域全体にあるオブジェクトを使用
for (int i = 0; i < sz / sizeof(T); i++) {
  p[i].DoStuff();
}

// 余剰領域のオブジェクトを破棄
for (int i = 5; i < sz / sizeof(T); i++) {
  p[i].~T();
}

// メモリの解放
// T[5]の領域にあるオブジェクトは破棄される
delete[] p;

これは冗長となり間違えやすいため、このリビジョンでnew式を使用してreturn_size_tを取る::operator newを呼び出せなくなりました。

::operator newを使用する場合はメモリの確保だけが行われ、オブジェクトの構築・破棄は全てプログラマの責任となるため、手動でそれを管理する必要があるにしても領域によって場合分けする必要はありません。

P1030R6 std::filesystem::path_view

パス文字列を所有せず参照するstd::filesystem::path_viewの提案。

以前の記事を参照

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

  • ロケールを取る全てのオーバーロードを削除
  • 一貫していないostreamのフォーマッタを修正
  • .render()を削除
  • render_zero_terminated()がフリー関数であるように修正
  • rendered_path()の生存期間のセマンティクスを明確化
  • 名前付き要件ではなくpath-view-like型によってpath_viewと同様の型を受け取るオーバーロードを指定
  • LEWGのメモを一部を除いて削除
  • 機能テストマクロの更新
  • render_null_terminated/render_unterminatedから誤った引数を削除
  • render_null_terminated/render_unterminatedの文言を追加
  • 必要に応じて‘implementation defined’を‘see later normative wording’に置き換え
  • 現在const path&を取っているオーバーロードを参考にnoexceptを付加
  • path(path_view)path_view::operator<<のセマンティクスを指定
  • path_view_fragmentにハッシュサポートを追加

などです。

P1324R1 RE: Yet another approach for constrained declarations

制約付きのautoによる関数宣言構文において、そのテンプレートパラメータ名を直接導入できる構文の提案。

この提案のモチベーションは以前に紹介したP2677R0と共通なので、以前の記事を参照

P2677ではauto:Tのような構文でテンプレートパラメータ名を導入しようとしていましたが、この提案はvoid sort(Sortable auto& c);void sort(Sortable S& c);のように書けるようにすることで、autoの代わりに直接テンプレートパラメータ名を導入しようとするものです。

namespace present {
  // 現在の制約付きauto関数テンプレート
  void f(Sortable auto&& x) {
    using S = decltype(x); // テンプレートパラメータ名Sを取り出す
    // use S
  }
}

namespace p2677 {
  // P2677提案の制約付き関数テンプレート
  void f(Sortable auto:S&& x) // テンプレートパラメータ名Sが導入される
  {
    // use S
  }
}

namespace p1324 {
  // この提案による制約付き関数テンプレート
  void f(Sortable S&& x) // テンプレートパラメータ名Sが導入される
  {
    // use S
  }
}

この提案ではさらに、この構文を戻り値型制約や変数宣言に対する制約にまで広げています

// 現在
void f(Sortable auto x);
Sortable auto f(); 
Sortable auto x = f(); 
template <Sortable auto N>void f();

// この提案
void f(Sortable S x);             // 関数引数のテンプレートパラメータ導入
Sortable S f();                   // 関数戻り値型のテンプレートパラメータ導入
Sortable S x = f();               // 変数宣言時の型名エイリアス導入
template <Sortable S N> void f(); // NTTP宣言時のテンプレートパラメータ導入

ただし、コンセプト パラメータ名 変数名、のような宣言であるため、変数名を省略するとコンパイルエラーになります。

// Numberはコンセプトとする

void f(Number N);     // ng、Numberが型名ではない
void f(Number auto);  // ok、テンプレートパラメータ名も変数名も導入されない
void f(Number N __);  // 別の提案(P1110)で提案されていたプレースホルダ

この制限によって、通常の関数とジェネリックな関数が混同されることが無くなります

void f(Foo V)     // 2id: 常に通常の関数宣言
void f(Foo F V)   // 3id: ジェネリック関数(この提案)
void f(Foo auto)  // 1id + auto: ジェネリック関数

この宣言によって導入されたテンプレートパラメータ名は以降の引数宣言のために使用できるほか、別のコンセプトのために使用することもできます

// Number, Concept, AnotherConceptは何かコンセプトとする

// 1つのテンプレートパラメータで2つの引数を宣言する
void f(Number N x, N y) { }

// 複数のコンセプトでそれぞれ変数を宣言し、テンプレートパラメータ名を別のコンセプトで使用する
Concept R f(Number N a, AnotherConcept<R> U b); 

P1383R2 More constexpr for <cmath> and <complex>

<cmath><complex>の数学関数をconstexprにする提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなるワーキングドラフトを更新したことと、<complex>関連の見落としを追加したことなどです。

この提案は2023年6月の全体会議で承認され、C++26WD入りしています。

P1729R2 Text Parsing

std::formatの対となるテキストスキャン機能の提案。

C++20でstd::formatが追加されたことで、いくつか問題を抱えていた従来のテキスト書式付き出力機能であるstd::ostreamprintf等に代わるものをC++は手に入れました。しかし、テキスト入力面では改善はなく、同様に問題を抱えている従来のstd::istreamscanf等を利用するしかありません。std::formatの対となるものを欠いているということでもあり、このことは標準ライブラリの一貫性を損ねてもいます。

この提案は、std::formatの対となる書式付きテキスト入力機能std::scanを提供し、その欠けている部分を補おうとするものです。

基本的な使用法

if (auto result = std::scan<std::string, int>("answer = 42", "{} = {}")) {
  //                        ~~~~~~~~~~~~~~~~   ~~~~~~~~~~~    ~~~~~~~
  //                          output types        input        format
  //                                                           string

  const auto& [key, value] = result->values();
  //           ~~~~~~~~~~
  //           読み取られた値

  // result == true
  // result.begin() points to the end of the given input (the null terminator)
  // key == "answer"
  // value == 42
} else {
  // エラーが起きた場合
  // result.error()からエラー情報を取得できる
}

基本的には、std::scan<Types...>(input, format)のようにして、入力文字列inputに対するフォーマット文字列をformat、型指定をTypesに渡して使用します。フォーマット文字列の構文はほぼstd::formatのものと共通しており、入力文字列とフォーマット文字列をマッチして、フォーマット文字列中の置換フィールド{}のある位置に対応する入力文字列中の文字列をTypesの対応する位置にある型の値として読み取ります。

auto input = "25 54.32E-1 Thompson 56789 0123";

auto result = std::scan<int, float, string_view, int, float, int>(
  input, "{:d}{:f}{:9}{:2i}{:g}{:o}");

// resultはstd::expected、 operator->は失敗時に例外をスローする
auto [i, x, str, j, y, k] = result->values();

// i == 25
// x == 54.32e-1
// str == "Thompson"
// j == 56
// y == 789.0
// k == 0123

std::scan<Types...>(...)の結果はstd::expectedで返されており、成功時はstd::scan_resultという型の値に対して.value()関数を呼ぶことでスキャン結果をstd::tuple<Types...>オブジェクトとして得ることができます。

スキャン対象の入力は文字列に限らず、スキャン可能な範囲をとることができます。この要件はscannable_rangeコンセプトで表現されています。

// scannable_rangeの定義例
template <class Range, class CharT>
concept scannable_range =
  ranges::forward_range<Range> && same_as<ranges::range_value_t<Range>, CharT>;

forward_rangeでありその要素型がCharT(文字型)であるような範囲であれば読み取ることができ、文字列の範囲となっている多くのものを対象にしています。

// views::reverseからの読み取り例

std::string input{"123 456"};
if (auto result = std::scan<int>(std::views::reverse(input), "{}")) {
  // 読み取り対象の値が1つなら、result->value()はそのオブジェクトを直接返す
  // result->value() == 654
}

std::scanにおいてはscannable_rangeに与えられるCharTはフォーマット文字列の文字型から取得されるため、入力文字列とフォーマット文字列の文字型は一致している必要があります。

std::scan<int>("42", "{}");   // OK
std::scan<int>(L"42", L"{}"); // OK
std::scan<int>(L"42", "{}");  // Error: wchar_t[N] is not a scannable_range<char>

エラー時はscan_errorという専用のエラー型(列挙型ではない)の値が得られ、.code()からエラーコードが取得できるほか.msg()からエラーメッセージを取得することができます。

if (auto result = std::scan<std::string, int>("answer = 42", "{} = {}")) {
  ...
} else {
  // エラーが起きた場合
  auto err = result.error();

  auto ec = err.code(); // エラーコードの取得
  std::println("Error! : {:s}", err.msg()); // エラーメッセージの出力
}

フォーマット文字列はstd::formatにほとんど準じていますが、数値型に対する次の一部のオプションは無効化されます

これらのオプションは読み取り時には意味がなく、スキャン時は全ての可能性を考慮しどれかの形式をデフォルトにしたり無効化することを回避しています。

そのほかのオプションは利用可能となりますが、出力ではなく読み取りに使用するものであるためstd::formatからその意味が少し変わっています。

また、std::scanでのみ利用できるオプションも追加されています

  • i : プリフィックスから基数を検出する(デフォルトは10進数)
    • 整数型のみ
  • u : -を考慮しない10進整数値
    • 整数型のみ
  • c : 入力文字(列)をそのままコピーする
    • 文字列型/文字型/数値型で有効

std::formatterと同様にstd::scannerクラステンプレートを特殊化することで、ユーザー定義型をスキャン可能な型として登録することができます。

// tmのスキャンを有効化する際の宣言の例

template <>
struct std::scanner<tm, char> {
  constexpr auto parse(scan_parse_context& ctx)
    -> expected<scan_parse_context::iterator, scan_error>;

  template <class ScanContext>
  auto scan(tm& t, ScanContext& ctx) const
    -> expected<typename ScanContext::iterator, scan_error>;
};

ロケールはデフォルトでは考慮せず、Lオプションとともにstd::localeオブジェクトを渡すことでロケール依存の読み取りを行うことができます。その際、ロケールオブジェクトは引数の先頭で渡します。

この提案の内容は、scnlib(特にdevブランチ)および{fmt}ライブラリにおいて試験実装されているようです。

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

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

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

以前の記事を参照

R5での変更はsimd_selectに関する議論を追記した事です。

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

  • LEWGのレビューを受けての文言の修正
  • simd_catsimd_splitをリネーム
  • simd_cat(array)オーバーロードを削除
  • simd_splitをP1928R4で提案されているように修正
  • output_iteratorの代わりにindirectly_writableを使用
  • ほとんどのsize_t, intの使用をsimd-size-type(符号付整数型)に置き換え
  • simd_abiに関するものをすべて削除
  • 説明専用のABIタグを用いてABIタグのセクションを置き換え
  • ジェネレータコンストラクタは、インデックスにつき1度だけcallableを呼び出すことを保証
  • ブロードキャスティングコンストラクタの変換規則から、int/unsigned intを例外としているのを削除
  • loadstore_flagssimd_flagsにリネーム
  • simd_flags::operator|constevalに変更
  • 最小SIMD幅を64に増加
  • hmin/hmaxreduce_min/reduce_maxにリネーム
  • simd_mask<T, Abi>basic_simd_mask<Bytes, Abi>リファクタリングし、それに応じ使用箇所を置き換え
  • simd<T, Abi>basic_simd<Bytes, Abi>リファクタリングし、それに応じ使用箇所を置き換え
  • ベクトル化可能な型からlong doubleを削除
  • is_abi_tag, is_simd, is_simd_maskを削除
  • simd_sizeを説明専用にした

などです。

この提案はLEWGでのレビューをいったん終えて、LWGに転送するための投票待ちをしています。

P2169R4 A Nice Placeholder With No Name

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

以前の記事を参照

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

この提案は6月の全体会議で投票にかけられ、C++26WDに導入されています。

P2407R4 Freestanding Library: Partial Classes

一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。

以前の記事を参照

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

  • <algorithm>からfill_nswap_rangesをフリースタンディングとして追加
  • optionalにおいて、.value()の使用を**thisで置き換え
  • variantにおいて、getの使用をget_ifで置き換え
  • string_viewfindおよび検索系関数において、at()の使用をdata_で置き換え
  • 機能テストマクロへの// freestandingを追記
  • "synopsis"と"header synopsis"の使い分けをより慎重に判断
  • freestanding-deletedfreestanding-partialは要件が変更され、それぞれ異なるフリースタンディング指定となった
  • // hosted// freestanding-deletedオーバーロード解決への影響の違いについて追記
  • フリースタンディング機能をより明確化

などです。

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

P2487R1 Is attribute-like syntax adequate for contract annotations?

契約プログラミングの構文について、属性likeな構文は契約の指定に適しているかを考察する文書。

以前の記事を参照

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

  • P2552R2および最新のWDに基づいて、属性の無視可能性に関する議論を更新
  • 属性の式に関する説明を追記
  • P2552R2で提示された意味論の無視可能性基準に関する議論を追加

などです。

P2521R4 Contract support -- Record of SG21 consensus

C++に最小の契約プログラミングサポートを追加する提案。

以前の記事を参照

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

  • 複数の仮想関数を異なる契約アノテーションでオーバーライドする方法に発見されたバグに関して追記
  • ビルドモードは廃止され、各契約条件ごとに何が起こるかは実装定義となった
  • longjmpなど通常とは異なる方法で終了する契約条件式で何が起こるかを指定
  • 違反ハンドラのセマンティクスを修正
  • 例外を投げる契約条件式の意味論を指定
  • トリビアルな関数に対する契約アノテーションに関するイシューを追記

などです。

P2542R3 views::concat

同じ要素型を持つ異なる型の範囲を連結するRangeファクトリ、views::concatの提案。

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

  • iter_swapの再設計
  • random_access_range制約の緩和
  • 異なる型の変換を修正
  • 提案する文言の修正

などです。

P2546R5 Debugging Support

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

以前の記事を参照

このリビジョンでの変更は、LWGのレビューに伴う文言の修正と投票結果を追記した事です。

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

P2548R6 copyable_function

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

以前の記事を参照

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

  • 型消去Callableラッパの2重ラッピングに関する文言を追加
  • 提案する文言の修正

などです。

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

P2552R3 On the ignorability of standard attributes

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

以前の記事を参照

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

  • CWG/EWGのフィードバックを反映して、__has_cpp_attributeの文言を修正
  • 理論的根拠としてcarries_dependencyに関するセクションを追加

などです。

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

P2591R4 Concatenation of strings and string views

std::stringstd::string_view+で結合できるようにする提案。

以前の記事を参照

このリビジョンでの変更

  • LEWGのフィードバックを反映
  • Hidden friendsの導入する非対称性を回避するために、フリー関数テンプレートに戻した
  • 実装中のプロトタイプに要求されたテスト(filesystem::pathおよび曖昧さを導入するテスト)を追加
  • 最新のWDに追随

などです。

この提案はLEWGのレビューを終えてLWGに転送するための投票待ちをしています。

P2630R4 Submdspan

std::mdspanの部分スライスを取得する関数submdspan()の提案。

以前の記事を参照

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

  • LWGのフィードバックを反映
  • submdspan_mappingをHidden friendsに変更
  • submdspan_mappingがADL経由で呼び出されることを規定
  • 説明専用の実装詳細関数をHidden frinedsとした
  • is-strided-sliceを削除し、strided_sliceの特殊化であるS_kに置き換え
  • [[no_unique_address]]とデフォルト初期化をstrided_slicesubmdspan_mapping_resultメンバに追加
  • first_, last_のテンプレートパラメータにkを追加
  • LWGのフィードバックを反映

などです。

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

P2637R3 Member visit

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

以前の記事を参照

このリビジョンでの変更は、文言の修正と機能テストマクロに関するメモを追記した事です。

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

P2641R4 Checking if a union alternative is active

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

以前の記事を参照

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

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

P2642R3 Padded mdspan layouts

std::mdspanpadding strideをサポートするためのレイアウト指定クラスを追加する提案。

以前の記事を参照

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

  • P2630R3(submdspan)の変更に追随
  • P2897(aligned_accessor)のリファレンスを追加
  • 既存のレイアウトマッピング型に合わせて、.extents()の戻り値型をextents_typeからconst extents_type&へ変更
  • LEWGの投票結果を受けた設計に関する議論を追加
  • rank 1のpadded layoutのrequired_span_size()の設計に関する議論を追加
  • 実装経験リンクの更新
  • layout_{left,right}_paddedマッピングクラスからの変換コンストラクタと、layout_{left,right}_paddedマッピングクラスとのoperator==
  • 共著者の追加

などです。

P2662R2 Pack Indexing

以前の記事を参照

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

以前の記事を参照

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

  • CWGレビューを受けての文言の改善
  • index-type-specifierから型を推論する方法に関するセクションを追加
  • 将来的な機能拡張に関するセクションを拡充

などです。

P2689R2 atomic_accessor

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

以前の記事を参照

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

  • atomic-ref-boundedatomic-ref-boundにリネーム
  • atomic-ref-unboundedatomic-ref-unboundにリネーム
  • 提案する文言の修正
  • P2616が採択された場合、同様の変更をこちらにも適用する必要があることについて追記

などです。

P2697R1 Interfacing bitset with string_view

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

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けての提案する文言の修正です。

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

P2714R1 Bind front and back to NTTP callables

std::bind_frontstd::bind_backにNTTPとして呼び出し可能なものを渡すオーバーロードを追加する提案。

以前の記事を参照

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

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

P2717R2 Tool Introspection

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

以前の記事を参照

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

  • Ecosystem ISの最新の変更に追随
  • 必要に応じてリテラルユニコードを使用
  • セマンティックバージョニングとJSON、及び先行0を許可しない仕様に従うようにバージョン番号を修正
  • フルレベルサポートにバージョン範囲の配列を追加し、サポートの不一致を報告できるようにした
  • JSONスキーマの修正
  • capability名で数字を使用できるように修正
  • コマンドオプションの処理と使用ファイルに関する説明を追加
  • 問い合わせのために使用するユーザーインタフェースに関する選択肢を追加し、現在推奨されている選択肢について説明

などです。

P2727R3 std::iterator_interface

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

以前の記事を参照

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

  • CRTPの代わりにdeducing thisを使用
  • 以前にHidden friendsだったものをそうではなくした

などです。

P2728R4 Unicode in the Library, Part 1: UTF Transcoding

P2728R5 Unicode in the Library, Part 1: UTF Transcoding

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

以前の記事を参照

R4での変更は

  • code_unitコンセプトの定義を変更し、as_charN_tアダプタを追加
  • replacement_characterを除くユーティリティとユニコード関連の定数を削除
  • utf_iteratorの制約をわずかに変更
  • null_sentinel_tユニコード固有のものに戻した

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

  • unpacking_owning_viewunpacking_viewに置き換え
    • アダプタでアンパッキングを行うのではなく、アンパッキングを行うために使用する
  • 提案するすべてのviewbegin/endconst/非constオーバーロードを追加
  • null_sentinel_tstd名前空間に移動し、その.base()を削除。単なるポインタ以上の用途に使用できるようにした

などです。

P2741R3 user-generated static_assert messages

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

以前の記事を参照

このリビジョンでの変更は、設計セクションを拡張してインスタンス化と再帰がどのように機能するかについて追記したことです。

P2752R3 Static storage for braced initializers

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

以前の記事を参照

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

  • この提案は記憶域期間を変更しないことを確認するEWGの投票の結果を受けて、Annex Cにそれを追記
  • mutableメンバの説明を追記
  • 例を追加
  • 「§5.1 However...」セクションを追加

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

P2757R3 Type checking format args

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善と機能テストマクロについて注記を追加したことです。

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

P2767R1 flat_map/flat_set omnibus

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

以前の記事を参照

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

  • アロケータを受け取るコンストラクタの調整については編集上の修正として受け入れられたため提案から分離された
  • LWGのレビュー受けての提案する文言の更新と、関連する根拠の追記
  • LEWGのレビューが必要なため、Heterogeneous insertに関して§7の残りのものと分離(§13へ)
  • 注意を引くために非explicitコンテナコンストラクタに関してを§12から分離
    • これは2つのコンテナからの非explicitコンテナコンストラクタとして§14へ移動

などです。

P2776R0 2023-05 Library Evolution Poll Outcomes

2023年5月に行われたLEWGの全体投票の結果を報告する文書。

次の13本の提案が投票にかけられ、否決されたものはありませんでした。

全て、C++26に向けてLWGに転送するための投票です。これらのうちのいくつかは、2023年6月の全体会議でC++26 WDに導入されています。

P2779R1 Make basic_string_view's range construction conditionally explicit

std::string_viewrangeコンストラクタのexplicitを条件付きに緩和する提案。

以前の記事を参照

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

  • ベースとなるドラフトの更新
  • オプション1で提案する型特性の名前を変更し、2つのサブオプションに分割

などです。

分割された代替案は、変換しようとしているstring_view-likeな型の内部traitsの(std::char_traitsとの)互換性をチェックを行うかどうかによる、次の2つです

  1. 内部traitsの互換性を無視する
  2. 内部traitsの互換性を考慮するものとしないものの2つを用意する

(提案の現在の記述では、2つ目のサブオプションがどう使用されるのかわかりませんでした・・・)

SG16でのレビューではこの提案の問題としているところの解決に積極的な賛同は得られなかったようですが、解決するのであれば明示的なオプトイン(コンセプトによるチェックによる自動判定ではなく)によるものが望ましいというコンセンサスは得られています。この提案を進めるかどうかはLEWGに委ねられています。

P2781R3 std::constexpr_v

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

以前の記事を参照

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

  • 後置戻り値型の不必要な使用の修正
  • 代替トークン(and or not)を使用しないように修正
  • constexpr-param(説明専用)コンセプトの要件の修正
  • constexpr_vのテンプレートパラメータTがあることによって与えられるADLサポートの不完全な側面について追記

などです。

P2785R0 Relocating prvalues

P2785R1 Relocating prvalues

P2785R2 Relocating prvalues

P2785R3 Relocating prvalues

prvalueからのリロケーションを可能とするための機能を導入する提案。

リロケーションについてと提案のモチベーションは以前の同種提案と共通しているのでそちらを参照

この提案では特に、構築後に動かせなくなるオブジェクトの扱いを改善するためにもリロケーション操作が有効であると述べています。

例えば、gsl::not_nullというクラスはnullptr状態を取り得るクラスのムーブコンストラクタ等を無効化することでそのクラスがnullptrではないことを保証するクラスです。gsl::not_null<std::unique_ptr<T>>のように使用する場合、この型のオブジェクトはムーブもコピーもできなくなります。

このようなオブジェクトはnullptrではないことが保証されていることからコードの正確性やパフォーマンスを向上させるために有効ですが、現在のC++コードでは扱うのが現実的ではありません。コピーもムーブもできないため構築後にメモリ上を移動できなくなり、関数に渡したりコンテナに保存したりすることやクラスのメンバとなることを妨げます。

しかし、リロケーション操作が可能になればそのようなオブジェクトは、そのクラスの不変条件を保ったままリロケーションによってメモリ上を移動することができるようになり、その取り扱いのしやすさが改善します。

同様の問題は定数(const)オブジェクトにもあります。定数オブジェクトはその生存期間を通じて変化しないため、人間にとっても機械(コンパイラ)にとってもプログラムの状態に関する推論がしやすくなります。そのため、自動変数はさまざまなガイドラインで可能ならconstとすることが推奨されています。

しかし、constオブジェクトはムーブすることができず、そのため後でムーブすることを意図するオブジェクトはconstにすることができません。生存期間中はconstでありながらも、その終わりに所有するリソースを手放すことができれば、より安全で読みやすいコードが可能になります。

この提案のリロケーション操作はこれらの2点を改善することを主目的としており、その点が以前の提案と大きく異なる部分です。

この提案が導入しようとしているのは次のものです

  • 2つの特殊メンバ関数
    • リロケーションコンストラクタ : T(T)
    • リロケーション代入演算子 : T& operator=(T)
  • 新しいキーワードreloc
  • オーバーロード解決ルールの若干の変更
  • 一部の関数にABI破壊が生じる可能性がある
  • リロケーション操作のサポートのためのライブラリ関数

この提案では、リロケーションのために新しい型を導入することを避け、代わりにprvalue(修飾なしの素の型)をそのために利用しようとしています。

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

void foo(std::string str);
auto get_string() -> std::string;
auto get_strings() -> std::pair<std::string, std::string>;

std::string gStr = "static string";

void bar(void) {
    std::string str = "test string";
    foo(reloc str);   // OK: std::stringにリロケーションコンストラクタがあればリロケーションされる
    foo(reloc gStr);  // ill-formed: gStrはローカル変数ではない

    std::pair p{std::string{}, std::string{}};
    foo(reloc p.first); // ill-formed: p.firstは完全なオブジェクトではなく、変数名でもない

    foo(reloc get_string());        // ill-formed: 変数名ではない
    foo(reloc get_strings().first); // ill-formed: 完全なオブジェクトではなく、変数名でもない

  foo(auto(str));   // ill-formed: リロケーション後の変数名は使用できない
}

void foobar(const std::string& str) {
    foo(reloc str); // OK: 参照をリロケーションする
                  // strの参照先オブジェクトの生存期間は影響を受けない
}

void foobar(std::string* str) {
    foo(reloc *str); // ill-formed: *strは変数名ではない
}

void foobar2(std::string* str) {
    foobar(reloc str); // OK, ポインタをリロケーションする
                     // strの参照先オブジェクトの生存期間は影響を受けない
} 

class A {
    std::string _str;
public:
    void bar() {
        foo(reloc _str); // ill-formed: _strは完全なオブジェクトではなく、ローカル変数でもない
    }
};

P2786R2 Trivial relocatability options

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

以前の記事を参照

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

  • P1144への参照を最新のもの(5月公開分)に更新
  • 用語と定義の明確化のためにセクションを追加
  • 新しい構文について欠けていた例を追加
  • 基礎的ではない関数をライブラリ関数とした

などです。

この提案はP1144とともにEWGIでレビューされ、今後も議論を続けていくことに合意がとれています。また、この提案単体で既にEWGにおけるレビュー中です(とはいえP1144と強調して、より論点を明確にすることなどが求められています)。

P2795R0 Correct and incorrect code, and "erroneous behaviour"

P2795R1 Erroneous behaviour for uninitialized reads

P2795R2 Erroneous behaviour for uninitialized reads

未初期化変数の読み取りに関して、Erroneous Behaviourという振る舞いの規定を追加する提案。

未初期化変数の読み取りに関するErroneous BehaviourとはP2754R0で導入された概念で、未初期化変数を実装定義の値に初期化した上でその値の(ユーザーが書き込む前の)読み取りに対して指定される標準で定義された動作の1つです。

P2754R0については以前の記事を参照

Erroneous Behaviour(誤った動作、EB)は未定義動作ではなく、実装はユーザーによって初期化されていない変数を特定の値で変数を初期化しているためそれが起きたとしても安全であり、初期化する値を調整することでテストやデバッグに役立てることができます。また、EBを未定義動作として扱えば現状維持になり、EB/UBのまま維持しておくことで将来のさらなる大胆な改善(デフォルト初期化仕様の値初期化への完全な置き換えなど)のための門戸を開いておくこともできます。

未初期化変数の読み取りという問題の解決策としてEBの導入は最善(実現可能性、後方互換性、表現力の保護の観点で最善)であると認識されていますが、現在のC++標準ではEBという概念は定義されておらず、その導入そのものがハードルだとされていました。

この提案は、そのErroneous Behaviourという概念を標準に導入し、未初期化変数読み取り問題の解決を図るものです。

この提案では、自動変数のデフォルト初期化を次のように変更することを提案しています

自動変数のデフォルト初期化は、実装によって定義された固定値で変数を初期化する。
実装はこのエラーを診断することが許可されており、また推奨されているが、エラーを無視して読み取りを有効なものとして扱うことも許可されている。

Erroneous Behaviourというワードが直接出現するわけではありませんが、この文章の後半部分(2行目)がそれを意図しています。すなわち、初期化されていない値を読むことは意図したものではなく間違いなくバグではあり修正する必要があるものの、それを含むコードはwell-definedでありその点について診断されないとしてもプログラムは予測可能である(未定義動作ではない)、とするものです。

言い換えると、未初期化の値を読み取ることは誤りではあるものの、実装がそれを止めなければプログラムはその読み取りの結果として(未定義ではない)何らかの特定の値を得ることになります。実装は診断をしてもいいが、してもしなくてもそれについてwell-definedであることを保証する必要があり、誤った振る舞いが実行された場合に未定義動作及びそれに起因する結果(ソースコードからは予測できない命令の実行、タイムトラベルなど)をもたらすことはありません。

extern void f(int);

int main() {
  int x;     // default-initialized, value of x is indeterminate
  f(x);      // glvalue-to-prvalue conversion has undefined behaviour
}

この現在UBとなるコードについて、現在及びP2723R1(強制ゼロ初期化)とこの提案によるコンパイル結果の違いは次のようになります

動作について C++23 P2723R1 この提案
未定義動作? UB well-defined EB
それはバグ? 確実にバグ 意図的に0初期化しているのか忘れているのかわからない 確実にバグ
コンパイラの診断は可能か? コンパイラはこれを拒否することが許可されている 適合するコンパイラは診断できない 適合するコンパイラは受け入れなければならないが、QoIによって拒否することが許可されている

この提案の下での動作においては、xの値は何か特定の値に初期化されていますがその値は必ずしも0ではありません。これは、デバッグやテストに役立てるために特定の値を設定することを許可することと、特定の固定値にプログラマが依存してしまうことを回避することを意図しています。

この提案による標準の他の部分への影響は次のようなものが想定されます

  • 自動変数は全て何かしら初期化されるため、パフォーマンスに影響を与える可能性がある
    • このコストに関する分析はP2723R1でなされている
    • また、このコストは非クラス型だけではなく、パディングを持たずデフォルトコンストラクタが全てのメンバを初期化するようなクラス型にもかかってくる
  • 特に、共用体は完全に初期化される
    • 一般に、共用体をコピーしても誤りではなくパディングビットをコピーしても誤りではない
    • これは、誤りのある値のglvalueからprvalueへの変換自体は誤りではないものの、その値をコピー以外のことに使用するのは誤りであることを意味する
  • この提案は初期化のセマンティクスのみに影響し、不定値の使用の全般に影響するわけではない
    • 例えば、初期化された変数に不定値をコピーすることができ、その値を読み取ると未定義動作となる可能性がある
  • この提案は単一操作としての自動変数のデフォルト初期化にのみ影響する
    • 自動変数をplacement newでデフォルト初期化するような場合はこの提案の保証はない

この提案の実装に関しては、ほぼ同様のことを行うことがgcc/clangにおいてftrivial-auto-var-init=zeroというオプションによって利用可能となっています(ただしこれは診断を意図したものではないようです)。この提案の変更は主に標準内の動作仕様に関する変更であり、実装の負担は軽微だと思われます。

この提案の採択によって未初期化変数読み取りに関する未定義動作が誤った動作に変更される場合、コードベースに対する影響は次のようになるでしょう

  • 今日の正しいコード : パフォーマンスが低下する可能性がある他は観測可能な変化はない
  • 今日の正しくないコード : UBをEBに変更した場合(診断しない場合)、そのコードは依然として正しくはないもののその振る舞いは未定義動作ではなくなり、特定の振る舞いするようになる

また、EBはこの提案では未初期化変数の読み取りのために導入しようとしていますが、同様に現在未定義動作となっているもののバグと意図的なものを弁別でき、バグについてのみ診断が可能な未定義動作についてもこれと同様にEBとして指定することで未定義動作やそれに伴う意図しない動作を回避しながら安全な動作をさせるようにすることができる可能性があり、その候補リストも示されています。

P2809R1 Trivial infinite loops are not Undefined Behavior

自明な無限ループを未定義動作ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、提案する内容のC言語におけるセマンティクスとの一致についてSG1のフィードバックを追記したことです。

現在のCのセマンティクスでは次の2つのループは同じものとみなされません

// 少なくとも定数ではない変数
extern bool cond;

void f1() {
  while (cond) {
    // ...
  }
}

void f2() {
  while (true) {
    if (!cond)
      break;
    // ...
  }
}

この2つのループは意味的には等価なものに見えますが、f1()の方のループの継続条件は定数式ではないためこの問題の下では異なります。Cのセマンティクスと一致するようにこの問題を解決すると、f1()のループは未定義のままですがf2()のループは未定義ではなくなります。

このことの問題点は、f2()のループに対してコンパイラはその終了を仮定することができない(プログラマの意図的なものとして処理せざるを得ない)と言うことです。なぜそれが問題なのかというと、逆に意図的に並行処理の進行保証を活用して無限ループを書いている場合に、そのループが終了すると仮定した振る舞いや最適化(ループの削除やマージ、並べ替え)が行えなくなるためです。同様の問題は、f2のようなループ内にreturnthrow(スレッドの終了処理とみなされる)がある場合にも起こります。

実際に、一部のGPUではワープの1つが無限ループに陥った場合に同じスレッドブロック内のすべてのワープの進行を停止するものがあるようです。この振る舞いは、無限ループが未定義動作である場合は許容されますが、そうでない場合は規格違反となります。f2のようなループによってそれが記述されている場合、現在は規格に適合した振る舞いですが、この提案の後(あるいはCのセマンティクスの下)では規格違反となります。

回避策として、f2のようなループ中にstd::this_thread::yield()を置いておくと言う方法が考えられます(他のスレッドに実行を委譲し続けることで上記動作を正当化する)。しかし、これは無限ループの中断という振る舞いを正当化するだけで、ループを最適化できないという問題は解決できません。

つまるところ、ループの継続条件が定数式であるかどうかという判定方法では不十分だということです。そのため、このリビジョンではCのセマンティクスとは一致しない解決を提案し、それをCにも適用することを提案しています。

提案ではまず、std::this_thread::halt()というライブラリ関数を追加します。これは、その呼び出しがwhile(true) std::this_thread::yield();の呼び出しと等価な関数であり、呼び出して使用するものというよりはある種のループをこれに置換する(後述)ためのものです。

次に、スレッドが最終的に行うと仮定して良い処理(これがあると無限ループは未定義ではなくなる処理)のリストに次の2つを追加します

  • std::this_thread::yield()の呼び出し
  • [[noreturn]]関数の呼び出し

これはこの提案の主題とはあまり関係なく、議論の過程でこの2つがスレッドにおける進行保証を担うのに有効なもので見落とされていることが分かったために追加されています。

最後に、次に該当する無限ループをstd::this_thread::halt()と等価なものであるとして、その呼び出しとして実装することを規定します(しても良いではなくしなければならない)。ここでは2つのオプションが提案されています

  1. オプション1 : 次の全てを満たすループ
    • スレッドが最終的に行うと仮定して良い処理を何もしていない
    • ループ継続条件が定数式でtrueとなる
    • ループは次のいずれも含んでいない
      • 直接のbreak;
      • ループの外側に抜けるgoto
      • return
      • co_return
      • co_await
      • co_yield
    • ループは次のどちらも含まない
      • 例外を投げうる処理
      • std::longjmp()の呼び出し
  2. オプション2 : 次の全てを満たすループ
    • スレッドが最終的に行うと仮定して良い処理を何もしていない
    • ループ継続条件が定数式でtrueとなる
    • ループ本文が空

どちらのオプションでも、この条件を満たすループはスレッドが最終的に行うと仮定して良い処理をしていなくても未定義動作にはならず、プログラマの意図的なものとみなされます(つまり、最適化の対象となりません)。どちらにしても、以前(現在のC)のセマンティクスであるループ継続条件が定数式(でtrueとなる)という条件に制約をいくつか加えた形になっています。また、そのようなループをstd::this_thread::halt()の呼び出しに置換してしまうことで無限ループは意図的なものであるとして終了しないが実行もしないという実装が可能になり、上記の一部のGPUが行なっているような動作(無限ループに突入すると実行中断)を正当化することができます。

P2810R1 is_debugger_present is_replaceable

P2546で提案されているis_debugger_present()をユーザーが置換可能にする提案。

以前の記事を参照

このリビジョンでの変更はP2546の最新リビジョン(R5)の内容を適用したことです。

P2811R5 Contract-Violation Handlers

P2811R6 Contract-Violation Handlers

P2811R7 Contract-Violation Handlers

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

以前の記事を参照

R5での変更は

  • 契約違反ハンドラ提案の簡単な沿革を追加
  • すべての形式の契約チェックアノテーションに対して、期待される評価地点を明確化
  • デストラクタの事前条件に関する契約違反の特定に関して注記を追加

R6での変更は

  • ヘッダ名を<contract>から<contracts>へ変更
  • contract_semantic列挙型にobserve列挙子を追加(P2877R0から)

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

  • 文字列プロパティの期待されるエンコーディングについて注記
  • 契約機能を使用するのに<contracts>のインクルードが必要ないことを明記
  • 契約違反ハンドラの呼び出し元特定とテストに関するセクションを追加
  • 適合実装がlocation()comment()に何を提供できるかを明確化

などです。

P2814R1 Trivial Relocatability --- Comparing P1144 with P2786

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

以前の記事を参照

このリビジョンでの変更はよくわかりません。

この提案、特にtrivially relocationについてさらに時間をかけて議論していくことがEWGにおいて合意されています。relocation全体よりもまずtrivially relocationを優先し、なおかつ言語機能とする方向性が選好されているようです。

P2819R1 Add tuple protocol to complex

std::complextupleインターフェースを追加する提案。

以前の記事を参照

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

  • get()オーバーロードをHidden friendにした
  • tuple-likeコンセプトをタプルベースRangeアルゴリズムをサポートするように拡張
  • Annex Cへ追加する文言を修正

などです。

P2821R2 span.at()

P2821R3 span.at()

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

以前の記事を参照

R2での変更は、機能テストマクロの修正とP1024へのリンクを追加したことです。

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

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

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

以前の記事を参照

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

P2834R1 Semantic Stability Across Contract-Checking Build Modes

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

以前の記事を参照

このリビジョンでの変更は、Appendixの各節はその問題に関する推奨事項を提案する形で終了するようにしたことです。

P2835R1 Expose std::atomic_ref's object address

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

以前の記事を参照

このリビジョンでの変更は、代替となるAPIの提案を追記したことです。

この提案のメインはstd::atomic_refにその参照先オブジェクトのアドレスを取得するための.data()関数を追加することですが、これは別にアトミックなポインタではないのでここから取得したポインタ経由でアクセスすることができてしまい、これを間違って使用されるとデータ競合によるUBを起こしやすくなってしまいます。

そのため、この提案では代替設計として、.data()const void*を返すようにするか、uintptr_tを返す別の関数を追加するものの2つを挙げています。

P2836R1 std::basic_const_iterator should follow its underlying type's convertibility

std::basic_const_iterator<I>Iに対応するconst_iteratorに変換できるようにする提案。

以前の記事を参照

このリビジョンでは、以前の解決策が別の問題を引き起こすことなどから取り下げ、R0で問題としていたことは別のIssueとして分離し、問題の範囲を限定しています。

このリビジョンでは、ある定数イテレータIに対してstd::basic_const_itertor<I>が得られる場合に、それがIに変換できないことを問題視しています。

void f(std::vector<int>::const_iterator i) {}

auto v = std::vector<int>();
{
  auto i1 = std::ranges::cbegin(v); // returns vector<T>::const_iterator
  f(i1); // ok
}

auto t = v | stdv::take_while([](int const x) { return x < 100; });
{
  auto i2 = std::ranges::cbegin(t); // returns basic_const_iterator<vector<T>::iterator>
  f(i2); // error(現在のC++23では
}

std::basic_const_itertor<std::vector<T>::iterator>std::vector<T>::const_iteratorに変換可能ではない(.base()を取れば可能ではある)ため、2つ目のf()の呼び出しはエラーになります。ただ実は、これはC++20ではranges::cbegin()vector<T>::iteratorを返していたため通っていました(結果的には問題なかっただけで間違った振る舞いです)。

コンテナ型Cに対して、std::basic_const_itertor<C::iterator>は意味的にはC::const_iteratorと同一のものであり、同等の扱いが可能であることが望ましく、それが期待と一致する振る舞いとなります。

このリビジョンでは、C++20の振る舞いを引継ぎつつも改善するために、std::basic_const_itertor<I>Iから変換可能な定数イテレータ型に同様に変換可能となるようにすることを提案しています。

namespace std {
  template<input_iterator Iterator>
  class basic_const_iterator {
    ...

    // 提案している変換演算子の宣言例

    template<not-a-const-iterator CI>
      requires constant-iterator<CI> && convertible_to<Iterator const&, CI>
    constexpr operator CI() const &;

    template<not-a-const-iterator CI>
      requires constant-iterator<CI> && convertible_to<Iterator, CI>
    constexpr operator CI() &&;

    ...
  }
}

変換先のイテレータCIbasic_const_iteratorではなく(not-a-const-iterator)、かつすでに定数イテレータである必要があります。これによって、const性を破壊するような変換を行うことはできません。

なお、std::const_iterator_tranges::cbegin()が一致しないことがある問題については、LWG Issue 3946にてstd::const_iterator_tの定義を変更することで解決が図られます。

namespace std {
  template<range R>
  //using const_iterator_t = const_iterator<iterator_t<R>>;
  using const_iterator_t = decltype(ranges::cbegin(declval<R&>()));

  template<range R>
  //using const_sentinel_t = const_sentinel<sentinel_t<R>>;
  using const_sentinel_t = decltype(ranges::cend(declval<R&>()));
}

おそらくこの変更はC++23に適用されます。この提案自体はC++26ターゲットですがC++23へバックポートすることを実装者への推奨事項とすることを提案しています。

なお、LEWGのレビューではこれをC++23のDRとすることに合意されています。

P2845R1 Formatting of std::filesystem::path

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

以前の記事を参照

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

  • fill-and-alignwidthについて既存もフォーマット文字列へのリンクを追加
  • range-format-specpath-format-specで置き換え
  • 欠落していた文字コード変換についての規定を追加

などです。

P2865R1 Remove Deprecated Array Comparisons from C++26

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

以前の記事を参照

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

  • オーディエンスにSG22を追加
  • gccの警告表示について追記
  • 提案する文言を最新のドラフトに適合させた
  • オペランドと配列からポインタへのdecayを説明する文言の誤りを修正
  • 文言レビューの結果を受けての修正

などです。

P2872R1 Remove wstring_convert From C++26

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

以前の記事を参照

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

  • コピペミスの修正
  • 最初のレビュワーとしてSG16をアサイ
  • 提案する文言を最新のドラフトに適合させた
  • 標準ライブラリ実装がこれに関して警告を発し始めた時期の記録
  • SG16レビューの概要と結果を追記

などです。

P2874R1 Mandating Annex D

P2874R2 Mandating Annex D

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

以前の記事を参照

R1での変更は

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

  • POD型/PODクラスの定義を斜体で表示する
  • is_podのMandatesをPreconditionsに切り替え
  • “shall (not) be” を “is (not)”へ修正
  • 隣接するPreconditionsの統合
  • 元のテキストから欠落していたnullを追加
  • D.24の番号変更
  • 半開区間を示す際のマークアップが壊れていたのを修正

などです。

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

P2877R0 Contract Build Modes and Semantics

現在の契約プログラミングの仕様を、現在明らかなユースケースをサポートし将来の拡張に対して開いているようにする提案。

現在SG21で議論されている契約プログラミング仕様では、No_evalとEval_and_abortの2つのビルドモードのみが提供されており、翻訳単位間でのビルドモードの混合は実装定義のセマンティクスによって条件付きでサポートされています。

No_evalビルドモードでは全ての契約アノテーション(1つの関数に指定されている契約条件の全体)はignoreセマンティクスを持ち、契約アノテーションに含まれる各条件式は評価されず契約違反が起こることはありません。そのため、このビルドモードでは他のセマンティクスに影響を与えません。

Eval_and_abortビルドモードでは全ての契約アノテーションenforceセマンティクスを持ち、契約アノテーションに含まれる各条件式が評価されそれがtrueを返さない場合は契約違反処理プロセスが発生し、その終了後にプログラムは終了されます。

これによってC++の実装には制限が課されており、2つのビルドモードのプロパティはすなわちC++コントラクト機能のプロパティであるとみなされ、プラットフォーム固有のビルドモードまたは将来のビルドモードの追加によってこの2つのビルドモードのプロパティや前提に反するようなセマンティクスを持つビルドモードを追加することが妨げられます。これらのプロパティの重要な特徴は、翻訳単位内の全ての契約アノテーションが同じセマンティクスを持つことです。

この提案は、契約アノテーションのセマンティクスとビルドモードの制限を取り払い実装の自由度を最大化することを目指し、それによってC++契約プログラミング機能が満たすべき現在明らかなニーズを満足しつつ、将来のニーズに適応するための拡張の余地を残しておくようにしようとするものです。

提案で挙げられている現在明らかなニーズとは次のようなものです

  • パッケージマネージャ
    • 現在利用可能なパッケージマネージャの多くは、各パッケージについて単一のビルドのみを提供する。つまり、デバッグとリリースの両方を提供したりしない
    • 契約アノテーションのセマンティクスがビルド時に決定しなければならない設計は、パッケージマネージャにとって大きな負担となる
    • パッケージマネージャの管理者が契約機能のビルドモードのどちらを選択するにせよ、その決定はパッケージの作成者の契約機能の使用意欲にネガティブな影響を与える可能性がある
    • パッケージマネージャの管理者がビルドモードの決定を各プロジェクトのビルドモードまで遅延する場合、エコシステム内での契約機能の使用は一貫性がなくなる可能性がある
      • ことなるオブジェクトファイルが異なるビルドモードでビルドされている場合、条件付きサポートとなる
  • パッケージとして配布されるソフトウェア
    • 歴史的に、一部のパッケージ提供者はアサーションを有効にしたパッケージを配布することを選択し、そうではない提供者はアサーションを含むテストをパスしていることを根拠として品質とサポートの目標を達成しているとみなしてアサーションを無効化したパッケージを配布することを選択している
    • C++契約機能はこのような選択肢をサポートし続ける必要がある。すなわち、契約アノテーションが評価されないことを許可しない設計は実行可能ではない
  • 契約の解除
    • 運用環境で契約チェックを利用する際の最も難しい問題の1つは、既存のコードに契約チェックを導入すること。
    • 契約違反によるプログラムの終了のコストが高くつくため、契約違反によってプログラムが終了する場合、現在動作しているプログラムに契約チェックを導入する意欲が失われる
    • 現在の2つのセマンティクス(ビルドモード)だけではこの問題を解消できない
  • REPL
    • REPL環境ではプログラムはコンパイルされないため、ビルドモードが存在しない
    • 契約の評価が有効かどうか、及び違反時の振る舞いについては、ユーザーがいつでも変更できる動的なプロパティとなる
  • デバッグ
    • 契約条件式はプログラムの本質的な動作に影響を与える副作用を持つべきではないが、それを検出するメカニズムが存在しないため、副作用が混入する可能性を受け入れなければならない
    • 副作用が発生しているかを判断するテクニックの一つは、契約アノテーションの評価を無効化した時と有効化した時の間で同じプログラムの動作を比較すること
    • 現在の2つのビルドモードではこれをサポートできない(この場合、契約違反を検出したいわけではないため)

これらのニーズは全て、現在のC++契約プログラミング仕様ではサポートできていません。これらのニーズを満たすには少なくとも次の要件を満たしている必要があります

  • 標準に準拠した契約実装では、リビルドを必要とせずに契約アノテーションの評価の有効/無効を切り替えられなければらない
  • 標準に準拠した契約実装では、ユーザーが契約アノテーションごとに、もしくは違反ハンドラの動作を通して、契約違反後に実行を継続するかを選択可能である必要がある

また、これらの要件がユーザーベースに適さない場合、実装はこれらの要件をサポートしないことも選択できるべきです。この提案の意図は、全てのC++実装や実行環境が全て同じ機能セットをサポートする必要があることではなく、それらの実装が全て標準に実装しwell-definedであり、他のC++プログラムに適用可能な動作に関するトレーニングと推論の対象になり続けるように維持することにあります。

その上でこの提案の変更は、大きく分けて次の2つです

  1. 契約アノテーションが評価される場合、その評価はignoreobserveenforceのいずれかのセマンティクスを持つ
    • ignore : 契約アノテーションは各契約条件を評価しないため、契約違反を起こさない
    • observe : 契約アノテーションは各契約条件を評価し、そのいずれかがtrueを返さない場合は契約違反処理プロセスが発生する
    • enforce : 契約アノテーションは各契約条件を評価し、そのいずれかがtrueを返さない場合は契約違反処理プロセスが発生する。契約違反処理プロセスの終了後、プログラムは実装定義の方法で終了する
  2. 契約アノテーションの個々の評価において、それがどのようなセマンティクスを持つかは実装定義とする
    • プログラム内の全てのアノテーションが同じセマンティクスを持つように強制される場合がある
    • 異なる評価で異なるセマンティクスを持つことにより、同じ関数の異なるインライン版で異なるセマンティクスをコンパイル時に選択することができ、それはODR違反ではなくなる
    • 実装は、その選択をどのように行うかを指定する仕組みがユーザーに公開されていれば、契約アノテーションのセマンティクスをコンパイル時・リンク時・実行時のいずれかのタイミングで選択できる

この提案は、(ビルドモードの)セマンティクスとしてobserveを1つ追加するとともに、契約アノテーションの評価のセマンティクスを契約アノテーション全体からその個々のプロパティとすることで、契約アノテーションを持つもののコンパイル時プロパティが契約アノテーションのセマンティクスに依存しない(できない)ようにするものです。

これによって、P2834R0で提案されている契約アノテーションに関する三原則の1つ目(契約アノテーションがビルドモードによって他の言語機能に影響を与えないこと)が満たされ、翻訳単位間で契約アノテーションのセマンティクスが混合していることも許可されます。

この提案の後でも、実装は現在の2つのビルドモードだけをサポートすることを選択することができ、また、コンパイラはリンク時やコンパイル時、実行時で契約アノテーションのセマンティクスを選択できるようなビルドオプションを提供することもできます。

この提案の内容はどうやらSG21の合意のもとで契約仕様に導入(Contratcts MVPにマージ)されたようで、P2811R6の採用と相まって、C++契約プログラミング仕様はビルドモードの概念から解き放たれることになります。そこでは、この提案で解説されているように、契約条件のセマンティクス(評価されるかどうか、評価された時にどうなるかなど)はignoreobserveenforceのいずれかが契約アノテーションごとに決定され、その決定は実装定義となります。さらに、observeの場合に契約違反を起こした場合の振る舞い(即終了するのか、例外を投げるのか、継続するのか)は違反ハンドラの差し替えによってユーザーがカスタマイズすることができます。

P2878R2 Reference checking

P2878R3 Reference checking

P2878R4 Reference checking

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

以前の記事を参照

R2での変更はサンプルコードの修正などです。

R3での変更は

  • do式/パターンマッチングの未解決の問題に対する解決策の追加
  • 一時オブジェクトの名前付き変数への代入がエラーになることを追加
  • 関数が個別に分析されることを示すために、再帰関数呼び出しの例を追加
  • ラムダ式の別の例を追加
  • Usageセクションの追加
  • Viral Attribution Effortセクションの追加

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

  • サンプルコードの修正
  • FAQの追加(ポインタを対象としないことについて)

などです。

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

P2885R0 Requirements for a Contracts syntax

C++契約プログラミング機能の構文の選択のための評価基準や要件をまとめる文書。

少し上のP2877R0などの採択によって、契約プログラミング機能の意味論の部分はほぼ固まりつつあります。残っている大きな問題は構文の選択に関するものです。

現在のContratcts MVPでは属性likeな構文を採用しており、これはC++20の契約プログラミング機能から受け継いでいるものです。それに対して、クロージャベースの構文(P2461R1)や条件中心構文(P2737R0)が提案され、また他の構文を考えることもできます。しかし、現在のところこれらの構文提案を統一的に比較し評価するためのフレームワークが存在しません。

この提案は、契約構文の選択のために、契約構文に求められる要件をまとめその比較基準を提供しようとするものです。ただし、この提案は構文を選択することを意図しておらず、既存提案の詳細な分析を行おうとするものではなく、あくまで契約機能の構文に求められる要件やその比較基準を確立することを目的としたものです。

提案されている要件の概要は次のようなものです

  • 基本要件
    • 美学
      • 読みやすく、目立ちすぎない
    • 簡潔さ
      • なるべく短いトークンで記述できる
    • アクセシビリティ
      • 学びやすく、教えやすい
    • 既存プラクティスとの一貫性
    • 残りのC++の部分との一貫性
      • 既存のC++言語の構文に自然に適合する
  • 互換性の要件
    • C++言語の一部としての有効性
      • 既存の言語機能に対して、曖昧さ、混乱、非一貫性、意図しない相互作用を起こさない
    • 破壊的変更がない
      • 既存のC++コードを壊さない
    • マクロなし
    • パースしやすさ
      • C++コードをパースするために新たなハードルを生み出してはならず、既存文法を不必要に複雑化させない
    • 実装経験
    • 後方互換
    • ツールによる利用が可能であること
    • C互換性
      • Cで別の意味を持つ既存構文と同じものを追加すべきではない
  • 機能性の要件
    • 述語を受けられる
      • 文脈的にbool変換可能な任意のC++における式を受けられる必要がある
    • 契約種別
    • 位置と名前探索
      • 事前/事後条件は関数宣言の一部である必要がある
      • アサーションは関数定義内部でのみ現れる
    • 複数の事前/事後条件
      • 同じ関数に対して複数の事前/事後条件を追加できる
    • 戻り値
      • 事後条件においては、戻り値を参照する方法を提供しなければならない
  • 将来の進化に備えた要件
    • 非参照非const引数の参照
      • 事後条件において、非参照非const引数を参照する拡張が可能である
    • 明示的なキャプチャ
      • 契約アノテーションで使用する変数を明示的にキャプチャする拡張が可能である
    • 戻り値の構造化束縛
      • 事後条件において、戻り値を構造化束縛して参照する拡張が可能である
    • 契約アノテーションの再利用
      • 同じ事前/事後条件の集合を共有する関数の間で、それを抽出してまとめて再利用できるようにする拡張が可能である
    • メタアノテーション
    • 引数をとるメタアノテーション
      • メタアノテーション構文はさらに、引数をとることができるように拡張可能である
    • ユーザー定義メタアノテーション
      • 標準で定義されるものと競合しない、ユーザー定義のメタアノテーションを可能にする拡張が可能である
    • 無視できないメタアノテーション
      • 標準属性の無視可能性ルールに適合しないようなメタアノテーションを可能にする拡張が可能である
    • 一次情報と二次情報の識別
    • クラス不変条件
      • クラス不変条件を表現するための新しい構文を導入可能であること
    • 手続き型インターフェース
      • P0465R0で提案されている手続き型インターフェースに対応可能であること
    • requires
      • 契約アノテーションrequires節で制約できるようにする拡張が可能であること
    • より一般的な拡張性
      • ここの要件のリストは大量だが、これでも将来的に契約機能に必要になる可能性のある拡張を網羅していない

これらの要件は排他的なものもあるため必ずしも全てを満たす必要はなく、それはトレードオフとして比較することを意図しています。

P2898R1 Build System Requirements for Importable Headers

モジュールにおけるインポート可能なヘッダ(importable header)というものを修正する提案。

以前の記事を参照

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

  • 誤解に基づいていたため、インポート構文に関するセクションを削除
  • インポート可能なヘッダの導入におけるインクリメンタルビルドのパフォーマンスの重要性を強調
  • フィードバック等に基づいて、推奨事項のセクションを書き換え
  • 新しい結論を反映するためにタイトルを変更

などです。

このリビジョンでの提案(推奨事項)は

  • 依存関係のスキャンでは(ヘッダユニットの)インポートをエミュレートする必要がある
    • 一般的には、#includeに基づくプリプロセスはimportを使用した時とほぼ同等と理解されている
    • しかし、それが誤って(その理解を破る形で)行われた場合にそれを検出したり検証する方法はない
    • 依存関係スキャンと実際のコンパイルとの間で動作及び理解を一貫させるためには、依存関係スキャンはimport処理を完全にエミュレートしなければならない
  • インポート可能なヘッダの検出を許可する
    • インポート可能なヘッダの実装における最大の課題の1つは、ヘッダユニットのリストとその引数を依存関係スキャンに提供する時に生じるボトルネックを回避するという要件が、そのステップでインポートを正しくエミュレートするための要件と矛盾していること
    • これは中間ターゲットが同じ出力を生成した場合に下流ターゲットの無効化を解除できるビルドシステムによって軽減できるが、その機能をサポートしていないビルドシステムでは使用できない
    • この提案ではその代替案として、依存関係スキャンが動的にどのヘッダがインポート可能かを識別できるようにすることを提案する
      • 実装する方法の1つとして、ヘッダと同じ場所に同じ名前かつ特定の拡張子で、識別のためのメタデータファイルを置く方法が考えられる
      • メタデータファイルはインポートプロセスエミュレーションのために必要なローカルプリプロセッサ引数に関する情報を含む
      • この方法の利点はヘッダのインクルードとインポートで同じ検索メカニズムが使用されること
  • ビルドグラフで動的ノードをサポートしないビルドシステムにおいても、インポート可能なヘッダが宣言されたリストが必要
    • ビルドグラフでノードの動的生成(つまりコンパイル実行時にコンパイル対象が増える)に対応していないビルドシステムでは、プロジェクトで必要となる可能性のあるすべてのバリエーションですべてのインポート可能ヘッダのビルドルールを事前生成する必要がある
      • 従って、インポート可能なヘッダのリストを事前に知っている必要がある

などです。

P2902R0 constexpr 'Parallel' Algorithms

並列アルゴリズムを定数式で使用できるようにする提案。

並列アルゴリズムとはC++17で追加された実行ポリシーを受け取るオーバーロードの事で、これれらのものは並列化やベクトル化によってアルゴリズムの実行時パフォーマンスを向上させることが目的であるため、cosntexpr指定はされておらず定数式では使用できません。

並列アルゴリズムも含めたアルゴリズムは他の処理や他のアルゴリズムと組み合わせて使用するものであり、他のアルゴリズムや標準ライブラリの多くのものがconstexpr対応を果たしている中で並列アルゴリズムconstexpr対応しない場合、そのような並列アルゴリズムと組み合わされた一連の処理を定数式で実行することができなくなります。

その場合でも、std::is_constant_evaluated()if constevalを使用して分岐をすればとりあえず対応はさせられますが、そもそも並列アルゴリズムconstexpであればより単純に目的を達成できます。

この提案はそのような目的のために並列アルゴリズムconstexpr対応させることを目指すものです。ただし、コンパイル時にも並列化を要求するものではありません。

この提案では、全ての実行ポリシーを定数式で指定可能とすることを提案しており、その実装はコンパイル時に対応する通常のアルゴリズム関数に処理を委譲することでコンパイル時実行することを意図しています。

P2904R0 Removing exception in precedence rule(s) when using member pointer syntax

メンバポインタ構文で()の使用を許可する提案。

クラスCのメンバBarのメンバポインタを取得する構文は&C::barのように書きますが、この時&(C::bar)のように括弧で括る形式は明確に禁止されており、コンパイルエラーとなります。

なぜこのような制限があるのかは不明ですが、この制限は不要のものと思われ他の場合と一貫していないため、この提案はこの制限を取り払うことを提案するものです。

struct C {
  void Bar(int);
};

int main() {
  void (C::*ptr)(int) = &(C::Bar); // MSVCはok、clang/gccはng
}

また、これは実装間で挙動の相違を生み出してしまっているようで、MSVCでは意図通り(この提案の提案通りに)になるようです。

P2905R0 Runtime format strings

P2905R1 Runtime format strings

std::make_format_args()が左辺値でフォーマット対象の値を受け取るようにする提案。

std::formatはフォーマット文字列のコンパイル時チェックを行うため、フォーマット文字列はコンパイル時に決定していなければなりません。そのため、フォーマット文字列を実行時に与えたい場合はstd::vformat()を使用します。

std::string str = translate("The answer is {}."); // gettextライブラリによる翻訳後文字列をフォーマット文字列として使う
std::string msg = std::vformat(str, std::make_format_args(42));

ただ、このv系の型消去APIはテンプレートの肥大化を回避するためのAPIであり、エンドユーザーではなくフォーマット内部実装や独自のフォーマット関数を作成する場合などに使用するものであるため、実行時のフォーマット文字列指定のために使用するのは本来の用途ではありません。

特に、std::make_format_args()は簡単に間違った使い方ができてしまいます。

std::string str = "{}";
std::filesystem::path path = "path/etic/experience";
auto args = std::make_format_args(path.string()); // path::string()はprvalueを返す

std::string msg = std::vformat(str, args);  // UB、argsの参照するstd::stringオブジェクトは寿命が尽きている

std::make_format_args()はフォーマット対象の値を型消去して渡すためのものであり、内部でその値を保持するわけではなく、一時オブジェクトを渡してもその寿命は延長されません。このことは、関数のインターフェースや効果などから読み取ることは難しく、これはあくまで内部実装のための関数です。

この提案はstd::make_format_args()のこの問題を改善するために、引数型を非const左辺値参照をとるように変更することで一時オブジェクトを渡すとコンパイルエラーになるようにしようとするものです。

namespace std {

  // make_format_args()の現在の宣言例
  template<class Context = format_context, class... Args>
  format-arg-store<Context, Args...> make_format_args(Args&&... fmt_args);

  // この提案による変更
  template<class Context = format_context, class... Args>
  format-arg-store<Context, Args...> make_format_args(Args&... fmt_args);
}

これによって、先ほどのようなコードはコンパイルエラーとして弾かれるようになります。

これは破壊的変更となりますが、この提案はC++23へのDRとすることがLEWGによって承認されています。

std::make_format_args()の利用率は低いと思われ、コンパイルエラーとなるのは一時オブジェクトか右辺値を渡した場合なので、正しい使用法のほとんどはこの提案の後でも変更の必要はありません。エラーとなる場合はstd::forward()してしまっているか、一時オブジェクトを直接渡しているかの場合のどちらかのはずで、前者はstd::forward()を削除する修正が必要となり、後者は一時オブジェクトの寿命を延長する変更が必要となります。

この提案のR0では、実行時のフォーマット文字列指定のためのAPIstd::runtime_format)も同時に提案していましたが、それは別の提案に分離されました。

P2906R0 Structured bindings for std::extents

std::extentsに構造化束縛サポートを追加する提案。

std::extentsstd::mdspanに対してその要素数を指定するためのクラステンプレートです。

// double型の3x3行列を表すmdspan
using mat33d = std::mdspan<double, std::extents<std::size_t, 3, 3>>;

// double型の3xN行列を表すmdspan(Nは実行時に決定)
using mat3nd = std::mdspan<double, std::extents<std::size_t, 3, std::dynamic_extante>>;

// double型の2次元行列を表すmdspan(サイズは実行時に決定)
using matd = std::mdspan<double, std::dextents<std::size_t, 2>>;

std::mdspanメンバ関数.extents()からこのstd::extentsオブジェクトは取得できて、std::extents.extent(n)からn + 1次元の要素数を取得できます。特に実行時にその要素数が決まる場合はこれを用いてループを回す必要があります。

// 3次元mdspanを出力する関数
template<typaname D, typename E, typename L, typename A>
void print_3d_mdspan(std::mdspan<D, E, L, A> mat) {
  using I = std::mdspan<D, E, L, A>::index_type;

  // 要素数情報の取得
  const auto e = mat.extents();

  // 全要素の走査
  for (I z = 0; z < e.extent(2); ++z) {
    for (I y = 0; y < e.extent(1); ++y) {
      for (I x = 0; x < e.extent(0); ++x) {
        // 要素取得
        const auto v = mat[z, y, x];

        // 出力
        std::print("[{}, {}, {}] = {}\n", z, y, x, v);
      }
    }
  }
}

現在のstd::extentsAPIは限定されており、次元ごとの要素数の取得はこうするしかありません。

この提案は、std::extentsを構造化束縛対応させることで、各次元の要素数取得をより簡易に行えるようにしようとするものです。

先ほどの例は例えば次のように書き直せます

// 3次元mdspanを出力する関数
template<typaname D, typename E, typename L, typename A>
void print_3d_mdspan(std::mdspan<D, E, L, A> mat) {
  using I = std::mdspan<D, E, L, A>::index_type;

  // 要素数情報の取得
  const auto [depth, height, width] = mat.extents();  // 👈 構造化束縛によって直接要素数を取得する

  // 全要素の走査
  for (I z = 0; z < depth; ++z) {
    for (I y = 0; y < height; ++y) {
      for (I x = 0; x < width; ++x) {
        // 要素取得
        const auto v = mat[z, y, x];

        // 出力
        std::print("[{}, {}, {}] = {}\n", z, y, x, v);
      }
    }
  }
}

さらに、P1061にて提案されている構造化束縛でパラメータパックを導入する機能を加味すると、このコードはさらに簡易に書くことができます

// 3次元mdspanを出力する関数
template<typaname D, typename E, typename L, typename A>
void print_3d_mdspan(std::mdspan<D, E, L, A> mat) {
  using I = std::mdspan<D, E, L, A>::index_type;

  // 要素数情報の取得
  const auto [...es] = mat.extents();

  // 全要素の走査
  for (const auto [...is] : std::cartesian_product(std::views::iota(0, es)...)) {
    // 要素取得
    const auto v = mat[is...];

    // 出力
    std::print("[{}, {}, {}] = {}\n", is..., v);
  }
}

このことを実装するにあたって、静的エクステントをどう扱うかについて選択肢があります。静的エクステントはコンパイル時に定まっている定数値であるため、構造化束縛でそのことを維持するかどうかで少し実装が異なります(構造化束縛はconstexpr指定できない)。

  1. 静的エクステントを実行時の値に降格する
  2. std::integral_constantなどを利用して、コンパイル時定数であることを維持する

オプション1に比べてオプション2では実装の複雑さが増大し、定数値の扱いが難しくなります(std::integral_constantの暗黙変換によって容易に定数性が失われるため)。

この選択に関してはLEWGの投票において決定される予定で、(この記事を書いてる時点では)まだその投票は行われていません。

P2910R0 C++ Standard Library Ready Issues to be moved in Varna, Jun. 2023

6月に行われたVarna会議でWDに適用されたライブラリに対するIssue報告の一覧

P2911R0 Python Bindings with Value-Based Reflection

提案中の値ベースリフレクションによってPythonバインディングの作成を簡素化できることを示す文書。

この文書は、pybind11によるPythonバインディングの作成に値ベースリフレクション(P1240R2)を活用することでその作業を簡素化できることを示し、その利点と課題についての議論を行おうとするものです。

例えば次のような列挙型があるとき

struct Execution {
  enum class Type {
    new_,
    fill,
    partial,
    cancelled,
    rejected
  };
};

これのPythonバインディングを作成するコードは次のようになります

py::enum_<Execution::Type>(/*binding scope*/, "Type")
  .value("new_" , Execution::Type::new_)
  .value("fill" , Execution::Type::fill)
  .value("partial" , Execution::Type::partial)
  .value("cancelled", Execution::Type::cancelled)
  .value("rejected" , Execution::Type::rejected);

これは列挙型のメンバを手動で展開しているため、繰り返し同じようなコードを書かなければならず、Execution::Typeが更新されるとこちらも手動で更新しなければならないなど、不便な面があります。

対して、値ベースリフレクションを利用すると同等のバインディングは次のようなコードで完結させられます。

bind_enum<Execution::Type>(/*binding scope*/);

bind_enum()はこの目的に書かれた汎用的なコードであり、ライブラリなどとして提供されることを意図しています。そして、このbind_enum()は次のように実装できます

// 型名だけを取り出す関数
template<typename T>
std::string basename() {
  auto name = std::string{name_of(^T)}; // ^T reflects type T

  if (size_t pos = name.rfind(':'); pos != std::string::npos) {
    return name.substr(pos + 1);
  }
  
  return name;
}

template<typename EnumT, typename Scope>
void bind_enum(Scope& s) {
  auto enum_ = py::enum_<EnumT>(s, basename<EnumT>().c_str());

  // 列挙型EnumTの全てのメンバに対してイテレートする
  template for (constexpr auto e : members_of(^EnumT)) {
    enum_.value(name_of(e), [:e:]); // [:e:] un-reflects e
  }
}

このように、値ベースリフレクションを用いると、型のメンバなどを列挙しその名前を取得してそれを特定の関数に投げることを繰り返すような処理をかなり自動化して書くことができ、現在手動で書かざるをえずに不便やバグの元となっている部分をより簡易かつ安全に記述することができます。

提案には、そのほかにもクラスメンバや継承、関数オーバーロードなどの同様の例が記載されています。

これらの試みによって得られた利点と課題は次のようなものです

  • 利点
    • ボイラープレートの大幅(~95%)な削減
    • リフレクションを使用すると多くの場合にエラーの可能性を軽減しうる
      • 元のC++コード変更への追随忘れなどを回避できるなどによる
    • ほとんどのバインディングは慎重に選択されたデフォルトの動作を使用して合理的に自動化できる
      • この提案では、pybind11で指定されたデフォルトを活用している
  • 課題
    • バインディングのカスタマイズ機能はPythonC++の言語機能の違いをカバーする必要がある
      • そのサポートのために、ユーザー定義属性があると便利だと思われる
    • パラメータ名リフレクションなど、いくつかのリフレクション機能は危険
      • Pythonの名前付き引数をサポートさせるために、C++の関数引数名をリフレクトしイテレートする方法が考えられるが、C++においては関数引数名はそのシグネチャの一部ではなく、宣言/定義によって変わりうる
    • いくつかのコーナーケースでは、リフレクションに基づく自動化は問題を隠蔽し、誤った安心感を与える可能性がある
    • rangeのリフレクションをその要素名のリストに展開する機能があると便利な場合があった

この文書はこのようなことを行うライブラリを提案しているのではなく、値ベースリフレクションの利点を示すとともに足りないものやあった方がいい機能などを示し、その議論を前進させようとするものです。

P2912R0 Concurrent queues and sender/receivers

P1958R0で提案されているbuffer_queueにP2300のsender/receiverモデルをベースとしたAPIを拡張する提案。

P1958R0のbuffer_queueは、内部の固定長バッファをリングバッファとして使用して同期/非同期の両方のAPIを提供することで並行プログラミングで使用可能な並行キューを提供しようとするものです。

この提案は、このbuffer_queueをP2300のsender/receiverに適応可能なように拡張しようとするものです。また、それに伴ってAPIのスタイルも変更されています。

提案及び実装より、buffer_queueの概要

template <typename T, typename Alloc = std::allocator<T>>
class buffer_queue {
public:
  using value_type = T;

  explicit buffer_queue(size_t max_elems, Alloc alloc = Alloc());
  ~buffer_queue() noexcept;

  // observers
  bool is_closed() noexcept;
  size_t capacity() const noexcept;

  // modifiers
  void close() noexcept;

  // 同期pop
  T pop();
  std::optional<T> pop(std::error_code& ec);
  std::optional<T> try_pop(std::error_code& ec);

  // 同期push(コピー)
  void push(const T& x);
  bool push(const T& x, error_code& ec); // used to be wait_push
  bool try_push(const T& x, error_code& ec);

  // 同期push(ムーブ)
  void push(T&& x);
  bool push(T&& x, error_code& ec); // used to be wait_push
  bool try_push(T&& x, error_code& ec);

  // 非同期push/pop
  sender auto async_push(const T& x) noexcept(is_nothrow_copy_constructible_v<T>);
  sender auto async_push(T&& x) noexcept(is_nothrow_move_constructible_v<T>);
  sender auto async_pop() noexcept;
};

この提案で追加されたのは、async_~という名前の3つの操作です。これらは非同期的にpush/popを行う関数で戻り値としてsenderを返します。これによって、非同期操作の待機や継続はP2300のsenderアルゴリズムを利用して行うことができます。

また、同期操作のインターフェースも変更されており、操作の成否を判定できるインターフェースがstd::error_codeを受け取るようになり、pop()操作では戻り値がstd::optionalを返すようにされています。

P2915R0 Proposed resolution to CWG1223

後置戻り値型関数宣言との曖昧さを解消するために、autoの存在を構文的に扱うようにする提案。

後置戻り値型関数宣言はautoを戻り値型に持つ関数宣言でのみ考慮されますが、この制限は意味的なものであり構文的なものではありませんでした。それによって、C++11以前は有効だった変数宣言や式が関数宣言として扱われてしまう場合がありました。

struct A {
  A(int *);
  A *operator()(void);
  int B;
};

int *p;
typedef struct BB { int C[2]; } *B, C;

void foo() {
  A (p)()->B;  // ng、関数宣言として扱われる(C++11以降

  A a(B ()->C);       // ng、関数宣言 or 変数宣言
  sizeof(B ()->C[1]); // ng、関数宣言に対するsizeof or 式に対するsizeof
}

この提案は、この場合にautoの存在を構文的にもチェックし制限することでこの問題を解決しようとするものです。この提案後、上記コードは次のようにコンパイルされるようになります

void foo() {
  A (p)()->B;  // ok、式(A::Bへのメンバアクセス)

  A a(B ()->C);       // ok、変数宣言
  sizeof(B ()->C[1]); // ok、式に対するsizeof
}

提案文書より、その他の例

struct M;
struct S {
  S* operator()();
  int N;
  int M;

  void mem(S s) {
    auto(s)()->M; // S::Mは::Mを非表示にし、これは式とみなされる
  }
};

void f(S s) {
  {
    auto(s)()->N; // 式
    auto(s)()->M; // 関数宣言(::Mが見えているため)
  }
  {
    S(s)()->N; // 式(autoがないため関数宣言とはみなされない)
    S(s)()->M; // 式(autoがないため関数宣言とはみなされない)
  }
}

この問題はIssue1223としてC++11に対して2010年に提出されて以降放置されていましたが、C++23にてauto(x)のdecay-copy構文が導入されたことによって影響が大きくなることがわかったため、この提案で迅速に解決されました(既に2023年6月の会議で承認され、C++26WDに取り込まれています)。

P2917R0 An in-line defaulted destructor should keep the copy- and move-operations

P2917R1 An in-line defaulted destructor should keep the copy- and move-operations

クラス定義内でdefault宣言されたデストラクタがある場合に、コピー/ムーブコンストラクタ及び代入演算子を暗黙default宣言するようにする提案。

C++11以降、default含むユーザー宣言デストラクタが存在する場合、そのクラスのコピーコンストラクタは暗黙的にdefault定義されますがそれは非推奨とされます。また、そのようなクラス型のムーブコンストラクタ及び代入演算子は暗黙定義されません。

class Apple {
public:
  ~Apple() = default;
  // コピーコンストラクタ及び代入演算子は暗黙default定義される(非推奨の振る舞い)
  // ムーブコンストラクタ及び代入演算子は暗黙定義されていない
};

このAppleのようなクラスをコピー/ムーブ可能にしたければ、全てのメンバ関数を明示的に宣言しなければなりません。

class Apple {
public:
  ~Apple() = default;
  
  // コピーを有効化
  Apple(const Apple&) = default;
  Apple& operator=(const Apple&) = default;

  // ムーブを有効化
  Apple(Apple&&) = default;
  Apple& operator=(Apple&&) = default;
};

このことは、特殊メンバ関数のデフォルト宣言というボイラープレートコードを量産しており、また、デストラクタは他の特殊メンバ関数とは独立しているべきだとして、この提案はこの制限を解除しようとするものです。

この提案では、クラスの最初のデストラクタの宣言がdefault宣言である時、コピー/ムーブコンストラクタ及び代入演算子を暗黙default宣言するようにします。それによって、上記のAppleのようなクラスは追加の特殊メンバ関数の宣言なしでコピーとムーブが可能になります。

class Apple {
public:
  ~Apple() = default;
  // コピー/ムーブコンストラクタ及び代入演算子は暗黙default定義される
};

ただし、最初のデストラクタの宣言がdefaultではない場合、すなわちクラス定義外でdefault宣言されている場合は現行通りにムーブコンストラクタ/代入演算子は定義されず、コピーコンストラクタ/代入演算子の暗黙default定義は非推奨です。

class Apple {
public:
  ~Apple();
  // コピーコンストラクタ及び代入演算子は暗黙default定義される(非推奨の振る舞い)
  // ムーブコンストラクタ及び代入演算子は暗黙定義されていない
};

Apple::~Apple() = default;

この提案の内容は、以前のコードに破壊的変更をもたらします。

// 現在このクラスはムーブ可能ではないが、この提案の後ではムーブ可能となる
struct A {
  std::unique_ptr<int> pi;
  virtual ~A() = default;
};


// 現在このクラスはムーブ可能ではないが、この提案の後ではムーブ可能となる
struct B {
  std::string s;
  virtual ~B() = default;
};

void f() {
  B b1;
  B b2;

  b2 = std::move(b1); // 現在はコピーされているが、この提案の後ではムーブされる
}


// 現在このクラスはムーブ可能ではないが、この提案の後ではムーブ可能となる
struct C {
  // ムーブ操作を無効化することを意図するデストラクタ宣言
  ~C() = default;
};


struct Base {
  int x{};
  
  virtual ~Base() = default;

  // その他virtualメンバ関数
};

struct Derived : Base {
  int y{};
};

void g() {
  Base b{};
  Derived a = b;  // スライスが発生する、この提案後は気付きづらくなる可能性がある
}

P2918R0 Runtime format strings II

P2918R1 Runtime format strings II

std::format()の実行時フォーマット文字列のためのAPIを追加する提案。

モチベーションの一部は少し上のP2905R1と共通しているのでそちらもご覧ください。

std::format()のフォーマット文字列はコンパイル時検査される関係上、コンパイル時に確定している文字列でなければなりません。実行時文字列によってフォーマット文字列を指定したい場合はstd::vformatなどを使用することになるのですが、これは内部実装用のもので利用しやすいAPIではありませんでした。

この提案は、実行時文字列によるstd::formatのための専用のAPIを追加することで、実行時フォーマット文字列によるstd::formatの利便性を改善しようとするものです。

提案では、std::runtime_format()という関数に実行時フォーマット文字列を渡し、この関数の戻り値をstd::format()のフォーマット文字列(第一引数)として渡すことで実行時フォーマット文字列によるフォーマットをstd::format()に組み込みます。

void f(std::string_view str) {
  // 現在の実行時文字列によるフォーマット文字列指定
  auto rfmt1 = std::vformat(str, std::make_format_args(42));

  // この提案
  auto rfmt2 = std::format(std::runtime_format(str), 42);
}

std::runtime_format()は受け取った文字列をラップする専用の型を返し、std::basic_format_stringがその型を受け取れるようにすることでこのサポートを行えるようにしており、フォーマット文字列の検証はコンパイル時と同様にstd::basic_format_stringのコンストラクタで(実行時に)行われます。

P2920R0 Library Evolution Leadership's Understanding of the Noexcept Policy History

LEWGの管理者が認識する、noexceptの指定ルールについてのこれまでとこれからの議論についてのまとめ。

おそらく、5月公開の提案においてLakos Ruleに関する提言が相次いだことを受けてのものです。Lakos Rule周りの議論についての歴史がまとめられており、今回提出された提案を受けてどう議論をしていくのかなどがスライドで説明されています。

P2921R0 Exploring std::expected based API alternatives for buffer_queue

P2912R0で提案されているbuffer_queueAPI拡張について、std::expectedを返すAPIを検討する提案。

少し上のP2912R0によるP1958R0のbuffer_queueAPI拡張においては、一部の失敗しうる操作に対してstd::error_codeを受けてそこにエラー情報を出力する無例外APIを提案していました。

template <typename T, typename Alloc = std::allocator<T>>
class buffer_queue {
  
  ...

  // 同期pop
  std::optional<T> pop(std::error_code& ec);
  std::optional<T> try_pop(std::error_code& ec);

  // 同期push(コピー)
  bool push(const T& x, error_code& ec);
  bool try_push(const T& x, error_code& ec);

  // 同期push(ムーブ)
  bool push(T&& x, error_code& ec);
  bool try_push(T&& x, error_code& ec);

  ...
};

この提案は、これらのAPIstd::error_codeを受け取るのではなくstd::expectedを返すようにすることを検討するものです。

この提案ではいくつかのタイプのAPIを検討しています。

まず一つは、std::error_codeの代わりにstd::nothrowを渡してstd::expectedを受けることを明示するタイプです。

template <typename T, typename Alloc = std::allocator<T>>
class buffer_queue {
  
  ...
  
  // P2912R0のAPIの同期push
  void push(const T& x);
  bool push(const T& x, error_code& ec);

  // expectedを返すAPI
  void push(const T&);
  auto push(const T&, nothrow_t) -> expected<void, conqueue_errc>;
  ...
};

int main() {
  buffer_queue<T> q{};

  // P2912R0
  std::error_code ec;
  if (q.push(5, ec))
    return;
  println("got {}", ec);

  // この提案
  if (auto result = q.push(5, nothrow))
    return;
  else
    println("got {}", result.error());
}

この場合の利点は、API呼び出し前にerror_codeオブジェクトを用意しなくても良いところです。

もう一つは、例外を投げうるAPIを削除して、全てstd::expectedを返すAPIに統一するものです。

template <typename T, typename Alloc = std::allocator<T>>
class buffer_queue {
  
  ...
  
  // P2912R0のAPIの同期push
  void push(const T& x);
  bool push(const T& x, error_code& ec);

  // expectedを返すAPI
  auto push(const T&) -> expected<void, conqueue_errc>;

  ...
};
int main() {
  buffer_queue<T> q{};

  // 無例外の例
  {
    // P2912R0
    std::error_code ec;
    if (q.push(5, ec))
      return;
    println("got {}", ec);

    // この提案
    if (auto result = q.push(5))
      return;
    else
      println("got {}", result.error());
  }

  // 例外を投げる例
  {
    // P2912R0
    try {
      q.push(5);
    } catch(const conqueue_error& e) {
      ...
    }

    // この提案
    try {
      q.push(5).or_else([](auto code) {
        throw conqueue_error(code);
      });
    } catch(const conqueue_error& e) {
      ...
    }
  }
}

この場合、APIは基本的に例外を投げないため、標準ライブラリの他の部分(特にコンテナ)と一貫性がなく、1つ目のAPI候補よりも劣っています。

また、この提案ではtry系関数について連想コンテナのtry_emplace()の設計を踏襲して、キューに値が挿入されない場合は渡された引数は変更されないようにすることを推奨しています。そしてその場合に、それに反してstd::expectedのエラー値で渡された値を返すAPIについても検討しており、その場合はユーザーに対する負担が大きくなると報告されています。

もう一種類、try_emplace()の設計を踏襲する方針のもと、1つ目のAPI候補においてtry系関数でstd::nothrowの指定を省略するAPIについても検討されています。

とはいえこの提案の結論としては、std::expectedベースのこれらのAPIがP2912R0で提案されているAPIと比較して明確に改善されているとは思えない、と報告しています。

P2922R0 Core Language Working Group "ready" Issues for the June, 2023 meeting

6月に行われたVarna会議でWDに適用されたコア言語に対するIssue報告の一覧。

P2925R0 inplace_vector - D0843R7 LEWG presentation

提案中のinplace_vectorの紹介スライド。

inplace_vectorP0843R8で提案されています。

ここでは、要素を追加するインターフェースについて解説されているほか、各インターフェースとstd::vectorとの比較ベンチマークの結果も記載されています。

P2926R0 std::simd types should be regular - P2892R0 LEWG presentation

P2892R0の解説スライド。

P2892R0の反対意見がいくつか紹介された上で、それに対する回答が肯定的立場から説明されています。

P2929R0 simd_invoke

std::simdで組み込み関数の使用を簡易にする呼び出しラッパ関数の提案。

C++26を目指して提案中のstd::simdは、任意の環境で効率的なデータ並列処理を簡易に書くことができるようにするためのデータ並列型です。そのため、std::simdは多くのプラットフォームで使用可能なSIMD演算等を意識して設計されています。

それによって、特定のハードウェアでは組み込み関数(intrinsic)を使用してより効率的なSIMD演算を呼び出したい場合が想定されます。そのためには、std::simdが保持する値を環境のSIMDレジスタに移して、それを用いて組み込み関数を実行し、その結果を再びstd::simdに保持させる、といったことをする必要があります。

このために、現在のstd::simdにはネイティブのSIMDデータ型とやり取りするためのコンストラクタと変換演算子を実装定義で追加することが許可されています

template<class T, class Abi = ...>
class simd {
  ...

  // ネイティブのSIMDデータ型からの変換コンストラクタ
  constexpr explicit simd(const implementation-defined& init);

  // ネイティブのSIMDデータ型への変換演算子
  constexpr explicit operator implementation-defined() const;

  ...
}

これを用いて、std::simdを組み込み関数呼び出しのために必要なネイティブのSIMDデータ型へ変換しで組み込み関数を呼び出すことができます。また、std::simdのコンストラクタはこの逆の変換を行えるため、その結果をstd::simd型に戻すこともそのまま行えます。

// _mm256_addsub_psは奇数要素を足し算し、偶数要素を引き算するintel AVXの組み込み関数
// std::simdには対応するマッピングがない
auto addsub(simd<float> a, simd<float> b) -> simd<float> {
  return static_cast<simd<float>>(_mm256_addsub_ps(static_cast<__m256>(a), static_cast<__m256>(b)));
}

ただし、この変換はstd::simdのサイズがネイティブのSIMDレジスタサイズに等しい場合にのみ正しく動作します。ネイティブのSIMD幅と異なる場合はそれを考慮しなければならず、ネイティブのSIMD幅を超え複数のレジスタにまたがるサイズの場合はこのような変換は利用できません。

その場合は、大きな幅のstd::simd値をネイティブのSIMDレジスタサイズにマッチするサイズで分割して順番に組み込み関数を呼び出して、その結果をstd::simdに格納するようなことをすることになります。

// AVXを使用する環境で、ネイティブSIMDレジスタサイズは256(floatx8)とする
// simd<float, 16>はネイティブSIMD幅の倍のサイズ
auto addsub(simd<float, 16> a, simd<float, 16> b) -> simd<float, 16> {

  // ネイティブのレジスタサイズごとに分割
  auto [lowA, highA] = simd_split<simd<float>>(a);
  auto [lowB, highB] = simd_split<simd<float>>(b);

  // 分割した部分ごとに組み込み関数を呼び結果を取得
  auto resultLow = simd<float>(_mm256_addsub_ps(static_cast<__m256>(lowA),
                                                static_cast<__m256>(lowB)));
  auto resultHigh = simd<float>(_mm256_addsub_ps(static_cast<__m256>(highA),
                                                 static_cast<__m256>(highB)));

  // 分割して処理した結果をsimd<float, 16>に再結合する
  return simd_concat(resultLow, resultHigh);
}

この例はネイティブのSIMDレジスタサイズの倍のstd::simd値に対してのみ動作し、異なるサイズの場合はそれに合わせて書き直す必要があるほか、ネイティブのSIMDレジスタサイズの整数倍とならないようなサイズの場合はそのための対応が必要となります。そのようなコードを書くことは難しくはなくほとんど典型的なコードとなると思われますが、それでも冗長であり、汎用的なソリューションでは技巧的なコードを書くことになります。

ここでやるべきことは、std::simd値の引数をネイティブのSIMDレジスタサイズで分割し、その部分ごとに組み込み関数を呼び出し、その結果をstd::simd値に再びまとめる、ということです。やるべきことのほとんどは典型的な処理であり、個別ユーザーそれぞれにそれを書かせる代わりに、この一連の手順を抽象化した一般的なメカニズムとして提供することができます。この提案ではそれを、simd_invoke()という関数テンプレートとして提案しています。

template<typename Fn, typename... Args>
auto simd_invoke(Fn&& fn, Args&&...);

fnはネイティブのSIMDレジスタサイズのstd::simd値を受け取って組み込み関数を呼びだす部分を指定する呼び出し可能なものです。例えば、先ほどの_mm256_addsub_psを呼び出す関数の場合は次のようになります

// AVXレジスタ1つ分の_mm256_addsub_psを処理する
inline auto native_addsub(simd<float> lhs, simd<float> rhs) {
  auto nativeLhs = static_cast<__m256>(lhs);
  auto nativeRhs = static_cast<__m256>(rhs);

  return simd<float>(_mm256_addsub_ps(nativeLhs, nativeRhs));
}

これを利用して、先ほどのようなネイティブのSIMDレジスタサイズを超える幅のstd::simd値で組み込み関数を呼びだすコードは次のように書き直せます

auto addsub(simd<float, 32> x, simd<float, 32> y) {
  return simd_invoke(native_addsub, x, y);
}

入力のstd::simd値をネイティブSIMD幅ごとに分割して組み込み関数を呼びだす部分と、その処理結果を再統合する部分はsimd_invoke()が勝手にやってくれています。それによって、ユーザーは単位SIMD幅のstd::simd値に対して何をするかだけを用意する(上記native_addsub()のように)だけでSIMD幅を気にせずにstd::simdを使って組み込み関数を呼びだすことができるようになっています。

先ほどの例の直接比較

現在 この提案
auto addsub(simd<float, 16> a, simd<float, 16> b) -> simd<float, 16> {

  auto [lowA, highA] = simd_split<simd<float>>(a);
  auto [lowB, highB] = simd_split<simd<float>>(b);

  auto resultLow = simd<float>(_mm256_addsub_ps(static_cast<__m256>(lowA),
                                                static_cast<__m256>(lowB)));
  auto resultHigh = simd<float>(_mm256_addsub_ps(static_cast<__m256>(highA),
                                                 static_cast<__m256>(highB)));

  return simd_concat(resultLow, resultHigh);
}
auto addsub(simd<float, 16> a, simd<float, 16> b) {
  auto do_native = [](simd<float> lhs, simd<float> rhs) {
      return simd<float>(_mm256_addsub_ps(
          static_cast<__m256>(lhs),
          static_cast<__m256>(rhs)));
  };

  return simd_invoke(do_native, x, y);
}

また、ネイティブのSIMDレジスタサイズの整数倍とならないような入力に対してより適切に組み込み命令を選択することもできます

auto addsub(simd<float, 19> x, simd<float, 19> y) {
  // AVXでfloat 19要素の場合、8, 8, 3 と分割して処理する

  // 入力のsimdのサイズごとに適切な組み込み関数を選択する
  auto do_native = 
    []<typename T, typename ABI>(basic_simd<T, ABI> lhs, basic_simd<T, ABI> rhs) {
      constexpr auto size = basic_simd<T, ABI>::size;
      if constexpr (size <= 4) {
        // 4要素(128bit)SIMD
        return simd<float, size>(_mm_addsub_ps(static_cast<__m128>(lhs),
                                               static_cast<__m128>(rhs)));
      } else {
        // 8要素(256bit)SIMD
        return simd<float, size>(_mm256_addsub_ps(static_cast<__m256>(lhs),
                                                  static_cast<__m256>(rhs)));
      }
  };

  return simd_invoke(do_native, x, y);
}

他にも、simd_invoke()はNTTP値で指定することでより狭いレジスタ幅で処理をするようにできたり、インデックスを同時に渡すsimd_invoke_indexed()が用意されていたりします

// より狭いレジスタ幅で呼び出しを行う例
auto addsub(simd<float, 32> x, simd<float, 32> y) {
  auto do_native = [](simd<float, 4> lhs, simd<float, 4> rhs) {
    return simd<float, 4>(_mm_addsub_ps(static_cast<__m128>(lhs), static_cast<__m128>(rhs)));
  };

  // NTTPでレジスタサイズを指定
  return simd_invoke<4>(do_native, x, y);
}


// 組み込み関数によって何か特殊なメモリストアを行う例
// _mm256_special_store_psは実際には存在しない
auto special_memory_store(simd<float, 32> x, float* ptr) {
  // ptrの領域に適切に出力していくために、オフセットが必要
  auto do_native = 
    [=]<typename T, typename ABI>(basic_simd<T, ABI> data, auto idx) {
      (_mm256_special_store_ps(ptr + idx, static_cast<__m256>(data)));
  };

  // 分割ごとの通し番号を同時に渡す
  simd_invoke_indexed(do_native, x);
}

P2930R0 Formatter specializations for the standard library

標準ライブラリのクラス型について、std::formatで直接文字列化できるようにする提案。

C++20でstd::formatが追加されると同時に基本型のためのフォーマット指定構文が導入され、C++23ではstd::printが追加されるとともに任意のrange型やstd::tupleなどがサポートされるようになりました。しかし、依然としてその他の多くの標準ライブラリのクラス型にはstd::formatのサポートがなく、効率的に基本型へ変換できない場合は文字列化するためには従来のストリーム出力(<<)に頼るしかありません。

この問題は以前から認識されており、P1636R2で同様の提案がなされLEWGでの設計合意を得ていましたが、著者の方と連絡が取れなくなったため議論は停止していました。この提案は、P1636R2をベースに、C++23におけるstd::formatterの改善なども盛り込みながら、より多くの標準ライブラリクラス型をフォーマット可能にすることを目指すものです。

この提案でフォーマットサポート追加を提案しているのは次のものです

  • std::bitset
  • std::byte
  • std::complex
  • std::error_category
  • std::error_code
  • std::error_condition
  • std::sub_match

これ以外のものはそれぞれ簡単には解決できない問題があるとしてここでは提案されていません(ただし、std::filesystem::pathは別の提案で提案されています)。

P2931R0 WG21 February 2023 Meeting Record of Discussion

2023年6月のWG21全体会議(Varna会議)の議事録

P2937R0 Freestanding: Remove strtok

フリースタンディングライブラリ機能として指定するものから、strtokを外す提案。

strtokP2338R4によってC++26のWDに対してフリースタンディング機能であると指定されました。

strtokは実際にはその動作のためにグローバルストレージを使用しており、スレッドセーフであることが保証されません。そのためP2338の初期のリビジョンには含まれていなかったのですが、C2X(Cの次期バージョン)に対してstrtokがフリースタンディングであると指定されたことで、P2338も最終的にはそれに追随しました。

しかしその後、C2XのCD2(comitee draft)の段階でstrtokのフリースタンディング指定は解除されたため、この提案はC++でも同様にstrtokをフリースタンディングでは無くすことを提案しています。

P2940R0 switch for Pattern Matching

パターンマッチングの構文として、switchを拡張する方向性についての提案。

現在のパターンマッチングの議論においては、swicthを再利用するのではなくinspectという新しいキーワードによってパターンマッチングのための領域を導入する方向性で議論が進んでいます。これは主に次の2つの理由によっています

  1. swicth構文が古いswitch文なのかパターンマッチ構文なのかを判別するのが難しい
  2. 古いswicthとパターンマッチでは機能や意味論が異なるためそれを教育する際の懸念がある

この提案は、どちらの問題も解決可能であるとして、switch構文を拡張する形でパターンマッチングを導入しようとするものです。

1つ目の問題の解決として、この提案ではswitchキーワード直後の値を指定する部分を()ではなく[]を使用することで区別することを提案しています。

// 現在のswitch文
switch (a) {
  ...
}

// この提案によるパターンマッチ構文、基本形
switch [a] {
  ...
};

// この提案によるパターンマッチ構文、aとbの両方でマッチングする
switch [a, b] {
  ...
};

これによって導入されるswitchは式となり、通常のswitch同様の構文(casedefaultラベルによって)でマッチングを記述でき、また現在提案中のパターンマッチング構文と同様の構文(=>など)を用いてマッチングし値を返すことができます(casedefaultラベルによるマッチングの場合は値を返すことができない)。ただしこのために、[]によるswitchでは末尾に;が必要とされます。

この提案のswitch式はフォールスルーに関してswitch文と異なるデフォルトとすることを提案しており、明示的にOR条件を記述しない限りフォールスルーしないようにしています。

switch文 switch式
auto some_value = 2;

switch (some_value) {
  case 1:   
    ... 
    break;
  case 2:   
    ... 
    // break忘れ、フォールスルーしてしまう
  case 3: 
    ... 
    break;
  ...
}
auto some_value = 2;

switch [some_value] {
  case 1:   
    ... 
    break;
  case 2:   
    ... 
    // break忘れ、フォールスルーしない
  case 3: 
    ... 
    break;
  ...
};

このフォールスルーに関して以外は、新しいswitch式は現在のswitch文の機能を包含しています。これによって、現在のswitch文はパターンマッチのより制限された場合と見ることができるようになり、教育において2つを区別する必要は無くなります(2つ目の問題を解決します)。

提案より、その他の例

auto some_value = string("hi");

// 文字列のマッチング
switch [some_value] {
  case "hi":   // handle "hi"
  case "bye":  // handle "bye" 
  default:     // handle all else
};

auto some_value2 = Point(12, 13);

// 現在のパターンマッチング構文によるマッチング
switch [some_value2] {
  case [0, 0]: // handle point at origin 
  case [0, _]: // handle x at origin 
  case [_, 0]: // handle y at origin 
  ...
};

auto some_value3 = true;

// 値を返すswitch式、戻り値型は推論され、const char*になる
auto result = switch [some_value3] {
  true => "yes";
  false => "no";
};

// 値を返すswitch式、戻り値型はstd::string
auto result = switch [some_value] -> std::string {
  true => "yes";
  false => "no";
};

enum class Op { Add, Sub, Mul, Div };

// caseラベルと=>を混合させることで、値を返さないパターンを簡易に記述できる
Op parseOp(Parser& parser) {
  return switch [parser.consumeToken()] {
    '+' => Op::Add;
    '-' => Op::Sub;
    '*' => Op::Mul;
    '/' => Op::Div;
    case [[noreturn]] let token: {
      std::cerr << "Unexpected: " << token;
      std::terminate();
    }
  };
}

これらの例が示すように、パターンマッチングサポートのためにswitchを拡張することは可能でありかつ自然な拡張となり、導入キーワードやcase記述などを再利用することで言語の変更を抑えることができ、現在のswitchの自然な拡張となることで初心者が学ぶべきことも減少します。

P2941R0 Identifiers for Pattern Matching

パターンマッチング中で使用される識別子を区別可能なパターンマッチング構文の提案。

現在議論中のパターンマッチング構文においては、マッチングを記述するところに識別子(なんらかの名前)を使用してマッチングを行うことができます。この時問題となるのは、そのように使用されている識別子が型なのか変数なのかはたまた新しく導入されたものなのかわからない場合があるという点です。

static constexpr int zero = 0, one = 1;
int v = 42;

inspect (v) {
  zero => { std::cout << zero; }  // zeroはどれ?
};

他言語におけるパターンマッチングを見てみてもこの問題に正解はないようです。しかし、次の2つの理由により、C++ではこのことが深刻な問題となる可能性があります

  1. C++には変数導入のキーワードがない
    • 例えば、C#varやswiftのletのようなもの
  2. C++では変数と型が同じ名前を持つことができる

このことは現在ではあまり問題とはなりませんが、パターンマッチング構文の柔軟さと合わさると複雑な問題を引き起こします。

パターンマッチング構文内でxという識別子が表れているとき、それは次の3つのいずれかであるはずです

  • xはパターンマッチング構文内で新しく導入された変数もしくはバインディング(マッチング対象の値のサブオブジェクトへの参照)
  • xは比較対象となる外部の変数
  • xは型

これに対して、現在のパターンマッチング提案は次のようになっています

  • P1371R3
    • デフォルトではバインディングを作成する
    • 外部の変数を比較対象とする時は、caseを使用する
    • 型名は<type>のように記述する
  • P2688(P1371の将来バージョン)
    • バインディングの作成はletで行う
    • 外部変数は単なる式として扱われ、識別子名のデフォルトは外部変数となる
    • 型についてはそのまま
  • P2392(is asを使用する提案)
    • 識別子がis asの左側にある場合、バインディングが作成される
    • 外部変数はisの右側にくる
    • 型もis asの右側にくる

ただし、どちらのアプローチにおいても式と型名を同じ構文で指定できてしまうため、型のマッチング記述に問題があります。

P1371R3のAlternative Pattern(パターンマッチング対象の値を直接マッチングする代わりに、そこから何かしら値を取り出してそれによってマッチングする。std::varintのマッチングなど)

void sample1(auto some_value) {
  constexpr auto size = 13;

  inspect (some_value) {
    <size> => // マッチングには定数13が使用される
  };
}

void sample2(auto some_value) {
  using size = int;

  inspect (some_value) {
    <size> => // マッチングにはint型が使用される
  };
}

一応この場合<>の中に指定する式は定数式である必要はあります。

P2392の場合

void sample1(auto some_value) {
  constexpr auto size = 13;

  inspect (some_value) {
    is size => // some_valueが13に等しい時にマッチング
  };
}

void sample2(auto some_value) {
  using size = int;

  inspect (some_value) {
    is size => // some_valueの型がintの時にマッチング
  };
}

同じ構文によって異なるマッチングができてしまうことによって、ある時点で識別子の意味が変化するとパターンマッチングの意味もまた静かに変化してしまいそれに気づけない可能性があります。より大規模な関数ではこのような状況が容易に発生することが予想されます。

また逆に、同じ名前で別の種類のエンティティがパターンマッチングのスコープに導入されている場合も考えられます。マッチングの記述方法によっては問題とならない場合もありますが、上記のように式も型も書ける構文でそのような識別子を使用してしまうとコンパイルエラーとなります。

いずれにせよ、この型マッチング記述の曖昧さの問題によってパターンマッチングはプログラマの意図通りに動作せず、さらに悪い場合は動作しているように見えてしていないという問題に頭を悩ませることになる未来があり得ます。

この問題の根本的原因はパターンマッチングによって導入されたわけではありませんが、今日のコードで問題となる場合はほとんどありません。なぜなら、識別子名が型であるか変数であるかが競合する場所というのは言語機能としては存在していないからです。NTTPと型を受け取る関数テンプレートのようにそのような場所を作り出すことはできますが、これを使用する場合は何らかのライブラリAPIとして使用することになり、それは言語機能ではありません。

パターンマッチングの場合は言語機能で識別子名の種類が競合するコンテキストが表れてしまっています。現在のC++の式は、同じ名前の型や変数の有無で意味が切り替わることはなく、コンパイルエラーとなるはずです。パターンマッチングは現在の言語の振る舞いより悪くなることはなくむしろ良い振る舞いをしなければなりません。そうならない場合、新しい罠を生み出してしまい、それを回避するためのガイドラインを普及させることになります。

また、パターンマッチで外部変数を使用する場合の構文として、マッチングとその結果あるいはサブオブジェクトの代入を同時に行うものが考えられます。例えば、構造化束縛を拡張して既存の変数に代入するようなもの(std::tie()のような)が考えられます

some_t a,b;

// 構造化束縛構文を拡張して既存の変数に代入するようにしたもの
[a,b] = something;

これはパターンマッチングと関係なく有用であり導入可能である可能性がありますが、同様にパターンマッチングにおいても有用である可能性があります。その場合、パターンマッチングで現れる識別子の種別として代入に使用される既存変数名が追加されます。

さらに、パターンマッチングにおいてバインディングを作成する場合、その不変性を制御できることが望ましいことは明らかです。現在のパターンマッチング提案ではこれはマッチング対象のオブジェクトのconst性とその伝播の通常のルールによって制御されますが、それは次のような理由から完璧ではありません

  • パターンマッチングの全ての分岐パスで同じ使い方をするわけではなく、分岐によってconstでよかったり変更が必要だったり変化しうる。マッチング全体で一律にconstであるかそうでないかを指定させることは、全体として最適なconst性を表現できなくなる
  • ポインタやビューなど参照セマンティクスを持つものの場合、そのconst性は参照先のconstを意味しない。現在のC++でもそのようなオブジェクトは広く使用されており、マッチングにおけるバインディングそれぞれで個別にconst性を制御しない方法は実用的ではない

これらの検討の結果、パターンマッチングにおいて現れる識別子の種類としては次の5つが考慮されることになります

  • xはパターンマッチング構文内で新しく導入された、観測専用(constな)変数もしくはバインディング
  • xはパターンマッチング構文内で新しく導入された、変更可能な変数もしくはバインディング
  • xは比較対象となる外部の変数
  • xはマッチング結果の代入対象となる外部の変数
  • xは型

パターンマッチング構文においては、これらのパターンを区別しながら識別子を使用できることが望ましいです。

この提案では、その解決のための構文として次のようなものを提案しています。

識別子xがあったとき

ただし、値マッチングに常に==を要求するのは冗長であり見づらくなる可能性があるほか、単一の識別子がいつも型名とみなされるのも混乱を招く可能性があります。そこで、次のような制限を加えています

  • 最後のパターンの場合、==を省略できる
  • 最後のパターンは常に型名マッチングではない
    • 最後のパターンに現れる識別子は型名とみなされない

最後のパターンというのは、パターンマッチングにおける一つのパターンのマッチングにおいて、パターンの外側から内側へ再帰的にマッチングされる際の最も内側のパターン(つまり一番最後にマッチングがチェックされるパターン)のことです。この制限によって、パターンマッチングに現れる単一の識別子のデフォルトは==xとなり、型マッチングはx __のように記述することになります。

struct size{...}; // #1
size size;        // #2

inspect (...) {
  size __          => ...  // 型マッチング(sizeは型名#1)
  size             => ...  // 値のマッチング(sizeは変数名#2)
  [size, size]     => ...  // これも値マッチング(#2)
  [==size, ==size] => ...  // 上と同じ意味
   
  size size     => ...     // size型(#1)のsize値(#2)とのマッチング
  size (==size) => ...     // 上と同じ意味
};

==が必要とされる例

struct Size{...};
Size size;  // #1

inspect (...) {
//  size [w=, h=] => ...      // error、sizeは型名ではない
//  Size size[w=, h=] => ...  // error、sizeは型名ではない(最後のパターンではないため変数名とみなされていない)
    ==size[w=, h=] => ...     // ok、::size(#1)とのマッチング、そのサブオブジェクトをw, hにバインディング
};

この場合、最後のパターンは[]の中のw=, h=(の両方)になるため、それ以外のところで==を省略するとその識別子は型名だとみなされ、それによって最初の2つの例はエラーになります。

同様に、型の一致は最後のパターンに来ないようにする必要があります。

struct Size{...};
Size size;

struct Rectangle { int x; int y; Size size; };
Rectangle rect = ...;

inspect (rect) {
//  [__, __, size __] => ...    // error、sizeは型名ではない
    [__, __, ==size __] => ...  // ok、明示的な値マッチング
    [__, __, size] => ...       // ok、最後のパターンのため値マッチングとみなされる

//  [__, __, Size] => ...           // error Sizeは型名(最後のパターンのためSizeは変数名とみなされている)
    [__, __, Size __] => ...        // ok、最後のパターンではないためSizeは型名とみなされる
    [__, __, Size [w=, h=]] => ...  // ok、最後のパターンではないためSizeは型名とみなされる
};

その他の例

int x = 5;

inspect (...){
    x     =>        // ::x(変数)とマッチング
    ==x   =>        // 上と同じ意味(明示的)
    x=    =>        // バインディングの導入、::xを隠蔽
    x=x   =>        // バインディングの導入 + ::xとのマッチング
  x=(==x) =>        // 上と同じ意味(明示的)
  int x   =>        // int型かつ::xとマッチング
  px= (int x=x) =>  // int型かつ::xとマッチング、マッチングした場合その値はint型の値xにバインドされ(値は::xと一致する)、多態的型にバインドされたpxにさらにバインドする
  int &x=x      =>  // int型かつ::xとマッチング、マッチングした場合その値はint型の参照xにバインドされる(値は::xと一致する)
  ...
};

P2944R0 Comparisons for reference_wrapper

reference_wrapperに比較演算子を追加する提案。

reference_wrapper<T>には比較演算子が定義されていませんが、reference_wrapper<T>T&に暗黙変換が可能であり、比較を行う際はADLにおいてT名前空間が探索候補になることから、Tに定義されている比較演算子を使うことができます。ただし、これには制限もあり比較演算子の定義のされ方などによって比較が可能かが変化します。

reference_wrapper<T>Tの種類と演算子の定義のされ方、比較の仕方によって比較が可能かは次の表のようになります。

T ref(t) == ref(t) ref(t) == t
組み込み型
クラス(テンプレート)で==はメンバで定義
クラスで==は非メンバで定義
クラステンプレートで==はhidden friendで定義
クラステンプレートで==は非メンバ関数テンプレートで定義
std::string_view

reference_wrapper<T>を介してTに定義された比較演算子の上で比較を行いたい場合、そのままだとこの表にあるようにできたりできなかったりします。

reference_wrapper<T>を介してTの比較演算子によって比較したいユースケースがあり、それはreference_wrapperを使用する理由と関連しています。

// valueと等しいかをチェックする述語生成ラッパ
inline constexpr auto equals = [](auto&& value) {
  return [value=FWD(value)](auto&& e){ return value == e; };
};

これはvalueを値でキャプチャするためvalueがコピーの重い型だと非効率になりますが、ここで適応的に参照キャプチャしようとすると実装が複雑化します。この時、このラッパの入力にreference_wrapperを用いることによって、使用者の意思で明示的に参照キャプチャするかどうかを切り替えることができます。

if (std::ranges::any_of(v, equals(0))) {
  // ...
}

if (std::ranges::any_of(v, equals(std::ref(target)))) {
  // ...
}

これは先ほどの表のようにtargetの型と比較演算子の定義のされ方によって動作したりしなかったりします。この提案の目的は、これが常に動作するようにすることにあります。

この提案による解決は単純で、reference_wrapper<T>に対して直接的に比較演算子を定義することで、ADLに頼らずにTの比較演算子を利用できるようにします。

namespace std {
  template<class T>
  class reference_wrapper {
  public:
    ...

    // 追加される比較演算子
    friend constexpr bool operator==(reference_wrapper, reference_wrapper);
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, reference_wrapper);
  };
}

これらの比較演算子は、reference_wrapper<T>が参照しているTの値で直接対応する比較を行うことによって比較を実行するため、現在のreference_wrapper<T>のように比較できたりできなかったりすることは無くなります。

また、比較演算子の導出によってTでそれが可能ならば全ての種類の比較が可能となります。

P2945R0 Additional format specifiers for time_point

<chrono>time_point型のフォーマット指定を追加する提案。

C++20で<chrono>ライブラリは拡張され、カレンダー表現やタイムゾーンのサポートなどと共にstd::format()によるフォーマット対応もなされました。それによってtime_point型の値(時刻の値)はそのままstd::format()で文字列化することができ、またフォーマット指定を添えることで文字列化のされ方を制御することができます。

#include <chrono>
#include <format>
#include <iostream>

using namespace std::chrono;

int main() {
  // system_clockのtime_point値(現在時刻)の取得
  auto now = system_clock::now();

  // time_point値のフォーマット出力
  std::cout << std::format("{}\n", now);
  std::cout << std::format("{:%Y年%m月%d日 %H時%M分%S秒}\n", now);
  std::cout << std::format("{:%D %T}\n", now);
  std::cout << std::format("{:%F %T %Z %z (%Ez)}\n", now);
}

出力例

2023-09-05 12:28:41.739583722
2023年09月05日 12時28分41.739583722秒
09/05/23 12:28:41.739583722
2023-09-05 12:28:41.739583722 UTC +0000 (+00:00)

この提案は、現在のchronoのフォーマット指定に欠けているフォーマット方法を追加しようとするものです。

この提案が問題視しているのは、上記例にも表れているように秒の単位以下の出力精度を制御する方法が提供されていないことです。%Sは秒単位かつミリ秒未満は10進小数で出力しますが、この時ミリ秒未満をどこまで出力するのかあるいは出力しないということを指定する方法が提供されていません。%T%H:%M:%Sの省略指定であるため同様の問題があります。これによって、time_point値を秒単位で出力、ミリ秒単位(小数点以下3桁)だけ出力、などの指定は少し遠回りをする必要があります。

#include <chrono>
#include <format>
#include <iostream>

using namespace std::chrono;

int main() {
  // system_clockのtime_point値(現在時刻)の取得
  auto now = system_clock::now();

  // 秒単位まで出力
  std::cout << std::format("{:%H:%M:%S}\n", time_point_cast<seconds>(now));
  // ミリ秒単位(小数点以下3桁)まで出力
  std::cout << std::format("{:%H:%M:%S}\n", time_point_cast<milliseconds>(now));
  // マイクロ秒単位(小数点以下6桁)まで出力
  std::cout << std::format("{:%H:%M:%S}\n", time_point_cast<microseconds>(now));
}

出力例

12:42:14
12:42:14.700
12:42:14.700612

この提案では、これに対して%.nSのようなフォーマット指定を提案しており、nで秒単位の出力精度(桁数)を指定します。

#include <chrono>
#include <format>
#include <iostream>

using namespace std::chrono;

int main() {
  auto now = system_clock::now();

  // 秒単位まで出力
  std::cout << std::format("{:%H:%M:%.0S}\n", now);
  // ミリ秒単位(小数点以下3桁)まで出力
  std::cout << std::format("{:%H:%M:%.3S}\n", now);
  // マイクロ秒単位(小数点以下6桁)まで出力
  std::cout << std::format("{:%H:%M:%.6S}\n", now);
}

同時に、%Tに対しても%.nTのように同様に秒単位の出力桁数を指定するフォーマット指定を提案しています。

#include <chrono>
#include <format>
#include <iostream>

using namespace std::chrono;

int main() {
  auto now = system_clock::now();

  // 秒単位まで出力
  std::cout << std::format("{:%.0T}\n", now);
  // ミリ秒単位(小数点以下3桁)まで出力
  std::cout << std::format("{:%.3T}\n", now);
  // マイクロ秒単位(小数点以下6桁)まで出力
  std::cout << std::format("{:%.6T}\n", now);
}

このフォーマット指定の利点は以下の3点が主張されています

  1. これがない場合、time_point値の精度を変換するための冗長なキャスト(time_point_cast)を記述しなければならず、不便
  2. 提案しているフォーマット指定では、実際のtime_point値の精度を気にする必要がないことを保証している
    • time_point値の精度によらず、一貫して同じ出力を得られる
  3. std::optionalrangeなどの要素となっている時のフォーマット時にフォーマット指定を再利用できる
    • 単に入れ子にすれば良い
    • time_point_castで回避する場合、追加の変換(.transform()views::transform)が必要になり、これは場合によって安全性の問題を引き起こす

また、多言語におけるこのtime_point値のフォーマットに対応するフォーマット指定を見てみると、次のような明確なコンセンサスが確認できます

  1. %Sは秒数を00 ~ 59の2桁の整数としてフォーマットする
  2. %sは秒数をエポックからの整数値としてフォーマットする

現在のC++は残念ながらどちらも満たしていません(%s指定は存在しない)。そこでこの提案では、%s指定をこの意味論に沿った効果を持つフォーマット指定として追加することも提案しています。

#include <chrono>
#include <format>
#include <iostream>

using namespace std::chrono;

int main() {
  auto now = system_clock::now();

  // エポックからの経過秒数(or経過単位時間カウント値)の出力

  // 現在
  std::cout << std::format("{:%Q}\n", now.time_since_epoch());  // 最大精度
  std::cout << std::format("{:%Q}\n", time_point_cast<seconds>(now).time_since_epoch());  // 秒単位

  // この提案
  std::cout << std::format("{:%s}\n", now);    // 最大精度
  std::cout << std::format("{:%.0s}\n", now);  // 秒単位
}

出力例

1693915931163502515
1693915931
1693915931163502515
1693915931

まとめると変更は次のようになります

  • %sを追加して、time_point値をその精度単位でエポックからのカウント数を整数値でフォーマットする
  • %S %sの両方で、プリフィックスとして出力精度(秒未満の桁数)を受け付けるようにする
    • %.nS %.ns
  • %T%Sと同様に拡張
  • %fを追加して、%0S%.nf%.nSと同じ意味になるようにする
  • %Q %qtime_pointでも機能するようにする
    • 現在はdurationでのみ有効

この提案ではさらに踏み込んで、%Sの出力を他言語と一貫性のあるものに修正することを提案しています。それが受け入れられる場合、%S %sの両方で%.nS %.nsの指定の代わりに小数点以下の部分を表すフォーマット指定として%fを追加することを提案しています。

#include <chrono>
#include <format>
#include <iostream>

using namespace std::chrono;

int main() {
  auto now = system_clock::now();

  // `%S`の出力修正が受け入れられる場合の提案

  // 秒単位まで出力
  std::cout << std::format("{:%H:%M:%S}\n", now);
  // ミリ秒単位(小数点以下3桁)まで出力
  std::cout << std::format("{:%H:%M:%S%.3f}\n", now);
  // マイクロ秒単位(小数点以下6桁)まで出力
  std::cout << std::format("{:%H:%M:%S%.6f}\n", now);

  // 秒単位まで出力
  std::cout << std::format("{:%T%}\n", now);
  // ミリ秒単位(小数点以下3桁)まで出力
  std::cout << std::format("{:%T%.3f}\n", now);
  // マイクロ秒単位(小数点以下6桁)まで出力
  std::cout << std::format("{:%T%.6f}\n", now);

  // エポックからの経過秒数(or経過単位時間カウント値)の出力
  std::cout << std::format("{:%s}\n", now);    // 最大精度
  std::cout << std::format("{:%.0s}\n", now);  // 秒単位
}

こちらの提案の場合変更は次のようになります

  • %Sは秒数を00 ~ 59の2桁の整数としてフォーマットするように変更
    • %H %Mと同様になる
  • %sを追加して、time_point値をエポックからの秒数を表す整数値としてフォーマットする
  • %fを追加して、time_point値の出力時にその最大精度でフォーマットする
    • %.nfのようにして、精度を指定することもできる
  • %Q %qtime_pointでも機能するようにする
    • 現在はdurationでのみ有効

こちらの提案は出力結果に関して破壊的変更となり、現在の%S%S%.f%T%T%.fが対応します。

%Sの修正を含む提案を提案2、%Sを現状維持する提案を提案1とすると、2023年7月09日15時40分34秒(+0.295314673秒、UTC)の時刻を保持するtime_pointtpに対して同じ出力を得るためのフォーマット指定には次のような差が生じます

提案2 期待する出力 提案1
std::format("{:%s%9f}", tp) 1688830834295314673 std::format("{:%s}", tp)
std::format("{:%s}", tp) 1688830834 std::format("{:%.0s}", tp)
std::format("{:%H:%M:%S}", tp)
std::format("{:%T}", tp)
15:40:34 std::format("{:%H:%M:%.0S}", tp)
std::format("{:%.0T}", tp)
std::format("{:%H:%M:%S%.3f}", tp)
std::format("{:%T.%3f}", tp)
15:40:34.295 std::format("{:%H:%M:%.3S}", tp)
std::format("{:%.3T}", tp)
std::format("{:%H:%M:%S%.6f}", tp)
std::format("{:%T.%6f}", tp)
15:40:34.295314 std::format("{:%H:%M:%.6S}", tp)
std::format("{:%.6T}", tp)

この提案では提案2を押していますが、破壊的変更となるため受け入れられない場合のために提案1を用意しています。

LEWGの最初のレビューでは、%Sの修正を受け入れることに弱いコンセンサスがあり、その場合実装はC++20/23モードまで遡って修正を適用することにコンセンサスがありました。

P2946R0 A flexible solution to the problems of noexcept

noexceptよりも弱い無例外指定である[[throws_nothing]]の提案。

関数に対するnoexcept指定はその関数が例外を投げないことを指定し、例外が投げられた場合はプログラムを終了させます。これは、std::vector等のコンテナの挿入操作などにおいて強い例外保証とムーブコンストラクタの効率性を両立させるために導入されました。ムーブはムーブ元の状態を変更してしまうため、ムーブコンストラクタが例外を投げるとムーブ元の状態を元に戻せなくなり、強い例外保証を守ることができなくなります。そのため、ムーブコンストラクタが例外を投げないことをコンパイル時に調べてその場合にのみムーブによって要素を構築するために、コンストラクタ(関数)が例外を投げないことの表明と関数が例外を投げないことのチェックをnoexcept指定子と演算子によって行えるようにしています。

その後これを標準ライブラリの他の関数等にも適用する際のルールとしてLakos Ruleが整備されました。これは関数が事前条件を持つかによって2種類に分類してその上でnoexceptを指定すべきかを決定するシンプルなルールです。簡単には次のようなものです

  • 関数は事前条件を持たず(広い契約を持つ)、例外を投げない場合、関数をnoexcept指定する
  • 関数は事前条件を持っている(狭い契約を持つ)か、事前条件を満たして呼び出された場合にも例外を投げる可能性がある場合、関数はnoexcept指定するべきではない

単純なvector実装における例

template <class T, class A>
class vector {
  // ...
  constexpr size_type size() const noexcept;  // 広い契約, 例外を投げない
  constexpr reference at(size_type);          // 広い契約, 例外を投げうる
  constexpr reference operator[](size_type);  // 狭い契約, 例外を投げない
  constexpr reference front();                // 狭い契約, 例外を投げない
};

Lakos Ruleは効果的であり、理論的にも実践的にも強い裏付けを持っています(下位互換性のある拡張を可能にしたり、より広いインターフェースに適合することができるなど)が、このルールに従わない理由として次の2つのようなものが明らかになっています

  1. 多くの状況ではnoexcept関数の呼び出しに伴って生成されるコードが少なくなる。そのため、コード生成を改善するためにnoexceptをとにかく使用したい需要がある
  2. C++の規格書において、「Throws: nothing」と指定されているものとnoexceptの区別が明確ではない

また、その他の動機として、noexcept関数が例外を投げた場合に即終了しないでほしい場合があり、単なる事前条件のチェックのために例外を用いたいなどの需要もあるようです。

この2つの需要に対しては、関数にとにかくnoexceptを付けるという動機が生まれてきます(つまり、Lakos Ruleを無視する)。しかし、ある時点で関数にnoexceptを付加してしまうと将来のバージョンで下位互換性を壊すことなく削除することができなくなるため(関数にnoexceptが付加されている場合、その関数を例外を投げる可能性のある値を受け入れるように拡張できなくなる)、どうしてもコンパイル時にその関数のnoexcept性が重要になる説得力のある理由がない限りはLakos Ruleに違反することは望ましくありません。

このような現状に対してこの提案は、現在のnoexcept及びLakos Ruleを維持しながらも、これらのような現在のnoexceptでは満たすことのできないいくつかの需要に対処するために、[[throws_nothing]]という無例外を表明する属性を追加しようとするものです。

この提案の目的は現在のnoexceptでは対応しきれない次ような需要に対処することです

  • より小さなコードサイズ
    • 生成されるコードサイズの削減の恩恵を最も受けるのはメモリに制約のある環境
    • 組み込み環境において例外が使用しやすくなる可能性がある
  • 正常なシャットダウン
    • エラー(例外)が発生したからといって即終了されることは受け入れられない環境がある
    • 終了の前にリソースを解放したりログをとったりデータを保存したりする必要がある
  • 終了しない例外
    • 決して終了しないプログラムが存在する
    • テストドライバーはその一例だが、関数の事前条件のテストにおいては意図的な失敗も含めてテストをするが、ここで例外が投げられプログラムが終了する場合テストは不可能になる

[[throws_nothing]]は関数宣言に指定する属性であり、その関数の事前条件が満たされていれば例外を投げないことを指定します。しかしこの指定はコンパイル時に検出する方法がなく、noexcept演算子はこれを無視し関数型にも現れません。

[[throws_nothing]] void g1(int);
static_assert(noexcept(g1(0)) == false);

[[throws_nothing]] void g2(int) noexcept;
static_assert(noexcept(g2(0)) == true);

[[throws_nothing]] void g3(int) noexcept(false);
static_assert(noexcept(g3(0)) == false);

void g4(int);
static_assert(std::is_same_v<decltype(g1), decltype(g4)>);

[[throws_nothing]]noexcept演算子等で検出できないため、それによってコンパイル時に分岐することもできなくなり、これによって関数の動作が以前に有効だった入力に対して変更されない限りは、後からこの指定を削除することができます。

[[throws_nothing]]指定された関数が例外を投げることで終了する場合にプログラムが終了するか(あるいは例外が通常のように伝播するか)は実装定義とされます。推奨される実装としてはその動作をユーザーが指定できることが望ましいとしています。これによって、例外発生時に終了することを選択した実装ではnoexcept同様にコードサイズ削減効果が期待でき、終了しない実装では正常なシャットダウンや事前条件テストなどを例外を用いて行うことができるようになります。

また、標準ライブラリ実装においては現在のnoexcept同様に標準で指定されていなくても[[throws_nothing]]を指定することを許可します。これによって、「Throws: nothing」と指定されている関数に対して[[throws_nothing]]属性を指定することができ、現在のnoexceptと同等の効用を得ながらもより柔軟な運用が可能となります。前述のように、後から削除する場合でも問題なく行うことができます。

noexcept及び何も指定しない場合との効用の比較表

指定なし noexcept [[throws_nothing]]
関数の自己ドキュメント no yes yes
コード生成へのヒント no yes 終了する場合yes
予期しない例外時の終了 no yes 終了する場合yes
広い契約に最適 yes yes yes
狭い契約に最適 yes no yes
正常なシャットダウンに対応 yes no 無視する場合yes
ログをとって継続に対応 yes no 無視する場合yes
例外による防衛的テストに対応 yes no 無視する場合yes
コンパイル時の検出と分岐のサポート no yes no

この表を見るとわかるように、noexcept[[throws_nothing]]はどちらがどちらよりも優れていて対立するものではなく、それぞれ異なる目的を果たしており、Lakos Ruleを維持しながらより広い無例外指定のユースケースを満たそうとするものです。

P2947R0 Contracts must avoid disclosing sensitive information

提案中のcontract_violationクラスのメンバ関数comment(), location()が意味のない応答を返すようにオプトアウトできることを必須とする提案。

C++26契約プログラミングに向けて議論が進んでおり、現在の仕様では違反ハンドラ(契約違反が起きたときに呼ばれる関数)がデフォルトとなりビルドモードの概念が違反ハンドラのカスタマイズで置き換えられています。

違反ハンドラはその引数で起きた契約違反についての情報をcontract_violationというクラスのオブジェクトとして受け取ります。contract_violationクラスについてはP2811R7で議論中ですが、このクラスには契約違反を起こした契約条件式をテキストで取得する.comment()と、契約違反が起きたソースコード上での場所を取得するlocation()という2つの関数が用意されています。P2811R7では、これらの関数が意味のある応答をするのはオプショナルであり、空の文字列やstd::source_locationオブジェクトを返すことが許可されています。

これはあくまで許可でありいつも意味のある応答をする実装が前提となっていますが、この提案は意味のある応答をしないようにする(comment(), location()が空の応答を返せるようにする)オプションを必ず提供しなければならないように規定しようとするものです。

この提案の目的はコンパイル後のバイナリファイルに不要な情報が含まれることを回避することにあります。

contract_violationクラスのメンバ関数comment(), location()ソースコードに関する情報を提供し、契約アノテーションは実行時に評価されるまでどれが破られるかは分かりません。すなわち、これらの関数を使用すると契約アノテーションに関するソースコードの情報がコンパイル後のバイナリに何かしらの形で書き込まれることになります。現在の契約プログラミング仕様ではビルドモードの概念が削除されているためリリースビルドにおいてもこれらの情報がバイナリに記録されることを避けることができません。これは、リバースエンジニアリングを行う人に対して大きな助けとなる情報を与えることになってしまいます。

製品のセキュリティに大きな労力を費やしているベンダーでは、この理由により契約プログラミングの使用が妨げられる可能性があります。提案では、筆者の方々の経験として次のような例が報告されています

  1. A社では、本番コードからログメッセージを削除することを要求された結果、コンパイル時のテキスト難読化機能とそれに付随するログテキストデコーダユーティリティを導入した
  2. B社では、コンパイル時に全てのログメッセージをIDに変換し、出荷バイナリと共にログメッセージをIDにマップするマップファイルを作成した。バイナリがIDのみのログを生成している間、マップファイルは社内にあり続ける(出荷されない)
  3. C社では、出荷バイナリにプレーンテキストでシンボル名が含まれることを避けるためにRTTIを無効化した。これはペネトレーションテストの結果を受けての措置。

これらの例は、機密性の高いテキストがバイナリに埋め込まれることを回避し知的財産を保護することを最優先事項として他の安全性やセキュリティへの配慮と同等以上のものとするために、企業が多大な労力を払い、また制限を受け入れる姿勢を示しています。

このような理由から、この提案ではcontract_violationクラスのメンバ関数comment(), location()が何の情報も返さないようにするオプションを提供しなければならないことを規定することで、契約プログラミングを使用するとソースコード上の情報がバイナリファイルに含まれてしまうことを回避できるようにしようとしています。

P2949R0 Slides for P2861R0: Narrow Contracts and noexcept are Inherently Incompatable

P2861R0の紹介スライド。

P2861R0は標準ライブラリにおけるnoexcept適用基準であるLakos Ruleとそこから得られる効用について説明したものです。このスライドはそれをWG21のメンバに紹介するものです。

P2950R0 Slides for P2836R1: std::basic_const_iterator should follow its underlying type's convertibility

P2836R1の紹介スライド。

P2836R1は、ある範囲について、そのイテレータstd::constant_iteratorに通して得られる型とstd::ranges::cbegin()の返すイテレータ型が異なり相互変換不可能な場合があるとして、それを正そうとするものです。詳細は上の方にあるのでそちらを参照してください。

このスライドは、LEWGのメンバにむけて提案の内容や問題点、解決方法などを解説するものです。

P2951R0 Shadowing is good for safety

P2951R1 Shadowing is good for safety

変数のシャドウィングを活用した、安全性向上のための言語機能の提案。

この提案では主に安全性の向上を目的として、変数のシャドウィングの制限を解除することで安全性やコードのシンプルさに資する機能を導入しようとするもので、4つの提案が行われています。

1つ目は、voidで既存変数を再宣言することで以降そのスコープでのその変数の利用を禁止するものです。別の言い方をすると、シャドウィングを明示的に行う構文を導入しようとするものです。

現在 提案1
#include <string>
#include <vector>

using namespace std;

// シャドウィングのためのタグ型
struct dename{};

int main() {
  vector<string> vs{"1", "2", "3"};
  for (auto &s : vs) {
    dename vs;
    // 以降、vsをvectorとして使用できない
  }
}
#include <string>
#include <vector>

using namespace std;

int main()
{
  vector<string> vs{"1", "2", "3"};
  for (auto &s : vs) {
    void vs;  // vsを明示的にシャドウィング
    // もしくは
    auto vs;
    // 以降、vsを使用できない
  }
}

これは例にあるようにdenameのようなクラスを標準化することによっても達成できるため、言語機能として提案しているものが受け入れられない場合はライブラリ機能でも良いとされています。ライブラリ機能ではなく言語機能である事のメリットは、エラーメッセージを改善できることにあります。

シャドウィング方法 エラーメッセージ例
void vs; error: 'vs' was not declared in this scope
dename vs; error: 'struct dename' has no member named '*****'

2つ目は、シャドウィング対象の変数名で変数を宣言し初期化することでシャドウィングするものです。

#include <string>
#include <vector>
#include <optional>

using namespace std;

int main() {
  vector<string> vs{"1", "2", "3"};
  for (auto &s : vs) {
    // constで宣言しなおす
    const vector<string>& vs = vs;  // 現在できない

    // 以降、vsはコンテナを変更しない操作のみが可能
    ...
  }

  ...
  
  auto s = optional<string>{"Godzilla"};
  if (s) {
    // optionalの中身でシャドウィング
    auto s = *s;  // 現在できない

    // 以降、optionalとしてのsは必要ない
    ...
  }
}

どうやらこれは、現在でもGCCでのみ意図通りに行えるようです(他のコンパイラは未初期化変数扱いになる)。

3つ目は、子スコープを導入することなく変数をシャドウィングすることを許可するものです。これは、前2つの提案と組み合わせることもできます。

#include <string>
#include <vector>

using namespace std;

int main() {
  vector<string> vs{"1", "2", "3"};

  // ある時点からconstにする
  const vector<string>& vs = vs;  // 現在できない

  // 以降、vsはコンテナを変更しない操作のみが可能
  ...
}

同じスコープで同じ変数名を宣言できないため、現在でも前2つの提案でもシャドウィングするには子スコープを導入しなければなりません。この制限を解除することによって、このように最初の宣言における変数のconst性を後から変更することができるようになります。

4つ目は、前のものとは少し毛色が異なるもので、条件付きキャストによってシャドウィングを行うものです。

#include <string>
#include <optional>
#include <memory>

using namespace std;

int main() {
  auto s = optional<string>{"Godzilla"};

  // sをstringにキャスト(*による)
  if (s as string)  // もしくは if (s is string)
  {
    // このスコープでは、sはstring&
  }
  else
  {
    // このスコープでは、sはoptional<string>
  }


  auto i = shared_ptr<int>{42};
  
  if (i as int&)// もしくは if (i is int&)
  {
    // このスコープでは、iはint&
  }
  else
  {
    // このスコープでは、iはshared_ptr<int>
  }
}

ここでのキャストは*.get()等によって行われるもので、パターンマッチングにis asを使用する提案(P2392R2)をベースとしています。

P2392ではパターンマッチングにおける利用と一般化に焦点を置いていますが、この提案ではシャドウィングとそれによってもたらされる安全性向上に焦点を当てています。

これら4つの提案は互いに直行しているため、全てを導入しなくてもどれか1つだけから導入することもできます。

この提案を導入することによって次のようなメリットが得られます

  • プログラマは、イテレータや参照、ポインタの無効化の問題を回避しデバッグするために、コンパイラを使用できるようになる
  • 普遍性とスレッド安全性のためのconstの活用を支援する
  • シャドウィングを増やすと、コードがシンプルかつ簡潔になる

また、この提案は静的解析ツールのためのヒントとなるとも述べられています。

おわり

この記事のMarkdownソース

[C++] autoによるキャスト

C++23からauto(x)の形式のキャストが可能になります。

template<std::copy_constructible T>
void f(T x) {
  T p = auto(x);  // ok、C++23から
}

これに関連する仕様等のメモです。

prvalue値へのキャスト

auto(x)の形式のキャストはxをその型をdecayした型の値としてキャストするものです。

型のdecayとはその型からCV/参照修飾を取り除き、配列/関数はそのポインタに変換するものです。配列/関数以外の場合、autoによるキャストはxをその型のprvalueへキャストします。

そして、auto(x)のキャストは単なる型変換ではなく、キャスト結果のprvaluexの値をコピーしたものになります。したがって、その結果はxとは異なるオブジェクトとして得られ、xの状態を変更しません(コピーコンストラクタが変なことしない限り)。

template<std::copy_constructible T>
void f(T x) {
  // auto(x)式の値カテゴリはprvalue
  static_assert(std::same_as<decltype(auto(x)), T>);

  // pとxは異なるオブジェクト(コピーされる)
  T p = auto(x);

  // auto(x)後もxの利用は安全
  x.use();

  // pに変更を加えてもxには波及しない
  p.mutate();

  // auto(x)は同じオブジェクトに対して何度も行える
  T p2 = auto(x);
}
auto f(int) -> std::size_t;

int main() {
  int a[5]{};

  // 配列型、関数型の場合は対応するポインタ型のprvalueへキャスト
  auto* p1 = auto(a);
  auto* p2 = auto(f);
}

コピーを行うため、Tcopy_constructibleである必要があります。move_constructibleでしかない場合はキャストは失敗しコンパイルエラーとなります。

struct no_copy {
  no_copy() = default;
  no_copy(const no_copy&) = delete;
};

int main() {
  non_copy nc{};

  non_copy cp = auto(nc); // ng
}

また、auto(x)だけでなくauto{x}も有効であり、どちらも同じ効果になります。

template<std::copy_constructible T>
void f(T x) {
  // どちらも同じprvalueへのキャスト
  T p1 = auto(x);
  T p2 = auto{x};
}

細かい仕様の話

auto(x)の形式のキャストは明示的型変換(関数スタイルキャスト)の一種であり、型名の代わりにautoを使用するものです。この場合に使用可能なのは丁度autoのみで、decltypeauto&const auto&だとかは使用できません。

template<std::copy_constructible T>
void f(T x) {
  // すべてコンパイルエラー
  decltype(x);
  decltype(auto)(x);
  auto&(x);
  auto&&(x);
  const auto&(x);
}

auto(x)の形式のキャストにおいてはautoは他の所と同じくプレースホルダ型として扱われており、その型はxの型が推論された後で置き換えられます。その推論においては通常のautoの推論と同様に、単一のテンプレートパラメータをもちそのテンプレートパラメータ型の引数を1つだけ受ける関数テンプレートに対してxを渡した時にそのテンプレートパラメータに推論される型が取得されます。

// これらのautoに推論される型は
auto c = x;
auto(x);
auto f() { return x; }

// このような関数テンプレートに対して
template<typename T>
void hf(T);

// xをそのまま渡した時のTに推論される型として取得される
hf(x);

この場合の推論時に行われるxの型から修飾を取り除いたりポインタ型に変換したりといった調整を型に対するdecayと呼び、結果の型はstd::decay_t<decltype((x))>で得られる型と一致します。これは、配列型・関数型以外の場合は元の型に対してprvalueになります。

こうして取得された型をTとすると、auto(x)プレースホルダautoはこの型Tで置き換えられ、T(x)として通常の関数スタイルキャストとして処理されます。この式はxをコピーしてTの新しいオブジェクトを構築する式となります。これはauto{x}においても同様です。

template<std::copy_constructible T>
void f(T x) {
  // この4つは実は同じ意味
  auto(x);
  T(x);
  auto{x};
  T{x};
}

autoを用いたキャストにおいては、{}の中の初期化式は1つだけでないとその型が推論できないため渡せる式は1つに限定され、()の場合はカンマ区切りの式とみなされやはり渡せる式は1つだけになります。また、推論の仕様上取得される型はxと修飾だけが異なる同じ型となるため、大きな型変換は起こりません。したがって、(){}の違い(式の評価順序や縮小変換の禁止など)はここでは顕在化せず両方は真に同じ意味を持ちます。

文法上auto(x), auto{x}xには任意の式を渡すことができ、意味論的にも制限がないため、autoによるキャストは必ずしも変数名のみに対して作用するわけではありません。とは言え、何が渡されたとしてもやることは変わらず、与えられる式の結果をその型のprvalue値へキャストすることです。

auto f() -> std::string;
auto g() -> const std::string&;

int main() {
  std::vector vec = {1, 2, 3, 4};

  auto v1 = auto(vec);  // ok、コピー
  auto v2 = auto(std::move(vec)); // ok、ムーブ
  auto s1 = auto(f());  // ok、コピー省略
  auto s2 = auto(g());  // ok、コピー
}

auto(x)xが右辺値の場合はコピーではなくムーブされて結果が生成されます。さらに、xprvalueの場合はコピー省略によってauto(x)ではコピーもムーブも発生しません(受けている変数が非参照ならば、そこに直接構築される)。

利点や用途

前述のように、auto(x)xの素の型Tに対してT(x)と同じ意味になります。さらに、auto(x)によるコピーはauto var = x;のような変数宣言でも同じことを達成できます。

template<std::copy_constructible T>
void f(T x) {
  // この4つは実は同じ意味
  auto(x);
  T(x);
  auto{x};
  T{x};

  // 次の3つの宣言は同じことを行う
  auto v1 = auto(x);
  auto v2 = x;
  T v3 = x;
}

とすると、auto(x)のキャストは冗長で無価値なものにしか見えなくなります。

autoキャストが有用なのは、上記のようにTが素直に得られず、一時変数を作る必要がない場合においてです。

例えばコンテナをテンプレートで受け取って、その先頭要素と同じ値をコンテナから削除したい場合を考えます。

// front()が呼べるコンテナコンセプト
template<typename C>
concept container = 
  std::ranges::forward_range<C> and
  requires(C& c) {
    {c.front()} -> std::same_as<std::ranges::range_reference_t<C>>;
  };

// コンテナから先頭要素と同じ要素を削除する
void pop_front_alike(container auto& x) {

  // 先頭要素が削除された後、3番目の引数はダングリング参照となる
  std::erase(x.begin(), x.end(), x.front());

  // 予め先頭要素をコピーしておいて、それを使う
  auto tmp = x.front();
  std::erase(x.begin(), x.end(), tmp);

  // 1行で書こうとすると面倒・・・
  using T = std::decay_t<decltype(x.front())>;
  std::erase(x.begin(), x.end(), T(x.front()));
}

.front()は要素への参照を返し、std::erase()の第3引数は要素型のconst参照を受け取ります。そのため、std::erase()の第3引数にx.front()を直接渡すと先頭要素が削除された後(つまり処理が開始されてすぐ)にその参照はダングリング参照となり、UBです。それを回避するためには、先頭要素を予めコピーしてからstd::erase()に渡すことが必要となります。

ここでは、コピーしてる変数tmpはその後使うことはないため一時変数は導入しない方が望ましく、要素型Tは直接的に見えていないため取得が面倒になります。

そこで、auto(x)を使用すると、それらの懸念を解消しつつ同じことをよりシンプルに記述できます。

// コンテナから先頭要素と同じ要素を削除する
void pop_front_alike(container auto& x) {
  // auto(x)を使う
  std::erase(x.begin(), x.end(), auto(x.front()));
}

前述のように、auto(x.front())x.front()の結果をdecayしてコピーした新しいオブジェクトのprvalueを返します。std::erase()の実行によってコピー元音オブジェクトが削除されても、auto(x.front())でコピーされたオブジェクトには何の影響もありません。なお、auto(x.front())で渡したオブジェクトはstd::erase()の呼び出しが終わるまで有効であり、この場合に生存期間の問題は発生しません。

また、Tの名前が5文字以上の場合(おそらく多くの場合はそうなるでしょう)なら文字数のアドバンテージを得ることができます。さらに言えば、目が慣れればT(x)よりもauto(x)の方が一貫性が高くその意図が明確になるでしょう。

class very_long_name_my_class {
  ...
};

auto f(const auto&) {
  ...
}

int main() {
  very_long_name_my_class v{};
  int n = 10;
  long double l = 1.0;

  // vをコピーしてfに渡したい場合
  f(very_long_name_my_class(v));
  f(int(n));
  f(long double(l));  // ng

  f(auto(v));
  f(auto(n));
  f(auto(l)); // ok
}
struct my_class {
  my_class(const my_class&) noexcept(...) {
    ...
  }

  my_class& operator=(my_class&&) noexcept {
    ...
  }

  my_class& operator=(const my_class& other) noexcept(std::is_nothrow_copy_constructible_v<my_class>) {
    if (this == &other) {
      return *this;
    }

    // コピーしてムーブ代入することで実装する
    auto copy = other;
    *this = std::move(copy);

    // あるいは
    *this = my_class(other);

    // auto(x)
    *this = auto(other);

    
    return *this;
  }
}

decay-copyとの違い

auto(x)の行うようなコピーは規格書中ではdecay-copyという用語でよく知られており、対応する説明専用のライブラリ関数も用意されています。

// 実際の名前はdecay-copy
template<class T>
constexpr decay_t<T> decay_copy(T&& v) noexcept(is_nothrow_convertible_v<T, decay_t<T>>)
{
  return std::forward<T>(v);
}
template<std::copy_constructible T>
void f(T x) {
  auto v1 = auto(x);
  auto v2 = decay_copy(x);  // これではダメなの?
}

auto(x)のような構文を新たに導入せずとも、この関数を標準化すれば同じことは達成できるように思えます。そうしないのは、auto(x)decay_copy(x)では前者がキャスト式となり後者は関数呼び出し式となることから、その振る舞いに違いがあるためです。

まず1つ目の違いは、decay_copy(x)xprvalueである場合にその引数でprvalueが実体化されてしまいコピー省略を妨げる点です。auto(x)の場合はこれ自体がprvalueの式であるためxprvalueである場合はコピー省略によって一切のコンストラクタ呼び出しを伴いません(というか何もしません)。

auto f() -> std::string;

int main() {
  std::string s1 = auto(f());       // コピー省略によって、s1はf()のreturn文の式から直接構築される
  std::string s2 = decay_copy(f()); // s2はf()の戻り値からムーブコンストラクタによって構築される
}

2つ目の違いは、クラス型のプライベートへのアクセスが可能なコンテキストでdecay_copy()はそのコンテキストを引き継げない点です。

class A {
  int x;

public:
  A();

  auto run() {
    f(A(*this));           // ok
    f(auto(*this));        // ok
    f(decay_copy(*this));  // ng
  }

protected:
  A(const A&);
};

この場合のdecay_copy(*this)で実際にAのコピーコンストラクタが呼ばれるのは、decay_copy()の定義内のreturn文においてであり、decay_copy()AfriendではないためそこからはprotectedであるAのコピーコンストラクタにアクセスできません。

auto(*this)は単なる式であるため、コピーが発生するのはその直接のコンテキストであるA::run()の定義内であり、そこからは問題なくAのコピーコンストラクタにアクセスできます。

この例は直接的ですが、friendを介すると別のクラスのコンテキストにおいても同様の違いが観測されます。

class S;

class A {
public:
  A() = default;

private:
  A(const A&);

  friend S;
};

class S {
public:
  S() = default;

  void f(A& a) {
    auto ca1 = auto(a);       // ok
    auto ca2 = decay_copy(a); // ng
  }
};

規格書における置き換え

前述のように、規格書においては以前からauto(x)とほぼ同等の意味合いでdecay-copyという用語が使用され、また説明専用の関数が使用されていました。この機能のもう一つの目的としてはそれらを置き換えることで規格書の記述をシンプルにすることも含まれています。

ただし、単にdecay-copyと書かれていてもその意図が微妙に異なっているらしく、auto(x)とは異なりxprvalueの場合でもコピーを行うこと(prvalueを実体化させること)を意図した場合があるようです。そのため、機械的な置き換えではなく、その微妙な意図を汲み取った上でauto(x)と同じ意味でdecay-copyが使用されている場所についてのみ置き換えを行なっています。次の表はその大まかな分類と方針をまとめたものです

規格表現 C++20の例 C++23からの例
特定の式を指定したdecay-copy decay-copy(begin(t)) auto(begin(t))
特定の式を指定しないdecay-copy decay-copy(E) 変更なし
decay-copyの評価結果についての説明 decay-copyの呼び出しはコンストラクタを呼び出したスレッドで評価される autoによって生成された値はコンストラクタを呼び出したスレッドで実体化される

表中の規格文章は一例で、それぞれranges::beginstd::threadのコンストラクタにおけるものです。

正直こんなニュアンスわかるはずもありませんが、今後はauto(x)が使用されている場所はprvalueの実体化を意味しないコピーであり、引き続きdecay-copyが使用されている場所はそうではない、と思うことができます。

コンセプト定義における利用例

少し変わった使用法になりますが、この機能はコンセプトの戻り値型制約において役立つ場合があります。

例えば、コンセプトにおいて何か呼び出し可能な型をチェックする際に、その戻り値型を特定の1つの型に指定したい場合を考えます。

// Fは引数なしで呼び出し可能であり、bool型を返してほしい
template<typename F>
concept returning_bool = 
  std::invocable<F> and
  requires(F& f) {
    {f()} -> std::convertible_to<bool>; // bool型を返すという意味になっていない
  };

この場合に要求したいことが、「f()の戻り値型がbool型そのものであってほしいが別に修飾(&&&)はついていても構わない。ただしbool以外の型は遠慮してほしい」だった場合、std::convertible_to<bool>ではその要求を表現できていません。なぜなら、std::convertible_to<bool>だとboolに変換可能な型であればOKという意味になってしまっているからです。例えば、ポインタ型を返す引数なしの関数は全てこの制約をパスしますが、それは明らかに意図するところではないでしょう・・・

std::same_asを使うとちょうどbool型のみという制約になりますが、これだと今度は少し厳しくなります。

template<typename F>
concept returning_bool = 
  std::invocable<F> and
  requires(F& f) {
    {f()} -> std::same_as<bool>; // bool&などが弾かれる
  };

この戻り値型制約(および他のところでコンセプトの第一引数の自動補完が働く場所)においては、コンセプトの第一引数にはdecltype((expr))が充てられます。例えば上記の場合、std::convertible_to<decltype((f())), bool>という制約がチェックされますが、decltype((expr))で取得していることによってexprの値カテゴリの情報が含まれることになります。

この場合にその修飾情報を無視した素の型をチェックするのは意外に難しく(decay_tしたりremove_cvref_tしたりする必要がある)、戻り値型制約の利用は諦めざるを得なくなります

template<typename F>
concept returning_bool = 
  std::invocable<F> and
  std::same_as<std::remove_cvref_t<std::invoke_result_t<F>>, bool>; // ようやくほぼ意図通りになる

これは目を凝らさないと何してるのかわからないものがあり、戻り値型制約と比べると可読性に劣り、書くのもの大変面倒です。

このような場合にauto(x)を使用すると、型のdecaystd::remove_cvref_t)を式としてスマートに記述することができます。

template<typename F>
concept returning_bool = 
  std::invocable<F> and
  requires(F& f) {
    {auto(f())} -> std::same_as<bool>; // 修飾を無視して戻り値型がboolであること!
  };

もちろん、auto(x)によって戻り値のコピーが入る場合があるので若干制約の意味は異なることになるのでその点に注意が必要ではありますが、invoke_result_tremove_cvref_tしてのような謎の呪文を書かなくても、同じことを戻り値型制約の構文の範囲内で表現可能になります。

このことは、上記f()のような関数呼び出し式だけでなく任意の式においても有用となるでしょう。個人的には、本来の用途よりもこちらの用法の方がよく使いそうな気がしています。

参考文献

この記事のMarkdownソース

[C++]C++23<ranges>のユーティリティ

C++23で追加された<ranges>関連の小さめのユーティリティをまとめておきます。ここには新しいファクトリ/アダプタやranges::toは含まれていません。ここで紹介するものは基本的にstd::ranges名前空間にありますが、名前空間指定を省略しています。

const_iterator_t/const_sentinel_t

const_iterator_trange型からその定数イテレータconst iterator)の型を取得するエイリアステンプレートです。const_sentinel_tはそれに対応する番兵型を取得するものです。これらは、iterator_t/sentinel_tの亜種です。

namespace std::ranges {
  template<range R>
  using const_iterator_t = const_iterator<iterator_t<R>>;

  template<range R>
  using const_sentinel_t = const_sentinel<sentinel_t<R>>;
}

定数イテレータはその要素が変更できないイテレータのことで、ほぼ間接参照結果がconst参照になっていると思って差し支えありません。const_sentinel_tで取得できる番兵型も意味合いは同様なのですが、通常番兵を間接参照することはほぼないのでこれはconst_iterator_tと対になるように用意されている側面が強いものです。

定義で使用されているconst_iterator/const_sentinelイテレータ(番兵)型を受けてそれを定数イテレータ(番兵)型に変換するエイリアステンプレートで、この変換に際しては入力イテレータ型の間接参照結果が既にconstではない場合にのみstd::basic_const_iteratorでラップしてイテレータを確実に定数化します。

const_iterator_t/const_sentinel_tで得られるイテレータ型は、C++23以降のstd::ranges::cbegin/std::ranges::cendで取得できるイテレータの型と一致します。

#include <ranges>
using namespace std::ranges;

template<range R>
void f(R& rng) {
  const_iterator_t<R> cit = cbegin(rng);  // Rに関わらずok
  const_sentinel_t<R> cse = cend(rng);    // Rに関わらずok

  *cit = ...; // ng(ほとんどの場合)
}

ただし例外として、間接参照結果がprvalue(すなわち非参照)であるようなイテレータ型(range型)に対するconst_iterator/const_iterator_tの結果のイテレータ型は、間接参照結果がconst参照ではなくprvalueのままとなります(const修飾がある場合は外されます)。これは、間接参照結果のprvalueをどう変更したとしても元のrangeの要素を変更してはいないためと、prvalueの結果をconst参照化するとダングリング参照になるためです。

#include <ranges>
using namespace std::ranges;

// iota_viewの間接参照結果型は要素型のprvalue
void f(iota_view<int> vi) {
  const_iterator_t<iota_view<int>> cit = cbegin(vi);
  const_sentinel_t<iota_view<int>> cse = cend(vi);

  static_assert(std::same_as<decltype(*cit), int>); // パスする

  *cit = 10;  // 要素型が組み込み型の場合ng、クラス型の場合はエラーにならない場合がある
}

// 間接参照結果がstringのprvalueの場合
template<range R>
  requires std::same_as<range_reference_t<R>, std::string>
void g(R& rng) {
  const_iterator_t<R> cit = cbegin(vi);
  const_sentinel_t<R> cse = cend(vi);

  *cit = "str"; // ok、ただしこの変更は観測されない
}

他の特殊な場合として、views::zipのような特殊なイテレータ型(値型と参照型の関係が複雑なイテレータ型)の場合は、const_iterator/const_iterator_tを通した後の間接参照結果型は少し複雑な変換を受けます(参照型std::tuple<T1&, T2&, ...>に対して、std::tuple<const T1&, const T2&, ...>のようになる)。

とはいえそのような特殊なイテレータ型の場合はcommon_referenceのカスタマイズなどを通して適切にconst対応が取られているはずなので、結果的には、const_iterator/const_iterator_tを通したイテレータ型では常にその要素は変更されない(できない)とみなすことができます。よって、これと一致するstd::ranges::cbegin/std::ranges::cendで得られるイテレータも常に定数イテレータになるようになります(C++20時点では、必ずしも定数イテレータを得られない場合がありました)。

なお、これらのエイリアステンプレートによる定数イテレータの参照型の決定は最終的にstd::iter_const_reference_tによって行われます。これについては以前の記事を参照ください

range_const_reference_t

range_const_reference_trangeRから、その間接参照結果をconst化した型を取得するエイリアステンプレートです。

namespace std::ranges {
  template<range R>
  using range_const_reference_t = iter_const_reference_t<iterator_t<R>>;
}

この型は、先程のconst_iterator_tで得られる定数イテレータの間接参照結果の型となります。

#include <ranges>
using namespace std::ranges;

template<range R>
void f(R& rng) {
  const_iterator_t<R> cit = cbegin(vi);

  range_const_reference_t<R> cr = *cit;  // ok
}

ただし、前述のようにprvalueconst化した型はそのままprvalueとなるほか、viws::zipなど要素型が特殊な場合もあるため、range_const_reference_tによって得られる型は参照型ではない場合があり、常にconstであるわけでもありません。それでも、const_iterator_tが必ず定数イテレータを取得するように、range_const_reference_tもまたそれを通して元の範囲の要素を変更できない型を示します。

costant_range

costant_rangeはその要素を変更できないrangeを表すコンセプトです。

namespace std::ranges {
  template<class T>
  concept constant_range = input_range<T> && constant-iterator<iterator_t<T>>;
}

定義はinput_rangeかつそのイテレータconstant-iteratorであることを要求し、constant-iteratorイテレータが定数イテレータであることを表す説明専用のコンセプトです。

このコンセプトは主に、rangeを受ける場所で受け取ったrangeの要素を変更しない場合に使用すると良いでしょう。

#include <ranges>
using namespace std::ranges;

// constant_rangeコンセプトで制約することで、受け取ったrangeの要素を変更しないことを表明
void f(constant_range auto&& rng) {
  // constant_rangeを満たしているため、内部では要素を変更しようとしてもできない
  auto it = begin(rng);
  *it = ...; // ngもしくは無意味

  ...
}

int main() {
  std::vector<int> vec = {1, 2, 3, 4, 5};
  const auto& crv = vec;

  f(vec); // ng
  f(crv);  // ok
}

costant_rangeコンセプトは構文的にイテレータを介して要素が変更不可能であることを要求しているため、関数の実装側もcostant_rangeとして受け取った範囲の要素を変更しようとしても変更できません。

costant_rangeで制約されているところに渡すために範囲を手軽にcostant_range化するには、views::as_constを使用します。

#include <ranges>
using namespace std::ranges;

void f(constant_range auto&& R) {
  ...
}

int main() {
  std::vector<int> vec = {1, 2, 3, 4, 5};

  f(vec); // ng
  f(vec | views::as_const); // ok
}

views::as_constは、入力rangeを単にconst化したり、そのイテレータstd::basic_const_iteratorでラップするなどして、入力rangeconstant_rangeへ変換します。

range_adaptor_closure

ここまでのものは全て定数イテレータに関連するものでしたが、これは上のものとは無関係なものです。

range_adaptor_closureは、レンジアダプタを自作する際に標準のアダプタと|で接続できるようにするためのクラス型です。CRTPによって継承して利用します。

namespace std::ranges {
  template<class D>
    requires is_class_v<D> && same_as<D, remove_cv_t<D>>
  class range_adaptor_closure { }; 
}

まず、これを使用せず何も考えずに自作のレンジアダプタを作成すると、標準のアダプタなら可能なパイプライン演算子|)を使用した入力や合成ができません。

#include <ranges>
using namespace std::ranges;

// 自作のレンジアダプタ型
// 特にパイプのための実装をしていないとする
class my_original_adaptor {
  ...
};

inline constexpr my_original_adaptor my_adaptor{};

int main() {
  std::vector vec = {...};

  // パイプでviewを入力できない
  view auto v1 = vec | my_adaptor; // ng
  view auto v2 = vec | views::take(2) | my_adaptor;  // ng

  // レンジアダプタの合成もできない
  auto raco1 = views::take(2) | my_adaptor;  // ng
  auto raco2 = my_adaptor | views::take(2);  // ng
}

これを可能とするには標準ライブラリ実装が用いているのと同様の方法によってパイプライン演算子を有効化しなければなりませんが、それは実装詳細であり公開されているものではなく、各実装でバラバラです。そこで、range_adaptor_closureを使用すると統一的かつ実装詳細を気にしない方法でパイプライン演算子を有効化することができます。

#include <ranges>
using namespace std::ranges;

// 自作のレンジアダプタ型
// CRTPによってrange_adaptor_closureを継承する
class my_original_adaptor : range_adaptor_closure<my_original_adaptor> {
  ...
};

inline constexpr my_original_adaptor my_adaptor{};

int main() {
  std::vector vec = {...};

  // パイプでviewを入力できるようになる
  view auto v1 = vec | my_adaptor; // ok
  view auto v2 = vec | views::take(2) | my_adaptor;  // ok

  // レンジアダプタの合成も有効化される
  auto raco1 = views::take(2) | my_adaptor;  // ok
  auto raco2 = my_adaptor | views::take(2);  // ok
}

ここでは自作のレンジアダプタmy_original_adaptorの実装詳細を省略していますが、当然それはパイプライン演算子対応以外の部分はきちんとレンジアダプタとして実装されている必要があります。

range_adaptor_closureはこのように、本来とても複雑なパイプライン演算子対応を自動化してくれるものです。レンジアダプタを自作することは稀だと思われるためあまり使用機会はないかもしれませんが、レンジアダプタを自作する場合は非常に有用なものとなるでしょう。

ただ、range_adaptor_closureはその名の通りレンジアダプタクロージャオブジェクト型に対してパイプライン演算子を有効化することしかしません。追加の引数が必要なレンジアダプタオブジェクト型において、入力range以外の引数を予め受けておいてレンジアダプタクロージャオブジェクトを生成する部分についてはrange_adaptor_closureはもちろん、他にも特にサポートがありません。

#include <ranges>
using namespace std::ranges;

int main() {
  std::vector vec = {...};
  
  // views::commonはレンジアダプタクロージャオブジェクト
  view auto v1 = vec | views::common;

  // 追加の引数が必要なものはレンジアダプタオブジェクト
  view auto v2 = vec | views::take(5);
  view auto v3 = vec | views::filter([](auto v) { ... });
  view auto v4 = vec | views::transform([](auto v) { ... });

  // レンジアダプタオブジェクトに追加の引数を予め充填したものはレンジアダプタクロージャオブジェクト
  auto raco1 = views::take(5);
  auto raco2 = views::filter([](auto v) { ... });
  auto raco3 = views::transform([](auto v) { ... });

  // パイプライン演算子はレンジアダプタクロージャオブジェクトに対して作用する
  view auto v5 = vec | raco1; // v2と同じ意味
  view auto v6 = vec | raco2; // v3と同じ意味
  view auto v7 = vec | raco3; // v4と同じ意味
  auto raco4 = raco1 | raco2 | raco3;
}

レンジアダプタを自作する場合、必ずしもレンジアダプタクロージャとして実装できない場合が容易に考えられ、その場合は追加の引数を保存してレンジアダプタクロージャオブジェクトを生成するという部分を実装しなければなりません。

幸いなことに、C++23から追加されたstd::bind_back()std::bind_front()の逆順版)を使用すると、追加の引数の順番を保った保存の部分を委任することができます。

#include <ranges>
using namespace std::ranges;

// 自作のレンジアダプタクロージャ型
template<typename F>
struct my_closure_adaptor : range_adaptor_closure<my_closure_adaptor<F>> {
  F f;

  view auto operator()(viewable_range auto&& input) const {
    return f(input);  // bind_back()でラッピングされたcallbleに引数のrangeを入力しレンジアダプタを実行する
  }
};

// 自作のレンジアダプタ型(not クロージャ)
class my_original_adaptor {

  ...
  
  template<typename... Args>
  view auto operator()(viewable_range auto&& input, Args&&... args) const {
    // 入力rangeと必要な引数がすべてそろった状態で、レンジアダプタを実行し結果のviewを返す
    return ...;
  }
  
  // 追加の引数を受けてレンジアダプタクロージャオブジェクトを返す
  template<typename... Args>
  auto operator()(Args&&... args) const {
    // bind_back()で自身と追加の引数をラッピングし、レンジアダプタクロージャオブジェクトを作成
    return my_closure_adaptor{ .f = std::bind_back(*this, std::forward<Args>(args)...)};
  }
};

inline constexpr my_original_adaptor my_adaptor{};

int main() {
  std::vector vec = {...};

  // my_original_adaptorが追加の引数として整数を1つ受け取るとすると
  view auto v1 = vec | my_adaptor(1); // ok
  view auto v2 = vec | views::take(2) | my_adaptor(2);  // ok
  auto raco = my_adaptor(1);  // ok
  view auto v3 = vec | raco;  // ok、v1と同じ意味
}

my_original_adaptorはここでも実装を省略していますが、そこについては適切に実装されている必要があります(何かいい例があればいいのですが・・・)。ただ、おそらくはほとんどの場合は、この例のように必要なものをすべて受けてレンジアダプタとしての処理を実行する関数呼び出し演算子と、追加の引数だけを受けて対応するレンジアダプタクロージャオブジェクトを作成する関数呼び出し演算子の2つを記述することになると思われます。

おそらく、このmy_closure_adaptor(汎用的なレンジアダプタクロージャオブジェクト型)のようなものは自作のレンジアダプタとは別で作成できて、かつこの例のような典型的な実装になるはずです。そのため、自作のレンジアダプタ毎にこのようなものを作る必要は無いでしょう。この部分がC++23で追加されなかったのは、この部分の最適な実装がまだ確立されていないためだったようです。

宣伝ですが、これらレンジアダプタ(クロージャ)オブジェクトを簡易に作成できるC++20/C++23のライブラリを作っていました。range_adaptor_closurestd::bind_back相当のものをC++20で使用できるほか、my_closure_adaptor相当のものも用意しており、より簡易にレンジアダプタを作成できるようになります。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で122本あります。

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

もくじ

N4946 2024-03 Tokyo meeting information

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

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

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

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

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

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

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

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

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

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

N4950 Working Draft, Standard for Programming Language C++

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

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

N4951 Editors' Report - Programming Languages - C++

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

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

N4953 Concurrency TS2

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

N4954 2023 WG21 admin telecon meetings, rev. 1

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

P0342R2 pessimize_hint

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

以前の記事を参照

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

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

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

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

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

  ...
}

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

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

P0447R22 Introduction of std::hive to the standard library

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

以前の記事を参照

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

P0843R6 static_vector

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

以前の記事を参照

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

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

などです。

P1000R5 C++ IS schedule

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

P1028R5 SG14 status_code and standard error object

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

以前の記事を参照

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

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

などです。

P1061R5 Structured Bindings can introduce a Pack

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

以前の記事を参照

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

P1068R7 Vector API for random number generation

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

以前の記事を参照

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

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

std::array<float, N> array; 

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

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

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

P1112R4 Language support for class layout control

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

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

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

struct S1 {
  bool b;
  int n;
};

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

struct S2 {
  int n;
  bool b;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P1144R8 std::is_trivially_relocatable

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

以前の記事を参照

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

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

などです。

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

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

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

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

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

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

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

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

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

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

P1684R5 mdarray: An Owning Multidimensional Array Analog of mdspan

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

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

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

などです。

P1759R6 Native handles and file streams

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

以前の記事を参照

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

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

P1885R12 Naming Text Encodings to Demystify Them

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

以前の記事を参照

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

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

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

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

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

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

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

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

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

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

    using is_transparent = unspecified;
  };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

以前の記事を参照

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

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

などです。

P2019R3 Thread attributes

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

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

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

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

namespace std {

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

  class thread {
    ...

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

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

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

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

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

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

P2022R2 Rangified version of lexicographical_compare_three_way

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

以前の記事を参照

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

P2141R1 Aggregates are named tuples

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

以前の記事を参照

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

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

などです。

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

P2300R7 std::execution

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

以前の記事を参照

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

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

などです。

P2447R4 std::span over an initializer list

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

以前の記事を参照

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

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

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

P2495R3 Interfacing stringstreams with string_view

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

以前の記事を参照

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

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

P2500R1 C++ parallel algorithms and P2300

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

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

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

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

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

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

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

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

P2546R4 Debugging Support

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

以前の記事を参照

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

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

P2548R5 copyable_function

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

以前の記事を参照

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

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

P2552R2 On the ignorability of standard attributes

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

以前の記事を参照

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

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

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

P2561R2 A control flow operator

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

以前の記事を参照

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

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

P2621R3 UB? In my Lexer?

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

以前の記事を参照

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

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

P2637R2 Member visit

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

以前の記事を参照

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

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

P2641R3 Checking if a union alternative is active

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

以前の記事を参照

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

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

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

などの理由によります。

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

P2643R1 Improving C++ concurrency features

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

以前の記事を参照

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

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

などです。

P2654R0 Modules and Macros

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

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

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

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

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

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

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

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

P2662R1 Pack Indexing

以前の記事を参照

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

以前の記事を参照

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

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

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

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

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

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

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

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

以前の記事を参照

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

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

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

P2664R2 Proposal to extend std::simd with permutation API

P2664R3 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

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

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

などです。

P2685R1 Language Support For Scoped Objects

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

以前の記事を参照

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

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

などです。

P2686R1 constexpr structured bindings and references to constexpr variables

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

以前の記事を参照

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

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

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

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

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

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

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

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

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

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

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

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

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

P2689R2 atomic_accessor

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

以前の記事を参照

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

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

などです。

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

P2717R1 Tool Introspection

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

以前の記事を参照

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

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

などです。

P2727R2 std::iterator_interface

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

以前の記事を参照

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

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

などです。

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

P2728R1 Unicode in the Library, Part 1: UTF Transcoding

P2728R2 Unicode in the Library, Part 1: UTF Transcoding

P2728R3 Unicode in the Library, Part 1: UTF Transcoding

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

以前の記事を参照

R1での変更は

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

R2での変更は

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

R3での変更は

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

などです。

P2741R2 user-generated static_assert messages

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

以前の記事を参照

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

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

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

P2746R2 Deprecate and Replace Fenv Rounding Modes

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

以前の記事を参照

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

P2748R1 Disallow Binding a Returned Glvalue to a Temporary

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

以前の記事を参照

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

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

などです。

P2752R2 Static storage for braced initializers

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

以前の記事を参照

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

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

P2757R2 Type checking format args

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

以前の記事を参照

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

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

P2767R0 flat_map/flat_set omnibus

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

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

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

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

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

P2769R1 get_element customization point object

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

以前の記事を参照

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

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

などです。

P2771R1 Towards memory safety in C++

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

以前の記事を参照

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

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

P2774R0 Scoped thread-local storage

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2775R0 2023-05 Library Evolution Polls

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

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

P2781R1 std::constexpr_v

P2781R2 std::constexpr_v

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

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

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

namespace std {

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

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

    ...
  };
}

提案文書より、使用例

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

inline constexpr short foo = 2;

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

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

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

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

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

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

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

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

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

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

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

P2786R1 Trivial relocatability options

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

以前の記事を参照

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

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

などです。

P2811R2 Contract Violation Handlers

P2811R3 Contract Violation Handlers

P2811R4 Contract Violation Handlers

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

以前の記事を参照

R2での変更は

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

R3での変更は

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

R4での変更は

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

などです。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2814R0 Trivial Relocatability --- Comparing P1144 with P2786

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

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

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

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

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

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

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

P2821R1 span.at()

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

以前の記事を参照

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

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

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

以前の記事を参照

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

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

#include <type_traits>

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

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

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

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

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

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

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

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

struct Dog { 
  operator Cat();
};


Dog d;

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

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

P2829R0 Proposal of Contracts Supporting Const-On-Definition Style

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

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

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

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

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

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

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

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

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

void f(int x);

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

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

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

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

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

というようにします。

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

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

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

P2831R0 Functions having a narrow contract should not be noexcept

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

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

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

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

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

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

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

P2834R0 Semantic Stability Across Contract-Checking Build Modes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2835R0 Expose std::atomic_ref's object address

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2837R0 Planning to Revisit the Lakos Rule

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

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

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

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

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

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

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

P2839R0 Nontrivial relocation via a new "owning reference" type

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

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

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

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

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

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

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

struct T {
  int m;
};

void g(T& x);

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

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

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

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

  g(ref2);  // error
}

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

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

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

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

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

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

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

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

struct T {
  int m;
};

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

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

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

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

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

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

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

P2841R0 Concept Template Parameters

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

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

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

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

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

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

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

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

int main() {
  int n;

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

などの利点があります。

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

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

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

P2842R0 Destructor Semantics Do Not Affect Constructible Traits

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

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

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

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

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

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

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

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

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

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

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

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

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

#include <type_traits>

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

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

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

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

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

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

P2843R0 Preprocessing is never undefined

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

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

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

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

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

P2845R0 Formatting of std::filesystem::path

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2848R0 std::is_uniqued

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

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

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

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


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

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

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

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

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

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

P2850R0 Minimal Compiler Preserved Dependencies

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

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

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

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

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

P2852R0 Contract violation handling semantics for the contracts MVP

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

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

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

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

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

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

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

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

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

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

P2853R0 Proposal of std::contract_violation

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

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

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

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

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

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

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

  // 追加
  enum class contract_resolution { 
    abort_program
  };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

その他の例。

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

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

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

P2855R0 Member customization points for Senders and Receivers

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

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

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

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

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

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

};

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

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

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

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

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

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

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

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

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

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

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

};

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

その他の例

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

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

struct my_sender {

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

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

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

struct S {

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

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

};

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

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

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

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

P2857R0 P2596R0 Critique

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

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

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

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

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

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

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

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

P2858R0 Noexcept vs contract violations

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

同様に、次のようなシンプルな C++ コードでも、text_encoding::name() がヌルポインタを返す場合、容易に未定義動作となります。

std::cout << te.name();             // Violates [ostream.inserters.character] p3
std::format("Name: {}", te.name()); // Violates [format.arg] p5
""sv == te.name();                  // Violates [string.view.cons] p2 since traits::length doesn't accept null values

text_encoding::name() がヌルポインタを返すべき強い理由が見あたらなかったため、戻り値の型が const char* である source_location::file_name() が常に null 終端文字列を返す、という既存事例にならい、text_encoding::name() についても空の文字列を返すようにすることを提案しています。

P2863R0 Review Annex D for C++26

現在非推奨とマークされている機能について、C++26で削除/復帰を検討する提案。

C++23までの間に非推奨とされたコア言語/ライブラリの機能は、必ずしも削除されずに残されており、規格書のAnnex Dセクションにまとめられています。

現在そのような機能は29個(コア言語9, ライブラリ20)あり、この提案はそれらの機能を取り巻く環境や非推奨化の背景を検討したうえで、非推奨のままにしておくのか、非推奨を取り消すのか、削除するのか、を決定しようとするものです。

ただし、この提案はそれらのまとめとインデックスのような文書で、個々の機能それぞれについては個別の提案で詳しく検討されます。C++23では1つの提案にまとめて同じことを行おうとしていましたが、複数のフィードバックが寄せられた結果処理がパンクし提案の改訂が間に合わず、結局C++23設計サイクル中にほとんど議論できなかったためのようです。

現在非推奨となっているコア言語機能の一覧

機能 導入時期 非推奨時期
Arithmetic conversion on enumerations C++98 C++20
Implicit capture of *this by reference C++11 C++20
Array comparisons C++98 C++20
Deprecated use of volatile C++98 C++20
Redeclare static constexpr members C++11 C++17
Non-local use of TU-local entities C++98 C++20
Implicit special members C++98 C++11
Some literal operator declarations C++11 C++23
template keyword before qualified names C++98 C++23

現在非推奨となっているライブラリ機能の一覧

機能 導入時期 非推奨時期
Requires: clauses C++98 C++20
has_denorm members in numeric_limits C++98 C++23
Deprecated C macros C++98 C++23
relops C++98 C++20
char * streams C++98 C++98
Deprecated error numbers C++11 C++23
The default allocator C++17 C++23
polymorphic_allocator::destroy C++17 C++23
Deprecated type traits C++11 C++20
volatile tuple API C++11 C++20
volatile variant API C++17 C++20
std::iterator C++98 C++17
move_iterator::operator-> C++11 C++20
C API to use shared_ptr atomically C++11 C++20
basic_string::reserve() C++98 C++20
<codecvt> C++11 C++17
wstring_convert et al. C++11 C++17
Deprecated locale category facets C++11 C++20
filesystem::u8path C++17 C++20
atomic operations C++11 C++20

P2864R0 Remove Deprecated Arithmetic Conversion on Enumerations From C++26

C++20の一貫比較仕様に伴って非推奨とされた、列挙値から算術型への暗黙変換を削除する提案。

C++20の宇宙船演算子ではenum値と浮動小数点数型や異なる列挙型間の比較を禁止していますが、従来の比較演算子ではそれは暗黙変換によって可能となっており、異なる列挙型の間では算術演算すら可能です。それらの挙動はバグであると思われるため、C++20で非推奨とされました。

ただ、列挙型から浮動小数点数型への暗黙変換は、比較以外の場所では元々禁止されていました。

この提案は、C++26にてそれらの非推奨化されている暗黙変換を削除しようとするものです。この提案が削除しようとしているのは次の2つのものです

  • 列挙型から浮動小数点数型への暗黙変換
  • 異なる列挙型の値から同じ整数型への暗黙変換
int main() {
  enum E1 { e };
  enum E2 { f };

  bool b = e <= 3.7;  // C++20で非推奨、削除を提案
  int k = f - e;      // C++20で非推奨、削除を提案
}

この仕様はC++98にて導入されたもののようで、削除する事は破壊的変更となります。しかし、このような変換はそのメリットよりも意図せず起こしてしまう場合のデメリットの方が大きいため、削除することを提案しています。また、C++26で削除するとするとC++20で非推奨とされてから6年経過しており、その間に主要な実装はこの変換に警告を出すようになっています。

なお、列挙型と整数型の間の演算(列挙型から整数型への暗黙変換)は非推奨とされていないためC++20以降も影響を受けておらず、異なる列挙型間の演算や比較については単項+演算子を使用して片方を整数昇格させることで回避することができたりします。

int main() {
  enum E1 { e };
  enum E2 { f };

  int k =  f - e; // C++20で非推奨
  int x = +f - e; // OK
}

P2865R0 Remove Deprecated Array Comparisons from C++26

C++20の一貫比較仕様に伴って非推奨とされた、配列間の比較を削除する提案。

従来の比較演算子では配列型と配列型の比較が可能で、それは配列の先頭ポインタの比較をおこなっていました。C++20の宇宙船演算子はそれを禁止しており、それに倣って従来の比較演算子では非推奨とされました。

すなわち、配列と配列の等価比較を行っていてもそれは要素ごとの比較ではなく配列の先頭アドレスの比較になっており、trueとなるのは同じ配列同士を比較した時だけです。順序付比較(<など)はより悪く、特定の条件下を除いてほとんどの場合結果は未規定です。

オブジェクトの同一性チェックはそのアドレスを明示的に取得して行うのが一般的かつ最良です。このような暗黙的な変換に頼ることは非常に稀であると思われ、その意味を知らないプログラマがそのコードを見てもその意図を見抜くことはできないでしょう。

したがって、この提案はC++20で非推奨とされた配列同士の比較をC++26で削除することを提案するものです。

より正確には、比較演算子オペランドの型変換の際に、片方のオペランドが配列の場合は他方のオペランドがポインタの場合にのみ配列からポインタへの変換を適用する、というように修正します。

そのため、C++20及びこの提案採択後でも、配列とポインタの間の比較(配列オペランドのポインタへの減衰)は非推奨ではなく合法的な動作です。

int main() {
  int arr1[5];
  int arr2[5];

  bool same = arr1 ==  arr2; // C++20で非推奨、削除を提案
  bool idem = arr1 == +arr2; // OK、アドレスの比較、ただし結果は未規定
}

P2866R0 Remove Deprecated Volatile Features From C++26

C++20で非推奨とされたvolatile関連の機能を削除する提案。

C++20では、無意味だったり危険なvolatile関連の使用法がコア言語・ライブラリ両方において非推奨とされ、その後C++23にて、このうち複合代入演算子の非推奨化は解除されました。

この提案は、C++20で非推奨化されC++23で残っているすべてのvolatileの用法について、削除しようとするものです。

複合代入演算子を除いては、非推奨化に反対するフィードバックは寄せられていないようで、これらの用法を削除することで誤解を招くようなコードを書けないようにすることができます。

P2867R0 Remove Deprecated strstreams From C++26

長く非推奨となっていた、std::strstreamを削除する提案。

std::strstreamは生配列をラップする文字列ストリームであり、std::stringstreamstd::stringをラップする文字列ストリームであるのと比較すると、こちらはchar[N]をラップするストリームです。

std::strstreamメンバ関数.str()char*を返しますが、こうして返された領域をユーザーが解放すべきなのか気にしなくていいのか、どのように管理すべきかが不透明でした。コンストラクタでの構築時はユーザーがその領域を指定することもstd::strstreamに確保させることもでき、std::strstreamに確保させた場合はその領域がどのように確保されたのかはどこにも記載がありません。

正解は、構築時に領域を渡していない場合に.str()で文字列を取得した場合はデストラクタ実行までの間に.freeze(false)を呼び出すことでstd::strstreamのデストラクタがその領域を解放してくれます。しかし、この挙動は分かりづらく、実際あまり周知されていなかったようで、簡単に間違って使うことができてしまっていました。

#include <strstream>

int main() {
  {
    // 動的なバッファを確保してもらう
    std::strstream s1;
    s1 << "dynamic buffer";
    s1 << std::ends;  // 手動null終端

    std::cout << "Contents : " << s1.str() << '\n';
    
    s1.freeze(false); // 忘れるとメモリリーク
  }
  
  {
    // 静的なバッファを渡す
    char buffer[20];
    std::strstream s2{buffer, std::size(buffer)};
    s2 << "static buffer";
    s2 << std::ends;  // 手動null終端

    std::cout << "Contents : " << s2.str() << '\n';
    // freeze()はいらない
  }
}

他にも、.str()のnull終端のためにはユーザーがそれを(std::endsを利用するなどして)ストリームに入力しなければならないなどやはり使いづらいところがあり、これらの問題からC++98で非推奨とされました。

代替としてはstd::stringstreamを用いることができるのですが、こちらはこちらで内部文字列をいつもコピーして返すなどの問題があり、std::strstreamのようにあらかじめ用意した静的な領域を渡すことで動的確保を避けたいような用途としては代替機能がなく、削除されずに残されていました。

C++20では、std::stringstream.view()が追加されたり、.str()がムーブして返すことができるようになるなど、文字列の取得に伴うコピー回避の手段が提供されたほか、std::spansstreamstd::strstreamの完全かつ安全な代替機能として提供されました。

この提案は、std::strstreamを削除する準備が整ったとして、C++26でstd::strstreamと関連する機能を削除しようとするものです。

P2868R0 Remove Deprecated std::allocator Typedef From C++26

std::allocatorにある非推奨化された入れ子型定義を削除する提案。

std::allocatorsize_typepointer等の入れ子型を持っていましたが、これはstd::allocator_traitsによって自動で導出可能であったため、C++17で非推奨とされました。これらについてはC++20で削除されています。

その後、C++23にてis_always_equalも非推奨とされました。この提案は、これはC++26で削除しようとするものです。

std::allocator::is_always_equalは、アロケータがステートレスであるかを調べる入れ子型で、デフォルトではstd::true_typeが使用されます。std::allocatorから派生して独自のアロケータを実装しようとする時にそのアロケータがステートレスではない場合、is_always_equalを上書きしない場合デフォルトのis_always_equal(ステートレスであると表明)が使用されてしまい、静かなバグを埋め込むことになります。

このような誤用を防止し、またわざわざその必要性などを説明する必要をなくすために、この提案ではstd::allocator::is_always_equalを削除しようとしています。

P2869R0 Remove Deprecated shared_ptr Atomic Access APIs From C++26

C++20で非推奨とされた、std::shared_ptrのアトミックフリー関数を削除する提案。

std::shared_ptrには、そのポインタ値そのもの(not参照先)にアトミックアクセスするためのフリー関数が用意されていました。しかし、これらはフリー関数であるため、アトミックアクセスしたい対象のstd::shared_ptrオブジェクトはプログラマが区別する必要がありました。そのため、直接対象のstd::shared_ptrオブジェクトにアクセスすれば非アトミックアクセスとなり、それが複数スレッド間で同時に起きればデータ競合として未定義動作となります。

// アトミックにアクセスしたいshared_ptrオブジェクト
std::shared_ptr<int> atomic_ptr{};

void thread_f() {
  // アトミックにshared_ptrを更新
  std::atomic_store(&atomic_ptr, std::make_shared<int>(20));

  // ポインタへのアクセスがアトミックになっていない
  auto n = *atomic_ptr;

  // こうすると、ポインタへのアクセスがアトミックになる
  auto ptr = std::atomic_load(&atomic_ptr);
  auto m = *ptr;
  // 一行で書いても良い
  // auto m = *std::atomic_load(&atomic_ptr);
}

void f() {
  // アトミック操作関数を経由しなければ非アトミックアクセス
  atomic_ptr = std::make_shared<int>(20);
  auto n = *atomic_ptr;
}

このように、std::shared_ptrのアトミックアクセス用フリー関数は簡単に誤って使用することができ危険だったためC++20で非推奨とされ、代わりにstd::shared_ptr(とstd::weak_ptr)のstd::atomic特殊化が追加されました。こちらを用いると、どこからアクセスした時でもstd::shared_ptrのポインタ値にアトミックにアクセスすることができます。

安全かつ完全に代替できる機能がすでに追加されており、削除することで危険な利用をコンパイルエラーとして報告することができるようになります。また、コードの変更も対象のstd::shared_ptrstd::atomic<std::shared_ptr>に書き換えるだけで済みます。この提案はこれらの理由からstd::shared_ptrのアトミックアクセス用フリー関数を削除しようとする提案です。

ただし、以前にこれらの関数を使用していたコードはstd::shared_ptrstd::atomic<std::shared_ptr>に書き換えた後でstd::atomic*を引数に取るフリー関数を呼び出すようになります。これそのものに問題はないのですが、これは<atomic>ヘッダで定義されているためヘッダ依存関係が変更されます。これを回避するために、削除対象の関数と同名のstd::atomic*を引数に取るフリー関数を<memory>ヘッダで宣言しておくことも提案されています。

P2870R0 Remove basic_string::reserve() From C++26

C++20で非推奨とされたstd::string::reserve()C++26に向けて削除する提案。

std::string::reserve()は元々、キャパシティを増大させるだけではなく減少させることもサポートしていました。これはstd::vector::reserve()の挙動とは異なっており、引数の値によってはパフォーマンス低下を引き起こすなどの問題がありました。

このため、C++20にてstd::string::reserve()はキャパシティを減少させないことが規定され、それに伴ってデフォルト引数(0)を取っていたオーバーロードが非推奨とされました(このオーバーロードは減少しかしないため)。

この提案は、std::string::reserve()のデフォルト引数を持つオーバーロードを削除する提案です。

元々C++20での非推奨時に、その後のLEWGのレビューまでの間に重大な懸念が明らかにならなければこのオーバーロードを削除することに合意されていました。削除を急ぐ理由は特にないようですが、この提案ではその以前の合意に従って削除することを推奨しています。

P2871R0 Remove Deprecated Unicode Conversion Facets From C++26

C++17で非推奨とされた<codecvt>ヘッダをC++26で削除する提案。

<codecvt>ヘッダではユニコードとの間で文字コードの変換を行う機能が提供されていましたが、不正なユニコード文字列を入力されるような攻撃を受けた際にそれをエラーとして安全にハンドルする方法がなく、細かい仕様も曖昧だったりで、その必要性に反して文字コード変換のための機能としては不適当なものでした。

そのため、<codecvt>はヘッダごとC++17で非推奨とされ、SG16はこの議論を契機としてC++により適切なユニコードサポートをもたらすための作業を開始しました。そこでは<codecvt>ヘッダの機能に代わる文字コード変換機能も目標に入っていますが、C++23時点ではまだそのようなものは利用可能ではありません。

SG16がこれを改善する計画や余裕を持たないこと、C++26出荷時点で非推奨期間の方が長くなることなどの理由から、この提案では、<codecvt>C++26でヘッダごと削除することを提案しています。ただし、その名前(codecvt_utf8など)を規格書のゾンビ名セクションに追加しておくことで、標準ライブラリ実装がC++26以降もそれを提供し続けることを許可する(ゾンビ名セクションはこのためにあるようです)ようにしておくことを提案しています。

P2872R0 Remove wstring_convert From C++26

C++17で非推奨とされたwstring_convertC++26で削除する提案。

1つ前の<codecvt>と同様の理由によって、wstring_convertC++26で削除しようとする提案です。

wstring_convert<codecvt>にある機能を使用する窓口のようなもので、C++17でそれらと一緒に非推奨とされました。<codecvt>を削除したとしてもユーザーが代替のものを提供して使い続けることは可能であり、また標準でそれを提供することができる可能性もあるとして、C++23サイクル中のSG16での議論では削除に慎重な意見が聴かれていたようですが、一方で削除に反対する意見はなかったようです。

ここでは削除を提案していますが、<codecvt>とは異なりそれをゾンビ名セクションに入れとくことは提案されていないようです。

P2873R0 Remove Deprecated locale category facets for Unicode from C++26

C++20で非推奨とされたロケールカテゴリファセットをC++26で削除する提案。

ロケールカテゴリファセットとはstd::codecvt/std::codecvt_bynameの特殊化のことで、この提案の対象となっているのは次の4つのものです

codecvt<char16_t, char, mbstate_t>
codecvt<char32_t, char, mbstate_t>
codecvt_byname<char16_t, char, mbstate_t>
codecvt_byname<char32_t, char, mbstate_t>

削除の理由や経緯に関しては、前の<codecvt>wstring_convertと同じです。

P2874R0 Mandating Annex D

Annex Dセクションにある機能の規定について、標準の他の部分と記法を合わせる提案。

Annex Dには過去に非推奨とされまだ削除されていない機能が移動されています。C++20で事前条件や適格要件の書き方が変更(P0788R3)された際、議論時間の都合からAnnex Dの内容はその変更が適用されず、古い記法のまま記述されました。その後、変更が適用された機能が非推奨とされて移動されたことで新旧の記述が入り混じっています。

この提案はAnnex DセクションにもP0788R3を適用し、標準の書き方を完全に統一しようとするものです。

この提案はすでに、2023年6月の全体会議でC++26に適用されることが決まっています。

P2875R0 Undeprecate polymorphic_allocator::destroy For C++26

C++20で非推奨とされたpolymorphic_allocator::destroyの非推奨化を解除する提案。

polymorphic_allocator::destroy()は与えられた領域にあるオブジェクトのデストラクタ呼び出しを行う関数です。これは、std::allocator_traits::destroy()が提供するデフォルト実装と全く同じであり、polymorphic_allocatorがアロケータとして使用されることを考えると冗長なものです。そのため、C++20で非推奨とされました。

しかし、polymorphic_allocatorは語彙型として設計されており、必ずしも従来のアロケータのようにコンテナでstd::allocator_traitsを介して使用されるだけのものではありません。そのような場合、std::allocator_traitsが提供している関数を単体で提供する必要があります。

この提案は、それらの理由とconstruct()との対称性を確保するためにもpolymorphic_allocator::destroy()を非推奨としないようにする提案です。

P2876R0 Proposal to extend std::simd with more constructors and accessors

std::simdに対して、利便性向上のために標準ライブラリにあるデータ並列型等のサポートを追加する提案。

std::simdクラスは、SIMDレジスタとそれに対する演算・操作をラップするようなクラス型で、std::simdのオブジェクトに対してC++コードとして記述した計算をそのまま(自動で)SIMD演算に落とし込むことを目的とするものです。

std::simdは現在、C++26導入を目指して作業が進められています(P1928)。

この提案は、std::simdオブジェクトの入出力の利便性を向上させるために、P1928のstd::simdクラスに欠けている標準ライブラリのクラス型との相互変換を追加しようとするものです。対象となるものは次のものです

  • std::bitset
    • simd_mask型にstd::bitsetを受け取るコンストラクタを追加
    • simd_mask型にstd::bitsetへの変換(変換演算子or明示的な変換関数)を追加
  • 整数値のビット表現の利用
    • simd_mask型に符号なし整数値を受け取るコンストラクタを追加
      • constexpr simd_mask(auto std::unsigned_integral bits) noexcept;
    • simd_mask型に、マスクを整数値のビット表現として取得する.to_ullong()を追加
  • std::initializer_list
    • std::simdstd::initializer_listを受け取るコンストラクタを追加
  • contiguous_range
    • std::simdcontiguous_rangeを受け取って初期化するコンストラクタを追加
      • constexpr simd(std::ranges::contiguous_range auto x);
    • std::arraystd::spanに対しては推論補助も追加

これによって、std::simd及びstd::simd_maskはそれと意味的に同一視できるものから変換する形で構築したり、逆に変換することで値をストアすることができるようになります。

P2878R0 Reference checking

P2878R1 Reference checking

プログラマが明示的に関数の戻り値に関するライフタイム注釈を行えるようにする提案。

ここで提案されているのは、Rustのexplicit lifetimeと呼ばれる機能に近いものです

// 戻り値の生存期間(参照の有効期間)は、引数であるa, bの生存期間を超えないという注釈
fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
  x
}

このような機能は静的解析のような外部のツールによっても達成できるかもしれませんが、それはユーザーがかなりの手間をかけて導入し使用した場合にのみ機能するものでしかありません。このような機能を限定的であったとしても言語機能として持つことで、ライフタイムにまつわる問題を警告ではなくコンパイル時のエラーとして検出できるようになり、より効果的に言語の安全性を高めることができます。

この提案では、コンパイル時に参照に対して4つの生存期間に関するプロパティを付加します。

  1. (生存期間が)グローバル
  2. ローカル
  3. 一時的(一時オブジェクト)
  4. その他(不明)

参照は初期化が必須であるため、これらのプロパティは初期化時に確定する性質です。

const int GLOBAL = 42;

void f(int* ip, int& ir/*生存期間不明*/) {
  int local = 42;

  int& r1 = *ip;    // 生存期間不明
  int& r2 = ir;     // 生存期間不明
  int& r3 = GLOBAL; // 生存期間はグローバル
  int& r4 = local;  // 生存期間はローカル
}

そして、参照がコピーされるときはこのプロパティも同時にコピーされます。

const int GLOBAL = 42;

void f(int* ip, int& ir/*生存期間不明*/) {
  int local = 42;

  int& r1 = *ip;    // 生存期間不明
  int& r2 = ir;     // 生存期間不明
  int& r3 = GLOBAL; // 生存期間はグローバル
  int& r4 = local;  // 生存期間はローカル

  // 参照のコピー
  int& r5 = r1;     // 生存期間不明
  int& r6 = r2;     // 生存期間不明
  int& r7 = r3;     // 生存期間はグローバル
  int& r8 = r4;     // 生存期間はローカル
}

このプロパティだけでも、ローカル変数の参照をreturnする関数をエラーにすることができます

const int GLOBAL = 42;

int& f(int* ip, int& ir/* unknown lifetime */) {
  int local = 42;

  int& r1 = *ip;    // 生存期間不明
  int& r2 = ir;     // 生存期間不明
  int& r3 = GLOBAL; // 生存期間はグローバル
  int& r4 = local;  // 生存期間はローカル

  // 参照のコピー
  int& r5 = r1;     // 生存期間不明
  int& r6 = r2;     // 生存期間不明
  int& r7 = r3;     // 生存期間はグローバル
  int& r8 = r4;     // 生存期間はローカル

  return r8;  // error! ローカル参照を返している
}

たとえば次のようにして、ある参照の生存期間を別の参照の生存期間に関連付けることができるようにします

const int GLOBAL = 42;

// 戻り値の参照の有効期間は引数left/right(のより短い方)の有効期間より長くない
[[dependson(left, right)]]
const int& f1(const int& left/* unknown lifetime */, const int& right/* unknown lifetime */) {
  if (randomBool()) {
    return left;
  } else {
    return right;
  }
}

この時、先程のプロパティはtemporary < local < globalの順で生存期間が短いとされます。この順序によって、この関数の戻り値の参照の生存期間は引数left/rightの生存期間のより短いものに制限されます。

これによってさらに次のチェックが可能となります

  • 一時オブジェクトへの参照を返すとエラー
  • 一時的な生存期間を持つ参照(一時オブジェクトへの参照)を初期化した後、別の行(別の完全式)で使用するとエラー
int& f2() {
  int local = 42;

  const int& r1 = f1(local, local);   // local
  const int& r2 = f1(GLOBAL, GLOBAL); // global
  const int& r3 = f1(42, 42);         // temporary
  const int& r4 = f1(local, GLOBAL);  // local
  const int& r5 = f1(local, 42);      // temporary
  const int& r6 = f1(GLOBAL, 42);     // temporary
  
  if (randomBool()) {
    return r1;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r2;  // OK、r2はglobal参照
  }
  if (randomBool()) {
    return r3;  // error: temporary参照を返せない
  }
  if (randomBool()) {
    return r4;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r5;  // error: temporary参照を返せない
  }
  if (randomBool()) {
    return r6;  // error: temporary参照を返せない
  }

  int x1 = r3 + 43; // error: temporary参照を使用できない
  int x2 = r5 + 44; // error: temporary参照を使用できない
  int x3 = r6 + 45; // error: temporary参照を使用できない
  return f1(f1(GLOBAL, 4), f1(local, 2)); // error: temporary参照を返せない
}

これによって、ローカル変数の間接的なダングリングだけでなく、一時オブジェクトの間接的なダングリングも修正されます。

さらに、クラス型が参照をpublicメンバとして持っている場合も、同様の事を行うことができます。

// 参照をpublicメンバとして含むクラス型
struct S {
  int& first;
  const int& second;
};

int& f2() {
  int local = 42;

  S s1{GLOBAL, local};
  S s2{local, f1(GLOBAL, 24)};
  
  const int& r1 = s1.first;   // global
  const int& r2 = s1.second;  // local
  const int& r3 = s2.first;   // local
  const int& r4 = s2.second;  // temporary
  
  if (randomBool()) {
    return r1;  // OK: r2はglobal参照local参照を返せない
  }
  if (randomBool()) {
    return r2;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r3;  // error: local参照を返せない
  }
  if (randomBool()) {
    return r4;  // error: temporary参照を返せない
  }

  int x = r4 + 43;  // error: temporary参照を使用できない

  return 42;  // error: temporary参照を返せない
}

S f3() {
  int local = 42;

  S s1{GLOBAL, local};
  S s2{local, f1(GLOBAL, 24)};
  
  if (randomBool()) {
    return s1;  // error: local参照を含んでいる
  }

  return s2;  // error: localとtemporary参照を含んでいる
}

また、言語組み込み機能であればpublicではなくても同様の事を検出できます

// local/temporaryなものをキャプチャもしくは`return`するラムダを返せないようにする
auto lambda() {
  int local = 42;
  
  const int& ref_temporary = f1(GLOBAL, 24);

  return [&local, &ref_temporary]() -> const int& {
      if(randomBool()) {
        return local; // error: local参照を返せない
      }

      return ref_temporary; // error: temporary参照を返せない
    };
  // error: localとtemporary参照を含んでいる
}

// local/temporaryなものをキャプチャもしくは`return`するコルーチンを返せないようにする
auto coroutine() {
  int local = 42;
  
  const int& ref_temporary = f1(GLOBAL, 24);

  return [&local, &ref_temporary]() -> generator<const int&> {
      if(randomBool()) {
        co_return local;  // error: local参照を返せない
      }

      co_return ref_temporary;// error: temporary参照を返せない
    };
  // error: localとtemporary参照を含んでいる
}

クラス型が非publicな形で参照を内部に含む場合、それを取得しようとするメンバ関数に対してそのオブジェクトそのものに依存するライフタイムを注釈する追加の構文によって同様の検出を行います

namespace std {
  template <class charT,
            class traits = char_traits<charT>,
            class Allocator = allocator<charT> >
  class basic_string {
    ...

    // 戻り値の参照(likeなオブジェクト)はthisに依存する
    constexpr std::string::operator [[dependson(this)]] std::basic_string_view<CharT, Traits>() const noexcept;
  }
}

int main() {
  std::string_view sv = "hello world"s; // temporary

  sv.size();  // error: temporary参照を使用できない
}

これはstd::spanstd::function_refなどの他の参照セマンティクスを持つ型でも使用できます。ただし、reference_wrapperのように再束縛できる(あとから参照先を切り替えられる)ものについてはこれを適用できません。他にもポインタ型やstd::unique_ptr当のポインタセマンティクスを持つ型が該当します。

ただし、そのような型でもconstであれば初期化時に非nullで初期化されていると推定でき、また後から参照先が変化しないため同様のことが行えます。

[[dependson(left, right)]]
const std::reference_wrapper<const int> f(const int& left/* unknown lifetime */, const int& right/* unknown lifetime */) {
  if(randomBool()) {
    return std::cref(left);
  } else {
    return std::cref(right);
  }
}

最後に、これらのチェックはnew式による初期化時にも適用できます

struct S { int mi; const std::pair<int,int>& mp; };

S a { 1, {2,3} };
S* p = new S{ 1, {2,3} }; // error: オブジェクトはtemporaryに依存する

もし参照がpublicではない場合は、そのコンストラクタでその依存関係を指定する追加の構文が必要になります

class S {
  int mi;
  const std::pair<int,int>& mp;

public:
  [[parameter_dependency(dependent{"this"}, providers{"mp"})]]
  S(int mi, const std::pair<int,int>& mp);
};

S a { 1, {2,3} };         // error: オブジェクトはtemporaryに依存する
S* p = new S{ 1, {2,3} }; // error: オブジェクトはtemporaryに依存する

この提案では構文の説明のために属性構文が使用されていますが、属性として採用すべきか別の言語機能として採用すべきかは提案しておらず、むしろその能力を獲得することを目的としています。

この提案はSG23でレビューされ、引き続き議論されないことが決定しています。

P2880R0 Algorithm-like vs std::simd based RNG API

複数の乱数の効率的な生成のためのAPIとして、提案中のベクターAPIstd::simdによるAPIを比較する提案。

P1068では大量の乱数を効率的に生成するための高レベルなベクターAPIを提案しており、そこでは範囲に対して乱数を充填するAPIによって実装が効率的な乱数生成方法を選択できるようにしています。その実装には例えばSIMD演算によるものが想定されています。

一方で、P1928で議論されているstd::simdSIMDレジスタと命令のラッパクラスであり、std::simdそのものの操作あるいはAPIによって直接的にそのような大量乱数生成の効率実装を行うことができます。そのため、std::simd導入を見据えた場合にP1068の高レベルAPIによる複数乱数生成は必要なのか?あるいはstd::simdに乱数生成のためにどのようなAPIを持たせるべきか?と言ったことが疑問として浮かんできます。

この提案はそのような疑問に答えるために、両者のAPIによるコードを比較することで利点欠点を洗い出し、std::simdの乱数生成APIのいくつかの可能性を示すものです。

この提案では、"European options pricing"というベンチマーク中から複数の乱数を生成しているコードを抽出し、それをP1068とstd::simdを用いて実装してみるとどうなるかを示すことで比較を行なっています。

// "European options pricing"中の複数の乱数を生成し利用するコード

std::mt19937 engine(777); // 乱数エンジン
std::normal_distribution distribution(0., 1.);  // 分布生成器

double v0 = 0, v1 = 0;

// ループ(npath)の分乱数を生成する
for (std::size_t p = 0; p < npath; ++p) { //e.g., npath=1,000,000
  // 乱数の生成
  double rand = distribution(engine);

  // 乱数の利用
  double res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
  v0 += res;
  v1 += res * res;
}

// 結果出力
result     = v0 / npath;
confidence = v1 / npath;

P1068R7の高レベルAPI

まずP1068R7ではstd::ranges::generate_randomという関数(正確にはCPO)を用いて、範囲に対して指定されたエンジンと分布によって生成した乱数を充填します。それによって、先ほどのコードは次のようになります

std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);

// 生成した乱数を受けるための範囲
std::array<double, npath> rand; // npath=1,000,000 -> sizeof(rand)=8 MB

// 複数の乱数を一括生成し範囲に詰め込む
std::ranges::generate_random(rand, engine, distribution);

double v0 = 0, v1 = 0;

// 乱数の利用
for(std::size_t p = 0; p < npath; ++p) {
  double res = std::max(0., S * exp(sqrt(T) * vol * rand[p] + T * mu) - X);
  v0 += res;
  v1 += res * res;
}

result     = v0 / npath;
confidence = v1 / npath;

std::ranges::generate_randomによってループ前に使用予定の乱数を全て生成しているため、ループの中は乱数利用コードのみになります。前述のように、これはSIMD命令等を用いて効率的に乱数生成を行う実装がなされるはずなので、ループで生成しながら利用するコードよりも効率的になることが期待されます。

ただし、この場合生成した乱数を受けるためにそこそこ巨大な配列を最初に用意しなければなりません。これはキャッシュヒット率を低下させることが予想されるため、最適なコードとは言えません。そこで効率化のために、バッファリングを行います

std::mt19937 engine(777);
std::normal_distribution distribution(0., 1.);

// 乱数を受けるバッファ
std::array<double, nbuffer> rand; // e.g., nbuffer=128

double v0 = 0, v1 = 0;

// nbuffer分づつ乱数を生成して利用するループ
for(std::size_t p = 0; p < npath; p += nbuffer) {
  // 末尾の調整(npathはnbufferの倍数とは限らない)
  std::size_t local_size = (p + nbuffer <= npath) ? nbuffer : (npath - p); // dealing with tail
  // nbuffer分乱数を生成
  std::ranges::generate_random(std::span(rand.begin(), local_size), engine, distribution);

  // 乱数の利用
  for(std::size_t b = 0; b < local_size; ++b) {
    double res = std::max(0., S * exp(sqrt(T) * vol * rand[p] + T * mu) - X);
    v0 += res;
    v1 += res * res;
  }
}

result     = v0 / npath;
confidence = v1 / npath;

nbuffer分づつ処理を分けることで、1度に生成する乱数とそれを保存するためのストレージサイズをnbufferに減らします。その代わり乱数生成を一括で行う単位も減ってしまいますが、キャッシュミスによるペナルティよりも一度の乱数生成オーバーヘッドの方が小さい間はこちらの方が効率的になります。

std::simdAPI

次に、std::simdでも同じことを考えます。ただし、std::simdはまだそのようなAPIを持たないため、その可能な設計として幾つかのパターンが考えられます。この設計で重要なことは、ユーザーが要求するstd::simd型に関する情報をどのレベルで取得するか?ということです

1. エンジンのテンプレートパラメータ

1つ目の例は、エンジンと分布生成器の両方がstd::simd型に関する情報を持って構築されるものです。

std::mt19937<std::fixed_size_simd<std::uint_fast32_t, 16>> E(777);    // 乱数エンジン
std::normal_distribution<std::fixed_size_simd<double, 16>> D(0., 1.); // 分布生成器

// 乱数の生成
auto rand = D(E);

エンジンと分布生成器の数値型が異なっており、既存の実装はこのような場合normal_distributiondouble値1つに対してエンジンのuint_fast32_t値を2つ消費します(全体では、乱数16個に対してエンジン出力32個を消費する)。そのため、実装によっては次のように定義した方が効率的である場合があります

// 32 SIMD size passed to engine
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, 32>> E(777);
// 16 SIMD size passed to distribution
std::normal_distribution<std::fixed_size_simd<double, 16>> D(0., 1.);

auto rand = D(E);

より洗練された分布生成器では結果値ごとに異なった個数の入力エンジン値を消費する分布の実装が一般的となるため、分布生成器とSIMD幅が与えられた時にそれに最適なエンジンのSIMD幅の適切なサイズの普遍的な解答はありません。

そのような分布生成器において、エンジンが生成した固定幅の乱数配列の一部しか消費しない場合、使用しなかった残りの部分について選択肢が生まれます

  1. 残りの部分は分布オブジェクトが内部に保存する
    • 保存する領域のオーバーヘッドやどう使用されるかが問題となる
    • 残りの部分が次の生成に使用されるとすると、分布のランダム性に残った値の利用という要素が追加される
  2. 残りの部分は廃棄する

このことを念頭に置いて、最初のコードをこのAPIで書き直したのが次のコードです

// SIMD幅
constexpr std::size_t size = 16;

// エンジンと分布生成器
std::mt19937<std::fixed_size_simd<std::uint_fast32_t, size>> E(777);
std::normal_distribution<std::fixed_size_simd<double, size>> D(0., 1.);

double v0 = 0, v1 = 0;
std::size_t p = 0;

for(; p + size <= npath; p += size) {
  // 複数乱数(size個分)の一括生成
  auto rand = D(E); // std::fixed_size_simd<double, size>

  // 乱数の利用(自動SIMD化)
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
  v0 += std::reduce(res);
  v1 += std::reduce(res * res);
}

// 処理数(npath)が16(size)の倍数ではない場合の端数の処理
if (p != npath) {
  // 複数乱数(size個分)の一括生成
  auto rand_tail = D(E);

  // 乱数の利用
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
  for(std::size_t i = 0; p + i < npath; ++i) {
      v0 += res[i];
      v1 += res[i] * res[i];
  }
  // resには使用されない部分があり、捨てられる
}

result     = v0 / npath;
confidence = v1 / npath;

このコードでは、npathsize(16)の倍数ではない場合に必要以上の数の乱数を生成してしまうため最初のコードど完全に同じことをしているわけではありませんが近いコードではあります。そして、端数の処理の際に生成した乱数(rand_tail)の一部を捨ててしまうことになりますが、その捨て方が問題となる可能性があります(エンジンや分布生成器を再利用する場合など)。

2. エンジンのテンプレートパラメータ + 再バインドコンストラク

それらの問題を念頭に置いて次のAPI設計案では、異なるSIMD幅やスカラ型を持つエンジンの再バインド構築によって未使用部分の問題を回避します。再バインドによって以前のエンジンの内部状態を引き継ぐことで、余分な乱数の生成を防止し、捨てられる値や目に見えない状態を回避します。

constexpr std::size_t size = 16;

std::mt19937<std::fixed_size_simd<std::uint_fast32_t, size>> E(777);
std::normal_distribution<std::fixed_size_simd<double, size>> D(0., 1.);

double v0 = 0, v1 = 0;
std::size_t p = 0;

for (; p + size <= npath; p += size) {
  // 複数乱数(size個分)の一括生成
  auto rand = D(E);

  // 乱数の利用
  auto res = std::max(0., S*exp(sqrt(T) * vol * rand + T * mu)-X);
  v0 += std::reduce(res);
  v1 += std::reduce(res * res);
}

// 処理数(npath)が16(size)の倍数ではない場合の端数の処理
if (p != npath) {
  // エンジンの再バインド
  std::mt19937 E_tail(E); // rebinding to scalar type
  std::normal_distribution D_tail(0., 1.); // getting scalar distribution

  for (; p < npath; ++p) {
    // 乱数の生成(スカラAPI)
    auto rand_tail = D_tail(E_tail);

    // 乱数の利用
    auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
    v0 += res;
    v1 += res * res;
  }

  // 元のエンジンに状態を返す
  E = E_tail;
}

result     = v0 / npath;
confidence = v1 / npath;

乱数エンジンの再バインド機構によって、乱数エンジンの生成値の型を変更しつつエンジンの内部状態を引き継ぐことができるようにしています。

この場合、ユーザーレベルの柔軟性が得られる一方で余計なコピーが追加されており、状態の大きなエンジンではそのオーバーヘッドが問題となる可能性があります。

3. アルゴリズムlikeな関数のテンプレートパラメータ

更なる代替案として、std::simd型の情報を受け取る点をエンジン/分布生成器からその使用地点に移すAPIが考えられます。これによって、エンジンはその使用モードを意識することなく、内部のレイアウトは実装によってスカラ/ベクターどちらの生成も可能とするバランスの取れた形に選択されます。

std::mt19937             E(777);
std::normal_distribution D(0., 1.);

// 乱数生成
auto rand = std::generate_random_simd<std::fixed_size_simd<double, 16>>(E, D);

エンジンレイアウトの厳密な(環境のSIMD幅に合わせた)最適化が制限される代わりに、標準ライブラリに実装の自由が与えられ、エンジンからベース乱数を消費する方法はプラットフォームによって異なる可能性があリます。

constexpr std::size_t size = 16;

std::mt19937 E(777);
std::normal_distribution D(0., 1.);

double v0 = 0, v1 = 0;
std::size_t p = 0;

for (; p+size <= npath; p += size) {
  // 複数乱数(size個分)の一括生成
  auto rand = std::get_random_simd<std::fixed_size_simd<double, size>>(E, D);

  // 乱数の利用
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand + T * mu) - X);
  v0 += std::reduce(res);
  v1 += std::reduce(res * res);
}

// 処理数(npath)が16(size)の倍数ではない場合の端数の処理
for (; p < npath; ++p) {
  // 乱数の生成(スカラAPI)
  auto rand_tail = D(E);

  // 乱数の利用
  auto res = std::max(0., S * exp(sqrt(T) * vol * rand_tail + T * mu) - X);
  v0 += res;
  v1 += res * res;
}

result     = v0 / npath;
confidence = v1 / npath;

この例は最初のコードと完全に一致しており、余分な乱数を生成してエンジン状態が不明になったり、それを回避するためにエンジン状態をコピーするなどの問題を回避しています。

これらの比較と観察から得られる結論は次のようなものです

  • 高レベルのAPIは通常のC++開発者が作成する乱数利用アプリケーションの大部分をサポートすることを目的としている
    • APIを実装するベンダがHWアクセラレータを有効にする実装をとれば、そのようなアプリケーションのパフォーマンスを向上できる
    • SIMDを利用した実装はその一種であり、同じAPIを使用して利用可能となる
  • 低レベルの(std::simdによる)APIC++の慣習に則った上でよりHWに近いコーディングを必要とする上級開発者を対象としている
    • ただし、std::simdによるAPIではエンジンと分布生成器の概念に基づいた乱数生成アルゴリズムSIMD実装の詳細を開発者に理解させることは避けるべき

従って、P1068の高レベルのAPIstd::simdによるAPIはターゲットが異なるため排他的なものではなく、両方を標準ライブラリに持つことは合理的であると考えられます。

P2881R0 Generator-based for loop

範囲for文に新しいループカスタマイゼーションポイントを追加する提案。

<ranges>のRangeアダプタなどに見られるようにイテレータの定義は複雑で、何か処理を範囲for文でループさせるように書き直そうとするとその対象の状態をoperator*operator++に分割してエンコードしてやる必要があり、これによって処理のイテレータrange)への移行は非常に難しくなっています。

C++20のコルーチンとC++23std::generatorの利用によってそれは劇的に簡単になります。例えばstd::generatorを使用するとviews::filterviews::joinviews::concatは次のように簡単に定義できます

template <typename Rng, typename Predicate>
auto filter(Rng&& rng, Predicate predicate) -> std::generator<…> {
  for (auto&& elem : rng) {
    if (predicate(elem)) {
      co_yield std::forward<decltype(elem)>(elem);
    }
  }
}

template <typename Rng>
auto join(Rng&& rng_of_rng) -> std::generator<…> {
  for (auto&& rng : rng_of_rng) {
    co_yield std::ranges::elements_of(std::forward<decltype(rng)>(rng));
  }
}

template <typename ... Rng>
auto concat(Rng&& ... rng ) -> std::generator<…> {
  ((co_yield std::ranges::elements_of(std::forward<decltype(rng)>(rng))), ...);
}

ただしstd::generatorもいいとこづくめではなく、いくつかデメリットがあります

  1. パフォーマンスでイテレータに劣る
    • コルーチンステート保存のためのメモリ確保や、関数の中断のサポート、例外機構などによるオーバーヘッドが回避できない
  2. ネストした文脈でco_yieldを使用できない
    • スタックレスコルーチンはその内部で呼び出した関数内などのスタックフレームの異なる場所で中断できない

2つ目の問題は、ツリー構造のようなネストした構造に対してstd::generatorを直接適用できない場合がある問題で、次のようなものです

struct tree {
  using leaf = int;
  // ノードの途中か末端かのどちらか
  std::variant<leaf, std::vector<tree>> impl;
};

// ツリー構造を辿って末端の値を出力していく
std::generator<int> tree_data(const tree& t) {
  std::visit(
    overloaded(
      [&](int data) {
        co_yield data; // error
      },
      [&](const std::vector<tree>& children) {
        for (auto& child : children)
        co_yield std::ranges::elements_of(tree_data(child)); // error
      }),
    t.impl);
}

これは、コルーチンがその内部で呼び出した関数で中断(co_yieldco_await)できないというスタックレスコルーチンの特性によるものです。ネストした部分を別のコルーチンにすることで回避はできます

std::generator<int> tree_data(const tree& t) {
  auto sub =
      std::visit(
        overloaded(
          [&](int data) -> std::generator<int> {
            co_yield data;
          },
          [&](const std::vector<tree>& children) -> std::generator<int> {
            for (auto& child : children) {
              co_yield std::ranges::elements_of(tree_data(child));
            }
          }),
        t.impl);

  co_yield std::ranges::elements_of(sub);
}

この提案は、コルーチンによるgeneratorの利点を享受しつつこれらのデメリットを回避するような、generatorにちかい記述によって処理をループに落とし込むための仕組みを提案し、それを範囲for文のカスタマイゼーションポイントとして追加しようとするものです。

この提案によるgeneratorgenerator ranges(ジェネレータ範囲)と呼ばれており、それは、範囲for文の処理本体をラムダ式として受け取ってそれを適宜呼び出しながら処理を実行する関数オブジェクト的な何かです。ジェネレータ範囲による範囲for文は、begin()/end()を使用したイテレータループを行う代わりに、そのループ本文をラムダ式としてジェネレータ範囲のオブジェクトの関数呼び出し演算子として渡して、ジェネレータ範囲のオブジェクトの関数呼び出し演算子では、そうして受け取ったループ本文に適宜各要素を渡して呼び出すことでループを実行します。

// 1, 2, 3を生成するジェネレータ範囲
struct generator123 {

  // sinkには、呼ばれた範囲forの本体処理をcallableとして受ける
  auto operator()(auto&& sink) const {
    // 範囲forの本体がループを継続しているかを判定する
    std::control_flow flow;

    // 範囲forの本体処理に1を入力し(xに1が代入され)て実行
    flow = sink(1);
    if (!flow) return flow;

    // 範囲forの本体処理に1を入力し(xに2が代入され)て実行
    flow = sink(2);
    if (!flow) return flow; // この例ではここで終わる

    // 範囲forの本体処理に1を入力し(xに3が代入され)て実行
    return sink(3);
  }
};

for (int x : generator123{}) {
  std::print("{}\n", x);
  if (x == 2) {
    break;
  }
}
// 1
// 2

std::control_flowはジェネレータ範囲による範囲for文の本体内のbreak/continueの結果を表現するクラス型です。これは基本的に強く型付けされたbool型で、次のように定義されます

namespace std {

  /// `continue`に対応するタグ型とそのオブジェクト
  struct continue_t {
    // Empty.

    constexpr operator std::true_type() const noexcept {
      return {};
    }

    constexpr std::false_type operator!() const noexcept {
      return {};
    }

    friend std::strong_ordering operator<=>(continue_t, continue_t) noexcept = default;
  };
  inline constexpr continue_t continue_;

  /// `break`に対応するタグ型とそのオブジェクト
  struct [[nodiscard("need to forward break")]] break_t {
    // Empty.

    constexpr operator std::false_type() const noexcept {
      return {};
    }

    constexpr std::true_type operator!() const noexcept {
      return {};
    }

    friend std::strong_ordering operator<=>(break_t, break_t) noexcept = default;
  };
  inline constexpr break_t break_;


  /// `continue/break`または実装定義の`break`に似た状態を表す制御フローオブジェクト
  class [[nodiscard("need to forward control flow")]] control_flow {
  public:
      /// `continue`状態で構築
      constexpr control_flow(continue_t) noexcept;
      constexpr control_flow() noexcept : control_flow(continue_) {}

      /// `break`状態で構築
      constexpr control_flow(break_t) noexcept;

      /// Trivially copyable.

      /// `continue`の時true, それ以外の場合は`false`を返す
      constexpr explicit operator bool() const noexcept;

      constexpr friend bool operator==(control_flow, control_flow) noexcept;
      constexpr friend std::strong_ordering operator<=>(control_flow, control_flow) noexcept;
  };
}

std::continue_std::break_std::true_type/std::false_typeへの定数変換演算子を持つ個別のタグ型として定義されており、これによって常に継続や常に中断といった一般的なケースを型システムにエンコードして最適化を保証することができます。

ジェネレータ範囲による範囲for文は通常の範囲forと同様に言語組み込みマクロのようなもので、次のように展開されます

for (T binding : object)
{
  body
}

{
  auto __body = [&](T&& __element) -> see-below {
    T binding = std::forward<T>(__element);
    body
    return std::continue_;
  };

  auto __flow = object(__body);
  
  see-below // body内のreturn/gotoがここに配置される
}

関数本体であるbodyで特に終了(returnbreak)をしなければ、デフォルトでstd::continue_が返されることで処理を継続させます。bodyに何か制御文を書くとそれはstd::control_flowの値を返すように変換されます

  • continue;
    • return std::continue_;
  • break;
    • return std::break_;
  • return;
    • return implementation-defined
    • std::break_;と同じ効果となるが、コンパイラはループの後ろにreturnを配置する
  • return expr;
    • exprを実行してその結果をどこかに保存してから、return implementation-defined
    • std::break_;と同じ効果となるが、コンパイラはループの後ろにreturnを配置し戻り値を返す
  • goto
    • return implementation-defined
    • std::break_;と同じ効果となるが、コンパイラはループの後ろにgotoを配置する
  • throw
    • そのまま
  • co_await/co_yield/co_return
    • ill-formed

これによって、ジェネレータ範囲による範囲for文は利用者から見ると通常のfor文とほとんど透過的に使用することができます。

// このジェネレータ範囲によるループは
for (int x : generator123{}) {
  if (x == 0)
    continue;
  
  if (x == 2)
    break;
  
  std::printf("%d\n", x);
}

// こう展開される
{
  auto __body = [&](int&& __element) -> std::control_flow {
    int x = __element;
    
    if (x == 0)
      return std::continue_;
    
    if (x == 2)
      return std::break_;

    std::printf("%d\n", x);
    
    return std::continue_;
  };

  auto __flow = generator123{}(__body);
  (void)__flow;
}

この新しい範囲for文では、co_yieldの代わりに関数呼び出し演算子を通してループ本体に要素(値)を提供することで擬似的な関数の中断と値の生成を実現し、ループの処理が終わると自動的に再開されます。それはコルーチンのような複雑な仕組みを全く用いておらず、そのために導入されていたオーバーヘッドも全くありません。そのため、パフォーマンスではイテレータと同等かそれ以上のものを達成でき、コルーチンあるいはstd::generatorの制約によるデメリットも回避されます。

views::filterの例

template <typename Rng, typename Predicate>
auto filter(Rng&& rng, Predicate predicate) {
  // ループ処理本体を受け取るジェネレータ範囲を返す
  return [=](auto sink) {
    for (auto&& elem : rng) {
      if (predicate(elem)) {
        auto result = sink(std::forward<decltype(elem)>(elem));
        if (result == tc::break_) {
          return result;
        }
      }
    }

    return tc::continue_;
  };
}

ネストした構造に対しても、ほぼそのまま適用できます

auto tree_data(const tree& t) {
  return [&](auto sink) {
    auto flow =
      std::visit(
        overloaded(
          [&](int data) {
            // Forward break/exit.
            return sink(data);
          },
          [&](const std::vector<tree>& children) {
            for (auto& child : children) {
              auto flow = tree_data(child)(sink);
              if (flow == tc::break_) {
                // Forward early break and do actually break.
                return flow;
              }
            }
            return tc::continue_;
          }),
        t.impl);

    return flow;
  }
}

P2882R0 An Event Model for C++ Executors

実行コンテキスト間でやりとりするための標準的な方法を提供するための設計や問題点について探るための文書。

P2300では実行コンテキストをschedulerコンセプトによって抽象化しており、そこではschedulerを提供する以外のことを要求していません。そのため、異なる実行コンテキストから制御を移すためのインターフェースが欠けており、それを行うためのtransfer()アルゴリズムのようなものの実装が難しくなっています。

例えば、次のようなネットワークから音源をダウンロードしてきて、それをデコードし再生(再生デバイスへ転送)するような処理を考えます

// 音源のダウンロード
void receive() {
  SnapClient srv{srvAddr, srvPort};

  while (true) {
    // データを受信し
    std::span<uint8_t> buf = srv.receiveWireChunk(); // blocks

    // キューに入れる
    opusQueue.wait_push(buf);
  }
}

// PCMへのデコード
void decode() {
  while (true) {
    std::span<uint8_t> inBuf;
    // 受信データをキューから取り出し
    opusQueue.wait_pop(inBuf);

    // デコードし
    int samples = opus_decode(decoder,
                              inBuf.data(), inBuf.size(),
                              decodeBuf.data(), maxFrameSamples,
                              0);

    std::span outBuf(decodeBuf.data(), samples);
    // キューに入れる
    pcmQueue.wait_push(outBuf);
  }
}

// PCMの再生(デバイスへの転送)
void play() {
  while (true) {
    std::span<uint8_t> inBuf;
    // PCMデータをキューから取り出し
    pcmQueue.wait_pop(inBuf);
    
    uint32_t const *start = inBuf.data();
    size_t offset = 0;

    // 再生デバイスへ転送する
    while (offset < size) {
      size_t bytesDone;

      // i2s_channel_write blocks
      i2s_channel_write(tx,
                        start + offset,
                        size - offset,
                        &bytesDone,
                        noTimeout);
      
      offset += bytesDone;
    }
  }
}

この3つの関数はそれぞれ別のスレッドで実行されます。この時、各スレッド(実行コンテキスト)間のやりとりにはキューが使用されています。

これをP2300のsender/receiverによって実装すると例えば次のようになります

void network_speaker() {
  // デコードと再生を行うための実行コンテキストのschedulerを取得
  exec::scheduler auto sched0 = cppos::ContextCore0::LoopScheduler();
  exec::scheduler auto sched1 = cppos::ContextCore1::LoopScheduler();

  AudioServer srv(sched0, srvAddr, audioPort);

  srv.readPacket()                              // データ受信
      | bufferedTransfer<netBufferSize>(sched1) // 結果をバッファにつめて実行コンテキスト遷移
      | then(soundDecode)                       // 受信データのデコード
      | bufferedTransfer<pcmBufferSize>(sched0) // 結果をバッファにつめて実行コンテキスト遷移
      | then(sendI2sChunk())                    // PCMを再生デバイスへ転送
      | runForever();                           // これらの一連の処理を繰り返す
}

このbufferedTransferは内部的にキューを使用しているものとすると、この場合も実行コンテキスト間のやりとりにはキューが使用されています。

また、コルーチンを使用しても実装できます

CoHandle receive() {
  while (true) {
    std::span<uint8_t> buf = co_await srv.coReceiveWireChunk(); // blocks
    co_await opusQueuePush.wait_push(buf);
  }
}

CoHandle decode() {
  while (true) {
    std::span<uint8_t> inBuf;
    co_await opusQueuePull.wait_pop(inBuf);

    int samples = opus_decode(decoder,
                              inBuf.data(), inBuf.size(),
                              decodeBuf.data(), maxFrameSamples,
                              0);
    std::span outBuf(decodeBuf.data(), samples);

    co_await pcmQueuePush.wait_push(outBuf);
  }
}

CoHandle play() {
  while (true) {
    std::span<uint8_t> inBuf;
    co_await pcmQueuePull.wait_pop(inBuf);

    uint32_t const *start = inBuf.data();
    size_t offset = 0;
    
    while (offset < size) {
      size_t bytesDone;

      co_await co_i2s_channel_write(tx,
                                    start + offset,
                                    size - offset,
                                    &bytesDone,
                                    noTimeout);

      offset += bytesDone;
    }
  }
}

// 例えばこのように実行
void network_speaker() {
  while (true) {
    co_await receive();
    co_await decode();
    co_await pray();
  }
}

他にもファイバーによる実装も考えることができ、それはスレッド版とほぼ同じようなコードになります。

現在のC++はこれらの実装方法のうち最初のスレッド版だけをサポートしています。ただし、スレッド間で共有可能な並行キューはありません。とはいえ、コルーチンはすでに利用可能であり、P2300やファイバーも程なく利用可能になる予定なので、残りのコードも近いうちにサポートされます。すると、そこに欠けているのは実行コンテキスト間同期に使用している並行キューのようなものです。

この文書は、このキューのような実行コンテキスト間で通信(同期)を取るための標準的なメカニズムに必要な要件や設計について検討し、それに伴って浮かんだいくつかの疑問についてSG1に問うものです。まだ何かを提案しているわけではありません。

実行コンテキストに特化した同期メカニズムを提供するのは簡単ですが、全ての実行コンテキストでうまく動作するものを提供するのは困難です。例えば、上記例のキューはスレッド版とコルーチン版で異なるメンバ関数を提供しなければならないほか、動機を取る方法についても実行コンテキストによって最適なものが変わるでしょう。

それぞれの実装に特化したものを用意しても、複数の実行コンテキストを組み合わせて使用する場合にうまくいかなくなります。例えば、コルーチンによる実行においてその処理の一部(デコード)を別のスレッドで行おうとすると、コルーチンとスレッドという異なる実行方法にまたがって動作する同期方法(例ではキュー)が必要になります。

P0073R2ではそのような同期のためのキューではなくeventというクラス型を提案しています。

class event {
  // イベント通知
  void signal();
}

// イベント通知を待機
void block(event until);

このような抽象化は有用であると思われますが、異なる実行コンテキスト間で動作しようとするとき、signal()はイベントを受信する(送信先の)実行コンテキストを、block()は現在の実行コンテキストを知っていなければならないようです。そのために、eventクラスはそのオブジェクトでブロック中の全てのタスクのリストを保持する必要があります。実行コンテキストが静的にわかっていれば問題はないですが、動的にしかわからない場合が想定されるようで、その場合には実行時に現在の実行コンテキストを知る手段が必要となります

並行キューについては、キュー本体とキューのインターフェースを分離することで実装を効率化できる可能性が見出されました。その場合、キューは実行コンテキストによってテンプレート化され、実行コンテキストはそのブロッキングと通知のためのAPI(上記eventクラスのインターフェースと同様のもの)を提供する必要があります。

すなわち、eventクラスのようなAPIの設計はそれそのものやキューに限らず、同様の目的の同期メカニズムに対して一般化することができるはずです。すると、それらのものは実行コンテキストを知っている必要がありますが、これは必ずしも静的に検出可能ではない場合があります。

また、そのようなメカニズムではsignal()に相当するイベント通知処理がブロック中のタスクとその実行コンテキストを覚えておく必要があります。そのリストを保持するために追加のメモリ確保は許容されるのか、あるいは回避されるべきでしょうか。また、そのようなリストはおそらく型消去されることになります。すると型消去そのもののために動的確保が必要となる可能性があります。

この文書では、このようなまず浮かび上がった疑問をSG1に投げかけています。

  1. SG1はこのような汎用同期APIにより力を入れるべきか?
  2. P0073R2のeventは使用可能な抽象であるか?
  3. あるスレッドで実行中の処理内から、それを包含する最上位の実行コンテキスト(スレッドに対するスレッドプールなど)を検出するような実装を要求できるか?
  4. 実行コンテキストの完全なチェーンは必要か?
  5. 標準ライブラリにあるブロックする可能性がある関数のリストは必要か?
  6. 実行エージェントのペアのためのカスタマイズを許可するか?

この文書はこれらの解答を受けて、さらなる検討や作業を進めるつもりのようです。

P2883R0 offsetof Should Be A Keyword In C++26

offsetofキーワード化し、offsetofマクロの機能を言語機能とする提案。

この提案には、上の方のP2654R0の項も関連しています。

offsetofはマクロなのでモジュールからエクスポートすることができず、stdモジュールからもエクスポートされません。そのため、使用する場合は引き続き#include <cstddef>が必要となりますが、このヘッダはインポート可能であると指定されてはいないため、ヘッダユニットとしてインポートすることもできません。

この提案は、主にこの問題の解消のために、offsetofをキーワードとしてその機能を言語機能に昇格することでヘッダのインクルードによらず使用可能にしようとするものです。

これによって、offsetofの他の問題点の解決を図ることができます

  • マクロ引数に,が含まれていると展開がバグる
  • offsetofの第一引数に標準レイアウトクラス型以外を指定した場合、もしくは第二引数にデータメンバ以外を指定した場合のUB
  • 結果をポインタ演算に使用するとUBとなる

この提案によって解消される問題の例

import std.compat;  // offsetofはエクスポートされない
import <cstddef>;   // ポータブルではない
#include <cstddef>  // ok、これが最善

template <typename A, typename B>
struct Test {
  int data;
};

using TestInts = Test<int, int>;
static_assert(offsetof( TestInts, data) == 0); // ok

static_assert(offsetof( Test<int, int> , data) == 0); // error、型名にカンマが含まれる
static_assert(offsetof((Test<int, int>), data) == 0); // error、()で括ることをサポートしていない

class S1 {
  int data;
public:
  S2(int n) : data{n} {}
};

struct S2 {
  int data;
  void f();
};

static_assert(offsetof(S1, data) == 0); // UB
static_assert(offsetof(S2, f) == 0);    // UB

struct T {
  int i;
  double j;
  short k;
  void *p;
};

int main() {
  using namespace std;

  T x = {};
  size_t y = offsetof(T, k);
  short *p = (short*)((byte*)&x + y); // このx + yのポインタ演算はUB

  *p =123;
  printf("%d", x.k);
}

この提案では、キーワード化されたoffsetof演算子となり、上記のUBをエラーにしたり結果をポインタ演算に使用できるようにするなどの意味論が整備されます。

P2884R0 assert Should Be A Keyword In C++26

assertキーワード化し、assertマクロの機能を言語機能とする提案。

この提案には、1つ前のP2883及び上の方のP2654R0の項も関連しています。

assertはマクロなのでモジュールからエクスポートすることができず、stdモジュールからもエクスポートされません。そのため、使用する場合は引き続き#include <cassert>が必要となりますが、このヘッダはインポート可能であると指定されてはいないため、ヘッダユニットとしてインポートすることもできません。

この提案は、主にこの問題の解消のために、assertをキーワードとしてその機能を言語機能に昇格することでヘッダのインクルードによらず使用可能にしようとするものです。

1つ上のoffsetofと同様にキーワード化されたassert演算子となり、それによって式に,が含まれている場合に展開がバグる問題が解消されます。

この提案によって解消される問題の例

import std.compat;  // assertはエクスポートされない
import <cstddef>;   // ポータブルではない
#include <cassert>  // ok、これが最善

int main() {
  assert( std::is_same_v<int, int> );         // ng、マクロ引数が多い(カンマが含まれている)
  assert((std::is_same_v<int, int>));         // ok
  assert( std::vector{1, 2, 3}.size() == 3 ); // ng、マクロ引数が多い(カンマが含まれている)
  assert((std::vector{1, 2, 3}.size() == 3)); // ok

  int x = 0;
  int y = 0;

  assert( [x, y]{ return test(x, y);}() ); // ng、マクロ引数が多い(カンマが含まれている)
  assert(([x, y]{ return test(x, y);}())); // ok
}

このassertとよく似た機能は、関数中の不変条件の表現とチェックのための構文としてC++26予定の契約プログラミング機能においても議論されています。ただし、契約プログラミングにおいては契約の構文をどうするかまだ決定されていないため、その検討の一環としてもassertキーワード化をここで議論してSG21(契約プログラミング作業グループ)にフィードバックすることもこの提案の目的の一つです。

また、assert演算子化した場合には、その有効無効の切り替え(NDEBUGマクロによるなど)の方法の可否や、ビルドモードのよる構文評価の有無などの問題と向き合う必要がありますが、それらの細かい議論はSG21で提案されている実行時アサーションの機能の議論と合流させる(ここでは取り扱わない)ことを推奨しています。

P2886R0 Concurrency TS2 Editor's report

Concurrency TS v2の最新のドラフト(N4953)の変更点をまとめた文書。

次の提案と編集上の修正が適用されているようです。

P2887R0 SG14: Low Latency/Games/Embedded/Finance/Simulation virtual meeting minutes to 2023/05/11

2023年5月11日に行われたSG13の議事録。

P2888R0 SG19: Machine Learning Virtual Meeting Minutes to 2023/05/12

2023年3月9日と4月13日に行われたSG19の議事録。

機械学習に関連のある提案のレビューが行われているようです。

P2889R0 Distributed Arrays

複数の翻訳単位で分散している配列を1つの配列として扱う機能の提案。

C++のリンカには、複数の翻訳単位からシンボルを取り出してそれを1つの配列にまとめる機能があります。それは例えば、ある同じ名前の外部リンケージを持つ配列に対して各翻訳単位でそれぞれ初期化した後、リンカが最終的なプログラムを出力する際にそれぞれの翻訳単位(オブジェクトファイル)で初期化されている配列要素を何かしらの方法でマージします。

この機能はC++単体テストフレームワークにおいて活用されており、各翻訳単位で定義されている単体テストを集めて管理するグローバルなシングルトンの実装に使用されます。このようなグローバルな分散配列の初期化は、コンパイル時(リンク時)にはその初期化子は判明していますが、言語サポートなどはないのでその初期化は実行時に行われます。

registerというキーワードでこの分散配列を指定することにすると、例えば次のコードのような雰囲気のことが行われています

/// test_framework.h
using test_func = bool (*)();

// 分散配列g_testsの宣言
extern const test_func g_tests[register];
/// always_pass.cpp
#include "test_framework.h"

bool always_pass() {return true;}

// 分散配列に要素を追加
const test_func g_tests[register] = {always_pass};
/// always_fail.cpp
#include "test_framework.h"

bool fail1() {return false;}
bool fail2() {return false;}

// 分散配列に要素を追加
const test_func g_tests[register] = {fail1, fail2};
/// main.cpp
#include "test_framework.h"

int main() {
  // 分散配列g_testsは次の要素を含む : {always_pass, fail1, fail2}
  // ただし、その順序は不定
  for (test_func f : g_tests) {
    if(!f()) return 1;
  }

  return 0;
}

この機能は他のところでは、C++の実装(コンパイラ)が静的初期化子やスレッドローカルストレージ、例外処理テーブルなどを実装する際に使用されるほか、動的リフレクションの実装にも使用されているようです。

単体テストフレームワークにおける利用例はこの機能が有効に利用されている大きな例であり、その他の利用例もこの機能の有用性を物語っています。また、この機能をポータブルかつ簡易に利用できるようにすることで、static initialization order fiascoとして知られる問題をプログラマの希望に沿った初期化を確実に行われるように解決することができます。例えば、初期化関数と優先順位のペアからなる分散配列を用意しておき、プログラムの任意のタイミングでそれを優先順位でソートして対応する初期化を実行する、などの方法が可能となります。さらに、この方法は動的ライブラリの初期化にも活用できます。

既存の幅広い利用例や想定される利用法など、分散配列を言語サポートしてポータブルかつ簡易に利用可能にすることにはかなりの価値があり、この提案はそれを提案するものです。

この提案ではまだ具体的な構文が固まっていないようですが__distributed_arrayに分散配列の名前を渡して配列宣言に指定することで、分散配列であることを明示し、その初期化子は1つの配列にリンク時に統合されます。

/// テストフレームワークのヘッダ
using test_case_callback = void();

// テストケースを登録する分散配列
extern test_case_callback* const test_cases[];
/// テストフレームワークを使用するユーザーコード

// 分散配列への単一要素追加
__distributed_array(test_cases)
test_case_callback* const my_test_case = my_test_case_function_1;

// 分散配列への複数要素追加
__distributed_array(test_cases)
test_case_callback* const my_test_case_array[] = {
  my_test_case_function_2,
  my_test_case_function_3,
};
/// テストフレームワークのソース
void run_unit_tests() {
  test_case_callback* const* const test_data = ::test_cases;

  // 分散配列の要素数取得
  size_t const test_count = std::distributed_array_size(test_data);

  // 分散配列をspanで参照
  std::span const tests = std::span(test_data, test_count);

  // 分散配列のイテレーション
  for (test_case_callback* const test : tests) {
    test();
  }
}

分散配列そのものの宣言はexternによる要素数不明の配列であり、これは新しい構文やキーワードを追加する負担を避けたものです。別の提案(P2268)で提案されていたように(最初の例のように)registerキーワードを使用するなど、別の構文を妨げるものではありません。

分散配列の型に関しては、普通の配列型と区別しておくとコンパイル時の診断が行いやすくなったり、既存の範囲のためのユーティリティを直接分散配列で利用できるようになるなどのメリットがあるため、分散配列専用の配列型を追加することを提案しています。具体的には決まっていませんが、registerキーワードを用いる場合はT[register]T(&)[register]のようなものが考えられます。

静的な変数(配列)の定義に対して__distributed_array(A)のように指定すると、その配列は分散配列Aの部分定義として機能します。最終的なプログラムでは、それら部分定義は全てマージされてAの定義となります。分散配列がextern cv T A[];のように定義されている時、その部分定義はcv T型の変数もしくはcv T型の要素を持つ配列宣言であり、かつ名前空間スコープもしくは静的メンバ変数である必要があります。

分散配列のサイズを取得するためには、ここではstd::distributed_array_size()というライブラリ関数を利用していますが、sizeofや専用言語機能による取得方法も考慮されており、まだ決定していません。

分散配列の要素の順序はほぼ不定ですが、1つの翻訳単位で定義された(非inline)部分定義はその定義の順番で順序づけされます。

有効な利用例を見出せないため現在のところはthread_localな分散配列は提案されていません。

動的ライブラリにおいて実行時にそれらに含まれる分散配列を統合することはおそらくほぼ不可能であり、動的リンクはC++標準の範囲外のことであるため提案されていません(Windowsでは問題とならないとのことです)。ここではあくまで、静的なリンク時点で定義される分散配列を提案しており、静的ライブラリでは同じ分散配列定義は統合されますが、動的ライブラリにおいてはそれらシンボルは競合するとみなすことにすることを提案しています。言い換えると、動的リンカは分散配列を扱う必要はありません。これは既存のリンカの動作と一致しているようです。

この提案は、EWGIの初期レビューで好意的に受け止められており、さらなる作業が続行される予定です。

P2891R0 SG16: Unicode meeting summaries 2023-01-11 through 2023-05-10

SG16(ユニコード関連の作業部会)の2023年1月11日から5月10日の間のミーティングの議事録

9回分のミーティングの参加者や議題、発言、投票行動などが記録されています。

P2892R0 std::simd Types Should be Regular

提案中のstd::simdregularであるようにする提案。

C++26に向けて提案中のstd::simd型はoperator==が要素ごとの比較を行った結果をboolではなく要素ごとの比較結果を1/0で保存したsimd_mask型として返します。そのため、regularコンセプトを満たしません。

これは例えば、std::simd型をメンバとして保持するクラスにoperator==をデフォルト実装しようとした時に問題となります。

using uint32_4v = std::fixed_size_simd<std::uint32_t, 4>;

class Color {
public:
  bool operator==(const Color &) const = default;
private:
  uint32_4v data_;
};

void f() {
  Color a, b;
  
  ...
  
  if ( a == b ) // ERROR: operator==は削除されている
}

std::simdは値セマンティクスを持つ値型として設計されており、その演算をSIMD演算にエンコードする以外の部分では組み込み型と同様に動作することを意図しています。regularコンセプトはそのような値型が満たすべき基本的な性質です。また、regularな型には次のような関係があることが期待されます

  1. T a = b; assert(a == b);
  2. T a; a = b;T a = b;は同値
  3. T a = c; T b = c; a = d; assert(b == c);
  4. T a = c; T b = c; zap(a); assert(b == c && a != b);
    • zapはつねに引数の値を変更する

これらの性質は例えばstd::findのように内部で比較を行うような処理が暗黙的に要求していることでもあり、標準ライブラリの中でも一般のライブラリにおいても広く活用されています。

また、在野のライブラリでは多くのSIMD型が提供されており、そこではSIMDレジスタをラップするものから、より大きな数学的なベクトル・行列を表現するもの(DSL)まであります。そこでもoperator==による比較が提供されており、多くの場合は要素ごとの比較を行いその結果を(非boolで)返します。

そのようなライブラリのうち、DSLを目的とするライブラリはstd::simdとは目的が異なっているため、そこからstd::simdへ移行することはまずないと思われるためstd::simdの比較がどう選択されてもそのユーザーにはあまり関係ないでしょう。それ以外のライブラリのユーザーやDSLの実装者はstd::simdへ移行することにはかなりの利点があり、その場合のstd::simdの比較は他のC++の場合と同じ意味論が自然に要求されるでしょう。

C++の標準ライブラリにはstd::simdのようなデータ並列型は存在していませんが、値のシーケンスを表す型はいくつかあり(bitset, vector, array, valarrayなど)、std::valarrayを除いて全ての型がboolを返すoperator==を持っています。

この提案はこれらの背景から、std::simdoperator==をマスクではなくboolを結果として返すように変更し、結果をマスクで取得するのは別のフリー関数によって行うようにすることを提案するものです。

P2893R0 Variadic Friends

friend宣言でのパック展開を許可する提案。

クラステンプレート定義内でテンプレートパラメータに対してfriend指定を行う際、そこにパラメータパックを指定することはできません。

template <typename T>
class Foo1 {
  friend T; // ok
public:
  // ...
};

template <typename... Ts>
class Foo2 {
  friend Ts...; // ng
public:
  // ...
}

この提案はfriend宣言でのパック展開を許可し、パラメータパックに含まれるそれぞれのクラス型に対してfriendを適用するようにするものです。

この提案のモチベーションの一つとして、Passkeyイディオムというテクニックのサポートが挙げられています。

あるクラスCの定義内で他のクラスDfrined指定すると、DCの全てのプライベートメンバへアクセスすることができます。この時、アクセスしたいのがある1つのプライベートメンバだけだったとしても、その公開範囲を制限する方法はありません。

class D;

class C {
  friend D; // DはCの全てのメンバにアクセスできる

  ...
};

Passkeyイディオムは、この時に別のクラスPasskeyを間に挟むことでプライベートメンバの公開範囲を制限するものです。

class D;

class Passkey {
  friend D;
  Passkey() = default;  // PasskeyはDからしか生成できない
};

class C {
  ...

public:
  // Passkeyを生成できないとこの関数を呼べない
  auto do_something(Passkey) {
    // 必要なメンバにだけアクセスする
    ...
  } 
};

プライベートアクセスは関数経由にはなりますが、これによって必要なプライベートメンバだけにプライベートアクセス範囲を絞ることができます。

PasskeyイディオムにおけるPasskeyクラスはテンプレートにすることで使いまわすことができ、そうするとPasskeyを使用する場所でアクセス可能なクラス名が明示されるようになります。

class D;

template<typename T>
class Passkey {
  friend T;
  Passkey() = default;  // PasskeyはTからしか生成できない
};

class C {
  ...

public:
  // Dからのみ呼べる
  auto do_something(Passkey<D>) {
    // 必要なメンバにだけアクセスする
    ...
  } 
};

この場合でもdo_something()にはDからしかアクセスできません。

さらに、複数のクラスに対してアクセスを許可したい場合のためにPasskeyクラスのテンプレートパラメータを可変にすることが考えられます。

class D;
class E;
class F;

template<typename... Ts>
class Passkey {
  friend Ts...; // 現在これができない
  Passkey() = default;  // PasskeyはTs...からしか生成できない
};

class C {
  ...

public:
  // D, E, Fからのみ呼べる
  auto do_something(Passkey<D, E, F>) {
    // 必要なメンバにだけアクセスする
    ...
  } 
};

ただし、現在はfriend宣言におけるパック展開が不可能であるため、これはできません。

提案では他にも、CRTPにおいて基底クラスから派生クラスのプライベートメンバアクセスを許可する場合に派生クラスで可変長テンプレートを使用する場合の例を挙げています。

P2895R0 noncopyable and nonmoveable utility classes

※この部分は@Reputelessさんに執筆していただきました

派生クラスをコピー不可 / ムーブ不可にするユーティリティクラス std::noncopyable, std::nonmovable<utility> に追加する提案。

リソースを複製しない RAII クラスの実装のために、クラスをムーブオンリーに、あるいはムーブもコピーも禁止にしたいことがあります。Boost ライブラリが提供する noncopyable のようなクラスを継承すると、そうした実装を簡単に記述でき、意図も明確になります。この方法はコード検索でも多数ヒットするため、一般的なイディオムであると考えられます。

ただし、boost::noncopyable は、ムーブセマンティクスが導入される C++11 以前に設計されたもので、現在の C++ では、その派生クラスはコピー不可かつムーブ不可になります。これは標準に導入されたコンセプト std::copyable, std::movable と並べたときに違和感があります。C++ のクラスは std::copyable を満たさない場合でも、std::movable を満たすことはできるからです。

Boost の noncopyable の実装概略:

class noncopyable {
protected:
    noncopyable() = default;
    ~noncopyable() = default;
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
};

struct ObjectNonCopyable : noncopyable {};

int main()
{
    std::cout << std::boolalpha;
    std::cout << std::copyable<ObjectNonCopyable> << '\n'; // false
    std::cout << std::movable<ObjectNonCopyable> << '\n'; // false
}

この提案では、コピーを禁止させるためのクラス std::noncopyable と、ムーブおよびコピーを禁止させるためのクラス std::nonmovable をそれぞれ標準ライブラリで提供し、std::noncopyable ではムーブを禁止しないことで、標準のコンセプト std::copyable, std::movable と一貫させることを目指します。

提案の実装概略は次の通りです。

struct noncopyable {
    noncopyable() = default;
    noncopyable(noncopyable&&) = default;
    noncopyable& operator=(noncopyable&&) = default;
};

struct nonmovable {
    nonmovable() = default;
    nonmovable(nonmovable const&) = delete;
    nonmovable& operator=(nonmovable const&) = delete;
};

struct ObjectNonCopyable : noncopyable {};
struct ObjectNonMovable : nonmovable {};

int main()
{
    std::cout << std::boolalpha;    
    std::cout << std::copyable<ObjectNonCopyable> << '\n'; // false
    std::cout << std::movable<ObjectNonCopyable> << '\n'; // true

    std::cout << std::copyable<ObjectNonMovable> << '\n'; // false
    std::cout << std::movable<ObjectNonMovable> << '\n'; // false
}

いずれも Empty base optimization がはたらくため、派生クラスに余分なオーバーヘッドは生じません。

P2897R0 aligned_accessor: An mdspan accessor expressing pointer overalignment

mdspanのアクセサポリシークラスに、参照する領域ポインタにstd::assume_alignedを適用してアクセスするaligned_accessorの提案。

mdspanのアクセサポリシーとは、領域へのポインタとインデックスを受け取ってどのようにその領域へアクセスするか(要素を引き当てるか)を指定するポリシークラスです。デフォルトのアクセサ(std::default_accessor<T>)は要素型Tのポインタptrとインデックスidxに対してptr[idx]のようにアクセスしてその結果を返します。

この提案は、この場合に領域ポインタptrstd::assume_alignedに通してからアクセスすることで、mdspanに対してアライメント要件を宣言しつつコンパイラの最適化を適用する能力を与えるものです。

提案しているのはaligned_accessor<T, N>というクラスで、次のようなものです

namespace std {

  // aligned_accessor
  template<class ElementType, size_t the_byte_alignment>
  struct aligned_accessor {
    // オフセット結果に対するアクセサ
    // 領域先頭がアライメントされていても、オフセット結果までそうであるとは限らない
    using offset_policy = default_accessor<ElementType>;
    // 要素型
    using element_type = ElementType;
    // アクセス結果の型
    using reference = ElementType&;
    // データハンドル(参照領域を指定するもの)の型、ほとんどの場合ポインタのこと
    using data_handle_type = ElementType*;

    // 要求(仮定)するアライメント
    static constexpr size_t byte_alignment = the_byte_alignment;

    constexpr aligned_accessor() noexcept = default;

    // 非const ElementTypeからconst ElementTypeへの変換と
    // より大きなアライメントから小さいアライメントへの変換を行うコンストラクタ
    template<class OtherElementType, size_t other_byte_alignment>
    constexpr aligned_accessor(aligned_accessor<OtherElementType, other_byte_alignment>) noexcept;

    constexpr operator default_accessor<element_type>() const {
      return {};
    }

    // 指定したインデックスで要素を引き当てる
    constexpr reference access(data_handle_type p, size_t i) const noexcept {
      // assume_alignedを通して要素アクセス
      return assume_aligned<byte_alignment>(p)[i];
    }

    // 指定したインデックスでオフセットしたデータハンドル(ポインタ)を得る
    constexpr typename offset_policy::data_handle_type
      offset(data_handle_type p, size_t i) const noexcept {
        // pの指す領域はbyte_alignmentでアラインされている(はず)だが
        // その要素p + iの領域はそうとは限らない
        return p + i;
      }

    // 少なくともbyte_alignmentでアラインされているかを取得する
    constexpr static bool is_sufficiently_aligned(data_handle_type p);
  };
}

アクセサクラスはmdspanに指定すると内部で勝手によしなにしてくれるので、通常これを直接扱う必要はないはずです(is_sufficiently_aligned()はアライメントチェックのために使うことがあるかもしれません)。

簡単な使用例

#include <mdspan>
#include <ranges>

// nx4行列でアライメント要求をとるmdspan
template<typename T, size_t byte_alignment>
using aligned_mdspan_Nx4 = std::mdspan<T, std::extents<size_t, std::dynamic_extent, 4>, std::layout_right, std::aligned_accessor<T, byte_alignment>>;

int main() {
  using namespace std::views;

  // float配列を16バイトアライメントにアラインする
  alignas(16) float array1[] = {...};
  alignas(16) float array2[] = {...};

  // 4x4行列として参照
  aligned_mdspan_Nx4<float, 16> mat44_1{array1, 4};
  aligned_mdspan_Nx4<const float, 16> mat44_2{array2, 4};

  // assume_alignedを通していることで、このような計算は最適化されやすくなる
  for (auto [y, x] : cartesian_product(iota(0, 4), iota(0, 4))) {
    mat44_1[y, x] *= mat44_2[y, x];
  }
}

提案文書より、サンプルコード

// float要素の1次元動的mdspan
template<size_t byte_alignment>
using aligned_mdspan =
  std::mdspan<float, std::dextents<int, 1>, std::layout_right, std::aligned_accessor<float, byte_alignment>>;

// 32バイトアライメントを要求するインターフェース
// 例えば、floatで幅8のSIMDを使用するなど
extern void vectorized_axpy(aligned_mdspan<32> y, float alpha, aligned_mdspan<32> x);
extern float vectorized_norm(aligned_mdspan<32> y);

// 16バイトアライメントを要求するインターフェース
// 例えば、floatで幅4のSIMDを使用するなど
extern void fill_x(aligned_mdspan<16> x);
extern void fill_y(aligned_mdspan<16> y);

// Helper functions for making overaligned array allocations.

template<class ElementType>
struct delete_raw {
  void operator()(ElementType* p) const {
    std::free(p);
  }
};

template<class ElementType>
using allocation = std::unique_ptr<ElementType[], delete_raw<ElementType>>;

template<class ElementType, std::size_t byte_alignment>
allocation<ElementType> allocate_raw(const std::size_t num_elements) {
  const std::size_t num_bytes = num_elements * sizeof(ElementType);
  void* ptr = std::aligned_alloc(byte_alignment, num_bytes);
  return {ptr, delete_raw<ElementType>{}};
}

float user_function(size_t num_elements, float alpha) {
  // 32バイトアライメントでメモリを確保
  constexpr size_t max_byte_alignment = 32;
  auto x_alloc = allocate_raw<float, max_byte_alignment>(num_elements);
  auto y_alloc = allocate_raw<float, max_byte_alignment>(num_elements);

  // 32バイトアライメントで領域を参照
  aligned_mdspan<max_byte_alignment> x(x_alloc.get(), num_elements);
  aligned_mdspan<max_byte_alignment> y(y_alloc.get(), num_elements);

  fill_x(x); // 32バイトアライメントから16バイトアライメントへの変換
  fill_y(y); // 32バイトアライメントから16バイトアライメントへの変換

  vectorized_axpy(y, alpha, x);
  return vectorized_norm(y);
}

P2898R0 Importable Headers are Not Universally Implementable

モジュールにおけるインポート可能なヘッダ(importable header)というものを修正する提案。

インポート可能なヘッダ(importable header)とは、非モジュールのヘッダファイルのうち、ヘッダユニットとしてインポートすることができる種類のヘッダのことです。インポート可能なヘッダの#includeは実装によってimportに置換される可能性があります。

ただし、標準ライブラリのC互換ではないヘッダを除いて、C++標準はインポート可能なヘッダが何かを規定しておらず、それは実装定義とされています。

ヘッダユニットはモジュールの一種ではありますが、モジュール宣言によって作成される名前付きモジュールとは異なりあくまでヘッダファイルです。そのため、インポート可能なヘッダと名前付きモジュールには根本的な違いがあります

  1. 名前付きモジュールはその識別と探索のために以前には存在しなかったモジュール名という探索空間を提供するが、ヘッダユニット名はヘッダ名と同じ探索空間を共有する
  2. ヘッダユニットのインポートは、インポートされた先の翻訳単位にプリプロセッサの状態を漏洩する

この違いにより、いくつかの問題が生じています

ヘッダユニットの問題点

1. インポート可能なヘッダとインクルードされるヘッダの識別

現在のC++にはヘッダ名を正確に識別するための仕様やメカニズムがありません。そのため、インポート可能なヘッダの#includeimportに置換した場合にそれが同じヘッダファイルを指しているかすら保証できません。ともすれば異なるファイルを処理してしまったり、あるコンパイラでは意図通りになっていても別のコンパイラでは異なる結果が生じたりする可能性があります。

このことは、#pragma onceが有用であり実質ポータブルでありながら標準化されない(できない)理由にも通じます。#pragma onceでは、何が一度だけインクルードされるべきなのかを指定する方法がなかったため、標準化に至りませんでした。実際、コンパイラによって同じヘッダを区別する方法が異なっています。

2. 依存関係スキャンの依存関係

モジュールのimportプリプロセッサとして処理されるため、あるソースファイルの依存関係をスキャンする場合には少なくともそのファイルのプリプロセスを完了させる必要があります。

その時、ヘッダユニットのインポートはそのプリプロセス状態(マクロ)もエクスポートするため、ヘッダユニットのインポートはその内容によって依存関係スキャンの結果に影響を与える可能性があります。そのため、依存関係スキャン処理は次のいずれかの対応をしなければなりません

  • 依存関係スキャンへの入力としてヘッダユニットのビルド済モジュールインターフェースを受け入れる
  • 一貫した処理のため、ヘッダユニットからのコマンドライン引数を受け入れて現在のプリプロセッサの状態を更新し、その後にヘッダユニットのプリプロセッサ状態をマージする

clang/MSVCの初期の依存関係スキャン実装は、あたかもヘッダのインクルードを行なっているかのようにヘッダユニットのインポートを処理しているようです(つまり、どちらの方法でもない)。そのアプローチはモノリシックリポジトリのような環境ではうまく動きますが、ビルド済バイナリが依存関係として入ってくるようなより複雑なビルドではインポート可能なヘッダが上手く扱われることを保証できません。

そのような場合は、次のように依存関係スキャンプロセスへの入力を補うことで解決が図れます

  • 既知のインポート可能なヘッダの全てのリストを依存関係スキャン処理への入力とする
  • インポート可能なヘッダのローカルプリプロセッサの引数を依存関係スキャン処理への入力とする
  • 現在の翻訳単位のローカルプリプロセッサの引数を依存関係スキャン処理への入力とする

しかし、この代償としてある翻訳単位の依存関係に大きなボトルネックが生じます。つまり、これらの入力(インポート可能なヘッダのリストやプリプロセッサ引数)のどれかを少しでも変更すると、そこ以降の依存関係スキャン結果は全て無効になり、影響を受ける翻訳単位は全て依存関係スキャンのやり直しとその結果を受けてのリビルドが必要となります。これはC++プロジェクトのビルドに大きな追加のコストを導入します。

3. プリプロセッサ状態についての推論

名前付きモジュールのimportはマクロをエクスポートしないため、インポート先の翻訳単位のプリプロセッサ状態に何ら影響を与えません。

一方でヘッダユニットはそうではなく、ヘッダユニットのインポートを使用している場合にそこからエクスポートされるマクロについて知るには、そのヘッダユニットがどのようにコンパイルされるか(コンパイルオプション)を調べて、その状態がどのようにインポート先のプリプロセッサ状態にマージされるかを調べるためにビルドシステム(コンパイラ/依存関係スキャナ)の仕様を知りに行く必要があります。

この時、#includeimportに置換される場合、プログラマはどのヘッダがその対象なのかを知らなければプリプロセッサの状態を推測することができません。コンパイラによってその方法・基準が異なるという状況では、それはより困難となります。

このことは特にC++の教育時に問題となります。このような質問に対してどう答えるかは使用するビルドシステムにも依存することを考えると、この問題はより深刻です。

ヘッダユニットの目的

ヘッダユニットという仕様の目標は次のようなものでした

  • 事前コンパイル済ヘッダの経験を教訓として、共通の一貫した仕様を策定する
  • Clang Header Moduleの経験から、一貫した仕様を策定する
  • モジュールへの移行を容易にする

これらの目標そのものには価値があり重要な目標ではありますが、現在のヘッダユニットの仕様ではこれらのことを達成できていません。

事前コンパイル済ヘッダにあった制限が受け継がれていない

事前コンパイル済ヘッダの実装はコンパイラによってまちまちでしたが、共通のサブセット要件が存在していました。それは、事前コンパイル済ヘッダの内容はそれを使用する翻訳単位内のコードの影響を受けないというものです。

すなわち、あるヘッダファイルの#includeは事前コンパイル済ヘッダが利用可能であるかどうかでその動作が変わることがありません。

Clang Header Moduleの制限

Clang Header Moduleはそのヘッダがそれを使用する場所のプリプロセッサ状態の影響を受けないことを前提として実装されてており、それに違反するとエラーとなりこれはユーザーの責任とされていました。言い換えるとこれは、C++言語のサブセットを採用するコードベースに対して適用されます。

C++モジュール仕様におけるヘッダユニットにおいてはそのように適用可能な場所に言語サブセットを要求することは当然回避されるため、Clang Header Moduleのこの側面の経験をC++エコシステム全体にどのように適用するかという問題には対処していません。

ヘッダユニットが移行の役に立たない

事前コンパイル済ヘッダとClang Header Moduleの両方を経験したコードベースにおいては、どちらの場合の解釈もあくまでヘッダのインクルードであり、ツールが行うことはインクルードの最適化だと理解されています。

そのため、それら及び#includeimportに切り替えられた時の解釈は、インポート時点でのプリプロセッサの状態に影響されることなくヘッダをインクルードするセマンティクスに依存することになります。

しかし、よく使用するヘッダ(ソース)ファイルで新しい構文を使用するとそのファイルの使用可能性は、その構文をサポートするコンパイラとビルドシステムのみに制限されます。この時点で、インポート可能なヘッダに移行するか名前付きモジュールに移行するかの労力の差はそれほど変わりません。

実際、モジュールセマンティクスを必要とするコードベースにとっては与えられたヘッダのラッパモジュールを自動的に生成して、その名前付きモジュールでヘッダのエンティティをエクスポートすることはそれほど難しくないでしょう。それだけだとマクロをサポートできませんが、マクロは専用のヘッダにまとめて配置しそれをインクルードするようにすることは難しくなく、これらを組み合わせることは名前付きモジュールのための良い移行経路となるはずです。

ヘッダユニットのパフォーマンス上の利点は、ボトムアップに採用した場合に限られる

ヘッダユニットの初期の経験では、ヘッダユニットの採用が最も下位から(ネストした依存関係の内側から)開始されない場合にコンパイラのパフォーマンスが低下することがわかっています。

これは、同じヘッダのインクルードを含むようなより大きなヘッダ単位でインポートしてしまうと、それぞれのヘッダユニットのビルドにおいて同じヘッダを複数回処理することになってしまうためです。インクルードの場合、インクルードガードの処理が効率化されたことでそのスキップが効果的となるようです。

またそのような大きなヘッダをインポートした側では、同じヘッダをインクルードしていることで重複するエンティティ情報の統合を行う必要も出てきます。

これらのことを受けてこの提案では、インポート可能なヘッダの意味論を別に指定せず、インポート可能なヘッダを実装がインクルード処理を最適化する方法として指定することで、現在起きている問題を解決し元々の目標を達成することを提案しています。

このアプローチの重要な点は、ヘッダファイルの取り込みに関してはインクルードのセマンティクスが依然として標準的かつ期待される動作であり、実装に対して、そうすることが有利な場合に異なることを行う許可を与えて実装がその最適化に関する独自の注意事項を定義できるようにすることにあります。

この提案は、SG15の議論において方向性に同意が得られれば、このための文言を提供する改訂版を提出する予定です。

P2901R0 Extending linear algebra support to batched operations

P1673で提案中のBLASベース線形代数ライブラリに、バッチ操作のサポートを追加する提案。

BLASAPIは基本的に1関数につき1つの計算を行います。例えば、倍精度実数行列の行列積を計算するcblas_dgemm()という関数は、入力として3つの行列A, B, Cとその係数alpha, betaを受け取って、C = alpha * AB + beta * Cのような計算を行うものですが、これは3つの行列を使った1つの行列積を計算するものです。行列のサイズ(行数/列数)はA, B, Cで一貫している必要はありますが任意であり、問題を1つの行列に落とし込めればこれだけでも並列計算が可能ではあります。

対して、BLASにおけるバッチ操作(batched operation)では1つの関数呼び出しで複数の行列を使った複数の計算を行うものです。1つの行列に問題を落とし込むことができず独立した複数の行列で独立に同じ計算を行う必要がある場合、バッチAPIを用いると個別の関数呼び出しを(並列に)繰り返すよりも効率的に行列計算を行うことができます。

例えば、cblas_dgemm()に対応するバッチ操作はcblas_dgemm_batch()という関数で、行う計算自体は同じですが、パラメータは全て配列で受け取り複数の行列積を1つの関数呼び出しで並列実行します。実際どのように実行されるかは実装によるのですが、個別の関数を独立して呼び出すよりも効率的に実行されることが期待でき、実際そのように実装されます。

現在P1673で提案され作業中のBLASベース線形代数ライブラリはBLASの基本部分APIに対応するもので、バッチ操作は含まれていません。この提案はそこにバッチ操作も入れようとするものです。

バッチ操作の利点は次のようなものです

  • 同じ処理による大量の問題を1度に解くというユーザーの意図を明らかにすることで、単一の小さな問題を解く処理がもつものよりもはるかに大きな並列化・ベクトル化の機会を得られる
  • 個別の問題をBLAS APIに引き渡すために関数呼び出しの引数として表現することによるオーバーヘッドを償却できる
    • 関数呼び出しごとの引数チェックの償却など
  • バッチ引数に関する制約や仮定をインターフェースによって表現し、それに応じた計算の効率化を行える
    • メモリアクセスパターンの改善
    • 共通のデータの読み込みの再利用
      • 全て同じ係数を使用するcblas_dgemm_batch()における係数alpha, betaの再利用(ブロードキャスト)など
    • 潜在的に共通する計算の再利用
  • バッチ操作はBLASが利用されるさまざまな分野で計算効率化の役に立つ
  • NVIDIA, AMD, intelなどのハードウェアベンダはバッチ操作を実装したBLAS実装とそれを高速に実行するハードウェアを提供している
  • MAGMAやKokkosなどのオープンソースライブラリはクロスプラットフォームでバッチ操作を提供している

この提案では、std::mdspanで入力を表現することによってcblas_dgemm()に対するcblas_dgemm_batch()のようにAPI名を分岐する必要はなく、関数に対する要求事項の変更のみによってP1673のAPIを拡張できるとしています。

また、バッチ操作の実装では基本的に使用する行列のサイズやストライドは行列ごとに可変とすることができるようですがそれを考慮すると効率化が妨げられるため、この提案では基本のケースである全ての行列のサイズとストライドは等しい場合のみをサポートすることにし、その効率化に焦点を当てています。

さらに、バッチ操作の入力行列(複数)の表現についても自由度があり(配列の配列、ストライドによる分離、インターリーブ)、この提案では効率性の観点からストライドとインターリーブのみをサポートすることにしています。それはまたstd::mdspanのカスタムレイアウトによって表現可能であり、これによってインターフェースを1つにまとめることができます。

おわり

この記事のMarkdownソース