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

文書の一覧

全部で173本あります。

今月分は@Reputelessさんに一部手伝っていただきました、ありがとうございました!

もくじ

N4974 Wroclaw meeting information

2024年11月にポーランドのWroclaw(ブロツワウ)で行われる全体会議のインフォメーション。

予定(2024年11月18日~23日)と場所、ホテルの案内などが記載されています。

N4975 2024 WG21 admin telecon meetings

2024年に行われる、WG21管理者ミーティングの予定表。

今年の残りは6/10と11/04に予定されています。

N4976 WG21 agenda: 18-23 March 2024, Tokyo, Japan

2024年3月に東京で行われた会議のアジェンダ

N4978 WG21 2024-03 Admin telecon minutes

2024年3月4日に行われた、WG21管理者ミーティングの議事録。

前回(kona会議の前)からどのような活動があったかや、東京会議で何をするかなどの報告がなされています。

N4979 Hagenberg Meeting Invitation and Information

2025年1月にオーストリアのHagenberg(ハーゲンベルグ)で行われる全体会議のインフォメーション。

予定(2025年1月10日~15日)と場所、生き方や現地の観光案内などが記載されています。

N4980 WG21 2024-03 Tokyo Minutes of Meeting

2024年3月に東京で行われた全体会議の議事録。

最終日に行われた全体会議での各グループの作業報告と、全体投票の様子が記録されています。

N4981 Working Draft, Programming Languages -- C++

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

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

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

P0260R8 C+ Concurrent Queues

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

以前の記事を参照

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

  • 文書全体の再構成
  • conqueue_errcsystem_errorから派生するようにした
  • rangeコンストラクタを追加
  • .capacity()を追加
  • concurrent_bounded_queue()を既存のプラクティスとして追加
  • pop()のインターフェースについての議論を別の提案に分離
  • P1958のbuffer_queueを統合
  • buffer_queueの名前をbounded_queueに変更
  • 提案する文言を追加
  • この提案をTS(technical specifications)として発行することについての質問を更新

などです。

P0562R1 Initialization List Symmetry

P0562R2 Trailing Commas in Base-clauses and Ctor-initializers

コンストラクタ初期化子リストと基底クラスリストにおいて、末尾カンマを許容する提案。

カンマはC++の様々なところでエンティティの区切り文字として使用され、一部のコンテキストでは冗長な末尾のカンマが許容されます。

enum class E {
  a,
  b,
  c,  // ok
};

int array[2] = { 
  0,
  1, // ok
};

std::vector<int> vec = {
  0,
  1, // ok
};

struct S {
  int a;
  int b;
};

S s = { 
  .a = 0,
  .b = 1,  // ok
};

このことは、単純なマクロの展開によってカンマ区切りリストを生成する際に便利であったり、単純なコード整形などによって見やすさや保守性の向上等のための助けとなります。

ただし、コンストラクタ初期化子リストでは末尾のカンマは許容されていません。

class C {
  int a, b;
public:

  C(int n1, int n2)
    : a{n1},
      b{n2},  // ng
  {}
};

コンストラクタ初期化子リストの順番はクラスのメンバの宣言順と一致している必要があり、一致しない場合に時に微妙なバグを静かに埋め込むことになります。宣言順に一致させるために行の入れ替えをした後でカンマを消すというのはとても簡単な作業ではありますが、時に忘れがちで、コンパイルエラーを起こしてはじめて気づくこともあります。コンパイル時間が長い場合はその損失は大きなものになります。

また、そもそもその些細な作業の地味な面倒さから、並べ替えの必要性に気づいた人が作業をやらない誘因を与えています。

そのため、この提案ではコンストラクタ初期化子リストにおける末尾の不要なカンマを許容することを提案しています。

また、同様のメンテナンス性の問題と一貫性の向上のために、基底クラスリストにおいて同様に末尾カンマを許容するようにすることも提案しています。

現在 この提案
foo::foo(int x, int y, int z) :
  a(x),
  b(y),
  c(z)
{...}

class bar :
  public base,
  public mixin
{};
foo::foo(int x, int y, int z) :
  a(x),
  b(y),
  c(z),
{...}

class bar :
  public base,
  public mixin,
{};

この提案は内容が小さいこともありEWGのレビューをすでに終えてCWGでレビューされています。

P0609R3 Attributes for Structured Bindings

構造化束縛の個々の名前に属性を指定できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する構文定義を調整した事です。

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

P0843R11 inplace_vector

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

以前の記事を参照

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

  • erase/erase_ifメンバ関数がフリースタンディングになった
  • operator=(inplace_vector&& other)にはis_nothrow_move_constructible_v<T>が要求される
  • .epmty()から[[nodiscard]]を削除

などです。

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

P0876R16 fiber_context - fibers without scheduler

スタックフルコルーチンのためのコンテキストスイッチを担うクラス、fiber_contextの提案。

以前の記事を参照

このリビジョンでの変更はかなり多いので省略します。LWGのレビューを受けての文言レベルの調整がメインで、設計の変更を伴うものはなさそうです。

P1061R8 Structured Bindings can introduce a Pack

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

以前の記事を参照

このリビジョンでの変更は、名前空間スコープの除外が再度導入されたことと、P0609R3を考慮して文言がリベースされたことなどです。

構造化束縛宣言によるパック導入は非テンプレート関数でも使用でき、その場合それが使用された関数をテンプレートの文脈にします。しかし、その場合でも構造化束縛宣言によるパック導入が現れて以降だけをテンプレート化するだけで済むのに対して、名前空間スコープでこれを許可するとその名前空間全体に影響が及びます。これに対する防止策やサポートを追加することは利点が不明瞭で時間がかかるため、この提案ではとりあえずそれを禁止することにしています。

P1068R11 Vector API for random number generation

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

以前の記事を参照

このリビジョンでの変更は、提案する文言についてLWGのフィードバックを適用した事です。

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

P1317R1 Remove return type deduction in std::apply

std::applyの戻り値型推論をやめる提案。

std::applyの宣言は次のようになっており、戻り値型はdecltype(auto)によって推論されています。

namespace std {
  template<class F, tuple-like Tuple>
  constexpr decltype(auto)
    apply(F&& f, Tuple&& t) noexcept(...);
}

これによって、applyの引数に呼び出し不可能なペアを渡すとapply内部においてエラーが起こることによって、SFINAEのような動作をしないという問題があります。

例えば次のようなコンセプトとその使用例において

#include <tuple>

template<class Func, class Tuple>
concept applicable =
  requires(Func&& func, Tuple&& args) {
    std::apply(std::forward<Func>(func), std::forward<Tuple>(args));
  };

int main () {
  auto func = [](){};
  auto args = std::make_tuple(1);

  static_assert(!applicable<decltype(func), decltype(args)>); // static_assert()によらずにエラーが起こる
}

このfuncは引数無しのラムダ式なのでstd::tuple<int>では呼び出しできません。applicableコンセプトの意図としては制約を満たさずにfalseとなることが期待されますが、実際にはapply本体内でのハードエラーとなることによってコンセプトの定義内でのエラーとなってしまいます。

これは、decltype(auto)によって戻り型が推論されているためで、これによって関数のシグネチャの決定のために本体のインスタンス化が必要となり、呼び出しできない引数が渡っている場合にそのインスタンス化の途中でハードエラーを起こしています。

この問題を回避するため、この提案はstd::applyの戻り値型を求めるためにdecltype(auto)ではなく別の型特性を使用することを提案しています。

この提案では、std::applyの戻り値型導出のためにstd::apply_result_tという型特性を使用することを提案しています。

namespace std {
  template<class Fn, class Tuple>
  struct apply_result;

  template<class F, class Tuple>
  using apply_result_t = apply_result<F, Tuple>::type;

  // 新しいapplyの宣言
  template<class F, class Tuple>
  constexpr std::apply_result_t<F, Tuple> apply(F&& f, Tuple&& t);
}

このstd::apply_result_tは呼び出し不可能な型のペアを受け取ると、巧妙な実装によってそれをハードエラーではなくSFINAE-friendlyなエラーとして報告するように実装されます(提案中に実装例があります)。

また特に触れられてはいないのですが、上記のapplicableのようなコンセプトを同時に追加しようともしています。

P2034R3 Partially Mutable Lambda Captures

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

以前の記事を参照

このリビジョンでの変更は、作者のメールアドレスの更新と、サンプルコード中のany_invocablemove_only_functionに変更した事です。

P2075R5 Philox as an extension of the C+ RNG engines

<random>にPhiloxというアルゴリズムベースの新しい疑似乱数生成エンジンを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGのフィードバックを受けて提案する文言を修正したことです。

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

P2127R0 Making C+ Software Allocator Aware

Allocator AwareなC++コードのための解説書。

この文書は、Bloomberg社内の教育のために書かれた、C++コードでアロケータを受け取るクラスを記述するために知っておくべきことをまとめたものです。

LEWGにおけるポリシーの策定に当たってアロケータ関連のポリシーが提案されており、LEWGのメンバにとっても有用である可能性があるとして公開されたものです。

P2135R1 P2055R1: A Relaxed Guide to memory_order_relaxed

memory_order_relaxedによるアトミックアクセス時に起こりえるOOTA/RFUB問題を回避する設計を解説する文書。

OOTA(out-of-thin-air)は複数スレッドからのmemory_order_relaxedなアトミックアクセス時に理論的に発生しうる、何もない所から値を読みだしているかのような振る舞いの事です。

// OOTAの例
atomic<int> x(0);
atomic<int> y(0);

void thread1() {
  unsigned long r1 = x.load(memory_order_relaxed);
  y.store(r1, memory_order_relaxed);
}

void thread2() {
  unsigned long r2 = y.load(memory_order_relaxed);
  x.store(42, memory_order_relaxed);
}

このサンプルコードはOOTAが起こる例であり、x, y, r1, r2の最終的な値は全て42になります。ただしこれはハードウェアとコンパイラの両面から禁止されており、規格的にも禁止されているため、実際には起こりません。

RFUB(read from untaken branch)も複数スレッドからのmemory_order_relaxedなアトミックアクセス時に理論的に発生しうる、実行されていない分岐から値を読みだすような振る舞いの事です。

// RFUBの例
atomic<int> x(0);
atomic<int> y(0);

void thread1() {
  unsigned long r1 = x.load(memory_order_relaxed);
  y.store(r1, memory_order_relaxed);
}

void thread2() {
  bool assigned_42 = false;
  unsigned long r2 = y.load(memory_order_relaxed);  // (3)
  if (r2 != 42) {
    assigned_42 = true;
    r2 = 42;
  } // (1)
  x.store(r2, memory_order_relaxed);  // (2)
}

このサンプルコードはRFUBが起こる例であり、x, y, r1, r2の最終的な値は全て42になります。コードを眺めるとthread2が先に実行されればそうなりそうですが、ここで起こることは次のようなことです

  1. (1)の行で、コンパイラr2が常に42であることを認識する
  2. それにより、(2)の行でコンパイラr2を42という即値に置き換える
  3. コンパイラとCPUの両方は、(2)と(3)の行の実行を入れ替える権利を持つ
  4. まず(2)が実行され、xに42を書き込む
  5. thread1()r1xからのロードは42を読み取る
  6. thread1()r1yへのストアは42を書き込む
  7. (3)のr2yからのロードは42を読み取る
  8. r2は42に等しいため、thread2()の分岐は実行されない
    • このような最適化の根拠がこの分岐内にあるにもかかわらず・・・

RFUBの場合は特に現状禁止されておらず、有識者の間でも許可すべきという意見があるようです。

この2つの種類の問題はmemory_order_relaxedなアトミックアクセス時にのみ起こります。C++の規格においてOOTA/RFUBの扱いについて何らかの合意のものとでしっかりと規定されるまでの間にmemory_order_relaxedの使用率が高まっており、memory_order_relaxedを使用する場合にこれらの問題を回避する安全な使用パターンを特定する必要性が高まっています。

この提案はそのようなパターン集の第一歩であり、人間によるコードレビューに役立てるほか、将来的には何らかの自動的なチェックに活用されることを意図しており、目的は既存のコードのバグを特定し、将来のコードのバグを防止することにあります。

なお、これは提案文書ではありません。

P2141R2 Aggregates are named tuples

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

以前の記事を参照

このリビジョンでの変更は、タプルプロトコルを拡張するのではなく、集成体型専用のタプルプロトコルにベースのAPIを使用するようにするLEWGの推奨に関する説明と文言を追加したことです。

現在のタプルプロトコルAPIと同名のものをstd::aggr名前空間に用意してそちらを集成体専用にすることで、既存のタプルプロトコルが集成体型に対して後方互換を損ねてしまうのを回避します

std::aggr::get<1>(aggregate) = 42;

static_assert(std::aggr::tuple_size_v<Aggr> == 3);

std::aggr::apply(func, aggregate);

この提案の内容はP1061によってカバーされているとのことで、こちらの提案は追求されないようです。

P2248R8 Enabling list-initialization for algorithms

値を指定するタイプの標準アルゴリズムにおいて、その際の型指定を省略できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けて提案する文言を修正した事です。

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

P2300R8 std::execution

P2300R9 std::execution

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

以前の記事を参照

R8~R9にかけての変更は多岐にわたりますが、ほとんどは規格の文言上の修正です。特筆すべき点は、P2855R1がマージされたことで、カスタマイゼーションポイントへのアダプトはメンバ関数によってのみ行われるようになり、tag_invokeは使われなくなったことです。

P2355R2 Postfix fold expressions

可変長テンプレートの畳み込み式において、() []の2つの演算子を使用可能にする提案。

以前の記事を参照

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

  • P2128(多次元添字演算子)を考慮してベースとなる規格ドラフトを更新
  • 関数畳み込みの追加の構文についての説明を追加

などです。

この提案はEWGIのレビューにおいて、これ以上議論しないことになりました。

P2389R1 dextents Index Type Parameter

std::dextentsから、整数型の指定を省略する提案。

以前の記事を参照

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

  • std::dimsの実装可能性についての議論と、実装リンクの追加
  • 提案する文言の追加
  • フリースタンディングへの影響について考察(影響なしと思われる)

などです。

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

P2414R3 Pointer lifetime-end zap proposed solutions

Pointer lifetime-end zapと呼ばれる問題の解決策の提案。

以前の記事を参照

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

  • 2024年3月の全体会議におけるフィードバックの反映
  • reachabilityからフェンスセマンティクスに変更し、provenance_fence()を追加
  • ワーキングドラフトの[basic.life]へのリンクを追加

などです。

この提案は現在、次の3つの事を提案しています

  1. 無効なポインタ値の表現バイト列を忠実に計算すること
    • ポインタ値そのものを扱う操作(比較ではなくデリファレンスでもないもの)において、ポインタが無効であったとしてもその表現バイト(ポインタの値そのもの)を用いて操作を行う
    • たとえばあるポインタに定数を足すという操作の場合、そのポインタが無効なものであっても、有効なポインタに同じ操作を行った場合と同じ結果となる
  2. provenance_fence()を含むusable_ptr<T>クラステンプレート
  3. アトミック操作にはポインタの無効化に寛容な副作用があること
    • 及び、volatileアクセスはポインタの無効性に寛容であり続けること

このリビジョンでは特に、以前に提案していたusable_ptr<T>はそのままにprovenance_fence()が追加されています。

provenance_fence()はこの関数の呼び出しの後でアクセスされる、以前にその表現バイトが公開(1度整数値として使用されたことのあるポインタなど)されたことのあるその時点で無効なポインタのうち、同時点で生存期間内にある互換性のある型のオブジェクトのアドレスに対応するものについて、その由来(provenance)の再計算を実装に指示するものです。

言い換えると、provenance_fence()はゾンビポインタ(一度無効化したポインタの指す位置にオブジェクトが再構築された後のポインタ)に対してのみ動作し、なおかつその位置に作成された新しいオブジェクトのポインタが公開されている場合にのみ動作するものです。これにより、ゾンビポインタをその時点で有効なオブジェクトへのポインタに自動で修正することができます(std::launder()とやっていることは実質的に同じです)。

P2542R8 views::concat

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

以前の記事を参照

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

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

P2573R2 = delete("should have a reason");

関数のdelete指定にメッセージを付加できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、文言の修正と機能テストマクロ名を変更(__cpp_deleted_function)した事です。

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

P2591R5 Concatenation of strings and string views

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

以前の記事を参照

このリビジョンでの変更はLWG Issue 3950を考慮して文言を変更したことなどです。

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

P2746R5 Deprecate and Replace Fenv Rounding Modes

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

以前の記事を参照

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

  • make()の引数をstd::string_viewを取るように変更
  • sqrt()シグネチャを修正

などです。

P2747R2 constexpr placement new

定数式において、placement newの使用を許可する提案。

以前の記事を参照

このリビジョンでの変更は、配列のサポートを追加したことです。

// 定数式中とする

int* p = std::allocator<int>{}.allocate(3);

std::construct_at(p, 1, 2, 3);  // ng
new (p) int[]{1, 2, 3};         // ok
new (p + 1) int[]{2, 3};        // ng (in this paper)
new (p) int[]{1, 2, 3, 4, 5};   // ng

この提案では、領域サイズに対して配列が過不足なくぴったりと収まる形で初期化される場合のみを許可しようとしています。

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

P2748R5 Disallow Binding a Returned Glvalue to a Temporary

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

以前の記事を参照

このリビジョンでの変更は、CWGでの議論の結果R2の文言に戻したことです。

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

P2755R1 A Bold Plan for a Complete Contracts Facility

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

以前の記事を参照

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

  • P2961R2の構文(natural syntax)をサポートするように更新
  • P2900R6をベースラインとして更新
  • セクション4.4において、契約を標準ライブラリに適用する方法を明確化

などです。

P2786R5 Trivial Relocatability For C++26

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

以前の記事を参照

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

  • EWGのレビューを反映
  • vexing parseに関する東京会議での議論を追加
  • 標準ライブラリがこの機能をどのように使用できるかについてFAQを追加
  • CWGレビューを受けての文言修正

などです。

P2795R5 Erroneous behaviour for uninitialized reads

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

以前の記事を参照

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

  • 未初期化変数を複数回読み取っても同じ値が読み取られることを明確化
  • サンプルコードの改善
  • erroneous value(未初期化変数を初期化する値)からbyte/unsigned charへのbit_castは誤った操作ではなくなり、不確定な値(indeterminate value)と同等になった
  • 機能テストマクロが無いことについての注記を追加

などです。

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

P2809R3 Trivial infinite loops are not Undefined Behavior

自明な無限ループを未定義動作ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、フリースタンディングにおける動作を考慮したCWGの推奨に従って文言を変更したことと、提案する文言からyield_forever()を取り除いた(不要になるように文言が調整されたため)ことなどです。

この提案は2024年3月の全体会議でDR(C++11以降)として採択されています。

P2810R4 is_debugger_present is_replaceable

P2546で提案されているis_debugger_present()をユーザーが置換可能にする提案。

以前の記事を参照

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

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

P2825R1 Overload Resolution hook: declcall(unevaluated-postfix-expression)

P2825R2 Overload Resolution hook: declcall(unevaluated-postfix-expression)

与えられた式が呼び出す関数の関数ポインタを取得する言語機能の提案。

以前の記事を参照

R1及びR2での変更はよくわかりませんが、タイトルが変更され、クエリ構文が__builtin_calltarget()からdeclcall()に変更され、それに伴って命名に関する説明等が追記されています。

P2826R2 Replacement functions

ある関数を指定したシグネチャでそのオーバーロード集合に追加できるようにする、一種の関数エイリアスの提案。

以前の記事を参照

このリビジョンでの変更はよく分かりませんが、例やユースケースをさらに追加し、FAQを追加しているようです。

P2830R2 Standardized Constexpr Type Ordering

P2830R3 Standardized Constexpr Type Ordering

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

以前の記事を参照

R2での変更は

  • EWGI等でのフィードバックに従い、TYPE_ORDER(X, Y)の定義を実装定義かつconstexprにする
  • 文言の追加
  • 例を追加

このリビジョンでの変更は、寄せられたフィードバックを反映した事などです。

P2841R2 Concept and variable-template template-parameters

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

以前の記事を参照

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

  • テンプレートの半順序がテンプレート引数に依存しないように設計を変更
  • 変数テンプレート/コンセプトの特殊化の引数からテンプレートパラメータを推論するセクションを追加

などです。

この提案は現在CWGでレビューを受けています。

P2845R7 Formatting of std::filesystem::path

P2845R8 Formatting of std::filesystem::path

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

以前の記事を参照

R7での変更は、SG16での投票結果を追記した事です。

このリビジョンでの変更は、文言の修正と機能テストマクロの追加などです。

この提案は2024年3月に東京で行われた全体会議において採択され、C++26WDに導入済みです。

P2855R1 Member customization points for Senders and Receivers

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

以前の記事を参照

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

  • 呼び出しに際して渡すタグ型の必要性を完全削除し、メンバ型によって判定するようになった
  • tag_queryは単にquery()に変更された

などです。

このリビジョンにおいてstd::execution::schduleCPOにアダプトするためには、次のようにします

// 自作スケジューラ実装
struct my_scheduler {
  // std::execution::schedule()にアダプトすることを表明
  using scheduler_concept = std::execution::scheduler_t;

  // schedule CPOにアダプトする(タグ型が不用になった
  std::execution::sender auto schedule() {
    // schedulerにアクセスするためのsenderを返す
    ...
  }

};

std::execution::schduleCPOは呼び出し前にこのタグの存在をチェックして、存在する場合にのみ同名のメンバ関数を呼び出します。

P2300のCPOは実際には対応するコンセプトを定義するために使用されており、タグの存在はそのコンセプト内でチェックされます。P2300のコンセプト名execution::xxxに対してタグ名はxxx_conceptになり、タグとして指定する型はexecution::xxx_tです。

例えばoperation_stateの場合は、タグ名はoperation_state_conceptになり、タグ型にはexecution::operation_state_tが使用され、operation_stateコンセプト内でそれがチェックされます。

namespace std::execution {
  // operation_stateコンセプトへのアダプトをチェックする
  template<class Rcvr>
    concept enable-opstate = // exposition only
      requires {
        requires derived_from<
          typename Rcvr::operation_state_concept, // 用意すべきタグ型名
          operation_state_t                       // 指定するタグ型
        >;
      };

  // operation_stateコンセプト定義
  template<class O>
    concept operation_state =
      enable-opstate<remove_cvref_t<O>> &&  // タグのチェック
      queryable<O> &&
      is_object_v<O> &&
      requires (O& o) {
        { start(o) } noexcept;  // operation_stateの主要な要求、execution::start()で呼び出し可能であること
      };
}

この提案は既にP2300にマージされています。

P2863R5 Review Annex D for C++26

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

以前の記事を参照

このリビジョンでの変更は追跡中の提案のステータス更新のみです。

P2866R2 Remove Deprecated Volatile Features From C++26

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

以前の記事を参照

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

P2867R2 Remove Deprecated strstreams From C++26

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

以前の記事を参照

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

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

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

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

以前の記事を参照

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

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

P2872R3 Remove wstring_convert From C++26

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

以前の記事を参照

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

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

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

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

以前の記事を参照

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

  • co-authorを追加
  • 冗長なサブセクション番号を削除
  • 概要とHistoryセクションを改訂
  • Historyセクションを修正し、非推奨の理由とchar8_tベースの置換の間違った追加に関する詳細を追加
  • Deployment Experienceセクションを追加
  • 標準ライブラリ実装における非推奨に関するテストを追加
  • 初期レビューのフィードバックを反映
  • 文言の修正

などです。

P2875R4 Undeprecate polymorphic_allocator::destroy For C++26

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

以前の記事を参照

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

この提案は2024年3月に東京で行われた全体会議において採択され、C++26WDに導入済みです。

P2893R3 Variadic Friends

friend宣言でのパック展開を許可する提案。

以前の記事を参照

このリビジョンでの変更は、文言の修正とオプション2(提案されていない)を削除したことなどです。

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

P2900R6 Contracts for C++

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

以前の記事を参照

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

  • 契約注釈に対する属性の指定を許可
    • P3088R1とP3167R0で提案されていたもの
    • 特に、[[maybe_unused]]post()の結果変数に使用可能
  • await式とyield式がcontract_assert()の述語内に出現するのはill-formed
  • 定数評価中に副作用のある述語を評価するとODR違反になる可能性がある事を明確化
  • Design Principlesセクションの拡張
  • 文面修正や例の追加

などです。

P2927R2 Observing exceptions stored in exception_ptr

std::exception_ptrを再スローせずに例外オブジェクトの取得を試みる関数の提案。

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

  • try_castからexception_ptr_castへ変更
  • モチベーションセクションを追加
  • 機能テストマクロの追加

などです。

P2944R3 Comparisons for reference_wrapper

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

以前の記事を参照

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

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

P2977R1 Build database files

ツール間で同じモジュールを生成するために必要となるコンパイルのための情報をファイルにまとめておく提案。

以前の記事を参照

このリビジョンでの変更はR0で説明されている情報の保存と利用を実装した経験をもとに、必要な事項を追加したことなどです。

特に、次の情報が追加されています

  • モジュールセットのベースライン引数
  • 関連するセット間の関連付けを容易にするファミリ名
  • 提供されたモジュールの外部パス
  • モジュール提供ソース以外の情報を含める

また、このリビジョンでは、これらの情報を表現する方法としてR0でスタンドアロンとして紹介していた方法(特定ファイルに保存)を提案に格上げしています。R0ではコンパイルコマンドデータベースを推していたようですが、上記の実装経験に基づく情報の追加によってコンパイルコマンドデータベースの参照はかなりの重複が発生し価値が低下するとのことです。

P2988R4 std::optional<T&>

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

以前の記事を参照

このリビジョンでの変更は、問題となっていたmake_optional()value_or()のそれぞれについて、常に値(std::optional<T>/T)で返すようにしたことです。

どちらも後方互換性が問題であり、議論の結果良い解決策はないとして現状のstd::optional<T>の挙動に合わせることになったようです。

P2993R0 Constrained Numbers

制約付きの整数型を生成するライブラリ機能の提案。

2018年から2022年の5年間で整数オーバーフローに関連するCVEレコードが1515件あります。1996年6月4日のアリアン5G V88/501ロケットの打ち上げ失敗は整数オーバーフローのエラー処理のバグが原因となり、3億7000万ドルの以上の損害を出しました。2015年と2020年には、ボーイング787のシステム内の時間値の整数オーバフローに関連する2つの別々のバグにより、飛行システムがクラッシュしたり計器に誤った情報が表示されたりするインシデントがありました。

これらのような整数型のオーバフローとそれに伴う結果は、プログラムの機能やセキュリティ、安全性を損なう重大なバグの原因となります。実行時に整数オーバーフローを検出できた場合でも、エラー処理を正しく行わなければそれもバグの元となります。

整数型の実行時の演算に伴う問題としてはそのほかにも、アンダーフロー、ゼロ除算、少し毛色は異なりますが配列の範囲外アクセスなどがあります。

C++コアガイドラインをはじめとする、安全なC/C++プログラムのためのコーディングルールにおいても必ず整数型のオーバーフローに関する注意やルールが存在しています。しかし、そのようなルールが取り入れられている現場においても、整数型のオーバーフローをはじめとする数値演算におけるバグは依然として危険性の高い問題であり続けています。

数値を正しく扱うために、既存のC++言語標準及びライブラリ、ガイドライン、コーディングルールなどは十分ではありません。

この提案では、標準ライブラリに対して新しい制約付きの数値システムを提案するものです。このシステムは、基本的に制約をコンパイル時にチェックし、必要な場合にのみ実行時チェックを行うことで、実行時への影響を最小限に抑えつつ安全で使いやすい数値型を提供するものです。

この提案による機能はconstrained_number<C, T>という型を中心にしています。テンプレートパラメータのCには制約を指定し、Tには内部表現のための整数型を指定して使用します。

// 64bit符号付整数型としての制約を満たす値でのみ初期化可能
constrained_integral<int64_t> foo = 42; // ok

// [0, 10]の区間の整数値のみ表現可能な整数型
// この場合、符号付き整数型の値で初期化できない
constrained_number<constrain_interval<0, 10>> bar = 4;  // ng

ここでの制約とはコンセプトとは関係ありません。詳細は後述しますが、これは構文的な制約だけではなく意味論的な制約も表現可能なDSLになっています。

整数定数によってconstrained_numberを初期化するには、_cnリテラルを使用します。

// 定数値'4'は指定されている制約を満たす
constrained_number<constrain_interval<0, 10>> bar = 4_cn;   // ok

// 定数値'42'は制約を満たさない
bar = 42_cn;    // ng

このチェックはすべてコンパイル時に行われ、実行時のオーバーヘッドはありません。

異なる制約を持つconstrained_number値の間で代入や初期化は可能ですが、代入・初期化しようとしている値が制約を満たしているかどうかがコンパイル時にチェックされます

// 制約を満たす値によって正しく初期化されているとして
constrained_number<constrain_interval<0, 100>> foo = ...;

// 'foo'には'bar'の制約を満たさない値が含まれうる
constrained_number<constrain_interval<0, 10>> bar = foo;    // ng

// 上記エラーが無く、制約を満たす値によって正しく初期化されているとして
bar = ...;

// `bar`の値は`foo`の制約を常に満たす
foo = bar;  // ok

とはいえ、実行時に決定される値によって初期化できなければ実用には耐えません。ただしその場合でも制約を満たすかどうかはチェックされなければなりません。make_constrained()を使用すると、実行時の値から実行時に制約を満たしているかをチェックして変換することができます。このとき、制約を満たさない場合は例外が送出されます。

// 5の倍数であることを要求する制約
constexpr any_constraint auto multiple_of_five_c =
  constraint_of<int64_t> &&
  constrain_multiple<5>;

// 5の倍数値のみからなる整数型
using mult_of_five_t = constrained_number<multiple_of_five_c>;

// コンパイル時にチェックされ、実行時には何もしない
auto v1 = make_constrained<multiple_of_five_c>(1005_cn);    // ok

// 実行時にチェック、制約が満たされない場合は例外を投げる
mult_of_five_t v2 = make_constrained<multiple_of_five_c>(get_some_raw_int());   // ok

// コンパイルエラー
auto v3 = make_constrained<multiple_of_five_c>(12_cn);  // ng

// 実行時に例外が送出される
mult_of_five_t v4 = make_constrained<multiple_of_five_c>(12);   // ok

make_constrained()は引数値に対してコンパイル時の検証が行える場合(_cnリテラルの値など)はコンパイル時にチェックを完了しようとします。

constrained_numberは他の整数型に暗黙変換されません。constrained_numberの値から生の整数値を得るためにはstatic_castによって明示的にキャストします

// 制約を満たす値によって正しく初期化されているとして
constrained_number<constrain_interval<0, 10>> foo = ...;

// 組み込み整数型に変換
auto raw_foo = static_cast<uint32_t>(foo);  // ok

constrained_number<C, T>に指定する制約Cの指定は、簡易なDSLによって行います。ここまでに使用していますが、整数型のとり得る値を区間で制限するためにconstrain_intervalが用意されています

// [-100, 100]区間の値のみ保持可能
constrained_number<constrain_interval<-100, 100>> small_number{};

// ↑はこれと等価
constrained_number<constrain_interval<-100, 100>> small_number = 0_cn;

// 0を除きたい場合([-100, 0) ∪ (0, 100])
constrained_number<constrain_interval<-100, -1> || constrain_interval<1, 100>> small_nonzero_number = 1_cn;

これによって、このライブラリではゼロ除算をコンパイル時に防止することができます。

// small_numberは0を保持しうる
auto result_1 = 10_cn / small_number;   // ng

// small_nonzero_numberは0を保持しない
auto result_2 = 10_cn / small_nonzero_number;   // ok

この制約の実体は変数テンプレートであり、constrained_numberのテンプレートパラメータの外側で定義しておくこともできます。

constexpr auto non_zero_req = constrain_interval<-100, -1> || constrain_interval<1, 100>;
constexpr auto small_num_req = constrain_interval<-100, 100>;

// <=演算子を⊆の意味で使用可能
static_assert(non_zero_req <= small_num_req);

constrained_number<non_zero_req> non_zero = 1_cn;

// この初期化が安全であることは、制約の<=によってコンパイル時にチェックされる
constrained_number<small_num_req> small_num = non_zero;

この制約は、演算によって更新されます。

constexpr auto one_to_ten_req = constrain_interval<1, 10>;
constexpr auto non_zero_req = constrain_interval<-100, -1> || constrain_interval<1, 100>;

constrained_number<non_zero_req> a = 42_cn;
constrained_number<one_to_ten_req> b = 3_cn;

auto c = a * b; // ok

// 実行時の値は期待通りに更新される
assert(c == 126);

// 制約もまた、期待通りに更新される
static_assert(c.constraint == constrain_interval<-1000, -1> || constrain_interval<1, 1000>);

用意されている制約は次のものです

名称 定義 API 備考
区間 [a, b] constrain_interval<a, b> 区間
集合 {a, b, ...} constrain_set<a, b, ...> 外延的な集合
マスク {x ∈ Z | 0 <= x <= 2^n && (x & ~V) == C} constrain_mask<V, C> ビットマスクによる整数値の指定
剰余類 {x ∈ Z | mod(x, a) == 0} constrain_multiple<a> aの倍数からなる集合
Constraint of [min(T), max(T)] constraint_of<T> 整数表現の制約区間
空集合 constraint_empty 表現可能な値なし
不正 constraint_invalid 他の方法では表現できない値が含まれていたであろう集合

これらの制約は集合的に指定されており、集合の演算によって制約を合成したりすることができます。制約の定義のために使用可能な演算は、次のものです

constrained_numberを使用して、[]の引数値を制限する例

template<class T, size_t Extent = dynamic_extent>
class span {
  ...

  constexpr reference at(constrained_number<constrain_interval<0, Extent - 1>> pos) noexcept const
    requires (Extent != dynamic_extent);

  constexpr reference operator[](constrained_number<constrain_interval<0, Extent - 1>> pos) noexcept const
    requires (Extent != dynamic_extent);
  
  ...
};

P2997R1 Removing the common reference requirement from the indirectly invocable concepts

イテレータを介した間接呼び出し系のコンセプトから、common_reference要件を取り除く提案。

以前の記事を参照

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

この提案は2024年6月の全体会議で承認され、C++26ドラフトに導入されています。

P3008R2 Atomic floating-point min/max

浮動小数点数型のstd::atomicにおけるfetch_max()/fetch_min()の問題を解消する提案。

以前の記事を参照

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

  • 以前に未規定としていたところを実装定義に変更
  • fetch_min/maxのデフォルトは不要であり、明示的なfetch_fminimum/minimum_numへ変更
  • 入力の値の一つがNanの場合、結果の値がNanか他の値かどうかは未規定
  • fetch_fminimum/fminimum_num/fmaximum/fmaximum_numは対応するC23のAPiを用いて設計され、シグナリングNaNは無視する
  • fetch_min/maxではシグナリングNaNの動作を明示的に指定しない

などです。

P3016R3 Resolve inconsistencies in begin/end for valarray and braced initializer lists

std::valarrayと初期化子リストに対してstd::beginstd::cbeginを呼んだ場合の他のコンテナ等との一貫しない振る舞いを修正する提案。

以前の記事を参照

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

  • LWG Issue 3624とLWG Issue 3625の変更(Issue解決)を提案する文言から外した
    • これらの解決策を承認しているように見えることが望ましくなかったため

などです。

P3019R7 Vocabulary Types for Composite Class Design

P3019R8 Vocabulary Types for Composite Class Design

動的メモリ領域に構築されたオブジェクトを扱うためのクラス型の提案。

以前の記事を参照

R7での変更は

  • indirectの無条件コピーコンストラクタについて、それを可能にする実装について説明
  • 曖昧さの解消のために代入演算子に関する文言を修正
  • valueless_after_moveメンバ関数の動機について説明

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

  • indirectpolymorphicのコンストラクタでallocator_traits::construct()を使用するためのより明示的な文言を追加
  • indirectpolymorphicin_place_t/in_place_type_tの特殊化としてインスタンス化されるのを防止する
  • reference_wrapperとの一貫性を保つために、Tが完全型であることを強制する

などです。

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

P3029R1 Better mdspan's CTAD

std::span/std::mdspanコンパイル時定数によってインデックス指定を受ける場合のCTADを改善する提案。

以前の記事を参照

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

  • maybe-static-extの特殊化を{T::value}で初期化することで、負の値からの変換を防止
  • 最新のWDへの追随

などです。

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

P3032R1 Less transient constexpr allocation

P3032R2 Less transient constexpr allocation

定数式における動的メモリ確保の制限を少しだけ緩和する提案。

以前の記事を参照

R1での変更は、提案する文言の修正と、機能テストマクロの追加などです。

このリビジョンでの変更は、提案する文言の問題点の指摘に対応したことと、問題の解決方法としてあがっていた2つの方法をどちらも採用し提案するようにしたことです。

この提案では、定数評価コンテキストの分断によってコンパイル時に動的に確保したメモリを開放すべきコンテキストの境界が、ユーザーの期待と一致しないことを解決しようとしており、そのための次の2つの変更を必要としていました

  1. 定数式にならない即時関数呼び出しが定数式になるように、それを含むより大きな式を定数式として扱う
  2. 定数式における動的メモリ確保に関して、開放のタイミングが同じ評価内にある場合に加えて、同じ直接のコンテキスト内にある場合も許可する

1つ目の変更は複雑になり利得が低いとして、R1までは2つ目の変更だけを提案していました。

このリビジョンではどちらの変更も提案するようになっています。ルールは複雑になるものの、それによって解消される問題についてユーザーが細かいルールを学ぶ必要が無くなる利点がある(この利点が当初の見積もりよりも大きい)としています。

P3034R1 Module Declarations Shouldn't be Macros

名前付きモジュールのモジュール宣言において、モジュール名のマクロ展開を禁止する提案。

以前の記事を参照

このリビジョンでの変更はDRであることを明記した事のようです。

この提案は2024年3月の全体会議によって採択されており、C++26に導入済みです。

P3037R1 constexpr std::shared_ptr

std::shared_ptrを定数式でも使える様にする提案。

以前の記事を参照

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

  • モチベーションとなる例を追加
  • libc++とMSVC STLのアトミック操作について追記

などです。

アトミック操作については、std::is_constant_evaluated()を利用することでコンパイル時にはアトミック操作を使用しないように実装可能であり、既存の実装(libc++とMSVC STL)でもそれで実装可能と思われる、と報告しています。

P3049R0 node-handles for lists

リストに対してノードハンドルのサポートを追加する提案。

C++17では、連想コンテナに対してノードハンドルAPIが追加されました。ノードハンドルとはノードベースコンテナのある一要素の所有権を引き取るもので、ノードハンドルを介することで、互換性のあるコンテナ間でムーブすらすることなく要素を移動することができます。

// cpprefjpのサンプルを微修正したもの

import std;

int main() {
  std::map<char, int> m1, m2;

  m1.insert(std::make_pair('a', 10));
  m1.insert(std::make_pair('b', 20));
  m1.insert(std::make_pair('c', 30));

  // m1の'c'をキーとするノードハンドルを取得
  std::map<char, int>::node_type node = m1.extract('c');

  // 取得したノードハンドルを別のmap'm2'へ挿入
  // この時、ノード(要素)はコピーもムーブもされていない
  m2.insert(std::move(node));

  std::println("{}\n{}", m1, m2);
}

出力例

{'a': 10, 'b': 20}
{'c': 30}

一方std::list及びstd::forward_listもノードベースコンテナではありましたが、元々spliceAPIを備えていたこともあり、APIの一貫性以上の利点はないとしてリスト系コンテナに対してはノードハンドルAPIは追加されませんでした。

この提案は、APIの一貫性向上も含めてさらに追加の利点があるとして、std::list及びstd::forward_listにノードハンドルAPIを追加することを提案するものです。

ここで提案されているAPIは、連想コンテナのAPI

// ノードハンドルの型
using node_type = implementation defined specialization of node_handle;

// ノードハンドルの取り出しAPI
node_type extract(const_iterator pos);
node_type extract(const key_type& key);
template<typename Key> 
node_type extract(Key&& key);

// insert()の戻り値型
struct insert_return_type {
  iterator position; 
  bool inserted; 
  node_type node; 
};

// ノードハンドルの挿入API
insert_return_type insert(node_type && handle);
iterator insert(const_iterator pos, node_type && handle);

に対してこのサブセットとなるもので、次の2つの関数からなります

// ノードハンドルの型
using node_type = implementation defined specialization of node_handle;

// ノードハンドルの取り出し
node_type extract(const_iterator pos);

// ノードハンドルの挿入
iterator insert(const_iterator pos, node_type && handle);

// forward_listのAPI
node_type extract_after(const_iterator pos); 
iterator insert_after(const_iterator pos, node_type && handle);

減った分のAPIは、連想コンテナに固有の事情に特化したものだけです。

リスト系コンテナへのノードハンドルAPI追加の利点として、この提案ではsplice()操作と比較してソースとターゲットをより適切に分離できる点を挙げています。

// listでsplice()を使用する例

// valに指定した値に一致する値を持つリスト要素を移動する
template<typename T> 
void splice_if(list<T> & from, list<T> & to, T val) { 
  const auto it{ranges::find(from, val)};
 
  if (it != from.end()) {
    to.splice(to.begin(), from, it); 
  }
} 

// usage: 
list<int> l1 = …; // filled with random ints 

// splice()によって要素を移動させようとする場合
// 移動元と移動先コンテナがそろっている必要がある
list<int> l2 = …;

splice_if(l1, l2, 42);
// この提案のノードハンドルAPIを使用する例

// valに指定した値に一致する値を持つリスト要素のノードハンドルを取得する
template<typename T> 
list<T>::node_type extract_if(list<T> & from, T val) { 
  const auto it{ranges::find(from, val)};

  if (it != from.end()) {
    return from.extract(it);
  }

  return {}; 
} 

//usage: 
list<int>& l1 = …; // filled with random ints 
auto nh{extract_if(l1, 42)};
// nh は転送先とは独立して取り廻すことができる

// これにより、要素取り出しと要素挿入が分離される
list<int>& l2 = …; 
if(nh) l2.insert(l2.end(), move(nh));

この提案の内容は既に、MSVC STLのフォークにて実装可能なことが確認されています。

P3050R1 Fix C++26 by optimizing linalg::conjugated for noncomplex value types

std::linalg::conjugated()を非複素数型に対してアクセサの変更をしないようにする提案。

以前の記事を参照

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

  • 提案する文言についての説明を追加
  • 実装へのリンクを追加
  • タイトルとAbstractを変更して、これをC++26に間に合わせないと破壊的変更になることを強調

などです。

P3064R0 How to Avoid OOTA Without Really Trying

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

OOTAについては上の方のP2135R1を参照。

C++メモリモデルのmemory_order_relaxedにおいてはOOTAが理論的に起こりえることが知られており、これは基本的に望ましくないものとして認識されており、C++規格では追加の制約によって禁止しています。しかし、そのようなメモリモデルからOOTAを取り除こうとする試みが長年行われてきたものの、そのようなメモリモデルは実現不可能、複雑、あるいは実装者に不評なものになり、上手くいかないことが分かってきています。

しかし(OOTA禁止実装が実質的に行われていない?)一方で、現実のC++実装で実際にOOTAが起こる事例は確認されていないようです。

この文書は、現在のハードウェアとそれに対するC++実装ではOOTAが発生しないようになっている理由について解説するものです。それにより、現在の実装においてOOTAを回避するための複雑な実装は必要ないことを説明しています。

標準仕様でOOTAが禁止されているとはいえ、その実装は複雑となることから忌避されているようです。しかし実際にはその実装は必要なく、ほとんどの現実のコンパイラでは強いメモリモデルのシステム(x86など)においてはTotal Store Ordering(TSO)によって、弱いメモリモデルのシステム(ARMなど)においてはdata-dependency orderingによって防止されているとのことです。

data-dependency orderingとはコードの意味的な依存関係から導かれるメモリの読み書きの順序の事です。例えば次のOOTAのサンプルコード

// OOTAの例
atomic<int> x(0);
atomic<int> y(0);

void thread1() {
  unsigned long r1 = x.load(memory_order_relaxed);  // A
  y.store(r1, memory_order_relaxed);                // B
}

void thread2() {
  unsigned long r2 = y.load(memory_order_relaxed);  // C
  x.store(42, memory_order_relaxed);                // D
}

においては、A->B、C->Dそれぞれに意味的な依存関係(後続のストアは先行するロードの結果の値をストアしている)があります。現実世界のCPUはストアする値が得られるまでストア操作を行えないため、BとDのストアはAとCのロードを待たないと実行できません。そのため、ハードウェアはこれらのロード->ストアをコード上での順序と同じ順序で実行するため、OOTAは防止されます。

文書は長いので、その詳細は文書をご覧ください。

P3068R1 Allowing exception throwing in constant-evaluation

定数式においてthrow式による例外送出を許可する提案。

以前の記事を参照

このリビジョンでの変更は、定数式において生成された例外オブジェクトがexception_ptrを用いてその評価の外側にリークしないように文言を調整したことと、その例を追加したことです。

この提案の変更においては、ある定数式の評価中にthrow式によって生成された例外オブジェクトは、生存期間がその定数式の評価を超えてはならない、としています。これはその暗黙のコピー、すなわちstd::exception_ptrによって取り廻されるものについても同様で、例外オブジェクトはその投げられた定数式の評価の内側で処理されなければなりません(されない場合、コンパイルエラーとなります)。

提案より、例

consteval auto fail() -> std::exception_ptr { 
  try { 
    throw failure{}; 
  } catch (...) { 
    return std::current_exception() 
  } 
}

constexpr auto stored_exception = fail(); // 許可されない、定数評価から例外オブジェクトはリークしてはならない

P3072R2 Hassle-free thread attributes

スレッドへの属性指定APIについて、集成体と指示付初期化によるAPIの提案。

以前の記事を参照

このリビジョンでの変更は、 initializer-clausesの例を追加したことです。

P3074R3 trivial union (was std::uninitialized)

定数式において、要素の遅延初期化のために共用体を用いるコードを動作するようにする提案。

以前の記事を参照

このリビジョンでの変更は、代わりのソリューションとしてunionの仕様を変更する提案を追記したことなどです。

ここで新しく提案している解決策は、次のように注釈付きのunionを宣言することでこの目的に沿った新しい共用体を提供することです

template <typename T, size_t N>
struct FixedVector {
  trivial union { T storage[N]; };
  size_t size = 0;
};

このtrivial unionは、要素型によらず常にトリビアルにデフォルト構築可能かつトリビアルに破棄可能であり、そのメンバは初期化されません。ただし、その最初のメンバがimplicit-lifetime typeならば、そのメンバの生存期間を暗黙的に開始しそれをアクティブメンバとします(上記例の場合、FixedVectorのオブジェクトの生存期間が開始された時、storageメンバの配列型オブジェクトとしての生存期間が開始されるものの、配列要素は初期化されない)。

これは未初期化領域を得るためのものであると同時に依然として共用体でもあり、扱いは共用体に準じるため、言語の既存の部分に変更の必要性がなく、未初期化領域を得る目的ですぐに使い始めることができます。

このトリビアルunionのメンバにデフォルトメンバ初期化子がある場合の動作については検討が必要そうですが、ここでは未初期化領域を得るためのものである意図と矛盾しているため、トリビアルunionではデフォルトメンバ初期化子を禁止することを提案しています。

P3085R1 noexcept policy for SD-9 (throws nothing)

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

以前の記事を参照

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

  • principled designとnotable omissionsセクションを追加
  • パススルー関数という定義されていない用語の使用を削除

などです。

P3086R1 Proxy: A Pointer-Semantics-Based Polymorphism Library

P3086R2 Proxy: A Pointer-Semantics-Based Polymorphism Library

静的な多態的プログラミングのためのユーティリティ、"Proxy"の提案。

以前の記事を参照

R1での変更は

  • 抽象化モデルのnoexcept指定を追記し、ytaproxy::invokeproxy::operator()noexcept指定を更新
  • コード生成でより最適化を促進するために、basic_facadeコンセプトとproxyの制約を削除

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

  • make_proxy()を文言から削除
  • facadeコンセプトの文言を更新し、facadeまたはディスパッチの実装でtuple-likeな型を使用できるようにした
  • facadeコンセプトのセマンティクスを改訂し、ディスパッチの呼び出しでフォールバックを使用できるようにした
  • 提案するライブラリ機能の場所を`に変更
  • フリースタンディングについてのセクションを追加
  • P31019R6との比較に関する説明を追加
  • 順序付けとハッシュサポートに関する説明を追加
  • 未解決の質問についてのセクションを追加
  • 2つのヘルパマクロの説明セクションを追加`

などです。

P3091R1 Better lookups for map and unordered_map

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

以前の記事を参照

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

  • .get()の戻り値型をoptional<mapped_type&>に変更
  • .get_ref().get_as()の役割をoptional<T&>に委譲する
  • optional.or_construct()を追加する提案を追記
    • .get_as()の代替
  • optional<T&>.value_or()の拡張を提案
  • 機能テストマクロの追加

などです。

このリビジョンで提案されているのは次の2つのメンバ関数だけになりました

optional<      mapped_type&> get(const key_type& k);
optional<const mapped_type&> get(const key_type& k) const;

以前の.get_ref()optionalcalue_orによって再現可能です

std::map<std::string, int> theMap{ ... };

...

// 文字列範囲namesに含まれている名前と同じ名前の要素をインクリメントする
for (const std:string& name : names) {
  int temp = 0;
  ++theMap.get(name).value_or(temp);  // 参照を通じてインクリメント
  // Possibly-modified value of `temp` is discarded.
}

以前に.get_as()で行っていたことをは、std::optionalに対して.or_construct()を追加することで対応しようとしています

std::optional<std::string> theOpt;
std::string_view sv1 = theOpt.or_construct<std::string_view>("empty");

std::map<int, std::string> theMap{ ... };
std::string_view sv2 = theMap.get(key).or_construct<std::string_view>("none");

.or_construct()std::optionalに追加することを提案しています。

namespace std {
  template<typename T>
  class optional {
    ...

    template <class U = remove_cvref_t<T>, class... Args>
    U constexpr or_construct(Args&&...);

  };
}

opt.or_construct<U>(args...);のように呼んで、optが保持している値をUに変換し、optが無効状態ならばargs...からUを構築して返します。

そして、std::optional<T&>.value_or(U&&)の戻り値型は以前にget_ref()がそうであったようにT&U&&common_referenceにすることを提案しています。

P3094R1 std::basic_fixed_string

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

以前の記事を参照

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

  • この用途にstd::arrayを使用する場合の不便な点について追記
  • Design rationaleとOpen questionsのチャプターを追加
  • Synopsisセクションを更新

などです。

P3097R0 Contracts for C++: Support for virtual functions

契約プログラミング機能において、仮想関数に対する契約の指定をサポートする提案。

以前の契約プログラミング仕様においては仮想関数にも通常の関数と同様に契約注釈を行うことができました。ただし、派生先の契約注釈は派生元の契約を暗黙に継承し、明示的に指定する場合でも派生元の契約と同じにならなければならないとされていました。これは、Assertion Redeclaration ruleというルール(事前条件は同じかより弱いもの、事後条件は同じかより強いものでなければならない)に沿うものでしたが、派生先に明示的に指定された契約注釈が派生元と同じであることをどう表現するかや異なる場合にどうするかなどや、そのルールがC++の実際の仮想関数のユースケースを満たしていないということが問題となり、より議論を尽くすためにC++26に向けては仮想関数に対する契約注釈を目指さないことになりました。それを受けて、現在のContracts MVP仕様(P2900R5)では仮想関数に対する契約注釈の指定は禁止されています。

例えば、Assertion Redeclaration ruleに従わないような2つの呼び出し、事前条件をより強くする場合と事後条件をより弱くする場合の2つのケースにおいても、ユースケースが存在しています。

例えば次のようなCarクラスを基底クラスとして

struct Car {
  virtual void drive(float speedMph)
    pre (speedMph < 100) // don't go too fast!
};

void testCar(Car& car) {
  car.drive(90);  // OK
  car.drive(120); // bug: precondition violation
}

次のようにCarクラスを派生させると、事前条件をより強くすることになります

struct ElectricCar : Car {
  void drive(float speed_mph) override
    pre (is_charged);   // 事前条件が強くなっている

  void charge() { is_charged = true }

private:
  bool is_charged = false;
};


void testCar(Car& car);

int main() {
  ElectricCar myElectricCar;
  testCar(myElectricCar);
}

void testCar(Car& car) {
  car.drive(90); // 事前条件違反、charge()を呼んでいない
}

このように派生クラスで事前条件を強くしてしまうと、ElectricCarインスタンスは一般的にCarインスタンスとして使用することができなくなるため、正統的なオブジェクト指向設計に準拠していません。

それでもなお、これが合理的である場合があります。使用準備が整ったElectricCarインスタンスをより広いスコープで使用することで、より具体的な事前条件を非ローカルで保証することができます。

int main() {
  ElectricCar myElectricCar;
  myElectricCar.charge();
  
  testCar(myElectricCar); // everything works now!
}

デフォルト構築されたElectricCarはそのままCarの代わりに使用することはできませんが、.charge()を呼び出すことでその準備が整います。この場合、ElectricCar::drive()で強められている契約はこの追加のセットアップ手順を省略されないようにすることを表現するものとしてみることができます。このような設計は現実のC++アプリケーションで一般的です。

(提案には、他の3つの場合と多重継承の場合のユースケース例があります)

また、以前のMVP仕様(派生先に派生元と同じ契約を強制する)では、この例だけでなく、Assertion Redeclaration ruleに従うような継承を行っている場合を完全にサポートしきれてはいません。

この提案は、仮想関数に対する契約注釈は契約機能の普及のために必須であるとして、以前の問題を解決して仮想関数に対する契約注釈を個萎えるようにするための新しいソリューションを提案するものです。

この提案では仮想関数に対する契約注釈を許可しますが、派生先の関数は派生元の関数の契約を継承しません。派生元関数の契約は派生元の関数の契約から完全に独立しており、派生先の事前条件及び事後条件は拡大と縮小の両方が可能になります。

そして、仮想関数呼び出し時の契約チェックの評価は次のようになります

  1. 派生元関数の事前条件が評価される
  2. 派生先関数の事前条件が評価される
  3. 派生先関数の本体が実行される
  4. 派生先関数の事後条件が評価される
  5. 派生元関数の事後条件が評価される

仮想関数が基底クラスのポインタ/参照を介して呼び出される場合、派生元の関数は静的に決定されておりその契約も呼び出す場所から見えています。一方、派生先の関数は動的に決定され動的にディスパッチされますが、ディスパッチされた先ではどの派生型の実装された関数が呼ばれているのか、およびその契約についてが分かっています。

そのため、仮想関数の呼び出しにおいては直接関与する2つの型(静的型と動的型)についての情報を利用することができ、それによって上記のような呼び出しシーケンスが可能になります。

例えば、静的型と動的型が一致する場合(派生クラスオブジェクトを派生クラスのポインタ/参照、あるいはオブジェクトから直接呼び出した場合)、その派生クラスにおいてなされている契約だけが評価されます。

提案より、深い継承の例

struct X {
  virtual void f() pre(a) post(b);
};

struct Y : X {
  void f() override pre(c) post(d);
};

struct Z : Y {
  void f() override pre(e) post(f);
};

int main() {
  Z z;

  X& x = (X&)z;
  x.f(); // a -> eの順でチェックされ、z::f()が実行された後、f -> bの順でチェックされる
  
  Y& y = (Y&)z;
  y.f(); // c -> eの順でチェックされ、z::f()が実行された後、f -> dの順でチェックされる
}

多重継承の例

struct X1 {
  virtual void f() pre(a1) post (b1);
};

struct X2 {
  virtual void f() pre(a2) post (b2);
};

struct Y : X1, X2 {
  void f() override pre(c) post (d);
};

int main() {
  Y y;

  X2& x2 = (X2&)y;
  x2.f(); // a2 -> cの順でチェックされ、Y::f()が実行された後、d -> b2の順でチェックされる
}

このように、仮想関数呼び出しにおいてはコード上から見えている直接の静的型における関数と実際に呼び出される動的型における関数だけが契約チェックに関与することになり、直感的になるとともに非常に教えやすくもなります。

この提案における仮想関数に対する契約注釈は、仮想関数が純粋であるかどうかによらず行うことができ、同じように動作します。

提案しているこの方法では、事前条件は派生先の物より強くすることができ、どちらの条件においても派生元の契約を無視するようにすることができます。そのため、Assertion Redeclaration ruleに従ったものではありません。これは、実際のC++における仮想関数の使用パターンとの互換性と一貫性を最大化しつつ、全体として最も幅広いユースケースを可能にしようとした結果です。この提案では、Assertion Redeclaration ruleに従わない契約と継承を許可する一方で、Assertion Redeclaration ruleに従った契約と継承も同時に許可しています。

この提案において重要なことは、呼び出し側(派生元)と呼び出し先(派生先)の関数の両方がその呼び出しに伴って契約をチェックする一方で、必要なところではその契約を変化させたり回避する柔軟性を提供している点です。

P3103R1 More bitset operations

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

以前の記事を参照

このリビジョンでの変更は、rotlrotrの対になるものとして定義することで、文言の指定を簡素化したことなどです。

P3104R1 Bit permutations

P3104R2 Bit permutations

<bit>にビット置換系操作を追加する提案

以前の記事を参照

R1での変更は、AVX512などを考慮するようにハードウェアサポートの項目を拡張したことなどです。

このリビジョンでの変更は、*_bit_permutation()関数を削除したこと(LEWGIにおいて好まれなかったため)です。

P3105R1 constexpr std::uncaught_exceptions()

P3105R2 constexpr std::uncaught_exceptions()

std::uncaught_exceptionsstd::current_exceptionを定数式でも使用可能にする提案。

以前の記事を参照

R1での変更は

  • P3068R0に関して追記
  • Motivationセクションにて、将来性について追記
  • 提案する文言の調整

などです。このリビジョンでの変更は

  • 東京会議でのP3068R0のフィードバックについてのメモを追記
  • 2つの機能テストマクロを変更することについて説明を追加
  • std::rethrow_exceptionconstexprとしてマークされていない理由について説明
  • Overviewセクション名の変更

などです。

P3106R1 Clarifying rules for brace elision in aggregate initialization

素数不明の配列の{}省略を伴う初期化時の規定の矛盾を修正する提案。

以前の記事を参照

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

この提案は2024年3月に行われた東京会議で承認され、C++26ドラフトに取り込まれています。

P3107R1 Permit an efficient implementation of std::print

P3107R2 Permit an efficient implementation of std::print

P3107R3 Permit an efficient implementation of std::print

P3107R4 Permit an efficient implementation of std::print

P3107R5 Permit an efficient implementation of std::print

std::printのより効率的な実装を許可する提案。

以前の記事を参照

R1での変更は

  • libstdc++での予備実験の結果について追記
    • 既存実装と比較して約25%の高速化
  • std::println()の定義をstd::format()を呼び出さないより効率的な形に置き換え

R2での変更は

  • ユーザー定義フォーマッターのformat()でロックをする際のデッドロックの可能性を防ぐため、新しい動作をオプトインにした
  • __cpp_lib_print機能テストマクロを更新するための手順を追加
  • インターリーブ出力の問題をについての例を追加
  • C++及びJavaでのロックの問題を示す例を追加

R3での変更は

  • 継承時の問題を解消するために、ネストされたメンバ名の代わりに名前空間スコープの変数テンプレートを使用するように変更

R4での変更は

  • has_locking_formatterをLEWGのリクエストによりenable_nonlocking_formatter_optimizationにリネーム

このリビジョンでの変更は、標準フォーマッターがこの提案の効率実装にオプトインするため別の方法について追記したことなどです。

この提案は2024年3月に行われた東京会議で承認され、C++26ドラフトに取り込まれており、C++23へのDRとなったようです。

P3119R0 Tokyo Technical Fixes to Contracts

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

2023年3月に東京で行われたWG21全体ミーティングの中で、Contracts MVP(P2900R6)がEWGに転送され、最初のレビューが行われました。この提案は、その際に寄せられたごく小さい問題について報告し、その解決策を提案するものです。

報告されているのは次の3つの問題です。

配列引数と事後条件

事後条件から関数引数を参照する場合、その引数はconst指定されている必要があります。ところが、(参照ではない)配列引数の場合はdecayされることによってポインタによって宣言した場合と同等になりますが、その際にそのポインタそのものにconstを付加することができません。

例えば、次の2つの宣言は等価となります。

void f(const int a[]);
void f(int * const a);

2つ目の宣言を見ればわかるように、これはポインタ自体がconstなのではなく、ポインタの指す先がconstとなる宣言です。2つ目の宣言の場合はconstを追加することは容易ですが、1つ目の配列引数に対してそのポインタにconstを付加することはできません。

これによって、そもそも事後条件からの関数引数を参照することを禁止している理由が再燃します

void f(const int a[]) post( a[0] == 5 ) {
  static int x[1];
  a = x;    // ローカル静的配列へのポインタを代入(ポインタそのものはローカル変数なので関数の外には漏れていない)
  a[0] = 5; // 関数本体では事後条件は満たされている
}

void g() {
  int b[5] = {0,1,2,3,4,5};
  f(b);
  contract_assert(b[0] == 5); // これは決して満たされない(常に失敗)
}

Cの可変長関数引数と事前/事後条件

現在のMVPの規定では、Cの可変長引数を持つ関数に対して事前条件及び事後条件を指定した場合、それらの中からその可変長引数をどのように使用可能なのかについての指定がありません。一般に、va_start()からva_end()までの一連の流れは1つの関数内で完結している必要があり、現在の契約においては次の2点が問題となります

  1. va_start()からva_end()の使用はある1つの述語内で完結しなければならない
    • Cの可変長引数から見ると、異なる契約述語は異なる関数
  2. Cの可変長引数をconstにする方法が無いため、事後条件はそれを参照できない

少なくとも事前条件から可変長引数を参照するには1つ目の問題を解消しなければなりませんが、それは性的に検証できることではないので、破られた場合に未定義動作とするしかありません。

契約条件の評価回数の制限

現在のMVPの仕様では、ある契約述語は違反の検出のために0回以上、違反の処理のために1回以上評価されうるとしていますが、最大何回評価するかについては制限がありません。これについて2つの問題点が指摘されました

  1. 契約述語の評価を何度も繰り返した末にUBになるとしたら、その契約は常にUBになるとみなせる。
  2. 実行時の計算量について厳密な制限を必要とするリアルタイムシステムでは、標準で1回の述語評価から生じる評価回数が制限されていない場合、契約を使用できない
    • 実際に実装がこれについて制限を設けたとしても、使用が無制限を許可する以上契約を使用しないようにする人が居るかもしれない

一方で、無制限の評価を許可する合理的な理由もあります

  1. 異なる翻訳単位で定義されている関数では、関数呼び出しの両側でチェックを行う必要がある場合があるかもしれない
    • これによって、契約のチェックが全く行われないことを回避できる
    • このことは、各契約述語は少なくとも2回評価されることを要求している
  2. 何回評価されるかが不定であることは、副作用が発生する正確な回数への依存を困難にするため、契約述語に副作用があることが推奨されていないことを明確にする
  3. 破壊的な契約述語をテストするための特に徹底的な方法は、テスト中に繰り返して契約述語を評価することで結果が変わるかを観察すること
    • 任意の繰り返し回数を要求する適合コンパイラオプションはこれを検証するための優れたメカニズム

これらの3つの問題について、この提案では次のような解決を提案しています

  1. 配列引数と事後条件 : 事後条件からの配列引数の参照は禁止(コンパイルエラーとする)
    • それを行いたい場合、等価なポインタによる宣言に書き換える
  2. Cの可変長関数引数と事前/事後条件 : 契約述語でのva_startの使用は禁止(コンパイルエラーとする)
  3. 契約条件の評価回数の制限 : 評価回数の上限は実装定義
    • 標準としては具体的な数の指定を避ける

とてもマイナーな問題ではありますが、この解決によってContracts仕様はよりロバストになるはずです。

標準ライブラリ関数に対する[[nodiscard]]指定を個別関数に行うのではなく、推奨事項として単一の文章で指定するようにする提案。

以前の記事を参照

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

  • std::remove_if等をカバーするためにより多くの根拠を追記
  • 提案されたLEWGポリシーの文言と標準に対する文言に箇条書きを追加

などです。

この提案はP3201R1に取り込まれる形になったようです。

P3135R1 Hazard Pointer Extensions

ハザードポインタの拡張機能の提案。

以前の記事を参照

このリビジョンでの変更はtypoやスタイルの調整のみのようです。

P3146R1 Clarifying std::variant converting construction

std::variantの変換コンストラクタ/代入演算子において、コンパイル時定数からの縮小変換を許可する提案。

以前の記事を参照

このリビジョンでの変更はC++17での互換実装について追記した事です。

以前の提案ではC++17以前での実装可能性が不明でしたが、このリビジョンでは実装例が示されています

// 縮小変換が起こるとエラーを起こす(ここは変化なし)
template <typename T>
struct NarrowingDetector { T x[1]; };

// std::declval()を定数式で呼ばないための間接層
// From&& fは評価されない文脈のdeclval()から受け取る
template <typename To, typename From>
auto is_convertible_without_narrowing_helper(From &&f) ->
    decltype(NarrowingDetector<To>{ {std::forward<From>(f)} });

// プライマリテンプレート
template <typename From, typename To, typename = void>
constexpr inline bool is_convertible_without_narrowing_v = false;

// 検出を行う方
template <typename From, typename To>
constexpr inline bool is_convertible_without_narrowing_v<
    From, To,
    std::void_t<decltype(is_convertible_without_narrowing_helper<To>(std::declval<From>()))>  // declval()を型の文脈で関数引数に渡す
  > = true;


// テスト用関数
template <typename From,
          std::enable_if_t<is_convertible_without_narrowing_v<From, float>, bool> = true>
    void FUN(float);

?

//テストケース
int main() {
  using IC = std::integral_constant<int, 42>;

  FUN<IC>(IC{}); // OK after P2280R4(DR)

  IC ic;
  FUN<IC &>(ic); // OK after P2280R4(DR)
}

この実装では、is_convertible_without_narrowing_helperという関数テンプレートによる間接層を追加して、定数式において値を得るのにstd::declval()の呼び出しが型の文脈(評価されない文脈)でしか現れないようにしています。

以前の実装ではdecltype(NarrowingDetector<To>{ { std::declval<From>() } })のようにしていたため、NarrowingDetectorのメンバ配列を初期化する参照は存在しないものを読み取ることになり、エラーになっていました。

この実装では、NarrowingDetectorによる検知はis_convertible_without_narrowing_helper()の後置戻り値型内で行われており、これはis_convertible_without_narrowing_vの部分特殊化のdecltpye()内で参照されています。これは評価されない文脈なので関数は評価されておらず、is_convertible_without_narrowing_helper()の戻り値型だけを取得しようとします。is_convertible_without_narrowing_helper<To>(From&& f)の戻り値型では、この引数の参照fのコピーが発生しており、ここではその先にアクセスしようとしていないため参照の有効性は問われず(P2280R4の効果が適用され)、integral_constant<int, 42>からfloatへの変換を直接書いた時と同様に定数式における変換が考慮されることで、int -> floatの縮小変換で値が失われないことが分かるので定数式で許可され、エラーにならなくなります。

P3147R1 A Direction for Vector

std::vectorに代わる、隣接可変サイズシーケンスコンテナ開発の方向性を探る提案。

以前の記事を参照

このリビジョンでの変更は、サイズを表す型を指定するメンバ型のカスタマイズ方法を追加したことと、P0274の推奨事項を変更したことなどです。

サイズ型のカスタマイズは、sequence_traits_t構造体のメンバ型size_typeを、テンプレートパラメータSIZEによって調整するようにしています

template<std::unsigned_integral SIZE = size_t>
struct sequence_traits_t {
  using size_type = SIZE; // The type of the size field.

  ...
};

これによって、vectorがそのサイズを保持する変数の長さを抑えてSBOの有効長を長くする効果が得られます。

P3149R1 async_scope -- Creating scopes for non-sequential concurrency

P3149R2 async_scope -- Creating scopes for non-sequential concurrency

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

P3149R0 async_scope -- Creating scopes for non-sequential concurrency - WG21月次提案文書を眺める(2024年02月)

R1での変更は、実装経験の追記と受け取ったフィードバックを適用したことです。

R2での変更は

  • counting_scope::nest()を更新して、スコープの未処理のsenderの数がいつ減少するかの説明を追加
  • SG21の投票結果に従って、counting_scope::joined(), counting_scope::join_started(), counting_scope::use_count()を削除

などです。

P3159R0 C+ Range Adaptors and Parallel Algorithms

Rangeアダプタを並列実行する際に必要となる最適化とその方針についての提案。

この提案は、P3300で提案されている並列Rangeアルゴリズムの実装に伴って発生する設計の選択肢について説明し、その選択を提案するものです。以下、ほぼ提案の翻訳

Rangeアダプタ/ファクトリは範囲の各要素の変換をローカルに(グローバルメモリを更新することなく)行うことができ、またコード上でもその変換を分かりやすく表現することができます。

ただし、Rangeアダプタ/ファクトリが生成するviewは遅延評価を行い、範囲の要素に対して1つづつその変換処理を適用していきますが、並列化のためにはその変換処理を抽出してベースとなる範囲に対して一括適用(複数の操作を順番にやるのではなく、1度にすべての操作を適用する)する必要があります。

範囲を消費する並列Rangeアルゴリズムを実装するには、入力の範囲を適用されているアダプタと一番基底にある元の範囲/Rangeファクトリに再帰的に分解する最適化カーネルビルダーを構築する必要があります。ただし、このカーネルビルダーはライブラリ内部に閉じているもので、公開されるAPIには含める必要はありません。

このカーネルビルダーは、必要に応じて各アダプタとファクトリを並列処理に適した代替実装に置き換えたり、前処理パスを挿入したりします。

// 最適化カーネルビルダーの簡単な例
auto optimize_range(range auto&& rng) {
  if constexpr(has_base(rng)) {
    // このアダプタを最適化するロジックにディスパッチする
    return optimize_range(rng.base());
  } else {
    return rng;
  }
}

Rangeアダプタ/ファクトリを標準ライブラリ由来のものだけを考慮することにすると、この最適化カーネルビルダーは次の事を行います

  • 入力の範囲がRangeアダプタなのか、Rangeファクトリなのかを判断する
  • 入力の範囲がどのRangeアダプタ/ファクトリから来たのかを特定する
  • 入力の範囲から、基底となる範囲を取得する
  • 各アダプタを最適化するロジックにディスパッチする

Rangeアダプタの最適化が必要な場合として、例えば次の場合があります

  1. 自明ではない要素の削除
  2. 自明ではない要素のグループ化

自明ではない要素の削除

filtertake_whileなどのアダプタは、事前に自明に計算できない方法で範囲の要素を削除(フィルタ)します。

並列アルゴリズムの実装では、N個の実行エージェントがM個の要素を処理するように、入力をスレッド間で事前に分散することができます。この分散時に範囲内のどの要素が削除されるのかを計算することができないため、各実行エージェントはM個の初期要素を受け取ってから遅れて各自で条件を満たす要素を削除することになります。

これらの非自明な削除を行うアダプタを並行的に使用するためには、削除対象の要素をそれとマークされた特定の値(tombstone : 墓石)に置き換える必要があります。墓石要素は範囲から削除する必要がある事を示すstd::optionalとよく似た要素です。

並列アルゴリズムが範囲を消費する場合、この墓石を取り除く必要があります。このためのもっとも簡単な方法はcopy_if前処理パスを挿入することです。ただしこの処理を適用した範囲がメモリ上で実体化するのを回避するために、インプレースで行う必要があります。このような処理パスはストリーム圧縮(stream compaction)と呼ばれます。

// このような処理は
for_each(rng | filter(f), g);

// このように最適化する必要がある
void kernel(range auto&& rng0) {
  // フィルタ要素の墓石への置き換えと、ストリーム圧縮
  rng1 = compact(rng0 | filter_tombstone(f));
  for_each_collective(rng1);
}

ストリーム圧縮は多くの場合、scanによって実装されます

auto copy_if(range auto&& in, output_iterator auto out, auto pred) {
  vector<uint8_t> flags(size(in));

  transform(par, in, begin(flags), pred);

  vector<size_t> indices(size(in));

  exclusive_scan(par, flags, begin(indices), 0);

  for_each(par, zip(in, flags, indices),
    apply([&in] (auto e, auto flag, auto index]) {
      if (flag) out[index] = e;
    }));

  return subrange(begin(out), next(out, indices.back()));
}

墓石要素を取り除く別の方法は、後続のアダプタ等をすべてラップして墓石要素を無視するようにすることです。全てを単純にラップできる場合、ストリーム圧縮パスを挿入する必要が無くなります。

// このような処理は
for_each(rng | filter(f), g);

// このように最適化(ストリーム圧縮を使用しない)
for_each(rng | transform([f] (auto x) {
                  if (!f(x)) return tombstone(x); else return nullopt;
               }),
         [g] (auto x) { if (x) g(*x); });


// このような処理は
reduce(rng | filter(f), g);

// このように最適化(ストリーム圧縮を使用しない)
reduce(rng | transform([f] (auto x) {
               if (!f(x)) return tombstone(x); else return nullopt;
             }),
       [g] (auto l, auto r) {
         if (l && r) return g(l, r);
         else if (l) return l;
         else if (r) return r;
         else        return {};
       });

ただし、このようなラッピングを常に可能ではありません。adjacentenumrateをはじめとする一部のアダプタは要素の位置を認識します。これは、隣接する要素のアクセスするなど範囲内の要素の位置を考慮することを意味しており、墓石要素の存在はその位置ロジックを混乱させます。これを回避するためには、そもそも回避しようとしている遅延フィルタリングが必要となってしまいます。

このようなラッピングによって遅延的に墓石要素を取り除こうとする場合、最適化カーネルビルダーが範囲を再構築する際に、墓石が必要な範囲を見つけたらそれとマークし、墓石要素を無視できない範囲を見つけたらストリーム圧縮パスを挿入し墓石スキップが不要であるとマークする必要があります。

// 墓石要素の要必要が入り乱れる例
sort(rng | filter(f) | transform(g) | adjacent<2> | filter(h) | transform(i));
  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   XXXXXXXXXXX   ^^^^^^^^^^^^^^^^^^^^^^^^
  //           Tombstoned             Untombstoned         Tombstoned

// ↑を正しく処理する例
void kernel(range auto&& rng0) {
  rng1 = compact(rng0 | filter_tombstone(f) | transform_tombstone(g));
  rng2 = compact(rng1 | adjacent<2> | filter_tombstone(h) | transform_tombstone(i));
  sort_collective(rng2);
}

ラッピングによる遅延的な墓石要素の除去は、最適化カーネルビルダーの複雑さを増大させる代わりに、処理コストの高いストリーム圧縮パスを回避できるためパフォーマンスで有利になります。

C++26(予定)の並列Rangeアルゴリズムに向けては、簡素化のために、現時点ではこのラッピングによる遅延的な墓石要素の除去を行わず、墓石要素が必要となる範囲を見つけた場合はストリーム圧縮パスを挿入するようにすることを提案しています。

自明な要素のグループ化と削除

drop. strideなどのRangeアダプタは事前に自明に予測可能な方法で要素を削除し、chunk, adjacent等のアダプタは事前に自明に計算可能な方法で要素をグループ化もしくは結合します。そのため、これらのアダプタを並列実行する場合、エージェントに分割して作業の分配を行う際にそのグルーピングを考慮することができます。

// この様な処理は
for_each(rng | drop(X), f);

// このように最適化
auto start = begin(rng) + X;
auto end = end(rng);
for_each(start, end, f);

しかしこれらの入力アダプタが非自明な要素の削除もしくは非自明な要素のグループ化(chunk_by, split)を行うアダプタの適用を含んでいる場合、そのアダプタも非自明な要素削除を伴うアダプタとなるため、ストリーム圧縮パスを挿入する必要があります。

これらの、自明な要素のグループ化と削除を行うアダプタを処理するには2つのアプローチがあります

  1. 常にストリーム圧縮パスを挿入し、作業の分散中にそれらを処理しない
    • 実装が単純
  2. 以前に非自明な要素削除/グループ化のパスを含んでいる場合にのみストリーム圧縮パスを挿入し、それ以外の場合は作業分散中に処理する
    • 実装が複雑化するが、より効率的

この提案では、1のアプローチを提案しています。

P3160R1 An allocator-aware inplace_vector

提案中のinplace_vectorにアロケータサポートを追加する提案。

以前の記事を参照

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

  • 提案の要約セクションを追加
  • 設計オプションセクションの名前をAlternatives Consideredに変更
  • Compile-Time Dataセクションが拡張され、Performance and Compile-Time Costsに名前を変更

などです。

P3161R0 Unified integer overflow arithmetic

P3161R1 Unified integer overflow arithmetic

オーバーフローを処理可能な整数演算の提案。

一部のアプリケーションでは、標準でサポートされている(あるいは今後されるであろう)整数値の最大幅を大きく超える値を扱わなければならない場合があります。また、そのような拡張幅を必要としない場合でも、整数演算の結果が有効かどうか(オーバーフローしていないかどうか)を知ることが重要な場合があります。前者の場合は多倍長整数型を使用することになりますが、その実装においては同様に整数演算の桁上がりを検知する必要があります。

それに対処するアルゴリズムは非常に単純で、ほとんどのCPUはそれをハンドルする命令をサポートしているものの、標準C++の範囲内でそれらの機構を利用可能な抽象化は存在していません。C++だけで同等の機能を実装するのはかなり難しく、その結果非常に非効率なコードになってしまいます。

C++23では整数型の飽和演算ライブラリが追加されましたが、これらはオーバーフローに対処する別のアプローチであり、オーバーフローを検知したい場合には向きません。

また、オーバーフローを検知可能な操作を行う関数があり、それがCPU命令を用いて効率的に実装されているとします。CPUは命令の実行のためあるいは結果の退避やレジスタ値の復帰のために特定のレジスタオペランドをmovのような命令によって読み込みますが、一部のCPU(x86-64)では入力と出力のレジスタが一致していることから連続して同じ命令を同じ結果に対して適用していくような場合(多倍長整数の実装でよく現れるパターン)にmov命令の数を削減することができます。独立した計算を並べ替えたり可換なオペランドを入れ替えたりするオプティマイザはさらにmov命令を抑制可能になります。

このような最適化は個別の関数を実装するだけのライブラリ側では不可能であり、よりコンパイラに近いところで作業した場合に可能となります。このことは、このような操作が標準化されていることが重要であることを示唆しています。

この提案は、オーバーフローを検知することのできる整数型に対する操作を行うライブラリ機能を提案するものです。

提案より、整数型の演算のグループの要約表

操作 現在の標準 飽和演算 オーバーフロー安全 Wide arithmetic
足し算 + add_sat() N/A add_carry()
引き算 - sub_sat() N/A sub_borrow()
掛け算 * mul_sat() N/A mul_wide()
割り算, 剰余 /, %, div() div_sat()* is_div_defined div_wide(), is_div_wide_defined
キャスト static_cast saturate_cast would_cast_modify N/A

ここで提案されているのは、Wide arithmeticの列の関数群およびwould_cast_modifyです。

add_carry()はキャリーありの加算を行います。

template<class T>
constexpr add_carry_result<T> add_carry(T v1, T v2, bool carry) noexcept;

result = v1 v2 (carry ? 1 : 0);のような計算を行い、resultTと同じ幅の部分のビットによるTの値とオーバーフローが起きたかどうかのbool値を返します。

sub_borrow()はキャリーあり減算を行います。

template<class T>
constexpr sub_borrow_result<T> sub_borrow(T v1, T v2, bool borrow) noexcept;

result = v1 - v2 - (borrow ? 1 : 0);のような計算を行い、resultTと同じ幅の部分のビットによるTの値とオーバーフローが起きたかどうかのbool値を返します。

mul_wide()はオーバーフローなしの乗算を行います。

template<class T>
constexpr mul_wide_result<T> mul_wide(T v1, T v2) noexcept;

result = v1 * v2;のような計算を精度の制限が無いかのように行い、Tの幅をNとして、resultの下位Nビットと上位Nビットの値をまとめて返します。Nビットの値同士の乗算の結果は2Nビットを超えないので、オーバーフローすることなく乗算を行い結果を得ることができます。

div_wide()は商と剰余の計算を同時に行います。

template<class T>
constexpr div_result<T> div_wide(T dividend_high, T dividend_low, T divisor) noexcept;

これは、次のような計算を精度の制限が無いかのように行います

dividend = (dividend_high << sizeof(T)*8) | dividend_low;
result_quo = dividend / divisor;
result_rem = dividend % divisor;

結果として、Tの幅に切り詰めたresult_quoの値とresult_remの値を返します。

この関数では、divisor == 0もしくは商の値result_quoがオーバーフローしている場合は未定義動作となります。これはis_div_wide_defined()によって調べることができます。

template<class T>
constexpr bool is_div_wide_defined(T dividend_high, T dividend_low, T divisor) noexcept;

引数はdiv_wide()と同じで、div_wide()の結果が未定義にならない場合にtrueを返します。

would_cast_modify()は、キャストによって値が変化するかを調べるものです。

template<class T, class U>
constexpr bool would_cast_modify(U) noexcept;

Uの入力値が型Tで表現可能かを調べ、できる場合にtrueを返します。

また提案にはしていないものの、std::div_sat()の削除とstd::divシグネチャ修正も解決すべき問題として挙げています。

P3162R0 LEWG [[nodiscard]] policy

標準ライブラリの関数に対する[[nodiscard]]の付加についてのポリシーの提案。

基本的なモチベーションは前回の[[nodiscard]]関連の提案と共通しています

ただし、この提案では標準ライブラリの規定で[[nodiscard]]を付加するのをやめようとするのではなく、ポリシーに従ってある程度機械的に判断できるようにすることを目指しています。

この提案では次のようなポリシーを提案しています

  1. 戻り値を無視するとリソースリークなどの重大な欠陥が避けられない関数には[[nodiscard]]を付加する
  2. 関数名が誤解されやすいなど、戻り値を見落とすことがよくある間違いである関数には[[nodiscard]]を付加する
  3. 関数の戻り値としてエラーを伝えるように設計された型には[[nodiscard]]を付加する

加えて、このポリシーを補強する3つの原則を提案しています

  1. 複雑さを最小化する
    • C++の複雑さを最小限に抑えると、新規ユーザーにとって使いやすくなり、メンテナンス負荷が減少し、コードの寿命が延びる
    • この原則によって、void以外の戻り値型を持つ関数すべてに[[nodiscard]]を付加するのが禁止される
  2. 90%の使用例に焦点を当てる
    • 全ての人の問題ではなくほとんどの人の問題に集中することで、一般的により良い結果が得られる
    • 最も一般的なバグの警告を生成するには[[nodiscard]]を数回付加するだけで済む
  3. 結果に焦点を当てる
    • (委員会の)決定の現実の影響を考慮する。[[nodiscard]]ポリシーは次の2つの影響が考えられる
      1. 標準ライブラリ実装
        • 実装者は標準ライブラリの[[nodiscard]]ガイダンスに従う必要は無いため、ここでの影響は最小
        • listdc++/MSVC STLは独自の決定を下し、libc++は標準の規定に倣っている
      2. レーニングコンテンツ - C++の新規学習コンテンツ、本やcppreferenceをはじめとするwebサイトなどでは、標準ライブラリの関数シグネチャが頻繁に再掲している
        • これらのシグネチャは結果としてコーディングスタイルに大きな影響を与えている

ここでのポリシーと原則に従った[[nodiscard]]の付加例

// 'empty'はよく新規学習者に'clear'と間違われる
[[nodiscard]]
bool map<T>::empty() const noexcept;

// Discarding the return value of 'new' is always a memory leak.
[[nodiscard]]
void* operator new(size_t);

// ==の戻り値を捨てるのは頻繁に発生するバグではなく
// [[nodiscard]]を正当化するほど壊滅的でもない
template<class T, size_t N>
constexpr bool operator==(const array<T, N>& x, const array<T, N>& y);

// `expected`戻り値の破棄はエラーの隠蔽につながる
// クラスに対する[[nodiscard]]は少しの構文オーバーヘッドで済む
template<class T, class E>
class [[nodiscard]] expected { ... };

P3164R0 Improving diagnostics for sender expressions

提案中のExecutorライブラリにおいて、senderチェーンのエラーを早期に報告するようにする提案。

P2300にてsender/receiverベースのExecutorライブラリの提案が進行中です。senderはなにかの処理を表現する型のことを指しており、sender同士は|でチェーンすることで非同期処理グラフを構成することことができ、その結果もまた何らかのsender型となります。

処理グラフを構成しているsenderの型チェックのためには通常、その完了シグネチャ(成功・失敗・完了の3つのチャネルが何を返すかなどの情報)を計算する必要があり、一般に完了シグネチャを計算するためにはreceiverのもつ実行環境(execution environment)の情報が必要であるため、senderreceiverに接続されるまではsenderの表現している処理グラフの型チェックを行うことができません。

例えば、read(get_stop_token)のようなsenderstop_tokenを取得して次に渡すsender)は、rcvrというなにかreceiverを接続し処理が開始されると、その内部でrcvrの環境から次のようにstop_tokenを取得してそれを返そうとします

auto st = get_stop_token(get_env(rcvr));  // stop_token取得
set_value(move(rcvr), move(st));          // このsenderの処理の結果としてそれを返す

receiverが与えられてその実行環境が存在している場合にのみ、このsenderread(get_stop_token))はその完了方法を知ることができます。

receiverの実行環境の型は接続されて初めて分かり、これはそのsenderが構成された場所から遠く離れている場合があります。そのsenderがエラーとなりうる構成をされている場合、そのエラーはそのsenderが構成された場所から離れたところで起こることになります。

ところが、用意されているsenderのほとんどはreceiverの実行環境に依存せずにその処理を完了することができます。例えば次のようなsenderによる処理があるとき

just(42) | then([](int* p) { return *p; });

このjust(42)は結果として42という整数値を生成しますが、その結果は他の何かに依存せずに決まります。そして、後続のthenではそこに指定されたラムダの引数型が前段のjust(42)の結果を受けることができないことがすぐに(receiverの接続を待たずして)分かります。

このような依存なしのsender(non-dependent sender)の場合、その後続のsenderにおいてはその結果と自身の入力の期待が一致するかをすぐにチェックすることができます。

P2300でもそのような依存なしのsenderは区別されていますが、現在その特性を使用してはいません。

この提案は、依存なしのsenderが接続された場合の型チェックを早期に(receiver接続前に)行うようにする提案です。ここで提案されているのは次のことです

  • 依存なしのsenderを実行環境無くても完了を知ることのできるsenderとして定義
  • awaitableヘルパコンセプトの定義を変更して、任意のコルーチンで(promise型を知らなくても)awaitableかどうかのを問い合わせられるようにする
    • 例えば、awaiterインターフェース(await_ready, await_suspend, await_resume)を定義していればどのコルーチンでもawaitableであり、依存なしのsenderとして機能するはず
  • 環境引数なしでget_completion_signatures()を呼び出せるようにする
  • completion_signatures_of_tエイリアステンプレートの定義を修正し、senderの非依存シグネチャを問い合わせられるようにする(存在する場合にのみ)
  • senderアダプタアルゴリズムに対して、可能な限り依存なしsenderの性質を維持するように要求する
  • senderアダプタアルゴリズムの"Mandates"にて、依存なしsenderが渡されて型チェックに失敗した場合にハードエラーを起こすように規定する

これによって、多くのsenderに対して早期の型チェックが可能となり、P2300の提案するライブラリ機能のユーザーエクスペリエンスが向上します。

P3165R0 Contracts on virtual functions for the Contracts MVP

契約プログラミング機能において、仮想関数に対する契約の指定をサポートする提案。

背景などは少し上のP3097R0の項を参照。この提案では、仮想関数はC++の主要なテクニックかつスタイルであり、仮想関数に対する契約注釈という重要な機能を最初のMVPの後に延期すべきではなく、最初のMVPに対してそのサポートを有効化することを提案しています。

提案している仮想関数に対する契約注釈のセマンティクスはP3097R0と同じものです。すなわち、仮想関数のオーバーライドにおいてはオーバライドした関数は基底の関数の契約を継承せず、仮想関数がポインタや参照を通して呼び出された場合は呼び出している参照の型(静的型)と実際に呼び出される型(動的型)の2つの型におけるその関数の契約条件だけがチェックされます。その順番は次のようになります

  1. 派生元関数の事前条件が評価される
  2. 派生先関数の事前条件が評価される
  3. 派生先関数の本体が実行される
  4. 派生先関数の事後条件が評価される
  5. 派生元関数の事後条件が評価される

静的型と動的型が一致している場合はその直接の型の関数の契約のみがチェックされ、その型が仮に派生型だったとしても基底クラスの関数になされた契約条件はチェックされません。

提案文書より、ネストしているサンプルコード

struct Vehicle {
  void drive(int speed) pre(speed_within_limit(speed)); // #1
};

struct WheeledVehicle : Vehicle {
  bool tiresSufficientlyInflated = false;
  void drive(int speed) pre(tiresSufficientlyInflated) override; // #2
};

struct MotorVehicle : WheeledVehicle {
  bool engineRunning = false;
  void drive(int speed) pre(engineRunning) override; // #3
};

void use1(Vehicle* veh) {
  veh->drive(80); // #4
}

void use2(WheeledVehicle* veh) {
  veh->drive(80); // #5
}

void use3() {
  MotorVehicle mv;

  use1(&mv);
  
  mv.drive(400); // #6

  use2(&mv);
}

このuse3()の呼び出しに際して、まずuse1()の中の#4では#1#3の契約がチェックされます。次に#6の地点ではその動的型の契約#3だけがチェックされます。そして、use2()の呼び出しに伴う#5では、#2#3の契約がチェックされます。

派生関数は基底関数の契約を継承せず独自の契約を持つことができる、というセマンティクスの根拠として、この提案でもステートフルな派生クラスを挙げています。派生クラスが独自の状態を持つ場合、構築直後は基底クラスの代替として使用できる準備ができていないということはよくあることであり、派生クラスにおける契約はその準備が整ったことを確認する事の出来るポイントとなります。

この提案では次のような設計原則を掲げています

  • このアプローチはABIを破損しない
  • 標準的な代替可能設計は比較的簡単に表現可能である必要があるが、強制する理由はない
  • オーバライドする関数が基底の関数よりも狭い契約を持つなど、標準的ではない設計を許容する必要がある
  • このアプローチでは、契約階層内での契約の包含の証明を要求しない
  • このアプローチでは、呼び出し側での契約チェックを行う実装を可能な限り許可するように努める必要があるが、それ以上ではない

ここで提案されているアプローチは、これらを満たしながらルールはかなりシンプルなものになっています。

P3166R0 Static Exception Specifications

例外機構の欠点を改善することで使いやすくしようとする提案。

C++の例外機構にはいくつかの問題があり、現在積極的に使用される状況ではなく、一部の分野においては例外機構をオフにすることが推奨されてさえいます。これは標準C++の言語方言を実質的に生み出しており、そのような領域では標準ライブラリも使用できないか、使用されていません。これは言語の深い断絶であり、この2種類の言語にわたって共通のコードを書くことはできず、それぞれのプログラムを相互にリンクすることもできません。

そのような分断を防ぐために必要なことはエラーハンドリングの方法色々とを発明しその中から勝者を1つ選び抜くことではなく、既に言語に深く根差したエラーハンドリング機構である例外機構の現在の問題点を改善し、現在例外をオフにしているプログラムにおいてオンにしても何も問題が無くなるほどに普遍的に使用可能に修正することです。

多くのプラットフォームでは、例外処理にはコストがかかることが知られています。例えば、例外を送出するパスを通過する場合、そうでないパスを通過する場合と比較して100~1000倍も遅くなる場合があります。この例外の実行時コストは、例外処理に伴う動的なメモリ確保、実行時型情報、および例外ハンドラの動的な型マッチング等の例外機構の動的な性質によってもたらされており、例外送出時のコストを見積もることを困難にしています。

このような例外処理のパフォーマンスの問題はかなりよく知られており、例外の使用を避けさせている主要な理由の1つになっています。

一方で、エラーハンドリングのモデルとしての例外機構には多くの利点があり、言語にうまく統合されています。もし通常の戻り値返しと同等かそれ以上に効率的に例外機構を再設計することができれば、様々な可能性が開けます。そうなれば、パフォーマンス上の理由で従来は例外を使用していなかった環境で使用できるようにもなります。

そのような領域には、例えばゲーム開発等のリアルタイム環境や組み込み等のフリースタンディング環境等があります。特に、フリースタンディング環境では通常例外が有効化されないため、そこで使用可能な標準ライブラリの機能は大幅に制限されています。フリースタンディング環境で例外を使用可能にするためには、実行時パフォーマンスだけでなく例外機構のためのコードサイズが小さくならなければなりません。この実行時パフォーマンスとバイナリサイズの要件はまた、ゲーム開発等のリアルタイム性が重要な環境においても当てはまります。

また、現在の例外機構は関数のインターフェースからは見えない制御パスが導入されることにより、関数で発生する全てのエラーパスに対処したかどうかが分かりづらいという問題も抱えています。例外機構を改善あるいは代替しようとするこれまでの提案では、プログラムが意図せず例外をハンドリングしないという問題に対処していません。もし、関数を作成する際にその関数の例外仕様をコンパイル時にチェックして、例外を透過することを意図的に選択できるようになれば、未処理の例外に関する2種類のバグを削減することができます。

そのうちの1つは、関数を最初に作成する際に処理すべき例外を見落としていた場合で、もう一つは関数が作成された後でその関数内で呼ばれている関数の例外仕様が変化して処理すべき例外が増えた場合です。現在、C++の例外機構はこのどちらに対しても有効な対策を提供できていません。

この提案は、ここで述べたものも含めてよく知られている例外機構の欠点を改善することで、現在例外が使用されていない(無効にされているか使用できなかった)環境においても例外機構を使用可能にしようとするとともに、コンパイル時に処理すべき例外型を宣言・取得できるようにすることで例外の処理漏れやエラーパスに関わるバグを排除できるようにしようとするものです。

この提案ではおおよそ次の事を提案しています

  • 静的/動的な例外指定を持つ関数を宣言するためのthrow()指定子を追加する
    • throw(T1, T2, ...)thorw(...)、またはthrow(std::any_exception)のいずれかの指定となる
  • 関数の例外指定をその定義から推定するためのthrow(auto)構文を追加する
  • throw()指定子を持つ関数の本体がその例外指定に違反していないことをコンパイル時に静的にチェックすることを強制する
    • 指定に無い例外をハンドルするか、処理されない例外を転送することを宣言しないと、コンパイルエラー
  • 特定の式から送出される可能性のある例外型を照会するためのdeclthrow(expr)構文を追加する
  • 静的な例外指定を持つ関数から送出される例外をキャッチするための、テンプレートcatch構文を追加する
    • これにより、例外の型消去を必要とせずに1つのハンドラで複数の型の例外を処理できる

提案文書より、例

まず、この提案ではC++17で削除された動的例外指定(throw())の構文を再利用、また拡張して、静的及び動的な例外指定を次のように行います

void throws_nothing() throw(); // 空の静的例外指定(noexcept(true)と等価)
void throws_a_b() throw(A, B); // 静的例外指定
void throws_any() throw(...);  // 動的例外指定(noexcept(false)と等価)
void throws_any2() throw(std::any_exception); // throw(...)と同じ意味

この例の2番目のような静的例外指定は、throw(auto)によってコンパイラに任せることができます

template<std::ranges::range R, typename F>
  requires std::invocable<F&, std::ranges::range_reference_t<R>>
void for_each(R rng, F func) throw(auto) {
  for (auto&& x : rng) {
    func(static_cast<decltype(x)>(x));
  }
}

この関数の静的例外指定(その関数が送出しうる例外オブジェクト型)のリストは、declthrow()によって問い合わせることができます。そして、そのように静的に飛んでくる型が分かっている例外オブジェクトはテンプレートハンドラによってキャッチすることができ、これは静的にインスタンス化されたハンドラによってキャッチされます

std::variant<std::monostate, declthrow(throws_a_b())...> err; // variant<monostate, A, B>

try {
  throws_a_b();
} template catch (auto& e) { // instantiated for each static exception type in set { A , B }
  err.template emplace<decltype(auto(e))>(std::move(e));
}

静的例外指定がなされた関数では、その内側から送出される例外の型がチェックされ、その関数の静的例外指定に無い型が送出されうる場合はコンパイルエラーになるようにします

void nothrow_caller() throw() {
  throws_a_b(); // Ill-formed: 例外仕様に記述されないA, Bが送出されうる

  try {
    throws_a_b(); // Ill-formed: 例外仕様に記述されないAが送出されうる(Bしかハンドルされてない)
  }
  catch (B) {}

  try {
    throws_a_b(); // OK: 全ての創出される可能性のある例外型は関数ローカルでハンドルされている
  }
  catch (A) {}
  catch (B) {}
}

throw()な関数内でthrow(...)な関数を呼び出すときも同様

void nothrow_caller() throw() {
  throws_any(); // Ill-formed: 動的な例外が関数ローカルでハンドルされていない

  try {
    throws_any(); // OK
  }
  catch(...) {}
}

throw(A)な関数内でthrow(...)な関数を呼び出すときも同様

void caller() throw(A) {
  throws_a_b(); // Ill-formed: 例外仕様に記述されないBが送出されうる

  try { throws_a_b(); /* OK */ }
  catch (B) {}

  try { throws_a_b(); /* OK */ }
  catch (A a) {
    if (!can_handle(a)) throw; // OK: Aの再創出
  }
  catch (B) {}
}

また提案では、これらの静的な例外指定を利用した効率的な例外処理機構の実装戦略についても詳しく説明しています(ただし、実際の実装経験は無いため予測的なものです)。

P3167R0 Attributes for the result name in a postcondition assertion

事後条件構文内の戻り値名に対して属性を指定できるようにする提案。

P3088R1がContaracts MVP仕様にマージされたことで、契約注釈そのものに対して属性指定を行うことができるようになりました。

int f(int i, int j)
  pre [[foo]] (i > 0 && j > 0)
  post [[bar]] (r: r > 0)
{
  [[likely]] contract_assert [[baz]] (i   j < 10);
  return i  j;
}

ただし、そこでは事後条件の中の戻り値名(post [[bar]] (r: r > 0)r)に対する属性指定は提案されておらず、これは可能になっていません。

既存のC++のエンティティ名に対しては、ごく一部の例外を除いて属性指定ができるようにされています(そのごく一部の例外も例外となっているのは意図的ではないようです)。そのため、この提案はその慣例通りに、事後条件における戻り値名に対しても属性指定を可能にする提案です。

この提案では次の2点の変更を提案しています

  • 事後条件構文(post(r : expr))の戻り値名rの後にattribute-specifier-seqを配置して、その属性はその直前にある戻り値名に作用することを規定
  • [[maybe_unused]]属性が事後条件内の戻り値名に対しても作用するように規定
int g()
  post (r [[maybe_unused]]: r > 0);

この提案はSG21にて採択され、すでにContracts MVP(P2900R6)に対してマージされています。

P3168R0 Give std::optional Range Support

P3168R1 Give std::optional Range Support

std::optionalrangeにする提案。

この提案は、P1255で提案中のviews::maybe/views::nullableの代替案として、そもそもstd::optionalrangeとして扱えるようにしてしまおうとする提案です。

P1255については以前の記事を参照

maube_viewstd::optionalの本質的な違いはrangeであるかどうかの一点のみで、その他の点でほぼ同じセマンティクスを持ち、互換性のあるインターフェースを備えています。一方で、インターフェースには相違点がありものによっては意図的でなかったりどこまでstd::optionalに合わせるべきかが不明瞭なものもあります。

結局、この2つのよく似た型が標準に存在しているとC++標準ライブラリのシンプルさと一貫性を損ね、どちらを使用するべきかについてユーザーに混乱が生じ教育コストも上昇します。

そのため、この提案ではstd::optionalを直接rangeにしてしまうことで、1つの型でP1255のモチベーションを満たそうとしています。

この提案では、std::optionalrangeかつviewであり、またcontiguousかつsizedとなる範囲とすることを提案しています。

namespace std {
  // optional本体
  template<class T>
    class optional;
  
  // viewへの対応
  template<class T>
    constexpr bool ranges::enable_view<optional<T>> = true;   

  // rangeとしてのformatを無効化
  template<class T>
    constexpr auto format_kind<optional<T>> = range_format::disabled;
}

そして、optionalに対してイテレータインターフェースを追加します

template<class T>
class optional {
public:
  ...
  
  // イテレータ型は独自のもの(ポインタではない)
  using iterator       = implementation-defined; // see [optional.iterators]
  using const_iterator = implementation-defined;

  ...
    
  // [optional.iterators], iterator support
  constexpr iterator begin() noexcept;
  constexpr const_iterator begin() const noexcept;

  constexpr iterator end() noexcept;
  constexpr const_iterator end() const noexcept;   

  ...
};

optionalイテレータ型はポインタ型を使用しないことを提案しています。これは、optionalからポインタを取得できる経路が存在してしまうことで、それが誤用されたりすることを防止するためです

void takes_pointer(T const*);
    
void f(optional<T> const& o) {
  // これはどういう意味?
  T const* p = o.begin();  
  
  // これを動作させたい?
  takes_pointer(o.begin());
  
  // もしくはこれも?
  if (o.begin()) { /* ... */ }
}

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

// A person's attributes (e.g., eye color). All attributes are optional.
class Person {                        
    /* ... */                               
public:                               
    optional<string> eye_color() const;   
};                                    
    
vector<Person> people = ...;
P1255R12 この提案
// Compute eye colors of 'people'.
vector<string> eye_colors = people
  | views::transform(&Person::eye_color)
  | views::transform(views::nullable)
  | views::join
  | ranges::to<set>()
  | ranges::to<vector>();
// Compute eye colors of 'people'.
vector<string> eye_colors = people
  | views::transform(&Person::eye_color)
  // ラップが必要ない
  | views::join
  | ranges::to<set>()
  | ranges::to<vector>();
for (auto&& opt : views::nullable(get_optional())) {
  ...

  use(opt); // optの利用は安全
}
for (auto&& opt : get_optional()) {
  ...

  use(opt); // optの利用は安全
}
auto opt_int = get_optional();
for (auto&& i : views::nullable(std::ref(opt_int))) {
  ...

  i = 123;
}
auto opt_int = get_optional();
for (auto&& i : opt_int) {
  ...

  i = 123;
}

この提案の方向性はLEWGのレビューにおいて弱いながらもコンセンサスを得ています。ただし、よりコンセンサスを高めるために実装と開発の経験が必要とされています。

P3169R0 Inherited contracts

契約プログラミング機能において、仮想関数に対する契約の指定をサポートする提案。

この提案は、上の方のP3097R0やP3165R0と同様に、現在MVP仕様で無効化されている仮想関数に対する契約指定とチェックを有効化しようとするものです。動機については、下の方のP3173R0も参照されるといいかもしれません。

ここで提案している仮想関数の契約チェックの方法はP3097R0やP3165R0とは少し異なっています。この提案では仮想関数呼び出し時に次のような手順で契約をチェックします

  1. 呼び出し側は、呼び出された関数の静的型についての事前条件をチェックする
  2. 動的型の関数本体を実行する
  3. 呼び出し先は、オーバーライドされたすべての関数の事後条件をチェックする

すなわちこの提案では、事前条件は基底クラスの関数及びそれをオーバーライドする全ての関数の事前条件の論理和となり、事後条件は呼び出されている全てのオーバーライド及び基底の関数の事後条件の論理積となります。これは、Assertion Redeclaration rule(事前条件は同じかより弱く、事後条件は同じかより強くなければならない)に沿うものです。

またこの提案では、オプティマイザが契約を見て、特に事後条件について同等かより狭くなっていることが分かる場合は1つの契約チェックのみを行い不要なチェックを省略できるようにすることも提案しています。

提案より、基底クラスの場合の例

struct C1 {
  virtual R1 f1(A1)
    pre(c1())
    post(c2())
  {
    contract_assert(c1());  // 常に満たされる
    return ...;             // c2()が満たされていなければならない
    // 事後条件c2()のチェック
  }
};

auto g1(C1& c,A1& a) {
  // 事前条件c1()のチェック
  auto r = c.f1(a);
  contract_assert(c2); // 常に満たされる
}

単一継承の例

struct C2 : C1 {
  R1 f1(A1) override
    pre (c3())
    post (c4())
  {
    contract_assert(c3() || c1);  // 常に満たされる
    return ...;                   // (c4() && c2())が満たされていなければならない
    // 事後条件(c4() && c2())のチェック
  }
};

auto g1(C2& c,A1& a) {
  // 事前条件c3()のチェック
  auto r = c.f1(a);
  contract_assert(c4()); // 常に満たされる
  return r;
}

多重継承の例

struct C3 {
  virtual R1 f1(A1)
    pre (c5())
    post (c6());
};

struct C4 : C2, C3 {
  virtual R1 f1(A1)
    pre (c7())
    post (c8());
  {
    contract_assert(c7() || c3() || c1() || c5()); // 常に満たされる
    return ...; // (c8() && c4() && c2() && c6())が満たされていなければならない
    // 事後条件(c8() && c4() && c2() && c6())のチェック
  }
};

auto g1(C4& c,A1& a) {
  // 事前条件c7()のチェック
  auto r = c.f1(a);
  contract_assert(c8()); // 常に満たされる
  return r;
}

この提案では、より強い事前条件とより弱い事後条件を行いたい場合、contract_assert()によって関数内で記述すればよく、pre()/post()で行う必要は無いとしています。

提案によれば、この実装は難しくなくABIに影響を与えることもないとのことです。

P3170R0 sinkable exception error message

標準の例外クラスに対して、動的確保無しでエラーメッセージを設定可能にする提案。

std::logic_errorstd::runtime_errorをはじめとする標準の例外クラスは、構築時にエラーメッセージを指定することができます。しかし、このコンストラクタにstd::stringを渡す場合、それはコピーされて保持される可能性があります。

std::string temp = to_string(pos, location);
// std::stringはコピーされる可能性がある
throw std::out_of_range(temp);

これは、これらの型のコンストラクタがexplicit T(const string& what_arg);の様に宣言されているためです。

この提案は、右辺値のstd::stringを受け取るコンストラクタを追加することで、例外オブジェクト構築時の余計なstd::stringのコピー及び動的メモリ確保の発生を削減しようとするものです。

この提案では各例外クラスのコンストラクタにT(string&& what_arg);の様なコンストラクタを追加することで、エラーメッセージを指定するstd::stringをムーブできるようにするものです。

std::string temp = to_string(pos, location);
throw std::out_of_range(std::move(temp));

// もしくは
throw std::out_of_range(to_string(pos, location));

これによって、例外を投げる処理がそれに伴って発生しうる動的メモリ確保をその制御下に置くことができるようになります。

この提案は、以前のP3056R0で提案されていたものの一部を分離したものです。

P3171R0 Adding functionality to placeholder types

std::bind用のプレースホルダを活用した短縮ラムダ機能の提案。

標準ライブラリにはいくつかの演算子をラップした関数オブジェクト(std::plus<>など)があり、これらの関数オブジェクトはアルゴリズムのようなライブラリ機能に対して渡してその動作をカスタマイズするのに便利です。

ただし、標準ライブラリにある組み込み演算子に対応する関数オブジェクトは四則演算と比較及び論理演算くらいしか用意されておらず、添字演算子やシフト演算子など欠けているものが多くあります。

また、それらの関数オブジェクトはラムダ式を手書きするのと比較すると記述量を削減できるメリットがありますが、反面行うことが関数オブジェクト内部に隠蔽され対応する名前に置き換えられてしまうため、必ずしも可読性向上に繋がらず、直感的なものでもありません。

Boost.Lambda2というライブラリは、std::bindで使用されているプレースホルダに対して各種演算子オーバーロードを提供しそれによって即席の関数オブジェクトを作成することで、やりたいことを直感的に記述しながらも記述量を削減するということを達成しています。

views::zipイテレータoperator*およびoperator++の実装における各記述方法の比較

// 手書きのラムダ式
auto operator*() const {
  return tuple_transform(current_,
    [](auto& it) -> decltype(auto) {
      return *it;
    });
}

auto operator++() -> iterator& {
  tuple_for_each(current_,
    [](auto& it) { ++it; });
  return *this;
}
// 名前付き関数オブジェクト
auto operator*() const {
  return tuple_transform(current_,
    dereference{});
}

auto operator++() -> iterator& {
  tuple_for_each(current_,
    prefix_increment{});
  return *this;
}
// Boost.Lambda2
auto operator*() const {
  return tuple_transform(current_, *_1);
}

auto operator++() -> iterator& {
  tuple_for_each(current_, ++_1);
  return *this;
}

Boost.Lambda2による記述は圧倒的に簡潔であり、行う作業を直接的に表現することができています。

Boost.Lambda2による記法は、対応する演算子の関数オブジェクトが既に存在する場合でも、一般的な述語を記述するために威力を発揮します。例えば、負の数を判定する述語を記述してみると

// 手書きラムダ式 (28文字)
[](auto e) { return e < 0; }

// std::lessの使用 (19文字)
bind(less{}, _1, 0)

// Boost.Lambda2 (6文字)
_1 < 0

この提案は、Boost.Lambda2の機能を標準化することと、欠けている関数オブジェクトを追加することの2つを提案するものです。

とはいえ、標準ライブラリにはすでにプレースホルダstd::bindが存在しているので、追加で必要なのはプレースホルダに対して作用する演算子オーバーロードのみです。

追加する関数オブジェクトは次のものです

namespace std {
  ...

  // [additional.operations], additional transparent operations
  struct subscript;                                                                 // freestanding
  struct left_shift;                                                                // freestanding
  struct right_shift;                                                               // freestanding
  struct unary_plus;                                                                // freestanding
  struct dereference;                                                               // freestanding
  struct increment;                                                                 // freestanding
  struct decrement;                                                                 // freestanding
  struct postfix_increment;                                                         // freestanding
  struct postfix_decrement;                                                         // freestanding

  // [compound.operations], compound assignment operations
  struct plus_equal;                                                                // freestanding
  struct minus_equal;                                                               // freestanding
  struct multiplies_equal;                                                          // freestanding
  struct divides_equal;                                                             // freestanding
  struct modulus_equal;                                                             // freestanding
  struct bit_and_equal;                                                             // freestanding
  struct bit_or_equal;                                                              // freestanding
  struct bit_xor_equal;                                                             // freestanding
  struct left_shift_equal;                                                          // freestanding
  struct right_shift_equal;                                                         // freestanding

  ...
}

どちらも、アドレス取得演算子&)に対応するものは見送られています。

P3172R0 Using this in constructor preconditions

コンストラクタにおける事前条件にて、thisを使用した場合の動作を規定する提案。

現在のContarcts MVP仕様ではコンストラクタに事前条件を指定することができ、これは主にコンストラクタ引数についての事前条件を指定するものです。

class X {
  std::string name;

public:

  explicit X(const char * n)
    pre(name != nullptr)  // まずこっちが評価され
    : name{n}             // 次にこっちが評価される
    {}
};

また、メンバ関数における契約条件として同じクラスのメンバ関数を使用することができます。

T& container<T>::front()
  pre(!empty());

ただし、この2つの事を組み合わせてコンストラクタの事前条件でメンバ関数を呼び出してもそれは機能しません。デストラクタの事後条件でメンバ関数を呼び出すことも同様です。なぜなら、コンストラクタの呼び出し前はまだそのクラスのオブジェクトは初期化されておらず、デストラクタの実行後にはそのクラスのオブジェクトは既に破棄されているからです。

現在のMVP仕様ではこれについての規定がなく、暗黙的に未定義動作となります。

この提案は、コンストラクタの事前条件でのthisの使用とデストラクタの事後条件でのthisの使用の2つの場合について、どのような動作となるかを明確にしようとするものです。

この提案では次の2つの選択肢を挙げています

  1. 明示的に未定義動作とする
    • 現在のコンストラクタにおける純粋仮想関数呼び出しと、コンストラクタの関数tryブロックにおけるthisの使用時と同じ挙動とする
    • ユーザーはそのような危険なコードを書かないものとして信頼する
  2. ill-formedとする
    • コンストラクタの事前条件とデストラクタの事後条件で暗黙的にでもthisを使用している場合にコンパイルエラーにする
    • 一部の有用なアサーションが禁止されるかもしれない
    • Contractsの設計の精神に則った選択であるように思われる
    • GCC13のContracts実験実装では、この動作を取っている

この提案では、このどちらを選択するかはSG21に委ねています。

P3173R0 P2900R6 may be minimimal, but it is not viable

Microsoftによる、現在のContracts MVPのC++26への導入について反対する意見書。

次の3点について、懸念が解消されない限りP2900R6のContracts MVPをC++26に導入するのに反対する、としています

  1. 契約条件で未定義動作が起こることを許容していること
    • P2680R1のような仕組みを導入し、ContractsをUBフリーにする
  2. 仮想関数と関数ポインタ経由などの間接呼び出しにおける契約チェックが欠けている
    • 仮想関数呼び出しも関数ポインタなどを経由した間接呼び出しもC++における実装の定番であり、これをサポートしない機能はC++26に適しているとは言えない
  3. C++26の段階で標準ライブラリに対して適用される予定が無い事
    • 標準ライブラリのような基本的なコンポーネントで使用しないことは機能が使用可能ではないことを示している
    • 標準ライブラリに対して適用することで、機能の実装・使用経験が得られる

P2900R6で確立されているContracts MVPの設計やその原則などに反対するものではありません(1を除いて)が、これらの懸念を解消しなければMicrosoftとしてはC++26への導入に反対する意向のようです。

この提案を受けて、P2680の方向性をSG23で再検討することにしたようです。

P3174R0 SG16: Unicode meeting summaries 2023-10-11 through 2024-02-21

2023年10月から2024年2月に行われたSG16のミーティングの議事録。

全部で7回のミーティングが開催されており、どの提案をレビューしてだれがどういう発言をしたのかが詳しく記されています。

P3175R0 Reconsidering the std::execution::on algorithm

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

sendersenderアルゴリズムをあまり知らない人が次のコードを見て

namespace ex = std::execution;

ex::sender auto work1 = ex::just()
                      | ex::transfer(scheduler_A);

ex::sender auto work2 = ex::on(scheduler_B, std::move(work1))
                      | ex::then([] { std::puts("hello world!"); });

ex::sender auto work3 = ex::on(scheduler_C, std::move(work2))

std::this_thread::sync_wait(std::move(work3));

最終的にstd::puts("hello world!")はどこのschedulerで(scheduler_A, scheduler_B, scheduler_Cのどれか)実行されるか?と問われたとします。次のような合理的な推論によって、scheduler_Cだと答えるかもしれません

  1. 明らかに最初に実行するのはon(scheduler_C, work2)
  2. とすると、work2scheduler_Cで実行されるのは確実
  3. puts("hello world!")work2の一部なので、当然scheduler_Cで実行される

しかし実際には、scheduler_Aputs("hello world!")が実行されます。

work2scheduler_Bwork1を実行し、work1はすぐにex::on()によってscheduler_Aに遷移して以降戻りません。そのため、work1の継続処理はscheduler_Aで実行されることになり、puts("hello world!")scheduler_Aで実行されます。

work3に追加の作業が接続されている場合、それもscheduler_Aで実行されます。

P2300の作者の1人は実際にこの混乱に遭遇し、何人かのプログラマの友人に聞いたところ全員が実際の動作とは異なる動作を期待することが分かりました。

この問題は一部のアルゴリズムの名前が悪さをしていると考えられるため、名前を変更すればその動作に誤った期待をしづらくなります

namespace ex = std::execution;

ex::sender auto work1 = ex::just()
                      | ex::continue_on(scheduler_A);

ex::sender auto work2 = ex::start_on(scheduler_B, std::move(work1))
                      | ex::then([] { std::puts("hello world!"); });

ex::sender auto work3 = ex::start_on(scheduler_C, std::move(work2))

std::this_thread::sync_wait(std::move(work3));

continue_onstart_onという名前はどちらもその実際の動作と一致する一方向の実行コンテキスト遷移を表しています。

この提案はまず、これをメインに提案するものです。

元々のstd::execution::onという名前は、人々に対してそこへ行ってからまた戻ってくるものだと思わせてしまい、それを修正するためにstart_onに変更しようとしていますが、一方でそこへ行ってから戻ってくるアルゴリズムの需要もありそうです。

非同期作業においては、開始時と同じ実行コンテキストで完了するとより適切にカプセル化されます。例えば、OSのスレッドプールからタスクをco_awaitして、作業完了後に再開したスレッドがOSのタイマースレッドだった場合、利用者はかなり驚くことになるでしょう。当然ながら暗黙的にそのようなことを行うのはひどい設計です。

std::execution::onという名前は行って戻るという処理の印象を抱かせるものでしたが実際にはそういう動作をしておらず、この提案でその名前が変更されたことによってそのための名前が解放されることになります。

そこでこの提案では、実行元のコンテキスト(scheduler)を記憶し指定された処理を指定されたコンテキストで実行した後で元のコンテキストに自動的に戻ってくるstd::execution::onという新しいアルゴリズムを追加することを提案しています。

これは次のように既存のsenderアルゴリズムによって構成できます

template <ex::scheduler Sched, ex::sender Sndr>
sender auto on(Sched sched, Sndr sndr) {
  return ex::read(ex::get_scheduler)
       | ex::let_value([=](auto old_sched) {
           return ex::start_on(sched, sndr)
                | ex::continue_on(old_sched);
         });
}

onをそこへ行ってまた戻るアルゴリズムとして定義すると、別のタイプのそこへ行ってまた戻るアルゴリズムを考えることができます。例えば次のコードについて考えてみます

ex::sender auto work = async_read_file()
                     | ex::on(cpu_pool, ex::then(crunch_numbers))
                     | ex::let_value([](auto numbers) {
                         return async_write_file(numbers);
                       });

async_read_file()async_write_file(numbers)はともにsenderを返す関数です。

このコードでは、まず非同期にファイルを読み込みそれをon-senderに転送します。ここ(2行目)のonは前の作業のsenderscheduler、そして継続を受け取る別のonオーバーロードです。

senderasync_read_file())の結果を受け取って指定されたschedulercpu_pool)に遷移し、受けた結果を継続に転送してex::then(crunch_numbers)を実行します。その後元の実行コンテキストに戻り、async_write_file(numbers)-senderを実行します。

これを1つ前で提案したonで書くと次のようになります

ex::sender auto work = async_read_file()
                     | ex::let_value([=](auto numbers) {
                         ex::sender auto work = ex::just(numbers)
                                              | ex::then(crunch_numbers);
                         return ex::on(cpu_pool, work)
                              | ex::let_value([=](auto numbers) {
                                  return async_write_file(numbers);
                                });
                       });

元のonは2引数であり、前の作業のsenderを受け取ることができず、基本的に処理グラフ(パイプライン)の先頭で使用するものです。こちらのおーバロードのonは3引数で、1つ目の引数として前の作業のsenderを受け取ることができ、それによってパイプラインの途中で別の実行コンテキストへ移って少し作業して、完了したら戻ってくるという処理を簡単に書けるようになります。

この提案ではこちらの形式のonを追加することも提案しています。

この提案では追加でいくつかの関連する変更も提案しています。ただし最初の2つ以外はオプショナルとしており、まとめると次の事を提案しています。

  • 提案
    • std::execution::onstd::execution::start_onに変更
    • std::execution::transferstd::execution::continue_onに変更
  • オプションの提案
    • 指定された処理を指定されたコンテキストで実行した後で元のコンテキストに自動的に戻ってくるstd::execution::onアルゴリズムを追加
    • receiverの実行環境に値を書き込むことのできるwrite_envを追加し、readread_envに変更
      • 新しいonの実装に使用する
    • write_envの簡単な応用であるカスタマイズ不可なunstoppableアダプタを追加
      • 現在のreceiver環境のstop_tokennever_stop_tokenに変更するもの
      • これはschedule_fromアルゴリズムの再指定(次の項目)に使用する
    • schedule_fromを一般化して、senderschedulerの代わりに2つのsenderを受け取るようにして、finallyという名前に変更し、カスタマイズ不可にする
      • schedule_from(sch, snd)のデフォルト実装をfinally(snd, unstoppable(schedule(sch)))として指定
    • パイプラインの途中に挿入することのできるonオーバーロードを追加

これらのことはstdexecで1年前から実装されているようです(ただし、巧妙に名前空間が分けられているようです)。

P3176R0 The Oxford variadic comma

前にカンマが無い省略仮引数引数(...)の使用を非推奨にする提案。

この提案が対象にしているのは関数引数の宣言におけるf(int...)のような...の使用です。これは現在f(int, ...)と等価な宣言として扱われています。

// OK, 関数テンプレートパラメータパック
template<class... Ts> void a(Ts...);

// OK, 略記関数テンプレートパラメータパック
void b(auto...);

// OK, 省略パラメータ, Cとの互換性あり
void c(int, ...); 
void d(...); 

// Deprecated, 省略パラメータ, Cではill-formed
void e(int...);   // 👈
void f(int x...); // 👈

// Ill-formed, but unambiguous
void g(int... args);

このf(int...)のような...の使用はCでは許可されておらずC++で導入されてしまったものです。そのため、これは可変長引数関数の宣言として解釈されるべきではなく、多くのユーザーもパラメータパックの一種だと認識するでしょう。

このような宣言((int...))が間違って使用されてしまっていることは将来の機能に対して悪影響を与えます。すでにP1219R2に影響を与えている他、将来的に出てくる提案にも影響を与えるでしょう。

また、可変長テンプレートに慣れていないユーザーはclassの後に...を置くのを忘れがちです。さらに、autoによる略記を行うとさらに間違いが分かりづらくなります

// 略記可変長関数テンプレート
void g(auto ...args);

// 略記関数テンプレート(可変長ではない)
void g(auto args...); // おそらく書き間違い

可変長テンプレートのパラメータパックの宣言と展開の記述はややこしく、このような書き間違いはよく起こってしまうため、これが何かしら診断されることが望ましいです。

さらには、C++ではf(auto......)f(T......)のような宣言さえ許可されています(それぞれ、f(auto ..., ...)f(auto, ..., ...)と同じ宣言となる)。

この提案は、これらの問題からこのようなタイプの宣言を非推奨とすることを提案するものです。

以前の提案ではこのような宣言を禁止しようとするものもありましたが、既存コードへの影響から頓挫しており、この提案では同じ轍を踏まないために単に非推奨にすることまでを提案しています。

提案より、その他の例

void a(...);                // OK
void b(auto...);            // OK
void c(auto, ...);          // OK

void d_depr(auto......);    // deprecated
void d_okay(auto..., ...);  // OK

void e_depr(auto x...);     // deprecated
void d_okay(auto x, ...);   // OK

void f_depr(auto...x...);   // deprecated
void f_okay(auto...x, ...); // OK

void g_depr(int...);        // deprecated
void g_okay(int, ...);      // OK

void h_depr(int x...);      // deprecated
void h_okay(int x, ...);    // OK

P3177R0 const prvalues in the conditional operator

条件演算子の型の決定において、スカラ型とクラス型で異なる結果となるのを修正する提案。

次のようなコードにおいて

// add_rvalue_referenceしないdeclval<T>()
template <class T>
auto make() -> T;

template <class T, class U>
using cond = decltype(true ? make<T>() : make<U>());

template <class T>
using cref = cond<T, T const&>; // この型は何になる?

crefの型は何になるでしょうか?

Tに修飾が着いた型になるだろうということがまずわかります。片方がconst T&なのでT&T&&ではないことが分かります(両辺を受けられないので)。したがって、Tconst T&のどちらかになるであろうと予想できます。このコンテキストでprvalueなTが左辺値に昇格されるのは変なので、Tになると考えることができます。

実際の結果は、スカラ型の場合はTになり、クラス型の場合はconst Tになります。

cref<int> // int
cref<std::vector<int>> // const std::vector<int>

これは驚きの挙動であるだけでなく、有害な場合があります。

auto get() -> T;
auto lookup() -> T const&;

T obj;
obj = flag ? get() : lookup();  // ここ

このコードの最後の行において、flagが何であれTがスカラ型の場合は結局コピーなので問題はありません。クラス型の場合flagによって起こることが変化します。そして、この右辺の条件演算子の結果はconst Tになるわけですが、これが仮にTになるとした場合と比較すると非効率なことになります。

flag const T T
true コピー代入 ムーブ代入
false コピー構築後コピー代入 コピー構築後ムーブ代入

=の右辺の型にconstが付いてしまうことによってこの違いが発生しています。また、これはif (flag) { obj = get(); } else { obj = lookup(); }のようなコードよりも効率が悪くなっています。

また、より出会いやすい所では<ranges>const_iteratorで出会うかもしれません。例えば次のようなイテレータ型があるとき

// プロクシイテレータ型とする
template <class T>
struct Priterator {
  using value_type = T:
  auto operator*() const -> T;

  // other stuff
};

これをconst_iteratorに通すことを考えます。

template<indirectly_readable It>
  using iter_const_reference_t =
    common_reference_t<const iter_value_t<It>&&, iter_reference_t<It>>;

template<class It>
  concept constant-iterator =                                                   // exposition only
    input_iterator<It> && same_as<iter_const_reference_t<It>, iter_reference_t<It>>;

template<input_iterator I>
  using const_iterator = see below;

const_iterator<I>constant-iteratorコンセプトによってIが既に定数イテレータであるかどうかを調べて、std::basic_const_iteratorでラップするかどうかを決定します。そして、その決定にはcommon_reference_tが使用されます。これも非常に複雑ですが、今回の場合は次のようなコードと結果が等しくなります

template <class T>
using iter_const_reference_t = cond<iter_value_t<T> const&&, iter_reference_t<T>>;

condは冒頭で見たものです。今回はPriterator<T>に対してconst<const T&&, T&>のようになります。この場合でも結果は同じで、Tがクラス型の場合この型はconst Tになります。

すると、constant-iterator<Priterator<T>>Tがクラス型の場合falseになり、const_iterator<Priterator<T>>std::basic_const_iteratorでラップされます。そして、その間接参照結果型はconst Tになります。

本来間接参照結果型がprvalueなTの場合、iter_const_reference_tは単にTになるのが正しいふるまいです。しかし、この場合そうならず、余計なconstがついてしまっています。

これは特殊な場合ではなく、value_typereference_typeもprvalueなTの場合に同じことになります。この場合に不要なラッピングが発生することも問題ですが、何より問題なのは本来ムーブできたものが出来なくなっていることが問題です。

まとめると、条件演算子の結果は:の左右のオペランドの組み合わせによって次のようになります

T | T constとなっているところはTがスカラ型(左)かクラス型(右)かによって結果が変わる事を表しています。これらのグループはconst Tが条件演算子の後ろ側のどちらかのオペランドにある場合(黄色)と無い場合(オレンジ)の2つのグループに分けることができます。

オレンジのグループはここで問題にしてきたもので、この場合にconst Tを生成するのは間違っています。

黄色のグループは少し質問が異なります。ここでconst Tを生成するのはユーザーの求めるものなのでしょうか?そうすることに全くメリットが無いわけではなく、ムーブセマンティクス以前は関数がTの代わりにconst Tを返すようにしてf() = xのような代入を防止することが推奨されていた時代がありました。しかしこれは現在では推奨されず、代入演算子の左辺値修飾によって防止することができます。

とはいえ、const Tという型が現れる場合が稀であるため、オレンジのグループの修正の重要度はあまり高くありません。オレンジのグループを修正することでさらに重要度が低下するでしょう。

この提案はこの問題を解決するためのものであり、そのために(主張の)弱い提案と強い提案の2つを挙げています。

弱い提案は、両方のオペランドconst Tが現れていない場合にクラス型のTについての結果をTになるようにするものです。すなわち、先ほどのオレンジのグループだけを修正するものです。

強い提案は、クラス型とスカラ型で結果が異ならないようにするものです。すなわち、弱い提案に加えて黄色のグループも修正するものです。

ただし、この強い提案には一点だけ違和感があるかもしれない部分があり、cond<const T, const T>Tになる所です。スカラ型の場合は現在そうなっているのですが、これがconst Tになる方が自然な挙動かもしれません。その場合、この部分だけ現在の動作を維持するように強い提案を修正することもできます

この場合スカラ型とクラス型で異なる型を生成することになりますが、おそらく問題にはならないでしょう。

P3179R0 C+ parallel range algorithms

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

別の言い方をすると、並列アルゴリズムをRange化する提案です。

P2500R2では、既存の並列アルゴリズムをRange対応したうえで、さらにschedulerを受け取れるようにしようとするものでした。その後SG9ではこの2つの事(並列アルゴリズムをRange対応と並列アルゴリズムscheduler対応)を分割して処理することになったようです。この提案は、前者の並列アルゴリズムのRange対応を担うものです。後者を担うのはおそらくP3300です。

この提案では、既存のRangeアルゴリズムに対して標準の実行ポリシーを受け取ることのできるオーバーロードを追加しようとしています。それらのものの事を、並列Rangeアルゴリズムparallel range algorithms)と呼称しています。

設計の概要は次のものです

  • 並列Rangeアルゴリズムは、最小限のコードの変更で使用できるようにするために、C++17の並列アルゴリズムに近いものにする
  • 並列Rangeアルゴリズムは、対応するシリアル(非並列の)Rangeアルゴリズムの戻り値型と同じ戻り値型となる
  • 並列Rangeアルゴリズムは、シリアルRangeアルゴリズムと同様にADLで発見されない関数である
  • 並列Rangeアルゴリズムが要求する範囲及びイテレータのカテゴリは、std::execution::seqポリシーの場合を除いて少なくともランダムアクセス可能である必要がある
  • 並列Rangeアルゴリズムに呼び出し可能オブジェクト(述語やfor_eachの処理など)を渡す場合、const修飾されたoperator()が必要
  • 提案しているAPIはカスタマイゼーションポイントではない
  • ここで提案している並列Rangeアルゴリズムconstexprではない

この提案では特に、既存の並列アルゴリズムのRange移行、あるいは既存のRangeアルゴリズムの並列アルゴリズム移行において、コードの変更が最小になるように注意を払っています

// 既存の並列アルゴリズムの使用
std::for_each(        std::execution::par, v.begin(), v.end(), [](auto& x) { ++x; });

// イテレータペアを使用したまま並列Rangeアルゴリズムへ移行
std::ranges::for_each(std::execution::par, v.begin(), v.end(), [](auto& x) { ++x; });

// さらに、範囲を直接渡す形に移行
std::ranges::for_each(std::execution::par, v, [](auto& x) { ++x; });
// 既存のRangeアルゴリズムの利用
std::ranges::for_each(                     v, [](auto& x) { ++x; });

// 実行ポリシーを指定して並列Rangeアルゴリズムへ移行
std::ranges::for_each(std::execution::par, v, [](auto& x) { ++x; });

このように、並列Rangeアルゴリズムへの移行作業は名前空間名を追加するか、実行ポリシーを追加するだけで行うことができます。Rangeアルゴリズムからの移行の場合は戻り値型も同じままなので結果を利用するコードにも変更が必要ありません。

for_eachで示すと、提案するAPIは次のようになります

namespace std::ranges {
  // Policy-based API
  template <class ExecutionPolicy, policy-dependent-iterator I, sentinel_for<I> S,
            class Proj = identity, indirectly_unary_invocable<projected<I, Proj>> Fun>
    ranges::for_each_result<I, Fun>
      ranges::for_each(ExecutionPolicy&& policy, I first, S last, Fun f, Proj proj = {});

  template <class ExecutionPolicy, policy-dependent-range R, class Proj = identity,
           indirectly_unary_invocable<projected<iterator_t<R>, Proj>> Fun>
    ranges::for_each_result<ranges::borrowed_iterator_t<R>, Fun>
      ranges::for_each(ExecutionPolicy&& policy, R&& r, Fun f, Proj proj = {});
}

P3180R0 C+ Standard Library Ready Issues to be moved in Tokyo, Mar. 2024

3月に行われたKona会議でWDに適用されたライブラリに対するIssue報告の一覧

P3181R0 Atomic stores and object lifetimes

オブジェクトへの最後の書き込みがその破棄の前に行われなければならない、というルールを緩和する提案。

これは、マルチスレッドにおける共有変数のアトミックアクセスにおける一部の挙動の実装可能性の問題から提起されているようです。

オブジェクトへの最後のアクセスがスレッドAで行われ、それがstd::atomic<bool>へのストアである場合を考えます。スレッドBはそのアトミック変数に対してacquireロードを実行し、その後そのアトミックオブジェクトを破棄します。

/// Thread A:
a->store(1, release); // a1: 1をストア

/// Thread B:
r1 = a->load(acquire);  // b1: 1を観測
delete a;               // b2: aを破棄

この場合、a1のストアがreleaseストアであればa1はb2よりも前に発生(happens-before)します。しかし、これがreleaseフェンスとrelaxedストアだと問題があります。

/// Thread A:
fence(release);       // releaseフェンス
a->store(1, relaxed); // a1: 1をストア

/// Thread B:
r1 = a->load(acquire);  // b1: 1を観測
delete a;               // b2: aを破棄

ここでのスレッドAにおけるreleaseフェンスとrelaxedストアは基本的に最初の例のreleaseストアとほぼ等価でありその差は通常問題になりませんが、ここでは問題になります。

この場合、b1のacquireロードと同期するのはストアそのものではなくreleaseフェンスであるためで、a1のストアはb2よりも後に実行される可能性があります。

問題を顕在化させるために、スレッドCを追加してそこでAとBで使用されたのと同じメモリを再利用する事にします。

/// Thread A:
fence(release);
a->store(1, relaxed); // a1: 1をストア

/// Thread B:
r1 = a->load(acquire);  // b1: 1を観測
delete a;               // b2: aを破棄

/// Thread C: aの再配置を行う
b = new atomic<int>(0); // c1: aと同じストレージを取得するとする
r2 = b->load(relaxed);  // c2: スレッドAからの1は観測されないはず

この例においてr2で1が観測されないためにはまず、アロケータ(newの実装)はb2がc1の前に起こる(happens-before)ことを保証する必要があります。そのうえで、a1がc2よりも前に発生する(happens-before)ことを保証する必要がありますが、そのためにはa1がb1よりも前に起こる(happens-before)ことを保証する必要があります。すると、a1のストア(memory_order::relaxed)が実質的にmemory_order::releaseに変更されてしまうことになり、これは受け入れられません。

すなわちこの場合にr2で1が観測されないという保証は実装不可能となります。

一般的に、同期プリミティブの重要な性質は、自身の破棄を同期させることができる、ことにあります。例えば、オブジェクトにそれを保護するミューテックスと参照カウントが埋め込まれている場合、スレッドがミューテックスを獲得し、参照カウントをデクリメントし、参照カウントが0になったことを確認し、ミューテックスを開放し、オブジェクトを破棄する、ということは有効でなければなりません。

フェンスがこの保証を機能させるのに十分強力でない場合、relaxedアトミックアクセス+フェンスは突然この保証を失うことになります。これは一般に、メモリモデルの健全性を損ないます。

seq_cstフェンスをrelaxedアクセスの前後に挿入することでseq_cstセマンティクスを実現するということは非常に直感的であり、エッジケースを除けばほぼ正しいはずです。同様に、releaseストアをreleaseフェンス+relaxedストアに置換することは正しい、という明らかに正しいはずの主張も破綻します。結局、フェンスは基礎となるハードウェアの制限とは無関係な、奇妙な安全上の制限を持つことになってしまいます。

// 次のような参照カウントのデクリメントの例を
int count = x->refcount.fetch_sub(1, acq_rel);
if (count == 1) delete x;

// このように書き直すと微妙に正しくないことになる
atomic_thread_fence(release);
int count = x->refcount.fetch_sub(1, relaxed);
atomic_thread_fence(acquire);
if (count == 1) delete x;

ちょっとした同期処理を書くとき、その処理を呼び出した側がその同期をどのように使うかはわかりません。他人の用意した同期プリミティブを使用する場合、同期の保証がどのように実装されているかはわからず、安全に破棄するためにそれを知る必要は本来ありません。

この提案は、オブジェクト生存期間とアクセスタイミングに関する標準の制限を緩和して、これらの問題を解決しようとするものです。

ここではまだ文言まで定まっていませんが、広く一般的に制限を緩和するのではなく、アトミックアクセスのより限定的な状況(上記例のような)にのみ制限を緩和するようにしようとしているようです。

P3182R0 Add pop_value methods to container adaptors

標準ライブラリのコンテナアダプタに、値を取り出して返す.pop_value()を追加する提案。

標準ライブラリに用意されている3つのコンテナアダプタstd::stack, std::queue, std::priority_queueには、.pop()メンバ関数があります。その名前とは裏腹に、これらの関数はそれぞれのコンテナの先頭要素を削除しますがその値を返しません。

先頭の値は.top()もしくは.front()によって参照を取得し、必要ならコピー/ムーブによって他の場所へ退避させてから、.pop()を呼んで先頭要素を削除します。

そのため、C++においてこれらのコンテナアダプタを使用する際は、.pop()が取り除く値を返す場合に1行で済む処理を行うのに最低3行かかります。

class Widget {
  std::stack<int>      d_stack;
  std::array<int, 100> d_array;

  ...

  int foo() {
    // これは間違い
    return d_array[d_stack.pop()];

    // これが正解
    const auto top = d_stack.top();
    d_stack.pop();
    return d_array[top];
  }
}

この提案は、この3行を1行で行う.pop_value()メンバ関数を3つのコンテナアダプタに追加しようとするものです。

現在の.pop()がこうなっているのは主に2つの理由によります

  1. 効率性
    • .pop()は参照を返すことができず(呼び出し側では常にダングリング参照となるため)、値を返すとコピーコストが避けられない
  2. 例外安全性
    • ムーブが例外を投げる場合に例外安全を提供しきれない

1の効率性に関してはこの制約はC++03とかの時代のものであり、現在のC++ならば暗黙ムーブによってコピーコストは最小にできます。

// std::stackのpop_value()の実装例
value_type pop_value() {
  value_type x = std::move(c.back());
  c.pop();
  return x; // 暗黙ムーブ
}

このような実装ではさらに、NRVOによってreturn文における戻り値構築の際のムーブコンストラクタ呼び出しが省略され、その場合はvalue_typeのムーブコンストラクタは1回だけ呼ばれます(NRVOが行われない場合は2回)。

例外安全性の問題は現在でも厄介です。上記のような実装においてreturn文においてNRVOが実行されない場合に、戻り値構築のためのムーブコンストラクタが例外を投げると、強い例外安全保証を提供できなくなります。強い例外安全保証を提供する場合、コンテナの操作が例外を投げる場合その操作の呼び出し前まで状態が戻ることを保証しなければなりませんが、スタックまたはキューの状態を.pop_value()の呼び出し前に復元するには取り出したオブジェクトをコンテナの先頭に戻さなければなりませんが、そのためには直前で失敗したばかりのムーブ操作を再度実行する必要があります。

そのため、.pop_value()においては強い例外安全を保証できないため、値が失われることを受け入れなければなりません。

std::stackにおける実装の例。std::queueもほぼ同様になるとのことです。

value_type pop_value() {
  struct Guard {
    stack* __this;
    bool failure = false;
    ~Guard() noexcept(false) {
      if (!failure) {
        __this->c.pop_back(); // 先頭要素の削除
      }
    }
  } guard{this};
  
  try {
    return move(c.back());  // 先頭要素の取り出し
  } catch (...) {
    guard.failure = true;
    throw;
  }
}

この実装例では、要素取り出しとreturnを同時に行うことでムーブコンストラクタが呼び出される回数を最大1回まで削減しています。これによって、値が消失する可能性のあるポイントが一箇所だけになっています。

std::priority_queueの場合は他2つと大きく異なります。std::priority_queueの場合はその不変条件を損なうことなく先頭の要素をムーブすることができないため、まず基底のコンテナ上でstd::pop_heapを実行する必要があります(これはpop()の動作とほぼ同じ)。

この場合、現在の要素数に対してその対数を取ったものに近い回数の要素のムーブが起こり、2回目以降のムーブコンストラクタで例外が送出されるとstd::priority_queueの状態が壊れます。このリスクは現在でもpop()が同様の動作をするために存在しています。そのため、std::priority_queueではムーブコンストラクタが例外を投げうる場合に本質的に安全ではありません。

現在でもtop() + pop()操作が強い例外安全を提供できていない以上、std::priority_queueに対してpop_value()の追加を行わない根拠はほぼありません。

std::priority_queueにおける実装例

value_type pop_value() {
    pop_heap(c.begin(), c.end(), comp); // 現在の先頭要素が末尾に来るようにヒープを再構成

    struct Guard {
        priority_queue* __this;
        ~Guard() noexcept(false) {
            __this->c.pop_back(); // 末尾要素(元先頭要素)を削除
        }
    } guard{this};
    
    return move(c.back());  // 末尾要素(元先頭要素)の取り出し
}

要素型のムーブコンストラクタが例外を投げうる場合、std::priority_queueではpop_heapステップで例外が発生する可能性の方が高くなり、呼び出し側はどこで例外が発生したのかを知ることはできないため、仮にreturnで例外が発生したとしても状態に関する保証を提供する必要はありません。

なおこれらの例は概念実証であり、ムーブコンストラクタが例外を投げない場合の最適化などを妨げる意図があるわけではありません。それはおそくら文言によって許可するように示されます。

P3183R0 Contract testing support

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

契約注釈を行った関数に対してその契約条件のテストを行うことに需要があり、SG21でもそれについて議論されてきました。現在のContracts MVP仕様ではその方法として、違反ハンドラをカスタマイズしてそこから例外やlongjmp等によって契約違反をハンドラするという方法が提供されています。ただし、例外はnoexcept指定された関数をテストすることができず、longjmpは引数を正しく破棄できないためこちらも問題があります。

この提案では、そのための専用の関数を追加することで、関数本体を実行することなくその契約条件だけを事前事後条件を分離してテストできるようにすることを提案しています。

提案しているのは次の2つの関数です

// 事前条件のテスト
template<auto F, typename... Args>
bool check_preconditions(Args&&... args);

// 事後条件のテスト
template<auto F, typename R, typename... Args>
bool check_postconditions(const R& r, Args&&... args);

どちらの関数も対象の関数はNTTPで渡し、関数引数ではその引数や戻り値を渡します。そして、それぞれ指定された関数の事前条件(pre())か事後条件(post())だけを実行してその結果をbool値で返します。

このような関数は普通に実装できないため、コンパイラマジックによって実装することを意図しています。

提案より、使用例

void f(int x) pre(x > 0) post(r: r >= 0) {
  return sqrt(x);
}

// Check that the pre condition is correct.
CHECK(check_preconditions<f>(1));
CHECK(!check_preconditions<f>(0));
CHECK(!check_preconditions<f>(-1));

// Check post condition
CHECK(check_postconditions<f>(1, 0));
CHECK(check_postconditions<f>(0, 0));
CHECK(!check_postconditions<f>(-1, 0));

このCHECKは、各種テストフレームワークが備えているテスト用マクロのようなものだとします。

この方法だとオーバーロードを処理できませんが、その場合は渡す関数名をstatic_castすることで特定のオーバーロードを指定することができます。提案している2つの関数内で、受け取っている関数引数から自動的にオーバーロード解決を行うようにすることは可能ですが、それはもはや関数テンプレートとは異なるものになるため、新キーワードによる言語機能にした方がいいとしています。この提案では相対的に作業が重くなるそちらのアプローチを避けています。

P3187R1 remove ensure_started and start_detached from P2300

P2300からいくつかの機能を削除する提案。

この提案では次の2つのことを提案しています

  1. P2300から、ensure_startedstart_detachedを削除する
  2. P2300から、execute()関連機能を削除する

ensure_started

senderは一般的に、どの時点でも安全に破棄可能であると仮定されています。senderを構成するアルゴリズムでは、例外やアルゴリズムの短絡動作のために子の(後続の)senderの接続/開始が保証されないことが一般的です。

ただし、ensure_startedはバックグラウンドで非同期的に実行されている可能性のある作業を表すsenderを返します。ensure_startedが返したsenderが接続/開始されずに破棄された場合にそのsenderが表す既に開始されている非同期作業に何が起こるのかを指定する必要があります。この場合の対処方法として次の4つの戦略が考えられます

  1. 非同期作業が完了するまでそのデストラクタの実行をブロックする
  2. 非同期作業から切り離し、バックグラウンドで完了するまで実行させる
    • 使用されているリソースを安全に開放できるタイミングが必ずしも分からなくなるので、クリーンなシャットダウンの実装が困難になる
  3. UBにする
  4. std::threadがしているように、プログラムを終了させる

現在のensure_startedはこのうち2の戦略を取っていますが。これは全て良くない戦略の中でも一番悪いものです。

返されるsendersenderアダプタによって他の作業に合成されている可能性が高く、ほとんどの場合は機能するものの、失敗コードパスでのみ稀に明確ではないデータレースを引き起こすことがあるため、ensure_startedの利用は危険となります。

start_detached

start_detachedsenderを返さないensure_startedです。そのため、これにもensure_startedと同じ問題があります。ただし、誤解の元となるような戻り値を返さないためいくらか危険性は低くなります。

それでも、リソースを安全に破棄するためにはデタッチされた作業に参加するための他の帯域外メカニズムが必要になります

execute()

execute()scheduleを渡すことのできるstart_detachedです。そのためstart_detachedと同様の問題があります。

これらのリソース安全性に関わる問題に加えて、いずれの機能についてもP3149で提案されているasync_scopeを用いて書き直すことができます

// これは
std::execution::ensure_started(sender);

// async_scopeによってこう書ける
std::execution::spawn_future(sender, scope);


// start_detached()の場合
std::execution::start_detached(s);

std::execution::spawn(s, scope);


// execute()の場合
std::execution::execute(sched, fn);

std::execution::spawn(std::execution::then(std::execution::schedule(sched), fn), scope);

spawn_future()の返すsenderが接続/開始されることなく破棄された場合(上記の例のような場合)、停止要求を発行した後でそのsenderは非同期作業から切り離され、非同期作業はその後バックグラウンドで継続します。

counting_scopeのオブジェクト(scope)のscope.join()を使用することで、切り離された作業の完了と後で結合することができ、これによって呼び出し元は開始された作業が停止してリソースが使用されなくなるまで待機することができるため、リソースの破棄を安全に作業の完了後に行うことができます。

回避しづらい危険性があり、回避策および代替機能も別に提案されているため、これら3つの機能を削除しようという提案です。

P3188R0 Proxy: A Pointer-Semantics-Based Polymorphism Library - Presentation slides for P3086R1

P3086の解説スライド。

P3086で提案されている仮想関数によらない実行時ポリモルフィズムを実現するためのライブラリ機能について、その動機や機能性などについて詳しく述べられています。

P3189R0 Slides for LEWG presentation of P2900R6: Contracts for C++

LEWGのメンバーに向けて、P2900R6時点のContracts MVPの仕様について説明するスライド。

LEWGのメンバに向けてということで、言語機能の詳細には踏み込まずに主に標準ライブラリのAPIについての部分に詳しく触れています。

P3190R0 Slides for EWG presentation of D2900R7: Contracts for C++

EWGのメンバーに向けて、P2900R6時点のContracts MVPの仕様について説明するスライド。

↑のスライド資料(P3189)の完全版です。読めば現時点でのC++ Contaractsを知ることができます。

P3191R0 Feedback on the scalability of contract violation handlers in P2900

LLVM/libc++におけるC++コードのセキュリティ向上のための取り組みからの、Contracts MVP仕様へのフィードバック。

LLVMでは、最近までの数年間にわたって、C/C++コードベースのセキュリティ向上を図るための様々な取り組みを進めていました。例えば-fbounds-safetyというポインタの実行時境界チェックを行うオプションや、libc++の堅牢化(hardeningフレームワークの開発などです。

これらの取り組みに共通することは、言語機能とライブラリ機能の両方において様々な事前条件を実行時に検査する方法を見つけて実装することであり、実装方法が異なっても(コンパイラによる任意箇所への挿入か、ライブラリマクロの手動挿入)、事前条件が満たされなかった場合にするべきことは同じです。

したがって、LLVMでは契約違反のハンドリングメカニズムを既存のコードベースに大規模に実装したうえでいくつもの実製品コードベースに展開した経験があります。その中には、コードサイズとパフォーマンスについて厳しい要件を持っているコードベースもあります。そのようなコードベースにおける要求こそが、大半のC++ユーザーが必要とする物の代表であると思われます。

この提案は、C++契約仕様がよりスケーラブルに設計されるために、LLVMにおけるこれらの取り組みで得られた経験をフィードバックとして提供するものです。

P2900のContracts MVP仕様は契約違反ハンドラのメカニズムを契約注釈のセマンティクスや違反ハンドラのカスタマイズによって柔軟に指定していることで多くの要件に応えている一方で、コードサイズやパフォーマンスへの影響を最小限に抑えて契約違反を取り扱う要求には対処できていません。そのためこの提案では、LLVMにおける取り組みの過程で得られた最も厳しいコードサイズ要件について紹介しています。

  1. 契約違反ハンドラは分岐と__builtin_trap()に相当するもの以外のコードを生成してはならない
    • 例えば、weakシンボルな違反ハンドラの呼び出しやstd::abort()の呼び出し、述語の周囲の例外処理コード、などを生成してはならないことを意味します
  2. 実行時に、std::contract_violationオブジェクトは都度生成する必要は無いはず(静的ストレージにあるかに関わらず)
    • これはコードの肥大化を回避するために重要
    • 静的ストレージにそのようなオブジェクトを置いておくことは、契約述語それぞれについてRTTIのような構造を生成することに似ており、スケールしない
  3. 合理的なユーザーエクスペリエンスを提供するためには実装が契約違反に関する情報を保持できる仕組みが必要だが、その情報は実行ファイル中に保存されている必要は無い
    • LLVMにおける取組においては、デバッグ情報の中にそれを保存している

提案では、現在の契約違反時処理と違反ハンドラ実装がこれらの要件を満たしていないことを説明し、libc++の堅牢化フレームワークがどのようにこれを満たしているのかを比較しています(提案3~4章、ここでは省略)。

そのうえで、次の事を提案しています

1. すぐに(ほぼ何も考えずに)使用可能なスケーラブルな契約セマンティクスのサポート

上記要件に沿うような契約違反の処理方法は最も基本的かつ重要なユースケースであり、実装定義のセマンティクスに委ねるのではなく標準で用意てしておくべきです。このために必要なことは新しい契約セマンティクスを追加する事だけです。

違反ハンドラが基本的に高度なロギング機能であると思うことにすると、P2900の3種類のセマンティクスは次のように分類されます

セマンティクス ロギングを行う プログラムを終了する P2900
ignore no no ある
observe yes no ある
???? no yes ない
enforce yes yes ある

この提案では、この表で不足している行に対応するセマンティクスを追加することを提案しています。これは、契約違反が起きた時にすぐにプログラムを終了させ、違反ハンドラを呼び出さないものです。

2. 契約述語からの例外送出

これはスケーラビリティの要件から外れている側面です。実際には契約述語において例外が送出されることは非常に稀であると思われますが、P2900の現在の仕様ではコンパイラは契約述語の周囲にtry-catchブロックを生成する必要があり、これはコードの肥大化を招きます。

これについて取れる方法は

  • 契約述語はnoexcept関数のみを指定できる
  • 契約述語をnoexceptコンテキストで評価し、例外が送出されたらプログラムを終了する

3. プログラムの終了のためにstd::abort()を使用しない

P2900の現在のバージョンでは、契約違反ハンドラ呼び出し後のプログラム終了のためにstd::abort()の使用を義務付けています。しかし、場合によってはstd::abort()を使用せずにプログラムを終了したい場合があります。

例えば、プログラムの状態が悪意あるものによって侵害されている場合、簡単に上書き可能なメカニズムを使用することは望ましくありません。

また、std::abort()は標準ライブラリ関数であるため、言語機能からそれを呼び出すことは実際には困難です。実装では最終的に__builtin_abort()を使用することになりそうですが、これはCライブラリにリンクするときに_abortシンボルへの呼び出しを生成します。これはabort()などの関数をヘッダでインライン関数として定義する一部のフリースタンディング環境で役に立ってない。

この提案では、契約違反に関わるプログラムのの終了においては、実装定義の方法とすることを提案しています。

4. 契約違反ハンドラの置換可能性

ユーザー提供の違反ハンドラはプログラムの全体で使用される必要があるため、リンク時に提供する必要があります。ただし、リンク時のカスタマイズは扱いがかなり難しく、共有ライブラリが自分のコード内でのみoperator newオーバーロードしておこうとしてプログラム全体でオーバーロードしてしまうという事がよくあります。

この提案では、リンク時のカスタマイズは基本的に辞めることを推奨していますが、C++にはこの問題を解決するのに有用なツールが無いため、ここでは具体的な提案を挙げてはいません

P3192R0 LEWGI/SG18 Presentation of P3104R1 Bit Permutations

P3104R0で提案されている新しいビット操作ライブラリ機能の解説スライド。

おそらくLEWGのメンバに向けてプレゼンするためのものです。

P3194R0 LEWGI/SG18 Presentation of P3105R1 constexpr std::uncaught_exceptions()

P3105で提案されているstd::uncaught_exceptions()constexpr化について解説するスライド。

おそらくLEWGのメンバに向けてプレゼンするためのものです。

P3196R0 Core Language Working Group "ready" Issues for the March, 2024 meeting

3月に行われた東京会議でWDに適用されたコア言語に対するIssue報告の一覧。

P3197R0 A response to the Tokyo EWG polls on the Contracts MVP (P2900R6)

Contracts MVPに対してEWGで行われた投票の結果を受けて、SG21の行動をまとめた文書。

2024年3月に東京で行われたWG21ミーティングにおいて、Contracts MVP提案がSG21からEWG/LEWGに転送され、そこでレビューが開始されました。初回の機能の説明の後でEWGにおいていくつかの投票が行われており、この文書はその結果を受けてSG21の次の行動を示すものです。

この提案では、次にとるべき行動を次の3つのバケットに分類しています

  1. MVPが間違っている可能性があり、SG21で議論する必要がある
  2. SG21はMVPが正しい解決策だと確信しているが、EWGで懸念を抱いている人々を説得するにはさらに作業を行う必要がある
  3. 現時点ではSG21での作業は必要ない

EWGの投票とバケット分類

  1. 契約チェックのセマンティクスはenforce(常にチェック、違反はハンドラ呼び出し)のみ
  2. 定数式における契約チェックのセマンティクスはenforceのみ
  3. MVPに仮想関数に対する契約注釈を含める必要がある
  4. MVPに関数ポインタに対する契約注釈を含める必要がある
  5. MVPにコルーチンに対する契約注釈を含める必要がある
  6. 違反ハンドラからの例外送出を許可しない
  7. 1回の関数呼び出し毎に、1つの事前・事後条件及びアサーションが複数回評価されないようにする
  8. 契約機能は通常のC++コードに比べてUBを少なくする
  9. 標準に契約機能を追加するよりも前に、標準ライブラリにおける何らかの使用経験が必要
  10. 標準に契約機能を追加するよりも前に、標準ライブラリで契約機能の使用を指定するべき

文書にはさらに、EWGでの投票結果とその理由についての説明が記されています。

バケット事に分類すると次のようになります

  1. バケット1 : SG21で議論し直す
    • MVPに仮想関数に対する契約注釈を含める必要がある
    • 違反ハンドラからの例外送出を許可しない
    • 1回の関数呼び出し毎に、1つの事前・事後条件及びアサーションが複数回評価されないようにする
    • 契約機能は通常のC++コードに比べてUBを少なくする
  2. バケット2 : EWG説得のために追加作業
    • 定数式における契約チェックのセマンティクスはenforceのみ
    • 標準に契約機能を追加するよりも前に、標準ライブラリで契約機能の使用を指定するべき
  3. バケット3 : 何もしない
    • 契約チェックのセマンティクスはenforce(常にチェック、違反はハンドラ呼び出し)のみ
    • MVPに関数ポインタに対する契約注釈を含める必要がある
    • MVPにコルーチンに対する契約注釈を含める必要がある
    • 標準に契約機能を追加するよりも前に、標準ライブラリにおける何らかの使用経験が必要

P3198R0 A takeaway from the Tokyo LEWG meeting on Contracts MVP

Contracts MVPに変更を適用する可能性のあるLEWGからのフィードバックのリスト。

前項の提案同様に、2024年3月に行われた東京会議においてContracts MVP提案はLEWGにも転送され、そこで初期のフィードバックを得ました。この文書はそのうち、MVPに変更を加える必要がある可能性があるものをまとめたものです。

  1. contract_kind等列挙型の基底型の指定が冗長(intを指定するならば省略すべき)
    • 提案 : 基底型指定の削除
  2. 契約違反と述語が例外を投げた場合の検出の混同
    • 述語の評価が例外を投げた場合でも違反ハンドラが呼び出されるが、これは契約違反とは異なる
    • 提案 : "contract violation"の代わりに"contract verification failure"を使用する
    • 提案 : あるいは、別のハンドラを使用する
  3. 名前空間と一部の修飾名にcontractが重複している
    • std::contracts::contract_violationstd::contracts::contract_kind
    • 提案 : contracts名前空間を削除する
    • 提案 : あるいは、プリフィックスcontractを別のものに変更する
  4. invoke_default_contract_violation_handlerが長すぎる、invoke_に意味がない
    • 提案 : 名前の変更
  5. 名前空間を追加する根拠を示す
    • 標準ライブラリでは基本的に名前空間のネストを避けており、それに従わないためには根拠が必要
    • 行動 : 根拠を示す(文書を参照)
  6. 別の評価セマンティクスの追加
    • パフォーマンスのために、違反ハンドラ機構が介在しない評価セマンティクスが必要
      • 契約チェックは実行時に行われるが、違反時に違反ハンドラは呼び出されずプログラムは停止する
  7. 例外送出の検出はコストが高すぎる
    • 述語のチェックにtry-catchブロックを導入しなければならないことはコードの肥大化を意味する
    • 提案 : 述語が例外を投げる場合はterminate()を呼び出す
    • 提案 : あるいは、述語がnoexceptであることを要求
    • 提案 : あるいは、通常通りに例外を伝播する
    • 提案 : あるいは、terminate()を呼ぶか違反ハンドラを呼ぶかを実装定義(オプションで変更)とする
  8. 実装にstd::abort()の使用を強制しない
    • パフォーマンスに影響があり、ライブラリシンボルのインポートが必要になり、SIGABORTのハンドルなどのアクションが必要になる
    • 提案 : 実装定義の方法で終了するように戻す
  9. 違反ハンドラの置き換えに対するODRの懸念
    • カスタム違反ハンドラのインストールに必要なメカニズムが悪意のあるODR違反を引き起こす可能性がある
    • 提案 : 違反ハンドラのインターフェースを保持するものの、違反ハンドラのインストール方法は実装定義とする
  10. std::contracts名前空間内の名前にcontract(s)を使用しない

P3199R0 Choices for make_optional and value_or()

optional<T&>make_optional()value_or()が何を返すべきかについての考察をまとめた文書。

P2988で提案されているstd::optional<T&>は、既存のoptional<T>を部分特殊化する形で導入される予定です。そのため、そのAPIも既存のstd;;optional<T>のものを踏襲しようとしています。その際にmake_optional()value_or()の戻り値型についてが議論の的となっています。

現在のstd::make_optional<T&>(...)は、std::optional<T>を返します。そのため、これをstd::optional<T&>を返すようにしてしまうと暗黙的に戻り値型が変更されることで既存のコードが壊れる可能性があります。

#include <optional>

struct MyStruct {};

MyStruct& func();

// 右辺値から構築する
static_assert(std::is_same_v<
                  std::optional<MyStruct>,
                  decltype(std::make_optional(
                      MyStruct{}))> == true);

// 左辺値参照から構築する
static_assert(std::is_same_v<
                  std::optional<MyStruct>,
                  decltype(std::make_optional(
                      func()))> == true);

// 全て現在コンパイルが通る

std::optional<MyStruct> r1 =
    std::make_optional<MyStruct>(func());

std::optional<MyStruct> r1a =
    std::make_optional<MyStruct&>(func());

std::optional<MyStruct> r1b =
    std::make_optional(func());


std::optional<MyStruct> r2 =
    std::make_optional<MyStruct>(
        std::move(MyStruct{}));

std::optional<MyStruct> r2a =
    std::make_optional<MyStruct&&>(
        std::move(MyStruct{}));

std::optional<T&>は右辺値から構築できないのでダングリング参照となる危険性はありませんが、この例のfunc()から構築している例ではコンパイルが通ったうえで戻り値型が変わります。そのような場所で現在でもmake_optional<T&>としている意図は明らかではありませんが、そうしているコードが想定でき、それがきちんと動作しているため、それを変更してしまうことは危険な可能性があります。

value_or()optionalが無効値を保持している場合に指定された値を返すもので、現在のstd::optional<T>Tを返しています。P2988の当初の提案はT&を返すようにしていましたが、これが問題となっているようです。

この文書では既存の実装を調査しており、結果は次のようになりました

実装 動作
標準 optional<T>::value_or returns a T
Boost optional<T>::value_or returns a T
optional<T&>::value_or returns a T&
TL optional<T>::value_or returns a T
optional<T&>::value_or returns a T
Flux returns result of ternary, similar to common_reference
Think-cell returns common_reference, with some caveats

TでもT&でもなく、common_referenceを返す選択肢が浮かび上がりました。

ここでは最終的に、次のようにまとめています

  • make_optional() : 何もしないことを推奨
    • make_optional<T&>(...)optional<T&>{...}で十分なはず
  • value_or() : common_referenceを返すことを推奨
    • あるいはTを返す
    • それか、value_orを削除する

LEWGの議論では、value_orに関してはTを返すことに弱い合意が取れたようですが、反対している人も多く、モチベーションが足りない可能性があるとしています。

P3201R0 LEWG [[nodiscard]] policy

P3201R1 LEWG [[nodiscard]] policy

LEWGの[[nodiscard]]付加に関するポリシーについて、SD-9に対する文言の提案。

これは、以前のP3262R0やP3122R1で提起されたポリシーに関する議論を受けて、最終的に決定されたポリシーそのものの提案です。

このポリシーでは、標準ライブラリの関数には[[nodiscard]]を付加しない、ことを基本としています。

この提案はすでにLEWGの投票を経て、SD-9にマージされています。

P3203R0 Implementation defined coroutine extensions

std::coroutine_handleのユーザーによる特殊化を許可する提案。

コルーチンフレームの実装は主要な実装(clang, GCC, MSVC)においてほぼ同じ実装になっており、与えられたpromise_typeに対して次のようになっているようです

struct coroutine_frame {
  void (resume *) (coroutine_frame * );
  void (destroy *)(coroutine_frame * );

  promise_type promise;
  
  // 関数の引数などの補助データがここに配置される
};

このコルーチンフレームはコルーチン開始時に確保されたメモリ領域に保存され、コルーチン呼び出し側とコルーチン本体の間で共有されます。そして、std::coroutine_handleはコルーチン呼び出し側からこれを参照して最低限の操作を行うインターフェースとなっています。

std::coroutine_handle.resume().destroy()はコルーチンフレームに保存された対応する関数ポインタを呼び出し、.promise()はコルーチンフレームのpromiseメンバ変数への参照を返し、.done()はコルーチンフレームのresumeメンバがnullptrかどうかを返します。

std::coroutine_handleはこのコルーチンフレームへのポインタを保持しており、それを経由してこれらの操作を実現しています。また、std::coroutine_handle::from_promisestd::coroutine_handle::from_addressを使用して、プロミス型オブジェクトやコルーチンフレームのアドレスからstd::coroutine_handleを作成することができます。

すなわち、コルーチンフレームをユーザー定義することが実質的に可能であり、かつそれを通常のコルーチンのコードに載せることも可能です。その場合、通常のコルーチンの起動手順を省いてコルーチンフレームやプロミス型を直接構築することができるため、コルーチン起動に伴う動的確保等のオーバーヘッドを削減できる可能性があります。

この提案の著者の方はBoost.cobaltというライブラリを作成しており、そこで

ただし、現在のところstd::coroutine_handleのユーザーによる特殊化は許可されておらず、コルーチンフレームは実装詳細でありユーザーがカスタマイズすることを許可してはいません。

この提案は、実装毎にABI互換のある形でのユーザー定義コルーチンフレームと、そのためのユーザー定義std::coroutine_handle特殊化を許可するために、標準の規定を修正してそれらがUBにならないようにしようとするものです。

この提案では、まずstd::coroutine_handleクラスに対する次の規定

プログラムがcoroutine_handleの明示的または部分的な特殊化を宣言する場合、その動作は未定義

を次のように変更します

プログラムがcoroutine_handleの明示的または部分的な特殊化を宣言する場合、その動作は実装定義

次に、std::coroutine_handle::from_address()の事前条件

addrfrom_address()の引数)は、coroutine_handleの特殊化である型のオブジェクトに対する以前の.address()の呼び出しによって取得されたものであること

を次のように変更します

addrは、明示的でも部分的でもないcoroutine_handleの特殊化である型のオブジェクト、もしくはnoop_coroutine_handleに対するに対する以前の.address()の呼び出しによって取得されたものであるか、前者によって提供される実装とABI互換性のあるメモリセクションを指していること

この2つの標準の規定のみの変更であり、現在未定義動作となっているもの実装定義にまで緩和するものです。しかし、この標準の規定のわずかな修正によって実装の作業なしで、Boost.cobaltのようなライブラリが許可されるようになり、C++コルーチンをより活用したライブラリの可能性が広がります。

P3205R0 Throwing from a noexcept function should be a contract violation.

noexcept関数からの例外送出時の対応について、EBとして契約違反ハンドラ呼び出しを行うようにする提案。

noexpcet関数から例外が送出される場合というのは、関数に付加された無条件noexceptという関数契約が破られている場合であるといえます。同時にそれは明らかに誤ったコードに起因する動作(erroneous behaviour)でもあります。そして、現在それが起きた場合の動作というのは「そういうことはしないで!」という意思表明であり、noexceptという明示的な関数契約の一部が実際に破られることを防止する最後の防壁です。

P2795R5に始まり現在WG21では、より一般的なEBを分類してどう対処すべきかを考えるための作業が行われているようです。実際に、[except.terminate]にあるプログラムを終了させるコードの実行の一覧を見てみると、どれも何かしらの誤ったコードに起因する動作を指定しているように見えます。

この提案は、その議論の一環としてnoexpcet関数からの例外送出というケースを加えようとするものであり、かつ契約違反ハンドラを誤ったコードに起因する動作が起きた場合にどう対応するかをユーザーに委ねるための機能であるとみなして、誤ったコードに起因する動作(EB)をハンドリングするメカニズムとして使用しようとするものです。

この提案では、その最初の1つとして、noexpcet関数から例外が送出された場合の動作をEBとして分類して、それを関数のエピローグにおける契約違反であるとして扱うことを提案しています。

疑似コードで示すと、この提案の変更は次のようなものです

現在 この提案
int halve(int x)
  interface {
    try {
      implementation;
    } catch (...) {
      std::terminate();
    }
  }
{ return x/2; }
int halve(int x)
  interface {
    try {
      implementation;
    } catch (...) {
      contract_assert(false); // EB
      std::terminate(); // fallback
  }
}
{ return x/2; }

ここでのcontract_assertのようにEBを暗黙の契約違反として扱う場合、既存の3(+1)つのものではそれを達成できないため、P3100R0で提案されているimplicit(暗黙契約違反)セマンティクスのようなものを使用する必要があります。なお、ここのセマンティクスがignoreになる場合、現在の動作にフォールバックしてプログラムが終了されます。

誤ったコードに起因する動作(EB)は診断可能な誤動作であるもののWell-definedな動作でもあります。これは、各EB毎に特定のWell-definedなフォールバック動作が指定されていることを意味しています。このフォールバック動作は、EBが発生した(発生しうる)場合に実装がその代わりに実行するWell-definedな動作です。

ここでの提案においてのそのようなフォールバック動作とはstd::terminate()呼び出しであり、EBを契約違反として扱えば契約チェックがなされていない場合は常にstd::terminate()が呼び出されます(現在の動作と同等になる)。

また、この提案では同様にEBとして扱うべき現在プログラムを終了させている動作について大きく次のようなカテゴリに分類しています

  1. 例外ハンドラ呼び出し前の例外送出
    • 例外オブジェクト初期化完了後、例外ハンドラを呼び出す前に例外介して終了する関数を呼び出す場合
    • 例外が値によってキャッチされ、そのコピーコンストラクタが例外を送出する場合など
  2. スローされた例外にマッチするハンドラが見つからない場合
    • 例外ハンドラ不足をEBとして診断できると便利かもしれない
  3. noexcept関数からの例外送出
    • 例外ハンドラの検索が、例外を送出しないと宣言されている関数のもっとも外側に到達した場合
    • noexcept指定とは関数が例外を送出しないという意味ではなく、例外ハンドラの検索限界を指定するものであることを示唆している
  4. スタック巻き戻し中の例外送出
  5. 静的初期化
    • 静的/スレッド記憶域機関を持つオブジェクトの初期化が例外で終了する場合
    • スコープが無い場合に何を巻き戻すのか?
  6. 静的オブジェクトの破棄
    • 静的/スレッド記憶域機関を持つオブジェクトの破棄が例外によって終了する場合
    • 上と同様
  7. コールバック呼び出しのクリーンアップ
    • std::atexit等のコールバックが例外で終了する場合
    • これらのハンドラには本来noexpcetが必要だが、それが間に合わなかったため、これをEBにする必要がある
  8. 何も送出しないthrow;
    • オペランドなしのthrow式が例外を再スローしようとするものの、ハンドルされている例外が無い場合
    • 現在実行時エラー、EBにすべき
  9. ラップされた例外をスロー市内
    • 例外をキャプチャしていないオブジェクトに対してstd::nested_exception::rethrow_nested()が呼ばれたとき
    • 上と同様
  10. スレッドの初期化
    • スレッドの初期関数(実行する関数ではない)が例外によって終了した場合
    • 元に戻す方法が分からない
  11. 並列アルゴリズム
    • 並列アルゴリズムの要素アクセス関数が例外によって終了する場合
    • 並列分岐した例外を統合する方法が分からない
  12. joinableなスレッド
    • std:threadのオブジェクトのデストラクタ/ムーブ代入演算子が呼ばれた時、そのオブジェクトがjoinable() == trueなスレッドを参照している場合
  13. wait系関数の事後条件
    • condition_variablewait(), wait_until(), wait_for()が事後条件を満たさない場合

これらのものはいずれも、現在std::terminate()を呼び出してプログラムを終了させています。ここでは提案までしていないものの、これらのものも同様にEBとして扱うようにして、現在の動作をフォールバックとし、契約違反ハンドラによってハンドリング可能にすることを意図しています。

P3207R0 More & like

標準ライブラリの参照セマンティクスを持つ型の振る舞いを、より参照に近づける提案。

ここで提案している変更の一つ目はstd::function_refです。これに対して、アドレス取得演算子&)をdelete定義し、現在のfunction_reffunction_ptrに名前を変えて、function_refconst function_ptrエイリアスとして定義します。

// function_refは再代入可能(参照するものの変更が可能)なため、function_ptrにリネーム
template< class >
class function_ptr;

template< class R, class... Args >
class function_ptr<R(Args...) noexcept>
{
public:

    function_ptr() = delete;

    constexpr function_ptr(const function_ptr& other) noexcept = default;
    constexpr function_ptr(function_ptr&& other) noexcept = default;
    constexpr function_ptr& operator=( const function_ptr& ) noexcept = default;
    constexpr function_ptr& operator=( function_ptr&& other ) noexcept = default;
    
    ~function_ptr() noexcept = default;

    // 参照型への参照を防止するために、アドレス取得を禁止
    function_ptr* operator&(void) = delete;
    function_ptr const *operator&(void) const = delete;
    function_ptr volatile *operator&(void) volatile = delete;
    function_ptr const volatile *operator&(void) const volatile = delete;

    // 欠けているコンストラクタ : 効率性のため
    constexpr function_ptr(void* s, R (*f)(void* state, Args...) noexcept) noexcept
     : state{s}, thunk{f} {}

    R operator()( Args... args ) const noexcept
    {
        return thunk(state, std::forward<Args>(args)...);
    }
private:
    void* state;
    R (*thunk)(void* state, Args...) noexcept;
};

// 参照型はデフォルトでconstであるべき、より&らしく(参照そのものを変更できない)
template< class T >
using function_ref = const function_ptr<T>;

アドレス演算子を削除することで、参照型はコピーで渡すべきであることがより明確になり、ダングリングを発生させる可能性のある操作を制限することができます。またこのことは一般的な参照とも一致しており(参照そのもののアドレスを取れない)、もし&を残すならば参照先のアドレスを返すようにすべきです。

左辺値参照そのものは不変であり、参照そのものはT* constの様なポインタ(ポインタそのものがconst)と似ています。参照もconstポインタもそのものに再代入(参照の変更)はできず、コンパイル時に指したもの以外のものを指すことはありません。function_refのデフォルトはconst function_ptrにしておくことでこの振る舞いに近づけることができ、constが組み込まれていることでconst忘れも防止できます。

int main() {
  function_ptr<int (int) noexcept> fp{nullptr,
      [](void* state, int i) noexcept { return test(i); }};
  
  // function_ptrは再代入可能
  fp = {nullptr, [](void* state, int i) noexcept { return test(i + 1); }};


  function_ref<int (int) noexcept> fr{nullptr,
      [](void* state, int i) noexcept { return test(i); }};
  
  auto a = &fr; // ng、削除されている。function_refは参照型である

  // functionr_refは言語参照のように再代入禁止であるべき
  fr = {nullptr, [](void* state, int i) noexcept { return test(i + 1); }};  // ng
  

  // function_ptrはfunction_refを代入できる
  fp = fr;
  
  std::vector<function_ptr<int (int) noexcept>> fps;  // ok
  
  std::vector<function_ref<int (int) noexcept>> frs;  // ng、正しくコンパイルエラー

  return 0;
}

ここで提案している変更の2つ目は、std::optional<T&>です。これはP2988で提案中のものでまだ標準には入っていません。これにも先程と似たような変更を加えます。正し、std::optional&delete定義は無いためそこは慎重な議論が必要である可能性がありますが、constデフォルトのエイリアスを追加する事には支障がないはずです。

// const baked in by default
template<class T>
using optional_ref = const optional<T&>;  // OR opt_ref

これによって、std::optional_refの振る舞いをconstポインタあるいは言語参照により近づけることができます

constポインタ optional_ref
int main() {
  int value = 0;
  optional<int&> op00{value};
  optional<const int&> op10{value};
  const optional<int&> op01{value};
  const optional<const int&> op11{value};
  op00 = op00;
  //op00 = op10;// error
  op00 = op01;
  //op00 = op11;// error
  op10 = op00;
  op10 = op10;
  op10 = op01;
  op10 = op11;
  //op01 = op00;// error
  //op01 = op10;// error
  //op01 = op01;// error
  //op01 = op11;// error
  //op11 = op00;// error
  //op11 = op10;// error
  //op11 = op01;// error
  //op11 = op11;// error
  return 0;
}
int main() {
  int value = 0;
  optional<int&> op00{value};
  optional<const int&> op10{value};
  optional_ref<int> op01{value};
  optional_ref<const int> op11{value};
  op00 = op00;
  //op00 = op10;// error
  op00 = op01;
  //op00 = op11;// error
  op10 = op00;
  op10 = op10;
  op10 = op01;
  op10 = op11;
  //op01 = op00;// error
  //op01 = op10;// error
  //op01 = op01;// error
  //op01 = op11;// error
  //op11 = op00;// error
  //op11 = op10;// error
  //op11 = op01;// error
  //op11 = op11;// error
  return 0;
}

この提案で直接提案しているのはこの2つだけですが、ほかにもstd::spanstd::string_viewstd::mdspanに対しても同じ変更を考えることができます。

P3208R0 import std; and stream macros

import std;した時でもストリームマクロを利用可能にするために、対応する非マクロのグローバル定数を追加する提案。

ストリームマクロとは、3種類のCの入出力ストリームstdin, stdout, stderrのことです。これらはマクロであるため、import std;によって導入されません。C++のライブラリには対応する非マクロのものが無いため、ユーザーはこれを利用するためにヘッダをインクルード/インポートしなければなりません。

stdモジュール使用時のこの不都合に対処するために、この提案はグローバル定数std::in, std::out, std::errとしてそれぞれのマクロに対応するものを追加しようとするものです。

constポインタ optional_ref
#include <cstdio>  // stderrのために
//import <cstdio>; // あるいは
import std;

int main() {
  std::println(stderr, ...);
}
import std;

int main() {
  std::println(std::err, ...);
}

std::print()はデフォルトでstdoutに出力するため、標準エラー出力に出力したい場合はstd::print(stderr, ...)を使用するのが合理的です。std::print(std::cerr, ...)も可能ですが、わずかにパフォーマンスで劣ります。

ここで提案している3つのグローバル定数は、std::FILE* const型の定数です。

調査によると、GCC/MSVCのストリームマクロの実体はFILE*であるため、この提案の実装は可能とのことです。

P3210R0 A Postcondition is a Pattern Match

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

P2688で提案されているパターンマッチの構文は次のようになっています

<subject> match { <pattern-output>; }

対象となる式(<subject>)を受け取って、<pattern-output>でそれについての値を生成します。

この式における<pattern-output>のセマンティクスは、戻り値を参照する事後条件における述語のセマンティクスと同じものだと見ることができます。すなわち、事後条件の述語とは戻り値に対するパターンマッチであるといえます。

従って、同じセマンティクスを持つものは同じ構文を取るようにすると、事後条件の構文は次のようになります

post(<pattern-output>)

これに対して、戻り値を参照しない事後条件の形式を次のような形式として導入できます

post(<expression>)

これ自体はMVPの構文と同等のものですが、この提案では、戻り値を参照する場合の事後条件の構文もこれと同等な、P2737R0で提案されていたもの(post(0 < result)、戻り値名としてresultを固定的に使う)に変更することを提案しています(現在のMVPの仕様だとpost(r : 0 < r))。

これにはP2737で主張されていた利点に加えて、パターンマッチに関して利点があるとのことです。

まず、この構文はパターンマッチ形式と式形式の間に単純で概念的な橋渡しを作成します。

// 通常の式による事後条件構文は
post(<expression>)

// 次のような構文と等価
post(let result => <expression>)

// これはより一般的な構文の特殊な場合とみなせる
post(<pattern-output>)

次に、post(0 < result)のような構文を採用しておくと、post(<pattern-output>)のような将来の拡張構文(post()の中にパターンマッチングのマッチング部分を直接書けるようにする拡張)を前方互換性を維持して導入できます。

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

// 例1: 戻り値なし関数の事後条件
int global;

void f()
  post(global == 42); // P2900と同等

// 例2: 値を返す関数の単純な事後条件
int f()
  post(result > 0); // P2737のもの

// 例3: 値を返す関数の複雑な事後条件
float f()
  post(let r => r*r*r + 2*r*r - 3*r + 4);

// 例4: 整数戻り値の事後条件
int f()
  post(
    0 => default_available();
    1 => true;
    _ => false);

// 例5: 文字列戻り値の事後条件
std::string f()
  post(
    "foo" => false;
    "bar" => true;
    let s => is_zipcode(s));

// 例6: tuple戻り値の事後条件
tuple<int,int> f()
  post(
    [0, 0] => true;
    [0, let y] => y < 10;
    [let x, 0] => x < 20;
    let [x, y] => x + y < 4);

// 例7: variant戻り値の事後条件
variant<int32_t, int64_t, float, double> f()
  post(
    int32_t: let i32 => i32 < (1 << 30);
    int64_t: let i64 => i64 < (1ll << 60);
    float  : let fl => fl < 1.0e48;
    double : let d => d < 1.0e96);

// 例8:conceptによる戻り値の事後条件
template<typename T>
T f()
  post(
    std::integral: let i => i < 100;
    std::floating_point: let f => f < 1.0;
    _: false);

// 例9: ポリモルフィックな戻り値の事後条件
struct Shape { virtual ~Shape() = default; };
struct Circle : Shape { int radius; };
struct Rectangle : Shape { int width, height; };

Shape& f()
  post(
    Circle: let [r] => r > 0;
    Rectangle: let [w, h] => w > 0 && h > 0);

この提案では結局、次の2点

  1. 事後条件の構文をP2737で提案されていたものに変更する
  2. パターンマッチングが導入されたのと同じバージョンで、post(α)result match { α; }と同じ効果を持つように変更する

を提案しています。

P3211R0 views::transform_join

views::transformviews::joinの合成であるRandeアダプタ、views::transform_joinの提案。

views::transform_joinは巷ではflatmapのように呼ばれているものに対応しており、シーケンスの各要素に対してviews::transformを適用した後、その結果となるシーケンスをviews::joinするものです。

これはP2760でTier1に挙げられている他、他の言語でもデフォルトで用意されており利用頻度が高い事、C++でもユーザーの入力の手間を減らせることなどから、C++でも標準で用意しておくことを提案しています。

ここで提案されている実装は、rng | views::transform_join(func)rng | views::transform(func) | views::joinと全く同じになるようにするものです。transform_join_viewのような内部実装クラスを用意して特殊対応するものではありません。

これは、transform_join_viewを実装しようとする非常に複雑でコストが高くなってしまい導入のメリットが薄くなる(join_viewが特に既に複雑であり、これをもう一度実装したうえでtransfoemの機能を加えなければならない)のに対して、既存のアダプタを2つ組み合わせるだけなら非常に低コストでありながら、join_viewがすでに実装している右辺値範囲のキャッシュ機構やborrwoed_range性の継承などの機能性と将来的にそれらに適用される機能強化を継承することができるためです。

提案より、ピタゴラス数を計算する例

// 全てのピタゴラス数を含む無限範囲を定義
auto triples = views::iota(1) | views::transform_join([](int z) {
  return views::iota(1, z + 1) | views::transform_join([z](int x) {
    return views::iota(x, z + 1) | views::transform_join([z, x](int y) {
      return views::repeat(tuple(x, y, z), x * x + y * y == z * z ? 1 : 0);
    });
  });
});

// 最初の100個を出力
for (auto triple : triples | views::take(100)) {
  println("{}", triple);
}

P3213R0 2024-04 Library Evolution Polls

2024年4月に行われる予定の、LEWGにおける投票の予定。

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

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

P3215R0 Slides: Thread Attributes as Designators (P3072R2 presentation)

スレッドに対する属性(追加のプロパティ)の指定方法として、指示付初期化を活用する方法(P3072の提案)についての紹介スライド。

P3216R0 views::slice

元の範囲の連続した一部分を切り出すRangeアダプタ、views::sliceの提案。

views::slicerng | views::slice(start_idx, end_idx)のようにして、入力範囲rngから[start_idx, end_idx)区間を切り出すRangeアダプタです。これはシンプルに、rng | views::drop(start_idx) | views::take(end_idx - start_idx)のように実装することができ、この提案でもこの実装での追加を提案しています。

slice_viewのようなものを提供しない理由としては、views::sliceの実装で行うことはviews::take/dropがすでに行っていることと全く同等になるためと、views::take/dropが現在行っている最適化(views::iotaspanへの特殊対応)及び将来の改善を自動的に継承できるためです。

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

auto ints = views::iota(0);
auto fifties = ints | views::slice(50, 60);
println("{} ", fifties); // prints [50, 51, 52, 53, 54, 55, 56, 57, 58, 59]

P3217R0 Adjoints to "Enabling list-initialization for algorithms": find_last

find_lastアルゴリズムにP2248の変更を適用する提案。

P2248は、引数として値を取るタイプのアルゴリズムに値を渡す際に、{}を使用できるようにするものです。これは2024年3月の東京会議においてC++26 WDへ採択されたようですが、そこからはfind_lastアルゴリズムが抜けていたようです。find_lastC++23で追加された比較的新しいアルゴリズムであるため、以前から進行していたP2248の対象からは抜けてしまったのでしょう。

この提案は、P2248で適用されたような変更をfind_lastにも適用するだけのものです。

struct point { int x; int y; };

std::vector<point> v;

v.push_back({3, 4}); // OK、point型の指定は不要

// このような値の渡し方が可能になる
std::ranges::find_last(v.begin(), v.end(), {3, 4});
erase(v, {3, 4});

P3218R0 const references to constexpr variables

定数へのconst参照の参照先を、暗黙的にstaticにする提案。

ここで提案されているのは、const参照が定数値に束縛される場合に、その定数値を暗黙にstatic定数とすることでその寿命を拡大させようとするものです。これによって、現在ダングリング参照を生成しているようなコードがそのまま安全になります。

struct point {
  int x;
  int y;
};

// ダングリング参照を容易に生成できる関数
const point& dangler(const point& p) {
  // ...
  return p;
}

// ダングリング参照を容易に生成できる関数
const std::string& dangler(const std::string& s) {
  // ...
  return s;
}

int main() {
  // {4, 2}はstaticになる
  const point& always_safe = {4, 2};
  const point& safe = dangler({4, 2});

  // "42"sはstaticになる
  const std::string& always_safe = "42"s;
  const std::string& safe = dangler("42"s);
}

この提案の対象とするような参照とその参照先は、現在未定義動作に陥っているものだけであり、それが自動的にUBではなく動作が保証されたものになります。また、この仕様は暗黙的に適用されますが、これを利用するには次の3ステップを踏まなければなりません

  1. (この提案の対象となる)型を定義するプログラマは、constexprコンストラクタを用意するなどして型がコンパイル時に構築される手段を用意している
  2. 変数または関数引数を宣言するプログラマは定数(const)が必要である、と宣言している
  3. 最終利用者となるプログラマは、変数や関数引数を定数値によって初期化した

暗黙に適用されるとはいえ、このようなconstの要求を3ステップ通過しているため、プログラマの意図と異なったことにはならないはずです。

また、参照によって引数を受け取る関数内からは、渡された参照の参照先がローカル変数なのかグローバル変数なのか、あるいはヒープにあるオブジェクトなのかといったことを認識する方法を持ちません。むしろ、関数引数を参照で取っている場合、その参照先は少なくともその関数の呼び出しの間中生存期間内にあることを期待しており、関数が引数の参照をそのまま返す場合はさらに長い生存期間を持つことを期待しています。そのため、この提案による暗黙static化はその期待を暗黙に叶えるものであり、プログラマの意図しない結果にはならないはずです。

さらには、このような定数値の暗黙的なstatic化というのは、奇抜で斬新なものではありません。P2752ではinitializer_listの裏側の配列を同様にstaticなものとして実装することを許可しており、P2752R3は既にC++26に取り込まれています。

P3220R0 views::delimit

入力の範囲を指定した値が最初に出現する位置を終端として切り出すRangeアダプタ、views::delimitの提案。

r | std::views::delimit(v)はちょうど、r | std::views::take_while([v](const auto& e) { return v != e; })と同じ範囲を生成します。しかしこの提案ではdelimit_viewを用意したうえでviews::delimitを追加することを提案しています。

その理由としてはまず、vをどう保存するべきかに選択があり、どちらを選んでも追加のオーバーヘッドが避けられないためです。

take_whileのラッパとして実装すると、[v](const auto& e) { return v != e; }のようにラムダ式に比較する値を保存するか、bind_front(ranges::not_equal_to, v)のようにするかの選択があります。これはどちらを選んでも追加の余分な関数呼び出しのコストを避けることができません。例えば後者を選んだ場合、invoke(bind_front(ranges::not_equal_to, v), *it) -> ranges::not_equal_to(v , *it) -> *it == vのような経路でようやく比較が行われることになり、コンパイラがこれを除去することもより難しくなります。

次に、関数呼び出し演算子に追加の制約を行わなければならなくなる点があります。

take_whileのラッパとしてラムダ式を用いると、[v = forward<V>(v)](const auto& e) -> bool { return e != v; }のような構築が可能である必要がありますが、評価されない文脈のラムダ式内部のエラーはハードエラーとなることから、キャプチャとreturn文について別の制約式の形で記述する必要があります。そのような複雑な制約は基底の文章量を肥大化させ、必要な制約を見えづらくします。

delimit_viewクラスの場合、ラムダ式を使用しないため制約は簡易になり、delimit_viewの制約によってそれを記述しておけばviews::delimitの制約はdelimit_viewが構築できるかを言うだけで良くなります。

最後に、views::delimitviews::take_whileで実装される場合、生成されたviewオブジェクトには比較する述語を取得する.pred()が含まれたままになりますが、これが返すものはviews::delimitの場合は未規定にするしかなく、実質的に意味がありません。

この提案では、これらの理由によりdelimit_viewクラスを用意したうえでtake_whileとは別のアダプタとしてviews::delimitを導入しようとしています。

// 終端を含まないnull終端文字列を得る例
const char* s = /* from elsewhere */;
const auto ntbs = views::delimit(s, '\0');
const char* one_two = "One?Two";
for (auto c : views::delimit(one_two, '?')) {
  std::print("{:c}", c);  // prints One
}

P3221R0 Disable pointers to contracted functions

契約がなされている関数のアドレス取得を禁止する提案。

現時点のContracts MVPでは関数ポインタに対する契約注釈は許可されていませんが、EWGレビュー時の投票やP3173R0などでその需要がありそうなことが確認されています。おそらくMVPおよび最初のContracts仕様には入らないと思われますが、将来の拡張の一つとしては想定されるものではあります。

従って、あとから追加する場合に問題が起こらないように今から対策しておくのは理にかなっています。現在からではそのような将来の契約に対応した関数ポインタがどのようになるかはわかりませんが、現在の関数ポインタが契約がなされた関数に対してどのように動作するのかは分かっています。

// P2900(Contracts MVP)の仕様

void fun(A& a) pre (a.ok());
using F = void(A& a);

F* f = fun; // ok

現在のままだと、関数ポインタへの変換に際して元の関数の契約情報はすべて失われます。これは、将来契約に対応した関数ポインタとして、関数ポインタ型に契約注釈を含むもの(noexceptのように)が登場した際に問題を引き起こします。

// 将来

void fun(A& a) pre (a.ok());
using F = void(A& a);
using G = void(A& a) pre(a.ok());

F* f = fun; // 危険
G* g = fun; // ok

現在の仕様をこのまま出荷してしまうとそれはもう変更できなくなるため、同じ関数を指す関数ポインタには契約条件を保存して安全なものと、契約条件をすべて忘れる(相対的に)危険なものとの2つが存在してしまいます。

しかし、今からそれをエラーにしておけば将来のこのような分断を回避することができます。

// この提案

void fun(A& a) pre (a.ok());
using F = void (A& a);

F* f = fun; // ng
F* g = +[](A&a){fun(a)}; // ok

この場合でも、この例のようにラムダ式による簡単なラッパによって安全に関数ポインタを取得することができます。

このような理由からこの提案では、契約注釈を持つ関数及び仮想関数のアドレスを取得する行為をill-formedとすることを提案しています。

SG21におけるレビューでは、この提案の変更には合意が得られなかったようです。

P3222R0 Fix C++26 by adding transposed special cases for P2642 layouts

std::linalg::transposedlayout_left_padded/layout_right_paddedのサポートを追加する提案。

std::linalg::transposed()は2次元行列のmdspanに対して、その転置行列となるmdspanを返す関数です。この転置は要素のコピーなどを伴うことなくレイアウトの調整によって行われます。

#include <mdspan>
#include <linalg>

template<typename T>
using mat33 = std::mdspan<T, std::extents<std::size_t, 3, 3>>;

template<std::formattable<char> T, typename L>
void print_33(std::mdspan<T, std::extents<std::size_t, 3, 3>, L> mat) {
  for (auto y : std::views::iota(0, 3)) {
    for (auto x : std::views::iota(0, 3)) {
      std::print("{} ", mat[y, x]);
    }
    std::println();
  }
}

int main() {
  int data[] = {
    1, 2, 3,
    4, 5, 6,
    7, 8, 9
  };

  mat33<int> m{data};

  print_33(m);

  std::println();

  auto mt = std::linalg::transposed(m);

  print(mt);
}
1 2 3 
4 5 6 
7 8 9 

1 4 7
2 5 8
3 6 9

std::linalg::transposed()はレイアウトポリシーがlayout_leftlayout_rightの場合、このレイアウトを相互に交換することで転置を行います。

  • layout_left::mapping<extents<int, 3, 4>>{}transposed()によってlayout_right::mapping<extents<int, 4, 3>>{}に変換される
  • layout_right::mapping<extents<int, 3, 4>>{}transposed()によってlayout_left::mapping<extents<int, 4, 3>>{}に変換される

layout_strideの場合はストライド計算が必要になりますが、これは単純に入力のストライドを逆順にすることで得られます。

// このストライド付きレイアウトはtransopode()によって
auto m = layout_stride::mapping<extents<int, 3, 4>>{
  extents<int, 3, 4>{}, array{2, 6}};

// こう変換される
auto mt = layout_stride::mapping<extents<int, 4, 3>>{
  extents<int, 4, 3>{}, array{6, 2}};

これ以外のケースでは、layout_transposeレイアウトクラスによって転置を実現しており、これは単純に与えられたインデックスの順序を入れ替えて元のレイアウトにおけるインデックスを計算するようにするものです。

transpose()が一部の型に対してこのような特殊対応を行っているのは、レイアウトがある特性を満たしている場合にBLASの最適化された実装にディスパッチすることができる場合があり、その条件の一部がレイアウトポリシー型によって表現されているためです。そのため、専用のレイアウトポリシー型を実装してしまうとその判定を妨げることになり、継承や透過的にしようとすると実装や規定が複雑化します。

このような条件には例えば次の3つがあります

  • is_unique() == true
    • 1つの要素にアクセスするインデックスの組が1組しかない
  • is_strided() == true
  • 少なくとも1つのストライドが1に等しい

layout_leftlayout_rightはこの条件をすべて満たしており、layout_strideは3つ目の条件を満たすかどうかは実行時に調べる必要があります。いずれにしても、標準のレイアウトポリシーは全てこのような条件をどこまで満たすかを型によって表現し、<linalg>実装は型によってそれをディスパッチすることができます。

P2642R3では標準のレイアウトポリシーとしてlayout_left_paddedlayout_right_paddedの2つが追加されました。これはlayout_strideの特殊な場合であり、その性質から上記の3条件を含めた最適化の要件をすべて満たしていることが分かっています。ただし、std::linalg::transposed()は現在のところlayout_left_paddedlayout_right_paddedに対して特殊な対応をしておらず、layout_transposeが使われてしまいます。

std::linalg::transposed()が既知のレイアウトに対して特殊対応をしない、例えばlayout_transpose<layout_left>のように実装されるとすると、これでも依然として実装が最適実装へのディスパッチすることは可能です。ただし、そうなると実装はBLAS互換レイアウトとそのlayout_transpose特殊化の2倍の候補をチェックする必要があり、ディスパッチを行うコードでは現在のstd::linalg::transposed()がやっているようなレイアウト計算を個別に行わなければならなくなります。さらに、ユーザーが独自で実装した線形代数アルゴリズム<linalg>に合わせる形で実装する場合、transposed()はレイアウトに最適な結果を返すことを期待するでしょう。それが叶わない場合、独自の転置関数を実装することになり、これがユーザーごとに行われることになります。

さらに、もしlayout_left_paddedlayout_right_paddedに対する特殊対応をC++26よりも後に行う場合、transposed()の戻り値型が観測可能であることからAPIの破壊的変更(戻り値型mdspanのレイアウト型が変わる)となります。

この提案は、これらの理由からstd::linalg::transposed()に対してlayout_left_paddedlayout_right_paddedの特殊対応を追加しようとするものです。

layout_left_paddedlayout_right_paddedはその意図からしBLASにおいてよく使用される既知のレイアウト(leading dimensionと呼ばれる)を効率的に表すためのレイアウトです。そのレイアウトは、左端/右端のストライドが1になり、その次の左端/右端のストライドが1以外になる、というもので、残りのストライドはこの値から計算されます。それによって、NTTPでその1にならない部分のストライドを指定することでストライド値をコンパイル時に既知にし、サイズを削減します。

これはlayout_strideの特殊な場合であるためそのストライド計算も同様にストライド順を逆にするだけです。しかも、layout_leftlayout_rightのように型レベルでそれを行うことができます。

  • layout_left_padded<PaddingValue>の転置はlayout_right_padded<PaddingValue>になる
  • layout_right_padded<PaddingValue>の転置はlayout_left_padded<PaddingValue>になる

また、オプションの提案としてユーザー定義のレイアウトでもtransposed()の結果を最適化するためにtransposed_mappingのようなカスタマイゼーションポイントを用意することを提案しています。これはsubmdspan()のためのレイアウトカスタマイゼーションポイントであるsubmdspan_mappingとほぼ同じように動作するもので、レイアウトマッピングクラスの入れ子型として定義しておくことでtransposed()における結果をカスタマイズすることを許可しようとするものです。

P3223R0 Making std::basic_istream::ignore less surprising

istream::ignore()の第二引数に負の値を与えた場合の動作を修正する提案。

istream::ignore(n, delim)は入力ストリームからn文字読み込むかdelimが出現する、まで入力を読み飛ばす関数です。2番目の引数delimには負の値を渡すことができますが、負の値を渡した場合はどの文字とも一致しません。これは、内部的に文字の比較がtraits_type::eq_int_type(rdbuf()->sgetc(), delim)で行われますが、rdbuf()->sgetc()は内部でtraits_type::to_int_type(c)を使用して変換を行うことで負の値を返すことが無いためです。

std::char_traits<char>::to_int_type(char c)は文字c(int)(unsigned char)cのようにして正の整数値に変換しており、これによってcharの符号有無を無視することができるようになり、(int_type)-1EOFとして予約することができます。ただし、ストリームの文字列比較が常に文字をこの関数で変換して扱うということは、文字の比較を行うような処理に対して(関数がその変換まで行わない限り)ユーザーはto_int_typeを使用して文字を渡さなければならないことを意味しています。

istream::ignore(n, delim)がまさにそのような処理であり、ストリームからの文字はto_int_type()で正の値に変換するのに対して、ユーザーの指定したdelimはそのような変換を行いません。それでも正の値に対しては問題なく動作しますが、負の値については全く正しく動作しません。

delimに負の値が来るのはcharが符号付である場合の環境のみ(x86-64環境)であり、かつASCIIの範囲外の文字です。ただし、UTF-8の2バイト目以降の文字としては有効な値です。そのような環境でそのような文字に対してistream::ignore()を使用する場合、ユーザーはstd::cin.ignore(n, std::char_traits<char>::to_int_type(c))あるいはstd::cin.ignore(n, (unsigned char)c)のように変換を行わなければなりません。

次のようなコードでそれを確かめることができます

#include <string>
#include <spanstream>
#include <cassert>

int main() {
  std::string input = "abcdef\U0001F607ghijk";
  // \U0001F607 = 😇︎ = u8"\xF0" u8"\x9F" u8"\x98" u8"\x87"

  {
    std::spanstream strm{input, strm.in};
    strm.ignore(std::numeric_limits<std::streamsize>::max(), '\xF0');
    std::println("{:s}", strm.eof()); // true、最後まで読み飛ばす
  }
  {
    std::spanstream strm{input, strm.in};
    strm.ignore(std::numeric_limits<std::streamsize>::max(), (unsigned char)'\xF0');
    std::println("{:s}", strm.eof()); // false、絵文字の最初の文字で止まる
  }
}

これはcharのみで起こる事であり、wchar_tでは起こりません(char_traits<wchar_t>::to_int_type(c)が符号の変換をしないため)。ユーザーはジェネリックコードでも非ジェネリックコードでも、charについてこのような罠がある事を考慮しなければなりません。しかし、<cctpye>の関数にあるほぼ同様の問題と同じように、このことは広く理解されてはおらず、むしろ知られてすらいないようです。

また、get()getline()など区切り文字を受け取るその他の関数はchar_typeを受け取りint_typeへの変換は内部で行われるためこの問題は発生しません。

この提案は、この問題の解決のためにistream::ignore()オーバーロードを追加しようとするものです。

namespace std {
  
  template<class CharT, class Traits = char_traits<CharT>>
  class basic_istream : virtual public basic_ios<CharT, Traits> {
    ...

    // 現在のignore()
    basic_istream& ignore(streamsize n = 1, int_type delim = traits::eof());
    // 追加するignore()
    basic_istream& ignore(streamsize n, char_type delim);

    ...
  }
}

追加される2つ目のignore(streamsize, char_type)では、return ignore(n, traits_type::to_int_type(delim));のようにしてdelimを正しく変換を行った上で元のignore()を呼び出します。これによって、charが符号付である環境における負の値の動作の問題が解消され、かつcharの符号有無にかかわらず動作が一貫するようになります。

ただしこの追加によって、int_typeでもchar_typeでもない区切り文字を渡す呼び出し、例えばstd::cin.ignore(n, 1ULL)のような呼び出しが曖昧になります。とはいえこれはバグであると思われるので、これがコンパイルエラーになることはむしろ良いことです。

P3224R0 Slides for P3087 - Make direct-initialization for enumeration types at least as permissive as direct

P3087R0の紹介スライド。

SG17のメンバに向けて、P3087の提案内容を簡単に解説したものです。

P3225R0 Slides for P3140 std::int_least128_t

P3140の紹介スライド

モチベーションや実装への影響などについて説明されています。

P3226R0 Contracts for C++: Naming the "Louis semantic"

Contracts MVPに追加された4つ目の契約チェックのセマンティクスの名前についての提案。

4つ目のセマンティクスとは、少し上にあるP3191R0で提案されたセマンティクスで、契約チェックは行うものの契約が破られた際のハンドルは行わない(無条件で即終了する)というもので、Contractsをとりあえず使い始める際に使用されることを意図した簡易なセマンティクスです。

この提案によれば、この4つ目のセマンティクスは非公式にLouis semantic(P3191R0の筆頭著者の名前から)と呼ばれているようで、SG21は2024年3月の東京会議でこのセマンティクスをMVPに追加することを決定したようです。ただし、P3191R0ではその名前は特に指定されたおらず、いまだにそのセマンティクスを指す名前が無いようです。

この名前は直接的に利用者に露出するものではありませんが、evaluation_semantic列挙型の列挙値名となるほか、言語の規定の中でセマンティクスの動作を指定する際に使用され、さらにはC++ユーザーに契約機能について教育や解説を行う際にも使用されることが予想されます。したがって、その名前は軽々に決められるものではありません。

この提案は、SG21で議論されていた名前が大きく3つのカテゴリに分けられることに着目し、契約セマンティクスの命名についての38個の設計要件の重要度についてのアンケート調査をWG21のメンバ(14人)に対して行い、その結果を集計してどのカテゴリが一番求められる要件を満たしているかを絞り込んだうえで、そのカテゴリに沿った命名を調査し提案するものです。

そのような手順の末に選択されたカテゴリ(既存のセマンティクスの命名を変更せず、それらにフィットするもの)に該当するSG21で上がった名前候補に対して、同様に設計要件と照らし合わせてなるべく求められる要件を満たすような名前を選択していった結果、最もそれらしい名前として、fast_enforcequick_enforceの2つが残りました。

この提案では最終的に、std::exit()std::quick_exit()からのアナロジー(前者はユーザー定義コールバックやデストラクタを呼び出し手終了、後者はそれらを呼び出さずに終了)から、quick_enforceを4つ目のセマンティクスの名前として提案しています。

SG21の投票では、この名前を採用することに合意が取れたようです。

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

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

現在のContracts MVP仕様では、ある契約条件の評価回数は0回もしくは1回以上の任意の回数評価されうる、としています。これは、契約条件の評価の省略を許可したり、ABI境界における契約条件の重複評価を許可するほか、ユーザーが契約条件の副作用の依存しにくくするためなど、いくつかの意図の下決められています。

ただ、この評価回数に関しては、少なくとも上限を指定してほしい、あるいは正確に一回と指定してほしいなどの要望があるようです、例えば

  • アサーションを何回も繰り返した後で未定義動作に陥る契約条件は、評価回数の上限が指定されない場合UBとなる
    • 例えば符号付整数の可算の繰り返し
  • 低レイテンシおよびリアルタイムシステムでは、契約条件評価に関する実行時複雑度の上限が決定論的である必要がある
  • 安全性が重要な一部のシステムでは、決定論的な上限では不十分であり、契約条件がチェックされる場合は既知の決定論的な回数評価されることを保証する必要がある

また、EWGにおけるガイダンス投票では、かなりの人数の人が契約条件を複数回評価出来ないようにすることを希望していることが明らかになりました。

P3119R0(上の方)でこの問題について意の解決案が提案されていますが(64回という上限を導入する)、正確に一回だけ評価してほしいという要望には応えられておらず、この問題の解決策にはついては意見が分かれています。

C++26に向けてContractsの承認を取り付けるためには、現在よりも説得力があり強力なモデルを打ち立てる必要があり、P3197R0ではこの問題についてSG21に差し戻すことを提案しています。

このような流れを受けてこの文書は、契約条件の評価回数に関して、議論の背景や上がっている設計要件とソリューションについてまとめ、またどのソリューションがどの設計要件を満たすのかについての分析を提供するものです。したがって、この文書は何かを提案するものではありません。

ここでまとめられている設計要件は次のものです

  1. 決定論的な回数の評価
  2. 丁度1回だけの評価
  3. 評価回数の決定論的な上限
  4. 重複(同じ契約条件を複数回評価する)の許可
  5. 評価の省略の許可
  6. 2回以上の繰り返しの許可
  7. 評価回数を明記しない
  8. 「0、1、もしくは複数回」以外の数字は回避する

(提案には、各要件についての詳細な説明があります)

これをもとに、まとめられているソリューションは次のものです

  • 評価の省略を許可する
    • A0: 最大1回、評価は省略できるが重複はできない
    • B0: 最大2回、評価は省略と重複ができる
    • C0: 回数は指定されていないが、実装定義の上限Nがある
    • D0: 回数は指定されていないが、上限はない
  • 評価の省略を許可しない
    • A1: 丁度1回
    • B1: 1回または2回、重複できるが省略できない
    • C1: 回数は未規定、上限は実装定義のNだが、少なくとも1回評価される
    • D1: 回数は未規定、上限なし、少なくとも1回評価される

評価の省略を許可するかどうかは、他の要件(何回評価されるか)とは直交しています。

こうして得られた設計要件とそのソリューションによって決定マトリックスを作成することができ、pre/postについては次のようになります

要件\ソリューション A0 A1 B0 B1 C0 C1 D0 D1
丁度1回だけ評価
決定論的な上限を指定
実装定義の上限
評価の重複の許可(ABI境界におけるチェックのため)
評価の省略を許可
2回以上の繰り返しを許可(破壊的テストのため)
副作用防止のために評価回数を規定しない
0、1、もしくは複数回以外を指定しない

この表から、ソリューションA1だけが丁度一回だけ評価するという要件を満たすことができることが分かります。そして、ソリューションCはそれと決定論的な上限を指定するという要件以外の残りのすべての要件を満たしており、これら以外のソリューションはC0のサブセットにすぎないことが分かります。

pre/postのためのソリューション選択の決定木

contract_assertの場合はこのpre/postとは少し要件が異なります。要件4のABI境界におけるチェックのための重複チェックの許可は、contract_assertが関数本体内にしか現れないことから不要になります。

ただし、新しい要件としてpre/psotと一貫した動作の期待という要件が加わるかもしれません。それらの間でこの意味論が異なれば、利用者はpre/psotcontract_assertのどちらを使うべきか迷ってしまう可能性があります。

とはいえ、逆にcontract_assertpre/postとは異なるソリューションを採用できる可能性もあります。例えば、既存のassertマクロからの移行を考えると、決定論的な回数の評価が重要となり、一貫性要件やpre/postの要件とは異なるもののソリューションA1を選択することができる可能性があります。

P3230R0 views::(take|drop)_exactly

より効率的なviews::take/dropである、views::take_exactlyviews::drop_exactlyの提案。

views::take_exactlyviews::drop_exactlyは、元となるviews::take/dropに対して範囲チェックを行わないことで効率化するものです。views::take/dropはどちらも指定された長さを取得/スキップしますが、元の範囲がその長さより短い場合にオーバーランすることがなく効率的になります。ただし、オーバーランしないことをユーザーが知っている必要があり、その前提が満たされない場合はUBとなります。

どちらも既存のもので似た動作を実現することはできますが(views::counted(ranges::begin(r), N)subrange(ranges::next(std::ranges::begin(r), N), ranges::end(r)))、範囲に対して直接使用できず右辺値範囲に対応できないなどの問題があり、これらのview型を追加しても既存のtake_view/drop_viewの縮小版にしかならないため標準の負担は少ないとしています。

auto ints = views::iota(0) | views::take_exactly(10);
for (auto i : ints | views::reverse) {
  cout << i << ' ';  // prints 9 8 7 6 5 4 3 2 1 0
}

vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i : is | views::drop_exactly(6) | views::take_exactly(3)) {
  cout << i << ' '; // prints 6 7 8
}

P3232R0 User-defined erroneous behaviour

呼ぶとErroneous behaviourとなるライブラリ関数を追加する提案。

C++26では新しいプログラムの実行時動作状態であるErroneous behaviour(EB)が追加されています。これは、特定のプログラムエラーがある場合にUBにするのではなくWell-definedな動作を提供し、それによってそのエラーがプログラムの安全性・セキュリティに与える影響を軽減しようとするものです。EBはプログラムの観察可能な動作(observable behaviour)の一部であり、UBと異なりコンパイラがEBが発生しないことを仮定するのは許可されません。

C++26(P2795R%)では、未初期化変数の読み取りという1つの動さについてのみEBを指定しています。しかし、EBを任意の場所でユーザーによって指定できるようになれば、狭い契約(事前条件)を持つライブラリAPIにおいて有用となる可能性があります。

現在そのようなAPIを安全にするための方法にはいくつか欠点があります。浮動小数点数のゼロ割を例にして見てみると

UBのままにする。
指定が簡単なものの、安全性の向上はわずか

// Precondition: `den` must not be zero
float quotient(float num, float den) {
  return num / den;
}

assertによるもの。
NDEBGUが定義されていなければチェックされない。違反時の動作がプログラム終了しかない

// Precondition: `den` must not be zero
float quotient(float num, float den) {
  assert(den != 0);
  return num / den;
}

ifによるチェックとabbort()

// Precondition: `den` must not be zero,
// terminates otherwise
float quotient(float num, float den) {
  if (den == 0) { std::abort(); }
  return num / den;
}

Contaracstの事前条件(pre()/contract_assert())を使用
セマンティクスの正確な定義が不明瞭、チェックされない場合に到達するとUBになる。

// Precondition and contract: `den` must not be zero.
float quotient(float num, float den) pre(den != 0) {
  // Option #1: undefined behaviour on violation
  /* nothing */

  // Option #2: well-defined behaviour on violation
  if (den == 0) { return -2; }

  return num / den;
}

// Precondition: `den` must not be zero.
float quotient(float num, float den) {
  contract_assert(den != 0);

  // Option #1: undefined behaviour on violation
  /* nothing */

  // Option #2: well-defined behaviour on violation
  if (den == 0) { return -2; }

  return num / den;
} 

この提案

// Returns the quotient `num`/`den`;
// if `den` is zero, returns -2 erroneously.
float quotient(float num, float den) {
  if (den == 0) {
    std::erroneous(); return -2;
  }
  return quotient(unsafe_unchecked, num, den);
}

// 事前条件違反は未定義になるオーバーロード
float quotient(unsafe_unchecked_t, float num, float den) {
  return num / den;
}

この最後の例がここで提案していることです。ユーザー定義のコードにEBを許可することで、元のAPIが持っていた事前条件を持ちながら、それに違反した場合の動作をUBではない明確に定義された動作とする安全なAPIを提供することができるようになります。この場合、元のチェックしないAPIは別のアノテーション付きオーバーロードとして提供できます。

この提案は、このstd::erroneous()という関数を標準ライブラリに追加しようとするものです。

std::erroneous()は、その呼び出しがErroneous behaviourとなる他は(何の効果もない)関数です。これは呼び出しがUBとなるstd::unreachable()と対になるものでもあります。

std::unreachable()が最適化のためのものであるのに対して、std::erroneous()は狭い契約を持つAPIを安全に提供できるようにすることを目的とするものです。

EBは言語によって診断することが推奨されているため、std::erroneous()の実装では呼ばれた際に実行時の診断を出力するような実装をとるでしょう。ただし、単に何の効果もない関数として実装することも認められます。

P3233R0 Issues with P2786 (Trivial Relocatability For C++26)

現在進行中のTrivial Relocatabilityに関する提案について、問題点を報告する文書。

この提案では特に、2024年3月の全体会議でその設計が承認されたP2786R4について、その設計では制限が厳しすぎて多くの最適化を妨げており、標準ライブラリにおいてだけでなく、在野のライブラリがそれを採用できない可能性があると報告しています。

一方で、もう一つの提案であるP1144R10はP2786R4ではできない最適化が可能であり、在野のライブラリにおける既存の実装経験とも近いものですが、P2786R4で可能になる最適化のすべてがこちらの提案で可能であるというわけでもありません。

現在のこの2つの提案がTrivial Relocatabilityについて進行中の2つの提案ですが、この2つの間には何をどのように最適化できるかという点について相違があり、またその範囲は排他的で、どちらかがどちらかを包含しているというものではありません。以前の比較提案(P2814R0)はより低レベルな構文等の差異だけを見るのに終始したことで、この部分を見落としていた可能性があります。

この文書はこの2つの提案をより高レベルな観点から比較し、それぞれが抱える問題について焦点をあてるものです。それによって、2つの提案の利点を包含することのできる設計を見出すことを目的としています。

この文書はどちらの提案を支持するものでもありませんが、2つの提案のうちから一方だけを選ばなければならない場合はP1144を選択するとしています。

リロケーションとは、とても簡単に言うとムーブ直後にムーブ元オブジェクトを破棄する操作が複合したものです。このような操作の場合、ムーブ構築+デストラクタ呼び出しという2つの操作を個別に行うことなく、memcpyによるビットコピーとコピー元から宛先へのオブジェクト生存期間の移行という作業にまで最適化することができます。

トリビアルコピー可能な型は明らかにこれを満たしいていますが、原理的にはその実装がthisポインタに依存しないような任意の型はこの最適化が可能になります。例えばstd::unique_ptrstd::vectorなどが該当します。ただし、現在そのような最適化は言語によって禁止されており、トリビアルコピー可能な型以外でリロケーションは活用できません。

そのため、現在進行中のどちらの提案も、リロケーションという最適化がより広い型に対して有効化できるように、言語そのものでリロケーションという操作(特にTrivial Relocatabieという性質)を追加し、Trivial Relocatabieという型の性質が現在のTrivial Copyable等と同様に自動で伝播するようにしようとしています。実際のリロケーション操作はライブラリ関数によって行います。

// Trivial Relocatabieをオプトイン
class A TRIVIALLY_RELOCATABLE_TAG { ~~~ };
static_assert(std::is_trivially_relocatable_v<A>);

// 特殊なことをしなければ自然に伝播する
class B { A a; };
static_assert(std::is_trivially_relocatable_v<B>);

P2786R4では概ね次のような言語設計になっています

  • trivially relocatableという新しい型の性質を導入
  • スカラ型、trivially relocatableクラス型、それらの配列型はtrivially relocatable
  • コンテキスト依存キーワードtrivially_relocatableを使用してクラスがtrivially relocatableであることを手動でマークできる
  • マークされていないクラスは、次の場合にtrivially relocatable
    • そのサブオブジェクトがすべてtrivially relocatableであるか、参照型であること
    • 仮想基底を持たない
    • 削除されておらず、非ユーザー定義のデストラクタを持つこと
    • 削除されておらず、非ユーザー定義のコンストラクタを通してムーブ構築可能であること
  • マークされたクラスが仮想基底を持っていたり、trivially relocatableではないサブオブジェクトがあるとill-formed

一方P1144R10は概ね次のような設計になっています

  • trivially relocatableという新しい型の性質を導入
  • スカラ型、trivially relocatableクラス型、それらの配列型はtrivially relocatable
  • 属性[[trivially_relocatable]]を使用してクラスがtrivially relocatableであることを手動でマークできる
  • マークされていないクラスは、次の場合にtrivially relocatable
    • そのサブオブジェクトがすべてtrivially relocatableであるか、参照型であること
    • 資格のある(eligibleな)コピー・ムーブ操作とデストラクタがいずれもユーザー定義ではないこと
    • 仮想基底を持たない
  • サブオブジェクトの一部がtrivially relocatableではない場合でも、[[trivially_relocatable]]のマークが可能

2つの提案の違いは特に次のような点です

  • 仮想関数を持つクラスを許可するかどうか
    • P2786R4 : 許可する
    • P1144R10 : 許可しない
  • trivially copyable => trivially relocatableとなるか
    • P2786R4 : ならない(タグによってオフにできる)
    • P1144R10 : なる(自然に伝播した性質を上書きできない)
  • 代入演算子の考慮
    • P2786R4 : しない(リロケーション代入はない)
    • P1144R10 : する(リロケーション代入を考慮)

Ttrivially relocatableである場合、Tのムーブ+ソースの破棄という操作がmemcpy+生存期間の移行に最適化できるかどうかは次のように異なります

リロケーションへ最適化可能な操作 P2786R4 P1144R10
ムーブ構築+ソースオブジェクトの破棄
ムーブ代入+ソースオブジェクトの破棄

これは特に、trivially relocatableであるTstd::vector等のコンテナに入っていた場合に重要になり、ムーブ代入がリロケーションへ最適化可能かどうかはより複雑に分岐します。

例えば、次のような型はP2786R4のモデルではtrivially relocatableになりますが、P1144R10ではなりません

struct IRef {
  int &ref;
  IRef &operator=(const IRef &other) { ref = other.ref; return *this; }
};

これはP2786が代入演算子の定義を気にしないためですが、これによってP2786ではstd::vector<IRef>そのもののムーブ構築+ソースの破棄をリロケーション操作に最適化することができます。一方で、trivially relocatableな型Tstd::vector<T>の個別の要素を削除したり挿入したりする操作(insert, erase, swapなど)の場合は代入演算子がユーザー定義可能であることによって最適化したリロケーションによる要素の移行を行えず、通常のムーブ代入+破棄によって実装するしかありません。

P1144の場合、std::vector<IRef>そのものおよび個別要素のムーブ+ソースの破棄をリロケーション操作に最適化することは一切できないものの、trivially relocatableな型Tstd::vector<T>の個別の要素を削除したり挿入したりする操作(内部的にムーブ代入+破棄が行われる)の場合でも、最適化されたリロケーション操作によってそれを行うことができます。

リロケーションへ最適化可能な操作 P2786R4 P1144R10
std::vector<IRef>のムーブ構築+破棄
std::vector<IRef>のムーブ代入+破棄
std::vector<T>のムーブ構築+破棄
std::vector<T>のムーブ代入+破棄
std::vector<IRef>の要素のムーブ代入+破棄
std::vector<T>の要素のムーブ代入+破棄

(表中のTtrivially relocatableである任意の型とする)

このIRefのような型には例えば、std::tuple<int&>std::pmr::stringなどがあります。

すなわち、最適化機会としてはP1144の方がより有用ではあれど、P2786でないと最適化できない場合もあるという事です。

この文書では、今後の行動として次の事を提案しています

  1. EWG/LEWGはC++におけるtrivial relocationのモデルとしてP2786R4の採用を再検討する
  2. 理想的には、2つの提案を統合することが望ましい
    • これが不可能または受け入れられない場合、C++26にはP1144R10を採用することが望ましい
  3. 統合された提案では次の事に適切な名前とセマンティクスを与える必要がある
    • 「ムーブ構築+ソースオブジェクトの破棄」という複合操作
    • 特定の型について、「ムーブ代入+ソースオブジェクトの破棄」という操作が、「ムーブ先オブジェクトの破棄+ムーブ構築+ソースオブジェクトの破棄」(すなわち、「ターゲットの破棄+リロケーション」)と同等であることを表す型の性質
  4. 統合された提案には、2つの言語のオプトイン方法が必要
    • トリビアルなムーブ+破棄のみを有効にするもの
    • それに加えて、3の型の性質を持つことを表明するもの
  5. 4の2つ目のオプトイン方法には、よりシンプルで一般的な名前を与える
    • ほとんどの場合、trivially relocatableのためにはこれが使用されるはず
  6. 2つのオプトイン方法を提案しているので、2つのキーワードよりも属性の方が好ましく思える
  7. 明示的なオプトインが伝播しているtrivially relocatableの性質を上書き可能であるべきかどうかについて、SG12での再投票を行う
  8. 言語とライブラリは同じ提案で扱う必要がある
    • P2786R4はライブラリ機能をほぼ含んでいない
    • 例えば、std::uninitialized_relocate()などが必須と思われる候補
  9. trivially relocatableが上書き可能である場合、標準ライブラリの中で手動でマークが必要なデータ型についてマークを行う部分もその提案に含まなければならない
    • 標準ライブラリがtrivially relocatableに対応しない限り、これをC++26に導入すべきではない
    • 標準ライブラリのtrivially relocatable対応をQoIに任せると、機能が不十分になる

この文書及び同時期に提出された同種の提案を受けて、P2786R4はEWGに差し戻されています。少なくともこのリストにある事の一部は考慮されて再設計されるはずです。

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

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

ライブラリを書く場合にこの関数を使用したくなることがよくあります。例えば、文字等の範囲の重複が無い場合に最適なメモリコピー操作を使用するようにしたり

if (!pointer_in_range(ptr, data_, data_ + size_)) {
  std::copy(ptr, ptr + size, data_);
}

最初のうちはスタック領域を使用し、あとから動的確保を行うような独自のアロケータにおいてその判定に使用したりなどです

if (!pointer_in_range(ptr, store_, store_ + size_)) {
  ::operator delete(ptr);
}

このような関数は実際、正しく実装して移植性を確保することは非常に困難です

template<class T>
bool pointer_in_range(T* ptr, T* begin, T* end)
{
  // 組み込み演算子が狭義全順序の上でポインタを比較しない場合、不正確
  return begin <= ptr && ptr < end;
}

template<class T>
bool pointer_in_range(T* ptr, T* begin, T* end)
{
  // 実装定義の教義全順序の上での比較となるが、2つの配列がインターリーブされている場合にまだ不正確
  return std::less_equal<>()(begin, ptr) && std::less<>()(ptr, end);
}

// 上2つは定数式で使用できない。この実装は定数式で使用できるが、実行時に最適ではない
template<class T>
constexpr bool pointer_in_range(T* ptr, T* begin, T* end)
{
  for (; begin != end; ++begin) {
    if (begin == ptr) {
      return true;
    }
  }
  return false;
}

よく使用されるが実装が難しく移植性も低い関数を標準ライブラリで用意しておくことは理にかなっているため、この関数を標準ライブラリに追加する提案です。

P3236R0 Please reject P2786 and adopt P1144

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

リロケーションとはムーブと元オブジェクトのデストラクタ呼び出しの複合操作のことで、トリビアルなリロケーションとはそれをmemcpyによって行うことができる場合のリロケーション操作のことを言います。この操作を、主にライブラリ実装の最適化のためにC++標準がサポートするための提案が2つ出ており、P2786とはそのうちの一つです。

この提案は、ライブラリ実装の観点からP2786の提案するトリビアルリロケーションがライブラリの最適化には役に立たないことを示すとともに、P2786をリジェクトしてもう一つのリロケーション提案であるP1144を採択すべきとする提案です。

提案のP2786に対する批判は次のようなものです

  • ユーザー定義operator=を持つ型はデフォルトでtrivially relocatableとして扱われる
    • 値セマンティクスを持たない型(tuple<int&>など)をそうではない型と区別する方法が無い
    • これによって、vector::erase, rotate, swapなどを最適化の対象にできない
  • ベースラインがtrivially copyableではない
    • P2786では、trivially copyableだがtrivially relocatableではない型を作成できる
    • P1144はtrivially copyabletrivially relocatableとなり、よりシンプルなモデル
  • P2786では既存のライブラリでの利用に焦点が当てられていない
    • 既存のライブラリでどのように実装を切り替えるかの例がなく、機能はそれを考慮されていない
  • P2786のtrivially relocatableのためのマーキングは下位互換性が無い
    • P2786ではクラス名に続いてtrivially_relocatable文脈依存キーワードによってそれを宣言する
    • P1144では属性によってそれを指定し、以前の言語モードでも使用できる(予め備えておける)
  • 機能が未完成
    • 意図的なもので、いくつかの進化の方向性を残している。そのために、細部の動作について推論しづらい
    • P1144はほぼ完全なものですぐにでも利用可能であり、2018年から最適化実装が提供されている
  • P2786のリロケーションのためにコンテナセマンティクスへの変更が別に提案されているが、P1144ではそれは必要ない
  • P2786のマーキングはメンバ/基底クラスに再帰的に伝播する
    • 実装に当たっては、ライブラリユーザーに公開する型に関与する全ての型にマーキングが必要になる
    • P1144の属性マーキングは、リロケーションを使用したい型にのみマーキングすればいい

さらには、P1144の提案するトリビアルなリロケーション関連の機能は在野のライブラリで独自に実装されていたり、P1144に適合する形で先行して実装/使用されていたりと、既に実装経験が豊富にあります。一方P2786にはそれがなく(一応Bloomberg社内で使用されているとしている)、上記のような問題によりライブラリでの使用には向いていない部分があります。

この提案の著者に名を連ねているのは全て、何かしら公開され実用されているC++ライブラリ開発に携わっている方々のようで、この主張には一定の説得力がありそうです。

P3237R0 Matrix Representation of Contract Semantics

契約注釈のセマンティクスについて、特性の行列(表)によってセマンティクスを指定しようとする提案。

Contracts MVP仕様にはすでに、契約注釈のセマンティクスについて4つのもの(ignore,emforce, obserbwlouise semanteics)の4つが提案されており、将来的にさらに増えていくことが予想されます。その場合、louise semanteicsがそうであるように、セマンティクスに対して適切な名前をつけるのが困難になりうることが懸念されています。

この提案では、そのような問題に対処するために契約セマンティクスの表現法を修正しようとするものです。

この提案では、契約セマンティクスの特性を指定したうえで、その行列(表)を構成しそれによってセマンティクスを指定するようにすることを提案しています。

このアイデアの元になったのは、P3237やP3205でセマンティクスの説明に使用されていた次のような表です

semantic checks calls handler assumed after terminates proposed
assume no no yes no no
ignore no no no no [P2900R5]
“Louis” yes no yes trap-ish [P3191R0]
observe yes yes no no [P2900R5]
enforce yes yes yes std::abort-ish [P2900R5]

このように契約セマンティクスを記述することで、主要な特性を俯瞰し欠落しているセマンティクスを見つけることができます(louise semanteicsがまさにそうでした)。さらに、特性の間に依存関係(チェックしないならばハンドラが呼ばれることはない、など)を考慮することで、可能な組み合わせが削減され、役割が重複したセマンティクスが導入されてしまうことを防ぐこともできます。

そして、契約セマンティクスとは、この行列にあらわされた特性の集合として指定するようにすることを提案しています。

また、オプションの提案として、契約セマンティクスを表す行列の列をビットフィールドにエンコードして表すことを提案しています

struct contract_semantic {
  bool checks : 1;
  bool calls_handler : 1;
  bool assumed_after : 1;
  int terminates : 2;
};

これは、違反ハンドラの引数型のcontract_violationcontract_semanticメンバを今の列挙値から置き換えることを目的とするものです。このオプション提案が採用された場合さらに、現在定義されているセマンティクスの名前をもつエイリアスを定義しておくことも提案しており、例えば次の2つをcontract_violationpublicメンバとして定義しておくことを提案しています

constexpr contract_semantic enforce = {
  .checks=true, 
  .calls_handler=true,
  .assumed_after=true,
  .terminates=0x101
};

constexpr contract_semantic observe = {
  .checks=true,
  .calls_handler=true,
  .assumed_after=false,
  .terminates=0
};

これを使用すると、コンパイラオプションによって契約セマンティクスの指定を行う際に、セマンティクス名毎にオプションを個別に提供する必要が無くなり、オプションの使用が簡単になる可能性があります。

ただし、この提案ではその個別の特性については提案しておらず、この方針が受け入れられた後で別の提案によって行う予定のようです。

P3240R0 Slides for EWGI presentation on allocators, Tokyo 2024

スコープアロケータを言語サポートする必要性について解説した文書。

内容としては以前に提案されていたP2685R0のモチベーションと解決案を詳しく解説したものです。ただし、P2685を継続するのではなく書き直す予定でいるようで、NRVO保証やリロケーション、また静的リフレクションを活用しながら設計をし直すつもりのようです。

P3241R0 Slides for LEWG presentation on trivial relocation, April 2024

P2786のtrivial relocation提案のライブラリAPIについての紹介スライド。

LEWGのメンバに向けて、提案の内容を簡単に解説したものです。

P3242R0 Copy and fill for mdspan

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

C++23 で導入された std::mdspan に対して、copy 操作および fill 操作を行う関数を <mdspan> に追加する提案。

この提案では、さまざまなレイアウトやアクセサを持ちうる std::mdspan について、効率的な copy 関数と fill 関数を提供することで、多次元配列操作をより簡単に行えるようにし、画像処理や HPC, CG などの分野での std::mdspan の活用促進を目指しています。

標準ライブラリで提供するメリットとして、std::mdspan の実装が持っている情報を使うことで、範囲外アクセスのチェックや、メモリアクセスの最適化(memcpy の使用など)ができ、多次元配列操作の安全性とパフォーマンスを向上できることを指摘しています。

提案では、これらの関数をどのヘッダに追加するかを検討した過程や、既存の std::linalg::copy との関係、将来的に for_each などのアルゴリズム関数を追加できる可能性についても述べています。

P3243R0 Give std::optional Range Support - Presentation, Tokyo 2024

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

P3168R0 Give std::optional Range Support のプレゼンの際に使われたスライド。

2024 Tokyo WG21 Meeting において、P3168R0 を説明するために使用された資料です。

P3244R0 [[nodiscard]] Policy - Presentation, Tokyo 2024

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

P3162R0 LEWG [[nodiscard]] policy のプレゼンの際に使われたスライド。

2024 Tokyo WG21 Meeting において、P3162R0 を説明するために使用された資料です。

P3245R0 Allow [[nodiscard]] in type alias declarations

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

[[nodiscard]] 属性を型エイリアス宣言で使用できるようにする提案。

[[nodiscard]] 属性を型に対して使用すると、その型を返すすべての関数に対して、戻り値の無視に警告を発生させることができます。しかし、外部ライブラリで定義されている型を使用する場合、その型にもともと [[nodiscard]] 属性が付与されていなければ、その型を [[nodiscard]] な型として使用することができません。

この提案では、次のように型エイリアス宣言で [[nodiscard]] を使用できるようにすることを目指しています。

using MyError [[nodiscard]] = Error;

ただし、型エイリアス宣言は型ではないため、現行の言語仕様の範疇ではこうした仕組みを導入できない問題があります。この提案では 2 通りの方針を示し、今後の検討事項としています。

  1. 元の型を使って関数が宣言されているか、型エイリアスを使って宣言されているか、コンパイラが追跡する。
Error foo(int);

foo(42); // 警告なし

MyError foo(int); // 同じ関数の宣言

foo(42); // 警告あり
  1. 既存の型から新しい型を導入するようなメカニズムを追加する。つまり、ErrorMyError は一方が [[nodiscard]] 無し、もう一方が [[nodiscard]] ありの、異なる型になる。

なお、現行の Clang と GCC では、非標準の拡張を使用することで、型エイリアスでの [[nodiscard]] 相当のことが既に実現可能です。

// In clang
using MyError [[clang::warn_unused_result]] = Error; 

// In gcc
using MyError __attribute__((warn_unused_result)) = Error;

P3247R0 Deprecate the notion of trivial types

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

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

トリビアル型(trivial type)は、トリビアルコピー可能かつトリビアルなデフォルトコンストラクタを持つ型のことです。しかし、規格の文面には、std::is_trivial 自身の説明を除くと、本文のごく少数箇所にしか登場しません。それらについても「トリビアルコピー可能」「非トリビアルなデフォルトコンストラクタを持たない」のいずれかで言い換えたほうが合理的であり、「トリビアル型」の定義には実用上の意味がほとんどないと主張しています。

C++20 では「POD」という概念が std::is_pod とともに非推奨になりました。この提案は、「トリビアル型」についても std::is_trivial とともに非推奨にし、規格で使われる用語の合理化を目指しています。

おわり

この記事のMarkdownソース

[C++]定数式における未知の参照の利用の許可

C++23から、定数式における、非定数式な参照の読み取り(特にコピー)が定数式において許可されるようになります。

定数式における非定数式の参照

定数式における非定数式の参照という一見意味の分からないものは、生配列に対するstd::size()の使用で容易に出会うことができます

void check(int const (&param)[3]) {
  int local[] = {1, 2, 3};

  constexpr auto s0 = std::size(local); // ok
  constexpr auto s1 = std::size(param); // ng
}

https://wandbox.org/permlink/gaYeDLdSKZtFrENY

このs0, s1はともにconstexprローカル変数であり、その初期化式(ここではstd::size()の呼び出し)は定数式でなければなりません。しかし、ここでは変数localは定数式として扱われるのに対して、変数paramは定数式として扱われておらず、それによってエラーになっています。

これは定数式で禁止されている事項に引っかかっているためにこの差が出ています

N4861 7.7 Constant expressions [expr.const]/5.12より

  • 参照に先行して初期化されていて次のどちらかに該当するものを除いた、変数の参照または参照型データメンバであるid-expression
    • 定数式で使用可能である
    • その式の評価の中でlifetimeが開始している

[expr.const]/5では定数式で現れてはいけない式が列挙されています。これはそのうちの一つです。

loaclおよびparamという名前を参照する式はid-expressionであり、localは参照に先行していて初期化されていて定数式で使用可能である参照である(ただし参照先の読み取りはできない)のに対して、paramaは初期化されていないためこの事項に即抵触しています。

他にも、値ではなくその型にしか関心が無い場合に変数の型の取得のために関数テンプレートの引数推論を利用することはよくあると思います。このような場合にも、同様の問題に遭遇する可能性があります

template <typename T, typename U>
constexpr bool is_type(U &&) {
  return std::is_same_v<T, std::decay_t<U>>;
}

これは例えば次のように使用することを意図しています

auto visitor = [](auto&& v) {
  // vは非定数式の参照
  if constexpr(is_type<Alternative1>(v))  // ng
  {
    ...
  }
  else if constexpr(is_type<Alternative2>(v)) // ng
  {
    ...
  }
};

この例のvは定数式ではないので、if constexprの条件式(当然定数式でなければならない)の中で他の関数に渡すことはできません。この場合、参照vそのものには関心が無いのが分かりやすいでしょう。

ラムダ式constexprにしてもこれは解決できません。

また別のところでは、this引数として出会うこともできます

struct S {
  int buffer[2048];

  void foo() {
    constexpr size_t N = std::size(buffer); // ng

    ...
  }
};

https://wandbox.org/permlink/slJqqHKLw6bJmBqn

このエラーは今度は、std::size(buffer)の引数においてthisが定数式で使用できないと怒られています。ここのbufferはメンバ変数なので、その参照にはthisが必要ですが、このコンテキスト(Nの初期化式)でのthisは定数式ではないため定数式で使用できずに怒られています。

この場合、foo()constexprにしても変わりません。

また、冒頭のコードをstd::array.size()メンバ関数呼び出しに置き換えると、thisによる同様のエラーに出会えます

void check(const std::array<int, 3>& param) {
  std::array<int, 3> local = {1, 2, 3};

  constexpr auto s0 = local.size(); // ok
  constexpr auto s1 = param.size(); // ng
}

https://wandbox.org/permlink/XLH8LNnIUZ4X8gaN

このthisの使用でエラーの起きている2例はいずれも、thisを介して(間接参照して)アクセスしようとしているわけではありません。

thisとよく似た場合で、あるクラスのオブジェクトを通してそのクラスの静的メンバにアクセスする場合もこの問題に出会うことができるかもしれません

struct S {
  static constexpr bool f() {
    return true;
  }

  static constexpr int constant = 1;
};

void call(S& s) {
  // sは非定数式の参照
  constexpr bool b = s.f();     // ng
  constexpr int N = s.constant; // ng
}

https://wandbox.org/permlink/48AsQDbo7v3COWOw

この場合、s.は構文上の略記でしかなく、sの具体的な値に興味がありません。にもかかわらず、sが非定数式であることによってsを介した定数式の呼び出しがエラーになっています。

やりたいこととそうじゃないこと

これが例えば次のような例であれば、これができないことは仕方のないことだと理解できます

constexpr int read(auto& array) {
  return array[0];
}

void check(int const (&param)[3]) {
  int local[] = {1, 2, 3};

  constexpr auto s0 = read(local); // ng
  constexpr auto s1 = read(param); // ng
}

しかし先ほど一通り見ていた例はいずれも、このようなことがやりたいわけではありません。なんなら、参照(this)の参照先どころか参照そのものに対してすら全く興味がありません。

一連のコード例で重要なのは参照の型情報であって、参照そのものはどうでもいいのです。

しかしいずれの例でも、定数式の実行に際して非定数式である参照値の読み取り(別の関数への受け渡し、コピー)が発生しており、これによって全体の式の定数式としての実行が妨げられてしまっています。ここでの読み取りとは参照そのものの値に対してであって、参照先にアクセスしようとするものではありません。

定数式における参照そのものの読み取りの許可

このような問題の原因は、定数式の実行においては未定義動作が許可されないことによって、定数式で使用されるすべての参照は有効なものでなくてはならないためです。関数の中からでは関数引数の参照の有効性は判定できないためそのような参照の有効性は未知であり、最初の方で引用したルールはそれをあらかじめ弾いておくためのものでした。

従って、この問題は参照(言語参照、this、および一般のポインタ)でのみ起こります。ほぼ同等のコードであっても、それが値であれば起こりません。

void check(int const (&param)[3]) {
  int local[] = {1, 2, 3};

  constexpr auto s0 = std::size(local); // ok、参照のコピー渡し
  constexpr auto s1 = std::size(param); // ng、未知の参照のコピー渡し
}

void check_arr_val(std::array<int, 3> const param) {
  std::array<int, 3> local = {1, 2, 3};

  constexpr auto s3 = std::size(local); // ok、値の参照渡し
  constexpr auto s4 = std::size(param); // ok、値の参照渡し
}

https://wandbox.org/permlink/JCc3rgIcRXSc8UO7

もちろん値の場合でもこのように渡した先の関数内でその具体的な値にアクセスすることはできません。しかし、今回問題となっているケースでは参照先には全く関心が無いため、参照もこれと同じ扱いになってもよさそうです。

この問題はP2280によって補足され、P2280R4はC++23に対して採択されました。

P2280R4では、ここまで示したような定数式における未知の参照及びthisポインタそのもの読み取り(コピー、not間接参照)を許可します。これによりここまでいくつか示してきたような、参照そのものに興味はないが未知の参照がコピーされることによって定数実行できなかったコードがすべて定数式で実行可能になります。

void check(int const (&param)[3]) {
  int local[] = {1, 2, 3};

  constexpr auto s0 = std::size(local); // ok
  constexpr auto s1 = std::size(param); // ng -> ok
}
auto visitor = [](auto&& v) {
  if constexpr(is_type<Alternative1>(v))  // ng -> ok
  {
    ...
  }
  else if constexpr(is_type<Alternative2>(v)) // ng -> ok
  {
    ...
  }
};
struct S {
  int buffer[2048];

  void foo(){
    constexpr size_t N = std::size(buffer); // ng -> ok

    ...
  }
};
void check(const std::array<int, 3>& param) {
  std::array<int, 3> local = {1, 2, 3};

  constexpr auto s0 = local.size(); // ok
  constexpr auto s1 = param.size(); // ng -> ok
}
struct S {
  static constexpr bool f() {
    return true;
  }

  static constexpr int constant = 1;
};

void call(S& s) {
  // sは非定数式の参照
  constexpr bool b = s.f();     // ng -> ok
  constexpr int N = s.constant; // ng -> ok
}

ただし、この緩和の対象は参照とthisポインタのみで、より一般のポインタに対しては適用されません。

void f(std::array<int, 3>& r, std::array<int, 4>* p) {
  static_assert(r.size() == 3);    // #1、ok(この提案による)
  static_assert(p->size() == 4);   // #2、ng
  static_assert(p[3].size() == 4); // #3、ng
  static_assert(&r == &r);         // #4、ng
}

ポインタは参照と比べるとできることが多いため、定数式における未知のポインタに対して何の操作が適用可能で何がそうではないのかを規定しなければならず、作業が複雑になることからP2280R4では当初の目標である未知の参照とthisだけに的を絞って緩和されました。

欠陥報告

P2280R4はDR(Defect Report)として以前のすべてのバージョン(この場合constexprが初めて導入されたC++11まで)に対してさかのぼって適用されます。したがって、P2280R4を実装したコンパイラではC++23モードだけでなく、C++11やC++20モードなどでもこの問題が解決されます。

この記事を書いている時点では、GCC14だけがこれを実装しています。

requires式の引数

この未知の参照概念、基本的に関数引数で遭遇することが多いと思われるのですが、なんとrequires式の引数の参照さえもその対象です。

template<typename T>
concept to_char_narrowing_check = requires(T&& t) {
  { std::int8_t{ t.size() } };  // -128 ~ +127 の範囲内はtrue
};

static_assert(to_char_narrowing_check<std::array<int, 1>>);
static_assert(to_char_narrowing_check<std::array<int, 127>>);
static_assert(to_char_narrowing_check<std::array<int, 128>> == false);

GCC14: https://wandbox.org/permlink/ikcs2twJfdfumqa7
clang15: https://wandbox.org/permlink/dRmTPwTNeGMMrs0e

このto_char_narrowing_checkコンセプトは、std::int8_t{ t.size() }という式の有効性をチェックしています。引数型がすべてstd::arrayであるとすると、その.size()の戻り値型はstd::size_tなので常に縮小変換となり、{}初期化では縮小変換が許可されないことから常にコンパイルエラーになります(P2280R4以前は)。

ただし、このような縮小変換は定数式で行われた場合にのみ、変換元の値が変換先の型で表現可能であれば許可されます。

ただ前述のように、P2280R4以前はstd::array::size()thisポインタのコピーが必要になり、それが定数式ではないので定数式における縮小変換のチェックは行われませんでした。P2280R4以前の世界では、この例の上2つのstatic_assertが満たされることはありません(clang15の例)。しかし、P2280R4以降ではこの例の上2つのstatic_assertは満たされるようになります(GCC14の例)。

何が起きているかというと、requires式内部のstd::int8_t{ t.size() }の式の妥当性チェックの際に、定数式で縮小変換が可能かどうかがチェックされており、P2280R4の緩和によってそれを妨げるものが無くなったことで、t.size()の値が取得されてその値がチェックされるようになっています。

ただしここでは、arrayのオブジェクト(tの参照先)は具体的に使用されておらず、t.size()の値はtの型情報から取得されています。

std::declval()

P2280R4の緩和はC++17以前の世界にももたらされます。その世界で、requires式の引数のような役割を担っていたのはstd::declval()でした。残念なことにstd::declval()は定数式で呼べないため、その返す参照はP2280R4の緩和対象ではありません。そのため、SFINAEの世界では先程のto_char_narrowing_checkと同等の制約を表現でき・・・ないこともありません。

要は何とかして、std::declval()を使用していたところを関数引数から取ってくるように書き換えればいいわけです。

template <typename T>
auto to_char_narrowing_check_helper(T&& t)
  -> decltype(std::int8_t{ t.size() });   // 先程のrequires式の例と同等のチェックはここで行われる

template<typename T, typename = void>
constexpr bool to_char_narrowing_check_v = false;

template<typename T>
constexpr bool to_char_narrowing_check_v<
  T,
  std::void_t<decltype(to_char_narrowing_check_helper(std::declval<T&>()))> // std::declval()を使用して、ヘルパ関数を呼ぶ(呼んではいない)
> = true;

static_assert(to_char_narrowing_check_v<std::array<int, 1>>);
static_assert(to_char_narrowing_check_v<std::array<int, 127>>);
static_assert(to_char_narrowing_check_v<std::array<int, 128>> == false);

GCC14: https://wandbox.org/permlink/doHxvy8q1Mgw9C5g
clang15: https://wandbox.org/permlink/0tqFtsGEJSMF620m

かなり複雑な実装ですが、GCC14の実行結果を見れば、SFINAEの制約においても縮小変換が定数式でチェックされていることが分かります。

std::void_tを利用した検出テクニックについては説明を割愛します。

従来のSFINAEなら、このto_char_narrowing_check_helper()のような余分な関数は必要なく、void_tの中で直接std::void_t<decltype(std::uint8_t{ std::declval<T&>().size() })>のようにしてしまえば十分でした。ただ、P2280R4の緩和を考えると、チェックしたい式に直接std::declval()が現れるのは良いこととは言えません(前述のようにdeclval()の呼び出しは常に定数式ではないため)。

そこで、std::declval()を使用してdecltype()内でTの参照を取得するものの、それを別の関数(to_char_narrowing_check_helper())の引数に渡すことで参照の出所をロンダリングします。呼び出された関数内では、その引数の参照は未知の参照として出所に関わらず使用できる(参照先を見に行かない限り)ので、あとはそちらのSFINAEの文脈で対象の式をチェックするようにするわけです。このようにすると、従来のSFINAEの文脈でもP2280R4の緩和の恩恵にあずかることができます。

ここでは例としてstd::arraysize()を使っていたわけですが、実際にはそれに対してこういう事をする需要というのはほぼ皆無でしょう。より実用的なのは、std::integral_constantのような定数値クラスに対してです。

すでに、std::variantの初期化においてstd::integral_constantのような型の値からの変換時にこのようにP2280R4を考慮して縮小変換を定数式で検査するようにする提案(P3146)が提案されています。

using IC = std::integral_constant<int, 42>;

IC ic;
std::variant<float> v = ic; // C++23でもすべての実装でエラー
                            // P3146では値に応じて初期化できるようにすることを提案

この際に問題となったのがまさに、std::variantの制約の実装がSFINAEによって行われていることで、ここで説明していることはP3146R1でより詳細に解説されています。特に、SFINAEでもP2280R4の恩恵にあずかれるようにするテクニックは私が思いついたものではなく、この提案に例として載っているものをより簡易に直しただけです。

そして、この恩恵は提案中のconstexpr_tのような型(汎用的なconstexpr引数のための型)でより重要になってくるでしょう。

余談

P2280R4の変更は、定数式における未知の参照(とthis)の利用を許可するというよりも、それらのものが実際にその参照先にアクセスするまでエラーにしないようにするという性質のものです。元々参照されるされないにかかわらず危険な可能性があるとしてまず使用が禁止されていたものが、実際に危険になるまでは様子見するようにした感じです。

これはC++20以降の定数式の制限緩和に見られる、定数式をエラーにするのは実際に実行してみて実行できないものに出会ってから、という方針に従ったものです。

このことはあまり明言されてこなかったのですが、C++23のP2448R2の採択によって明示的にされました。P2448R2では、定数式におけるコンパイルエラーの発生条件を可能な限り遅延し、実行してみて実行できなかった場合にのみエラーにするという様に変更するものです。

参考文献

この記事のMarkdownソース

[C++] パラメータパックをprint/formatする

printfとその仲間たちに関する記事ではありません

フォーマット文字列とパラメータパック

std::format()およびstd::print()のフォーマット文字列は共通のもので、文字列内の{}(置換フィールド)に対して、与えられた引数(フォーマット対象の値)を文字列化したものを順番に置き換えていきます。

// フォーマット文字列の例
std::print("{}", 0);
std::print("{}{}{}", 0, 1.0, "2");

この時、この対応関係はコンパイル時にチェックされ、フォーマット文字列の{}の数に対して引数が足りない場合はエラーになります(なぜか、多いだけならエラーにはならないようです)。

std::print("{}{}{}", 0);      // ng
std::print("{}", 0, 1, 2, 3); // ok

引数の値は定数である必要はなく実行時の値であっても当然問題ないのですが、フォーマット文字列そのもの、およびそれに現れている置換フィールド{}の数とフォーマット対象の引数の数は、コンパイル時にコード上で一致している必要があります。

どちらも非常に便利でありC++20/23以降は手放せなくなること必至でしょう。それを自然に使用していると、パラメータパックに対して使用したくなる時が来るでしょう、いずれ。しかし、パラメータパックのその独特な性質により一筋縄ではいかないことに気づきます。

void any(auto&&...);

void f(auto&&... args) {
  // コンパイル時に要素数は取れる
  constexpr std::size_t N = sizeof...(args);

  // パックそのものは他に渡せない
  any(args);    // ng
  any(args...); // ok

  // パラメータパックそのものには型は無い
  using t = decltype(args); // ng
}

パラメータパックそのものに対してはサイズを取得することと展開することくらいしかできず、取りまわすようなことは不可能です。そのため、std::format()/std::print()でも、パラメータパックそのものを渡して文字列化することはできません。

void print_pack(auto&&... args) {
  std::print("{}", args);     // ng

  // パックの先頭の値のみが出力される
  std::print("{}", args...);  // ok(パックが空でなければ)
}

多い分には問題ないので先頭N個だけ出力したければこういう感じでもいいのかもしれませんが、パラメータパックに含まれる値をすべて出力したい場合に、フォーマット文字列をべた書きすることはできず、なんらか生成する必要が出てきそうに思えます。

コンパイル時生成

C++17以前だとメタメタプログラミングの世界へようこそ!するのですが、C++20であればstd::stringコンパイル時に使用できるのでいくらからくにはなります。

#include <ranges>
#include <print>
#include <string>

template<std::size_t N>
consteval auto make_format_string_for_pack() {
  std::string fmtstr = "";
  
  // デリミタはお好みで
  constexpr std::string_view append = "{}, ";
  
  for (auto _ : std::views::iota(0ull, N)) {
    fmtstr += append;
  }

  std::array<char, append.length() * N + 1> save;
  std::ranges::copy(fmtstr, save.begin());
  save.back() = '\0';

  return save;
}

void print_pack(auto&&... args) {
  static constexpr auto fmtstr_array = make_format_string_for_pack<sizeof...(args)>();
  std::print(fmtstr_array.data(), args...);
}

int main() {
  print_pack(1, "str", 3.1415, 'c', true);
}
1, str, 3.1415, c, true, 

これでもいいんですが、書くことが多いし、かなりテクニカルな部分が多いコードになっています。なぜわざわざstd::arrayに詰めなおしているのか?なぜstatic constexpr??などなど。

実行時フォーマットを使う

素直に実行時にやる方法です。

#include <ranges>
#include <print>
#include <string>

void print_pack(auto&&... args) {
  std::string fmtstr = "";
  
  for (auto _ : std::views::iota(0ull, sizeof...(args))) {
    // デリミタはお好みで
    fmtstr += "{}, ";
  }
  
  std::vprint_unicode(fmtstr, {std::make_format_args(args...)});
}

int main() {
  print_pack(1, "str", 3.1415, 'c', true);
}
1, str, 3.1415, c, true, 

記述量自体は減りましたし実装も単純になりました。しかしこの場合、フォーマット文字列のチェックがコンパイル時に行われなくなるため、例えばパックの中にフォーマットできない型が含まれていても実行時エラーになります。

std::tie()

古来より、パラメータパックを扱う際にはstd::tupleとその周辺ユーティリティが便利に活用できることが良く知られています。そして、C++23からはstd::tupleのフォーマットサポートが入るため、std::tupleはそのままフォーマット可能になります。

したがって、tuplestd::tie())を利用することで一番シンプルに書くことができます。

#include <ranges>
#include <print>
#include <string>

void print_pack(auto&&... args) {
  std::print("{}", std::tie(args...));
}

int main() {
  print_pack(1, "str", 3.1415, 'c', true);
}
(1, "str\u{0}", 3.1415, 'c', true) 

文字列出力がエスケープされているのはたぶんclangの実装バグです。本来エスケープされないのが正しいはず。

一番短くかつ単純でありながら、この方法であれば非パック引数と組み合わせることもできるしstd::tuple用のフォーマットオプションを使用することもできます。

ただし、std::tupleのフォーマットサポートがC++23からなので、C++20のstd::format()ではこの方法は使えません。前の2つのどちらかで頑張りましょう。

なお、std::tie(args...)ではなくstd::tuple(args...)を使用しても同じことは達成できますが、tupleを直接構築してしまうと各要素がコピーされるので注意が必要です。

std::tupleのフォーマットオプション

std::tie()による方法であれば、限定的なものではあるもののstd::tupleのためのフォーマットオプションを使用することができます。

まず、nオプションによって囲み文字(())を省くことができます。

void print_pack(auto&&... args) {
  std::println("{:n}", std::tie(args...));
  std::println("{{{:n}}}", std::tie(args...));
}
1, "str\u{0}", 3.1415, 'c', true
{1, "str\u{0}", 3.1415, 'c', true}

std::tuple固有のオプションはこれくらいで、残りは整数型等他の型と同じく幅指定に関するオプションが使用できます。

void print_pack(auto&&... args) {
  std::println("|{:*<40n}|", std::tie(args...));
  std::println("|{:+>40n}|", std::tie(args...));
  std::println("|{:-^40n}|", std::tie(args...));
}
|1, "str\u{0}", 3.1415, 'c', true********|
|++++++++1, "str\u{0}", 3.1415, 'c', true|
|----1, "str\u{0}", 3.1415, 'c', true----|

幅と寄せ、埋め文字のオプション指定はtupleの要素型毎ではなくtupleの全体に適用されます。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で112本あります。

もくじ

P0493R5 Atomic maximum/minimum

std::atomicに対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()を追加する提案。

以前の記事を参照

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

  • 文言の変更の一部を削除
  • floating point operationsについてのNoteを追加
  • polyfill実装の例を修正しセクションを移動
  • 実装に関するnoteを追加
  • 修正されたpolyfill実装によってベンチマーク結果を更新

などです。

この提案は2024年3月に東京で行われた全体会議においてC++26に向けて採択されています。

P0843R10 inplace_vector

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

以前の記事を参照

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

  • unchecked_append_rangeを削除
    • これによって追加される価値はほとんどなかった(挿入される全要素にわたって1つの分岐が償却されるだけ)
  • 要素の挿入中に例外が発生した場合でも正常に挿入された要素は保持されることを指定するために、要素の変更に関する記述を更新。
  • 要素を変更する操作の例外安全性保証の議論とLEWGの議論の結果を追記
  • allocator awareにしない
  • 容量超過時にbad_allocを投げる
  • 別のヘッダに分離する
  • Fallible APIとしてtry_append_range()を追加
  • イテレータ消去操作を [vector.erasure] から [vector.modifiers] に移動
  • いくつかの編集メモを更新
  • [vector.modifiers]のミスを修正
  • resize()から不要なComplexity句を削除

などです。

P0876R15 fiber_context - fibers without scheduler

スタックフルコルーチンのためのコンテキストスイッチを担うクラス、fiber_contextの提案。

以前の記事を参照

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

  • N4971にリベース
  • スレッドとファイバーの関係を明確にするセクションを追加
  • "制御の単一フロー"の定義をファイバーのために借用
  • [stacktrace.general]を参照して、状態としての"制御の流れ"を明確にするNoteを追加
  • スタックトレースの“invocation sequence”が実行スレッドではなくファイバーを参照するように変更
  • スレッドの定義を、ファイバーを実行する実行エージェントに変更
  • ファイバーが空のfiber_contextインスタンスを返して終了する場合、std::terminate()が呼び出されることを明確化
  • constexpr fiber_context::current_exception_within_fiber()を追加
  • "関数呼び出しスタック" の定義を削除
  • 式評価の競合の定義の変更を削除
  • プログラム内の2番目のファイバーに関するNoteを削除
  • "fiber_contextinstance"を"fiber_context object"に変更
  • "method"を"member function"に変更
  • 内部相互参照から段落番号を削除
  • 緑色にしていない新しいテキストの編集指示を明確にした
  • 安定ラベルに[fiber.context]を使用
  • [fiber.context]に唯一残っていた前文セクションを、"Empty vs. Non-Empty"から"Preamble"に変更
  • 空でないfiber_contextオブジェクトとサスペンドされたファイバーの1:1関係を"Preamble"に移動
  • empty()operator bool()で"Effects: Equivalent to return "を使用
  • main()の代わりにmainを参照

などです。

P1061R7 Structured Bindings can introduce a Pack

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

以前の記事を参照

このリビジョンでの変更は、2023年のvarnaでの議論の結果を文言に対して適用した事です。

P1144R10 std::is_trivially_relocatable

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

以前の記事を参照

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

  • AbseilやThrustなどでの実装経験を追加
  • operator=の妥当性についてのセクションを追加
  • 特殊化されたアルゴリズムのExecutionPolicyを取るオーバーロードの文言を追加
  • rangeオーバーロードが無い理由を追加
  • "trivially relocatable class"という言葉を[class.prop]に移動し、[[trivially_relocatable]][[no_unique_address]]の文言の類似性を高めた

などです。

P1729R4 Text Parsing

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

以前の記事を参照

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

  • std::formatとの整合性のために、scan_args_forscan_argswscan_argsに置き換え
  • ranges-v3(tail_view)からの命名を参考に、borrowed_ssubrange_tborrowed_tail_subrange_tに改名
  • std::scanのためのformat_stringscan_format_stringに置き換え、Rangeテンプレートパラメータを追加
  • 入力範囲とscanの引数の互換性をコンパイル時にチェックするようにした
  • (v)scan_result_typestd::scanstd::vscanの戻り値型)を説明専用にした
  • visit_scan_argを削除
    • P2637に従って、代わりにstd::variant::visitを使用する
  • SG9の投票に基づいて、stdinサポートに関する議論を追加
  • 文字列エンコーディングのエラーを、garbage-in-garbage-outの代わりにエラー文字列にする
  • フィールド幅に関する議論を追加
  • forward_rangeを必須とする根拠として例を追加

などです。

P2047R7 An allocator-aware optional type

Allocator Awarestd::optionalである、std::pmr::optionalを追加する提案。

以前の記事を参照

このリビジョンでの変更は、特殊メンバ関数swap()に関する設計上の考慮事項の追加と、モチベーションと要約の改善です。

P2075R4 Philox as an extension of the C++ RNG engines

<random>にPhiloxというアルゴリズムベースの新しい疑似乱数生成エンジンを追加する提案。

以前の記事を参照

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

  • counter_based_engineベースのアプローチを削除
    • 残りの1つはPhilox固有のもの
  • 関連するAPI制限をMandatesセクションに移動
  • synopsysを他のエンジンと整合させた(ストリーミング演算子を追加)
  • 実際の10000番目の値を対応するセクションに追加

などです。

P2249R5 Mixed comparisons for smart pointers

P2249R6 Mixed comparisons for smart pointers

スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。

以前の記事を参照

R5での変更は

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

  • 提案の範囲を縮小
    • ユーザー定義のオーバーロードとの衝突を避けるために、提案する演算子は生ポインタとの比較でのみ動作するように変更

などです。

P2299R4 mdspans of All Dynamic Extents

これはどうやら間違って公開されたようで、内容はR3と同一です。

P2389R0 dextents Index Type Parameter

std::dextentsから、整数型の指定を省略する提案。

std::dextentsstd::mdspanのインデックス指定のためのもので、次元数だけを静的に指定しておいて、次元ごとの要素数は動的に指定するものです。

import std;

// int型2x2行列
using imat22 = std::mdspan<int, std::extents<std::size_t, 2, 2>>;

// int型4x3行列
using imat43 = std::mdspan<int, std::extents<std::size_t, 4, 3>>;

// int型2次元行列
using imatnn = std::mdspan<int, std::dextents<std::size_t, 2>>;

int main() {
  int data[] = { ... };

  // 2x2行列
  imatnn mat22{data, 2, 2};
  // 4x3行列
  imatnn mat43{data, 4, 3};
}

すなわち、dextents<size_t, 2>extents<size_t, dynamic_extent, dynamic_extent>の略記です。

当初のmdspanではextents/dextentsの使用する整数型(インデックス型)はstd::size_tで固定でしたが、P2533によってそれが変更可能となり、それに伴ってextents/dextentsはテンプレートパラメータリストの最初で使用する整数型を受け取るようになりました。

特にdextentsではこれによって、本来できていたdextents<2>dextents<std::size_t, 2>のように書かなければならなくなり、わずらわしさが増加しています。

インデックス型のカスタマイズは重要な機能ですが、多くのユーザーはその変更を考慮する必要が無いため、dextentsはデフォルトでstd::size_tを使用するようにしておこうとする提案です。

// これを
using imatnn = std::mdspan<int, std::dextents<std::size_t, 2>>;

// こう書けるようにする
using imatnn = std::mdspan<int, std::dextents<2>>;

この実現方法としては、ソースの破壊的変更を受け入れてstd::dextentsのテンプレートパラメータを入れ替えることを提案しているようです。

namespace std {
  // 現在の宣言
  template <typename IndexType, std::size_t Rank>
  using dextents = ...;
}

これは、現在std::dextents<std::size_t, 2>と書いているところをstd::dextents<2, std::size_t>と書くようにしなければならなくなるので、破壊的変更となります。

LEWGのレビューでは、破壊的変更を回避してstd::dimsという新しいエイリアステンプレート?を追加してこの問題の解決とすることで合意が取れているようです。

namespace std {

  template <typename IndexType, std::size_t Rank>
  using dextents = ...;

  // 追加
  template <std::size_t Rank, typename IndexType = std::size_t>
  using dims = dextents<IndexType, Rank>
}

P2422R0 Remove nodiscard annotations from the standard library specification

規格署における標準ライブラリの関数から、[[nodiscard]]を取り除く提案。

operator new等をはじめとして、現在の標準ライブラリの一部の関数にはその戻り値を捨てることが望ましくないことから[[nodiscard]]が付加されています。この提案は、標準ライブラリの規定としてそれを取り除くとともに、今後も追加しないことを提案するものです。

その理由としては

  • その注釈を行うかどうかは、実装品質の問題
  • 何も指定しないため、規格書にあるべきかは疑問
  • 委員会の時間を消費することなく、一律的に利用可能にすることができる
  • 一律的に利用可能にするためには、実装の分析と経験が必要
  • この注釈を付加することが適切であるかの判断は場合によって困難であり、実装後の経験によって変更される可能性がある

としています。

[[nodiscard]]の注釈を付加することを決定するためには想像よりも多くの作業と時間を必要とする一方で、それによるメリットはあまり大きくなく、規格書に対してではなく各実装に対してその検討を促してほしい、という事のようです。

この提案では現在標準ライブラリの関数に付加されている[[nodiscard]]をすべて取り除くとと主に今後も付加しないことを基本とし、代わりに別の文書で[[nodiscard]]を付加することを推奨する標準ライブラリ中の関数をまとめておくことを提案しています。

P2643R2 Improving C++ concurrency features

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

以前の記事を参照

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

  • LEWGのメンバ向けに例と説明を更新
  • condition_variableとの一貫性のために、try_wait_fortry_wait_untilの名前をwait_forwait_untilに変更
  • wait_with_predicatecondition_variableのセマンティクスを使用するように変更
  • try_waitconst noexceptwait_for/wait_untilconstだが例外を投げる可能性がある
  • アトミックな待機操作のリストを更新
  • タイムアウトによって例外を投げる可能性のあるwait_for/wait_untilAPIからnoexceptを削除
  • latchAPI拡張についての文言を追加
  • 述語を受けてその結果によって待機する系のAPIの名前の接尾辞に_with_predicateを付加

などです。

P2686R3 constexpr structured bindings and references to constexpr variables

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

以前の記事を参照

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

  • “usable in constant expressions”の定義の問題を回避するために、[basic.def.odr]に移動
  • ローカルのstatic constexprな参照は、自動変数を参照しないことを明確化
  • ラムダ式内部のconstexpr参照がその外側の自動変数を参照できないことについての説明の改善

などです。

この提案はCWGのレビュー中ですが、実装経験が上がってくるのを待機しているようです。

P2688R1 Pattern Matching: match Expression

C++へのパターンマッチング導入に向けて、別提案との基本構造の違いに焦点を当てた議論。

以前の記事を参照

このリビジョンではR0に比べて提案をより具体化したとのことです。

C++23の機能を使用するようになっている他、型によるマッチングでは<>が不用になっています。基本的な構文はR0と変わっておらず、epxr match { pattern }のような構文を使用しています。

また、設計やその説明についての文書がかなり拡充されています。

P2721R0 Deprecating function

std::functionを非推奨にする提案。

std::functionの設計には、初期のころからいくつかの問題点が指摘されていました。

// the constness bug of std::function: 
// consider: 
auto lambda = [&]() mutable { … }; 
lambda(); // ✔
const auto & r{lambda}; 
r(); // ❌ lambda::operator() is mutable => can’t be called via const &!

// but: 
function<void(void)> func{lambda}; 
func();   //✔
const auto & cref{func}; 
cref();   // ✔ ⚡ func::operator() is const => can invoke mutable lambda through const &! 
          // this breaks the fundamental guarantee that concurrently calling const member functions is safe!

このようなconst性伝播の問題の他、同様にnoexceptを指定できないことやムーブオンリーなファンクタを格納できないなどの問題がありました。

このような問題を解決したものとしてC++23でstd::move_only_functionが追加され、C++26では呼び出し可能なものの非所有参照であるstd::function_refが追加されました。std::function_refstd::move_only_functionの設計を受け継いでおり、std::functionの問題点を解消しています。

std::move_only_functionはムーブのみ可能であり、コピーのみ可能なファンクタに対してや関数ラッパをコピーしたい場合などには従来のstd::functionを使用する必要があり、そのような場合に対してstd::move_only_functionの改善を適用するためにstd::copyable_functionが追加されました。

std::copyable_functionが追加されたのはstd::functionを変更することが後方互換性維持のために不可能だったためです。

こうしてC+26ではstd::functionの問題点が解消された新しい関数ラッパ型が3種類用意されており、std::fucntionを使用する意味はほぼ無くなっています。また、std::functionの同等物が全部で4種類ある状況はAPiを複雑化しており、std::functionを非推奨にすることでその複雑化を多少抑えることができます。

C++26のstd::copyable_function導入と同時にstd::functionを非推奨にすることで、古い問題があるものと新しい改善されたものが同時に提供される期間を無くすことができ、ユーザーに対する明確なメッセージになるはずです(これには、C++11におけるauto_ptrunique_ptrの前例があります)。

この提案は、これらの理由からstd::functionを非推奨化しようとするものです。

LEWGでは将来的に非推奨化の必要はあるもののまだ早いということでこの提案は否決されています。

P2727R4 std::iterator_interface

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

以前の記事を参照

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

P2746R4 Deprecate and Replace Fenv Rounding Modes

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

以前の記事を参照

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

  • fma()to_chars()を追加
  • APIをフリー関数から構造体ベースに変更
  • この提案の第一の、そして唯一絶対的に保証された目標は、真の値をバウンドする方法を提供することである事を明確化
  • そして第二の目標は、適切なハードウェア実装が提供されている場合にIEEE標準によって提供される丸め保証を公開すること(を明確化

などです。

このリビジョンでのAPIは次のようになっています

struct rounded {
  float_round_style round_style; // Exposition only.

  // std::float_round_style列挙値を渡して、その丸めモードによる丸めインスタンスを構築
  rounded(float_round_style rs = round_to_nearest);

  // IEC 60559に準拠していればtrueを返す
  // 実行時まで分からない場合があるためconstexprではない
  // これがtrueになる場合このクラスは、IEEEの厳格な評価ルールを強制するのに使用できる
  template<floating_point F>
  static bool conforms_to_iec_60559();

  // rounded(rs)のように渡された丸めモードrsによって四則演算を行う関数群
  // 理想的には全てconstexprである必要があるが、実装負荷が高いため延期
  template<floating_point F>
  constexpr F add(F x, F y);

  template<floating_point F>
  constexpr F sub(F x, F y);

  template<floating_point F>
  constexpr F mul(F x, F y);

  template<floating_point F>
  constexpr F div(F x, F y);
  
  template<floating_point F>
  F fma(F x, F y, F addend);

  // G型の浮動小数点数を別の型Fの値に丸める
  template<floating_point F, floating_point G>
  F cast(G x);

  // 定数を表す文字列sをそれが表す浮動小数点数値に変換する
  // 引数がサポート対象の形式ではない場合std::format_error例外を送出する
  template<floating_point F>
  constexpr F make(string s);

  // コンストラクタで指定された丸めモードを使用した丸めによって、浮動小数点数値を文字列化する
  template<floating point F>
  to_chars_result to_chars(char* first, char* last, F value, chars_format fmt, int precision);

  template<floating_point F>
  F sqrt(F x);
  
  // コンストラクタで指定された丸めモードによってxを整数に丸め、Rの値として返す
  template<typename R, floating_point F>
  R rint(F x);

};

この構造体はfloatdoubleの値をラップした値クラスのようなものではなく、この構造体に丸めモードを指定して構築したうえで各メンバ関数を呼び出すことでその丸めモードに従った正確な計算等を行うことができます。

// 正の無限方向の丸めによって0.1を表すdouble値を取得
rounded(std::round_toward_infinity).make<double>("0.1");

// 0方向への丸めによって足し算を行う
rounded(std::round_toward_zero).add(0.1, 0.3);

P2758R2 Emitting messages at compile time

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

以前の記事を参照

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

  • SFINAEへの対応を明確化
  • APIを1つのエラー関数に削減
  • 警告関数を追加

などです。

このリビジョンでは提案する関数は次の3つになりました

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

以前のリビジョンのconstexpr_fail_str()constexpr_error_str()にリネームした上で、警告メッセージを出力するためのconstexpr_warning_str()が追加されました。

P2781R4 std::constexpr_wrapper

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

以前の記事を参照

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

  • 単行演算子オーバーロードのIFNDRを修正
  • ++-=などの値を変更する演算子を追加
  • ADL対策のためのデフォルトテンプレートパラメータ名を説明専用にした
  • constexpr_vconstant_wrapperに、c_cwに変更

などです。

P2786R4 Trivial Relocatability For C++26

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

以前の記事を参照

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

などです。

P2822R0 Providing user control of associated entities of class types

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

ADLでは、非修飾名(名前空間指定なし)の関数呼び出しに際して、関数に渡されている引数の型の情報を用いてその関数を見つけようとします。

template<typename T>
void adl_example(T a) {
  NS::f(a); // 修飾名呼び出し、ADLは行われない
  f(a);     // 非修飾名呼び出し、ADLが行われる
}

この例では、adl_example()内部からfという名前を探索する必要があり、f(a)の呼び出しでは非修飾名探索が行われます。ここで何も見つからない場合、次にADLによって引数aの型Tの関連情報からfという推定関数名を探索します。

ADLは関数オーバーロードの探索のために必須であり、同時に関数によるカスタマイゼーションポイントを提供する役目も担っています。

この時、ADLによって探索が行われる場所を決定するもののことを関連エンティティ(associated entities)と呼びます。これは、ADLによって関数呼び出しに渡されている引数型(クラス型)Tに対して、次のもので構成される集合です

  • T自身
  • Tの直接/間接の基底クラス
    • それらの関連エンティティは含まれない
  • Tがネストしたクラス型ならば、Tを直接囲んでいるクラス型
    • 直接のもののみで、その外側のクラス型は含まれない
    • 囲んでいるクラス型の関連エンティティは含まれない
  • Tがクラステンプレートの特殊化であれば
    • そのテンプレート引数をAとすると、Aの関連エンティティ
    • そのテンプレートテンプレート引数として使用されているテンプレート

ここでのエンティティとはほぼクラス型のことです。

これらの関連エンティティのそれぞれごとに、そのエンティティのフレンドとして宣言されたADL対象の名前を持つ関数を探索し、さらにエンティティの関連名前空間(そのエンティティを囲う最も内側の非インライン名前空間とそのエンティティを囲う最も内側のインライン名前空間の集合)でもADL対象の名前を持つ関数を探索します。

この時、関連エンティティ及び関連名前空間の定義が複雑かつ予測しづらいことによって予期しない関数が呼び出される場合があり、長年問題になってきました(C++ ADLとかでググれば事例をいくつも見つけられるでしょう)。特に、クラステンプレートのテンプレート引数が関連エンティティに含まれてしまうというのが驚きの呼び出しに繋がります。そのほかにも

  • 関連エンティティが増加することによるコンパイル時間増大
  • 同名の候補がいくつも見つかる場合にエラーメッセージが膨大になる
  • 予期しないテンプレートのインスタンス化が起こる
    • 関連エンティティとしてTの基底クラスを見に行くため、Tインスタンス化される

などの問題があります。これらの解決のための提案もいくつか出されたものの、後方互換の問題や実装経験の乏しさなどもあり、解決策は確立されませんでした。

この提案ではADLによる問題の解決のために、クラス型の宣言ごとに関連名前空間を明示的に指定できるオプトイン構文を用いることによって、ADLの関連エンティティを制御することができるようにしようとするものです。

提案しているのは、namespace(...)というものをクラス型宣言内でクラス名の後においておくものです。...にはADLの関連エンティティもしくは関連名前空間として含めたいものを指定し、空にすることもできます。

// 関連エンティティは自身のみ
template<typename T>
class example namespace()
{ /* ... */ };

// some_containerの関連エンティティとして、要素型Tを追加
template<typename T, typename Alloc>
class some_container namespace(T)
{ /* ... */ };

// 関連エンティティのルールは現在と同様
class normal_adl
{};

namespace(...)に指定することのできるエンティティは型名とクラステンプレート名、および名前空間名の3つのみです。

あるクラス型Cに対して、namespace(Ts...)によって指定された型/テンプレート名Ts...Cの関連エンティティの設定を上書きし、CおよびTs...のみが関連エンティティとして扱われるようにします。ただし、何かを追加した時でもnamespace()のように空として指定した時でも、それが指定されているクラス型C自身は常に関連エンティティであり続けます。

namespace(...)名前空間名が指定されている場合、ADL探索対象の関連名前空間に指定した名前空間が追加されます。こちらは関連名前空間を上書きせず、関連名前空間は指定したものに加えて、通常通りに関連エンティティの関連名前空間が含まれます。

クラス宣言時にnamespace(...)を指定しなければ、関連エンティティの設定を現在と同じになります。すなわち、既存のコードの動作を変更しません。

このように、関連エンティティの決定を上書きしなおかつ限定することによって、ADL時の探索対象を限定するとともに、基底クラスやテンプレートパラメータなどの含まれてほしくないものが自動で含まれてしまうのを防ぐことができます。これにより、関連エンティティの検出の手間が減るとともに関連エンティティの削減によってコンパイル負荷を減らすことができ、いたずらに同名候補が増えないことでエラーメッセージを削減でき、意図しないインスタンス化も回避することができます。

namespace(...)には型名など指定可能なものをリストとして複数指定することができます。

// 複数の型を関連エンティティに追加
template<typename First, typename Second>
struct pair namespace(First, Second)
{ /* ... */ };

// Ts...に含まれる型だけを関連エンティティに追加(基底クラスは含まれない)
template<typename... Ts>
struct tuple namespace(Ts...) : detail::tuple_base<Ts...>
{ /* ... */ };

また、本来は関連エンティティとして扱われないような型を関連エンティティに含まれるようにすることもできます。

namespace std {
  // NTTP Xの型を関連名前空間に追加する
  template<auto X>
  struct constexpr_v namespace(decltype(X)) {
    ...

    constexpr operator decltype(X) const { return X; }

    ...
  }
}

template<typename T>
void f(T t) {
  std::cout << t << '\n'; // ok

  auto cv = std::c_<T{...}>;

  std::cout << cv << '\n';  // ok、Tを関連エンティティとするADLでTの関数を検出し
                            //     暗黙変換によってTの関数が使用できる
}

提案中のstd::constexpr_vでは、ラップしているコンパイル時定数の型が関連エンティティに含まれないことから、ラップしているNTTP値のインターフェースを使用できない問題が指摘されています。これを回避するために、テンプレートパラメータを増やしてNTTP値の型を関連名前空間に入れようとしていますが、この提案ならよりスマートにそれを解決できます。

また、全く関係の無い型を指定することもできます。

struct string_like namespace(std::string_view)
{
  
  ...

  operator std::string_view() const noexcept;
  
  ...
};

void f(string_like mystr, std::string_view strv) {
  std::cout << mystr; // ok、string_viewの<<がADLによって発見され暗黙変換によって使用される
  
  // mystrには比較演算子が定義されていないとしても
  bool b = strv == mystr; // ok、string_viewの==が使用される
}

このstring_likeは文字列を保持する簡単な型であるとして、現在はstring_likeの値に対するADLからではstd::string_viewのために定義されているstdの関数を発見することはできません。この提案の後、このようにstd::string_viewを関連エンティティとして追加することで、ADLによってそれを発見し、暗黙変換によって利用することができます。

P2835R3 Expose std::atomic_ref's object address

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

以前の記事を参照

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

  • 背景セクションを追加
  • このAPIが元のatomic_refの提案に含まれていなかった理由の履歴を追加
  • API名をaddress()に変更
  • 戻り値型をconst void*に変更
  • 戻り値型についてuintptr_tに対する根拠を追加
  • API名について、get()data()に対する根拠を追加
  • 新しいユースケースと例を追加
  • Discovery Patternの例を削除

などです。

P2845R6 Formatting of std::filesystem::path

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックに従って、ジェネリックフォーマットのサポートを追加したことです。

このリビジョンでは、pathのフォーマットオプションとしてgを追加し、これが指定されている場合はパス文字列を.generic_string<filesystem​::​path​::​value_type>()から取得し、gが無い場合は.native()からパス文字列を取得します。これは、Windowsにおいてのみ差が生じ、gオプションを付けた場合はパスの区切り文字が\ではなく/になります。

この提案は2024年3月に東京で行われた全体会議において採択され、C++26Wdに導入済みです。

P2863R4 Review Annex D for C++26

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

以前の記事を参照

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

  • 2023年のKona会議のレビューが記録されていることを確認
  • N4971のWDに取り込まれた提案のステータスをDONEに更新
  • 12月のLEWG投票の結果を追加
  • その他追跡中の提案のステータス更新

などです。

P2875R3 Undeprecate polymorphic_allocator::destroy For C++26

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

以前の記事を参照

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

  • typo等の修正
  • セマンティクスの変更を反映して、Analysisセクションを書き直した
  • ベースとなるWDをN4971に変更
  • 2024/01/23に行われたLEWGテレコンミーティングの結果を追記
  • 移行がどのようなものになるかのリクエストされたコード例を追記

などです。

この提案は2024年3月に東京で行われた全体会議において採択され、C++26WDに導入済みです。

P2893R2 Variadic Friends

friend宣言でのパック展開を許可する提案。

以前の記事を参照

このリビジョンでの変更は、CWGのフィードバックを受けての提案する文言の改善と機能テストマクロを追加したことです。

この提案は2024年3月に東京で行われた全体会議において採択され、C++26WDに導入済みです。

P2900R5 Contracts for C++

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

以前の記事を参照

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

  • 提案する文言を追加
  • contract_assertは式ではなく文になった
  • 仮想関数に対する契約注釈(pre/post)はill-fomred
  • contract_violation::will_continue()を削除
    • P3073R0で提案されたことの一部
  • detection_mode::evaluation_undefined_behavior列挙値を削除
    • P3073R0で提案されたことの残り
  • 構文構造に対する関数契約指定(function contract specifier)とそれが導入するエンティティに対する関数契約アサーション(function contract assertion)の個別の用語を導入
  • 関数契約指定のシーケンスの等価性に関するルールを追加
  • 再宣言時に関数契約指定シーケンスをリピートすることが可能になった
  • return nameをresult nameに変更
  • 契約アサーションに対する属性を指定する構文上の場所を追加
    • P3088R1が採択
  • 関数テンプレートの特殊化のセクションを追加
  • テンプレート内のfriendセクションを追加
  • 設計原則のセクションを拡張
  • 様々な細かい説明を追加・改善

などです。

P2927R1 Observing exceptions stored in exception_ptr

std::exception_ptrを再スローせずに例外オブジェクトの取得を試みる関数の提案。

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

  • try_cast<E>(exptr)EはCV修飾なしのオブジェクト型でなくてはならなくなり、返されるポインタはconst E&で宣言されたcatch節にマッチする例外オブジェクトのポインタとなった
    • const参照によるcatchを仮定する
  • try_cast<E>(exptr)Eにポインタ型を指定できなくなった
    • 派生クラスから基底クラスへのポインタ変換など、ポインタの調整が入る場合に一時オブジェクトを返すことになるので、それを回避する

このリビジョンでのtry_castの宣言例

namespace std {
  template <class E>
  const E* try_cast(const exception_ptr& p) noexcept;
}

P2964R0 Allowing user-defined types in std::simd

現在のstd::simd<T>Tは算術型や複素数型など標準にある数値型に限られています。この提案は、それを少し緩和してユーザー定義型(プログラム定義型)をSIMD要素として使用可能にする提案です。

用途としては、特定の型に保存された信号データや動画像データ、あるいは飽和整数型や固定小数点数型などの特殊な数値型をstd::simd上で取り扱えるようにすることを目的としています。

この提案のアプローチは、カスタマイゼーションポイントを通して必要な場所に特別な動作を注入するものです。ただし、std::simdで使用可能な型としてはトリビアルコピー可能であることを要求しています。

この提案ではまず、std::simdAPIのうち、ユーザー定義型で動作させるためにカスタムが必要なものとそうでないものなどを次の4つに分類します

  1. Basic
    • 要素型を使用するために提供されなければならない関数
    • 四則演算など
  2. Custom
    • 汎用的に実装できるが、より効率的な実装のためにカスタマイズを提供することもできる関数
    • 符号反転(0から引くというデフォルトを提供できるが、浮動小数点数型の符号ビット反転のように効率実装が可能)など
  3. Copy
    • ビット列が何を表しているかを知らなくても、std::simdの値間で要素を移動することができる
    • トリビアルコピー可能を要求することで実現
  4. Algorithm
    • 何らかの処理を実現するために他の関数(上記3つ)を利用するもの
    • 必要な関数をユーザー定義型が提供していない場合は使用できなくなる
    • アルゴリズムはカスタマイゼーションポイントを提供しない

この中で、カスタマイゼーションポイントの用意が必要なものは上2つのBasicとCustomに分類される関数です。std::simdについては次の関数群が該当しています

  1. Basic
  2. Custom
    • コンストラク
      • basic_simd(basic_simd<U>) : TUが異なる場合変換のためのカスタマイゼーションポイントが必要
    • 単項演算子
      • operator-
      • operator!
    • 比較演算子
      • operator!=
    • フリー関数
      • min
      • max
      • clamp
      • 数学関数

この提案では、これらの関数に対してカスタマイゼーションポイントを用意しておくことでユーザー定義型でもstd::simdが動作可能なようにしようとしています。カスタマイズが提供されていない場合、Basicな関数はコンパイルエラーとなり、Customな関数はデフォルトの実装が使用され実行されます。

上記関数群とは少し性質が異なりますが、そもそもstd::simdでユーザー定義型を扱うためにはそのストレージをどう用意したらいいのかについての知識が必要となります。この提案では、std::simd_custom_type_storage<T>というクラステンプレートを用意して、これをユーザー定義型で特殊化したうえでその入れ子型でストレージ型を指定することを提案しています。

namespace std {
  // stdで提供
  template<typename T>
  struct simd_custom_type_storage;
}

// ユーザー定義型に対しては特殊化して使用
template<>
struct std::simd_custom_type_storage<user-defined-type> {
  using value_type = /* some bit container */;
};

あるいは(もしくは追加で)、ユーザー定義型が直接これを提供するようにする方法も考えられます

struct user-defined-type {
  using simd_storage_type = /* some container */;
};

このsimd_storage_typeはユーザー定義型(ここでのuser-defined-type)と同じサイズであり、相互にstd::bit_cast可能である必要があります。すなわち、simd_storage_typeuser-defined-typeの範囲となるものではなく、user-defined-typeを表現できる何らかのストレージ型です(ユーザー定義型自身でもok)。例えば、8bit数値8個分のデータに対して64bit整数型を使用する、ようなことが可能です。

このストレージ型のカスタマイズはstd::simdでユーザー定義型を利用可能にするための必須の操作です。

そのうえで、先程上げた各種単項/二項演算のカスタマイズは、演算子オーバーロードではなく特定の名前付き関数テンプレートのカスタマイズによって行われます。

// std::simdに定義されている二項operator+オーバーロード
constexpr friend 
  basic_simd operator+(const basic_simd& lhs, const basic_simd& rhs)
    requires (details::simd_has_custom_binary_plus || details::element_has_plus)
{
    if constexpr (details::simd_has_custom_binary_plus)
      return simd_binary_op(lhs, rhs, std::plus<>{});    // ユーザー定義型を呼び出す
    else
        /* impl-defined */
}

simd_binary_op()は二項演算のカスタマイズを受ける統一的な名前の関数テンプレートです。このように二項演算でまとめてしまうことで、二項演算の種類ごとに別名のカスタマイゼーションポイントが生えてしまうことを抑止しています。二項演算の種類は第三引数に透過二項演算ファンクたオブジェクトを渡すことで識別します。

単項演算に対しても同様に、simd_unary_op()という名前のカスタマイゼーションポイントを提供できます(こちらはタグ型を用意する必要がありますが)。

シフト演算子に関しては対応する既存のファンクタ型が定義されておらず、何かしら対応が必要ですが、この提案(intel内部での実装)では専用のファンクタ型(std::simd_shift_left<>など)を追加することを採用しています。

残ったフリー関数については、ADLを利用した単純な関数オーバーロードによってカスタマイズすることを提案しています。

template<typename Abi>
constexpr auto abs(const basic_simd<user-defined-type, Abi>& v) {
  return /* special-abs-impl */;
}

これは既存のstd::complex等と共通することです。

この提案の内容はまだ、intelにおける社内実装における設計選択を説明する側面が強く、カスタマイゼーションポイントの提供方法あるいは提供する対象についてはは固まっていません。

P2988R2 std::optional<T&>

P2988R3 std::optional<T&>

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

以前の記事を参照

R3はおそらくR2と同一で、間違えて公開されているようです。このリビジョンでの変更は設計ポイントを明確化したことなどです。

LEWGのレビューにおいては、.value_or()T&を返すのが問題となっているようです。代替の戻り値型として、結果は弱いもののTを返すことに一番合意が取れています

P2989R1 A Simple Approach to Universal Template Parameters

より限定されたユニバーサルテンプレートパラメータの提案。

以前の記事を参照

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

  • 実装経験の更新
  • コード例の修正とCompiler Explorerのリンクを修正
  • ユニバーサルテンプレートパラメータから推論される値テンプレートパラメータの型を推論する新しいソリューションの提案
  • CPOの例を追加

などです。

ユニバーサルテンプレートパラメータに対して式が渡された場合、それは非型テンプレートパラメータ(NTTP)に推論される必要があります。その場合に、そのNTTPの型をどのように推論するかが問題になるようで、この提案ではいくつかのオプションを提示しています。

  1. autoセマンティクスの使用
    • 参照の推論ができなくなる
  2. decltype(auto)セマンティクスの使用
    • R0で提案していたオプション
    • decltype()の癖(a(a)で推論結果が異なる)に影響を受ける
  3. decltype(auto)セマンティクスを使用し、id式の参照修飾を推定する
    • a(a)の結果が一貫する(参照になる)
    • 受け取ったNTTPを別のテンプレートに転送する場合に不整合が生じる
  4. id式の型推論を延期する
    • 3の方法でdecltype(auto)との一貫性を保つために、ユニバーサルテンプレートパラメータに渡されたid式の型の推論は、それが別のテンプレートの値パラメータとして使用されるまで遅延する
    • 考案されたばかりであるため、実装やさらなる検討の時間が無かった

この提案での新しいソリューションとは4番目の方法のことで、筆者の方はこれを押しています。

P2992R1 Attribute [[discard("reason")]]

式の結果を破棄することを明示する[[discard]]属性の提案。

以前の記事を参照

このリビジョンでの変更は、式に対する属性指定の提案を別の提案(P3093)へ分離したことなどです。

P2994R1 On the Naming of Packs

パラメータパックそのものを指定する構文を検討する提案。

以前の記事を参照

このリビジョンでの変更は、パックインデックスアクセスの構文が実際には動作するものではなかったため、提案の内容を変更した事です

このリビジョンでは次のような提案をしています

  • パックのインデックスアクセス : pack...[0]
  • 展開ステートメント : template for (auto x : ...pack)
  • 範囲スプライシング : [: ... r :](これは展開前のパックを生成する)

これでも、ある程度の均一性が得られるとしています。

P2996R2 Reflection for C++26

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

以前の記事を参照

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

  • qualified_name_of()を追加(name_ofとの連携のため)
  • 曖昧だったため、is_static()を削除
    • 代わりにhas_internal_linkage(), has_linkage(), has_external_linkage(), is_static_member()を追加
  • is_class_member(), is_namespace_member(), is_concept()
  • reflect_invoke()を追加
  • 既存の型特性に対応する関数を追加
  • 名前付きタプルや型付きリフレクションなどのサンプルの追加
  • 構文や定数評価順序、エイリアスやフリースタンディング等についての議論を追加

などです。

P3002R1 Policies for Using Allocators in New Library Classes

標準ライブラリ機能がアロケータを使用する際のポリシーの提案。

以前の記事を参照

このリビジョンでの変更は、P2267R1で提示されたポリシー提案の要件を満たすために必要なモチベーションと情報を整理し追加したことです。

P3004R0 Principled Design for WG21

原則に基づいた設計のプロセスを紹介する文書。

標準化委員会での議論プロセスにおいて、事前に確認しておくべき設計原則の見落としや軽視による議論の手戻りなどの問題の発生を防止し、提案に関する作業のための時間を節約するために新しい提案が事前に確認しておくべきポリシーを文書としてまとめておこうとする活動が現在なされています。

この文書は、そのようなポリシーを含めて事前に定められた原則に従って提案の内容を評価し設計を進めていく作業プロセスを紹介するものです。

この提案は

  • 4章 : 企業イベントにおける会場選択という作業を例にして、プロセスの使用方法を紹介する
  • 5章 : 4で紹介したプロセスの詳細な手順の説明
  • 6章 : 委員会においてそのプロセスを適用する例を紹介
  • 7章 : プロセスに基づいて提案の作業を進めるために、WG21の標準化会議がどのように開催されるべきかについての説明
  • 付録 : C++における実際の問題についてプロセスを適用する例をいくつか紹介

のような内容で構成されています。

P3005R0 Memorializing Principled-Design Policies for WG21

機能提案が満たすべきポリシーそのものについてのプロセスとフレームワークの提案。

P2979やP2267などによって、委員会での議論を効率化するためのポリシーを策定し文書化しておこうとする作業が行われています。この提案では、そうしたポリシーそのものの策定や修正、維持管理プロセスを提案するものです。

P3008R1 Atomic floating-point min/max

浮動小数点数型のstd::atomicにおけるfetch_max()/fetch_min()の問題を解消する提案。

以前の記事を参照

このリビジョンでの変更は、P0493のmin/max関連の削除を考慮して内容を更新、signaling NaNに関するC17からC23の文言変更を適用したことなどです。

P3016R2 Resolve inconsistencies in begin/end for valarray and braced initializer lists

std::valarrayと初期化子リストに対してstd::beginstd::cbeginを呼んだ場合の他のコンテナ等との一貫しない振る舞いを修正する提案。

以前の記事を参照

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

  • LWG Issue 3624とLWG Issue 3625の変更をマージ
  • コードの破壊について反論
    • std::begin(initializer_list)の非推奨期間が実装不可能であることを追加
  • valarray::iteratorを説明専用ではなくした
  • Historical Backgroundセクションを削除
  • initializer_list.empty()の前例としてP2613とLWG Issue 4035に言及
  • 機能テストマクロの追加

などです。

P3019R4 Vocabulary Types for Composite Class Design

P3019R5 Vocabulary Types for Composite Class Design

P3019R6 Vocabulary Types for Composite Class Design

動的メモリ領域に構築されたオブジェクトを扱うためのクラス型の提案。

以前の記事を参照

R4での変更は

  • indirectが所有するオブジェクトに対してコピー構築可能であることを要求するために制約を使用する
    • これによって、is_copy_constructible_vが誤解を招く結果を与えないようにする
  • indirectの比較を変更して、空の状態の比較を許可する
    • 比較は、boolautoを返す== <=>によって実装される
  • 空の状態を処理できないため、indirectstd::formatサポートを削除
  • 空の状態のオブジェクトのコピー、ムーブ、代入、swapを許可し、std::variantとの類似点についての議論を追加
  • uses-allocatorで何かを構築するコンストラクタを削除
  • TCpp17Destructible要件を満たす必要がある
  • 説明専用変数名の変更
  • 不完全型についての議論を追加
  • explicitコンストラクタについての議論を追加
  • 算術演算子についての議論を追加

R5での変更は

  • 強い例外保証を提供するために代入演算子の文言を修正
  • 空の状態の場合のハッシュについて欠落していた文言を追加

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

  • indirectstd::in_place_t引数を取るコンストラクタを追加
  • 空の状態の場合を考慮するためにスワップの文言を修正
  • コンパイラが導出するindirectの比較演算子を削除
  • 説明専用変数名の変更
  • スワップに例外保証動作動作に関する草案メモを追加

などです。

P3032R0 Less transient constexpr allocation

定数式における動的メモリ確保の制限を少しだけ緩和する提案。

C++20では定数式における動的メモリ確保が許可されましたが、定数式中で確保されたメモリはその定数評価の範囲内で解放される必要があり、実行時に持ち越すことはできません。

この制限はコンパイル時に確保したメモリを実行時から参照することを防止するためのものですが、実際には実行時から参照される可能性が無いにも関わらずコンパイル時のメモリ確保が禁止される場合があります。

以下例ではP2996R1で提案中の静的リフレクションを使用しますが、リフレクションを使用することはこの提案の本質の部分とはほぼ関係がありません。知っておくべきことは、式Eに対して^Estd::meta::info型のオブジェクトを返すことと、enumerators_of()という関数がstd::meta::infostd::vectorを返すことです。

namespace std::meta {
  using info = /* ... */;

  consteval vector<info> enumerators_of(info);
}

以下、例

int main() {
  constexpr int r1 = enumerators_of(^E).size(); // ✅

  return r1;
}

constexpr変数r1の初期化式は定数式であり、enumerators_of()から返されるstd::vectorはその評価内で破棄されるため、問題ありません。

constexpr int f2() {
  return enumerators_of(^E).size(); // ❌
}

int main() {
  constexpr int r2 = f2();

  return r2;
}

r2の初期化に関してはr1と同様ですが、f2()constexpr関数であり実行時からも呼ばれる可能性があるため、consteval関数呼び出しenumerators_of(^E)は即時関数コンテキストに無く、その実行のために新しい定数評価コンテキストを導入します。ただし、そのコンテキストの範囲はenumerators_of(^E)のみであり、enumerators_of(^E).size()ではなく、enumerators_of(^E)の戻り値はその定数評価コンテキストの外側で参照されるために生存するため、定数式で許可されません。

consteval int f3() {
  return enumerators_of(^E).size(); // ✅
}

int main() {
  constexpr int r3 = f();
  return r3;
}

f2()に対してf3()consteval関数であり、その本体内のすべてが即時関数コンテキスト内にあります。これによって、enumerators_of(^E).size()全体が定数式になり、そこで生成される一時std::vector<info>オブジェクトはその評価の内側で破棄されるため、問題なくなります。

template<class E>
constexpr int f4() {
  return enumerators_of(^E).size(); // ✅
}

int main() {
  constexpr int r4 = f4<E>();
  return r4;
}

f4()constexpr関数テンプレートであり、含まれる式enumerators_of(^E).size()consteval関数を呼び出しているのに定数式ではない(f2()の時と同様の理由によって)ため、関数テンプレートf4()は即時関数(consteval関数)として扱われ、結果としてf3()と同じ状態になるため問題ありません。

これは、constexpr関数テンプレート内に即時関数呼び出しが含まれている場合にconsteval関数に直さなくてもそれを呼び出せるようにするために(そのような関数呼び出しは、場合によっては単なるconstexpr関数呼び出しである可能性もあるため)、関数テンプレートがそのインスタンス化に際して適応的に即時関数に昇格される仕組みによって、f2()と異なった結果となります。

consteval int f5() {
  constexpr auto es = enumerators_of(^E); // ❌
  return es.size();
}

int main() {
  constexpr int r5 = f();
  return r5;
}

consteval関数の中でconstexpr変数を初期化しているため、esの初期化式は別の定数評価コンテキストを導入しています。このため、es初期化式の評価内でenumerators_of(^E)戻り値のvectorは破棄されなければならないが、されていないため拒否されます。ただしこの場合、consteval関数f5()の呼び出し内部でコンパイルvectorが破棄されることは確実です。

リフレクションを使用していることを除いて、これら5つの例のうち問題の内3つはC++23でも有効です。残りの2つは現時点でも拒否されるものの、実際に実行時までコンパイルstd::vectorが持ち越されることは無いため、本来問題ないはずの例です。

2つ目の例

constexpr int f2() {
  return enumerators_of(^E).size(); // ❌
}

enumerators_of(^E)は即時関数コンテキストの外側で呼び出される即時関数であり、定数式ではない即時関数呼び出しとなるため、その呼び出しは即時関数呼び出しとして扱われます、しかしその戻り値から呼ばれる.size()は単なるconstexpr関数(定数式)であるため、その即時関数呼び出しの一部ではありません。これによってenumerators_of(^E).size()はその評価に異なる2つのコンテキストを含んでしまい、そのコンテキスト間でstd::vectorを移行できないためエラーになります。

これを許可するには、単にenumerators_of(^E).size()全体を定数式として扱ってやればよく、enumerators_of(^E)の呼び出しが定数式になりうる最も近い囲み式(それを含む式)を定数式として扱うようなことをする必要があります。

5つ目の例

consteval int f5() {
  constexpr auto es = enumerators_of(^E); // ❌
  return es.size();
}

ではenumerators_of(^E)の戻り値std::vectorがその評価を超えて生存することでエラーとなっています。しかし、ここではそれを囲む関数がconsteval関数であるため、esは実行時に参照されることはありません。

これを許可するためには、定数評価内の動的メモリ確保はその評価内で解放される、もしくはその割り当てが即時関数コンテキスト内にある場合はそのコンテキストの終わりで解放する必要がある、のようにすればよさそうです。

まとめると、最初の5つの例のうち拒否される2つの例を許可するようにするには、2つの異なる変更が必要となります

  1. 定数式にならない即時関数呼び出しが定数式になるように、それを含むより大きな式を定数式として扱う
  2. 定数式における動的メモリ確保に関して、開放のタイミングが同じ評価内にある場合に加えて、同じ直接のコンテキスト内にある場合も許可する

しかし、この2つ目の変更についてはすぐに標準文言の変更を考えることができてその利益も大きいですが、1つ目の変更は文言が複雑になるとともに利益があまり大きくありません(関数をconstevalにするか、constexpr変数の初期化式にすれば回避できるため)。

従って、この提案ではこの2つ目の変更のみを提案しています。

ここまでの例で使用されているように、この変更は静的リフレクションを使用するにあたって遭遇することになるであろう問題を取り除くものでもあります。

P3045R0 Quantities and units library

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

この提案は以前に個別に提出されていた関連提案を1つにまとめて、設計の詳細説明を追加したものです。

それぞれの提案については以前の記事を参照

以降、物理量と単位のライブラリの標準化作業はこの提案で行われます。

P3047R0 Remove deprecated namespace relops from C++26

std::relopsを削除する提案。

std::relopsはクラス型に< ==演算子を定義しておけば残りの比較演算子を自動で実装するものです。しかし、これは同等以上のものがC++20で一貫比較として言語機能に組み込まれたことで非推奨とされていました。

この提案は、これはC++26に向けて削除しようとするものです。

#include <cassert>
#include <utility>

struct Test {
  int data = 0;

  friend bool operator==(Test a, Test b){return a.data == b.data;}
  friend bool operator <(Test a, Test b){return a.data <  b.data;}
};

int main() {
  Test x{};
  Test y{2};

  assert(x == x);
  assert(x != y);

  // std::rel_opsをusingする
  using namespace std::rel_ops;

  // rel_opsによって利用可能になる
  assert(x <  y);
  assert(x <= y);
  assert(x >= y);
  assert(x >  y);
}

現在はMSVCのみrel_opsの使用に対して非推奨である警告を発するようです。

現在のこのようなコードは

  1. <utility>ではなく<compare>をインクルード
  2. <の代わりに<=>を定義する
    • 典型的な実装であればdefault指定で良い
  3. 利用側で、std::rel_opsusingを削除する

の3ステップで移行できます。

P3052R1 view_interface::at()

view_interfaceat()メンバ関数を追加する提案。

以前の記事を参照

このリビジョンでの変更は、フリースタンディングのための文言を追加したことです。

P3055R1 Relax wording to permit relocation optimizations in the STL

リロケーション操作による最適化を標準ライブラリのコンテナ等で許可するために、標準の規定を緩和する提案。

以前の記事を参照

このリビジョンでの変更は、shift_left/shift_rightのための文言を追加したこと、optional, function, anyについての議論を追加したことです。

P3060R1 Add std::views::upto(n)

0から指定した数の整数シーケンスを生成するRangeアダプタ、views::uptoの提案。

以前の記事を参照

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

  • uptostd::ranges名前空間からstd::views名前空間に移動
  • より説得力のある例を追加
  • 現在のiotaの文言に組み込む形で変更を適用するようにする

などです。

このリビジョンで追加された例は次のようなものです

/// C++20
std::vector rng(5, 0);
// auto res1 = views::iota(0, ranges::size(rng)); // does not compile
auto res2 = iota(range_size_t<decltype(rng)>{}, ranges::size(rng));
std::print("{}", res2); // [0, 1, 2, 3, 4]

/// C++23
std::print("{}", views::upto(ranges::size(rng))); // [0, 1, 2, 3, 4]

P3068R0 Allowing exception throwing in constant-evaluation.

定数式においてthrow式による例外送出を許可する提案。

現在定数式においては例外送出が許可されていません。そのほかに定数式におけるエラー報告を行うメカニズムが用意されているわけでもないため、定数式におけるエラー処理は省略されるか、optionalなどを使用して侵入的にハンドリングされています。いずれにせよ、エラーを報告するメカニズムを欠いていることで、定数式におけるエラーメッセージ出力はコンパイラに頼るほかなく、ほとんどの場合わかりづらいメッセージが出力されます。

また、P2996で提案中の値ベース静的リフレクションにおいては、エンティティから反射して(^によって)取得した鏡像(std::meta::info値)に対して作用するconstevalメタ関数が用意されており、入力が不正であることによってそれらの処理がエラーを返す場合のエラー報告メカニズムとして、例外が最もふさわしいものであることが示唆されています。

この提案は、これらの理由から定数式における例外の送出(throw式の評価)およびハンドリング(try - catchの実行)を許可するものです。

提案では、定数式におけるthrow式の評価を許可し、1つの定数式の評価内において送出された例外がその評価内でキャッチされない場合にコンパイルエラーとするようにしています。

constexpr auto just_error() {
  throw my_exception{"this is always an error"};
}

constexpr void foo() {
  try {
    auto v = just_error(); // ok、foo()が定数式で実行される場合、すぐにキャッチされるのでエラーにならない
  } catch (my_exception) { }

  try {
    constexpr auto v = just_error(); // ng、constexpr変数の初期化は別の定数評価コンテキストを構成するが、その評価内でキャッチされない
  } catch (my_exception) { }
}

また、この提案では例外を定数式で許可するような言語機能の変更のみを提案していて、<stdexcept>にあるような関連するライブラリ機能を定数式で使用可能にすることは提案していません。

P3072R1 Hassle-free thread attributes

スレッドへの属性指定APIについて、集成体と指示付初期化によるAPIの提案。

以前の記事を参照

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

LEWGのレビューではこの提案の方向性に同意が得られなかったようで、追及は停止されています。

P3073R0 Remove evaluation_undefined_behavior and will_continue from the Contracts MVP

Contracts MVPのライブラリ仕様から、evaluation_undefined_behaviorwill_continue()を削除する提案。

契約違反時に呼ばれる違反ハンドラの引数にはstd::contracts::contract_violationという型のオブジェクトへの参照が渡され、ここには契約違反時の関連情報が保持されており、メンバ関数を通して取得することができます。

そのうちの1つ、detection_mode()は契約違反がどのように起きたか(契約条件がfalseを返した、例外が投げられたなど)をstd::contracts::detection_mode列挙値によって返します。std::contracts::detection_modeの列挙値には未定義動作によって契約違反が発生した場合を意味するevaluation_undefined_behaviorという値が定義されています。

もう1つ、will_continue()は違反ハンドラが正常にリターンした後に違反地点の直後からプログラムが続行されることが期待されているか(違反ハンドラの終了がプログラム終了を意味するか)をbool値で取得することができます。

しかし、この2つのものについてはその導入後に、その意味や正しい仕様、動機付けとなる使用例等について未解決の問題が多く報告されたようです。これを受けてこの提案は、この2つのものはC++26で契約を使用できるようにするために必須のものではなく、それらの疑問の解決のための時間を確保するために、C++26に向けての仕様から削除して将来の拡張にすることを提案するものです。

evaluation_undefined_behavior

.detection_mode()evaluation_undefined_behaviorを返す場合、契約条件式の評価に伴ってプログラムが未定義動作に陥ることが検出されたことを意味します。この導入の後で、この値の使用方法に関して、UBサニタイザーのようなツールがプログラムの任意の場所で検出した未定義動作について報告するために違反ハンドラを使用するのに活用できることが提案されました。すなわち、違反ハンドラは契約機能を超えて、外部ツールが検出した事項について報告する標準APIとして機能しうるということです。

SG21においてこのアイデアを議論する過程で、evaluation_undefined_behaviorの意味するところについて疑義が上がり、次のようなことを意味しているかどうかについてコンセンサスがないことが確認されました

  • 契約述語の式自体、またはその直接部分式の1つを評価すると未定義動作が発生する可能性がある
  • 契約述語の評価中に式を評価すると、その評価中に呼び出される関数の本体内(別の翻訳単位にあるかもしれない)等も含めて、未定義動作が発生する可能性がある
  • プログラムの実行中に任意の式を評価すると、契約機能が使用されているかどうかに関係なく未定義動作が発生する可能性がある

外部ツールが実行時にUBを検出する場合にそれを報告するための標準APIを用意するというアイデアについてはSG21でも好意的に受け止められており、そのアプローチが実行されると違反ハンドラによる契約違反処理、サニタイザーによるUBの実行時検出、erroneous behaviourの概念についてを統合したC++における未定義動作を軽減するための包括的な機能を提供することができるようになるかもしれません。

そのような方向性の検討は内部的に行われているようですが、現在のところどのようにそれを実現するかについてはまだほぼ決まっていません。そのための議論もオープンに行われている段階ではなく、今の段階でそれに関する機能を追加してしまうことはそのような将来的な機能の設計自由度を制限することになり、時期尚早であると思われます。

そのために契約違反ハンドラを利用すべきかどうかについてのコンセンサスはなく、利用した場合でも.detection_mode()とその列挙値を使用することが適切な設計なのかも不明です。仮にその方向のためにevaluation_undefined_behaviorを利用したとして、.detection_mode()がそれを返す場合にcontract_violationの他のメンバ関数がどう振舞うべきか(他のメンバ関数は当然契約で使用されることを前提としている)に関しても不透明です。

さらには、evaluation_undefined_behaviorの意味を定義するためには未定義動作の検出についてを扱う必要がありますが、それによって実装可能性の問題にぶつかっており、その正しい規定については現在合意できていないようです。例えば、実装はUB検出能力を提供するべきなのか、翻訳単位を超えた場合にどうするのか、UB検出機能はQoIの推奨程度のものなのかなど、どれを選択するのかとそれをどのように標準文言に落とし込むかについてコンセンサスが得られていないようです。

また、evaluation_undefined_behaviorを実装及びサニタイザーも含めて何かしらのUB検出の詳細な報告メカニズムとして使用することには使用経験がありません。特に、どこで何が起きたかについての詳細な報告方法、コールバックにどのような引数を提供すべきかについては、現在のサニタイザー実装においても決定的なコンセンサスは見られません。

これらの理由によって、この提案ではevaluation_undefined_behavior列挙値を削除してC++26には含めないようにし、これらの問題点を解決するための議論や検討、実装の時間を取るようにすることを提案しています。

will_continue()

will_continue()は元々、契約のセマンティクスがignoreenforceだけだった時代に追加されたもので、enforceセマンティクスとその時点で標準に無かったobserveセマンティクスを区別することを目的としていました。その後observeセマンティクスが正式に標準に追加されたことで冗長になったかに見えましたがユースケースがあるとして残されました。

そのユースケースには次のようなものがあげられています

  • 非標準のセマンティクスの検出
    • 契約条件をチェックする非標準のセマンティクスで違反後終了するものを検出して、終了のための対応を行う
    • 違反ハンドラの終了後にプログラムが続行または終了することが何を意味するのか、違反ハンドラが正常終了して元の実行に戻った後が何を意味するのかを規定しなければならない
    • その規定をどう行うかについてのコンセンサスがない
  • 未定義動作の実行時検出
    • 戻り値はenforceの場合にfalseobserveの場合にtrueとなると思われるが、規定では後者の場合もfalseになりうる場合がある
    • 提供されている唯一の例は、契約チェック後のパスに未定義動作が含まれている場合にそれを検出することを意図したもの
    • evaluation_undefined_behaviorと同じ問題がある他、こちらはより目的外利用であると思われる
  • セマンティクスのクエリの短縮形
    • 前の2つの用途のいずれにも使用されていない場合、この関数はsemantic() == std::contracts::contract_semantic::observeの短縮形となる
    • コンテナの.empty().size() == 0の短縮形であるのと同様に、このような仕様は非常に明快であり、この提案で挙げている他の問題に悩まされない
    • ただし、前の2つのような混乱の可能性を標準に追加することを正当化するほど有用であるとは思えない

これらのユースケースにはいずれも問題があるため、will_continue()を削除してC++26には含めないようにし、これらの問題点を解決するための議論や検討、実装の時間を取るようにすることを提案しています。

P3074R1 std::uninitialized<T>

P3074R2 std::uninitialized<T>

定数式において、要素の遅延初期化のために共用体を用いるコードを動作するようにする提案。

以前の記事を参照

R1での変更は、以前のstart_lifetime(p)の代わりにstd::uninitialized<T>を提案するよう変更する理由についての説明の追加

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

  • std::uninitialized<T>を提案するように内容を変更したこと
  • これを言語機能とすることに対する反論を追加
  • std::uninitialized<T>が共用体となるように変更

などです。

start_lifetime(p)はコンストラクタとデストラクタをトリビアルにするために追加のアノテーションが必要となり、本質的な未初期化ストレージの問題を解決しないため敬遠され、R0でオプション1として挙げられていたstd::uninitialized<T>によるソリューションが選択されました。

template <class T>
struct UnionStorage {
private:
  union { T value; };

public:
  // accessors
};

例えばこのように未初期化ストレージを定義しようとすると、Tの特殊メンバ関数(主にコンストラクタ/デストラクタ)がトリビアルではない場合その共用体(ここではUnionStorage)の対応する特殊メンバ関数も削除されます。default実装にしても削除されることには変わらず、回避するには特殊メンバ関数をユーザー定義しなければなりません

template <class T>
struct UnionStorage2 {
private:
  union U { U() { } ~U() { } T value; };
  U u;

public:
  // accessors
};

こうすると、UnionStorage2の特殊メンバ関数トリビアル性が失われ、様々なデメリットが生じます。

std::uninitialized<T>はこれらの問題を解決して未初期化ストレージを提供するためのライブラリ型です。

namespace std {

  // 宣言例
  template<class T>
  union uninitialized {
    T value;

    constexpr uninitialized();
    constexpr uninitialized(const uninitialized&);
    constexpr uninitialized& operator=(const uninitialized&);
    constexpr ~uninitialized();
  };
}

std::uninitialized<T>は常にトリビアルに構築可能かつトリビアルに破棄可能な型となります(Tによらずに)。また、Tトリビアルにコピー可能であればstd::uninitialized<T>トリビアルにコピー可能ですが、そうでない場合はコピー不可能です。

メンバvalueは初期化されていないTのストレージであり、その生存期間管理は利用者が直接手作業で行う必要があります。

なお、このような共用体はC++20以降であればコンパイラのサポート無しで実装できるはずです。

template<typename T>
union uninitialized {
  T value;

  constexpr uninit() = default;
  constexpr uninit() requires (!std::is_trivially_default_constructible_v<T>) {}

  constexpr ~uninit() = default;
  constexpr ~uninit() requires (!std::is_trivially_destructible_v<T>) {}

  // 他省略
};

P3085R0 noexcept policy for SD-9 (throws nothing)

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

標準化作業のためにポリシーを策定する動きを受けて、LEWG/LWGにおいて無条件noexceptの付加基準に関するポリシーを検討する提案です。

この提案では、現在の運用に則った次の基準を推奨するポリシーとしています

  • LWGとLEWG双方が例外を投げないとして合意された関数についてのみ、無条件noexceptを指定する

そのうえで、Lakos Ruleに基づいた次のポリシーについても検討しています

  • 事前条件を持つ関数は無条件noexcept指定するべきではない

まだどちらがポリシーとして合意されたわけではなく、この提案はこのどちらかをポリシーとして採用することを推奨するものです。

提案では、これに関する過去の議論や実装の状況、様々な論点や意見などを詳細に記録しています。

P3088R0 Attributes for contract assertions

P3088R1 Attributes for contract assertions

契約注釈に対して属性を指定できるようにする提案。

C++26に向けて提案中の契約構文は、現在のところ3種類の契約注釈のいずれも属性指定を行うことができません。属性の主な役割は実装に対して独自のコードアノテーションを追加する場所と機能を提供することにあり、契約注釈に対する属性でも有用である可能性があります。

そのようなユースケースとして、契約注釈に対するラベル付があります。契約注釈に対するラベルはC++20の契約仕様には含まれていましたが現時点のMVPには含まれておらず、C++29以降の機能拡張として議論していくことがほぼ確定しています。C++20時点ではauditというラベルがチェックされないコードの仮定を表現するために提供されていました。

他にも、現在の契約注釈のセマンティクスは実装定義とされますが、それをラベルによって明示的に指定するような用途など、さまざまなラベルを考えることができます。

auditなど基本的なラベル付けはおそらく将来のC++で提供されますが、C++26の契約注釈に対しても属性構文を指定して実装がラベルを先行提供することで将来のラベルの実装実験を行うことができます。この余白が提供されない場合、実装毎に異なるキーワードや独自の構文などによってそれが行われる可能性があり、移植性や将来の互換性を損ねる可能性があります。

この提案は、そのようなラベル属性を提案するものではなく、3種類の契約注釈に一貫した方法で属性を指定できるようにしておくことを提案するものです。

提案では、契約注釈構文内のキーワードとそれに続く開きかっこの間に属性指定を入れ込む構文を提案しています。

bool binary_search(Range r, const T& value)
  pre [[vendor::audit]] (is_sorted(r));

void f() {
  int i = get_i();
  contract_assert [[vendor::assume]] (i > 0); // ...
}

属性がどの注釈に適用されているかが明白であり、他の場所(前、後、かっこの中)だと発生する問題が発生しないことからこの場所を選択しています。

また、noexcept(contract_assert(false))がどう振る舞うかについての議論を回避するために、現在のMVP仕様ではcontract_assert()は文(statement)とされています。これによって、他の文との属性指定方法が一貫しなくなるため、contract_assert()の場合にのみその先頭に対しても属性指定を許可することも提案しています。

ただし、文の先頭に対する属性指定はそのアサーション文(assertion-statement)に対する属性指定とされ、contract_assertの後に中置する属性指定は契約注釈に対する属性指定として区別されます。

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

  // 契約注釈に対する属性指定
  contract_assert [[vendor::assume]] (i > 0);

  // アサーション文に対する属性指定
  [[likely]] contract_assert(i > 0);
}

この違いは、契約注釈に対する属性指定(中置)なのか、文に対する属性指定(先頭)なのかの違いです。

P3090R0 std::execution Introduction

P2300のsender/receiverベース非同期処理ライブラリの紹介をする文書。

P2300のライブラリ機能がどういうもので何を目的としているかやその設計についてが簡単に紹介され、サンプルコードとともにその機能の解説が行われています。

P3091R0 Better lookups for map and unordered_map

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

連想コンテナの要素を引き当てる最も簡易な方法は添え字演算子[]を使用することですが、これにはいくつか問題点があります

  • constコンテナに対して使用できない
  • 要素型がデフォルト構築可能ではない場合は使用できない
  • キーに対応する要素が見つからない場合は要素をデフォルト構築して挿入する
  • キーに対応する要素が見つからない場合に挿入される値として、デフォルト構築された値が望ましいものではない場合がある

例えば、整数をキーとして浮動小数点数double)を要素とするmapthaMapとして、1~100の範囲の整数値にマップされた最大のdouble値を取得する単純なループを考えてみます

double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i) {
  largest = std::max(largest, theMap[i]);
}

まず、これはthaMapconstである場合に機能しません。

また、[1, 100]の範囲のキーに対応する要素が存在しない場合largestには0.0が得られますが、この値がデフォルトとして望ましくない場合があります。そして、ループ開始前にthaMapの要素が僅かしか含まれていない場合でも、ループ終了後にはthaMapの要素数は100になり増えた分の要素は全て0.0で初期化されています。

この問題を回避する手段はいくつか考えられます。例えば.at()を使用すると次のようになります

double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i) {
  try {
    largest = std::max(largest, theMap.at(i));
  } catch (const std::out_of_range&) { }
}

このコードはthaMapconstでも動作し、要素が見つからないキーは無視し、thaMapに無駄な要素を挿入しません。しかし、このコードを好ましいと感じる人はほとんど居ないでしょう。このようなループ中の例外処理は、要素が見つからないキーが稀でない限りかなり非効率になります。

そのほかには、.find()を使用する方法も考えられます

double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i) {
  auto iter = theMap.find(i);
  if (iter != theMap.end()) {
    largest = std::max(largest, iter->second);
  }
}

これは僅かに冗長なコードと引き換えに[].at()にあった問題をすべて回避しており、現在可能な最も望ましいコードです。しかし、その冗長性(イテレータの使用、2つのメンバ関数を呼び出さなければならない、イテレータの要素のさらにsecond要素を使用しなければならない)が認知的負荷を増大させています。さらに、この処理を一般化すると微妙なバグに遭遇する可能性があります

template <class Key, class Value>
void f(const Key& k, const std::map<Key, Value>& aMap) {
  Value obj = some-default-obj-value-expression;
  auto iter = aMap.find(k);
  if (iter != aMap.end()) {
    obj = iter->second;   // コピーしている
  }
  // code that uses `obj` ...
}

このコードは、Valueがコピー代入可能でない場合にコンパイルエラーとなります。しかし、そのような型の使用が稀である場合、そのエラーに遭遇するのも稀になるでしょう。このようなエラーを回避するためには、objの初期化を1行で書く必要があります

auto iter = aMap.find(k);
Value obj = iter != aMap.end() ? iter->second : some-default-obj-value-expression;

ただ、これは冗長さを残したままコードが複雑化しており、理想的な場合の[]の使用感とは程遠いコードになっています。

結局現在のところ、連想コンテナからの要素を引き当てという操作のために使いやすくて最適な方法が提供されていません。この提案は新しい3つのメンバ関数get, get_ref, get_as)を追加することによって上記のような問題のない連想コンテナからの要素引き当てインターフェースを提供しようとするものです。

提案されているメンバ関数の概要は次のようになっています

// Return by value
template <class... Args>
  mapped_type get(const key_type& key, Args&&... args) const;

// Return by reference
template <class Arg>
  common_reference_t<mapped_type&,       Arg&> get_ref(const key_type& key, Arg& ref);

template <class Arg>
  common_reference_t<const mapped_type&, Arg&> get_ref(const key_type& key, Arg& ref) const;

// Return as a specific type
template <class R, class... Args>
  R get_as(const key_type& key, Args&&... args);

template <class R, class... Args>
  R get_as(const key_type& key, Args&&... args) const;

.get()keyに対応する要素が存在する場合にそれをコピーして取得し、存在しない場合はargs...からmapped_typeを構築して返し、要素が存在せずargsが空のパックの場合はデフォルト構築結果を返します。これは組み込み数値型をはじめとするコピーコストが小さい型の場合に適する関数です。

.get_ref()keyに対応する要素の参照を取得し、要素が存在しない場合はrefをそのまま返します。これは要素型のコピーコストが重い場合や要素を変更したい場合などに適する関数です。

std::map<std::string, int> theMap;

...

// 文字列範囲namesに含まれている名前と同じ名前の要素をインクリメントする
for (const auto& name : names) {
  int temp = 0;  // Value is irrelevant
  ++theMap.get_ref(name, temp);  // 参照を通じてインクリメント
  // Possibly-modified value of `temp` is discarded here.
}

第二引数のrefは非const左辺値参照を取ります。これは、対象の連想コンテナがconstの場合に右辺値を束縛するのを防止するためです。

void f(const std::map<int, std::string>& theMap) {
  const std::string& ref = theMap.get_ref(0, "zero");  // ERROR: temporary `std::string("zero")`
  
  ...
}

仮にref引数がArg&&だった場合、この例では.get_ref()内部で文字列リテラルからstd::stringの一時オブジェクトが作成されてその参照が返されます。return文で一時オブジェクトが作成されていたとしても戻り値型が参照型のためそのような参照はダングリング参照となり、refの使用は未定義動作となります。このようなバグを回避するために、ref引数は単なる左辺値参照を取るようになっており、少なくとも.get_ref()から返される参照よりも長い生存期間を持つオブジェクトヘの参照を渡すことを意図しています。

また、この関数の戻り値型は要素型の参照型とrefの参照型との間のcommon_referenceになっており、両者のCV修飾が一貫しない場合や基底クラスと派生クラスが混じる場合などに望ましい結果を生成します

void g(const std::map<int, int>& theMap) {
  ...
  
  int alt = 0;
  auto& ref = theMap.get_ref(key, alt);  // `ref` has type `const int&`
  
  ...
}

最後の.get_as<R>()は、keyに対応する要素をR型に変換して返し、要素が存在しない場合はargs...からRを構築して返します。これは、要素型がRに変換出来る場合にRで代替値を返した方が効率的となる場合に適する関数です。そのもっとも普遍的な例はstd::string_viewでしょう

std::map<int, std::string> theMap;

...

std::string_view sv = theMap.get_as<std::string_view>(key, "none");

この例では、要素のstd::stringあるいは第二引数のchar[]から、一時的なstd::stringへの変換を介在させずに直接戻り値のstd::string_viewを取得しています。返されたsvは安全に使用できます。

これと同じことを前の.get(), .get_ref()でやろうとすると、バグを導入するかコンパイルエラーとなります。

// BUG: 戻り値の一時stringから変換されたダングリングstring_view
std::string_view sv = theMap.get(key, "none");

// ERROR: ダングリング参照を生成するため拒否される
std::string_view sv = theMap.get_ref(key, "none");

提案より、その他の例

現在 この提案
auto iter = m.find(k);
T x = iter == m.end() ? T{} : iter->second;
T x = m.get(k);
現在 この提案
auto iter = m.find(k);
T x = iter == m.end() ? T{a1...aN} : iter->second;
T x = m.get(k, a1...aN);
現在 この提案
T v;
auto iter = m.find(k);
T& x = iter == m.end() ? v : iter->second;
T v;
T& x = m.get_ref(k, v);
現在 この提案
std::map<K, std::vector<U>> m{ ... };
auto iter = m.find(k);
std::span<U> x = iter == m.end() ? std::span<U>{} : iter->second;
std::map<K, std::vector<U>> m{ ... };
std::span<U> x = m.get_as<std::span<U>>(k);
現在 この提案
std::map<K, std::vector<U>> m{ ... };
const std::array<U, N> preset{ ... };
auto iter = m.find(k);
std::span<const U> x = iter == m.end() ? std::span<const U>{preset} : iter->second;
std::map<K, std::vector<U>> m{ ... };
const std::array<U, N> preset{ ... };
std::span<const U> x = m.get_as<std::span<const U>>(k, preset);
現在 この提案
std::unordered_map<K, U*> m{ ... };
auto iter = m.find(k);
if (iter != m.end()) {
  U* p = iter->second;
  // ...
}
std::unordered_map<K, U*> m{ ... };
U* p = m.get(k, nullptr);
if (p) {
  // ...
}
現在 この提案
auto iter = m.find(k);
if (iter != m.end()) {
  T& r = iter->second;
  // ...
}
T not_found;
T& r = m.get_ref(k, not_found);
if (&r != &not_found) {
  // ...
}

このような関数を考えるとき、真っ先に思いつくのはstd::optionalstd::expectedの利用でしょう。提案ではそれも検討していますが、.get()に関しては同等の事をやろうとすると2回のコピーが発生するためoptional<T&>が無い場合は利点が無く、optional<T&>があったとしても.get_ref().get_as()に相当するものがない、としています。

P3092R0 Modules ABI requirement

モジュールがABIに課す要件についてまとめる提案。

モジュールはその仕様策定にあたってABIを壊さないように配慮して導入されています。とはいえその仕様は複雑で、ABIから見た時にモジュールの仕様において何が許可されていて何が許可されていないかは自明ではありません。

この提案の目的はモジュールを実装するにあたってABIの後方互換性を確保するために、ABIの仕様の何を変更する必要があり何を変更しなくてもいいか、あるいはABIの仕様をモジュールに合わせてどう変更することができるかについての見通しを良くするために、モジュール仕様とABI仕様の両面からその間にある要件についてを文書化しようとするものです。

次のようなことがリストアップされています

  • ABIからの要請による言語側の変更
    • TU-Localなエンティティ
    • クラス内で定義されたメンバ関数は暗黙的にinlineにならない
  • stdモジュール
    • #includeimportを混ぜたとしても定義が競合しないことが規定されている
    • これは、std及びstd.compatで宣言されたエンティティがヘッダで宣言されているものと同じリンケージかつ同じマングル名を持つことを意味している
  • 反映されていないABIからの要請
    • ABI境界
      • モジュール内の非inline関数/変数の定義がABI境界に影響しないことが望ましい
        • これによって、その定義の変更がインポートしている翻訳単位の再コンパイルをトリガーしなくても良くなる

この提案は初期のものであり、リストは完全ではない可能性があります。

P3093R0 Attributes on expressions

式に対して属性を指定できるようにする提案。

現在のC++において、属性の指定は実際には文に対して行われており、式に対して属性を指定することができません

// 全てNGな例

// 関数呼び出し式に対する属性指定
([[attr]] f(1, 2, 3));

// 関数引数の式に対する属性指定
process(([[lock]] g()), 42);

// カンマ式中の部分式に対する属性指定
for (int i = 0; i < N; ++i, ([[discard]] f()))
  doSomething(i);

// メンバ初期化子リスト内の初期化式に対する属性指定
struct S {
  S(int i)
      : m_i(([[debug_only(check(i))]] i)) {}

  int m_i;
};

この提案は、これらのような式に対する属性指定を許可しようとするものです。

この提案は[[discard]]属性を提案するP2922から分離されたもので、モチベーションはこの属性を関数呼び出しが可能な任意の場所に書けるようにすることにあります。

// 戻り値を無視してほしくない関数
[[nodiscard]]
int f(int i);

// 文に対する属性指定、現在も可能
[[discard("f always succeeds for 42")]] f(42);

// 式に対する属性指定、この提案
for (int i = 0; i < N; ++i, ([[discard("f succeeds for inputs >= 0")]] f(i)))
  doSomething(i);

他にも、P2946の[[throws_nothing]]属性を同様に関数呼び出しに対して指定可能にすることにも使用可能です

// 文に対する属性指定
[[throws_nothing]] f(42);

// 式に対する属性指定
struct S {
  S(int i)
    : m_i(([[throws_nothing]] f(i))) {}

  int m_i;
};

この場合、f()は狭い契約を持つ(何らかの事前条件を持つ)関数であり、通常無条件noexceptではありません。しかし、呼び出し側でその事前条件を満たしていることを確認していることをこの属性の付加によってコンパイラに伝達することで、例外処理周りのコード生成をスキップすることができ、noexceptが指定されている場合の恩恵をアドホックに適用することができます。

属性が文にしか指定できないのは意図的なもののようで、属性を導入した提案(N2761)によれば式に対する何らかの属性指定にはキーワードを導入すべき、としています。しかし、キーワードの追加はとてもハードルが高く、属性のように無視できるという性質がありません。

提案では、C++の現行文法における式の最上位であるassignment-expressionに対してattribute-specifier-seq(属性文法)を適用可能にすると現在可能な文に対する属性指定と衝突するためそれは提案せず、代わりにparenthesized expressions()に囲まれた式)の内側のexpressionに対してattribute-specifier-seqを指定可能にすることを提案しています。

primary-expression:
    literal
    this
    ( attribute-specifier-seq(opt) expression )
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    id-expression
    lambda-expression
    fold-expression
    requires-expression

この上で、セマンティクスの指定を追加して、この属性指定がその隣にある括弧の中のexpressionに対して適用されるようにします。

int a[10];

[[attr]] a[0] = x + y;      // 文に対する属性指定
([[attr]] a[1]) = x + y;    // 式`a[1]`に対する属性指定
a[2] = [[attr]] x + y;      // ill-formed
a[3] = ([[attr]] x) + y;    // 式`x`に対する属性指定
a[4] = ([[attr]] x + y);    // 式`x + y`に対する属性指定
a[4] = ([[attr]] (x + y));  // 同上
([[attr]] a[6] = x + y);    // 式`a[6] = x + y`に対する属性指定


// attr1はrequires式全体に適用される
// attr2は式`c.foo()`に適用される
// attr3は式`*c`に適用される
template <typename T>
concept C =
  ([[attr1]] requires (C c)
    {
        ([[attr2]] c.foo());
        { ([[attr3]] *c) } -> convertible_to<bool>;
    });


// attr1は文に対する属性指定
// attr2は式全体に適用される
// attr3はラムダ式の関数呼び出し演算子に適用される
// attr4はラムダ式の関数型に適用される
[[attr1]] ( [[attr2]] [] [[attr3]] () [[attr4]] {} () );

()が冗長に見えますが、()が必要である事によって属性がどの式に対して適用されているかが明確になるメリットもあります。

この新しい文法は、最初に示した例をすべて許可します

// この提案後OKになる

// 関数呼び出し式に対する属性指定
([[attr]] f(1, 2, 3));

// 関数引数の式に対する属性指定
process(([[lock]] g()), 42);

// カンマ式中の部分式に対する属性指定
for (int i = 0; i < N; ++i, ([[discard]] f()))
  doSomething(i);

// メンバ初期化子リスト内の初期化式に対する属性指定
struct S {
  S(int i)
      : m_i(([[debug_only(check(i))]] i)) {}

  int m_i;
};

P3094R0 std::basic_fixed_string

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

C++20では、クラス型のオブジェクトをNTTP(非型テンプレートパラメータ)に取ることができるようになりました。そうすると文字列をNTTPとして扱いたくなるのですが、標準ライブラリにある文字列型はいずれもNTTPに使用できるクラス型の分類である構造的型(structural type)の要件を満たしていないため、使用可能ではありませんでした。

このような文字列型は込み入ったテンプレートを扱うライブラリの作成時に便利な場合があります。この提案は、提案中の単位ライブラリ(P3045R0)においてこのような文字列型が必要となったため、その準備としてstd::basic_fixed_stringを標準ライブラリに導入することを目指すものです。

また、このような文字列型はGithubで探すといくつも見つかり、様々な人が個別に最発明していることが分かります。そのような型が標準ライブラリにあれば、そのような最発明を回避することができます。

提案するstd::basic_fixed_stringの要件として次のことがあげられています

  • 構造的型(structural type)の要件を満たす
  • 等価比較可能かつ全順序による順序付け比較が可能
  • null終端された文字列の保持と連結をサポート
  • コンパイル時に初期化された場合でも、実行時からも読み取り可能
  • 保持する文字列ストレージへの少なくとも読み取りアクセスを提供する
  • std::string準拠のインターフェースは提供しない
    • std::string_viewでラップすることでそのようなインターフェースを提供できる

提案より、実装全景

template<typename CharT, std::size_t N>
struct basic_fixed_string {
  // ストレージ
  CharT data_[N + 1] = {};  // exposition only

  // メンバ型
  using value_type = CharT;
  using pointer = CharT*;
  using const_pointer = const CharT*;
  using reference = CharT&;
  using const_reference = const CharT&;
  using const_iterator = const CharT*;
  using iterator = const_iterator;
  using size_type = std::size_t;
  using difference_type = std::ptrdiff_t;

  // コンストラクタ
  constexpr explicit(false) basic_fixed_string(const CharT (&txt)[N + 1]) noexcept;
  constexpr basic_fixed_string(const CharT* ptr, std::integral_constant<std::size_t, N>) noexcept;

  template<std::convertible_to<CharT>... Rest>
    requires(1 + sizeof...(Rest) == N)
  constexpr explicit basic_fixed_string(CharT first, Rest... rest) noexcept;

  // Rangeアクセスインターフェース
  [[nodiscard]] constexpr bool empty() const noexcept;
  [[nodiscard]] constexpr size_type size() const noexcept;
  [[nodiscard]] constexpr const_pointer data() const noexcept;
  [[nodiscard]] constexpr const CharT* c_str() const noexcept;
  [[nodiscard]] constexpr value_type operator[](size_type index) const noexcept;

  [[nodiscard]] constexpr const_iterator begin() const noexcept;
  [[nodiscard]] constexpr const_iterator cbegin() const noexcept;
  [[nodiscard]] constexpr const_iterator end() const noexcept;
  [[nodiscard]] constexpr const_iterator cend() const noexcept;

  // string_viewへの変換
  [[nodiscard]] constexpr std::basic_string_view<CharT> view() const noexcept;

  // 連結
  template<std::size_t N2>
  [[nodiscard]] constexpr friend basic_fixed_string<CharT, N + N2> operator+(const basic_fixed_string& lhs,
                                                                             const basic_fixed_string<CharT, N2>& rhs) noexcept;

  // 比較
  [[nodiscard]] constexpr bool operator==(const basic_fixed_string& other) const;
  template<std::size_t N2>
  [[nodiscard]] friend constexpr bool operator==(const basic_fixed_string&, const basic_fixed_string<CharT, N2>&);

  template<std::size_t N2>
  [[nodiscard]] friend constexpr auto operator<=>(const basic_fixed_string& lhs,
                                                  const basic_fixed_string<CharT, N2>& rhs);
  
  // ストリーム出力
  template<typename Traits>
  friend std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>& os,
                                                       const basic_fixed_string<CharT, N>& str);
};

// 推論補助
template<typename CharT, std::size_t N>
basic_fixed_string(const CharT (&str)[N]) -> basic_fixed_string<CharT, N - 1>;

template<typename CharT, std::size_t N>
basic_fixed_string(const CharT* ptr, std::integral_constant<std::size_t, N>) -> basic_fixed_string<CharT, N>;

template<typename CharT, std::convertible_to<CharT>... Rest>
basic_fixed_string(CharT, Rest...) -> basic_fixed_string<CharT, 1 + sizeof...(Rest)>;

// エイリアス
template<std::size_t N>
using fixed_string = basic_fixed_string<char, N>;

// std::format()サポート
template<typename CharT, std::size_t N>
struct std::formatter<basic_fixed_string<CharT, N>> : formatter<std::basic_string_view<CharT>> {
  template<typename FormatContext>
  auto format(const basic_fixed_string<CharT, N>& str, FormatContext& ctx)
  {
    return formatter<std::basic_string_view<CharT>>::format(str.view(), ctx);
  }
};

P3095R0 ABI comparison with reflection

静的リフレクションを用いてABI互換性チェックを可能にする提案。

この提案の言うABIの比較とは、ある構造体のレイアウトが2つのプログラム間で互換性があるかどうかをチェックできることを言います。そのような現実のユースケースは例えば

  • ネットワーク/共有メモリを介したプロセス間通信
    • データの受信側はそのデータをマーシャリングする際にそれが安全かどうか(必要なフィールドが存在し想定する型であること)を確認したい
    • また、送受信されるメッセージサイズが気にされる(確認のためのオーバヘッドが小さいことが要求される)場合もある
  • C++で記述された1つのpython拡張モジュールが別のpython拡張モジュールに渡される構造体を作成できるpythonバインディング
    • 例えばnumpyライブラリでは、ユーザーは全く別のシステムで作成されたnumpy配列を扱う独自のpython拡張ライブラリを作成可能
      • この場合、それらのC++モジュールはpythonプロセスを介してやり取りすることになる
    • 2つのC++モジュールが同じプロセスで実行されている場合、片方のモジュールの関数ポインタを他方のモジュールから呼び出すことができる
      • この時、モジュールを跨いで仮想関数を呼び出せる?また、それは安全?

などがあります。

この提案ではそのための簡易な方法として、P2996で提案中の静的リフレクションを用いてクラスレイアウトに関する情報をコンパイル時にハッシュ化しておき、それをプログラム間で比較することで構造体レイアウトの互換性を判定する方法を紹介し、そのためのユーティリティを標準ライブラリに導入することを提案しています。

提案する機能の使用感は次のようなものになります

constexpr auto HashConfig = std::meta::abi::ABIHashingConfig{.include_indirections = true};
size_t hash = std::meta::abi::get_abi_hash<MyStruct, HashConfig>();

ABIハッシュは他の方法(手動バージョニングやprotobuf等ライブラリ、JSONシリアライズ)と比べると、コンパイル時に自動化可能であり比較のためのデータ(ハッシュ値)は非常に小さく済むため通信やパフォーマンスのオーバーヘッドが最小で、なおかつその利用が簡単である点が優れています。

まず2つのコンフィグ構造体と関連する列挙型を追加します

// Common usecase optimized with future extensibility
struct ABIHashingConfig {
  static constexpr int MINIMUM_SUPPORTED_VERSION = 0; // To allow future rollover
  static constexpr int MAXIMUM_SUPPORTED_VERSION = 0; // To gracefully error out

  uint8_t version : 4 = 0;
  bool include_nsdm_names : 1 = true;     // 非静的メンバ名をハッシュに含めるか否か
  bool include_indirections : 1 = false;  // 同じプロセス内での比較時にのみ意味がある
} __attribute__((packed));

// Virtual ABI hashing use-case kept as an optional extension
enum class VTableHashingMode : uint8_t {
  NONE, SIGNATURE, SIGNATURE_AND_NAMES, NUM_VTABLE_HASHING_MODES
};

enum class VirtualABI : uint8_t {
  ITANIUM, MSVC
};

struct IndirectionABIHashingConfig {
  uint8_t version : 4 = 0;
  VTableHashingMode virtual_hashing_mode : 4 = VTableHashingMode::NONE;
  VirtualABI abi_mode : 2 = VirtualABI::ITANIUM; // Architecture dependent default
} __attribute__((packed));

ABIHashingConfiginclude_indirectionsメンバは仮想テーブルについてのハッシングを行うかどうかの指定のようで、次のような意味を持ちます

  • include_indirectionsfalse
    • versionが特定のバージョンに一致する場合
    • そうではない場合(将来の拡張)
      • versionが特定の数値より大きい場合に、拡張構造体を導入できる
  • include_indirectionstrueの場合
    • ABIハッシュの最後には、IndirectionABIHashingConfig構造体のシリアライズが付加される
      • versionフィールドの役割はABIHashingConfigとほぼ同様
    • この構造体はプロセス境界を越えてシリアライズされることはなく、サイズが最適化されていない

これらの設定を用いてABIハッシュを計算するget_abi_hash()は次のようなシグネチャになっています

template <typename T, ABIHashingConfig config = ABIHashingConfig{},
          IndirectionABIHashingConfig indirection_cfg = IndirectionABIHashingConfig{}>
consteval size_t get_abi_hash();

この関数はP2996の値ベース静的リフレクションをベースとして、完全にライブラリ機能として実装することができます。提案にはその実装例と、godboltへのリンクがあります。

P3096R0 Function Parameter Reflection in Reflection for C++26

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

P2996R1の現在の値ベース静的リフレクション仕様には、関数func()に対して^funcで取り出したmeta::infoオブジェクトからその関数の仮引数に関する情報を取得する手段が提供されていません。この提案は、いくつかの重要なユースケースでそれが必要であるとして、それを含めておくようにP2996に対して提案するものです。

ユースケースとしてはDependency Injectionや多言語バインディングデバッグやロギングを挙げています。

この提案では特に、仮引数の型を取得する機能と仮引数名を取得する機能を分けて論じており、前者に関しては有用であり導入に当たって問題ないとしていますが、後者を可能にすると問題が起こるとして詳細に検討しています。

リフレクションによって仮引数名を取得可能にする場合の問題とは、宣言と定義で仮引数名を変更可能なためどの時点の仮引数名を取得すべきがが不透明であることです。

#include <experimental/meta>
#include <iostream>

using namespace std::experimental::meta;

// function declaration 1
void func(int a, int b);

void print_after_fn_declaration1() {
  std::cout << "func param names are: ";
  template for (constexpr auto e : param_range(^func)) {
      std::cout << name_of(e) << ", ";
  }
  std::cout << "\n";
}

// function declaration 2
void func(int c, int d);

void print_after_fn_declaration2() {
  std::cout << "func param names are: ";
  template for (constexpr auto e : param_range(^func)) {
      std::cout << name_of(e) << ", ";
  }
  std::cout << "\n";
}

// function definition
void func(int e, int f) {
  return;
}

void print_after_fn_definition() {
  std::cout << "func param names are: ";
  template for (constexpr auto e : param_range(^func)) {
      std::cout << name_of(e) << ", ";
  }
  std::cout << "\n";
}

int main() {
  print_after_fn_declaration1();  // 出力: func param names are: a, b,
  print_after_fn_declaration2();  // 出力: func param names are: c, d,
  print_after_fn_definition();    // 出力: func param names are: e, f,
}

関数は何度でも再宣言することができ、その都度仮引数名を変えることができます。そのため、どの宣言の仮引数名を取得すべきかが不透明であり、何らかの保証が無い場合にコンパイル時の仮引数名の取得の使用は危険なものになりかねません。これはメンバ関数でも同様です。

特に、次のような性質が仮引数名リフレクションの安全な利用を困難にしています

  • 結果が宣言順に依存する
  • 結果の一貫性をチェックできない
  • リフレクションが行われる方法にも依存する

この提案ではこの解決策として、次の5つを検討しています

  • 保証なし : 対策をしない。結果は実装依存
  • 一貫性向上 : 直接・間接のリフレクション両者の一貫性を保証
    • has_consistent_paramater_names()によってそれをチェックできる
  • 一貫性した命名の強制 : 到達可能な複数の宣言・定義において、仮引数名が異なっている場合はその取得をコンパイルエラーにする
    • ただし、仮引数名が省略されている場合は異なっていてもよい
  • 言語属性 : [[canonical]]のような属性を導入し、この属性が指定された宣言の仮引数名を取得する
    • そのような関数宣言が存在しない場合はその取得をコンパイルエラーにする
  • ユーザー定義属性 : [[canonical]]のような属性をユーザー定義できるようにする

これらの選択肢の比較は次のようになります

一貫性 順序非依存 すぐに適用可能 自己完結型 堅牢
保証なし no no yes yes no
一貫性向上 yes no yes yes yes
一貫した命名の強制 yes yes partially yes yes
言語属性 yes yes no no yes
ユーザー定義属性 yes yes no no yes

表の列の意味はそれぞれ

  • 一貫性 : 全てのコンテキストで一貫して動作する
  • 順序非依存 : 到達可能な宣言の順序変更の影響を受けない
  • すぐに適用可能 : 既存のコードベースに対してほぼ変更を加えずに使用できる
  • 自己完結型 : リフレクション以外の言語の変更がほぼ必要ない
  • 堅牢 : 名前の不一致を検出する手段を提供する

となっています。この表からは、「一貫した命名の強制」のソリューションが最適であることが伺えます。

これらの検討から、P2996R1に対して次のことを提案しています

  • 関数仮引数の型のリフレクション
  • 「一貫した命名の強制」ソリューションに基づく関数仮引数名のリフレクション
  • has_default_argumentメタ関数を復元
  • is_function_parameterメタ関数を復元

 

P3101R0 Differentiating potentially throwing and nonthrowing violation handlers

Contractsの違反ハンドラが例外を投げるかどうかをコンパイル時に検出可能にする提案。

現在の契約仕様の違反ハンドラは、::handle_contract_violation(const std::contracts::contract_violation&)というシグネチャで、ユーザーはこれを置き換えたうえでユーザー定義の違反ハンドラ内部から例外を送出することが許可されています。その際、置換する違反ハンドラにはnoexceptを付けてもつけなくても良いとされています。

違反ハンドラからの例外送出に関してはそのユースケースがあるため意図的に許可されているものですが、逆に、コードによっては違反ハンドラが例外を投げうることが望ましくない場合もあります。そのようなコードでは、違反ハンドラが例外を投げないことをstatic_assertコンパイル時に検証しておきたいかもしれません。またあるいは、違反ハンドラが例外を投げないことを仮定した最適化が可能になることも考えられます。

例えば現在書かれている次のようなコードがあった時

auto resource = acquire_resource(); // 非RAIIなリソース取得
f(resource); // 例外を投げないことが知られている
release(resource);

f()に後から事前条件を追加した場合、違反ハンドラが例外を投げる可能性があるため、このコードはリソースリークを起こすものになります。このような場合に、違反ハンドラが例外を投げるかどうかをコンパイル時に検出できるとそのような危険を回避することができます。

この提案は、次の式をそのような判定に使用できるようにしようとしています

noexcept(
  ::handle_contract_violation(
    std::declval<const std::contracts::contract_violation&>()
  )
)

この式がtrueを返す場合、例外を投げうる違反ハンドラのインストールはill-formedであるため、この式を使用して違反ハンドラが例外を投げるかどうかを検出することができます。また、コンパイラフラグによって違反ハンドラのnoexcept性を制御できるようにすることもできるようにしようとしています。

これを実現するためにこの提案では、現在の仕様で「違反ハンドラはnoexceptである場合もそうでない場合もある」のようにされているところを「違反ハンドラがnoexcept指定されるかどうかは実装定義」のように修正することを提案しています。また、noexcept指定ありをデフォルトとしておくことを推奨しています。

P3102R0 Refining Contract Violation Detection Modes

契約違反ハンドラに渡される違反時の状況についてを伝達する2つの列挙型がどのような意味をもち、どのような列挙子を持つべきかを検討する提案。

現在のContracts MVP仕様には契約違反時の動作をカスタマイズすることのできる置換可能な契約違反ハンドラが組み込まれています。違反ハンドラはその唯一の引数としてcontract_violationという型のオブジェクトを受け取り、ここには契約違反を起こした原因についての情報が含まれています。その目的は主に次に2点にあります

  • 違反ハンドラ呼び出しを引き起こしたソフトウェア欠陥の迅速な診断と修正に役立てる
  • 違反ハンドラの動作を決定するための情報を提供する
    • 違反ハンドラでは、終了する、継続する、例外を投げる、イベントループに移行し中断する、などの契約違反時の対応を行うが、違反に至った経緯の情報がこの決定に影響を与える場合がある

これらの情報はまた、その詳細を取得し提供するのにかかるコストとのバランスがとれている必要があります。そのうえで、違反ハンドラが呼び出された際にその内部で何が起きたのかを推察することができるように、contract_violationオブジェクトのプロパティはきちんと指定されていなければなりません。

contract_violationオブジェクトに現在あるプロパティのうちの2つ、.kind().detection_mode()はそれぞれ違反ハンドラが次の2つの事を認識するのに十分であるようにすることを目的としています

  1. kind : 検出された契約違反の原因となった言語構成要素の種類を表す
  2. detection_mode : その言語構成要素(kindの1つ)を通過する可能性のあるルートのうち、どれが違反につながったのかを表す

この提案は、この2つのプロパティがそれぞれどのようなときにどのような値を取るべきなのかを考察し、これらの関数が返す列挙型の値として何が必要なのかを提案するものです。ただし、その目的は現在の仕様を明確化することにあり、現在の仕様とは異なる代替案を提案するものではありません。

現在の契約仕様には、事前条件・事後条件・アサーションという3つの形式の契約アサーションが存在します。kindの返す列挙値はこれら3つを区別することを目的としています。従って、kind()の返す列挙型の値は最低次の3つを備えている必要があります

  1. pre : 事前条件アサーションの評価に伴って違反が検出されたことを示す
  2. post : 事後条件アサーションの評価に伴って違反が検出されたことを示す
  3. assert : アサーション式の評価に伴って違反が検出されたことを示す

3つの契約述語はその評価のタイミングが異なるだけで、評価されて以降はほぼ同じように機能します。従って、契約違反が検出される可能性のあるルートも共通しており、現在のMVP仕様ではそのルートとして次の3つがあげられています

  1. 契約述語が評価され結果がfalseになる
  2. 契約述語の評価に伴って例外が送出される
  3. 述語が常にfalseになることをコンパイラが証明できる状況では、その評価を省略できる

従って、 detection_mode()の返す列挙値はこれらの3つのルートを表す必要があります

  1. predicate_false : 上記ルートの1に対応
  2. predicate_would_be_false : 上記ルートの3に対応
  3. evaluation_exception : 上記ルートの2に対応

また、契約違反ハンドラは、プログラム中のUBをwell-definedな方法で処理するための汎用機構の中核となることをその目的としてもいます。契約機能はその一部として、ライブラリのUBに繋がる事前条件違反を、well-definedな違反ハンドラの呼び出しに変換するメカニズムを提供するものであるともいえます。ただし、そのような機能をC++に導入するためには、そのほかの方法も考えられます

  • procedural function interfaces
    • 関数契約を表現する各種アサーションを纏めたコードブロックによって関数呼び出しをラップする手続き型インターフェース
    • 契約機能はこの特殊な場合と見ることができる
  • 既存のassert()マクロ
    • assert()は明らかに契約アサーションの一種であり、その違反の検出時に契約違反ハンドラを呼び出すようにする拡張が考えられる
  • サニタイザ
    • 現在の実装は検出時にカスタム可能なハンドラを備えていない
    • サニタイザのコールバックとして契約違反ハンドラを呼び出せるようにする拡張が考えられる
  • C標準のAnnex Kにある、ライブラリAPIに対して様々な事前条件チェックを行うハンドラを設定するメカニズム
    • この違反時のコールバックハンドラに契約違反ハンドラを使用できる
  • サニタイザのような高級なものではなく、簡単なチェックでUBを検出できる
    • その場合のコールバックとして契約違反ハンドラを使用できる

これらの方向性は何か固まったものがあるわけではありませんが、いずれも契約違反ハンドラを有効活用できる可能性があります。そのため、この将来の拡張あるいはベンダ拡張を考慮に入れると、kindの値は拡張可能である必要がありそうです。また、kindの値がなんであっても、違反が起きているものの契約述語を評価したり例外が送出されていたりするわけではなく、未定義動作が起こる可能性があったことを確認するためのdetection_mode()の列挙子が必要です。

  • evaluation_undefined_behavior : 評価が継続された場合、UBが発生したであろうことを表す

この提案による2つの列挙型の内容は次のようになります

namespace std::contracts {

  // 契約注釈の種類
  enum class contract_kind : int {
    pre,    // 事前条件アサーションの評価に伴って違反が検出されたことを示す
    post,   // 事前条件アサーションの評価に伴って違反が検出されたことを示す
    assert  // アサーション式の評価に伴って違反が検出されたことを示す
  };

  // 契約違反の起こり方
  enum class detection_mode : int {
    predicate_false,              // 契約述語が評価され結果が`false`になった
    predicate_would_be_false,     // 契約述語を評価すると`false`になる(評価は省略されている)
    evaluation_exception,         // 契約述語の評価に伴って例外が送出された
    evaluation_undefined_behavior // 評価が継続された場合、UBが発生していた
  };


  class contract_violation {
  public:

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

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

    ...

  };
}

P3103R0 More bitset operations

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

C++20で<bit>に追加されたビット操作関数群のうち、一部のもの(popcount()など)はstd::bitsetにも別名ではあるものの存在しますが、対応する操作が存在していないものもあります。

std::bitsetは内部の整数値を外部に公開しないため<bit>の関数を利用したり、ユーザーが効率的な実装を提供することはできません。これによって、std::bitsetの機能性が相対的に悪くなっています。そのため、std::bitsetにも対応する操作をメンバ関数として提供しようとする提案です。

追加する関数の対応関係は次のようになります

<bit>の関数 提案する関数
std::has_single_bit(T) one()
std::countl_zero(T) countl_zero()
countl_zero(size_t)
std::countl_one(T) countl_one()
countl_one(size_t)
std::countr_zero(T) countr_zero()
countr_zero(size_t)
std::countr_one(T) countr_one()
countr_one(size_t)
std::rotl(T, int) rotl(size_t)
std::rotr(T, int) rotr(size_t)
reverse()

カウントを行う系統の関数の追加のオーバーロードsize_tを取るもの)は、指定された位置からのカウントを行うものです。

また、reverse()はビット順を逆にする関数であり、<bit>には対応するものがありません。この関数はこの操作をビット操作でやるよりも整数値としての逆転を行った方が高速になるとして追加されています。例えば、ARMv8にはRBITという命令が用意されています。

P3104R0 Bit permutations

<bit>にビット置換系操作を追加する提案

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

  • std::bit_reverse
  • std::bit_repeat
  • std::next_bit_permutation/std::prev_bit_permutation
  • std::bit_compress
    • std::bit_compressl
  • std::bit_expand
    • std::bit_expandl

これらの操作は、ソフトウェア実装するのが少し難しく、直接あるいは間接のハードウェアサポートが受けられるものがほとんどです。また、*_bit_permutationを除いて広範なユースケースがあり、多くのアルゴリズム等の実装に必要な基礎的な操作として必要とされています。そのため、この提案ではこれらのものを標準ライブラリ関数として追加することを提案しています。

bit_reverse

template<unsigned_integral T>
constexpr T bit_reverse(T x) noexcept {
  T result = 0;
  
  for (int i = 0; i < numeric_limits<T>::digits; ++i) {
    result <<= 1;
    result |= x & 1;
    x >>= 1;
  }

  return result;
}

bit_reverse()は、入力xをビット単位で逆順に並べ替えた値を返します。

auto r = bit_reverse(uint32_t{0x00001234});
// r == 0x24c80000u

bit_repeat

template<unsigned_integral T>
constexpr T bit_repeat(T x, int length) noexcept(false) {
  T result = 0;
  
  for (int i = 0; i < numeric_limits<T>::digits; ++i) {
    result |= ((x >> (i % length)) & 1) << i;
  }

  return result;
}

bit_repeat()は入力xの下位lengthビットに含まれているビット列をTの幅に収まる回数だけ繰り返した値を返します。

auto r = bit_repeat(uint32_t{0xc}, 4);
// r == 0xccccccccu

next_bit_permutation/prev_bit_permutation

template<unsigned_integral T>
constexpr T next_bit_permutation(T x) noexcept {
  const int count = popcount(x);
  
  while (x != 0 && popcount(++x) != count) {}

  return x;
}

template<unsigned_integral T>
constexpr T prev_bit_permutation(T x) noexcept {
  const int count = popcount(x);

  while (x != 0 && popcount(--x) != count) {}
  
  return x;
}

next_bit_permutation()(x, numeric_limits<T>::max()]区間の中で、popcount(y) == popcount(x)となる値を返します。そのような値が存在しない場合は0を返します。

prev_bit_permutation()[0, x)区間の中で、popcount(y) == popcount(x)となる値を返します。そのような値が存在しない場合は0を返します。

int main() {
  for (uint8_t x = 0b111; x != 0; x = next_bit_permutation(x)) {
    std::println("{0:b} : {0}", x);
  }
}
111 : 7
1011 : 11
1101 : 13
1110 : 14
10011 : 19
...

prev_bit_permutation()next_bit_permutation()対して、出発点から逆向きにたどる値を返します。

bit_compress

template<unsigned_integral T>
constexpr T bit_compressr(T x, T m) noexcept {
  T result = 0;
  
  for (int i = 0, j = 0; i < numeric_limits<T>::digits; ++i) {
    bool mask_bit = (m >> i) & 1;
    result |= (mask_bit & (x >> i)) << j;
    j += mask_bit;
  }

  return result;
}

template<unsigned_integral T>
constexpr T bit_compressl(T x, T m) noexcept {
  return bit_reverse(bit_compressr(bit_reverse(x), bit_reverse(m)));
}

bit_compressr()mをマスクビットとして、mのうち1が立っている場所に対応するxのビットだけを取り出して、それを下位桁で連続するように詰めたTの値を返します。bit_compressl()は取り出したビット列を最上位桁で連続するように詰めたTの値を返します。

int main() {
  uint8_t x = 0b11001011u;
  uint8_t m = 0b10101010u;

  auto r1 = bit_compressr(x, m);
  std::println("{0:08b} : {0}", r1);

  
  auto r2 = bit_compressl(x, m);
  std::println("{0:b} : {0}", r2);
}
00001011 : 11
10110000 : 176

bit_expand

template<unsigned_integral T>
constexpr T bit_expandr(T x, T m) noexcept {
  T result = 0;

  for (int i = 0, j = 0; i < std::numeric_limits<T>::digits; ++i) {
    bool mask_bit = (m >> i) & 1;
    result |= (mask_bit & (x >> j)) << i;
    j += mask_bit;
  }

  return result;
}

template<unsigned_integral T>
constexpr T bit_expandl(T x, T m) noexcept {
  return bit_reverse(bit_expandr(bit_reverse(x), bit_reverse(m)));
}

bit_expandr()mの最下位ビットから立っているビットが見つかるごとに、xの最下位から1ビットづつmの立っているビットの位置にコピーします。

bit_expandl()mの最上位ビットから立っているビットが見つかるごとに、xの最上位から1ビットづつmの立っているビットの位置にコピーします。

ただしどちらも、mを直接変更するわけではなく、そのコピーに対してそのような置換操作を行います。

int main() {
  uint8_t x = 0b11001011u;
  uint8_t m = 0b10101010u;

  auto r1 = bit_expandr(x, m);
  std::println("{0:08b} : {0}", r1);

  
  auto r2 = bit_expandl(x, m);
  std::println("{0:b} : {0}", r2);
}
10001010 : 138
10100000 : 160

どちらの関数も、マスクm0の場所は変更されず、1の場所だけがその表れる位置(最下位or最上位からk番目)に応じて、xk番目のビットで置換されます。

これらの関数は直接的もしくは間接的に、現行CPUが備える命令を用いて実装することができます。

関数 x86_64 ARM RISC-V
bit_reverse (BSWAP) RBIT(SVE2) (vrgather(V))
bit_repeat
next_bit_permutation (TZCNT(BMI)/BSF) (CTZ) (ctzB)
prev_bit_permutation (TZCNT(BMI)/BSF) (CTZ) (ctzB)
bit_compressr PEXT(BMI2) BEXT(SVE2) (vcompress(V))
bit_expandr PDEP(BMI2) BDEP(SVE2) (viota+vrgather(V))
bit_compressl (PEXT(BMI2)+POPCNT(ABM)) BGRP(SVE2), (BEXT(SVE2)+CNT(SVE)) (vcompress(V))
bit_expandl (PDEP(BMI2)+POPCNT(ABM)) (BDEP(SVE2)+CNT(SVE)) (viota+vrgather(V))

表中で()に囲まれている所は、その命令を用いて実装できることを表しています。(BMI)とか(SVE2)などは対応している命令セットの略称です。

LEWGIによる最初のレビューにおいては*_bit_permutation以外のものは好意的に受け止められたようで、*_bit_permutationを削除することで次のステップに進めることができると推奨されています。

P3105R0 constexpr std::uncaught_exceptions()

std::uncaught_exceptionsstd::current_exceptionを定数式でも使用可能にする提案。

std::uncaught_exceptions()は現在キャッチされていない例外の数を返す関数であり、std::current_exception()は現在の例外オブジェクトを指すstd::exception_ptrを返すもので、定数式において例外送出が許可されていないためこれらの関数もconstexprではありません。

とはいえ、現在定数式で例外送出は許可されていないため、前者は常に0を返し、後者は常にnullptrを返すことでconstexprな実装が可能です。

std::uncaught_exceptions()ユースケースの1つは、RAIIオブジェクトのデストラクタにおいて例外送出中であるかどうかを検出する、というものがあります。例えば、LFTSv3に存在しているstd::scope_success(スコープ終端で例外が投げられていない場合に実行)とstd::scope_failure(スコープ終端で例外が投げられている場合に実行)において使用されています。

これらのクラスをconstexpr対応させることは妥当な改善だと思われますが、std::uncaught_exceptions()constexprではないためif constevalで分岐するなどの特殊対応が必要になります。

前述のように、定数式における例外送出が許可されない現状であればconstexpr指定するだけならば簡単に行えるため、同様の場合に余計なif constevalを書かなくても良くするために、std::uncaught_exceptions()constexpr対応させようとする提案です。

std::current_exception()は一貫性のために同時にconstexpr対応することを提案しているものの、ユースケースは不明としています。

提案より、実装例

namespace std {
  constexpr int uncaught_exceptions() noexcept {
    if consteval {
      return 0;
    } else {
      return __uncaught_exceptions_impl();  // 現在の実行時実装を実行
    }
  }

  constexpr exception_ptr current_exception() noexcept {
    if consteval {
      return exception_ptr(nullptr);
    } else {
      return __current_exception_impl();  // 現在の実行時実装を実行
    }
  }
}

この実装の場合、std::exception_ptrリテラル型にしなければなりませんが、現在の主要な実装は全て単なるvoid*のラッパとしてそれを実装しているため、これは簡単に対応可能だとしています。

この提案の動機自体は静的リフレクションとは無関係ですが、静的リフレクションの提案(P2996R1)においてはそのエラー報告メカニズムとして定数式における例外の利用を検討しており、その場合には上記のような単純な実装では済まなくなります。提案ではその場合の実装として

  • コンパイラstd::active_exceptions()およびstd::current_exception()が正しく動作するように定数式において全てのアクティブな例外オブジェクトを追跡しなければならない
    • このようなグローバル状態は現在定数式には存在しないため、コンパイラ側での実装が必要
  • std::exception_ptrは型消去され、参照カウンタによるスマートポインタのように動作する必要がある
    • P2738R1によってvoid*からのキャストが定数式で許可されているため、実装は可能になっている

の2点について、少なくともコンパイラによるサポートが必要としています。

P3106R0 Clarifying rules for brace elision in aggregate initialization

素数不明の配列の{}省略を伴う初期化時の規定の矛盾を修正する提案。

素数不明の配列を初期化する場合、その要素数は初期化子リスト内の初期化子の数で決まる、とされています。一方で、規格書にある次のような例はそれと矛盾した結果を規定しています

struct X { int i, j, k = 42; };

// どちらも同じ要素数の配列となる
X a[] = { 1, 2, 3, 4, 5, 6 };
X b[2] = { { 1, 2, 3 }, { 4, 5, 6 } };

aの初期化子リストには6つの初期化子があり、bは2つです。しかし、この2つの配列は同じ要素数(2)かつ同じ値になるとされています。

これと同様の問題はNTTPの推論においても存在しています

template<int N>
void f1(const X(&)[N]);

f1({ 1, 2, 3, 4, 5, 6 }); // Nは2か6のどちら?

template<int N>
void f2(const X(&)[N][2]);

f2({ 1, 2, 3, 4, 5, 6 }); // Nは1か6のどちら?

この場合のようなNは要素数不明の配列初期化時と同様に初期化子リスト内の初期化子の数で決まりますが、{}初期化可能な要素型を考慮するとその要素数がいくつになるのか不明です。

これはCWG Issue 2149で報告されており、この提案はその修正のための文言を提供するものです。

修正に当たっては、初期化子リスト内の各初期化子は初期化先の集成体型の各メンバにマッチするように割り当てられ、そのうえで初期化子リスト内の{}省略境界を判定して要素数を推論します。

先程の例の場合

struct X { int i, j, k = 42; };

X a[] = { 1, 2, 3, 4, 5, 6 };
X b[2] = { { 1, 2, 3 }, { 4, 5, 6 } };

この動作は変わらず、どちらも要素数2の配列であり同じ値を持ちます。

template<int N>
void f1(const X(&)[N]);

f1({ 1, 2, 3, 4, 5, 6 }); // Nは2

template<int N>
void f2(const X(&)[N][2]);

f2({ 1, 2, 3, 4, 5, 6 }); // Nは1

この場合はそれぞれN = 1, N = 2に推論されます。

提案よりその他の例

struct S1 { int a, b; };
struct S2 { S1 s, t; };

// xとyは同じ値を持つ
S2 x[2] = { 1, 2, 3, 4, 5, 6, 7, 8 };
S2 y[2] = {
  {
    { 1, 2 },
    { 3, 4 }
  },
  {
    { 5, 6 },
    { 7, 8 }
  }
};
// y[3]要素の初期化子は省略されている
float y[4][3] = {
  { 1, 3, 5 },
  { 2, 4, 6 },
  { 3, 5, 7 },
};

// この初期化子は上記と同じ効果を持つ
float y[4][3] = {
  1, 3, 5, 2, 4, 6, 3, 5, 7
};
struct S { } s;
struct A {
  S s1;
  int i1;
  S s2;
  int i2;
  S s3;
  int i3;
} a = {
  { },  // s1の初期化子
  0,
  s,    // s2の初期化子
  0
};      // s3とi3は未初期化
struct A {
  int i;
  operator int();
};
struct B {
  A a1, a2;
  int z;
};

A a;
B b = { 4, a, a };
// b.a1.iは4で初期化
// b.a2はaで初期化
// b.zはaのint変換演算子の結果で初期化

この提案による修正は、既存コンパイラの動作やユーザの期待するところをベースとしているため、おそらく既存のコードの振る舞いが変わることはないはずです。

この提案は2024年3月に行われた東京会議で承認され、C++26ドラフトに取り込まれています。

P3107R0 Permit an efficient implementation of std::print

std::printのより効率的な実装を許可する提案。

C++20で追加されたstd::printの現在の規定(正確には、内部で使用されるvprint_unicode/vprint_nonunicode)では、出力文字列を一旦一時std::stringオブジェクトに出力してから、その文字列をストリームへ出力するように規定されています。これは仕様の簡素化とインターリーブ出力しないことを明確にすることを目的としていました。

例えば、vprint_nonunicode()の効果は次のように指定されています

vformat(fmt, args)の結果をstreamへ書き込む

vformat(fmt, args)の結果はfmtに従ってargsをフォーマットした結果文字列を表すstd::stringオブジェクトであり、この呼び出しでstd::stringの一時オブジェクトが生成されています。vprint_unicode()の規定はもう少し複雑ですが、同様にvformat(fmt, args)の結果の文字列をストリームに出力するようになっています。

しかし、これに基づく実装ではロックしたストリームバッファに直接書き込むような効率的な実装を取ることが出来ないことが分かりました。CのprintfはCのストリームバッファをロックしながらインターリーブせずに出力する効率的な実装(POSIXflockfile/funlockfile)を行っており、Rustやjavaの出力関数も同様の実装を行っているようです。

現在の規定に沿った実装でそれを採用できないのは、ユーザー定義のフォーマッターに対して観測可能な影響があるためです。ユーザー定義のフォーマッターから例外が送出される場合、現在の規定のように出力文字列を一時文字列に出力してからストリーム出力する実装だと例外が送出された場合は文字は一切出力されませんが、直接ストリームバッファに書き込む効率実装では例外が送出される前にストリームに書き込まれた文字列は出力されてしまいます。

ただし、フォーマット文字列のエラーはほとんどの場合にコンパイル時に検証されるためそれが問題となるケースはまれであり、対策としてはユーザー側で中間文字列やバッファにstd::format出力してからstd::print出力するという方法を取ることができます。

また、現在の規定に沿った実装ではstd::printの出力時に無制限な動的メモリ確保を行う可能性があり、リソースに制約がある環境におけるstd::printの利用を適さないものにして、Cの関数を直接使用するような相対的に危険な方法を採用し続ける誘因を発生させる可能性があります。

この提案は、規定を修正してstd::printでもCのprintf同様の効率実装を許可しようとするものです。それによって、出力時の余分なメモリ確保が不用になり、提案のパフォーマンス比較の報告によれば20%程高速化可能とのことです。

この提案は2024年3月に行われた東京会議で承認され、C++26ドラフトに取り込まれています。

P3109R0 A plan for std::execution for C++26

P2300で提案中のExceutorライブラリについて、C++26サイクル中に取り組むべき作業項目リスト。

P2300で提案中のExceutorライブラリ(sender/receiverライブラリ)は、C++26の導入を目指して作業されておりLWGにおける初期レビューを終了いているため、おそらくC++26には入ると思われます。

この提案は、現在報告されている設計上の問題点とその解決や役立つ追加のユーティリティなど、C++26サイクル中にP2300に対して必要な追加の作業についてリストアップし、その優先度などを検討するものです。

提案で挙げられている破壊的変更を伴う設計変更は次のようなものです

  • ensure_started()アルゴリズムの削除
    • 非同期スコープへの置換
  • tag_invokeへの依存の削除
  • queryableコンセプトの調整
  • stop_callbackの同期オーバーヘッドを削減できるように、get_stop_token()の要件を修正
  • execution::run_loopクラスの設計改善
  • ヒープ割り当てを伴うsenderにおけるカスタムアロケータの許可

Exceutorライブラリの初期出荷に含めるべきユーティリティは次のようなものです

  • 非同期スコープ
  • コルーチンタスク型
  • システムExecution Context

これらの項目の変更は纏めると次のように類別されます

提案 項目 変更種別
P2519 非同期スコープ+ensure_started()アルゴリズムの削除 設計の追加
P2855 tag_invokeへの依存の削除 設計変更
P3121 queryableコンセプトの調整 設計変更(一般化)
get_stop_token()の要件修正 最適化
run_loopクラスの設計改善 設計変更
senderにおけるカスタムアロケータ 設計追加
コルーチンタスク型 設計追加
P2079 システムExecution Context 設計追加
アルゴリズム破棄順序の明確化 設計変更

この表は提案からのもので、おそらく上に行くほど優先度が高いものです。ただし、この提案ではまだ優先度を明確に決定していません。

提案には各問題について個別の説明がありますが、個別の提案でより詳細に検討されると思われるのでここでは省略します。

P3110R0 Array element initialization via pattern expansion

パターンの展開による配列初期化の提案。

配列の要素を宣言時に初期化する場合、デフォルト初期化以外の初期化が必要になると各要素に対して明示的に初期化子を指定する必要があります。要素数が大きい場合これはかなり面倒なことになり、また、要素数がテンプレートパラメータなどで指定される場合は直接初期化子を書くことができません。このため、配列と配列要素の初期化は分けて書かれることになりますが、その場合要素型がデフォルト構築可能でないとそれすらもエラーになります。

class E {
public:
  E(int);
};

E a[100];       // error: Eはデフォルト構築可能ではない
E b[100] = { }; // error: Eはデフォルト構築可能ではない
E c[100] = { 0, 0, 0, /* 97 more zeros... */ }; // OK、しかし煩雑

template <size_t N>
void f() {
  // Nが初期化子の数と合わないとエラー
  E x[N] = { 0, 0, 0, /* how many zeros here? */ };
}

この提案は、この問題の解決のためにパラメータパック展開の構文をベースにした新しい配列初期化構文を提案するものです。

提案する構文は次のように、配列初期化時に数値リテラルに対して...を使用するものです

int a[57] = { 5 ... }; // 配列aのすべての要素を5で初期化

この...による数値リテラルの展開は、ちょうど初期化対象の配列の要素数分だけ初期化子をコピペします。

注意点として、5...のように...を数値にくっつけてしまうと浮動小数点数リテラル5.05.と略記できる)として認識されてしまうため、数値と...の間にはスペースが必要です

int a[27] = { 5... };   // error, 5... は不正な数値リテラルとして解釈される
int b[27] = { 5 ... };  // OK
int c[27] = { (5)... }; // OK, pattern does not end with a numeric literal

この初期化構文は既存の各要素に対する初期化と組み合わせて使用することができます。その場合は初期化子リストの最後の初期化子として現れます

// 最初の4要素を1, 2, 3, 4で初期化し、残りの96要素は5で初期化
int a[100] = { 1, 2, 3, 4, 5 ... };

この初期化パターンの展開後の各初期化子は手書きされたときと同様に個別に評価されます。関数呼び出しが含まれている場合、各関数呼び出しは初期化ごとに呼び出されます。

#include <cassert>
#include <cstddef>

int array_elem(std::size_t index);

void f() {
  std::size_t n = 0;
  int a[32] = { array_elem(n++)... };

  assert(n == 32);
}

{}でも()でも、この場合の初期化子の評価順序は右から左とすることを提案しています。

集成体初期化における{}省略では、1つの初期化子リスト内のすべての初期化子が同じ配列の初期化に関与している場合にのみこの構文を使用できます。

struct A {
  int a, b, c[20];
};

A a1 = { 1, 2, { 3, 4, 5 ... } };   // OK
A a2 = { 1, 2, 3, 4, 5 ... };       // error: `1, 2`の初期化子は非配列の初期化に使用される

struct B {
  int x[20];
};

B b1 = { { 1, 2, 3, 4, 5 ... } };   // OK
B b2 = { 1, 2, 3, 4, 5 ... };       // OK, すべての初期化子は同じ配列の初期化に使用される

struct C {
  int a[20], b, c;
};

C c1 = { { 1, 2, 3 ... }, 4, 5 };   // OK
C c2 = { 1, 2, 3 ..., 4, 5 };       // error: この構文は初期化子リストの最後に現れなければならない
C c3 = { 1, 2, 3 ... };             // OK, bとcは0で初期化される

struct D {
  int a, b;
};

D d1[5] = { { 1, 2 }, { 3, 4 }... };    // OK
D d2[5] = { 1, 2, 3, 4 ... };       // error: `4 ...`の初期化子は配列の初期化に使用されない
D d3[5] = { 1, 2, { 3, 4 }... };    // error: 一部の初期化子は配列の初期化に使用されない

この提案による初期化構文は、同じ値(初期化子)を繰り返す形の初期化のみを行い、1 ... 5のような変化する値による初期化はサポートしていません。

EWGIにおける初期のレビューではこの提案のさらなる追求に合意が得られなかったものの、代わりにこのアプローチをより一般化した初期化方法に興味を持っているようです。

P3112R0 Specify Constructor of std::nullopt_t

初期化子リストからstd::nullopt_tへの変換が考慮されるのを防止するために、std::nullopt_tのコンストラクタを明示的に定義しておく提案。

この問題は、次のようなコードがGCCコンパイルエラーを起こすことに起因しています

#include <optional>

struct Widget {
  class PassKey {
    PassKey() = default;
    friend class Widget;
  };
  Widget(PassKey) {}

  static std::optional<Widget> make() {
    std::optional<Widget> result{{{}}}; // ng、GCCのみ
    return result;
  }
};

result{{{}}}の意図するところは、Widget::Passkeyをデフォルト構築したうえでWidgetを構築しようとしています。しかしこのコードは、GCC(13.2)では拒否され、エラーメッセージによると初期化子リストからstd::nullopt_t変換できないためエラーとなっているようです。

GCC(libstdc++)のnullopt_tのコンストラクタは、単一のタグ型を取るexplicitコンストラクタとして定義されています。これによって、nullopt_tのコンストラクタは{}からの暗黙変換シーケンスが存在しますが、そのコンストラクタがexplicitであることによってその経路は常に拒否されます。

ただ経路が存在することによってoptionalの構築時にその経路が考慮されてしまい、しかもnulloptを取るコンストラクタがテンプレートではないことからオーバーロード解決で優先的に選択され、結果コンパイルエラーとなります。すなわち、result{{{}}}は一番内側の{}nullopt_tコンストラクタのタグ型の構築、その内側(中間)の{}nullopt_tの構築、一番外側の{}optional<Widget>の構築、として認識されてしまっています。

一方で、この問題はClang(libc++)やMSVC(MSVC STL)では起こりません。なぜなら、これらの実装ではnullopt_tのコンストラクタは2つのタグ型を取るようにされており、{{}}nulloptの構築として認識されないためです。

このような実装揺れが発生しているのは、nulloptの構築に関する規定が明瞭ではないためです。規定では、「nullopt_t型は、デフォルトコンストラクターも初期化子リストコンストラクターも持たず、集成体ではない」としか規定されておらず、これをどのように実装すべきかを指定していません。この制限はnullopt_tの実体をstd::nullopt1つに留めて不用意に構築されないようにするためのものですが、この制限だけでは上記の問題における暗黙変換シーケンスを防止するほど協力ではありません。例えば、次のような実装も許可されます

namespace std {
  struct nullopt_t {
    nullopt_t(int) {}  // not explicit
  };
}

類似の問題はCWG Issue 2525で報告されており、こちらはコア言語におけるオーバーロード解決時の暗黙変換シーケンスの考慮を変更し、上記のGCCのような暗黙変換の考慮が正当となるようにしようとするものです。こちらはより一般的な問題の解決(この提案の意図と反するものではあるものの)ではありますが、上記の問題の解決のためにはより限定的な方法をとることもできます。

この提案ではCWG2525とは無関係に、上記のような初期化子リストからのoptional構築時にnullopt_tのコンストラクタがoptionalの構築に影響を及ぼすことを防止するために、nullopt_tに特殊なコンストラクタを明示的に追加することを提案しています。

struct nullopt_t {
  struct nullopt-construct-tag {};  // exposition only

  constexpr explicit nullopt_t(same_as<nullopt-construct-tag> auto) {}
};

inline constexpr nullopt_t nullopt(nullopt_t::nullopt-construct-tag());

nullopt_tコンストラクタのタグ型を明示的にし、nullopt_tの唯一のコンストラクタテンプレートとしてそのタグ型のみを受け入れるものを追加します。{}からの推論でこのコンストラクタが考慮されることは決してないため、上記の問題をコア言語の解決によらずに解消することができます。

P3113R0 Slides: Contract assertions, the noexcept operator, and deduced exception specifications

noexcept(contract_assert(false))の問題についての解説とソリューションを比較するスライド。

なぜこれが問題になるのかから考えられるソリューションを紹介し、そのソリューションを求められる要件によって比較した結果などを解説しています。

現在のところ、Contracts MVPの仕様はソリューション6aのcontract_assertを文にするという解決を採用しています。

P3114R0 noexcept(contract_assert(_)) -- slides

noexcept(contract_assert(false))の問題の解説とソリューションについての提言を行うスライド。

この問題についてのユーザー目線での要件を説明し、解決策として次の2つを挙げています

  1. 違反ハンドラからの例外送出を禁止する
  2. contract_assert()が例外送出することを受け入れる

P3115R0 Data Member, Variable and Alias Declarations Can Introduce A Pack

パラメータパックを導入できる場所を増やす提案。

現在パラメータパックを導入することができるのは

  • テンプレートパラメータ
  • 関数引数
  • 初期化キャプチャ
  • 構造化束縛(C++26予定)

の4箇所のみです。この提案はこれに加えて

でもパックを導入できるようにしようとするものです。

この提案の内容はP1858で以前に提案されていたもので、この提案はそれをベースに改善を加えてC++26に導入することを目指すとともに、Circleやclangにおける実装経験や静的リフレクションとの関連について説明するものです。

まず、エイリアスによるパック導入は型エイリアス名の代わりに型パックを宣言するものです。

template <typename ...Ts>
struct S{
  using ...value_types = std::remove_cvref_t<Ts>;
};

これによる最大の問題点は、パラメータパックが非テンプレートの文脈で任意に現れるようになってしまう点です。現在パラメータパックが出現しうるのは、可変長テンプレートのクラス/関数定義内だけですが、パックエイリアスは任意の場所で展開されて使用される可能性があります。

template <typename ...Ts>
void g();

void f() {
  g<S<int, int>::value_types...>();
}

この影響は主に実装に関してのもので、パック処理の実装によっては任意の場所でパック展開が起こることを想定していないものがあるかもしれず、実装した場合のコンパイル時間への影響は避けられません。

この問題も長く議論されてきましたが、C++26を予定している構造化束縛によるパック導入の機能の一部としてもそれが必要とされる部分があり、最終的には実装負荷を受け入れるべき、としています。

変数宣言によるパック導入は主に他の機能との一貫性向上のために提案されています。これによって例えば、std::integer_sequenceの使用感を改善することができます

template <typename T, T... V>
struct __ints {
  static constexpr decltype(V) ...values = V;
};

template <auto N>
constexpr auto ...ints = __make_integer_seq<__ints, decltype(N), N>::...values;

template<typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {

constexpr std::size_t size = sizeof...(Ts);
  (print(std::get<...ints<size>>(t)), ...);
}

メンバ変数宣言によるパック導入はこの提案の最も強力な部分です。その使用は単純に、メンバ変数をパックとして宣言することができるものです。その後は、パックであることは除いて通常のメンバ変数のように使用できます。

template <typename ...Ts>
struct tuple {
  [[no_unique_address]] Ts ...elems;

  tuple(Ts... ts) : elems(ts)... {};
};

template <typename ...Ts>
tuple(Ts...) -> tuple<Ts...>;

例えばこのように、tupleの実装をかなり改善することができます。

提案より、その他の例。

std::bindlikeなユーティリティの実装

template <typename Functor, typename... Args>
struct bind {
  [[no_unique_address]] Functor f;
  [[no_unique_address]] Args... args;

  template <typename Self, typename... Xs>
  auto operator()(this Self&& self, Xs&&... xs) -> decltype(auto) {
    return std::invoke(
      std::forward<Self>(self).f,
      (std::forward<Self>(self)....args)...,
      std::forward<Xs>(xs)...);
  }
};

template <typename Functor, typename... Args>
bind(Functor&& f, Args&&... args) -> bind<Functor, Args...>;

template <typename = void>
auto test() {
  return bind{[](int a, int b, int c) {
                return a + b + c;
              }, 1, 2}(3);
}

variantの実装

template <typename ...Ts>
struct variant {
  union {
    Ts ...data;
  };
};

リフレクションとの関連に関しては、これらパックの活用はリフレクションと相互に補完し合うもので、特に型のリストに関する処理を効率的に書けるようになる、としています。また、リフレクションはmeta::inforangeスプライシング[: rng :]...rngの各要素のmeta::infoに対して文法要素を生成する)をサポートすべきとということも提言しています。

P3116R0 Policy for explicit

標準ライブラリ関数にexplicitを付加するポリシーについての提案。

標準ライブラリ中でexplicitを使用するパターンは主に次の3つに類別されます

  1. explicit operator bool()
    • 限定的な状況でのみboolへの暗黙変換を許可する
    • 例えば、std::optionalifの条件分の中などではboolとして使用できるが、bool変数の初期化にはそのまま使用できない
  2. 単一引数コンストラクタにおける暗黙変換防止のためのexplicit
    • 単一引数コンストラクタは変換コンストラクタとして暗黙変換によって意図しない型からの構築を引き起こすことがある
    • 単一引数コンストラクタをexplicitにすると暗黙変換が防止され、コンストラクタ引数型に一致する型のみを引数として取るようになる
  3. みだりな構築を回避するための、タグ型における引数無しコンストラクタのexplicit
    • タグ型を{}から構築させないようにし、構築の際はその型名を明示的に書かせる
    • 例えばstd::unexpect_tはそれを受け取る場所(std::expectedコンストラクタ)で{}を使用できず、std::unexpect_t{}もしくはunexpectの使用を強制する

<ranges>の一部を除いてこの3つのパターンに反するexplicitの使用は見つからず、これは現在の標準ライブラリが暗黙の一貫性に従ってexplicitを使用していることを示しており、この一貫性を言語化することで1つのポリシーとなり得ます。

この提案は、上記3つの基準をそのままポリシーとして提案するものです。

なお、<ranges>における例外とはP2711R1で提案されていた、view型の複数引数コンストラクタの挙動を単一引数のものと一貫させることを意図したもの(foo_view x = {arg};foo_view x = {arg1, arg2};を共に禁止するもの)で、view型の使用が稀であることも相まって例外的なものなのでこれをポリシー化することは提案していません。

P3117R0 Extending Conditionally Borrowed

呼び出し可能なものを受け取るタイプのview型を条件付きでborrowed_rangeになる用ようにする提案。

C++20の<ranges>ではP2017R1の採択によって多くのRangeアダプタのview型は入力範囲がborrowed_rangeであるかによって自身もborrowed_rangeとなるようになっています。しかし、入力の範囲以外に追加の引数を受け取るようなタイプのview型では考慮されていない場合があります。

ある範囲がborrowed_rangeであるということは、その範囲は要素列を保有しておらず実質的に参照型のように扱うことができることを意味します。これはまた、その範囲オブジェクトを破棄したとしてももとの要素列が維持されているということで、ranges::subrangeの取得などの操作を安全に行えます。また、この性質はviewが状態を持たないか、持ったとしてもイテレーションにあたってそれを変更せず、イテレータ側にコピーされていることを意味しています。

view型がborrowed_rangeであることはRangeアダプタをパイプでチェーンした場合に重要となり、入力範囲がborrowedである場合に最終的なview型もborrowedとなるかどうかに影響します。なるべく多くのRangeアダプタのview型がborrowed_rangeを継承できるようにしておくと、それを妨げるケースを減らすことができます。

現在どうしてもborrowed_rangeにならないviewには例えばviews::transformがあります。views::transformの場合、入力範囲がborrowed_rangeであり受け取る変換関数がサイズ0(すなわち状態を持たない)の場合にtransform_viewborrowed_rangeにすることができます。通常のtransform_viewと比較すると変換関数を保存している領域の分サイズを小さくすることができ、これによって関数呼び出しパフォーマンスを向上させることが期待できます。

このようなborrowed_rangeとなれるviews::transformは既にP2728R6(ユニコード変換機能提案)にてviews::projectとして提案されています。そこでは、ユニコード文字列の各文字に対して別のユニコード形式へ変換するviewの実装に使用されており、変換パフォーマンスが重要になるためにサイズオーバーヘッドを回避することを目的として使用されています。

とはいえ、ほとんど同じことを行うRangeアダプタを別名で追加することなく、現在のviews::transformを調整することでこれは達成できるはずです。また、より一般化して、呼び出し可能なものを受け取るRangeアダプタ全体に対してそのような最適化を導入することができるでしょう。これは変更が必要なそれぞれに対して別のアダプタを追加するよりも有用なアプローチです。

この提案はこれらの理由から、呼び出し可能なものを受け取るタイプのRangeアダプタのview型が、入力範囲がborrowed_rangeであり受け取る呼び出し可能なものが空(ステートレス)である場合に自身もborrowed_rangeとなるように修正しようとするものです。

この提案では、次のRangeアダプタのview型について変更を提案しています

  • join_view
    • 入力範囲がborrowedでありjoin_viewforwardであれば、join_viewborrowed_rangeになる
  • transform_view, zip_transform_view, adjacent_transform_view, take_while_view, filter_view, chunk_by_view
    • 入力範囲がborrowedであり受け取るcallableが空なら、それぞれのviewborrowed_rangeになる
  • lazy_split_view, split_view, join_with_view
    • 使用するパターン範囲が小さい場合にそれをイテレータにコピーすることで、borrowed_rangeになようにする

呼び出し可能なものを受け取るview型以外に対しても、特定条件下でborrowed_rangeとなれるようにすることを提案しています。

提案にはlibstdc++の実装に対して変更を適用した上でのパフォーマンス測定結果が付加されており、transform_viewの場合のみ提案する変更によって呼び出しパフォーマンスの向上が確認できたと言うことです。他のものはほぼ変化がなかったものの、悪くなることはないようです。

標準ライブラリ関数に対する[[nodiscard]]指定を個別関数に行うのではなく、推奨事項として単一の文章で指定するようにする提案。

個別の関数に[[nodiscard]]を付加しようとするときでもWG21の正規の議論手順(LEWG -> LWG -> WG21)を踏まなければなりませんが、これはただでさえ限られている委員会のリソースを浪費してしまい、時間の使い方として適切であるとは言えない側面があります。

例えば、P2377R0では<iterator>の関数に対して[[nodiscard]]を付加しようとしていますが、文言だけで10P近くもあり、しかもR0には問題があったため改訂を必要としています。

重要なのは、現実の標準ライブラリの実装で、実際に使用しているユーザーに対して、実際の警告を表示することであって、標準のPDFの個々の宣言に[[nodiscard]]が存在するかどうかは重要ではありません。

個別の関数に1つ1つ[[nodiscard]]を付加していって実装を制御しようとするよりも、単一の規範的な推奨事項によってそれを行う方が時間の使い方としてはるかに有効です。

また、標準ライブラリ実装者はその実装にあたってどの関数に属性を追加するべきかを決定する最も適切な立場にいます。例えば、x == end;が引数を変化しないことをコンパイラが認識して警告を表示することができるのであれば、その実装ではoperator==に対して[[nodiscard]]を指定する必要はありません。標準ライブラリ実装者はこれを適切に判断することができます。

この提案は、このような理由から、標準ライブラリの規定で個別の関数に[[nodiscard]]を付加するのはやめて、どのような関数にそれを付加すべきかを推奨する文言によってそれを統一的に指定しようとするものです。

提案されている文言では、次のリストに該当する関数の呼び出しにおいて、評価されうる文脈でその値が破棄されている場合に警告を発すべき(=[[nodiscard]]を付加すべき)としています

  • operator==, operator<=>
  • 引数を介してアクセス可能なオブジェクトの状態を変更しない、explicitメンバ関数constメンバ関数
    • vector::empty() constunordered_map::bucket_size(size_type) constなど
  • ポインタ/参照/イテレータを返す非constメンバ関数で、オブジェクトパラメータ(this)や戻り値が参照するオブジェクトを変更しないもの
    • vector::begin()set::find(const key_type&)basic_string::operator[](size_type)など
  • 非参照引数を変更することを除いて、引数からアクセスできるオブジェクトを直接的にも間接的にも変更しない非メンバ関数
    • as_const(T&)back_inserter(Container&)ranges::next(I)など
  • 戻り値型がvoidではない、CPOの関数呼び出し演算子

このわずかな文章で、標準ライブラリ内の関数に対する[[nodiscard]]の指定を将来にわたっても制御することができるようになり、標準の文章量を削減するとともに委員会のリソースを効率的に使用できるようになります。

P3123R0 2024-02 Library Evolution Polls

2024年02月に行われる予定のLEWGの全体投票の予定。

次の5つの提案が投票にかけられる予定です

最後の1つを除いて、残りのものはC++26に向けてLWGに転送するための投票です。

P3126R0 Graph Library: Overview

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

この提案は、以前にP1709R5で提案されていたグラフライブラリの提案をいくつかの提案に分割したもののうちの1つで、提案する機能の大まかな概要を説明するものです。

以前の提案についてはそちらの記事を参照

グラフとそのアルゴリズムという分野はとても広いため、この提案ではそのうちで将来の基盤となる必須なものと有用でよく使用されるものに焦点を絞って提案しています。

記述されているゴールや優先度は次のようなものです

  • ライブラリにしっかりとした理論的基盤を提供する
  • 標準ライブラリによって確立された、範囲・アルゴリズム・コンテナ・ビューの分離に従った設計
  • ライブラリを有用ものにするために十分なアルゴリズムセットを含める
    • アルゴリズムの構文は、シンプルかつ表現力豊かで理解しやすいものである必要がある
    • 高性能なアルゴリズムを実装する能力を犠牲にしてはならない
    • アルゴリズムは、入力の頂点集合がrandom_access_rangeであり頂点IDが最初に整数であることを期待できる
  • 低レベルなインターフェースを使用することなく、グラフの頂点とエッジを走査することのできる一般的なビューを含む
    • 頂点リスト、頂点の入射エッジ、頂点の近傍、グラフの辺などを走査するためのシンプルなビュー
    • 深さ優先探索幅優先探索、トポロジカルソートのための複雑なビュー
  • ビューとアルゴリズムによって使用されるグラフコンテナインターフェース。様々なグラフデータ構造に対して一貫したインターフェースを提供する
    • このインターフェースには、コンセプト、型、型特性、関数が含まれており、<ranges>と同様の役割を提供する
    • ビューとエッジリストによる、頂点、エッジ、近傍の一貫したデータモデルの記述子
    • カスタマイズポイントによって、既存のグラフとデータ構図の両方に使用できる
    • 隣接リスト、頂点の外側の範囲と各頂点の発信エッジの内側の範囲
      • カスタマイゼーションポイントを通して、既存のグラフデータ構造でアルゴリズムとビューを使用できるようにする
      • エッジ・頂点・グラフ自体でのユーザー定義型のオプショナルなサポート
      • 二部グラフと多部グラフをサポート
    • エッジリスト、エッジリストはエッジディスクリプタの範囲
      • edgelist_viewからranges::transform_view を使用するユーザー定義範囲から
      • 圧縮された疎行列に基づく、高性能なcompessed_granhコンテナ

この初期の設計は、将来的なグラフライブラリの改善・拡張を妨げないようにする必要があります。現時点でのよていは

  • 並列アルゴリズムを含む、さらなるグラフアルゴリズム
  • スパースなグラフやbidirectionalなコンテナのサポート
  • 双方向グラフのサポート
  • 非整数な頂点のサポート
  • constexpr

などがあります。

このグラフライブラリは次の例のように、標準のコンテナを使用した範囲の範囲としてグラフを定義し使用します

// グラフコンテナ(頂点の範囲、頂点は接続されている辺の範囲)
using G = std::vector<std::vector<int>>;

// 13個の頂点を持つグラフ
G costar_adjacency_list = {
  {1, 5, 6}, {7, 10, 0, 5, 12}, {4, 3, 11}, {2, 11}, {8, 9, 2, 12}, {0, 1}, {7, 0},
  {6, 1, 10}, {4, 9}, {4, 8}, {7, 1}, {2, 3}, {1, 4} };

// ↑のグラフの各ID(頂点)に対応する非整数要素
std::vector<std::string> actors = { "Tom Cruise", "Kevin Bacon", "Hugo Weaving",
                                    "Carrie-Anne Moss", "Natalie Portman", "Jack Nicholson",
                                    "Kelly McGillis", "Harrison Ford", "Sebastian Stan",
                                    "Mila Kunis", "Michelle Pfeiffer", "Keanu Reeves",
                                    "Julia Roberts" };

int main() {
  std::vector<int> bacon_number(size(actors));

  // ID 1の頂点から、接続されている辺に沿って幅優先探索でイテレートする
  // 頂点ID 1はKevin Bacon
  for (auto&& [uid,vid] : basic_sourced_edges_bfs(costar_adjacency_list, 1)) {
    bacon_number[vid] = bacon_number[uid] + 1;
  }

  // 計算結果をID順で出力する
  for (int i = 0; i < size(actors); ++i) {
    std::cout << actors[i] << " has Bacon number " << bacon_number[i] << std::endl;
  }
}

このサンプルコードは、actors配列に格納された人物のベーコン数を計算しています。出力は次のようになるようです

Tom Cruise has Bacon number 1
Kevin Bacon has Bacon number 0
Hugo Weaving has Bacon number 3
Carrie-Anne Moss has Bacon number 4
Natalie Portman has Bacon number 2
Jack Nicholson has Bacon number 1
Kelly McGillis has Bacon number 2
Harrison Ford has Bacon number 1
Sebastian Stan has Bacon number 3
Mila Kunis has Bacon number 3
Michelle Pfeiffer has Bacon number 1
Keanu Reeves has Bacon number 4
Julia Roberts has Bacon number 1

提案するライブラリ機能は、stdgraphで実装が進められています。

P3127R0 Graph Library: Background and Terminology

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

この提案は、以前にP1709R5で提案されていたグラフライブラリの提案をいくつかの提案に分割したもののうちの1つで、提案の理論的背景やモチベーション等を説明するものです。

標準ライブラリのイテレータ抽象によるコンテナとアルゴリズムの分離はC++コーディングにとって革命的であり、標準で用意されるアルゴリズム関数群は十分に強力なものですが、本質的には1次元の範囲に対してしか適用できません。そのため、問題が1次元範囲で表現できないような場合には、標準ライブラリのアルゴリズムを活かすことができません。

多次元配列がそのよい例であり、別の重要な概念にグラフとそのアルゴリズムがあります。グラフはその対象の実体に関係なく、エンティティ間の関係をモデル化するための強力な抽象化であり、グラフは本質的に汎用的な概念です。グラフはコンピューターサイエンスの問題領域の至る所で現れ、現実世界のアプリケーションでも広く使用されています。例えば、インターネットのルーティング、地図上での最適経路探索、消費者の消費行動の分析、など様々な関係性を扱うのに使用され、もちろん機械学習の分野でも重要な役割を果たしています。

そのようなグラフ構造が使用されているところでは、グラフアルゴリズムもまた同時に使用されています。よく知られているものとしては、幅優先探索ダイクストラ法、連結成分などがあります。また、グラフは非常に多くの問題領域で使用されているため、それを表現するデータ構造も様々なものが使用されています。

グラフアルゴリズムを任意のグラフに適用するためには、原初のSTLと同様にアルゴリズムをその適用対象であるグラフの表現(データ構造)から適切に分離する必要があります。そのためには、グラフアルゴリズムを調査して各アルゴリズムがその入力に課す要件を最小化し、また体系的に整理して、これをコンセプトによって定式化する必要があります。

そのようにして確立されたグラフの標準インターフェースは、標準で定義されたグラフアルゴリズム以外のものを実装する場合にも有用となります。ユーザーは標準インターフェースに従ってグラフアルゴリズムを実装することで自然に再利用可能(特定のコンテナに縛られない)に定義することができ、それがオープンに公開されればC++コミュニティ全体にとって有益になります。

グラフは様々な場所で使用されており、現代のソフトウェアシステムにとって無くてはならないものであり、グラフアルゴリズムとデータ構造の標準化されたライブラリ機能はC++コミュニティに莫大な利益をもたらすことは間違いありません。この提案は、このような考え方に基づいて設計されたグラフライブラリを提案するものです。

提案ではグラフの初歩的な説明から、その表現とデータ構造などについての解説を行っています。

P3128R0 Graph Library: Algorithms

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、提案するグラフアルゴリズムについてまとめた文書。

ここで提案している、初期ライブラリに含めるべき優先度1のグラフアルゴリズムは次のものがあげられています

この提案ではこの優先度1のアルゴリズム群についての文言を提案しています。標準Rangeアルゴリズムのようにコンセプトによって制約された関数テンプレートであり、グラフに特有の次のような基本特性を指定しています

  • 複雑性(Complexity) : O(|E| + |V|)など
    • 辺の数(|E|)と頂点の数(|V|)によって指定されるアルゴリズムの計算量
  • 有向(Directed)? : yes/no
    • アルゴリズムは有向グラフにのみ使用できる(yes)か、無効グラフにも使用できる(no)か
  • 多重辺(Multi-edge)? : yes/no
    • ある2つの頂点間に同じ方向の辺が複数存在する場合にも、アルゴリズムは正しく動作する(yes)か否か(no)
  • 閉路(Cycles)? : yes/no
    • グラフに閉路が含まれている場合にも、アルゴリズムは正しく動作する(yes)か否か(no)
  • 自己ループ(Self-loops)? : yes/no
    • グラフに自己ループ(同じ1つの頂点をソースかつターゲットとする辺)が含まれている場合、アルゴリズムは正しく動作する(yes)か否か(no)
  • 例外? : yes/no
    • 関数が例外を投げるか(yes)否か(no)

提案には、優先度2以下のアルゴリズムの優先度案についても記載があります。優先度の低いアルゴリズムはこのライブラリ提案が最初に導入された後に追加していく予定です。

P3129R0 Graph Library: Views

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案のうち、提案するビューについてまとめた文書。

グラフに対してのビューは、<ranges>のビューがその要素をイテレーションするのと同様に、グラフの頂点を1次元の範囲としてイテレーションするためのものです。その際、ただイテレーションするだけではなく、幅優先や深さ優先などイテレーションをカスタマイズすることができるようになっています。そして、ビューであるので、それにあたって元のグラフをコピーせずにそれを行います。

このビューは、ユーザーが使用するものというよりは上記のアルゴリズム実装で使用されることを意図しています。

// uuの型は、vertex_descriptor<vertex_id_t<G>, vertex_reference_t<G>, void>
// gは何らかのグラフ、Gはその型
for(auto&& uu : vertexlist(g)) {
  vertex_id<G> id = uu.id;
  vertex_reference_t<G> u = uu.vertex;

  // ... do something interesting
}

vertexlist()はグラフの頂点をそのままイテレートするビューを返すものです。

グラフに対するビューの要素型はDescriptorと呼ばれるシンプルな構造体で、ビューの返す値を用途に合わせてカスタマイズできるようにするための中間表現です。これは構造化束縛によって分解することができます

for(auto&& [id, u] : vertexlist(g)) {
  // ... do something interesting
}

ビューの生成関数にはグラフの頂点を何かしらの値に変換するための関数オブジェクトを渡すことができます。

// グラフgの頂点をなにかの値に変換する
auto vvf = [&g](vertex_reference_t<G> u) { return vertex_value(g, u); };

// Descriptorの3つ目の値として変換結果を受け取る
for(auto&& [id, u, value] : vertexlist(g, vvf)) {
  // ... do something interesting
}

Descriptorは次の3つのいずれかの構造体となります

  • vertex_descriptor<ID_type, VertexType, VertexValueType>
    • 1つの頂点の情報を保持する型
  • edge_descriptor<IDType, Sourced, EdgeType, EdgeValueType>
    • 1つの辺の情報を保持する型
  • neighbor_descriptor<IDType, Sourced, VertexType, ValueType>
    • 1つの辺に接続されている頂点の情報を保持する型

Descriptorは3~4つのテンプレートパラメータの指定によってそのメンバ(保持する情報)を細かく調整します。

グラフに対するビューとしては次の4つが提案されています

  1. vertexlist Views
    • 頂点の範囲をイテレートし、各要素はvertex_descriptorの値
    • vertexlist(g)
  2. incidence Views
    • ある頂点に隣接する辺の範囲をイテレートし、各要素はedge_descriptorの値
    • incidence(g, vertex_id)
  3. neighbors Views
    • ある頂点に接続されている辺の範囲をイテレートし、各要素はneighbor_descriptorの値(イテレーション対象は辺に接続されている頂点)
    • neighbors(g, vertex_id)
  4. edgelist Views
    • グラフのすべての頂点の辺の範囲をイテレートし、各要素はedge_descriptorの値
    • edgelist()

どのビューにも、頂点/辺を値へ変換する関数を受け取るオーバーロードが1つ対応し、その2種についてIDにのみ注目するバージョンが用意されています(全部で、22種類4ビュータイプあります)。vertexlist Viewsだけは、それらバリエーションに対してさらにイテレート範囲を制限する範囲orイテレータペアを受け取るオーバーロードが用意されます(つまり、3オーバーロードタイプ2種類2IDタイプあります)。

さらに、探索を行うビューも3種類用意されています

  1. Depth First Search Views
    • *_dfs()
  2. Breadth First Search Views
    • *_bfs()
  3. Topological Sort Views
    • *_topological_sort()

この3つの探索ビューはそれぞれ、頂点・エッジ・有向エッジについて探索を行う関数(vertices,edges,sourced_edges)とそれのIDにのみ注目するバージョン(basic_*_(dfs|bfs|topological_sort)())の合計6種類が用意されており、それぞれには頂点/辺を値へ変換する関数を受け取るオーバーロードが1つ対応しています(つまり、全部で12種類*3探索タイプあります)。

P3130R0 Graph Library: Graph Container Interface

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

上で提案されている標準グラフアルゴリズムにおいては、頂点はrandom_access_rangeなコンテナかつその頂点ID(vertex_id_t<G>)は整数型であることを仮定しており、将来的に追加していくアルゴリズムにおいてもこれを前提としていく予定です。

しかし、グラフ構造の表現には定まったデファクト的なものはなく、在野のグラフ処理においては様々なデータ構造によってグラフが表現されています。ここで提案されているグラフコンテナインターフェースは、別に提案しているアルゴリズムの仮定するものよりも広い範囲のグラフ表現をカバーするものであり、標準ライブラリ内にあるかどうかに関わらず、std::liststd::vectorCSRベースのグラフ、隣接行列など、あらゆる形式の隣接グラフをカバーできるように設計されています。

これは、将来のグラフデータモデルの拡張の下地となることや、非標準グラフ実装のフレームワークとなることを意図しています。たとえば、スパースあるいは非整数な頂点ID、bidirectionalな連想コンテナに格納するなど、標準アルゴリズムの要件よりも緩い制約の下でグラフデータ構造が定義されるかもしれません。そのような実装においては、ビューやアルゴリズムも特化した実装を必要とします。その性能は標準グラフアルゴリズムの仮定によるものよりも性能は劣りますが、高性能なグラフコンテナにデータを移してから実行するより直接実行したほうが効率的な場合があり、グラフコンテナインターフェースはそのような場合をサポートするようになっています。

ここで提案されているグラフコンテナインターフェースはコンセプトを中心として設計されており、グラフコンテナのプロパティを取得する型特性やグラフコンテナ型から各種関連型を取り出すエイリアステンプレートと関数、エラーを表現するクラスやグラフを読み込む関数などが提案されています。この設計は、C++20の<ranges>, <iterator>のそれとよく似ています。

P3131R0 Graph Library: Graph Containers

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

1つ上のP3130R0では、グラフを保持するコンテナのインターフェースについてコンセプトを中心として指定していました。この文書では、このライブラリが主に扱う具体的なコンテナについて説明しています。

基本的には、標準ライブラリのコンテナを用いたrandom_access_range<forward_range<integral>>random_access_range<forward_range<tuple<integral,...>>>のようなものを隣接リストとして認識し、グラフとして扱うことができます。

次の例は、グラフをforward_listvectorとして定義し、エッジの値はターゲット(行先頂点)のIDとそのエッジの重み値の組とするものです。そして、そのグラフの全ての頂点をイテレートし、さらに頂点ごとにその頂点から出発する全てのエッジをイテレートしています。

// 標準コンテナによるグラフ型
using G = vector<forward_list<tuple<int,double>>>;

// エッジから重みを取得する関数
auto weight = [&g](edge_t& uv) { return get<1>(uv); }

G g;
load_graph(g, ...); // グラフデータの読み込み

// グラフコンテナインターフェースの関数を使用する
for(auto&& [uid, u] : vertices(g)) {
  for(auto&& [vid, uv]: edges(g,u)) {
    auto w = weight(uv);
    // do something...
  }
}

外側の範囲がrandom_access_rangeで内側の範囲がforward_rangeになっていて、内側の範囲の要素型が単なる整数値か整数値のtupleであるような型は、グラフコンテナインターフェース(P3130R0で提案されているもの)でそのまま使用することができます。これは標準以外のコンテナでも同様で、例えばboost::containersの各種コンテナを簡単に使用できます。

この提案ではこれに加えて、スパース行列によるグラフを表現するための特殊なコンテナ型であるcompressed_graphを提案しています。このグラフコンテナはCompressed Sparse Row形式で頂点とエッジ、および関連する値を格納するものです。1度構築した後は頂点とエッジを変更できませんが、その値(IDなど)は変更することができます。

template <class EV = void, // Edge Value type
          class VV = void, // Vertex Value type
          class GV = void, // Graph Value type
          integral VId = uint32_t, // vertex id type
          integral EIndex = uint32_t, // edge index type
          class Alloc = allocator<VId>> // for internal containers
class compressed_graph {
public:
  compressed_graph();
  explicit compressed_graph(size_t num_partitions); // multi-partite
  compressed_graph(const compressed_graph&);
  compressed_graph(compressed_graph&&);

  {tilde}compressed_graph();

  compressed_graph& operator=(const compressed_graph&);
  compressed_graph& operator=(compressed_graph&&);
}

提案する公開インターフェースはこれだけで、このグラフ構造へのアクセスは標準のグラフコンテナインターフェースを通してのみアクセスするようにすることを提案しています。

この2種類のどちらでもないその他のグラフコンテナの場合は、グラフコンテナインターフェースの各種関数(CPO)を利用するために、それらに対するアダプトが必要となります。ほとんどの場合は、次のCPO群を使用可能にすれば十分とされており、アダプトのためにはそのグラフ型の名前空間と同じところに対応する名前のフリー関数を用意しておきます。

  • vertices(g)
  • edges(g,u)
  • target_id(g,uv)
  • edge_value(g,uv) : エッジがvalue(s)を持つ(エッジに値がある)グラフの場合
  • vertex_value(g,u) : 頂点がvalue(s)を持つ(頂点に値がある)グラフの場合
  • graph_value(g) : グラフがvalue(s)を持つ場合
  • source_id(g,uv) : エッジがソースIDを持つ場合
  • グラフが複数のパーティションをサポートしている場合
    • partition_count(g)
    • partition_id(g,u)
    • vertices(g,u,pid)

これらのことは、P3130Rでコンセプト等によって指定されるグラフコンテナインターフェースの具体例でもあります。

P3133R0 Fast first-factor finding function

整数値の最小の素因数を返すstd::first_factor()の提案。

整数の素因数分解という問題はかなり活発的に研究されている問題の1つであり、Wikipediaにも12個のアルゴリズムが掲載されています。理想的なアルゴリズムには「効率性(時間と空間計算量の両方で)」「堅牢性(任意の入力に対して正しい答えを返す)」「実装の簡単さ」の3つの特性があります。

しかし、一般に整数の素因数分解アルゴリズムはこれら3つの特性のうち2つしか満たすことができません。例えば、trial divisionアルゴリズムは堅牢かつ実装が容易ですが効率的ではなく、Pollard’s rhoアルゴリズムは効率的で実装が簡単ですが堅牢ではありません。

そこで、標準ライブラリで実装を提供することで実装についての要件を要求から外すことができます。ユーザーから見れば、提供されるアルゴリズムは3つの特性を満たすアルゴリズムを使用している場合とほぼ同等になります。そのような実際の例としてUnixfactor()があり、この関数は1秒以内にあらゆる64ビット整数値を素因数分解することができます。

これらの理由と、別に提案中の物理量と単位のライブラリ機能の実装において素因数分解を行う関数が必要になったことから、この提案はそれを標準ライブラリ機能として備えておくことを提案するものです。

また、アルゴリズムconstexprとして実装できるため、標準ライブラリ機能として備えておくことで定数式においても整数値の素因数分解機能を提供することができるようになります。

物理量と単位ライブラリにおいては、基本単位に対して整数倍のファクター(大きさ)を持つような単位を扱う際にその表現のために使用されます。例えば、kmは基本単位であるmに対して103倍大きい単位となります。このような大きさをもつ単位の表現においても、元の基本単位で行える操作をすべてサポートしなければなりません(そうしないと複雑な単位を扱えなくなる)。物理単位においては和と差は意味がない操作であるためサポートする必要が無く、その他の操作は結局積と有理数乗の2つの操作に帰結されます。

例えば、力Fは組立単位ではm⋅kg⋅s^-2ですがその次元(基本単位の組み合わせ)はLMT^-2となります。この力に対してさらに距離をかけたのがエネルギーJで、組み立て単位ではm^2⋅kg⋅s^-2となり次元はL^2MT^-2となります。ここでは大きさは入っていませんが、大きさが入っている時でもこのような単位の組み立て(すなわち単位の積と有理数乗)を基本単位の上で成立させる必要があるわけです。

大きさを持つ単位におけるこのような操作をサポートするための効率的な表現方法として、大きさを素因数分解してその素因数の指数を利用する方法が知られています。例えば基本単位に対して一定のファクターを持つ2つの単位(同じでも、異なっていてもよい)同士の操作においては

  • 積の場合、各素因数の指数を足す
  • 有理数乗の場合、各素因数の指数をかける

となります。これは、それぞれの単位の大きさを覚えておいてそれを揃えて・・・のようなことをするよりも簡潔かつ効率的に基本単位に対する大きさを異なる単位同士の操作においても扱うことができます。

例えば、cmとmsの割り算(これは次元としては速度L/T)を考えると、距離Lの係数は10^-2=2^-2*5^-2、時間Tの係数は10^-3=2^-3*5^-3で、割り算cm/msの後での係数は、時間の係数の逆数の積となるのでそれぞれの素因数の指数によって、2^(-2+3)*5^(-2+3)=2^1*5^1=10となります。

このように、この素因数による表現では単位の大きさの換算をコンピューターにとって計算しやすい形で取り扱うことができます。この方法は提案の元になったmp-unitsライブラリをはじめとする在野の単位ライブラリでもうまく機能しているようです。

その際問題となるのは、非常に大きな素因数を含む数値を素因数分解する場合がある事です。堅牢であるが効率的でないアルゴリズムの場合、その計算に時間がかかるだけでなく定数式においての計算でconstexprループの制限に引っかかる場合があります。これは実際に、mp-unitsライブラリで陽子の質量を1単位とする単位を定義しようとしてできなかったという経験に基づいています。

実装は複雑だが効率的で正しいアルゴリズムを実装し、精密にテストして、このライブラリ機能のためだけに使用することもできます。その場合、同じ素因数分解を扱う処理やライブラリではそれぞれ個別に同様の再発明が必要となります。さらに言えば、そのようなアルゴリズムでも定数式におけるループの制限をすべての場合に回避できるわけではありません。

この提案では、そのような実装を標準ライブラリに追加してシンプルで使いやすいインターフェースによって提供することで、素因数分解という問題の解決策を広く提供するようにすることを提案しています。そして、そのような実装はコンパイラマジックによって定数式においても完全に使用可能なようにすることを意図しています。

提案するstd::first_factor()は次のようなものです

namespace std {
  constexpr std::uint64_t first_factor(std::uint64_t n);
}

これは入力nの最小の素因数を(1つ)返すものです。すべての素因数を取得するにはこの関数を用いて

std::vector<std::uint64_t> factorize(std::uint64_t n) {
  std::vector<std::uint64_t> factors;

  while (n > 1) {
    factors.push_back(std::first_factor(n));
    n /= factors.back();
  }
  
  return factors;
}

のようなコードを書くことで行えます。

P3135R0 Hazard Pointer Extensions

ハザードポインタの拡張機能の提案。

C++26で追加されたハザードポインタ機能の全景はこちらです

template <class T, class D = default_delete<T>>
class hazard_pointer_obj_base {
public:
  void retire(D d = D()) noexcept;
protected:
  hazard_pointer_obj_base() = default;
  hazard_pointer_obj_base(const hazard_pointer_obj_base&) = default;
  hazard_pointer_obj_base(hazard_pointer_obj_base&&) = default;
  hazard_pointer_obj_base& operator=(const hazard_pointer_obj_base&) = default;
  hazard_pointer_obj_base& operator=(hazard_pointer_obj_base&&) = default;
  ~hazard_pointer_obj_base() = default;
private:
  D deleter ; // exposition only
};

class hazard_pointer {
public:
  hazard_pointer() noexcept;
  hazard_pointer(hazard_pointer&&) noexcept;
  hazard_pointer& operator=(hazard_pointer&&) noexcept;
  ~hazard_pointer();
  [[nodiscard]] bool empty() const noexcept;
  template <class T> T* protect(const atomic<T*>& src) noexcept;
  template <class T> bool try_protect(T*& ptr, const atomic<T*>& src) noexcept;
  template <class T> void reset_protection(const T* ptr) noexcept;
  void reset_protection(nullptr_t = nullptr) noexcept;
  void swap(hazard_pointer&) noexcept;
};

hazard_pointer make_hazard_pointer();
void swap(hazard_pointer&, hazard_pointer&) noexcept;

これは例えば、次のように使用します

// 管理対象のリソース表現型は、hazard_pointer_obj_baseをCRTPする
struct Name : public hazard_pointer_obj_base<Name> {
  /* details */
};

// 共有ポインタ
std::atomic<Name*> name;

// 頻繁に複数スレッドから呼ばれる
void print_name() {
  // ハザードポインタを取得する
  hazard_pointer h = make_hazard_pointer();
  // nameをハザードポインタへ登録
  Name* ptr = h.protect(name);
  // 以降、*ptrには安全にアクセスできる(勝手に消えたり変更されたりしない)
}

// あんまり呼ばれない
void update_name(Name* new_name) {
  // nameを更新する
  Name* ptr = name.exchange(new_name);
  // 削除待ち登録、全てのスレッドが必要としなくなった時に削除される
  ptr->retire();
}

複数スレッドから頻繁に呼ばれるprint_name()は共有リソースであるnameの読み込みのを行います(nameポインタそのものは変更しない)。一方で、update_name()はたまに呼ばれて共有リソースを更新しnameポインタを変更します。update_name()print_name()が呼ばれるタイミングは不定であり、仮に同時に呼ばれた時でも共有リソース(name)の使用が安全(読み取っている最中に消さない、消したリソースを読み取らない)になるような管理をロックフリーで行うのがハザードポインタという仕組みです。

この例のようなコードはC++26で書けるようになる予定ですが、全景をここにコピペできる程度にはかなり最低限の物しか入っていません。これは、初期の機能セットを大きくし過ぎて議論が長引いてしまうのを回避して、最低限の機能から始めて徐々に拡張していくことを意図したものです。

この提案は、その予定に沿ってハザードポインタの第一弾の拡張を提案するものです。

この提案では、次の機能が拡張候補として挙げられています

  1. Batches of Hazard pointers
    • 目的 : 複数のハザードポインタをまとめて構築・破棄する
    • 根拠 : 複数のハザードポインタを個別に構築・破棄する際のレイテンシは、一括して構築・破棄することで償却できる
    • 利点 : レイテンシの改善
  2. Synchronous reclamation
    • 目的 : 開放しようとするリソースに依存するようなデリータをもつオブジェクトの保護をサポートする
    • 根拠 : オブジェクトのデリータが使用できなくなる外部リソース(ファイルやネットワーク接続など)に依存する場合がある
    • 利点 : そのような場合でもハザードポインタによって安全なreclamationを実現できる
  3. Integrated protection counting
    • 目的 : 可換/非可換カウンタによる保護とハザードポインタを組み合わせる
    • 根拠 : ハザードポインタによる保護とカウンタベースの保護を組み合わせたい場合がある
    • 利点 : そのような統合処理をユーザーが書く必要が無くなる
  4. Dedicated reclamation execution
    • 目的 : reclamation処理の実行ポリシーを指定して、実行される場所のカスタマイズを可能とする
    • 根拠 : 適切なreclamation実行ポリシーは、ハザードポインタを利用するアプリケーションによって異なる
    • 利点 : ユーザーがreclamation実行ポリシーを指定可能になる
  5. Custom domains
    • 目的 : ハザードポインタのドメインの構造やポリシーをカスタマイズし、デフォルトドメインやカスタムドメインと分離する
    • 根拠 : 特定のアプリケーションや使用シナリオに合わせてカスタマイズされたハザードポインタの構成を容易にする
    • 利点 : 特定のユースケースに合わせてハザードポインタの動作を微調整できるようになる

ここではこのすべてを一度に導入することを提案しておらず、このうちから1の"Batches of Hazard pointers"と2の"Synchronous reclamation"の2つだけをC++29に向けて導入することを提案しています。

facebookのfollyというライブラリにおいてこれらの機能は実装されており、それぞれ2017年と2018年ごろから実運用環境で活発に活用されているとのことです。

P3136R0 Retiring niebloids

Rangeアルゴリズム群を単なる関数オブジェクトとなるように規定しなおす提案。

C++20で<range>と共に導入されたRangeアルゴリズムおよびstd::ranges以下の関数は、追加の規定によって単なる関数としては実装できないようになっています。この主な目的はADLを無効化して同名の古い関数が呼ばれないようにすることにあります

void foo() {
  // std::ranges名前空間を取り込む
  using namespace std::ranges;

  std::vector<int> vec{1,2,3};
  distance(begin(vec), end(vec)); // #1 std::range::distanceが呼ばれる
}

std::ranges以下の新しい関数は、イテレータペアを受け取るものについてはイテレータと番兵の型が異なっており、その点について古い関数(イテレータと番兵は同じ型)よりも特殊化されておらず、オーバーロード解決において古いものが優先されてしまいます。そのため、このような追加の規定によってそれが起こらないようにしています。

このような振る舞いを持つ関数の事は、この事の発明者の名前を取ってniebloidsと呼ばれています。

また追加で、std::ranges以下の関数テンプレート(として宣言されているもの)はテンプレート引数リストを明示的に指定して呼び出せる必要は無いという規定があり、これらのことから標準ライブラリ実装はniebloidsを関数テンプレートとして実装しています。ただし規定としては関数テンプレートを指定しているため、そのような実装は規格的には厳密にいえば正しくないことになります。

このような規定を行っている背景には、通常の関数テンプレートにこのような特性を埋め込むことのできる新しいコア言語機能が追加されることを意図してのものだったようです。

しかし、そのような機能はC++23にはなく、C++26でも現れる気配はありません。

その間現実の実装は、niebloidsをsemirgularな関数オブジェクト(CPOのような)として実装するか関数テンプレートをより厳密にエミュレートしてコピー不可なものとして実装するかの相違がありましたが、結局現在(2024年)は全ての実装がniebloidsをsemirgularな関数オブジェクトとして実装しています。

また、関数オブジェクトであるかが曖昧なため、関数を転送するようなコードが妨げられる場合があります

auto x = std::views::transform(std::ranges::size);
auto y = std::views::transform(std::ranges::distance);
auto z = std::views::transform(std::ref(std::ranges::distance));
auto w = std::views::transform([](auto&& r) { return std::ranges::distance(r); });

xは有効ですが、yはそうではない時期がありました。zは事実上有効(少し経路が複雑ですが)で、wは有効なもののかなり冗長です。

また、関数オブジェクトはオーバーロードできませんが、規格では関数テンプレートのオーバーロードとしてrangeを受け取るものとイテレータペアを受け取るものの少なくとも2つを定義しています。これは実際には関数オブジェクトでは実現できません(関数呼び出し演算子オーバーロードにならざるをえない)。

この提案はこれらの問題から、niebloidsの実装を明確に関数テンプレートとして指定しなおすことを提案するものです。

この提案の後でも、std::ranges以下の関数に指定されたADLを無効化する性質が失われるわけではなく、niebloidsの実装が関数オブジェクトとして明確に指定されるようになるだけです。それによって上記の問題が解消され、関数名で転送するような操作も合法的に行えるようになります。

P3137R0 views::to_input

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

rng | views::inputは入力範囲rngのRangeカテゴリをinput_rangeかつ非common_rangeに弱めるもので、それ以外の性質はrngのものを保持します。

これは一見すると無意味にしか思えませんが、別のRangeアダプタが入力の性質をなるべく継承しようとして無茶をすることでRangeアダプタのイテレーションパフォーマンスが損なわれる場合に、あえて性質を弱めた範囲を渡すことでそれを回避するために使用するものです。

例えばviews::joinの場合、views::join(rng)rngcommon_rangeの場合、そのイテレータの比較において内部に保持する2つのイテレータ(外側/内側rangeのイテレータ)を順番に比較します。一方、rngcommon_rangeでなければそのイテレータと番兵の比較においては外側イテレータの比較だけが行われます。これによって、common_rangejoin_viewはそうでないものよりもイテレーションコストが重くなります。

あるいはviews::chunkの場合、rng | views::chunk(n)ではrngforward_rangeであるかによって処理方法が大きく変化します。rngforward_range(以上)である場合、各要素はviews::take(n)となり、すべての要素をイテレーションする場合は各要素の部分を2回イテレートすることになります。一方、input_rangeである場合は内外イテレータの進行時にのみ1つづつ元の範囲上でイテレータが進行し、入力範囲は1度だけイテレートされます。

このように、Rangeアダプタの実装によってはinputあるいは非commonな入力に対してより効率的なイテレーションを行える場合があります。それを明示的に活用するために入力範囲の性質をあえて弱めるのがviews::inputです。

namespace std::ranges {

  // views::inputの実装ビュー型
  template<input_range V>
    requires view<V>
  class to_input_view;

  // borrowed_rangeを受け継ぐ
  template<class V>
  inline constexpr bool enable_borrowed_range<to_input_view<V>> =
      enable_borrowed_range<V>;

  namespace views {
    // std::views::input実体
    inline constexpr unspecified to_input = unspecified;
  }

}

to_input_viewは次のようなごく薄いラッパ型です

template<input_range V>
  requires view<V>
class to_input_view : public view_interface<to_input_view<V>>{
  V base_ = V();                          // exposition only

  template<bool Const>
  class iterator;                         // exposition only

public:
  to_input_view() requires default_initializable<V> = default;
  constexpr explicit to_input_view(V base);

  constexpr V base() const & requires copy_constructible<V> { return base_; }
  constexpr V base() && { return std::move(base_); }

  constexpr auto begin() requires (!simple-view<V>);
  constexpr auto begin() const requires range<const V>;

  constexpr auto end() requires (!simple-view<V>);
  constexpr auto end() const requires range<const V>;

  constexpr auto size() requires sized_range<V>;
  constexpr auto size() const requires sized_range<const V>;
};

.begin()の呼び出しでは独自のイテレータを返し、.end()の呼び出しでは入力範囲の番兵をそのまま返すことで非common_rangeになります。また、返すイテレータinput_iterator相当のインターフェースしか用意しておらず、これらによってrng | views::inputは入力範囲をinput_rangeかつ非common_rangeに弱めています。

SG9の初期レビューでは、inputに弱めることと非commonにすることを分離する方向性で変更することに合意されているようです。

P3138R0 views::cache_last

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

これはRange-v3ではcahce1と呼ばれていたもので、入力範囲の最後の関節参照結果をそのイテレータ内部にキャッシュしておくものです。この用途は、同じ要素に対して関節参照が2回以上行われると効率的ではないような範囲に接続して、関節参照結果をキャッシュすることで効率的ではない関節参照が複数回行われないようにするものです。

そのような場合とは典型的にはviews::transformによる要素の変換が挟まっているような場合であり、それに対してviews::filterを接続すると問題を確認することができます。

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  auto even_squares = v
      | std::views::transform([](int i){
              std::print("transform: {}\n", i);
              return i * i;
          })
      | std::views::filter([](int i){
              std::print("filter: {}\n", i);
              return i % 2 == 0;
          });

  for (int i : even_squares) {
      std::print("Got: {}\n", i);
  }
}

この実行結果は次のようになります

transform: 1
filter: 1
transform: 2
filter: 4
transform: 2
Got: 4
transform: 3
filter: 9
transform: 4
filter: 16
transform: 4
Got: 16
transform: 5
filter: 25

理想的あるいは直感的には、views::transformの変換関数の呼び出しは入力範囲の各要素に対して一回だけ行われるように思えます。しかし、views::filterがフィルタリングを行うのはそのイテレータが進行した時であり、関節参照した時とは別に元の範囲の要素を参照します。そのため、この例ではviews::filterの条件を満たす入力要素に対して(入力範囲の要素を二乗した数が偶数となる場合に)のみ、views::filterの前段の範囲の要素は2回参照され、このためviews::transformの変換関数は2回呼ばれます。

この例の処理はさほどコストのかかるものではありませんが、views::transformの変換処理が重い場合などはこの挙動は直接観測できないこともあって問題となります。この場合に、views::cache_lastをそのような変換を適用した後に挟むことで前段の関節参照結果を保存して、変換が1度だけ行われるようにすることができます。

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  auto even_squares = v
    | views::transform(square)
    | views::cache_last         // 前段transform適用要素をキャッシュし、変換が1度だけ行われるようにする
    | views::filter(is_even);
  
  ...
}

この実行結果は次のようになります

transform: 1
filter: 1
transform: 2
filter: 4
Got: 4
transform: 3
filter: 9
transform: 4
filter: 16
Got: 16
transform: 5
filter: 25

あるいは、views::transformの入力範囲がinput_rangeでその要素も右辺値の場合など、複数回の関節参照が単に非効率なだけではなく危険になるような範囲を安全に取り扱うためにも使用できます。

views::cache_last実装の上で問題となるのは、イテレータoperator*constメンバ関数でなければならないことです。views::cache_lastイテレータはその初回のoperator*呼び出しで前段のイテレータの関節参照結果をキャッシュするため、constメンバ関数として実装するためにはmutableを用いたりポインタを経由するなどしてconst性の伝播を断ち切る必要があります。しかしこれをすると、標準ライブラリ関数に対して要求されている([res.on.data.races]/3constメンバ関数のスレッドセーフ性を達成することができなくなります。

この提案ではこれを回避するために、標準ライブラリのconstメンバ関数のスレッドセーフ性要求を、非forwardinput_iteratorに対して要求しないようにすることを提案しています。input_iteratorイテレータにはマルチパス保証が無いため、仮にコピーして複数スレッドで使用したとしてもあるスレッドが所持するイテレータのインクリメントは他のコピーイテレータを無効化します。そのため、並行的に使用するイテレータは少なくともforward_iteratorでなくてはならず、input_iteratorが並行処理に使用されることは無いはずで、それを例外に追加しても問題ないとしています。

なお、views::cache_lastborrowed_rangeにはならず常にinput_rangeで、common_rangeにもならずconst-iterableでもない範囲となります。

SG9の最初のレビューでは、このアダプタの名前としてcache<1>が最多でcache_latestが次点という投票結果が得られたようです。しかし、cache_lastから名前を変えるかどうかはLEWGに委ねています。

P3140R0 std::int_least128_t

128ビット幅の整数型とそのライブラリサポートを追加する提案。

現在のC++において、ビット幅が保証されている整数型の最大は64bitまでで、それを超える幅の整数型はオプションとされています。正確には64bit幅の整数型std::int64_t/std::uint64_tでさえ厳密にいえば存在は保証されていませんが、主要な実装では64bit幅の整数型までは使用するのに不自由することはありません。しかし、それを超えるビット幅、例えば128bitなどの整数型の存在はそれらよりも更に弱い立場にあり、あまり積極的に使用できるものではありません。

しかし、主要な3実装は現在すでに何かしらの形で128bit整数型を提供しています。ただ、そのライブラリのサポートはあまり充実していません。また、多くのC++ユーザーは64bitを超える幅の整数型の標準化に大きな関心を寄せてきました。

この提案は、標準ライブラリによって良くサポートされた128bit以上の整数型を手に入れるために、標準ライブラリにstd::int128_t/std::uint128_tのようなエイリアスを追加し、その他の標準ライブラリ機能でもこれをサポートするように提案するものです。この提案による変更はライブラリの変更のみであり、コア言語の変更はありません。

この提案では、既存のstd::int64_tなどと同じく、std::int128_t/std::uint128_tが提供されるかどうかはオプションとしています。ただし、std::int_least128_t/std::uint_least128_tが必須とされることでそれらのエイリアスも定義可能であるため、この提案は実質的に128bit整数型を使用可能にします。

// 追加するエイリアスの一覧
namespace std {
  using int_least128_t  = /* signed integer type */;
  using uint_least128_t = /* unsigned integer type */;

  using int_fast128_t   = /* signed integer type */;
  using uint_fast128_t  = /* unsigned integer type */;

  using int128_t  = /* signed integer type */;    // optional
  using uint128_t = /* unsigned integer type */;  // optional
}

この提案では、128bit整数型の多数のユースケースを紹介するとともに、既存の実装やライブラリの実装を調べることでこの変更の影響を詳細に見積もっており、それらの分析から次のような動機をあげています

  1. ユーティリティ : 128bit整数は様々な領域で非常に役に立つ
  2. 均一性 : 標準化することで多くのシーンで同じ名前が使用されるようになり、理想的には同じABIになる
  3. 既存の慣行 : 128bit整数はすでに複数のコンパイラで実装されている
  4. パフォーマンス : ソフトウェアエミュレーションされた128bit整数を組み込み型と同等に最適化することは不可能ではないにしても困難
  5. 影響が小さい : 実装と標準に対する影響は大きくなく、メリットに照らせば妥当なもの

標準ライブラリの変更の必要性は次のようにまとめられています

noとなっているところは変更が必要ないことを意味しており、それ以外のところは何かしらの実装が必要となります。

この提案はコア言語の変更を伴っていませんが、既存のコードに対してわずかに影響があります。

1つは次のようなコードの型が変更される可能性があることです

auto x = 18446744073709551615; // 2^64-1

最も幅の広い符号付き整数型が64bitである場合このコードはill-formed(UB)であり、コンパイラごとに異なる方法でこれを処理します

  • clang : decltype(x)unsigned long longとなり、警告が発せられる
  • gcc : decltype(x)__int128となり、警告が発せられる
  • msvc : decltype(x)unsigned long longとなり、警告なし

この提案の後では、この型はstd::int_least128_tとなるのが正しい振る舞いとなります。

2つめの問題は、オーバーロード解決に影響を与える場合があることです。

テンプレートを使用せずにすべての整数型に対応するために、それら型ごとにオーバーロードを定義する方法があります

void foo(int);
void foo(long);
void foo(long long);

この手法では拡張整数型に対応できていないため、この提案の後ではstd::int128_tのためのオーバーロードを追加する必要があります。

void foo(int);
void foo(long);
void foo(long long);
void foo(std::int128_t);

このとき、std::int128_tlong longでだったとすると、この追加したオーバーロードは多重定義となりエラーになります。そのような実装は実際に存在しないのでこのコードが問題になることはありませんが、同様に追加したstd::int128_tオーバーロードが現在のオーバロード解決結果に影響を与える例をいくつか考えることができます。

3つ目の問題は、整数の幅が128bit以下であることを仮定しているコードが多数存在しており、128bit整数型の追加によってそれらが壊れてしまうことです。とはいえこれは言語の発展を妨げる理由にならず、ユーザーがその使用を選択しなければ実際には壊れないとしています。

最後の問題はライブラリのもので、std::bitsetのコンストラクタの引数型をstd::int_least128_tにしたときに起こるものです

  1. std::bitset<N>(0)が曖昧な呼び出しになる
  2. 負の数を指定した場合、符号拡張はunsgined long lonの幅までしか行われず、それを超えた部分は0で初期化されてしまう

この提案では1に対しては変換ランクがint以上のすべての整数型に対するオーバーロードを追加することで対処し(intが含まれていることで0intが選択され、intで表現できない値で同様の問題が起きないように、intより大きい幅の整数型のすべてについてオーバーロードを用意する)、2の問題に対しては負の値が指定された場合の動作を全ビットセット(1にする)とみなすことで対処しています。

P3142R0 Printing Blank Lines with println

std::printlnを引数無しで呼んで空行を出力できるようにする提案。

C++23で導入されたstd::printlnC++における文字列出力を決定的に改善しました。しかし、単に空行を出力したいだけの場合、std::println("")と書かねばならず2文字とはいえ冗長です。

この提案は、std::println()のように引数無しで呼び出したときに空行を出力するようにしようとするものです。

とはいえ差はたった2文字であり、機能の重要性はかなり低いためそれくらいは許容すべきという反対意見が予想されます。提案では、反対に対していくつかの理由を添えています

  • 関数のデフォルトの動作を引き出すのに意味のない文字列リテラルを渡す必要があるのは直感に反する
  • よく使用される文字列を識別子(変数あるいはマクロ)によって表現することは非常に望ましいコーディング手法の一つであり、特に頻繁に使用される空行に対して標準的なスペルを提供する必要がある
  • コードの出力とソースコードの構造を一致させる目的で空行出力は使用される

この提案は2024年3月に行われた東京会議で全体投票をパスして、C++26に導入されています。

P3143R0 An in-depth walk through of the example in P3090R0

P2300のsender/receiverベース非同期処理ライブラリのサンプルコードを詳細に解説する文書。

この文書では、P3090R0で簡単に紹介されているP2300のサンプルコードを深堀して、より詳細に何が起きているかをステップバイステップかつ図を交えながら詳しく解説しています。

特に、senderアルゴリズムの接続やスケジューラの取得、処理の実行などに際して、内部的にどのような構造になっていて何が起こっているかを図によってわかりやすく説明しています。

P3144R0 Deprecate Delete of Incomplete Class Type

不完全型のポインタに対するdeleteを非推奨にする提案。

不完全なクラス型(定義されていないクラス型)は変数を宣言(コンストラクタ呼び出し)できないものの、そのポインタを宣言して使用することはできます。ただし、その型が不完全なコンテキストでメンバの呼び出し等はできません。

class C;          // OK, 不完全型の宣言
class C *cp = 0;  // OK, 不完全型のポインタ
class C c;        // Error, 不完全型を構築できない

しかし、deleteに渡すことはできます。このとき

class C; // OK, incomplete type

// cpの指すオブジェクトをdeleteする
void delC(C *cp)
{
  delete cp; // ??? 何が起こる?
}

現在のC++標準はこの場合、不完全型Cが完全型になった時にトリビアルデストラクタを持ちかつCで定義されたoperator deleteオーバーロードが存在しない、場合を除いて未定義動作となります。このコーナーケースのような条件を満たす場合、デストラクタがトリビアルであるため呼び出しは効果を持たず、グローバルのoperator deleteによって単にメモリが開放されます。このことは各翻訳単位ごとのコンパイルの段階では検出することはできず、リンクする段階で初めてこの条件を満たしているかどうかがわかります。

明示的にそのようなことをすることはおそらくないでしょうが、ライブラリの関数からあるクラス型のポインタを受け取って、ユーザーコードではそのポインタを定義が必要な形で使用していない状態で、ユーザーコードでdeleteしている場合、そのクラス型の定義がなされているヘッダのインクルードを忘れると、コンパイルエラーは起こらずに静かに不完全型のdeleteをすることになるでしょう。

不完全型のdeleteを検出することはコンパイラにとっては簡単なことですが、上記のようにすべての場合においてそれが未定義動作というわけではなく、リンクの直前にそのほんの僅かなwell-definedなケースを検出するのは簡単ではありません。そのため、この問題に対して警告を発するかどうかはQoIの問題としてコンパイラ実装者に一任されています。

提案では簡単なコードによってコンパイラの動作をチェックしたところ、最新のコンパイラではすべて何かしらの警告がなされたようです。ただし、デストラクタがトリビアルでクラス定義のoperator deleteが無い、合法的な場合についても同様に警告を発したようです。

この提案ではこの解決策についていくつかの候補を検討したうえで次の事を提案しています

  • 不完全型のポインタに対するdeleteを全ての場合において非推奨にする
  • それが行われた場合の動作については、誤った動作(Erroneous Behavior)としてデストラクタ呼び出しをせずにポインタの指すオブジェクトの生存期間を終了する

この解決策によって、非推奨となった後でも現在のWell-definedな場合の振る舞いは維持され、誤った動作を利用することでコンパイル時と実行時の両方で実装がよりよい診断を行うためのフックを提供することができます。

この提案の後でも、不完全型の定義がクラス定義のoperator deleteを持っている場合についてはEBではなくUBとなりますが、それも含めて非推奨かつEBとされていることで、実装はそれを診断することができます。

また、このアプローチによってコンパイル時/実行時の診断が浸透すれば、将来的にこれを削除しill-formedとする道が開けるかもしれません。

P3146R0 Clarifying std::variant converting construction

std::variantの変換コンストラクタ/代入演算子において、コンパイル時定数からの縮小変換を許可する提案。

この提案の目的は次のようなコードがコンパイルエラーにならないようにすることです

using IC = std::integral_constant<int, 42>;

IC ic;
std::variant<float> v = ic; // 現在全ての実装でエラー

これはstd::variantの変換コンストラクタ/代入演算子が縮小変換(この場合はint -> float)を拒否するようになっていることからエラーになっています。そこではおおよそ次のような制約によってそれを検出し、禁止しています

template <typename Ti, typename From>
concept FUN_constraint = requires(From &&from) {
  { std::type_identity_t<Ti[]>{ std::forward<From>(from) } };
};

変換先の型Tiを配列化して、その配列型に対して{}初期化を試みることによって変換元の型(From)の値(from)がTiの配列要素を初期化できるかをチェックしています。{}初期化では縮小変換が拒否されるため、From -> Tiの変換が縮小変換となる場合はこの制約を満たすことができません。

これをfloatに特化させて最初の例のコードを再現すると次のように

template <typename T>
  requires FUN_constraint<float, T>
void FUN(float);

// variant::variant(T &&t)コンストラクタのチェックは次のようなTとtを用意して
//   T = IC &
//   t はICの左辺値参照
// 次のように書いたのと同等
FUN<T>(std::forward<T>(t)); // well-formed?

// もしくは次と同等
FUN<IC &>(ic);              // well-formed?

最初のvariant構築の例がwell-definedであるかどうかは、これらの式がwell-definedであるかどうかによって決まります。そして、P2280R4採択前はこのコードはill-formedとなります。それは、FUN_constraint<float, T>が満たされないためです。

その検出は突き詰めると次のようなコードによって行われています

using IC = std::integral_constant<int, 42>;

IC ic{};

float x[1]{ic}; // 縮小変換が起こる

xの初期化においては、まずicintegral_constant)の変換コンストラクタによってicintに変換され、そのint値がfloat型に変換されています。この2つ目のint -> floatの変換仮定が縮小変換となりエラーが起きています。

ただし、このコードを直接実行するとエラーになりません。なぜなら、数値型同士の縮小変換の場合、定数式で変換が実行されて変換に当たって情報の欠落がないことが確認できれば、縮小変換はリスト初期化においても例外的に許可されるためです。integral_constantの変換コンストラクタはconstexprであり1段目のint値への変換は定数式によって行われ、返還後の値(42)はコンパイル時に取得されます。

ではなぜこれをFUN_constraintの制約にまとめた時にこの変換を弾く様になってしまうのかというと、FUN_constraintrequires式の引数from(の使用、id式)が定数式ではないためです。

template <typename Ti, typename From>
concept FUN_constraint = requires(From &&from) {
                                         ^^^^
  { std::type_identity_t<Ti[]>{ std::forward<From>(from) } };
                                                   ^^^^
};

requires式の引数は実体がなくその値を定数式で使用してしまうと未定義動作が発生し、それは参照でも同様です。そのため、ここでのfromの変換に際しては実際の変換は起こらずその変換結果の型のみがチェックされます。実際の変換が起こらないため縮小変換そのものも定数式で実行されず、そのため定数式における縮小変換の例外が適用されず、integral_constant<int, N>Nが何であったとしても縮小変換として扱われてしまいエラーになります。

ところで、ここでの参照fromの使用は、その参照そのものあるいはその先の値に全く関心がありません。integral_constantの変換コンストラクタもthisを使用せず、型に埋め込まれたNTTP値にのみアクセスします。この問題はrequires式の引数だけでなく広く通常の関数においても起こることであり、P2280R4の採択によってC++23で解決されました。P2280R4では、thisポインタおよび参照の読み取り(参照そのもののコピー)を定数式で許可するものです。

P2280R4の採択によってrequires式の引数の参照もその読み取りのみなら定数式となり、上記FUN_constraint<float, IC&>においてもfromは定数式として扱われることでintegral_constant -> int -> floatの変換が定数式で確認され、値が失われない場合にエラーにならなくなります(P2280R4を実装したGCCで確認できます)。

このような背景を適用して、std::variantの変換コンストラクタ/代入演算子において最初のようなコードが合法であることを明確にしようとするのがこの提案です。

ただし、GCC14においても実は冒頭のコードは依然としてエラーになります。それは、std::variantの制約がコンセプトによって行われておらずstd::declvalを使用したSFINAEによって行われているためです。要点だけを抽出すると、SFINAEでは次のような変換をチェックすることで先ほど見たような縮小変換の検出を行っています

template <typename To>
struct NarrowingDetector { To x[1]; };

NarrowingDetector<To>{ { std::declval<From>() } }

実際にはvoid_tのイディオムと共に使用されていますが、問題はstd::declvalconstexpr関数ではなく(定義がないので当然ですが)P2280R4の恩恵に預かることができないという点にあります。

この提案では、実質的にコンセプトによってこの制約を書き直すことを要求しています。C++17以前にも適用するにはコンパイラの特別扱いが必要ではあるものの可能としていますが、提案をC++17以前に適用するかどうかは明確にしていません。

この提案の懸念はこれによって破壊的な動作変更(以前well-formedだったコードがエラーになる or 危険な変換が許可されてしまう)です。提案ではその分析も行っており、まず提案の主目的であるintegral_constantのような型からの変換を除いて、現在ill-formedなコードはill-formedのまま(縮小変換が許可されるものの変換先が曖昧になるため)になるとしています。

using IC = std::integral_constant<int, 42>;

IC ic;

std::variant<float, double> v = ic;  // now     : ill-formed (縮小変換による拒否)
                                     // proposed: ill-formed (変換先が曖昧)

現在well-formedなコードがエラーになる場合も報告されています

IC ic;
std::variant<long, float> v = ic;  // now     : selects long
                                   // proposed: ill-formed (変換先が曖昧)

が、この場合はユーザーコードのバグが浮き彫りになるため歓迎すべきとしています。このような変換は関数呼び出しで直接書いた場合でも曖昧になる(縮小変換ではない場合のintからfloat/longへの変換順位は同等な)ためです。

void f(long);
void f(float);

f(ic); // ERROR, ambiguous

最も危険なケースは、コンパイルは通り続けるものの提案の変更によってvariantが構築する型が変わってしまう場合です。

From f;
std::variant<A, B> v = f;  // now  : selects A
                           // after: selects B. Is this possible?

問題はこのようなケースが存在するかどうかですが、提案では重厚な推論の末このようなFrom, A, Bの組は存在しないとしています(証明っぽい詳細な推論が提案にはあります)。

また、このケースが存在しないといえる根拠にも関連するのですが、この提案によって許可される変換の型Fromはクラス型でなければならず、組み込みの数値型の場合は決してこの影響を受けません。

constexpr int i = 42;

float f{i};                // OK, no narrowing
std::variant<float> v{i};  // この提案の後でもill-formed

なぜなら、数値型の縮小変換を定数式でチェックするためには、問題の中心にあるrequires式の引数の参照fromFrom&&、この場合はint&&)の指す値を読み取らなければならないためです。P2280はあくまで参照そのものの読み取り(参照先ではない)のみを許可しており、参照先を読み取ろうとするのは依然として許可されていません。このようなことを実現するためにはconstexpr引数が必要となり、それはまた、この提案のユースケースの対象でもあります(integral_constantを一般化したconstexper_tが提案中)。

P3147R0 A Direction for Vector

std::vectorに代わる、隣接可変サイズシーケンスコンテナ開発の方向性を探る提案。

std::vectorはおそらく標準ライブラリのコンテナの中で最も頻繁に使用されるコンテナであり、これを使っておけばほとんどの場合に困ることはないでしょう。しかし、少し異なる要件が必要になる場合、std::vectorそのままではそれを満たすことができなくなり、標準ライブラリに代替手段が提供されていない場合があります。例えば

  • ローカルstd::vector
    • 例えば、inplace_vector
  • small buffer optimizationが有効化されたstd::vector
    • 例えば、clumpやsmall vector
  • std::vectorのキャパシティ増加速度を制御したい
  • push_front()
  • 成長するスタック
  • 隣接・ローカルの特性を制御可能なdeque
    • 両方もしくはどちらか

などがあります。

これらの要求をすべて個別のコンテナによって満たそうとすることは明らかに非現実的です。標準化のコストも多大となりますが、結果として得られるvectorlikeコンテナのセットはユーザーに混乱をもたらすものになるでしょう。また、それらの個別のコンテナ間の相互作用(ムーブや比較)も問題となり、N個のコンテナがあればN*(N-1)/2パターンの相互作用を考慮しなければならなくなります。

さらに、クライアントコードの互換性(インターフェースの一貫性)も重要な目標です。ユーザーは個別要件に相対した場合、コードを破損することなくその要件を満たすコンテナに切り替える(宣言時の型だけを入れ替える)ことを望むはずです。これを満たすことの難しさは、コンテナが追加されるたび指数的に増大します。

この提案は、上記のstd::vectorによく求められる個別のプロパティを切り替えることのできるstd::vectorlikeな単一のコンテナを導入することによってこれらの問題を解決しようとするもので、まずその設計の方向性を提示するものです。

提案するコンテナは基本的にstd::vector同様に、要素は常に連続的(contiguous)に配置され、サイズとキャパシティを持ちどちらかが0である場合もあり、サイズは常にキャパシティ未満となります。

このコンテナの1つ目のテンプレートパラメータはstd::vector同様に要素型を取りますが、2つ目のテンプレートパラメータはシーケンスのプロパティを指定するための特性をまとめた構造体(sequence_traits_t)のNTTPで、3番目のテンプレートパラメータにアロケータ型を取ります。例えば次のようになります

// コンテナ名は未定
template<typename ElementType,
         sequence_traits_t SequenceTraits = {},
         typename Allocator = std::allocator<ElementType>
        >
class prposed_vector {...};

この2つ目のテンプレートパラメータがこの提案の肝となる部分であり、この構造体の値によって上で列挙されていたような各種特性を指定します。提案では次のような構造とされています

struct sequence_traits_t {
  bool dynamic = true;        // ストレージを動的に確保する場合はtrue
  bool variable = true;       // キャパシティが増加可能な場合はtrue(固定サイズの場合はfalse)
  size_t capacity = 0;        // 固定サイズの場合の最大キャパシティ(もしくはSBOサイズ)
  enum {FRONT, MIDDLE, BACK}  // コンテナタイプを示す列挙値、それぞれ vector, deque, stack を意味する
  location = FRONT;           // コンテナタイプの指定
  enum {LINEAR, EXPONENTIAL, VECTOR} // 成長戦略(メモリ増加速度)を示す列挙値
  growth = EXPONENTIAL;       // 成長戦略の指定
  size_t increment = 0;       // 線形な増加の場合の増分
  float factor = 1.5;         // 指数的な増加の場合の増加量(1.0以上)
};

このデフォルト値はstd::vectorのプロパティに一致するものであり、提案するコンテナをカスタマイズしない場合はstd::vectorと同等の振る舞いをします。

dynamicスイッチはメモリが動的に割り当てられるかローカルなのかを指定するもので、ローカル(false)の場合はその領域はコンテナオブジェクト内に含まれています。動的(true)の場合でも、SBOが有効な場合はコンテナオブジェクト内部にある場合があります。

variableスイッチは容量を可変にするか固定にするかを指定するもので、capacityは固定にする場合のシーケンスの最大長を要素単位(バイト長ではない)で指定するものです。

ローカルシーケンスの場合は固定キャパシティが必要で(dynamic == true => 0 < capacity)、動的かつ可変(dynamic == true and variable == true)なシーケンスに固定キャパシティが指定されている場合はcapacityの長さはSBOが有効化される最大長として扱われます。

locationスイッチは確保したメモリブロック内で要素がどのように配置されるかを指定するもので、FRONTの場合は先頭方向に詰めて配置され(std::vectorの動作)、MIDDLEの場合は前後に隙間を持つように中央に浮動する形で配置され(std::dequeの動作)、BACKの場合は末尾方向に詰めて(先に入れた要素が後ろに詰められる形)配置されます(スタック動作)。

MIDDLEでは、最初の要素はメモリブロックの中央に配置され、挿入によって前後どちらかの空きが無くなりもう片方に空きがある場合は全要素は中央に再配置されます。容量が足りない場合、許可されていれば(variable == trueならば)メモリの再確保が行われます。

可変シーケンス(variable == true)の場合、キャパシティを超えて要素が挿入される場合にメモリの再確保が発生します。この時の増加速度はgrowthスイッチによって指定され、LINEARの場合は現在のキャパシティにincrementを加えた量を確保し、EXPONENTIALの場合は現在のキャパシティにfactorをかけた量を確保します。

このコンテナはシーケンスの先頭と最後尾へのアクセス(.front().back())をサポートします。これは、クライアントコードを変更することなくコンテナの内部動作を変更可能にするという目標のためです。

キャパシティ操作の.reserve().shrink_to_fit()もサポートされ、固定容量シーケンスでは何もしない関数となります。SBOシーケンスの場合、要素がローカルにある場合は.shrink_to_fit()は何もしませんが、要素がヒープにありその長さがSBOサイズ以下の場合は要素はローカルに移動されます。

この提案の基礎設計としてはここまでで、制限を超えた場合のシーケンスの動作やエラーハンドルする関数、アロケータサポートなどについては将来的に漸次議論して追加していくとしています。

この提案は、現在提案中のinplace_vector等に影響を与えるであろうものの否定するものではなく、C++26に向けては進行中のvectorlikeコンテナを優先し、この提案のより汎用的なコンテナについては設計と議論を進めてC++29以降に導入することを目指しています。

LEWGIの初期のレビューでは、この方向性についてより時間をかけることに合意が取れなかったようです。

P3148R0 Formatting of chrono Time Values

chronoの値のフォーマットに関する問題を解決する提案。

P2945によってchronoの値のフォーマットに関するいくつかの問題が提起され、そこでは破壊的変更も含む形での修正が提案されました。破壊的変更そのものは受け入れられなかったものの、報告された問題と追加のいくつかの問題について解決の必要性が強く支持されました。

この提案は、それを受けてそれらの問題と追加の関連する問題の解消を図る提案です。

この提案で挙げられている問題は次の4つです

  1. time_point値の小数点以下の制御
  2. 12時間表示の細部
  3. 時分秒の端数
  4. エポックからの経過秒数

このうち、1と4はP2945で詳しく説明されていたのでそちらを参照

残った2と3はこの提案で追加された問題です。

12時間表示の細部

%I %rはどちらも、時間の単位を12時間時計の値(12進法)で出力するもので、%Iは時間のみを、%rは時分秒を出力し、どちらも2桁の数字として出力します。10未満の数字は左側が0で埋められますが、これを制御する方法がありません。

また、%rは時分秒の値の後でスペースで区切ってAM/PMを出力します。これは西洋言語における慣習であり、午前午後の出力方法としてはほかにもいくつか選択肢があります

  • スペースが入る場合と入らない場合がある
  • AM/PMは単にA/Pとする場合がある
  • 大文字ではなく小文字の場合もある
  • AM/PMはフォントによって区別される場合がある

これらすべてにフォーマット指定子だけで対応する必要はありませんが、ユーザーが追加の少しの作業によって他の形式を簡単に出力できるようにするために午前/午後の出力なしで12時間表示の時刻を出力できる方法が必要です。すなわち、24時間表示の%T %R(24時間表示の時分秒、時分の出力)に対応する12時間表示の物が求められています。

時分秒の端数

これは1の問題とも関連しますが、%H %M指定はそれぞれ時・分を2桁の整数値として出力するものですが、それぞれの単位で出力した時の端数を小数点以下の数字として出力することができません。%Sはミリ秒以下を小数として出力できますが、その精度を制御できません。

また、これらの指定では数字が1桁となる場合に数字の左側を0で埋めて2桁にしますが、これは時刻表現以外では望ましい動作ではない場合があります。

さらに、time_pointではなくdurationの値は継続時間を保持することができますが、それを出力する方法がサポートされていません。現在のchronoフォーマットは時分秒がそれぞれ24や60等の法を仮定しています。

この提案ではこれらの問題の解決のために、次のようなフォーマットオプションを新設します

  • 継続時間の出力
    • %i : 24進法ではない、合計の時間を端数と共に出力
    • %f : 60進法ではない、合計の分数を端数と共に出力
    • %s : 60進法ではない、合計の秒数を端数と共に出力
      • time_pointの場合、エポックからの経過秒数の出力
  • 精度
    • {:.n}の形式の精度指定(浮動小数点数型のオプション)を%f %H %i %M %s %Sでも有効化
    • 最下位の桁以下の数字は切り捨て
    • 精度を0(.0)にすると小数点以下は出力されない
  • ゼロ埋め
    • Eを先頭に追加した改良コマンドを%H %I %M %r %R %S %Tに追加
    • E付きオプションは数値の左側を0で埋めない
  • 12時間時計
    • %T %Rに対応する12時間時計での出力として、%J %Kを追加、時間の単位は0埋めされない
      • %J%EI:%M:%Sと同じ出力になる
      • %K%EI:%Mと同じ出力になる
    • これらのオプションでも精度指定を有効化
      • 例えば、.0%J.0%EI:%M:%Sと同じ出力になる

出力の例

ミリ秒単位で13:23:45.678を表現するtime_point値をtp、エポックは今日の0時、h = floor<hours>(tp - floor<days>(tp))として

フォーマット文字列 出力 引数 バージョン
{:%T} 13:23:45.678 tp C++20
{:.0%T} 13:23:45 tp この提案
{:%H:%M:%S} 13:23:45.678 tp この提案
{:.0%H:%M:%S} 13:23:45 tp C++20
{:.1%H:%M:%S} 13:23:45.6 tp この提案
{:.2%H:%M} 13:23.76 tp この提案
{:%I:%M:%S} 01:23:45.678 tp C++20
{:.0%EI:%M:%S} 1:23:45 tp この提案
{:%r} 01:23:45 PM tp C++20
{:%J} 1:23:45 tp この提案
{:%R} 13:23 tp C++20
{:%K} 1:23 tp この提案
{:%K}{} 1:23p tp, is_am(h) ? 'a' : 'p' この提案
{:%s} 48225.678 tp この提案

ミリ秒単位で9245678を表現するduration値をdとして

フォーマット文字列 出力 引数 バージョン
{:%H} 02 d C++20
{:.2%EH} 2.56 d この提案
{:.5%i} 2.56824 d この提案
{:%M} 34 d C++20
{:.2%M} 34.09 d この提案
{:.0%f} 154 d この提案
{:%S} 05.678 d C++20
{:.1%S} 05.6 d この提案
{:.1%ES} 5.6 d この提案
{:%s} 9245.678 d この提案
{:.0%s} 9245 d この提案
{} 9245678ms d C++20
{} 9245678 d.count() C++20

P2945では精度の指定をフォーマットオプション全体ではなく、個別のオプションに対して追加することを提案していました。例えば、.0%H:%M:%S(この提案)ではなく%H:%M:%.0S。この提案では、既存の浮動小数点数に対するオプションと一貫していない、最下位の単位以外で端数を出力するユースケースが想像できない(%.3H:%.2M:%.1Sはどのような出力になるのか?)などの理由からデメリットの方がメリットを上回るとしています。

P3149R0 async_scope -- Creating scopes for non-sequential concurrency

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

P2300ではC++26に向けて構造化された並行処理を記述するためのライブラリ機能を整備する作業が進行しています。しかし、そこには次の3つの場合に対応するための機能が欠けています

  1. 構造化されていない既存の並行処理を(P2300のライブラリを用いて)段階的に構造化する
  2. 並列実行するタスクを見失うことなく、動的に多数の並列タスクを開始する
  3. それが適切な場合には、senderによって形作られた作業を即座に実行(eager execution)する

この提案は、これらの場合に対応するために必要なユーティリティをP2300の拡張として提案するものです。ただし、次のような制約を設けています

  • 並行処理を分離しないのがデフォルト
    • そのように分離された、いわゆる孤立した作業(detached work)は望ましくない。分離された作業がいつ終わるのかを知る方法が無ければ、その作業に関連するリソースを安全に破棄できない
  • この提案にはP2300以外に依存関係は端い

例として、次のようなP2300の機能を使用した非同期並行処理のコードを考えます

namespace ex = std::execution;

struct work_context;
struct work_item;

// 並列化する処理
void do_work(work_context&, work_item*);

// 並列に処理するデータ
std::vector<work_item*> get_work_items();

int main() {
  // スレッドプールの開始
  static_thread_pool my_pool{8};
  // アプリケーションのグローバルコンテキストを作成
  work_context ctx;

  // 並列処理対象のデータを取得
  std::vector<work_item*> items = get_work_items();

  for (auto item : items) {
    // 複数個の並行処理を動的に生成する
    ex::sender auto snd = ex::transfer_just(my_pool.get_scheduler(), item)
                        | ex::then([&](work_item* item) { do_work(ctx, item); });

    // スレッドプールで処理を実行し、以降感知しない
    ex::start_detached(std::move(snd));
  }

  // `ctx` and `my_pool` are destroyed
}

この例では並行処理の並列数はget_work_items()が返すvectorの長さによって動的に決まります。作業を動的に生成するためにはP2300で提案されているstart_detached()を使用するしかありません。並列数がコンパイル時に分かっていればwhen_all()というアルゴリズムが使用できますが、これは動的な数のsenderの入力に対応していません。

start_detached()は入力のsenderに関連付けられた実行コンテキスト(完了scheduler)にその処理を投入しますが、それ以上のハンドルを行いません(fire-and-forgetスタイル)。したがって、開始された並行処理が完了したことを確認したり、その完了を制御したりといったことはできません。

スケジューラとしてはスレッドプールのものが指定されているため、start_detached()は処理の完了までブロックすることなくすぐにリターンし、全てのwork_itemに対して作業開始を完了するとforループを抜けてしまい、そのあとすぐにスレッドプールとアプリケーションのコンテキスト(my_poolctx)が破棄されます。しかしこの時、開始したすべての並行作業が完了しているかどうかは不明です。もし完了していなければ、最悪プログラムがクラッシュする可能性もあります。

P2300ではこの問題に対処するためのこれ以上の機能は提供されていません。この提案では、この対処のためにcounting_scopeという機能を提供します。これを使用すると、先程のコードは次のように書き直せます

namespace ex = std::execution;

struct work_context;
struct work_item;

// 並列化する処理
void do_work(work_context&, work_item*);

// 並列に処理するデータ
std::vector<work_item*> get_work_items();

int main() {
  // スレッドプールの開始
  static_thread_pool my_pool{8};
  // アプリケーションのグローバルコンテキストを作成
  work_context ctx;

  ex::counting_scope scope; // これを保護するリソースの*後*に作成しておく
  
  // 並列処理対象のデータを取得
  std::vector<work_item*> items = get_work_items();
  
  for (auto item : items) {
    // 複数個の並行処理を動的に生成する
    ex::sender auto snd = ex::transfer_just(my_pool.get_scheduler(), item)
                        | ex::then([&](work_item* item) { do_work(ctx, item); });

    // 変更前と同様にsndの作業を開始するが、生成された作業をscopeに関連付けて
    // 作業によって参照されるリソース(my_poolとctx)が破棄される前に待機できるようにする
    ex::spawn(std::move(snd), scope); // NEW!
  }

  // scopeに関連付けられた作業の完了を待機する
  this_thread::sync_wait(scope.join()); // NEW!

  // `ctx`と`my_pool`は参照されなくなった後で破棄される
}

この変更を簡単にまとめると次のようになります

Before After
struct context;
ex::sender auto work(const context&);

int main() {
  context ctx;

  ex::sender auto snd = work(ctx);

  // fire and forget
  ex::start_detached(std::move(snd));

  // `ctx` is destroyed, perhaps before
  // `snd` is done
}
struct context;
ex::sender auto work(const context&);

int main() {
  context ctx;
  ex::counting_scope scope;

  ex::sender auto snd = work(ctx);

  // fire, but don't forget
  ex::spawn(std::move(snd), scope);
  
  // wait for all work nested within scope
  // to finish
  this_thread::sync_wait(scope.join());
  
  // `ctx` is destroyed once nothing
  // references it
}

提案より、HTTPサーバーの接続待ちループの例

namespace ex = std::execution;

task<size_t> listener(int port, io_context& ctx, static_thread_pool& pool) {
  size_t count{0};
  listening_socket listen_sock{port};

  // この場合、保護するリソースは`ctx, pool, listen_sock`
  ex::counting_scope work_scope;

  while (!ctx.is_stopped()) {
    // 新しい接続を受け付ける
    connection conn = co_await async_accept(ctx, listen_sock);
    count++;
    
    // 接続を処理する作業を作成
    conn_data data{std::move(conn), ctx, pool};
    ex::sender auto snd = ex::just(std::move(data))
                        | ex::let_value([](auto& data) {
                            return handle_connection(data);
                          });

    // 作成した作業を`work_scope`のスコープ内で実行
    ex::spawn(work_scope, std::move(snd));
  }

  // 全てのリクエストが処理されるまで待機する
  co_await work_scope.join();

  // ここに来た場合、全てのリクエスト処理は完了している
  co_return count;
}

ここでは割愛しますが、提案にはこの一連のユーティリティの動作や設計についての詳細な説明があります。

ここで提案されているユーティリティ一式は、MetaのFollyライブラリやUnifexライブラリで実装され、実環境で使用されているとのことです。

P3150R0 SG14: Low Latency/Games/Embedded/Financial Trading virtual Meeting Minutes 2023/12/13-2024/2/14

SG14の2023年12月13日~2024年2月14日に行われたオンラインミーティングの議事録。

P3151R0 SG19: Machine Learning virtual Meeting Minutes to 2023/12/14-2024/02/8

SG19の2023年12月14日~2024年2月8日に行われたオンラインミーティングの議事録。

P3153R0 An allocator-aware variant type

Allocator Awarestd::variantの提案。

この提案のモチベーションはほぼ以前のpmr::optionalの提案と同一なのでそちらをご覧ください

この提案では、互換性の問題を引き起こすためstd::variantAllocator Awareに改修するのではなく、アロケータサポートのあるstd::basic_variantを追加することを提案しています。その違いは次のようなものです

  • アロケータ型を受け取るテンプレートパラメータを持つ
  • アロケータ対応であることを通知するために、publicallocator_typeメンバを持つ
  • std::variantの全てのコンストラクタに対応する形でアロケータを受け入れるコンストラクタが用意される
  • polymorpic_allcator固定のstd::pmr::variantが提供される
  • basic_variantに提供されたアロケータは、std::variant内でオブジェクトがアクティブになる際に、互換性のあるアロケータを使用していればそのコンストラクタに提供される
    • basic_variantは最初に受け取ったアロケータを保持しており、アクティブなオブジェクトが切り替わるたびにそのオブジェクトに供給される

提案するstd::basic_variantの宣言などの概観

namespace std {

  template<class Allocator, class... Types>
  class basic_variant {
  public:
    using allocator_type = Allocator;

    ...

  private:
    allocator_type alloc; // exposition only
  };

  ...
  
  namespace pmr {

    template<class... Types>
    using variant = basic_variant<polymorphic_allocator<>, Types...>;
  }
}

pmr::optional同様に、この提案の目的はstd::variantを介した時でもその内部のオブジェクトに適切にアロケータを伝播させられるようにすることにあって、basic_variantがそのストレージを渡されたアロケータを使用して確保するようにしようとするものではありません。

P3154R0 Deprecating signed character types in iostreams

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

C++では型としてchar, signed char, unsigned charを区別しており、charが文字型とされるのに対してsigned char, unsigned charは整数型とされています。

iostreamのストリーム出力(<<)もこれに対応してこの3つを受け取るオーバーロードをそれぞれ備えていますが、この出力には問題があります。

#include <iostream>
#include <format>

int main() {
  // Prints:
  std::cout
      << static_cast<         char>(48) << '\n'  // 0
      << static_cast<  signed char>(48) << '\n'  // 0
      << static_cast<unsigned char>(48) << '\n'  // 0
      << static_cast<       int8_t>(48) << '\n'  // 0
      << static_cast<      uint8_t>(48) << '\n'  // 0
      << static_cast<        short>(48) << '\n'  // 48

      << std::format("{}", static_cast<char>(48)) << '\n'     // 0
      << std::format("{}", static_cast<int8_t>(48)) << '\n'   // 48
      << std::format("{}", static_cast<uint8_t>(48)) << '\n'; // 48
}

iostreamのストリーム出力では、signed char, unsigned charオーバーロードcharのものに出力を委譲する形で規定されているため、その出力はcharのものと同じになります。しかし、charの出力はその値を文字として出力するため、整数値は何らかの文字コード(ほぼAscii)として解釈されて出力され、この例のようになります。

std::formatではこの問題は起こらず、charsigned char, unsigned charは区別して扱われ、signed char, unsigned charは整数型として出力されます(オプションによって文字として出力することも可能です)。

また、これと逆の問題(整数値ではなく文字として入力されてしまう)がストリーム入力(>>)にもあり、これらの文字列/配列版のオーバーロードにも同様の問題があります。

この提案は、これらのオーバーロードの挙動は間違っているため、非推奨にすることを提案するものです。

P3155R0 noexcept policy for SD-9 (The Lakos Rule)

LEWGのnoexceptに関するポリシーとして、Lakos Ruleを採用する提案。

LEWGにおいてライブラリ設計と議論のためのポリシーを策定しそれを常設文書(SD-9)とする動きが活発化しており、この提案はそのポリシーの一貫としてLakos Ruleを提案するものです。

提案されているポリシーは次のようなものです

  • ライブラリのデストラクタは例外を送出するべきではない
    • これには、暗黙的な例外仕様(例外送出しない)を使用する
  • ライブラリ関数が広い契約を持ち、例外を送出しない場合、無条件でnoexceptをマークする
    • 狭い契約を持ち、その契約内で呼び出された場合に例外を送出しない場合、"Throws: Nothing"を指定する
  • ライブラリのswao()またはムーブコンストラクタ/代入演算子が条件付きの広い契約を持つ場合、条件付きのnoexceptをマークする
  • ライブラリ型が基になる型と同じ動作を透過的に提供するラッピングセマンティクスを持つ場合、デフォルトコンストラクタ及びコピーコンストラクタ/代入演算子は、基になる型の対応するものの例外仕様と一致する条件付きのnoexceptをマークする
  • 他の関数では条件付きnoexceptを使用しない
  • Cとの互換性を考慮して設計されたライブラリ関数は、無条件noexceptを使用する場合がある

この提案は特に、P3005R0で提案されているポリシーの検討プロセスに適合した形でポリシーを提案することを意識しています。

P3156R0 empty_checkable_range

範囲が空であること定数時間で調べることができることを表すempty_checkable_rangeコンセプトの提案。

標準のRangeアダプタのview型は、制約付きで.empty()を提供するref_view/owning_viewを除いて.empty()メンバを直接提供しません。これによって、Rangeアダプタを通すと元のrangeの持つ.empty()で空かどうかをチェックできる性質が失われる場合があります。

std::istringstream ints("1 2 3 4 5");
auto s = std::ranges::subrange(std::istream_iterator<int>(ints),
                               std::istream_iterator<int>());

std::println("{}", s.empty());  // ok

// views::transformを通す
auto r = s | std::views::transform([](int i) { return i * i; })

std::println("{}", r.empty());  // ng!?

views::transformは元の範囲の要素数を変更しないため、元の範囲で.empty()が使用可能ならばそれを使用可能にしておかない理由がありません。このことは、views::as_rvlaueviews::enumrateなどその他の要素数を変更しないRangeアダプタについても同じです。

このような設計になっているのはどうやら、Rangeアダプタが.empty()関数をview_interfaceから取得することを見込んでいたためのようです。しかし、現在のview_interface<R>::empty()Rforward_rangeもしくはsized_rangeでなければならず、上記例のようにRinput_rangeの場合.empty()は提供されなくなります。

また、現在のranges::emptyの規定ではその意味や時間計算量についてを何ら規定していません。たとえばr.empty()vector::clear()のような動作をするものでたまたまbool値を返す場合などでもranges::emptyの動作は適格なものになってしまいます。例えばref_view/owning_view.empty()は次のように定義されています

constexpr bool empty() requires requires { ranges::empty(r_); }
{ return ranges::empty(r_); }

constexpr bool empty() const requires requires { ranges::empty(r_); }
{ return ranges::empty(r_); }

しかし、ここではranges::empty()が元の範囲r_に適用できることだけを要求しており、その戻り値の意味や時間計算量、またconst Rが範囲であるかどうかなどを要求していません。

このことは、範囲が空かどうかをチェックすることのセマンティクスをきちんと指定するためには、ranges::size()に対応するsized_rangeのようにranges::empty()に対応するコンセプトが必要であることを示しています。

これらの理由からこの提案では、範囲が空かどうかをチェックできるという性質を表すempty_checkable_rangeコンセプトを追加し、それを用いてRnageアダプタに.empty()を追加することを提案しています。

提案するempty_checkable_rangeコンセプトは次のようなものです

namespace std::ranges {

  template<class T>
  concept empty_checkable_range = range<T> && requires(T& t) { ranges::empty(t); };
}

意味論要件は、tstd::remove_reference_t<T>型の左辺値として

  • ranges::empty(t)の計算量はは償却定数であり、tを変更せず、ranges::distance(ranges::begin(t), ranges::end(t)) == 0と等しくなる
  • iterator_t<T>forward_iteratorのモデルである場合、ranges::empty(t)ranges::begin(t)の評価に関係なくwell-definedである

と指定されています。

次に、これを用いてref_view/owning_view.empty()メンバとview_interfaceoperator boolの制約を変更することを提案しています。

そして、既存のRangeアダプタに対して、それがinput_rangeとなる場合でも.empty()メンバを提供できるように、empty_checkable_rangeコンセプトによって制約された.empty()を直接提供するように変更します。

SG9の議論においては、この提案の解決する範囲がニッチすぎる(.empty()は伝播できるが.size ()を伝播できない型のユースケースが想定できない。ただしranges::subrangeが該当してはいる)ことと、解決によるメリットよりも議論のコストが上回るとしてこの提案は否決されました。

P3157R0 Generative Extensions for Reflection

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

現在P2996でC++26に向けて静的リフレクション機能が提案されています。その提案は初期の最小限のものであり、機能拡張の必要性は上の方のP3096R0等でも指摘されていますが、この提案も同様にP2996の静的リフレクション機能に必要な拡張について提案するものです。

この提案では、リフレクション抜きの現在のC++では困難なメタプログラミングの使用例として、あるクラスをラッピングして別の機能を追加したりするようなクラスの実装を挙げています(例えば、他言語バインディングの作成時やDecorator・Adapter・Null Object等のデザインパターンなど)。これらのクラス型は元のクラス型の持つメンバ関数等のインターフェースをすべて実装しなおかつ適切に転送する必要があり、定義することそのものは難しいことではないものの大量のボイラープレートコードを生成し、保守性を著しく低下させます。

P2996のリフレクションはこの方向性を認識しほのめかしてはいるものの、現時点の機能ではこのようなユースケースをサポートできていません。この提案では、このようなユースケースをはじめとしてリフレクションの結果を用いてC++のクラス型をしっかりと定義する機能こそがC++のリフレクティブなメタプログラミング機能の真髄であり必須の機能であるとして、その主要なコンポーネントとして次のものを挙げています

  • イントロスペクション(完全なラッパクラス作成)のために、全ての関数のシグネチャにアクセスできる必要があり、関数のシグネチャの完全な情報にアクセスするためのプリミティブを定義する必要がある
  • 関数シグネチャの合成が可能である必要がある
  • 関数シグネチャのリフレクションにコードを付加できる機能が必要
    • 例えば、各関数の入力(引数)と出力(戻り値)をロギングする処理を挿入するプロクシクラスを定義する場合など
  • std::meta::define_class()(リフレクション結果からクラス定義を返す関数)はstd::meta::nsdm_description()の結果だけではなく、同様の方法で合成されたメンバ関数定義(のリフレクション)を受け入れる必要がある
    • 例えばstd::meta::memfun_description()とすると、この関数はstd::meta::info(合成されたメンバ関数定義や他のメンバ関数のリフレクション結果など)と新しいメンバ関数の定義となるラムダ式等を指定したmemfun_optionsを受け取り、その結果はdefine_class()に渡される

これに加えてこの提案では、ラムダ式クロージャオブジェクトに対する同様の完全なリフレクションサポートと、あるクラス型Tに対してそのクラスのレイアウトを再現するのに必要なメンバ定義(コンパイラの生成する仮想関数テーブル等を含む)を返すrepresentation_of<T>の追加、もあわせて提案しています。

P3158R0 Headless Template Template Parameters

任意のテンプレートテンプレートパラメータを受けることのできる新しいテンプレートパラメータの提案。

この提案は以前にP1985とP2989で提案されていたユニバーサルテンプレートパラメータの提案から特に、汎用的なテンプレートテンプレートパラメータの部分に限って提案をするものです。

以前の2つの提案はそれぞれいくつかの異なった構文を提案していますが、それらにはいくつか問題点がありそれらを回避するためにこの提案では?を使用することを提案しています。

// P2989R0の例
template <universal template>
constexpr bool is_variable = false;
template <auto a>
constexpr bool is_variable<a> = true;

// ?で書き直した同じ提案
template <?>
constexpr bool is_variable = false;
template <auto a>
constexpr bool is_variable<a> = true;

この構文はtemplate headがないためHeadless Template Template Parametersと呼んでいます。

この提案ではユニバーサルテンプレートパラメータの意味論としてP2989で提案されているものを採用しています。

  • ユニバーサルテンプレートパラメータはあらゆる種類のテンプレートパラメータを受けられる
  • ユニバーサルテンプレートパラメータを使用するテンプレートには、ユニバーサルテンプレートパラメータの種類を調整するための部分特殊化が含まれる場合がある
  • ユニバーサルテンプレートパラメータは、あらゆる種類のテンプレートのテンプレート引数として使用できる
  • ユニバーサルテンプレートパラメータは他のコンテキストで使用できない

またこの提案では、P1985で提案されている追加の機能については提案していません

  • コンセプトテンプレートパラメータ
  • 変数テンプレートパラメータ
  • ユニバーサルエイリアス

この提案では特に、現在のテンプレートテンプレートパラメータのマッチングの特例である、パラメータパックをもつテンプレートテンプレートパラメータに対してパラメータパックを持たないテンプレートがマッチしてしまうという動作を禁止することを提案しています。

template <template <class...> class F, class... Args>
using apply = F<Args...>;

template <class T1>
struct A;
template <class T1, class T2>
struct B;
template <class... Ts>
struct C;

template <class T> struct X { };

X<apply<A, int>> xa;                              // OK
X<apply<B, int, float>> xb;                       // OK
X<apply<C, int, float, int, short, unsigned>> xc; // OK

このapplyFはパラメータパックを受け取ることを表明しているのにもかかわらず、非パラメターパックを持つテンプレート(A, B)もマッチしています。これはFの表明する契約に反する振る舞いであり、P1985ではこの仕様に乗っかったままでさらにユニバーサルテンプレートパラメータを有効化しようとしていました。

この提案では、この場合のFが真にその引数について知らないという構文を用意することでこれに対処しています

// Fは任意のパラメータパックを持つテンプレートにマッチする
template <template <?...> typename F, ?... Args>
using apply = F<Args...>; // easy peasy!

// Fは任意のテンプレートにマッチする
template <template  typename F, ?... Args>
using apply = F<Args...>; // easy peasy!

提案文書より、P1985の例を書き直した例

// is_specialization_of
template <typename T, template <?...> typename Type>
constexpr bool is_specialization_of_v = false;

template <?... Params, template <?...> typename Type>
constexpr bool is_specialization_of_v<Type<Params...>, Type> = true;

template <typename T, template <?...> typename Type>
concept specialization_of = is_specialization_of_v<T, Type>;

// リフレクションによる実装
template<typename Type, template <?...> typename Templ>
constexpr bool is_specialization_of_v = (template_of(^Type) == ^Templ);
template <?> constexpr bool is_typename_v             = false;
template <typename T> constexpr bool is_typename_v<T> = true;

template <?> constexpr bool is_value_v                = false;
template <auto V> constexpr bool is_value_v<V>        = true;

template <?> constexpr bool is_template_v             = false;
template <template <?...> typename A>
constexpr bool is_template_v<A>                       = true;

// The associated type for each trait:
template <? X>
struct is_typename : std::bool_constant<is_typename_v<X>> {};
template <? X>
struct is_value    : std::bool_constant<is_value_v<X>> {};
template <? X>
struct is_template : std::bool_constant<is_template_v<X>> {};
template <?>
struct box; // impossible to define body

template <auto X>
struct box<X> { static constexpr decltype(X) result = X; };

template <typename X>
struct box<X> { using result = X; };

template <template <?...> typename X>
struct box<X> {
  template <?... Args>
  using result = X<Args...>;
};

EWGIの初期レビューでは、この提案を追求していくことに合意が取れなかったようです。

P3160R0 An allocator-aware inplace_vector

提案中のinplace_vectorにアロケータサポートを追加する提案。

P0843で現在議論中のinplace_vectorは、ローカル(スタック領域)の限られた領域だけを用いてヒープの利用を回避するstd::vectorですが、このクラスはメモリを動的に確保しないためアロケータサポート(ポリシークラスによるアロケータ切り替え)をしていません。一方で、inplace_vectorはコンテナなのでその要素としてアロケータをカスタマイズ可能なクラス型を格納することができますが、こちらに関してのアロケータサポート(Allocator aware性)も無いためアロケータを要素型に適切に伝播させることができません。

pmr::monotonic_buffer_resource rsrc;
pmr::polymorphic_allocator<> alloc{ &rsrc };

// inplace_vectorそのものはアロケータを使用しない
using V = inplace_vector<pmr::string, 10>;

// 構築時にアロケータを渡して構築(uses allocator construct)
V v = make_obj_using_allocator<V>(alloc, { "hello", "goodbye" });

// アロケータが伝播していない
assert(v[0].get_allocator() == alloc);  // FAILS

ユーザーが標準ライブラリの型を使用する場合でもアロケータのカスタマイズが可能であり、他のコンテナとその動作を一貫させるために、inplace_vectorをAllocator awareにしようとする提案です。

ここで提案しているのはinplace_vectorをAllocator awareにすることであって、inplace_vectorがアロケータを使用するようにすることではありません。

Allocator awareにしようとする場合、構築時に渡されたアロケータをinplace_vectorは覚えておく必要があります。その受け取り方にはいくつかの方法があり、ここでは4種類の方法を提案しています

  1. inplace_vector<class T, size_t N, class Alloc = std::allocator<T>>
    • アロケータはオブジェクト内に格納され、.get_allocator()によって返される
    • 長所
      • 他のコンテナと一貫性があり、デフォルトの場合オーバーヘッドはない
    • 短所
      • アロケータを受け取らない型が要素型の場合、アロケータを格納する領域が無駄になる
  2. inplace_vector<class T, size_t N>
    • T::allocator_typeが存在する場合、inplace_vector<class T, size_t N>::allocator_typeも存在し、.get_allocator()とアロケータ受け入れコンストラクタが定義される
    • 長所
      • ユーザーにとって最も簡単(アロケータテンプレートパラメータを指定する必要がない)
    • 短所
      • ユーザーがアロケータ伝播させたくない場合にそれを回避することができない
      • 他のコンテナとの非一貫性
  3. inplace_vector<class T, size_t N, class Alloc = see below>
    • see belowの型はT::allocator_typeが存在する場合はT::allocator_typeであり、そうでないならばstd::allocator<std::byte>
    • 1と2の利点を組み合わせたもの。アロケータ領域を削減したい場合、std::allocatorを明示的に指定する
  4. basic_inplace_vector<class T, size_t N, class Alloc>
    • Allocator awareなbasic_inplace_vectorを分離する
    • 長所
      • inplace_vectorの仕様と複雑さが低減される
      • 他の方法と比較してコンパイル時間で有利な可能性がある
    • 短所
      • 2種類のinplace_vectorには互換性がない

現時点ではまだこれらのうちのどれかを選んではおらず、この4つの方法から1つを選択すべき、としています。

LEWGの初期のレビューでは、この提案の方向性には合意が得られなかったようです。

P3300R0 C++ Asynchronous Parallel Algorithms

並列アルゴリズムscheduler対応版と非同期版の統一的な設計についての提案。

C++17で追加された並列アルゴリズム(第一引数に実行ポリシーを取るオーバーロード)は大規模な範囲についてのアルゴリズムの実行を並列化することを許可するものですが、現在のものには次のような制限があります

  • 実行は同期的
    • 並列作業を開始すると、それが完了するまでの間呼び出し元はブロックされる
  • 並列作業がどこで実行されるかを厳密に制御する方法が無い
  • 実行のためのチューニングを行う方法やその他のパラメータを渡す方法がない

C++26では、sender/receiverベースのExecutorライブラリが導入される予定で、これによって非同期処理と実行場所の制御についての統一的なフレームワークが提供されるようになります。このライブラリの中心概念である、senderschedulerを使用することで、現在の並列アルゴリズムが抱えている上記のような問題を解消することができます。

この提案はsenderschedulerを受け取る、あるいはsenderを返す、C++26のExecutorライブラリに対応した並列アルゴリズムを追加する提案です。

この提案で追加しようとしているものは次の2つのタイプの並列アルゴリズムです

  • 同期かつスケジューリング可能な並列アルゴリズム
    • schedulerを利用することで実行場所を制御できる
    • 呼び出し元から見た実行は同期的
  • 非同期アルゴリズム
    • schedulerを利用することで実行場所を制御できる
    • senderを返し、アルゴリズムの実行完了を待機しない

これはどちらも実行ポリシーの代わりに単一のsenderを受け取ります。任意のイテレータ(Range)アルゴリズムalgorithm()と置いてその引数をa, b, c, dとすると、それぞれのアルゴリズムの関係は次のようになります

algorithm(a, b, c, d);          // 通常のアルゴリズム(非並列)
algorithm(policy, a, b, c, d);  // C++17の並列アルゴリズム(同期かつスケジューリング不可能)

T t = algorithm(snd, a, b, c, d); // 同期かつスケジューリング可能な並列アルゴリズム(提案1)
sender_of<T> auto s = async::algorithm(snd, a, b, c, d); // 非同期アルゴリズム (提案2)

追加する2つのタイプのアルゴリズムはどちらも、受け取ったsendersnd)を介したクエリによって実行ポリシーとschedulerを取得して利用します。渡すsenderアルゴリズム実行の前処理を表現するだけでなく、アルゴリズムを実行する場所を表すschedulerを添付することもできる形になります。

例えば、snd = schedule(sch)のようにschedulersch)から直接senderを取得したり、snd | transfer(sch)のように作業グラフに明示的に組み込むことでschedulerは指定します。実行ポリシー(policy)は、snd | attach_execution_policy(policy)のようにして作業グラフに添付したり、snd = schedule(sch, policy)snd | transfer(sch, policy)あるいはon(sch, policy, snd)のようにしてschedulerと一緒に指定することもできます。

追加するアルゴリズムでは、指定されたschedulerと実行ポリシーに互換性が無い場合や実行ポリシーが指定されずschedulerがデフォルト設定を持たない場合、あるいは同期かつスケジューリング可能な並列アルゴリズムschedulerが指定されていない場合にはコンパイルエラーとなります。

同期かつスケジューリング可能な並列アルゴリズムsender_of<void>を受け取ります(すなわち、先行作業から直接的にアルゴリズムに対して値を注入できるわけではありません)。これはsenderの消費者であり、受け取ったsenderに対してsync_wait()を使用して、受け取った先行作業(senderが表現する作業)と 自身が開始したアルゴリズムの実行の両方が終わるまで待機します。

対して、非同期アルゴリズムsenderアダプタであり、任意のsenderを受け取ってsenderを返します。返されるsenderは通常のアルゴリズムを指定されたschedulerによって実行した結果と、入力のsenderの結果の両方を同時に(tupleで)送信します。

sender_of<T> auto s0 = async::algorithm(schedule(sch), a, b, c, d);

sender_of<int, bool> auto s1 = just(17, true);
sender_of<T, int, bool> auto s2 = async::algorithm(s1, a, b, c, d);

また、非同期アルゴリズム|によって他のsenderと接続することができます

sender_of<U> auto s0 = schedule(sch)
                     | async::algorithm_x(a, b, c, d) // Result is type `T`.
                     | then([] (T t) -> void { /* … */ })
                     | async::algorithm_y(b, e, f);   // Result is type `U`.

sender_of<U, V> auto s1 = just(17, true);
                        | async::algorithm_x(a, b, c, d)
                        | then([] (T t, int i, bool b) -> V { /* … */ })
                        | async::algorithm_y(b, e, f);

こうして返されるsenderawaitableでもあり、コルーチンとして利用することもできます。

非同期アルゴリズムは古いイテレータアルゴリズムではなくRangeアルゴリズムをベースとしており、戻り値のセマンティクスが異なるためstd::async名前空間に配置されます。

同期かつスケジューリング可能な並列アルゴリズムイテレータペアを受け取るものとRangeを受け取るものの両方を提供し、またRangeアルゴリズムに対して実行ポリシーを受け取るオーバーロードを追加(C++17並列アルゴリズムのRange版)することも提案しています。

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

auto fgh = just() | for_each(v, f) | for_each(v, g) | for_each(v, h);
// Range view chaining.
auto rolling_max = rng | slide(N) | transform(max_element);

// Asynchronous parallel algorithm chaining.
auto unique_sort = just() | async::sort(v) | async::unique(v);
auto normalize_serial(range auto&& v) {
  auto mx = fold_left(v, ranges::max{});
  return transform(v, views::repeat(mx), begin(v), divides);
}

auto normalize_parallel(range auto&& v) {
  auto mx = fold_left(par, v, ranges::max{});
  return transform(par, v, views::repeat(mx), begin(v), divides);
}

auto normalize_async(range auto&& v) {
  return async::fold_left(v, ranges::max{})
       | let_value([] (auto mx, range auto&& v) {
           return async::transform(
             just(), v, views::repeat(mx), begin(v), divides);
         });
}

auto normalize_coroutine(range auto&& v) {
  auto mx = async::fold_left(just(), v, ranges::max{}) | split;
  co_return async::transform(mx, v, views::repeat(co_await mx), begin(v), divides);
}

おわり

この記事のMarkdownソース

[C++] std::arrayを初期化せずに初期化する

初期化せずに初期化する。一見矛盾しているようにしか思えない行いはしかし、生配列の場合は次のように初期化しながら初期化しないことによって行うことができます

int main() {
  int array_uninit[5];      // 各要素は未初期化
  int array_zeroinit[5]{};  // 各要素は0で初期化
}

この時std::arrayで同様に初期化しながら初期化しないことを行うにはどうすればいいのでしょうか?クラス型の場合、初期化をしない初期化(デフォルト初期化)の場合でもデフォルトコンストラクタが呼ばれてしまうため、なんとなくできないような気がしてしまいます。

先に結論を書いておくと、生配列と全く同様の書き方によって全く同様の初期化を行うことができます。

int main() {
  std::array<int, 5> array_uninit;      // 各要素は未初期化
  std::array<int, 5> array_zeroinit{};  // 各要素は0で初期化
}

デフォルト初期化

int n;のように変数の初期化子を指定せずに変数を宣言した場合、この形式の初期化はデフォルト初期化という初期化方法に分類されます。デフォルト初期化によってその変数(オブジェクト)の生存期間は開始されますが、非クラス型の場合はその値は初期化されず不定となります。

クラス型の変数をデフォルト初期化すると(例えば、std::string str;)そのクラスのデフォルトコンストラクタが呼ばれることによってその値が初期化されます。

配列型の場合は、各要素がデフォルト初期化されます。要素型が非クラス型ならばその値は初期化されず、クラス型の場合はデフォルトコンストラクタが呼ばれます。

デフォルト初期化における値が初期化されないとは、そのオブジェクトが占めるメモリ領域はその初期化時に一切書き込みがなされないということでもあります。変数初期化の直後で特定の値を書き込むことが分かっている場合など、初期化時のゼロ埋めを省くためにあえてデフォルト初期化したい場合が稀に良くあります。

一方で、int n{};のように空の初期化子を指定すると、これは値初期化(もしくは空のリストによる集成体体初期化)と呼ばれる形式の初期化になり、非クラス型の場合はその値はゼロ初期化(0あるいはそれに相当する値によって初期化)されます。クラス型の値初期化はデフォルト初期化と同様にデフォルトコンストラクタを呼びだし、配列型の場合は各要素が値初期化されます。

値初期化の場合はほとんどの場合、その領域はゼロ埋めされています。

int main() {
  // デフォルト初期化
  int n1;             // 未初期化、値は不定
  char carray1[5];    // 未初期化、各要素の値は不定
  std::string str1;   // デフォルトコンストラクタ呼び出し

  // 値初期化
  int n1{};           // 値は0で初期化済
  char carray2[5]{};  // 集成体初期化、各要素は0で初期化済
  std::string str2{}; // デフォルトコンストラクタ呼び出し
}

集成体型のデフォルト初期化

std::arrayはクラス型であるので、デフォルト初期化においてもデフォルトコンストラクタが呼ばれてしまい何かしら初期化されてしまうような気がします。ただし一方でstd::arrayは集成体型でもあり、C++17以降はデフォルトコンストラクタを宣言することはできません。std::arrayでデフォルト初期化を行うにはどうすればいいのか?という問いの答えを知るには、集成体型でデフォルト初期化を行うと何が起こるのかを知る事で近づくことができます。

集成体型は通常一切のコンストラクタをユーザーが宣言することができず、全てのコンストラクタ(デフォルト/コピー/ムーブ)はコンパイラによって暗黙に宣言・定義されています。集成体初期化は特定のコンストラクタを呼び出しているわけではありません。

したがって、集成体型の変数をデフォルト初期化した場合、クラス型の変数をデフォルト初期化したのと同じことが起こります。その場合、暗黙に定義されたデフォルトコンストラクタが呼び出され、暗黙に定義されたデフォルトコンストラクタは、引数なしでコンストラクタ初期化子リストが空で本体も空、なユーザー定義コンストラクタとほぼ同じ振る舞いをします。

コンストラクタ初期化子リストが空の場合、各非静的メンバ変数の初期化は、そのデフォルトメンバ初期化子があればそれによって、無ければデフォルト初期化されます。

したがって、集成体型変数のデフォルト初期化において各メンバ変数は、デフォルトメンバ初期化子が指定されていればそれによって、そうでないならばデフォルト初期化されます。

// 非集成体のクラス型
struct S {
  int n = 0;
  int m;

  S(){}
};

// 集成体型
struct A {
  int n = 0;
  int m;
};

int main() {
  // デフォルト初期化
  // どちらも、メンバnは0で初期化され
  // メンバmは未初期化
  S s;
  A a;

  // これは保証されている
  assert(s.n == 0 and a.n == 0);

  // 未初期化値の読み取りは未定義動作(C++26以降はErroneous Behaviour)
  int n = s.m;
  int m = a.m;
}

std::arrayの場合

std::arrayはとても単純には、次のような集成体型です

template<typename T, std::size_t N>
struct array {
  T m_inner_array[N]; // 内部配列

  ...
};

そのため、std::arrayのデフォルト初期化時の挙動はこの内部配列がどう初期化されるかによって決まります。それはつまり、この内部配列にデフォルトメンバ初期化子があるかないかによって変化します。

template<typename T, std::size_t N>
struct array {
  // どう宣言されている??
  T m_inner_array[N]{}; // デフォルトメンバ初期化子あり
  T m_inner_array[N];   // 初期化子なし

  ...
};

デフォルトメンバ初期化子がある場合({}とします)、std::arrayのデフォルト初期化は内部配列を空のリストによって集成体初期化し、生配列を空のリストによって集成体初期化すると各要素も{}によって初期化され、非クラス型の場合はゼロ初期化されます。

デフォルトメンバ初期化子がない場合、std::arrayのデフォルト初期化は内部配列をデフォルト初期化し、各要素もデフォルト初期化され、非クラス型の場合は初期化されません。

実は規格にはstd::arrayの内部配列がどのように宣言されるべきかについて規定が無いのですが、C++11時点では集成体型の非静的メンバのデフォルトメンバ初期化を行うことができなかったため、少なくともC++11時点のstd::arrayの内部配列は初期化子を持っていません。そして、後からデフォルトメンバ初期化子を追加するとstd::arrayのデフォルト初期化時の挙動が変化してしまうためそれは行われていないとみなすことができ、C++23時点のstd::arrayも同様にその内部配列は初期化子を持っていないはずです。

主要な3実装(gcc/clang/msvc)を調べると、いずれもstd::arrayの内部配列は初期化子を持っていません。

よって、要素型が非クラス型のstd::arrayをデフォルト初期化すると、その各要素は未初期化のまま初期化を完了することができます。もっと言えば、std::arrayの初期化周りの挙動は生配列と同じになります(たぶん)。

int main() {
  // デフォルト初期化、各要素は未初期化
  int raw_array_uninit[5];
  std::array<int, 5> array_uninit;
  
  // 空のリストによる集成体初期化、各要素は0で初期化
  int raw_array_zeroinit[5]{};
  std::array<int, 5> array_zeroinit{};
}

要素型がクラス型の場合でも、デフォルト初期化によって初期化されないメンバを持つクラス型の場合はそのメンバは未初期化とすることができます。

必ず初期化されるstd::array?

現在ではそのような実装はありませんが、C++23時点の標準としては特に禁止してはいないように見えます。通常あえてそのような実装を取る必要はないのですが、デフォルト初期化した時に各要素は未初期化となるため、その値の読み取りはUB(C++26からはEB)となります。

これは挙動としては安全ではないので、例えばコンパイラフラグのデバッグレベルなどによって、std::arrayの内部配列をデフォルトメンバ初期化するようにして、std::arrayがデフォルト初期化された時でもその各要素をゼロ初期化しておく安全に倒した挙動を取るようにする、ということは無意味ではないかもしれません。

未初期化領域の読み取りとEB

std::arrayに限らず、デフォルト初期化によって初期化されなかった領域を初期化する前に読み取ることはC++23までは未定義動作となるので、その読み取りに関しては注意が必要です。

void f(int);

int main() {
  int n;  // 未初期化、これそのものは問題ない

  f(n);   // 初期化前の値の読み取り、これがUB(C++23まで)

  // 一度初期化すれば問題ない
  n = 10; // 未初期化領域への書き込みは当然ok(オブジェクトが生存期間内にあれば)
  f(n);   // ok、初期化済の領域の読み取り
}

C++26からはこの未初期化領域の読み取りによるUBが緩和されてErroneous BehaviourとなりUB(何が起こるかわからない、最適化に悪用される)ではなくなります。このEBとして読み取られる値は実装定義とされますが、おそらくほとんどの場合0が読み取られます。

void f(int);

int main() {
  // 以下、C++26から

  int n;  // 未初期化、引き続き問題ない

  f(n);   // 初期化前の値の読み取り、EBとして実装定義の値(おそらく0)が読み取られる
}

UBあるいはEBとなるのは未初期化領域の読み取りのみであり、この場合のEBは不定値を読み取る代わりに特定の値(おそらく0)を読み取るようにするものですが、その実体は未初期化領域が特定の値によって初期化されるようになることによって実現されるはずです。

int main() {
  // C++26以降、どの初期化においても領域は0で初期化されるようになる

  // デフォルト初期化
  int raw_array_uninit[1];
  std::array<int, 1> array_uninit;

  // 集成体初期化
  int raw_array_zeroinit[1]{};
  std::array<int, 1> array_zeroinit{};
}

この動作は現在でも新しめのGCC/Clangで-ftrivial-auto-var-init=zeroオプションを使用すると先取することができます。なお、-ftrivial-auto-var-init=patternとすると、未初期化値の読み取りを検知することができます。おそらくよく似たオプションによって、C++26以降のEB時の振る舞いも制御できるはずです。

パフォーマンスのために従来未初期化だった領域のゼロ埋めが好ましくないなど、C++26以降でもこれまで通りの未初期化領域が欲しい場合は、[[indeterminate]]属性を指定することで挙動を維持することができます。

void f(int);

int main() {
  // デフォルト初期化、かつ未初期化
  int raw_array_uninit[1] [[indeterminate]];
  std::array<int, 1> array_uninit [[indeterminate]];

  // C++26でもどちらも未定義動作(EBではない)
  f(raw_array_uninit[0]);
  f(array_uninit[0]);
}

EB環境の下ではこのように、意図的に初期化せず不定値を持つ変数(初期化が必要な変数)を明示することを強制します。

コンパイラは不明な属性を無視することが規定されているため、[[indeterminate]]属性そのものは今日から使用し始めることができます。これはC++26でコンパイルするまでは全く効果はありませんが、今すぐC++26に移行しないコードでも未初期化変数を目立たせる目的で使用することができ、C++26における[[indeterminate]]属性の役割の一部を先取りすることができます。そして、そうしておくと将来C++26に移行したときでも[[indeterminate]]が指定された変数は変わらず未初期化のままになり挙動が変更されることがなく、すんなりとC++26に移行することができるでしょう(この部分に関してだけは)。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で22本あります。

もくじ

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

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

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

以前の記事を参照

R11での変更は、Historyセクションに過去の履歴を移動したことなどです。

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

  • 設計と実装の詳細を拡張
  • maybe_viewのモナディック関数をDeducing thisを使用するように変更
  • Constraints, Mandates, Returns, Effectsの調整

などです。

P1709R5 Graph Library

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

以前の記事を参照

このリビジョンでは

  • 深さ/幅優先探索及びトポロジカルソートを行うviewbasic_*バージョンを追加
  • 長い名前を避けるために深さ/幅優先探索についてdfs/bfsを使用するようにview名を変更
  • 定義を簡素化するために、アルゴリズム内のadjacency_listコンセプトをindex_adjacency_listコンセプトに置き換え
  • 最終的な定義を含めて、最短パスアルゴリズムを更新
  • トポロジカルソートアルゴリズムの説明を追加
  • compressed_graphの概要テーブルを追加
  • アルゴリズムの説明を追加及び更新
  • グラフ演算子の章を追加

などです。

P2019R5 Thread attributes

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

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

  • thread_namethread::name_hintへリネーム
  • thread_stack_sizethread::stack_size_hintへリネーム
  • 単一引数のコンストラクタをexplicitにした
  • thread::name_hintをコピー及びムーブ不可能な型に変更し、その引数をコピーする必要がないようにした
    • これによって、スタックを浪費することなく長いスレッド名を渡すことができる
  • jthreadのための完全な文言を追加

このリビジョンではスレッド属性クラスはstd::threadの内部クラスになっています

namespace std {
  
  class thread {
    class id;
    class name_hint;
    class stack_size_hint;
  };

  class jthread {
    using id = thread::id;
    using name_hint = thread::name_hint;
    using stack_size_hint = thread::stack_size_hint;
  };
}

また、name_hintクラスは次のような実装になります

namespace std {

  template<typename T>
  class thread::name_hint {
    explicit constexpr name_hint(std::basic_string_view<T> name) noexcept;
    
    name_hint(name_hint&&) = delete;
    name_hint(const name_hint&) = delete;

  private:
    std::basic_string_view<T> __name; // exposition-only
  };
}

P2527R3 std::variant_alternative_index and std::tuple_element_index

std::variantに対して、型からそのインデックスを取得するための方法を追加する提案。

以前の記事を参照

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

  • constな部分特殊化を削除
  • LEWGへの質問を追加
  • 実装可能性セクションを更新
  • 文言の修正

などです。

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

P2664R6 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

  • 名前付き順列生成関数に関するセクションを個別の提案へ分離
  • gather_fromscatter_toイテレータの代わりに範囲を使用するように変更
  • 動作詳細と実装経験を更新
  • 非メンバ添字演算子に関するセクションを削除

などです。

P2748R3 Disallow Binding a Returned Glvalue to a Temporary

P2748R4 Disallow Binding a Returned Glvalue to a Temporary

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

以前の記事を参照

R3での変更は

  • 参照が式ではなくオブジェクトに束縛されることをハンドルするために文言を調整
  • std::is_convertibleの規定における区別を削除

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

  • std::is_convertibleの規定における区別を復帰
  • LWG issue 3400の議論における不正確さの修正

などです。

現在のstd::is_convertiblereturn文を使用してその変換可能性が規定されているため、この提案による変更の影響を受けて結果が変わってしまいます。std::is_convertiblereturn文で変換できるかどうかではなく、暗黙変換が可能かどうかということを検出しているため、return文に限定された特別扱いであるこの提案の影響を受けることは望ましくありません。

その扱いに関しては紆余曲折あったようですが、現在のリビジョンではstd::is_convertibleに限定された除外規定を設けることでこの問題に対処しています。

P2835R2 Expose std::atomic_ref's object address

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

以前の記事を参照

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

などです。

P2894R2 Constant evaluation of Contracts

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

以前の記事を参照

このリビジョンでは、全体的に文章の改善や関連する提案の更新などが行われているようですが、基本的なところはR1と変化はないようです。

P2900R4 Contracts for C++

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

以前の記事を参照

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

  • 契約注釈の定数評価のルールを追加
    • P2894R2を採択
    • 定数評価できない契約条件式は契約違反、定数式における契約注釈のセマンティクスは実装定義(エラーになるかは選択されたセマンティクス次第)
  • <contracts>をフリースタンディング指定
  • enforceセマンティクスでプログラムの終了時に、実装定義の方法で終了するとしていたのをstd::abort()を呼び出すように変更
  • チェックされた契約条件式の副作用は、評価が正常にリターンしたときにのみ省略できることを明確化
  • contract_violationオブジェクトのメモリがoperator newを通して割り当てられないことを明確化(例外オブジェクトと同様)
  • Design Principlesセクションを追加

などです。

P2932R3 A Principled Approach to Open Design Questions for Contracts

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

以前の記事を参照

このリビジョンでの変更は、基本原則2を明確化したことです。

P2946R1 A flexible solution to the problems of noexcept

noexceptよりも弱い無例外指定である[[throws_nothing]]の提案。

以前の記事を参照

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

  • オプションでブール引数を取るようにした
  • [[throws_nothing]]noexceptの両方の指定を持つ場合の振る舞いについて注記を追加
  • [[throws_nothing]]を付加する場所に式を含めることを検討するセクションを追加

などです。

このリビジョンでは、[[throws_nothing(expr)]]として追加のbool引数を取ることができるようになりました。これはnoexcept(expr)演算子ではない方)と同様に、exprfalseに評価される場合には[[throws_nothing]]属性は効果を持たなくなります(指定されていない場合と同様になる)。

P2957R1 Contracts and coroutines

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

以前の記事を参照

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

  • 事前条件が関数引数のコピーの前後どちらのタイミングにおけるものを参照するかについて、未規定とした
  • コルーチンにおける事後条件指定を提案しない

などです。

P2963R1 Ordering of constraints involving fold expressions

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

以前の記事を参照

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

  • 畳み込み式の包摂関係を調べるためにパックのサイズにアクセスしないようにした
  • 実装経験セクションを拡張して、Compiler Explorerで利用可能なこの提案の完全な実装について追記
  • 例を追加

などです。

P2988R1 std::optional<T&>

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

以前の記事を参照

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

P3044R0 sub-string_view from string

std::stringから直接string_viewを取得する関数を追加する提案。

文字列の非所有参照(ビュー)としてstd:string_viewC++17で標準ライブラリに追加されましたが、std::stringAPIstd::string_viewを受けるものはあっても返すものはありません。現在の環境でstd::stringAPIを設計することを考えると、std::string_viewを返すことが妥当である関数として.substr()が挙げられます。特に、substr() const &は即時コピーを必要とするコンテキストではほとんど間違いなく呼び出されません。

現在の.substr()const左辺値参照オーバーロードは、部分文字列をコピーして返しています。この戻り値型をstd::string_viewに変更するのは互換性の問題から不可能であるため、この提案では新しいメンバ関数を追加することで部分文字列をstd::string_viewで取得可能にすることを提案しています。

提案しているメンバ関数.subview()という名前です。

template<typename charT, typename traits = char_traits<charT>, typename Allocator = allocator<charT>>
struct basic_string {
  ...
  
  // 既存のsubstr()
  constexpr basic_string substr(size_type pos = 0, size_type n = npos) const &;
  constexpr basic_string substr(size_type pos = 0, size_type n = npos) &&;

  // 提案するsubview()
  constexpr basic_string_view<charT, traits> subview(size_type pos = 0, size_type n = npos) const;

  ...

};

同時に、std::string_viewとのAPI一貫性のために、std::string_viewにもこの.subview()を追加することを提案しています。

また、さらにこの関数のcontiguousなコンテナに対する一般化として、.subspan()arrayvectorstring(_view)に対して追加する方向性を示しており、LEWGはその方向性に興味を持っているようです。

P3054R0 2023-12 Library Evolution Poll Outcomes

2023年12月に行われたLEWGの投票の結果を報告する文書

投票にかけられた提案は次のものです

全てC++26に向けてLWGに転送されています。文書では、投票の際に寄せられたコメントが記載されています。

P3079R0 Should ignore and observe exist for constant evaluation of contracts?

定数式で契約述語を評価する場合に、チェックする(enforce)以外のセマンティクスを考慮する必要があるかどうかについて問うスライド。

P2894などで議論されている、定数式における契約条件のチェックに関しては、実行時と同様のセマンティクス(3つのセマンティクスから実装定義)とすることが提案されています。

このスライドは、それに対して、enforce以外のセマンティクスを定数式における契約チェック時に考慮する必要があるのかについて問うものです。

ignoreセマンティクスは契約注釈を全て無視することでチェックにかかるコストをゼロにするもので、observeはチェックはするもののエラーにはしないものです。どちらも、実行時コストの最小化や実行時エラーが起きてもプログラムの実行を継続するなど、実行時においては有用性があります。

ただ、それは定数式には当てはまらないと思われます。

定数式では実行時と異なり、契約違反(=プログラムのバグ)を検出したとしてもそれを無視する合理的理由が見当たりません。実行時であれば、とにかく継続することが重要となるプログラムが想定されますが、定数式(すなわちコンパイル時)に検出されたプログラムのバグがその実行にとって致命的となることはなく、むしろバグを早期発見できているので通常のバグ修正フローに従ってバグを修正すべきです。

契約条件が定数式で実行できない場合は無視するよりもそれを定数式で動作するように変更すべきであり、契約注釈が間違っているのであれば検出して早期に修正するべきです。定数式において契約を無視することはそれによって得られるメリット(コンパイラ実装間の挙動差異を無視するなど)よりもデメリットが勝ると考えられます。

残された問題は、定数式における契約チェックにかかるコスト(コンパイル負荷)に関してですが、これについては契約チェックがどれほどコンパイル時コストに影響するのかについての報告が乏しいため、現時点では(定数式において契約を無視できるようにする)説得力のある理由ではない、としています。

このスライドではまとめとして

  • 定数式における契約のセマンティクスとしてignoreobserveを許可することについては、その正当性を示す必要がある
  • コンパイル時の契約評価のコストはその理由として十分かもしれないが、根拠を示す必要がある

としています。

P3084R0 Slides for LEWG views::maybe 20240109

P1255で提案されている、views::nullable/views::maybeの紹介スライド。

提案の主張等が簡単にまとめられています。

P3086R0 Proxy: A Pointer-Semantics-Based Polymorphism Library

静的な多態的プログラミングのためのユーティリティ、"Proxy"の提案。

これは以前にP0957で提案されていたものをさらに改善したものです。モチベーション等はそちらと共通するので以前の記事を参照

P0957はLEWGにおいて議論に時間をかけるコンセンサスが得られなかったため追及はストップされていました。

P0957R9と比較して、この提案では次のような変更が加えられています

  • 以前にあったdispatchfacadeの定義を支援する機能は削除された
  • proxiable_ptr_constraints構造体はポインタへの制約の抽象化
  • 1つのdispatch定義で複数のオーバーロードを制御可能になった
  • proxy::invoke()const修飾
  • dispatchが1つだけの場合、proxy::operator()を追加
  • basic_facade, facadeコンセプトを追加

IDrawableインターフェースのコードを書き直す例

// Draw()のメタデータ
struct Draw {
  using overload_types = std::tuple<void()>;

  template<class T>
  void operator()(T& self)
    requires(requires{ self.Draw(); })
  {
    self.Draw();
  }
};

// Drawableのインターフェース定義
struct FDrawable {
  using dispatch_types = Draw;

  static constexpr auto constraints = std::relocatable_ptr_constraints;
  
  using reflection_type = void;
};

// proxyへの登録
PRO_DEF_MEMBER_DISPATCH(Draw, void());
PRO_DEF_FACADE(FDrawable, Draw);

class Rectangle {
 public:
  void Draw() const;

  void SetWidth(double width);
  void SetHeight(double height);
  void SetTransparency(double);
  double Area() const;
};

class Circle {
 public:
  void Draw() const;

  void SetRadius(double radius);
  void SetTransparency(double transparency);
  double Area() const;
};

class Point {
 public:
  void Draw() const;

  constexpr double Area() const { return 0; }
};

void DoSomethingWithDrawable(std::proxy<FDrawable> p) {
  p.invoke<Draw>();

  // FDrawableに1つしかディスパッチ定義がないなら
  p.invke();
  // もしくは
  p();
}

Area()もインターフェースに追加したくなった場合は

// Draw()のメタデータ
struct Draw {
  using overload_types = std::tuple<void()>;

  template<class T>
  void operator()(T& self)
    requires(requires{ self.Draw(); })
  {
    self.Draw();
  }
};

// Area()のメタデータ
struct Area {
  using overload_types = std::tuple<double()>;

  template<class T>
  double operator()(T& self)
    requires(requires{ {self.Area()} -> std::same_as<double>; })
  {
    return self.Area();
  }
};

// Drawableのインターフェース定義
struct FDrawable {
  using dispatch_types = std::tuple<Draw, Area>;

  static constexpr auto constraints = std::relocatable_ptr_constraints;
  
  using reflection_type = void;
};

// proxyへの登録
PRO_DEF_MEMBER_DISPATCH(Draw, void());
PRO_DEF_MEMBER_DISPATCH(Area, double());
PRO_DEF_FACADE(FDrawable, PRO_MAKE_DISPATCH_PACK(Draw, Area));

...

void DoSomethingWithDrawable(std::proxy<FDrawable> p) {
  // .Draw()呼び出し
  p.invoke<Draw>();

  // .Area()呼び出し
  p.invoke<Area>();
}

P3087R0 Make direct-initialization for enumeration types at least as permissive as direct-list-initialization

スコープ付き列挙型の値の初期化時に、直接初期化を許可する提案。

スコープ付き列挙型の値の初期化宣言においては、直接リスト初期化は許可されている一方で直接初期化は許可されていません。

enum class E {};

E a{0}; // ok、直接リスト初期化
E b(0); // ng、直接初期化

直接リスト初期化は縮小変換を行わないない直接初期化として、直接初期化のより制限的な形式として認識されています。しかし、スコープ付き列挙型の初期化時にはその直感が成立していません。ここで直接初期化を許可することでその直感が復帰し、言語の一貫性が増します。

また、()による集成体初期化の許可と同様に、コンテナに完全転送する際の使いやすさを向上させることができます。

std::vector<std::byte> bytes;

bytes.emplace_back(0xff);   // 現在ng、この提案後はok

おわり

この記事のMarkdownソース

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

文書の一覧

全部で125本あります。

もくじ

N4966 St. Louis Meeting Invitation and Information

2024年6月24〜29日(米国時間)にかけてアメリカのセントルイスで行われるWG21全体会議の案内。

N4967 WG21 2023-10 Admin telecon minutes

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

前回(6月)のミーティング以降の各SGの進捗や作業の報告や、11月のKona会議におけるミーティングの予定などが報告されています。

N4970 WG21 2023-11 Kona Minutes of Meeting

2023年11月にハワイのKonaで行われたWG21全体会議の議事録

N4971 Working Draft, Programming Languages -- C++

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

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

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

P0447R24 Introduction of std::hive to the standard library

P0447R25 Introduction of std::hive to the standard library

P0447R26 Introduction of std::hive to the standard library

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

以前の記事を参照

R24での変更は

  • 代替実装詳細のappendixにある、オーバーアライメントなしで小さい型をサポートする方法についての見直し
  • shrink_to_fitの文言を、std::vectorのものに近づける様に変更

R25での変更は

  • spliceが終端イテレータを無効化することを明確化
  • block_capacity_limits()constexprを付加
  • reshape(), shrink_to_fit()で要素の並べ替えが発生する可能性があるタイミングを明確化するとともに、削減
  • sort()list::sort()と調和する様に変更
  • 標準の言葉の表現と一貫するように、"shall be"を"is"に、"into hive"を"into *this"に修正
  • reshape()がキャパシティを変更する可能性があることを追記
  • 他の部分でカバーされているため、sort()の例外に関する記述を削除

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

  • is_active()を削除
  • get_iterator()に事前条件が追加され、効果の指定を削除
  • googleグループのリンク更新
  • 代替実装ガイドラインを代替実装のappendixへ移動
  • よくある質問と、委員会からの一部の質問への回答をappendixにまとめた
  • 最初のリファレンス実装と現在の実装に関するメモとベンチマークをappendixにまとめた
  • 委員会への質問セクションを削除し、簡単な定義セクションに置き換え
  • 負の値を処理できるdistance()への参照を削除
  • size()を持たないことについての情報を、Design DecisionsからFAQへ移動
  • 時間計算量のappendixを削除し、個々の関数のDesign Decisionsセクションへ移動
  • 正確さの向上のため、オーバーアライメントをストレージを人工的に広げる、に変更
  • 代替実装appendixのビットフィールド+ジャンプカウントをより小さな型をカバーできる様に変更
  • 非常に小さな型をサポートするための3つのより良いアプローチについて代替実装appendixに追記
  • FAQにslot mapとの比較に関するセクションを追加

などです。

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

P0609R2 Attributes for Structured Bindings

構造化束縛の個々の名前に属性を指定できるようにする提案。

構造化束縛宣言には属性を指定することができますが、その属性が作用するのは構造化束縛宣言の裏に隠れている分解対象のオブジェクトであり、導入されている個々の名前ではありません。

auto f() -> std::tuple<int, double, char>;

int main() {
  // この属性指定はf()の戻り値(見えていない)に対するもの
  [[maybe_unused]]
  auto [a, b, c] = f();
}

構造化束縛対象そのものに対してアライメントを指定するなど、この指定にはユースケースがあります。

しかし一方で、構造化束縛宣言の個々の名前に対して属性を指定する方法はありません。標準属性で使用可能かつ意味があるのは[[maybe_unused]]のみですが、コンパイラベンダなどが提供する多くのアノテーション属性などを考慮すると、それを行いたい動機付けは大きくなる可能性があります。

この提案は、構造化束縛宣言内のそれぞれの名前に対して直接属性指定を行えるように文法を拡張しようとするものです。

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

auto f() -> std::tuple<int, double, char>;

int main() {
  // cにのみ[[maybe_unused]]を指定
  auto [a, b, [[maybe_unused]] c] = f();
}

構造化束縛宣言の[]の中で名前に対して直接属性を指定できるようにしています。最初の名前に指定する場合[[[のように角括弧が連続する可能性はありますが、構文は他の場所での属性指定構文と一貫しています。

P0952R2 A new specification for std::generate_canonical

std::generate_canonicalの仕様を改善する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言に、アルゴリズム指定の式中のRについての注記を追加したことです。

この提案は2023年11月のKona会議で採択され、C++26ドラフトに取り込まれています。

P1028R6 SG14 status_code and standard error object

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

以前の記事を参照

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

  • errc::successerrc::invalidに変更
  • status_code_domainstring_viewから構築するconstexprコンストラクタを追加
  • status_code_domaindo_*()protectedメンバ関数_を削除
  • status_code_domain<=>を追加
  • status_code_domainトリビアルコピー可能ではなくなった
  • 型消去されたステータスコードに対しては、通常のコピーコンストラクタではなくstatus_code(in_place_t, const status_code<void> & v)を使用する様に変更

などです。

P1061R6 Structured Bindings can introduce a Pack

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の変更とより複雑な例の追加などです。

この提案は、現在CWGのレビュー受けています。

P1068R10 Vector API for random number generation

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

以前の記事を参照

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

  • ADLの代わりに非静的メンバ関数を探索するようにCPOの動作を変更
  • std::ranges::generate_randomが一時オブジェクトなエンジンと分布生成器をサポートする様に変更(フォーワーディングリファレンスの使用による)

などです。

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

P1673R13 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更は多岐に渡りますが、LWGのレビューを受けての文言の細かい調整がメインです。

この提案は、2023年11月のKona会議で全体投票を通過し、C++26ドラフトに取り込まれています。

P1708R8 Basic Statistics

標準ライブラリにいくつかの統計関数を追加する提案。

以前の記事を参照

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

  • コンストラクタは基本的にexplicitではなくした
  • accumulator object.value()メンバ関数を単純化
  • 名前に使用されているstatsstatisticsに変更

などです。

P1709R4 Graph Library

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

以前の記事を参照

このリビジョンでは、4年分の経験や検討を反映した大規模な再設計が行われています。その変更は

  • 考慮すべきアルゴリズムの再確認
  • 外向きエッジを持つ隣接リスト、エッジリスト、remove/mutableインターフェースに焦点を絞って、提案の範囲を縮小
  • directed/undirectedコンセプトをグラフ型に対する順序なしエッジのオーバーロード可能な型、に置き換え
  • グラフコンテナ型と関数を単純化
    • 特に、const/非constの変種を1つの定義に統合し、必要に応じて両方の場合を扱える様にした
  • 全てのグラフコンテナインターフェースはカスタマイズポイントとなった
  • NWGraphライブラリの設計からインスピレーションを得たviewを導入し、グラフをトラバースするためのよりシンプルでクリーンなインターフェースを実現し、コンテナインターフェースの設計を簡素化
  • 二部グラフと多部グラフのサポート追加
  • 2つのコンテナ実装を、高性能グラフ処理でよく使用されているデータ構造であるCompressed Sparse Rowに基づく圧縮グラフに置き換え

などです。

このリビジョンでは特に、Boost.Graphの経験を踏まえてC++20で作成されたNWGraphライブラリにおける経験を取り込んでいます。NWGraphでは、範囲の範囲としてのグラフを定義し、その抽象の下でいくつかのグラフアルゴリズムを実装しています。このリビジョンでは、その設計の利点やアルゴリズム実装を取り込むことで以前のAPIを進化させています。

一方、NWGraphには既に使用されている任意のグラフデータ構造を使用可能にするためのAPIが欠けており、この提案では以前のリビジョンからのものを発展させてその部分を補っています。

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

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

以前の記事を参照

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

  • reduce_min_index/reduce_max_indexの戻り値としてstd::optionalを返すAPIの検討
  • CV修飾されていない算術型、をより明確な型のリストに置き換え
  • その他文章と文言の修正

などです。

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

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

以前の記事を参照

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

  • embed-element-widthを削除し、適切なCHAR_BITに置き換え
  • マクロ展開の方法を変更することでマクロ展開の問題の解消を図る

などです。

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

P2022R3 Rangified version of lexicographical_compare_three_way

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

以前の記事を参照

このリビジョンでの変更は、same_as_any_ofコンセプトを<concepts>に移動したこと、インターフェースを再考したことなどです。

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

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

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

R6での変更は、条件式のboolへの変換に際して、スコープ付き列挙型の値の変換を抑制する巧妙なトリックの採用によって<type_traits>への依存等の懸念を解消したことです。

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

R5では、assertマクロのオペランドを明示的bool変換することで渡された条件式の評価結果をbool値として取得していました。ただし、そうしてしまうとスコープ付き列挙型の値が渡された場合にもbool値に変換できてしまうためこれを防止するためのトリックが必要だったのですが、そのトリックのためには<type_traits>への依存関係や追加のラップ関数等が必要となり、それが懸念されていました。

この問題に対して、スコープ付き列挙値の変換を防ぎつつ他のものはboolに変換する次のようなトリックがフィードバックとして寄せられました

#define assert(...) ((__VA_ARGS__) ? (void)sizeof(bool(__VA_ARGS__)) : (void)__assert_fail(#__VA_ARGS__, __FILE__, __LINE__))

条件演算子の第一オペランドで、文脈的bool変換によって渡された条件式の結果をbool値に変換しています。文脈的bool変換はifオペランドで行われるのと等価の変換で、暗黙変換であるためスコープ付き列挙値をbool値に変換することができません。

R6ではこのトリックの採用を前提として、明示的bool変換の代わりに文脈的bool変換によって渡された条件式の結果を取得するように文言を修正しています。

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

P2267R1 Library Evolution Policies

C++標準ライブラリ設計のためのポリシーについて検討する提案。

以前の記事を参照

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

  • 以前に行った作業のリストに提案を追加
  • LEWGからのフィードバック(ポリシーを採用することで一貫性が向上し時間が節約される根拠)を追加

などです。

P2308R1 Template parameter initialization

非型テンプレートパラメータの初期化に関しての規定を充実させる提案。

以前の記事を参照

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

  • NTTPの模範となる値の宣言にconstexprを付加
  • テンプレートパラメータオブジェクトの制約と選択を明確化し、模範(exemplar)という用語を削除

などです。

このリビジョンでは、以前に使用されていた模範(exemplar)という用語は削除され、代わりに初期化子候補(candidate initializer)という用語が導入されています(その意味するところは若干異なっていますが)。

P2414R2 Pointer lifetime-end zap proposed solutions

Pointer lifetime-end zapと呼ばれる問題の解決策の提案。

以前の記事を参照

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

  • 在野のLIFOプッシュアルゴリズムの調査に基づく更新
    • LIFOプッシュライブラリが次のスタックノードへのポインタに直接アクセスできないという事実について
  • 選択されていないオプションを削除して、特定のソリューションに焦点を当てる
  • ソースコード内の特定の明確にマークされたポインタのみが、実装がポインタの無効性を再検討できる様にするアプローチのみに焦点を当てている

などです。

P2447R6 std::span over an initializer list

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

以前の記事を参照

このリビジョンでの変更は、コンストラクタからnoexceptを削除したことです。

この提案は既に、2023年11月のKona会議で全体投票を通過し、C++26WDに取り込まれています。

P2481R2 Forwarding reference to specific type/template

テンプレートパラメータ指定時にconstと値カテゴリを推論可能にする構文の必要性について説明した文書。

このリビジョンでの変更は、EWGにおける投票結果を追記したことと、提案する対象を絞ったことなどです。

EWGにおける投票では、この問題の解決の手段として新しい転送参照(forwarding reference)を求める意見が明確化された様です。

これを受けて、この提案は新しい転送参照として提案する対象を2つに絞っています。

1つは以前から提示されていたT auto&&形式

void f(std::string auto&& a);

template <typename... Ts>
void g(std::tuple<Ts...> auto&& b);

template <typename T>
void h(T auto&& c);

template <typename T>
void i(T auto& d);

もう一つはこの提案で提示されたforward Tの形式

void f(forward std::string a);

template <typename... Ts>
void g(forward std::tuple<Ts...> b);

template <typename T>
void h(forward T c);

template <typename T>
void i(forward T& d);

forward Tの形式T auto&&とほぼ同じように使用でき同じ利点がありますが、concept autoと構文が異なることで異なる動作をすることが明確になる点が改善されています。ただし、他の欠点(decltype()を使用しないと型を取得できない)はそのままであり、またforwardというキーワードが一般的すぎる点などが欠点として追加されます。

P2542R7 views::concat

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

以前の記事を参照

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

  • !common_range && random_access_range && sized_rangeのような範囲をサポートしないことを文言に適用
  • const変換コンストラクタを修正
  • 文言の修正

などです。

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

P2573R1 = delete("should have a reason");

関数のdelete指定にメッセージを付加できるようにする提案。

以前の記事を参照

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

  • C++26をターゲットとした
  • ベースとなるドラフトの更新と、関連する提案の追記
  • 以前の同様の提案であるN4186の投票結果を追記

などです。

P2642R5 Padded mdspan layouts

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

以前の記事を参照

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

  • 機能テストマクロを削除して、__cpp_lib_submdspanバンプアップするように変更
  • P2630R3(submdspan)の内容を適用
  • 実装経験などを更新

などです。

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

P2662R3 Pack Indexing

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

以前の記事を参照

このリビジョンでの変更はCWGレビューを受けての文言の改善です。

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

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

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

以前の記事を参照

このリビジョンでの変更は、std::complex浮動小数点数型の特殊化のみが許可されることを明確にしたこと、real/imagセッターのフリー関数を考慮するオプションを削除しメンバ関数のみを考慮するようにしたことです。

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

P2664R5 Proposal to extend std::simd with permutation API

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

以前の記事を参照

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

  • gatherscatterをそれぞれgather_fromscatter_toに変更
  • gather_fromscatter_toにマスキングオーバーロードを追加
  • メモリの動作を制御するために、gather_fromscatter_toにフラグを追加
  • gather_fromscatter_toのメリットについて追記
  • simd_splitsimd_catに新しい名前を使用するよう切り替え

などです。

P2717R4 Tool Introspection

P2717R5 Tool Introspection

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

以前の記事を参照

R4での変更は、ツールにとっての後方互換性の意味についての説明を追加したことです。

このリビジョンでの変更は、機能名の区切り文字(コマンドオプションではなく、返すJSONのキー名)をドッド(.)に変更し、文言をEcosystem ISドラフトにマージしたことなどです。

P2747R1 constexpr placement new

定数式において、placement newの使用を許可する提案。

以前の記事を参照

このリビジョンでは、以前に提案していた3つのことのうち1つ(placement newの定数式での許可)にのみ提案を絞ったことです。

以前のこの提案では

  1. void*からの適切なポインタキャストを定数式で許可する
  2. 定数式でplacement newを許可する
  3. 未初期化オブジェクトの配列の取り扱いの改善

の3つを提案していました。1はP2738R1の採択によって解決され、3は本質的に別の問題であるため分離され他のところで対処されようとしています(P3074R0など)。

そのため、この提案は2の解決にのみ対象を絞っています。

定数式でのplacement newを行う関数としては、std::construct_atが既に存在しています。しかし、この関数による初期化方法は非常に限定されています。

初期化方法 placement new construct_at()
値初期化 new (p) T(args...) std::construct_at(p, args...)
デフォルト初期化 new (p) T できない
リスト初期化 new (p) T{a, b} できない
指示付き初期化 new (p) T{ .a=a, .b=b } できない

また、ライブラリ関数であるためにコピー省略を妨げる問題もあります。

auto get_object() -> T;

void construct_into(T* p) {
  // get_object()の結果をムーブして構築
  std::construct_at(p, get_object());

  // get_object()の結果から直接構築
  :::new (p) T(get_object());
}

placement newが定数式で禁止されていたのはポインタをvoid*で受け取るためほぼなんでもできてしまうためで、それを回避するためにstd::construct_atという限定された機能を持つ関数が導入されていました。

しかし、定数式ではポインタの正しい型を追跡することができ、placement newができることを制限することができます。この能力を使用することでvoid*からのキャスト(static_cast<T*>(static_cast<void*>(p)))も許可されており、定数式でならplacement newを安全に使用することができます。

P2758R1 Emitting messages at compile time

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

以前の記事を参照

このリビジョンでの変更は、P2741R3と重複する内容を削除したことです。

以前のリビジョンでは、static_assertの第二引数に文字列範囲を渡せるようにすることとコンパイル時メッセージ出力のためのライブラリ関数の2つの事を提案していました。前者はP2741がC++26に採択されたことで不要になったため、このリビジョンでは後者のライブラリ機能だけに的を絞っています。

P2760R1 A Plan for C++26 Ranges

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

以前の記事を参照

このリビジョンでの変更は、output_iteratorの改善と並行アルゴリズムサポートについてを優先度1に追加した事です。

output_iteratorの改善とは、T*のように入力イテレータでもある出力イテレータに対して、back_insert_iteratorのように出力だけしかできない出力イテレータを区別することです。それを行うことで、出力イテレータの定義を簡略化するとともに出力動作を効率化する機会を提供できるためです。

出力イテレータは典型的には次のように使用されます

template <typename InputIt, typename OutputIt>
void copy(InputIt first, InputIt last, OutputIt out) {
  for (; first != last; ++first) {
    *out++ = *first;
  }
}

outが出力イテレータですが、これは入力イテレータよりも明らかに必要な操作が少ないことが分かります。これを反映して、back_insert_iteratorを例えば次のように実装できるかもしれません。

template <typename C>
class back_inserter {
  C* cont_;

public:
  explicit back_inserter(C& c) : cont_(&c) { }

  // these do nothing
  auto operator*() -> back_inserter& { return *this; }
  auto operator++() -> back_inserter& { return *this; }
  auto operator++(int) -> back_inserter { return *this; }

  // this one does something
  auto operator=(typename C::value_type const& val) -> back_inserter& {
      cont_->push_back(val);
      return *this;
  }

  // same
  auto operator=(typename C::value_type&& val) -> back_inserter& {
    ...
  }
};

この実装は有効ではありますが、冗長な関数をいくつも記述しなければならないなど面倒な部分が多数あります(本来=のみでいいはず)。

そして何より、出力イテレータの出力が要素を一個づつ出力していくことしか考慮していないことが、出力パフォーマンスを低下させています。入力のサイズが分かっている場合、reserve()したりC++23の範囲挿入関数を使用するなどしてより出力操作を効率化できる可能性があります。しかし、現在の出力イテレータの定義はその機会を提供していません。

出力専用のイテレータを定義することで、出力イテレータの実装簡易化と効率的な出力経路の提供が同時に達成できます。

P2761R0 Slides: If structured binding (P0963R1 presentation)

P0963R1の紹介スライド。

P0963R1については以前の記事を参照

EWGIのメンバに向けて、P0963R1で提案されている機能のモチベーションや動作について説明するものです。

P2767R2 flat_map/flat_set omnibus

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

以前の記事を参照

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

  • LWGを対象とするものとLEWGを対象とするものに分割し、並べ替えた
  • 1つの提案で一度に解決されるサブセクションを統合
  • 多くの例を前後比較するテーブルに置き換えた
  • コンテナのゼロ初期化についてを追加
    • 内部コンテナを値初期化していることで一部のコンテナでは非効率になる可能性がある

などです。

P2795R4 Erroneous behaviour for uninitialized reads

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

以前の記事を参照

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

  • 再び、対象をすべての自動変数(一時オブジェクトを含む)に戻した
  • std::bit_castに誤った値(EBとして読み取られた値)を処理するための文言を追加
  • オブジェクト表現がその型に対して有効ではない場合、誤った動作の後で未定義動作が発生する可能性がある事を明確化
  • 関数パラメータに対するオプトアウト属性指定は、関数の最初の宣言に指定する必要があることを明確化
  • 誤った動作が発生する状況を定義するために使用される標準のフレーズを確立するために文言を更新

などです。

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

P2806R2 do expressions

値を返せるスコープを導入するdo式の提案。

以前の記事を参照

このリビジョンでの変更は、do式からのreturndo return ...からdo_return ...へ変更した(曖昧さを解消するため)ことと生存期間に関するセクションを追加したことなどです。

P2810R2 is_debugger_present is_replaceable

P2810R3 is_debugger_present is_replaceable

P2546で提案されているis_debugger_present()をユーザーが置換可能にする提案。

以前の記事を参照

R2での変更は、2023年9月のLEWGにおける投票結果を追記し、それを反映してユーザー置換関数に事前条件を付加しないように修正した事です。

このリビジョンでの変更は、提案する文言から問題のあった注記を削除した事です。

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

P2819R2 Add tuple protocol to complex

std::complextupleインターフェースを追加する提案。

以前の記事を参照

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

  • 専用の機能テストマクロを削除
  • Hidden friendなget()はtuple-likeでは動作しなかったため、フリー関数に戻した
  • Annex Cの文言を削除

などです。

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

P2821R5 span.at()

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

以前の記事を参照

このリビジョンでの変更は、余分なフリースタンディングコメントを削除したことです。

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

P2826R1 Replacement functions

ある関数を指定したシグネチャでそのオーバーロード集合に追加できるようにする、一種の関数エイリアスの提案。

以前の記事を参照

このリビジョンでの変更はよく分かりませんが、文書の構成を整理していくつか例を追加しているようです。

P2827R1 Floating-point overflow and underflow in from_chars (LWG 3081)

std::from_chars浮動小数点数を変換する際にアンダーフローを検出できるようにする提案。

以前の記事を参照

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

P2830R1 constexpr type comparison

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

以前の記事を参照

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

  • std::type_info::beforeの変更を行わないオプションを追加
  • 匿名名前空間を空にすることができないことを明確化
  • FAQセクションを追加
  • 実用的なサンプルコードを追加
  • 提案する構文を追加
  • appendixを追加

などです。

この提案ではオプションとして次の5つを提示しています

  1. std::type_info::beforeconstexprを付加
    • 利点
      • 新しいトークンが必要ない
      • 発見されやすい
      • 組み込みのtype-erasureであること
    • 欠点
      • ABI破壊
      • 多くの場所で禁止されている<typeinfo>をインクルードする必要がある
      • NTTPに対して使用できない
  2. std::strong_ordertype_info比較のためのオーバーロード追加
    • 利点
    • 欠点
      • <compare><typeinfo>のインクルードが必要になる
      • NTTPに対して使用できない
  3. std::entity_ordering変数テンプレートの導入
    • 利点
      • 新しい名前であり、明確にコンパイル時に利用可能
      • <typeinfo>のインクルードが必要ない
    • 欠点
      • 新しい名前であること
      • 発見されづらい
  4. std::type_identityoperator<=>を追加
    • 利点
      • 若干やることが明白
    • 欠点
      • <type_traits><compare>に依存するようになる
      • NTTPに対して使用できない
  5. ユニバーサルテンプレートパラメータを用いたstd::__lift::operator<=>を導入
    • 利点
      • 強いて言うなら、そのうちこの__liftのようなものが必要になるかもしれない
    • 欠点
      • 何をするか明確ではない
      • 新しい名前であること
      • 発見されづらい
      • 本質的に別のtype_infoを追加している

3つ目のstd::entity_orderingとは次のような変数テンプレートです。

template <universal template T, universal template U>
inline constexpr std::strong_ordering entity_ordering = ORDER(T, U);

すなわち、std::entity_ordering<T, U> < 0のようにしてT, Uの順序付比較が行えます。

P2845R5 Formatting of std::filesystem::path

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

以前の記事を参照

このリビジョンでの変更は、「無効なコード単位」をより具体的な「不適格な部分列の最大部分」に置き換えたこと、LEWGでの投票結果を追記したことです。

P2863R3 Review Annex D for C++26

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

以前の記事を参照

このリビジョンでの変更は、関連提案のステータス更新や、その表記法の変更などです。

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

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

以前の記事を参照

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

  • Kona会議でのEWGのレビューの記録を追加
  • いくつかのセクションの並べ替え
  • 非推奨警告についての調査を更新
  • ベースとなるドラフトの更新
  • Annex Cのテキスト修正

などです。

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

P2865R4 Remove Deprecated Array Comparisons from C++26

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

以前の記事を参照

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

  • 最新のWDに追随
  • 委員会での進捗を追記
  • C互換性について追記
  • 非推奨警告の調査を更新

などです。

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

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

以前の記事を参照

このリビジョンでの変更は、最新のWDに追随したことと、Annex Cの修正などです。

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

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

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

以前の記事を参照

このリビジョンでの変更は全体的な修正などです。

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

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

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

以前の記事を参照

このリビジョンでの変更は、最新のWDに追随したことと、Annex Cの修正などです。

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

P2871R3 Remove Deprecated Unicode Conversion Facets From C++26

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

以前の記事を参照

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

  • 最新のWDに追随した
  • 互換性の文言を更新し、ヘッダユニットのインポートについて言及
  • C++03との差分にある<codecvt>ヘッダ周りの参照を削除

などです。

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

P2878R6 Reference checking

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

以前の記事を参照

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

などです。

P2890R1 Contracts on lambdas

P2890R2 Contracts on lambdas

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

以前の記事を参照

R1での変更は、キャプチャの問題を解決するための別のオプションを追加したことです。

このリビジョンでの変更は、無意味な名前探索ルールを修正したことです。

ラムダ式で契約注釈を使用可能にする際に最も問題となるのは、デフォルトキャプチャ使用時に契約注釈内部だけで使用されている外部エンティティを暗黙的にキャプチャするかどうかです。

constexpr auto f(int i) {
  return sizeof( [=] [[pre: i > 0]] {});  // 戻り値はどうなる?
}

constexpr auto g(int i) {
  return sizeof( [=] { [[assume (i > 0)]]; } ); // 戻り値はsizeof(int)
}

R0では契約注釈だけで使用されているラムダ式外部の変数はデフォルトキャプチャによるキャプチャの対象とすることを提案していました。これについて、R1では全部で7つのオプションが提示されています

  1. 契約述語はキャプチャをトリガーできる
    • assume属性における同様の議論では、そのようなキャプチャをill-formedにするために言語の複雑化を正当化するには実際のコードで問題が起きる可能性が低い(エッジケースである)として、キャプチャをトリガーすることを維持した
    • 契約注釈にも同じことが言える
  2. 契約述語はキャプチャをトリガーしない
    • P2932R2で提案されている
    • 単にキャプチャをトリガーしない、とするだけだと契約注釈で間違った変数を使用してしまう可能性がある(ローカル変数と同名のグローバル変数を暗黙的に使ってしまう)
  3. 契約述語がキャプチャをトリガーする場合ill-formed
    • オプション2の問題をカバーしたもの
    • 言語が複雑化し、ユーザーにとって自明ではないかもしれない
    • assume属性と矛盾する
  4. 契約述語がキャプチャをトリガーする場合警告する
    • 契約注釈からキャプチャをトリガーするコードは常に疑わしいコードであり、警告するならill-formed(オプション3)の方が望ましい
  5. 契約述語内で他の方法でodr-usedされないエンティティをodr-usedすることはill-formed
  6. デフォルトキャプチャを使用したラムダ式で契約注釈指定を許可しない
    • ユーザにとってあまりに多くの価値を奪うものであり、思い切りが良すぎる
  7. ラムダ式で契約注釈の使用を許可しない
    • 6と同様

この提案では、この中で実行可能な選択肢は1か3のどちらかであるとしています。なお、現在のContracts MVP仕様としては2が採用されています(別の提案によるもの)。

P2894R1 Constant evaluation of Contracts

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

以前の記事を参照

R0では、コンパイル時に契約チェックするかどうか(及び定数式で実行できない契約条件の扱いをどうするか)について、グローバルなスイッチによって一律的に制御するようなことを提案していましたが、翻訳単位によってその設定が異なる可能性があることを指摘されました。それによって再検討が行われ、このリビジョンでは結局実行時と同様に、全ての契約注釈のセマンティクスは実装によって定義され、注釈ごと、その評価ごとに変化しうる、というモデルを採用することになりました。

以前はコンパイル時にセマンティクスを区別する必要はない、としていましたが、実装が定数評価中にそのセマンティクスを選択する以上3種類のセマンティクスが定数評価中にどのように動作するかを指定する必要があります。

この提案での検討対象はR0と同様に次の場合のみです

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

まず1の場合、契約注釈を持つ関数が定数式で呼ばれるまではその契約条件式の定数実行可能性を気にしないことはR0と同様です。定数実行できない契約注釈を持つ関数が定数式で呼ばれてしまった時が問題になります。

その場合、R0では実装定義(コンパイラオプション等で指定)としていました。このリビジョンでは、契約違反が起きたものとして扱うことを提案しています。すなわち、コンパイル時に呼び出すことのできない事前・事後条件(及びアサーション)はコンパイル時には決して満たされることはないと言うことであり、それは契約違反となります。契約違反時の動作は契約注釈のセマンティクスによって指定されます。

2の場合とはつまり契約違反が起きたと言うことであり、その動作はセマンティクスごとに指定され、次のようになります

  • ignore, observe : 診断を発行(エラーにならない)
  • enforce : 診断を発行しill-formed(エラーになる)

そして、定数式における契約注釈のセマンティクスは契約注釈及びその評価ごとに実装定義(オプション等によって指定)となります。

また、定数初期化されうる変数の初期化式などの定数式で評価されるかどうかが決定的でない文脈においての契約注釈の評価については、上記のようなセマンティクス適用以前に、その契約注釈を含む式が定数評価可能かを契約注釈を無視してテストして、定数評価可能ならば再度契約セマンティクスを有効化した上で初期化式の評価を行うようにすることを提案しています。

P2900R2 Contracts for C++

P2900R3 Contracts for C++

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

以前の記事を参照

R2での変更は

  • 構文としてnatural syntaxを採用
    • P2961R2を採択
  • default関数に対する事前/事後条件の指定はill-formed
    • P2932R2を採択

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

  • delete関数に対する事前/事後条件の指定はill-formed
  • ラムダ式に対する事前/事後条件の指定を許可
  • 契約注釈は暗黙のラムダキャプチャをトリガーしない
    • P2932R2を採択
  • std::contracts::invoke_default_contract_violation_handler()(デフォルトの違反ハンドラを呼び出す関数)を追加
  • 契約条件式から参照されるローカルエンティティは暗黙的にconstとする
    • P3071R1を採択
  • 事後条件における戻り値名のセマンティクスを明確化
    • P3007R0を採択し、戻り値名は暗黙的にconstとする
  • Overviewセクションを追加
  • Recursive contract violationsセクションを追加

などです。

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

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

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

以前の記事を参照

R3での変更は

  • LEWGでの投票結果を追加

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

  • 機能テストマクロ__cpp_lib_format__cpp_lib_format_ucharに置き換え
  • wchar_tへの対応を改善するために文言を調整

などです。

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

P2918R2 Runtime format strings II

std::format()の実行時フォーマット文字列のためのAPIを追加する提案。

以前の記事を参照

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

  • 機能テストマクロ__cpp_lib_formatバンプするようにした
  • runtime-format-stringに説明専用メンバstrを追加
  • runtime-format-stringにコンストラクタを追加
  • この提案で追加される関数にはnoexceptを付加する
  • runtime-format-stringを固定化

などです。

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

P2932R2 A Principled Approach to Open Design Questions for Contracts

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

以前の記事を参照

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

  • セクションをいくつか追加
  • 定数式の処理セクション、observeセマンティクスを持つ契約注釈の実行時以外の評価についてのセクションを明確化
  • 提案1.Bをトリビアルな特殊メンバ関数に適用するように修正し、1.Cを追加
  • natural syntaxを使用するように書き換え

などです。

提案1.Cはトリビアルでないものも含めてdefaultで定義された関数に対して契約注釈を行えるのかどうかについてのもので、次のようなものです

  • default関数が、その最初の宣言で事前条件/事後条件を持つ場合、ill-formed

これは、defaultとはインターフェースなのか実装なのか、それに関して契約注釈はどうあるべきかなどについての議論を行うために、C++26に対してはとりあえず禁止しておくことを意図したものです。C++26 Contracts仕様にはこれが採用されています。

P2933R1 std::simd overloads for <bit> header

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

以前の記事を参照

このリビジョンでの変更は、文言も含めた全体のテキストの修正などです。

P2935R4 An Attribute-Like Syntax for Contracts

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

以前の記事を参照

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

  • [[assert : expr]]を式にした
  • 事後条件における戻り値命名の代替案の追加

などです。

2023年11月のKona会議において、C++契約プログラミングのための構文としてはこちらではなくP2961R2のnatural syntaxを採用することが決定されました。

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

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

以前の記事を参照

このリビジョンでの変更は、void戻り値型の指定とそのトリビアル性(この提案では許可しない)についての議論を追加したことです。

P2961R2 A natural syntax for Contracts

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

以前の記事を参照

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

  • アサートのキーワードとしてcontract_assertを採用
  • assertが契約におけるアサートのキーワードとしてふさわしくない理由を追記
  • 実装経験について追記
  • SG22での議論をもとに、Cでの実現可能性について追記

などです。

この提案は2023年11月のKona会議でC++26の契約プログラミング機能のための構文として採用されました。

P2968R1 Make std::ignore a first-class object

P2968R2 Make std::ignore a first-class object

std::ignoreを戻り値破棄のために合法的に使用できるようにする提案。

以前の記事を参照

R1での変更は

  • ビットフィールドについて言及し、それがstd::ignoreの代入にとって有効な右辺オペランドではないことを説明
  • 右辺オペランドとしてnon-volatileビットフィールドがサポートされるべきかの質問を追加
  • 既存の主要な実装の分析を含むフィードバック提案を組み込み
  • volatileビットフィールドによる副作用を防ぐために、ビットフィールドを右辺オペランドとして指定しないoperator=(auto&&)を使用してコードとして仕様を推奨

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

  • 個別のヘッダにしないことを決定
  • std::ignoreの代入演算子シグネチャとしてoperator=(auto const &) const noexceptを使用する
    • auto&& -> const auto&に変更し、左辺値参照修飾を削除

などです。

std::ignoreの代入演算子はあらゆる値を受けることができ、ほとんどの場合に元の値の読み取り(すなわちコピー)は発生しません。しかし、その例外がビットフィールドであり、ビットフィールドを渡そうとすると対応する整数型の一時オブジェクトを生成して渡す形になり、ビットフィールドの値へのアクセスが発生します。これが特に問題となるのはそのビットフィールドがvolatileである場合で、volatileの場合はその値へのアクセスを消し去ることができず、std::ignoreの代入演算子が何もしないという効果を達成できなくなります。

std::ignoreの代入演算子の規定としてどんな値でも受け取り何もしないと言う風に指定する代わりに、コードとして代入演算子シグネチャを指定し引数型を制限することでこの問題は解決できます。その際に、引数型をauto&&(ビットフィールド禁止)にするのかcosnt auto&volatileビットフィールドを受けられない)にするのかによってビットフィールドを許可するか、あるいは非volatileビットフィールドを許可するかが分岐します。

このリビジョンでは、cosnt auto&にすることでstd::ignoreではビットフィールドを許可するもののvolatileビットフィールドはサポートしないことにしています。

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

P2969R0 Contract annotations are potentially-throwing

contract_assert式に対するnoexcept演算子の振る舞いについての解決策を探る提案。

C++26 Contractsのセマンティクスとして、特に例外に関する部分についてSG21では次のことが合意されています

  • 契約条件式の評価に伴って例外がスローされる場合は契約違反として扱われ、関数の例外仕様とは無関係に違反ハンドラを呼び出す
  • 違反ハンドラからの例外送出は、通常の例外と同じ扱いとなる
    • スタック巻き戻しが発生し、巻き戻しがnoexcept関数呼び出しに及んだ場合はstd::terminate()によってプログラムは終了する
  • 契約注釈がチェックされているかどうかによって、noexcept演算子の結果や契約注釈が付加されている関数の型が変化することはない
  • 事前条件・事後条件・アサーションは全て、述語または違反ハンドラからの例外送出に関して同じように動作する

これによって、チェックされると常に例外を送出する、Well-definedな動作をもつWell-formedなC++プログラムが存在し得ます

#include <contracts>
using namespace std::contracts;

void handle_contract_violation(const contract_violation&) {
  throw 666;
}

int main() {
  contract_assert(false); // チェックされると必ず例外を送出する
}

しかもこのことは、契約違反ハンドラがnoexceptであることがわかっている場合にのみコンパイル時にわかります(違反ハンドラの置換は一般的にリンク時に行われるため、コンパイル時には分からないでしょう)。従って、違反ハンドラがどの様にカスタマイズされているか(あるいはされていないか)によらず、ユーザーから見た契約注釈は常に例外を投げうるものとして写ります。

現在のC++にはプログラムのコンパイル時のセマンティクスが特定の式が例外送出するかどうかに依存する場合があります。それは、noexcept演算子の結果を決定する時、最初の宣言がdefaultである特殊メンバ関数の例外仕様を決定する場合、の2つの場合です。

2023年11月のKona会議において、クラスの特殊メンバ関数の最初の宣言がdefaultである場合、契約注釈を付加できないことが決定されました。しかし、それでもなお、この2つの場合のそれぞれについて契約注釈の有無によって結果が変化しうる場合が存在し、現在のMVP仕様ではその状況について何も指定していません。

1つは、契約注釈そのものにnoexcept演算子を適用した場合で、これはcontract_assert()でのみ起こります。

noexcept(contract_assert(false)); // これはtrueになる?falseになる?

2つめは、クラスの特殊メンバ関数の例外仕様が推定される場合で、通常この場合はその関数が例外を投げうるものを含んでいなければnoexcept(true)と推定されます。現在その様な場所に直接契約注釈を付加することはできませんが、間接的に現れる可能性があります

struct B {
  int i = (contract_assert(false), 42);
};

struct S : B {
  // これらのコンストラクタのnoexcept性はどうなる?
  S() = default;
  S(S&&) = default;
};

この様なコンテキストは現在この2つだけのはずですが、将来の機能(例えば、noexcept(auto))を考慮するとさらに増加する可能性があります。

この提案は、C++26に契約機能を導入する前にこれらの仕様の空白を埋めるために可能な解決策を探るものです。

この提案で上げられている解決策は次の7つです

  1. 契約注釈を例外を送出しうるものとして扱う
    • 上記どちらも、noexcept(false)となる
    • ゼロオーバーヘッド原則に反する(契約注釈の追加によりムーブコンストラクタの例外仕様が変化するなど)。これは契約機能の採用の阻害要因となりうる
  2. 契約注釈を例外を送出しないものとして扱う
    • 上記どちらも、noexcept(true)となる
    • 契約注釈が存在する場合にnoexcept演算子が嘘をつくことなる
    • 例外中立なライブラリフレームワーク(P2300の様なもの)の記述が困難となる
  3. 1と2の両方の状況を許可する
    • 契約注釈のメタアノテーションコンパイラオプションによって契約注釈が例外を投げうるかを指定する
    • ユーザーが契約注釈にnoexceptメタアノテーションを付加する場合、単純に関数全体にnoexceptを付加するだけで良い
    • ビルドモードを導入するとC++の方言が生まれる
  4. 謝って送出された例外が推定されたnoexcept(true)例外仕様を回避できる様にする
    • 契約注釈は言語内で例外を投げないものとして扱われるが、違反ハンドラからスローされた例外のみnoexcept(true)な関数のスタック巻き戻しを行うことが許可される
    • 呼び出しが例外を投げないことを前提とする処理でこれが起こると、結局プログラムの実行は安全ではなくなる
    • 例外仕様のメンタルモデルが複雑化し言語内に矛盾が生じ、仕様と実装が複雑化する
  5. これらの場合が現れない様にする
    • 次の3つの方法が考えられる
      1. contract_assertを式ではなく文にする
      2. noexcept演算子contract_assertまたはcontract_assertを部分式として含む式に適用するとill-formed
        • さらに、例外仕様に影響を与えうる箇所にcontract_assertが出現するとill-formed
      3. noexcept演算子contract_assertまたはcontract_assertを部分式として含む式に適用するとill-formed
        • ただし、例外仕様に影響を与えうる箇所にあるcontract_assertがill-formedとなるのはその存在が例外仕様の推定に影響を与える場合のみ
    • 仕様や実装が複雑になり、ユーザーにより多くの苦労を強いることになるが、上記問題を回避し全ての場合にゼロオーバーヘッド原則を満たす
  6. ガイドラインと診断に頼る
    • ユーザーにその様なコードを書かないほうがいいと警告するならば、単にill-formedとしたほうがいい
  7. 違反ハンドラからの例外送出サポートを禁止する
    • 違反ハンドラからの例外送出を使用するのは実際には非常に難しいため、これを削除し例外を使用しない方法で同等の契約違反後の処理継続を可能にする方法を模索する
    • それを決定するとこの提案で挙げられている問題は全て解決されるが、後から違反ハンドラからの例外送出を許可するのは困難になる
    • 現時点では例外に代わる同等のメカニズムは存在していない

この提案では、それぞれの解決策による影響や実現可能性について考察していますが、どれかを推しているわけではありません。ただし、3,4の案は実現可能ではないと評されており、1,2,6の案は問題があり、7の案も困難が伴うとして、一番ポジティブな評価を受けているのは5(の2,3)の案です。

この提案でどの様な決定がなされたとしても事前・事後条件における違反ハンドラからの例外送出について同様の問題がある様に見えますが、それはすでに合意済みのことです。契約注釈の存在は例外仕様等のコンパイル時セマンティクスに影響を与えず、Lakos Ruleに基づいて契約注釈がなされている限り(無条件noexcept指定と契約注釈の両立は誤っている)それは問題になりません。例えば、あるクラスのムーブコンストラクタがnoexceptとなる場合、そのムーブコンストラクタは広い契約を持ちいかなる事前・事後条件も持たないはずで、そのクラスのメンバのムーブコンストラクタそれぞれについても同様の仮定が成立するはずです。この過程が成り立っていない場合、そのムーブコンストラクタはnoexcept(false)とするのが適切であり、その場合にのみ契約注釈によって事前・事後条件をチェックすることができます。

P2977R0 Module commands database format

ツール間で同じモジュールを生成するために必要となるコンパイルのための情報をファイルにまとめておく提案。

ビルドシステムのようなC++プロジェクトのビルドを管理するツールは、それをコンパイルしたコンパイラとは異なるコンパイラを使用するプロジェクトにおいてもモジュールを管理しなければなりません。しかし、一般的にビルド済みモジュールはコンパイラ間や同じコンパイラのバージョン間、あるいはモジュールを分析する必要のあるツール(静的解析ツールなど)等それぞれの間で互換性がありません。これらのツールはメインのビルドの設定に対応する形で、それぞれ独自の形式によってモジュール(のインターフェース)をビルドできる必要があり、メインのビルドと(形式等は除いて)同等のコンパイル済みモジュールを生成するために必要なフラグなどを知る必要があります。

この提案は、そのような情報をファイルに保存してツール間でやり取りすることを目的として、どのような情報が必要かやその保存の方法などを特定しようとするものです。

メインビルドと同等のコンパイル済みモジュールを作成するためには、ツールは少なくとも次の情報を知っている必要があります

  • モジュールインターフェースのソース
  • 生成されるモジュールのモジュール名
    • そのモジュールが使用される場所を特定するために必要
  • 必要なモジュールの名前(依存モジュール名)
    • モジュールの依存関係を解決するために必要
  • 必要なモジュールを提供するソースコード
  • ソース内でのモジュールの可視性
    • モジュール実装単位/実装パーティションのように、プライベートなモジュールが存在する
    • そのようなモジュールに属するシンボルは外部からアクセスできない場合があり、より的確な診断のためにモジュール名を使用できる
  • ソースを処理するビルド中に使用されるローカルプリプロセッサ引数
    • メインビルドの成果物に対応するものを生成するために使用する必要がある
    • ツールは、メインビルドに使用されるコンパイラのフラグを自身の利用のために変換する必要がある可能性がある
  • コンパイルのための作業ディレクト

これらの情報をビルドツリー内で表現する方法としては次の3つが挙げられています

  1. スタンドアロン
    • 全ての情報を何らかのファイルに保存する
    • (おそらくソースファイルごとに)別々のファイルとして存在している
    • この利点は、情報をビルドだけでなくインストールにも使用できる点
  2. コンパイルコマンドデータベースとの相互参照
    • Compile Commands Databaseと手元の情報とを組み合わせて使用する
    • 重複を減らすことができるが、必要な情報を全て取得するには2つのデータベースを手動でマージするツールが必要になる
    • この欠点は、あるソースが異なるフラグでビルドグラフ上に複数回現れうる点
  3. コンパイルコマンドデータベースとの共有
    • 手元の情報をコンパイルコマンドデータベースと共有できる部分とそうでない部分に分割し、共有できる部分はコンパイルコマンドデータベースのものを取得する

このようなモジュールコンパイルデータベースは、一般的にビルド中に作成される必要があります。なぜなら、モジュール名はビルドが完了するまでわからないからです。しかし、上記コンパイルコマンドデータベースがビルドの一部が完了するまで存在しない生成されるソースのコンパイルを参照することができ、新しい問題というわけではありません。

メインビルドに関わらない他のツールがこれらの情報を簡易に取得するために、ビルドシステムはモジュールコンパイルデータベースをよく知られた場所(おそらく、pkgconfigなどのようなものがある場所)にあるファイルにまとめる仕組みを提供する必要があります。これによって、他のツールが関連ファイルを探索する手間が軽減され、ファイル全体にわたって情報が一貫していることが保証されます。

この提案はこのような要件を列挙したもののようで、まだ何かを提案するには至っていないようです。

P2980R1 A motivation, scope, and plan for a quantities and units library

物理量と単位を扱うライブラリ機能の導入について説明する提案。

以前の記事を参照

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

  • ガーディアン紙の華氏の取り扱いに関する問題について追加
  • ScopeとPlan for standardizationを更新
  • タイトルからPhysicalを取り除いた

などです。

P2981R1 Improving our safety with a physical quantities and units library

↑の物理量と単位を扱うライブラリ機能について、コンパイル時の安全性に関する側面を解説する文書。

以前の記事を参照

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

  • ガーディアン紙の華氏の取り扱いに関する問題について追加
  • 燃料消費の例を拡張
  • いくつかの参考リンクを追加
  • 型を保持する型特製についてLack of safe numeric typesで説明
  • Non-negative quantitiesを書き直し
  • Limitations of systems of quantitiesセクションを追加
  • Temperaturesセクションを追加

などです。

P2982R1 std::quantity as a numeric type

↑の物理量と単位を扱うライブラリ機能について、数値と計算の側面に関する設計の説明をする文書。

以前の記事を参照

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

  • 燃料消費の例を拡張
  • Dimension is not enough to describe a quantityセクションを拡張
  • Lack of convertibility from fundamental typesセクションを追加
  • Terms and definitionsセクションにmp-unitsの用語集へのリンクを追加
  • Equivalenceセクションのサンプルコードを修正

などです。

P2984R1 Reconsider Redeclaring static constexpr Data Members

static constexprメンバ変数のクラス外定義の非推奨の扱いを検討する提案。

以前の記事を参照

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

  • コンパイラの警告についての状況を追加
  • 2023年11月のKona会議におけるレビュー結果について追加
  • conclusionセクションを追加

などです。

EWGのレビューにおいては、static constexprメンバ変数のクラス外定義を削除することが好まれた様でしたが、一方で現在十分に非推奨動作であることが警告されていない状況でいきなり削除してしまう事は好まれなかった様です。そのため、C++26に対してはこの点に関して変更を加えず、コンパイラベンダにはこの問題についての警告をよりユーザーに見える様にすることを推奨する、という方向性が支持されました。

P2996R1 Reflection for C++26

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

以前の記事を参照

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

  • 全てのサンプルにCompiler Explorerでの動作例リンクを付加
  • synth_structdefine_classに再指定
  • いくつかのメタ関数を関数テンプレートの代わりに関数として再指定
  • エラー処理メカニズムと例外の優先設定に関するセクションを追加
  • チケットカウンタとバリエーションの例を追加
  • entity_refpointer_to_membervalue_ofで表現する様に置き換え

などです。

静的リフレクションにおけるエラーとは例えば、std::meta::template_of(^int)のような間違ったコード(template_of(^T)は型Tのテンプレートの鏡像を取得する。intはテンプレートの特殊化ではないのでエラー)が何を返すか?という問題です。

この候補としては現時点で次のものが考えられます

  1. 静的リフレクション過程のエラーは定数式ではない(コンパイルエラー)
  2. 無効値(エラーであること、およびソースの場所とその他有用な情報)を保持する鏡像を返す
    • P1240の推奨アプローチ
  3. std::expected<std::meta::info, E>を返す。Eはソースの場所とその他有用な情報を提供する
  4. Eを例外としてスローする
    • 定数式で例外を許可する必要がある。おそらく、定数式中でキャッチされない例外のみコンパイルエラーとなる様にする

2つ目の方法の欠点は、メタ関数が範囲を返す場合(template_arguments_of(^int)の様な場合)に何を返すかという点です。この場合に単一の無効な鏡像値を返すと正常なものが帰る場合との一貫性がなくなり、使いづらくなります。かといって、空の範囲を返してしまうとエラー情報が失われてしまいます。結局、何を返したとしても、expectedもしくは例外のアプローチの方がより一貫性があり簡単なエラー処理を提供します。

例外にはいくつかの懸念がありますが、ここでの文脈は定数式であるため、実行時における例外の問題点はここでは問題になりません。その上で次のようなよく使用されると思われる様な例を考慮すると

template <typename T>
  requires (template_of(^T) == ^std::optional)
void foo();
  • template_of(^T)expected<info, E>を返す場合、foo<int>は置換失敗
    • expected<info, E>infoは等値比較可能であり、結果はfalseとなり制約を満たさなくなる
  • template_of(^T)が例外を送出する場合、foo<int>から送出された例外がキャッチされない場合はコンパイルエラー
    • これは置換失敗ではなく、制約が定数式ではなくなることによるエラー
    • 置換失敗にする場合、まず制約でTがテンプレートであることを確認するか、制約が定数式であることを要求する言語の規定を変更する

これらのことを考慮し、例外の懸念を考慮しても、この提案では静的リフレクションにおけるエラー伝搬にはexpectedよりも例外の方がユーザーフレンドリーであるとしています。

P2999R1 Sender Algorithm Customization

P2999R2 Sender Algorithm Customization

P2999R3 Sender Algorithm Customization

P2300のsenderアルゴリズムがカスタマイズを見つける手段を修正する提案。

以前の記事を参照

R1での変更は、提案する文言を追加したこと、比較テーブルを追加したことなどです。

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

  • 早期のカスタマイズと後からのカスタマイズの例を追加
  • 早期のカスタマイズを残す理由について追記
  • connect時のドメイン計算を修正
  • senderの要件に完了schedulerが全てドメインを共有するという要件を追加
  • transform_senderは必要に応じて再帰することを明確化
  • 不要になっていた説明専用のmake-transformer-fnを削除

などです。

P3006R0 Launder less

バイト配列上に構築した別の型のオブジェクトへのポインタをより直感的に取得できる様にする提案。

バイト配列を確保して、その配列の要素型とは異なる型のオブジェクトをその配列上に構築する場合、そのオブジェクトへの正しいアクセス方法は非常に限定されています。

// Tのオブジェクトを配置するバイト配列
alignas(T) std::byte storage[sizeof(T)];

// Tのオブジェクト構築、戻り値のポインタをそのアクセスに使用する
T* ptr1 = ::new (&storage) T(); // ok

// storageのアドレスから直接Tのオブジェクトへのアドレスを得る
T* ptr2 = reinterpret_cast<T*>(&storage);  // UB
T* ptr3 = std::launder(reinterpret_cast<T*>(&storage));  // ok

この場合、配置newによってstorage上に構築されているオブジェクトへのアクセスを行えるのはptr1ptr3のみであり、storageのアドレスからreinterpret_cast<T*>しただけのptr2からのアクセスは未定義動作となります。

これはポインタとその領域で生存期間内にあるオブジェクトとが結びつかないためで、配置newの戻り値を使用しない場合はptr3のようにstd::launderによってその領域上で生存期間内にあるオブジェクトへのポインタを取得しなければなりません。

とは言えこのことはかなり意味論的なことで、多くのコンパイラptr2の様にstd::launderを使用しない場合でもstd::launderを使用する場合と同等のコードを出力する様です。

この提案は、その様な既存の慣行を標準化し、このようなユーザーの期待に沿わないUBを取り除こうとするものです。

これによるメリットは、現在UBを回避するために配置newの戻り値を保存している様なコード(例えば、Boost.Optionalなど)において、その様なポインタを保存しなくてもよくなることでストレージサイズを削減でき、なおかつstd::launderというよく分からないものを使用せずとも直感的なコードによってUBを回避して意図した動作を実現できる点です。

P3007R0 Return object semantics in postconditions

事後条件の契約注釈で戻り値を参照する場合の、その戻り値のセマンティクスに関する提案。

事後条件注釈では、その関数の戻り値を使用するために戻り値の名前を指定してそれを参照することができます。

int f()
  post (r: r > 0);  // 戻り値は正の値

現在のMVP仕様では、まだこの戻り値に関するセマンティクスが正確に指定されていません。

C++20時点の仕様及びそれを受け継いだMVP仕様においては、「事後条件では、その関数のglvalueもしくはprvalueの結果オブジェクトを表す識別子を導入できる」とだけ言及されていて、その値カテゴリは何か、それは参照なのか、その型は何か、アドレスを取れるのか、変更可能なのか、などについての規定はありません。

この提案は、事後条件の戻り値のセマンティクスについての現在の不明な点について考察し、そのセマンティクスを決定しようとするものです。ここで提案されていることは次のような事です

  • 事後条件における戻り値を表す変数名は、本物の戻り値オブジェクトを参照する左辺値である、とする
    • これは構造化束縛によって導入される名前のセマンティクスからの借用
    • その値カテゴリは左辺値(lvalue)
    • 言語参照(T&)ではないが、戻り値を参照している
    • 従って、事後条件から戻り値を参照する場合にはコピーもムーブも行われず、RVOを妨げない
  • 戻り値名rに対するdecltype(r)の結果は、関数の戻り値型
  • 戻り値型がtrivially copyableである場合、戻り値名のアドレスと実際の戻り値のアドレスは異なる可能性がある
    • これは、trivially copyableオブジェクトをレジスタに配置して返す挙動を変化させない(ABI破壊を回避する)ため
    • その場合、戻り値名は戻り値を保持する一時オブジェクトを参照している
  • 呼び出し側で戻り値を受ける変数がconstである場合、関数の戻り値型がconstでなければ、事後条件における戻り値の変更は未定義動作とならない、とする
    • 変数に対するconstは初期化が完了するまで有効ではなく、事後条件は戻り値を受ける変数の初期化よりも前に呼び出される

この提案では、事後条件における戻り値名が暗黙constであるかは著者の間で合意できなかったことから提案していません。それには利点と欠点がありますがどちらを採用するべきかは明確ではないため、その方向性の決定は委員会に委ねています。

P3016R1 Resolve inconsistencies in begin/end for valarray and braced initializer lists

std::valarrayと初期化子リストに対してstd::beginstd::cbeginを呼んだ場合の他のコンテナ等との一貫しない振る舞いを修正する提案。

以前の記事を参照

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

などです。

P3019R1 Vocabulary Types for Composite Class Design

P3019R2 Vocabulary Types for Composite Class Design

P3019R3 Vocabulary Types for Composite Class Design

動的メモリ領域に構築されたオブジェクトを扱うためのクラス型の提案。

以前の記事を参照

R1での変更は

  • 機能テストマクロの追加
  • std::indirectstd::formatサポートを追加
  • 使用前後の比較サンプルをまとめたAppendix Bを追加
  • 型が値を持つことを事前条件として追加
  • constexprサポートを追加
  • std::polymorphicのQoIとしてsmall buffer optimizationを許可
  • アロケータサポートのために文言を追加
  • 不完全型のサポートを有効化
  • pointer入れ子型はallocator_traits::pointerを使用する様に変更
  • std::uses_allocator特殊化を削除
  • std::indirectコンストラクタのinplace_tを削除
  • sizeofエラーを削除

R2での変更は

  • std::indirect比較演算子の戻り値型がautoであることについての議論を追加
  • emplace()の議論をappendixに追加
  • allocator awarenessサポートのために文言調整

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

  • コンストラクタにexplicitを追加
  • indirect(U&& u, Us&&... us)コンストラクオーバーロードと制約を追加
  • polymorphic(allocator_arg_t, const Allocator& alloc)コンストラクオーバーロードを追加
  • std::variantとの類似/相違点についての議論を追加
  • 破壊的変更とそうではない変更の表を追加
  • 不足している比較演算子を追加し、それらが条件付きnoexceptであることを確認
  • std::indirectの推論補助を修正
  • 複雑な例におけるstd::indirectの間違った使用例を修正
  • swap()noexceptに関する文言を修正
  • std::indirectの比較演算子の制約に関する文言についての問題を解決
  • コピーコンストラクタはallocator_traits::select_on_container_copy_constructionを使用する様にした
  • 自己swapと自己代入が問題とならないことを確認
  • std::optional特殊化を削除
  • erroneousの使用をundefined behaviourで置き換え
  • コピー代入における強い例外保証を追加
  • コンストラクタにおいて、Tのuses-allocator構築を行う様に指定
  • 文言の見直し

などです。

P3022R1 A Boring Thread Attributes Interface

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

以前の記事を参照

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

  • P2019R4の変更に追随
  • 文言と例を追加
  • Conclusionセクションを追加

などです。

この提案の方向性はLEWGでの指示が得られなかったため、追及は停止されています。

P3023R1 C++ Should Be C++

C++標準化委員会の目標・見通しについて問い直す文章。

以前の記事を参照

R0が主張をリストアップしたものだったのに対して、こちらはそれを文章にまとめた形になっている完成版です。

P3024R0 Interface Directions for std::simd

C++26に向けて提案中のstd::simdのインターフェースやその設計動機などについて紹介するスライド。

P3025R0 SG14: Low Latency/Games/Embedded/Financial trading/Simulation virtual Minutes to 2023/09/12

SG14の2023年9月12日に行われたオンラインミーティングの議事録。

P3026R0 SG19: Machine Learning virtual Meeting Minutes to 2023/07/13

SG19の2023年7月13日に行われたオンラインミーティングの議事録。

P3027R0 UFCS is a breaking change, of the absolutely worst kind

P3021のUFCS(Unified function call syntax)提案に異議を唱える提案。

P3021については以前の記事を参照

P3021では、メンバ関数呼び出しを拡張する形でUFCSを有効化することを提案しており、メンバ関数呼び出し(x.f(a,b)x->f(a, b))を非メンバ関数の呼び出し(f(x, a, b)f(*x, a, b))にフォールバックすることでUFCSを可能にするものです。

P3021のUFCSは確かに既存のコードを壊しません(有効なコードがill-formedにならない)が、現在のメンバ関数呼び出しに関してユーザーが依存している保証を壊している、というのがこの提案の主張です。

その保証とは、メンバ関数呼び出しはADLとは無関係に行われるというものであり、別の言い方をするとメンバ関数呼び出しではADLは行われないという保証です。このため、確かに現在ある既存のコードが壊れることはありませんが、将来のコードは壊れる可能性があります。

例えばリファクタリングにおいて誰もが経験すると思われる、メンバ関数の名前を変更する場合を考えます。

struct Foo {
  // この関数名を変えたい
  void snap();
};

// この.snap()は例えばこのように呼び出されている
template <class T>
void do_snap(T&& f) {
  f.snap(); 
}

このFoo::snap()の名前をslap()に変更しようとする場合、これを宣言・定義しているファイルを編集し名前を変更してから使用されているところを修正していけば良いでしょう。修正を忘れたり、呼び出しを間違えればコンパイラコンパイルエラーとして修正が完全ではないことを教えてくれます。これは静的型付け言語の基本中の基本振る舞いであり、コードベースが大きくなったとしてもそれは変化しません。

しかしP3021が有効化された場合そのような保証はもはや期待できず、snap()slap()に変更してなにも修正しなかったとしても一切エラーが出ない可能性があります。なぜなら、ADLによってslap()を探しに行き、たまたま呼び出せる候補を見つけてしまうかもしれないからです。P3021が導入される以前はメンバ関数呼び出しからADLが発動されることはなく、ADLによって呼び出せてしまうようなslap()を定義していたとしても安全でした。しかし、P3021導入後はそのような関数がメンバ関数呼び出しからのADLによって呼び出されることを意図していたかどうかは分からなくなります(P3021以前の保証の上に立っているのか、P3021の機能の上に立っているのかが分からなくなるため)。

これはまた、メンバ関数を削除する場合や、関数の引数型を変えるなどのリファクタリングにおいても同様です。

本来であればメンバ関数呼び出しではそのようなADLの複雑さを考えなくても良いはずで、P2855のようにそれを有効に利用しようとする向きもあります。しかし、P3021の機能が導入されるとそのような保証はなくなり、メンバ関数呼び出しは常に意図しない関数呼び出しによってバグを静かに埋め込んでしまう可能性を抱えることになります。

意図的かどうかはともかく、現在の多くのC++ユーザーはメンバ関数呼び出しを使用することでクラスのメンバ関数のみが呼び出され、ADLの複雑さについて頭を悩ませることを回避しています。メンバ関数はクラスという単位でのカプセル化によって、そのアクセス範囲及び呼び出される方法はかなり明確です。

メンバ関数呼び出し構文がそのようなカプセルの外側に及ぶようにするというのは、その単純さと分かりやすさを壊しています。それはユーザーへのサービスではなく、ユーザーに対して破滅的な不利益を与えるものです。

このような理由によりこの提案はP3021のUFCSに反対し、UFCSが必要ならば専用の構文を導入して行うべきであり、既存の関数呼び出し構文を拡張する形でそれを行えば以前にはその複雑さが無かった場所に新しい複雑さを導入してしまい、UFCSによる利点を複雑さの増大による欠点が上回るとしています。

P3028R0 An Overview of Syntax Choices for Contracts

2つの契約構文候補を比較する提案。

C++26契約プログラミング機能に向けて議論が進んでおり、残す大きな問題は構文の決定のみとなっています。現在契約構文としてはC++20の契約機能由来の属性様構文(P2935)と新しい文脈依存キーワードによる自然な構文(P2961R2)の2つが候補として残っています。

P2695で示されたロードマップに従って、2023年11月のKona会議ではこのどちらの構文を採用するのかを決定する予定です。

この提案はその議論のために、2つの構文候補を比較することでそれぞれの特性を明らかにしようとするものです。

この提案では次の2つの観点から候補を比較しています

  1. 提案されている全ての構文候補を、契約のある機能についてそれぞれ対応する構文で記述したものを横に並べて比較
  2. P2885で挙げられている原則とともに、各構文候補がその原則をどの様に満たすのか(あるいは満たさないのか)を比較

事前条件構文の比較

// P2935
void f() [[ pre : true ]];

// P2961
void f() pre( true );

事後条件構文の比較

// P2935
int f() [[ post r : true ]];
int f() [[ post (r) : true ]];
// P2935R4の代替提案
int f() [[ post : r : true ]];

// P2961
int f() post( r : true );

アサーション構文の比較

// P2935
void f() {
  [[ assert : true ]];
}

// P2961
void f() {
  contract_assert( true );
}

かかる文字数の比較

P2935 P2961
事前条件 8 5
事後条件 9〜10 6〜7
アサーション 11 17

これ以外にも様々な観点からの比較が行われています。

2023年11月に行われたKona会議では、P2961の自然な構文をC++契約プログラミング機能のための構文として採用することで合意されました。

P3029R0 Better mdspan's CTAD

std::span/std::mdspanコンパイル時定数によってインデックス指定を受ける場合のCTADを改善する提案。

たとえば、mdspanでよく使用されることになると思われる配列ポインタとそれを参照する多次元インデックスの指定を受けるコンストラクタに対応する推論補助は次の様に定義されています

template<class ElementType, class... Integrals>
  requires((is_convertible_v<Integrals, size_t> && ...) &&
            sizeof...(Integrals) > 0)
explicit mdspan(ElementType*, Integrals...)
  -> mdspan<ElementType, dextents<size_t, sizeof...(Integrals)>>;

このため、このコンストラクタ(CTAD)が使用される場合にはインデックスにコンパイル時定数を渡していてもmdspanのエクステントはdextent(実行時エクステント)が使用されます。

mdspan ms (p, 3, 4, 5); // mdspan<int, extents<size_t, dynamic_extent, dynamic_extent, dynamic_extent>>
mdspan ms2(p, 3, integral_constant<size_t, 4>{}, 5);                              // 同上
mdspan ms3(p, integral_constant<size_t, 3>{}, 4, integral_constant<size_t, 5>{}); // 同上

後2つについてはコンパイル時定数を渡しているため、エクステントのその部分は静的になってほしいものがあります。現在これを叶えるためには、ユーザーは次の様に記述する必要があります

mdspan ms2(p, extents<size_t, dynamic_extent, 4, dynamic_extent>(3, 5)); // mdspan<int, extents<size_t, dynamic_extent, 4, dynamic_extent>>
mdspan ms3(p, extents<size_t, 3, dynamic_extent, 5>(4));                 // mdspan<int, extents<size_t, 3, dynamic_extent, 5>>

最初の例の後ろ2つが自動的にこれと同等になることが望ましいでしょう。また、std::spanにも同様の問題があります。

この提案は、std::mdspan及びstd::spanの推論補助を修正して、動的インデックス指定がコンパイル時定数(std::integral_constantのような型の値)によって指定されている場合にそれを静的な指定としてエクステントに反映する様にしようとするものです。

これによる利点は次の様なものが挙げられています

  • 動的・静的エクステントを同等な形式によって指定できるため、エクステント型の反映が直感的になる
    • 現在、動的エクステントは数値で直接コンストラクタに指定できるのに対して、静的エクステントはstd::extents<I, idx...>{...}の様に指定する必要がある
  • 正しい数の引数を渡すためにdynamic_extentsの数を計算する必要がなくなり、エラーが起こりにくくなる
    • extents<size_t, dynamic_extent, 4, dynamic_extent>(3, 5)の様に、静的エクステント中に動的エクステントが混ざっている場合にextentsのコンストラクタでその要素数を指定しなければならない
  • P2781のstd::constexpr_vを使用すると、エクステントが混在するmdspanmdspan(c_<3>, 4, c_<5>)の様に記述できる様になる。

この様なことはstd::submdspanC++26に導入済)では既に行われており、その仕組みを再利用することで実装可能です。

まず次の様な検出ユーティリティを用意して(integral-constant-likestd::submdspanとともに導入済)

// std::integral_constantと同等の型を検出する
template<class T>
concept integral-constant-like =        // exposition only
  is_integral_v<decltype(T::value)> &&
  !is_same_v<bool, remove_const_t<decltype(T::value)>> &&
  convertible_to<T, decltype(T::value)> &&
  equality_comparable_with<T, decltype(T::value)> &&
  bool_constant<T() == T::value>::value &&
  bool_constant<static_cast<decltype(T::value)>(T()) == T::value>::value;


template<class T>
constexpr size_t maybe-static-ext = dynamic_extent;        // exposition only

template<integral-constant-like T>
constexpr size_t maybe-static-ext<T> = static_cast<size_t>(T::value);

これを用いてstd::mdspan及びstd::spanの既存の動的エクステント指定に対応する推論補助を修正します

// 変更前
template<class It, class EndOrSize>
span(It, EndOrSize) -> span<remove_reference_t<iter_reference_t<It>>>;

// 変更後
template<class It, class EndOrSize>
span(It, EndOrSize) -> span<remove_reference_t<iter_reference_t<It>>, maybe-static-ext<EndOrSize>>;
//                                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


// 変更前
template<class ElementType, class... Integrals>
  requires((is_convertible_v<Integrals, size_t> && ...) && sizeof...(Integrals) > 0)
explicit mdspan(ElementType*, Integrals...)
  -> mdspan<ElementType, dextents<size_t, sizeof...(Integrals)>>;

// 変更後
template<class ElementType, class... Integrals>
  requires((is_convertible_v<Integrals, size_t> && ...) && sizeof...(Integrals) > 0)
explicit mdspan(ElementType*, Integrals...)
  -> mdspan<ElementType, dextents<size_t, maybe-static-ext<Integrals>...>;
//                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

P3031R0 Resolve CWG2561: conversion function for lambda with explicit object parameter

ジェネリックthisパラメータを持つラムダ式の関数ポインタへの変換の規定が曖昧なのを解決する提案。

C++23のDeducing thisはクラスのメンバ関数thisパラメータを明示的に書くことができる機能です(この引数の事を明示的オブジェクトパラメータと呼びます)。これはラムダ式においても使用することができますが、ラムダ式の場合はそのクロージャ型名を知ることができないので、通常this autoのように書かれます。

また、ラムダ式はキャプチャしていない場合にのみ関数呼び出し演算子シグネチャと互換性のある関数ポインタへ変換することができます。

このとき、明示的オブジェクトパラメータを持つラムダ式が対応する関数ポインタへ変換可能であるかについて実装間で挙動に差があります。

int main() {
  using T1 = decltype([](int x) { return x + 1; });  // ok、ラムダ式のクロージャ型
  int(*fp1)(int) = +T1{};  // ok、関数ポインタへの変換

  using T2 = decltype([](this auto, int x) { return x + 1; });  // ok、ラムダ式のクロージャ型
  int(*fp2)(int) = +T2{};  // Clangはng、MSVCはok
}

1つ目の例は通常のラムダ式から関数ポインタへ変換する例です。これはC++11以来のもので今回特に問題はありません。

2つ目の例がこの提案の主要な問題であり、ジェネリックな明示的オブジェクトパラメータを持つラムダ式が対応する関数ポインタへ変換できるかどうかが、現時点でこの機能を実装しているClangとMSVCの間で異なっているようです。

また、ラムダの明示的オブジェクトパラメータがジェネリックではなかったとしてもClangは拒否するようで、どうやら規格ではこの場合に生成されるクロージャ型のメンバがどうなるかについて曖昧なようです。

struct Any { Any(auto) { puts("called"); } };

int main() {
  auto a1 = [](int x) { return x+1; };
  auto a2 = [](this Any self, int x) { return x+1; }; // 明示的オブジェクトパラメータはジェネリックではない
  
  int(*fp)(int) = +a2;  // Clangはng、MSVCはok
}

このa1, a2それぞれのクロージャ型は例えば次のようになります

// 型変換演算子で複雑な型を使用するためのエイリアス
template<class T>
using Just = T;

// a1のクロージャ型の例
struct A1 {
  int operator()(int x) const { return x+1; }
  operator Just<int(*)(int)>() const { return +[](int x) { return A1()(x); }; }
};

// MSVCにおけるa2のクロージャ型の例
struct A2_MSVC {
  int operator()(this Any self, int x) { return x+1; }
  operator Just<int(*)(int)>() const { return +[](int x) { return A2_MSVC()(x); }; }
};

// Clangにおけるa2のクロージャ型の例
struct A2_Clang {
  int operator()(this Any self, int x) { return x+1; }
  operator Just<int(*)(Any, int)>() const { return &A2_Clang::operator(); }
};

Clangにおいては明示的オブジェクトパラメータの部分の引数についても関数ポインタ型へ現れるようです。一方、MSVCは明示的オブジェクトパラメータは関数ポインタに現れません。

MSVCの挙動は、従来の暗黙的オブジェクトパラメータを持つラムダを明示的オブジェクトパラメータを持つものにそのままリファクタリングでき(その逆も可能)たり、自身の型を関与させない形で再帰ラムダを定義できたりと、よりユーザーフレンドリーであると思われます。

// Error on Clang, OK on MSVC
auto fib = [](this int (*fib)(int), int x) {
  return x < 2 ? x : fib(x-1) + fib(x-2);
};
int i = fib(5);

この例は、Clangの場合はthisパラメータの型に自身の型が再帰的に現れるのを回避することができませんが、MSVCはこのように関数ポインタによってその再帰を断ち切ることができます。これは再帰ラムダを非ジェネリックに定義できるため、コンパイル時間で有利になるかもしれません。

ラムダ式の関数ポインタへの変換演算子に関して、MSVCはラムダ式における明示的オブジェクトパラメータをかなり特別扱いしており、その引数は常にそのラムダ式自身と同等とみなせると強く仮定しています。ラムダ式が関数ポインタへ変換可能であるのはキャプチャしていない場合のみなので、これは実際にはあまり問題にならないかもしれません。

一方で、this autoを用いずにテンプレート構文によって明示的オブジェクトパラメータを記述するとMSVCでもClangと同様なクロージャ型を生成するようです。

auto a = [](this auto) {}; // MSVCは非ジェネリックとして扱う
auto b = []<class T>(this T) {}; // ジェネリックラムダ

auto pa = +a; // OK on MSVC
auto pb = +b; // error on MSVC
void (*qa)() = a; // OK on MSVC
void (*qb)() = b; // error on MSVC

この非一貫性は非自明ではあります。

この問題はCWG Issue 2561で捕捉され、当初はClangのアプローチを標準化する方向性でした。この提案はそれに対してMSVCのアプローチの方を推すものでしたが、それには文言についてさらなる検討が必要になるということで、明示的オブジェクトパラメータを持つ関数の関数ポインタへの変換をとりあえず禁止しておくことを提案するものです(このR0が出る前に6つのリビジョンがあった様子)。

この提案には、明示的オブジェクトパラメータを持つラムダ式の関数ポインタへの変換を禁止する、Clangのアプローチを採用、MSVCのアプローチを採用、の3つのオプションが含まれており、EWGは1つ目のアプローチを採用したようです。ただし、これはMSVCのアプローチを将来的に採用することを妨げるものではありません。

P3033R0 Should we import function bodies to get the better optimizations?

モジュールのインターフェースにある関数定義をそのモジュールをインポートした翻訳単位にインポートしないようにする提案。

Clangの最適化においては、あるモジュールからインポートされた関数について、その関数の定義をインライン化するような最適化をリンク前に行っているようです。

// a.cppm
export module a;
export int a() { return 43; }

// use.cpp
import a;
int use() { return a(); }

たとえば、最適化を有効にするとuse.cppuse()int use() { return 43; }であるかのようにコンパイルされます。

これはゼローバーヘッド原則に則っており、一見合理的であるように見えます。しかし、use.cppコンパイルする時にオプティマイザはモジュール内のa()に対しても作用してしまうため、プロジェクトの規模が大きくなるとコンパイル時間に跳ね返ってきます。

とはいえそれでも、この問題は単に実装の問題であり規格が口を出す話ではないように思えます。しかし、実際にはABI依存関係と潜在的なODR違反に関連しています。

例えば上記コード群が次のようなコンパイル結果を生成していた場合

a.o       // モジュールaのオブジェクトファイル
a.pcm     // モジュールaのBMI
use.o     // use.cppのオブジェクトファイル
libuse.so // use.cppを含む共有ライブラリ

この時にモジュールa内のa()44を返すように変更して再コンパイルした場合、再コンパイルが発生して再生成されるのはどのファイルでしょうか?これには2つのオプションがあります

rebuild a.o
rebuild a.pcm
rebuild use.o
link libuse.so

もしくは

rebuild a.o
rebuild a.pcm
link libuse.so

この2つの違いはuse.cppが再コンパイルされるかどうかだけです。モジュールの位置づけを考えた場合は再コンパイルされないのが正しい振る舞いにも思えますが、その場合インライン化されているa()の定義の変更が反映されません。 再コンパイルする場合は実装の一貫性は保たれますが、コンパイル時間が増大します。あるいは、最適化を有効にしている場合にのみABI依存関係(現在一般的ではない)に基づいて再コンパイルを行うべきでしょうか?

この提案ではこの問題への対処として、モジュール本文内の非inline関数本体を変更する場合、対応するモジュールインターフェースのBMIを変更するべきではない、とすることを提案するものです。

すなわち、ユーザーがモジュールインターフェースユニット(の本文)内の非インライン関数の本体のみを変更したプロジェクトを再コンパイルする場合、再コンパイルされるのは変更されたモジュールインターフェースのみであり(そのBMIすらも再コンパイルするべきではなく)、他のすべてのコンパイルは行われるべきではありません。ただし、リンクを除きます。

提案では、これによりユーザーエクスペリエンスが向上するはずとしています。

ビルドシステムのレベルでは、ビルドシステムが再コンパイルが必要かを決定する依存関係はモジュールのソースではなくモジュールのBMIに依存するように実装することでこれが実現できます。そして、コンパイラBMIにはモジュールからエクスポートされているインターフェースのみを保存しておき、その定義を保存しないようにする必要があります。GCC/MSVCは現在そのようにしていますが、Clangは2フェーズコンパイルモデルを実行する都合上そのようになっていないようです。

実行時のパフォーマンスについても、ヘッダファイルベースのライブラリをモジュールに移行することを考えた場合、ヘッダファイル内の関数はほぼインライン関数であるためこの提案の制約に接触せず、パフォーマンスの低下が発生する場合は限定されるとしています。また、LTOを使用することで翻訳単位を跨いだ定義のインライン化のような最適化が可能となるため、問題とされている最適化が全く利用できなくなるわけではありません。

P3034R0 Module Declarations Shouldn't be Macros

名前付きモジュールのモジュール宣言において、モジュール名のマクロ展開を禁止する提案。

モジュールのソースファイル形式は他のものと区別されておらず、あるファイルがモジュールソースであるかは、そのファイルの先頭にモジュール宣言があるかどうかによって決まります。モジュール宣言およびグローバルモジュールフラグメントの宣言はプリプロセッサによって導入することができませんが、モジュール名はマクロの展開によって指定することができます。

例えば、次の様なコードはC++20時点で有効です

// version.h
#ifndef VERSION_H
#define VERSION_H

#define VERSION libv5

#endif


// lib.cppm
module;
#include "version.h"
export module VERSION;

あるソースファイルがモジュールファイルであり、かつそのモジュール名が何であるかを知ることは、ソースの依存関係を知るために必要な作業です。従来のヘッダインクルードであれば、その依存関係を知らなくてもビルドを行うことができますが、モジュールの場合はビルドにあたってその依存関係を把握しソースのビルド順を決定する必要があります。これを行うのはコンパイラではなくビルドシステムの仕事であるため、ビルドシステムはソースファイルを読み込みそれがモジュールであるか、モジュールの場合はその名前は何かを読み取る必要があるかもしれません(これを行わなくてもいい方法がいくつか考案されていますが、まだ標準化等されてはいません)。

モジュール宣言はプリプロセッサによって導入されないものの、モジュール名はマクロの展開を完了させた上で読み取らなければなりません。そのためには、上記例のようにグローバルモジュールフラグメント内のインクルードやマクロ定義を読み込んだ上でマクロの展開を行わなければなりません。その作業は実装も処理も簡単なものとは言えず、ビルドシステムの実装および処理時間にかなりの負担になります。

この様な理由から、この提案はモジュール名もマクロによって導入できない様にする提案です。

その目的はモジュールファイルのパースを簡単にすることでビルドシステムがモジュール名パースを実装しやすくすることにあります。また、ビルドの前にモジュールの依存関係解決フェーズを行う場合に、パース処理が単純化されることで依存関係解決フェーズの遅延時間を短くすることもできます。

ただし、この変更はC++20への破壊的変更になります。提案では、モジュールの実装はまだ出揃っておらず使用例も稀であるため、影響は最小限である、としています。

この提案はSG15でもEWGでもほぼ反対なく支持されたようで、EWGではこの提案をC++20へのDRとするつもりの様です。

P3037R0 constexpr std::shared_ptr

std::shared_ptrを定数式でも使える様にする提案。

C++20で定数式における動的メモリ確保が可能になり、C++23でstd::unique_ptrconstexpr対応され定数式で使用できる様になりました。

スマートポインタは実行時と同様に定数式においてもメモリ管理を自動化することができます。しかし、std::shared_ptrはその実装に必要な言語機能の一部が定数式で使用可能ではなかったためすぐにconstexpr対応することができませんでした。

C++23におけるP2738R1(void*からの正しいポインタキャストの許可)とP2448R2(定数式で実行不可能なものは評価されるまではエラーにならない)の採択によりその障害は取り除かれており、この提案はそれを受けてC++26に向けてstd::shared_ptrconstexpr対応しようとするものです。

筆者の方はlibstdc++のstd::shared_ptr実装をベースとして実装を試みており、アトミック操作の使用を回避の必要性やstd::make_shared()などの行う1回のメモリ確保による初期化の問題などを報告していますが、いずれも回避は可能であり実装可能であるとしています。

また、この提案ではさらに、C++23ではconstexprで実装できなかったため外されていたstd::unique_ptrの比較演算子に対してもconstexprを付加することも提案しています。

P3038R0 Concrete suggestions for initial Profiles

既存のC++コードの一部により強力な保証を付加するためのプロファイルについての提案。

この提案は、P2687で提案されていたアイデアについて、より具体的な最初の機能について説明するものです。

プロファイルはC++コード上でユーザーによって指定されるもので、スコープもしくはモジュールに対して付加することができます。

// モジュール宣言にmemory_safetyプロファイルを適用
export module DataType.Array [[enforce(memory_safety)]];

// 名前空間宣言にプロファイルを適用
namespace N [[enforce(p)]] {
  ...
}

[[enforce(p)]]はプロファイルpをそのスコープに対して適用するもので、そのスコープの内側にあるコードに対してプロファイルpの保証が強制されます。モジュールの場合の適用範囲は、そのモジュール本文の全体です。

// モジュールMに対してプロファイルPを適用
import M [[enable(P)]];

// モジュールoldでtype_safetyプロファイルを無効化
import old [[suppress(type_safety)]];

プロファイルは既存コードに付加して保証を強化するものであり、[[enable(P)]]によって特にプロファイルを使用していないモジュールのインポート時にプロファイルを適用することができます。また、プロファイルはスコープに対して指定されある程度広い領域でその保証が強制されるため、[[suppress(P)]]によって部分的にプロファイルを無効化することもできます。

想定されるプロファイルにはいくつかの種類が考えられますが、この提案では実装負担の軽減のために最初の小さいものとしてtype_safetyプロファイルに焦点を当てています。与えられる保証は例えば

  • 変数初期化の強制
    • [[uninitilize]]とマークされない変数には初期化が必要
  • ポインタの利用の制限
    • ポインタは単一要素を指すか、nullptrであるかのどちらか
      • ポインタによる範囲のランダムアクセス禁止、その用途にはspanvectorを使用する
    • ownerとマークされていない限り、ポインタは所有権を持たない
      • ownerはポインタの先のオブジェクトを破棄する責任を負う
      • owner以外のポインタに対してnew/deleteできない
    • nullptrチェックなしのポインタアクセスの禁止
  • ダングリングポインタ(参照)の抑止
    • ポインタ(参照)はオブジェクトを指すか、nullptrのどちらか
    • ownerではないポインタはdeleteできない
    • 生存期間が不明なポインタを外側のスコープに漏出できない
    • returnできるポインタを制限する
  • ポインタ(参照)の無効化の防止
    • const参照によってコンテナを取得する関数では参照の無効化が発生する可能性があり、const参照によってコンテナを取得する関数では参照の無効化が発生しないと仮定
    • const参照によってコンテナを取得するがコンテナを変更しない関数では[[not_invalidating]]アノテーションによってそれを表明する
      • 間違った[[not_invalidating]]の利用は検出できるはずで、エラーにする

提案文書より、例

void f1() {
  int n;  // error

  [[uninitialized]]
  int m;  // ok
}

void f2(int* p1, owner<int*> p2) {
  delete p1; // error、ownerでは無いポインタをdeleteいている
  delete p2; // p2はdeleteしないとエラー
}

void f3(int* p1, owner<int*> p2) {
  p1=p2; // OK、p1はownerではないが、p2と同じオブジェクトを指す
  p2=p1; // error、p2は上書きされる前にdeleteされなければならない
}

int* glob = nullptr;
void f4(int* p) {
  glob = p; // error、不明な生存期間のポインタを保存しようとしている
}

int glob2 = 0;
int* f5(int* p) {
  int x = 4;
  return &x;          // error: ローカルオブジェクトへのポインタを返そうとしている
  return p;           // OK: pは関数呼び出し時に有効であり、無効化されていない
  return new int{7};  // error, ownerポインタを非ownerで返そうとしている
  return &glob2;       // OK 静的オブジェクトへのポインタ
  throw p;            // error: pを*pのスコープ外に漏出しうる
}

void f6(vector<int>& vi) {
  vi.push_back(9); // 要素の再配置が発生しうる
}

void f7() {
  vector<int> vi { 1,2 };
  auto p = vi.begin(); // viの最初の要素を指すイテレータ
  f6(vi); // 参照を無効化しうる関数呼び出し
  *p = 7; // error、参照が無効化されている可能性がある
}

提案では、このtype_safetyプロファイルに加えて、vector等の範囲に対するアクセスの境界チェックを行う実行時検査を伴うプロファイルであるrangesプロファイルや、組み込み数値演算の安全性向上(オーバーフロー防止、縮小変換・符号変換の禁止など)のためのプロファイルであるarithmeticプロファルなどを初期のプロファイルの候補として挙げています。

このようなプロファイルに基づく保証の提供はC++ Core Guidelineおよびそのチェッカー実装とガインドラインサポートライブラリの経験から来ています。それはあくまで静的解析としてC++コンパイルとは別でチェックされることでしたが、プロファイルとしてその保証をC++のコードに対して取り込むことで、既存のC++コードの上に被せる形でC++コードの安全性を高めることができ、プロファイルの指定は小さななスコープから始めることができます。

この提案のプロファイルとその静的な検査については、コアガイドラインチェッカーにて現在利用できるものであり、実装可能であることが確かめられています。また、この提案による安全性の静的検査は、コンパイラに強力なフロー解析などを強いるものではなく、危険を招く可能性のある操作を制限することで抑止するとともに、コンパイラの静的解析にいくつかの仮定を与えることで解析を補助する事を目指す物です。

P3039R0 Automatically Generate operator->

<=>演算子の様な書き換えによってoperator->を導出する提案。

この提案では、->->*演算子オーバーロード解決時に書き換えて実行することでこの2つの演算子を自動で導出できる様にすることを提案しています。それぞれ次の様になります

  • lhs->rhs(*lhs).rhsに書き換えて実行
  • lhs->*rhs(*lhs).*rhsに書き換えて実行

ライブラリソリューション(Boost.Operatorsのような)でこれと同じことを行おうとする場合、*lhsがprvalueを返す場合(例えばプロクシイテレータなど)に一時オブジェクトの寿命が->の定義内で尽きてしまうことによって未定義動作が発生する問題が回避できません。しかし、言語機能による演算子の書き換えはその様な問題を回避することができます(その場でインラインに置換される形になるので、->の呼び出しコンテキストと書き換え後の*lhsの生存コンテキストは一致する)。

また、比較演算子の場合は逆順の演算子や細かいコーナーケースを処理するためにその書き換えルールが複雑になっていますが、->->*はどちらも逆順を考慮する必要がなく、->はクラス内でのみ定義でき右辺のオペランドオーバーロード解決とは無関係となるため、書き換えに伴う仕様はかなりシンプルになります。

どちらの演算子でも、まずは->/->*として定義されたもの(delete含む)を優先して選択し、それが見つからずoperator*が利用可能である場合にのみ書き換えた候補を使用します。->を定義したいクラス側でdefault宣言しておく必要はなく、書き換えによって導出されたくない場合はdelete宣言をしておくことで書き換えを抑止できます。

これによるメリットは、主にイテレータ定義時の->に関する記述をほぼ完全に削除することができる点です。

提案では、<=>にならってこの提案が採択された場合に既存の->定義を削除するオプションについて検討されており、そこではC++20時点の標準ライブラリで->を持つクラスにおける定義のされ方を調査しています。それによれば、スマートポインタやstd::optional、コンテナのイテレータ型や一部のRangeアダプタのイテレータ型など、多数のクラス型において->定義を削除することができることが示されています。

ただし、std::iterator_traitspointerメンバ型の定義や、std::to_addressstd::pointer_traitsなどその動作について->演算子の存在に依存している部分があるライブラリ機能について、この提案の影響を回避する様にしなければなりません。それについてはいくつか方法が提示されているものの未解決です。

P3040R0 C++ Standard Library Ready Issues to be moved in Kona, Nov. 2023

11月に行われたKona会議でWDに適用されたライブラリに対するIssue報告の一覧

P3041R0 Transitioning from "#include" World to Modules

ヘッダファイルによるライブラリをモジュールベース変換する際の実装戦略についての報告書。

標準ライブラリモジュールstdと標準ヘッダファイルは同時にインポート/インクルードしたとしてもODR違反等を起こさず一貫して使用可能であることが規定されています。

#include <vector>
import std;

int main() {
  // vectorおよびそのメンバ関数実体は曖昧にならない
  std::vector<int> vee { 1, 2, 3, 4, 5 };
  return vee.size();
}

これは、通常のユーザーが定義できる名前付きモジュールでは得られない保証です。

ヘッダインクルードによるエンティティはグローバルモジュールに属しており、モジュールのエンティティは名前付きモジュールという翻訳単位に属しています。この2つのものは例え同じ名前で定義されていたとしても異なるものとして扱われるため、上記の様なコードをユーザー定義ライブラリでやると意図通りになるか曖昧になるかは実装次第となります。しかし標準ライブラリモジュールとヘッダファイルに関しては、これが確実に動作する(ヘッダファイルとstdモジュールとの対応する名前は同じ1つのエンティティを参照する)ことが規定され、要求されています。

これは、標準ヘッダと標準モジュールの両方を適用する必要がある現状においても両方を自然に同居させるための要求ですが、もしこの様な保証をユーザー定義の名前付きモジュールに対しても与えることができれば、ヘッダファイルとモジュールを同時に提供するライブラリの実装が可能になり、ヘッダからモジュールへの移行を促進することができます。

また、上記の様な標準ライブラリの保証を実現する実装戦略は、グローバルモジュール(ヘッダファイル)のエンティティに対してstdモジュールのエンティティを対応づけるような形になる様ですが、これは名前付きモジュールのいくつかの利点を犠牲にしています。

この報告書は、ヘッダとモジュールを同時に提供しながら名前付きモジュールの利点を余すところなく享受し、なおかつそれを任意のC++ライブラリで利用可能にする実装戦略について説明するものです。

この戦略は、ビルド定義とコンパイラが連携してBMIマッピング#include変換を組み合わせることで、現在のstdモジュールの保証を実現するものです。標準ライブラリヘッダに関しては、ビルドは次の様に行われます

  1. 標準ライブラリヘッダのインクルードをヘッダユニットのインポートへ変換
  2. 全ての標準ヘッダユニットに対して、stdモジュールのBMIを使用する様にコンパイラへ指示する
  3. 標準マクロを強制的にインクルードする

マクロに関してはこの方法では導入できないため、別途(コマンドライン等から)インクルードする必要があります。この文書では、C互換ではない標準ヘッダが提供する必要のあるマクロをまとめたヘッダファイルを用意しそれをインクルードする事を推奨しています。

この実装戦略は標準モジュールに対してのものですが、より一般のC++ライブラリに対しても適用可能です。ただし、そのためには次のようなものが必要です

  1. あるヘッダが名前付きモジュール(または別のヘッダ)に含まれていることを記述する機能
  2. ヘッダのインクルードをヘッダユニットのインポートへ変換する機能
  3. 2のヘッダユニットのBMIを包含モジュールのBMIマッピングする機能
  4. ヘッダファイルで導入されるはずのマクロを強制的にインクルードする機能

現在のところこれらの機能のいずれも非標準ライブラリに対しては提供されていません。

P3042R0 Vocabulary Types for Composite Class Design

P3019の紹介スライド。

std::indirectstd::polymorphicのモチベーションや設計要求などについて丁寧に説明されています。おそらく、提案を見るよりも分かりやすそうです。

P3043R0 Slides: Using variable template template without meta programming

変数テンプレートテンプレートの動機付けについて説明する文書。

lldにあるコードを簡略したものを整理することを例にとって、変数テンプレートテンプレート(変数テンプレートを受け取るテンプレートパラメータ)の必要性を説明しています。ただし、これ自体は何かを提案しているわけではありません。

これを可能とする提案としては例えばP2989があります。

P3046R0 Core Language Working Group "ready" Issues for the November, 2023 meeting

11月に行われたKona会議でWDに適用されたコア言語に対するIssue報告の一覧。

P3050R0 Optimize linalg::conjugated for noncomplex value types

std::linalg::conjugated()を非複素数型に対してアクセサの変更をしないようにする提案。

std::linalg::conjugated()複素数配列を参照するmdspanの各要素を、その複素共役となるように変換する関数です。ただし、戻り値もmdspanで返され、変換はmdspanのアクセサポリシーを変更することで行われます。従って実際の要素は変更されず、mdspanからの要素参照時に引き当てられた要素に対して複素共役への変換を行うことで配列全体の変換を行います。

namespace std::linalg {

  // conjugated()の宣言例
  template<class ElementType,
           class Extents,
           class Layout,
           class Accessor>
  constexpr auto conjugated(mdspan<ElementType, Extents, Layout, Accessor> a);
}

std::linalg::conjugated()の現在の動作は次のようになっています

  • 入力mdspan<T, E, L, A>のアクセサ型Aconjugated_accessor<NestedAccessor>NestedAccessorは任意の他のアクセサ型)である場合、mdspan<NestedAccessor::element_type, E, L, NestedAccessor>を戻り値型として入力aの領域とレイアウトをそのまま渡して返す
  • それ以外の場合、mdspan<T, E, L, conjugated_accessor<A>>を戻り値型として入力aの領域とレイアウトをそのまま渡して返す
    • 複素共役を行うアクセサポリシーconjugated_accessorで元のアクセサをラップする

conjugated_accessor<A>はアクセサポリシーAをラップして、Aで定義されたアクセス結果に対して複素共役変換を適用して返すアクセサポリシー型です。

conjugated_accessor<A>による変換は、conj-if-needed()という説明専用の関数によって行われ、conj-if-needed(c)cに対するADLによって非メンバconj()が使用可能であればそれを使用して複素共役を取得し、それが見つからない場合はcをそのまま返します。これによって、cおよびmdspanの要素型が複素数型ではない場合はこの変換は最適化によってスキップされることが期待できます。

しかし、その呼び出し階層を削除することができたとしても、conjugated_accessor型の存在を削除することはできません。

mdspanを扱う多くのユーザーは関数等でmdspanを受け取る場合デフォルトのポリシーを使用して型を記述し、特にアクセサ型を変更する形で記述されることは稀だと思われます(すなわち、要素型Tに対してdefault_accessor<T>が専ら使用されるはず)。

そのようなユーザーは<linalg>の主機能であるBLASラッパを使用しないとしても、std::linalg::conjugated()などのユーティリティは使用することになるでしょう。そして、自身の持つmdspanstd::linalg::conjugated()に通すと、その要素型がなんであれアクセサポリシーが変更されたmdspanが得られ、デフォルトのアクセサを使用したmdspanを受け取るように定義された関数に対してそれを渡すとコンパイルエラーに遭遇するでしょう。

std::linalg::conjugated()複素数要素に対して作用するためこれは回避不可能なものであるといえるかもしれません。しかし、BLASそのものやMatlab等では、転置と共役転置(随伴)の操作は同じものとして統合されており、<linalg>でもconjugate_transposed()が用意されている他、conjugated(transposed(x))のように書かれることもあるでしょう。これらの関数に対する入力はその要素型が浮動小数点数型か複素数型かを意識せずに渡されるはずで、この場合に非複素数要素型のmdspanに対してアクセサ型の変更が行われることは驚きを伴う可能性があります。

これに対応するにはアクセサポリシー型をジェネリックにしなければならず、それはコンパイル時間の増大を招くとともに、デフォルトアクセサを仮定する最適化を行えなくなることを意味します。

LWGにおけるP1673のレビュー中にこの問題が指摘され、そのままでも致命的な問題ではなかったためP1673はそのままレビューされC++26 WDにマージされました。この提案は、改めてこの問題を解決するために提出されました。

この提案による変更は、std::linalg::conjugated()がアクセサポリシーをconjugated_accessorに変更しようとする場合(共役の共役とならない場合)にその要素型が複素数型ではないならば入力のmdspanをそのまま返すようにします。これによって、次の2点の変更は観測可能となります

  • std::linalg::conjugated()の戻り値型は必ずしもconjugated_accessor<A>ではなくなる
  • 複素数型の要素型のmdspanに対して、戻り値型のmdspanconst element_typeを持たなくなる

これは、conjugated()の呼び出しは常にconst element_typeを持つわけではないことや、結果を他の関数に渡しているコードの呼び出しが壊れるわけではないことなどから許容されるとしています。

P3051R0 Structured Response Files

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

現在、いくつかのコンパイラはそのオプションをファイルにまとめて指定する方法を提供しています。それはよく似た方法で行われていますが、コンパイラ間で互換性はなく、相互運用が可能なものではありません。例えばファイルを渡すオプション名が異なり、ファイル形式もバラバラです。

そのような方法を標準化することで、ツールが他のツールへ(例えばビルドシステムからコンパイラへ)そのコマンドラインオプションを渡すことが容易になり、ツール間の相互運用性が向上します。また、そのような一貫した方法/フォーマットはツールに対する一貫した共通オプションのようなものを定義するための下地にもなります。

この提案は、コマンドラインオプションをまとめたファイルのフォーマットとそれを受け渡す方法について提案するものです。

提案ではファイルの形式としてJSONファイルとすることを提案しています。

そして、そのファイルにツールのオプションを記録する方法として、引数とオプションの2つのスタイルを提案しています。引数はコマンドラインオプション文字列をそのまま記録するもので、オプションは実際のコマンドラインオプションに対応するより概念的な指定となるものです。

提案より、それぞれの表現例

// 引数の例
{
  "arguments": [
    "-fPIC",
    "-O0",
    "-fno-inline",
    "-Wall",
    "-Werror",
    "-g",
    "-I\"util/include\"",
    "-c"
  ]
}

// オプションの例
{
  "options": [
    "fPIC",
    { "O": "0" },
    "fno-inline",
    { "W": [ "all", "error" ] },
    "g",
    { "I": [ "util/include" ] },
    "c"
  ]
}

引数のスタイルは既存のコマンドラインオプション構文に直接対応しており移行しやすいものですが、ツール依存になります。オプションは既存のコマンドラインオプション構文をより抽象化したもので、ツール間のオプション構文の差異を吸収できる可能性があります。また、これら2つのスタイルは1つのファイル内に同居することができます。

それぞれの利点/欠点

  • 引数
    • 利点
      • 既存のJSON compilation databaseをベースとしており、これをパースする実装は既に存在している
      • ツールのコマンドラインオプションとの直接のマッピングがあり、サポートに労力がかからない
    • 欠点
      • オプションとその値を取得するにはパースが必要
      • 通常のコマンドラインオプションと同じ制限を受ける
  • オプション
    • 利点
      • オプション名はオプションのプリフィックス- -- /など)を省略しているため、ツールに依存しない共通名を使用できる
      • オプション値に配列やオブジェクトを利用できることで論理的なグループ化が可能になり、コマンドライン引数のパースで発生するような追加の引数マージ処理のようなものが不用になる
    • 欠点
      • 既存ツールはこの新しいオプション構文を読み取る実装が無い
      • JSON compilation databaseでもこの形式が採用される場合、さらに追加の作業が発生する

そして、このファイルを指定するコマンドラインオプションとしてstd-rspを提案しています。

tool --std-rsp=file
tool -std-rsp:file

実際のファイル全体は例えば次のようになります

{
  "$schema": "https://raw.githubusercontent.com/cplusplus/ecosystem-is/release/schema/std_rsp-1.0.0.json",
  "version": "1",
  "arguments": ["-fPIC", "-O0", "-fno-inline", "-Wall", "-Werror", "-g", "-I\"util/include\"", "-c" ]
}

提案では、オプションスタイルの場合の各オプション名について、実際のコマンドライン引数名に対応させるか、より抽象的な名前にするかについて未解決としています(例えば、W, o, Iwarning, output, include)。これにも利点欠点があるため、どちらを選択するかやこの提案でそれを追求するかについてはSG15の決定に委ねています。

P3052R0 view_interface::at()

view_interfaceat()メンバ関数を追加する提案。

C++26でP2821が採択されたことでstd::spanでもインデックスアクセスにat()が使用できる様になり、既存の標準コンテナ等とのインターフェースの一貫性が向上しています。これにより、標準にある2つのview型(spanstring_view)でat()が使用できる様になったため、これをより汎用的なview型でも一貫させることの根拠が生まれました。

残りのview型とは<ranges>の各種view型(subrangeやRangeアダプタのview型)のことで、これらの型はその共通インターフェースをview_interfaceというCRTPベースクラスを継承することで提供しています。

この提案は、インデックスアクセスの安全性とインターフェースの一貫性を向上させるために、view_interfaceat()メンバ関数を追加しようとするものです。

namespace std::ranges {
  template<class D>
    requires is_class_v<D> && same_as<D, remove_cv_t<D>>
  class view_interface {
    ...
  public:
    ...
    
    // 現在の添字演算子オーバーロード
    template<random_access_range R = D>
    constexpr decltype(auto) operator[](range_difference_t<R> n) {
      return ranges::begin(derived())[n];
    }

    template<random_access_range R = const D>
    constexpr decltype(auto) operator[](range_difference_t<R> n) const {
      return ranges::begin(derived())[n];
    }

    // 提案するat()
    template<random_access_range R = D>
      requires sized_range<R>
    constexpr decltype(auto) at(range_difference_t<R> n);

    template<random_access_range R = const D>
      requires sized_range<R>
    constexpr decltype(auto) at(range_difference_t<R> n) const;
  };
}

このat()はコンテナ等のそれと同様に動作し、指定されたインデックスが範囲外参照となる場合にout_of_range例外を送出するものです。view_interfaceで提供されることで、<ranges>のほぼ全てのview型で使用可能になります。

P3053R0 2023-12 Library Evolution Polls

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

次の19個の提案が投票にかけられます

後ろの2つを除いて、残りのものはC++26に向けてLWGに転送するための投票です。

P3055R0 Relax wording to permit relocation optimizations in the STL

リロケーション操作による最適化を標準ライブラリのコンテナ等で許可するために、標準の規定を緩和する提案。

C++26に向けてリロケーション(relocation)操作を言語に導入する議論が進んでいます。リロケーションは意味的にはムーブ+破棄に相当し、ムーブした直後にムーブ元オブジェクトを破棄する操作をひとまとめにしたものです。中でも、トリビアルリロケーションはmemcpyによってオブジェクト表現(ビット列)をコピーするだけで行うことができます。

リロケーション後の元のオブジェクトはコード上で使用不可になるという性質から、一部のムーブを伴う操作はリロケーションによって効率化できる可能性があります。特に、標準コンテナの操作やアルゴリズムに関わる操作などにおいて最適化を促進することが期待されています。

仮にリロケーション操作が言語に入った時に問題となるのは、それらコンテナやアルゴリズムの規定、特に計算量の規定がリロケーションではなくムーブを前提として指定されていることです。たとえば、std::vector::erase()をリロケーションによって書き換えると次の様な実装になるでしょう

void erase(iterator it) {
  if constexpr (std::is_trivially_relocatable_v<value_type>) {
    std::destroy_at(std::to_address(it));
    std::uninitialized_relocate(it + 1, end_, it);
  } else {
    std::ranges::move(it + 1, end_, it); // operator=
    std::destroy_at(std::to_address(end_ - 1));
  }
  
  --end_;
}

しかし、std::vector::erase()の計算量の指定は、「要素型Tのデストラクタは消去された要素の数と等しい回数呼ばれ、Tの代入演算子は消去された要素の後にある要素の数と等しい回数呼ばれる」と規定されています。トリビアルリロケーションの場合、要素のムーブはそのオブジェクト表現のコピーのみで元オブジェクトの破棄は行われない(オブジェクトの配置場所が変わるだけでオブジェクトそのものは何ら変化しない)ため、std::vector::erase()ではリロケーション操作が利用可能になったとしても標準の範囲内でそれを利用することができません。

このような規定がなされているものがコンテナの操作やアルゴリズムに関して存在しており、これがある限り言語にリロケーションが導入されても標準ライブラリはそれを活かすことができません。しかし、これを取り除いておけばリロケーションの到来と関係なく、標準ライブラリはトリビアルリロケーション可能であると現在わかっている型(std::deque<int>など)についてそのような最適化を行うことができます。

この提案は、その様な過剰な制限を強いてしまっている現在の規定をリストアップし、それをトリビアルリロケーションをサポート可能なように緩和しようとするものです。

提案する変更は、全て現在の制限を若干緩めるものなので、既存の実装がこれを受けて何か変更する必要があるものではありません。たとえば、先ほどのstd::vector::erase()の計算量の規定の場合、「元のvector上で削除された一番先頭にある要素の後にある要素の数について線形」の様に変更しています。これによって、特定の操作に対して計算量を指定する事を回避しています。

P3056R0 what ostream exception

現在例外を投げる際に避けることのできない動的メモリ確保を回避する提案。

標準ライブラリにあるstd::exception派生クラスは.what()メンバ関数からエラーメッセージを返すためにそのコンストラクタで文字列を受け取りますが、動的に構成した文字列をstd::stringに保持している状態で渡そうとする場合、文字列をコピーして受け取る以外の選択肢がありません。

void f(int n) {
  std::string err_msg = std::to_string(n);
  std::runtime_error err{err_msg};  // コピーされる

  throw err;
}

これは、std::exception派生クラスのstd::stringを受け取るコンストラクタはconst std::string&を受け取るものしかないためです。

また、このように例外が発生するコンテキストでエラーメッセージを動的に構成する場合、その作業そのものに伴って動的メモリ確保が発生しています。例えば例外の.what()が呼ばれない場合、このコストは余分なものとなります。

この提案は、std::exception派生クラスおよびstd::exceptionに2種類のメンバ関数を追加することによって、この2つの動的メモリ確保を回避もしくは遅延させ、ライブラリ実装者およびそのユーザーが動的メモリ確保を制御できる様にしようとするものです。

追加するのは次の2つです

  1. std::exception派生クラスのコンストラクタにstd::string&&を受け取るコンストラクタを追加する
  2. std::exception.what()オーバーロードとして、std::ostream&を受け取りエラーメッセージの構築と出力まで行うオーバーロードを追加する

1つ目の変更によって、std::exception派生クラスにエラーメッセージのstd::stringをムーブ渡しできる様になり、コピーに伴う動的メモリ確保を回避することができます。

void f(int n) {
  std::string err_msg = std::to_string(n);
  std::runtime_error err{std::move(err_msg)};  // ムーブされる

  throw err;
}

2つ目の変更ではさらに、例外オブジェクト内部に必要な情報を保持しておき、エラーメッセージが必要になったタイミングでエラーメッセージをオンデマンドに構成することが可能になります。

class runtime_error_v2 : virtual public runtime_error_v2 {
private:
    const int m;
    const std::source_location location;
public:
  runtime_error_v2(int n, const std::source_location location)
    : m{n}
    , location{location}
  {}

  virtual std::ostream& what(std::ostream& os) const noexcept override {
    // 呼ばれてからメッセージを構成する
    return os << "file: "
        << location.file_name() << '('
        << location.line() << ':'
        << location.column() << ") `"
        << location.function_name() << "`: "
        << "value: " << n << '\n';
  }
};

void f(int n) {
  std::runtime_error err{n, std::source_location::current()};
  throw err;
}

P3057R0 Two finer-grained compilation model for named modules

名前付きモジュールの依存関係管理について、より細かい単位で依存関係を管理する方法についての報告書。

名前付きモジュールでは、ヘッダファイルとは異なり個々のモジュールが1つの翻訳単位を成しているため、プログラム全体をビルドするためにはその依存関係を把握した上で依存関係の根本から順番にビルドしていく必要があります。そのため、インクリメンタルビルド等においては、ある1つのファイルの変更がより多くのモジュールや翻訳単位のリビルドを引き起こす可能性があります。

この文書は、この問題を軽減するために、より細かい単位で依存関係管理を行うコンパイルモデルを説明するものです。

この文書で挙げられているモデルは2つあります。

  1. 使用したファイルベースのソリューション
    • あるソースファイルのコンパイル中に使用されたソースファイルを記録しておき、2回目以降のビルドでは自身及び使用したファイルが変更されていなければ再コンパイルを省略する
    • この場合の使用されたかされていないかは、ファイルのインポートやインクルードではなく、その中身の宣言が使用されているかによって判定される
    • ここでのファイルの変更は、ファイルシステムにおける変更によって判定する
  2. 宣言のハッシュによるソリューション
    • あるソースファイルのコンパイル中に、そこで使用されている宣言のハッシュを記録しておき、2回目以降のビルドでは記録した宣言ハッシュを比較して変更がなければ再コンパイルを省略する
    • ハッシュの計算と比較のコストやビルドシステム側での対応など課題がある

この2つの方法はClangのプラグインを通して既に試すことができるようで、文書中でも実際のデモの様子が報告されています。

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

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

<ranges>にある各種のview型は、その動作の実装のほとんどの部分をイテレータによって行なっています。その様なイテレータは構築時に親のviewを受け取りそのポインタを保存しますが、そのコンストラクタは親のview型からアクセス可能であれば良いはずで、他のところからアクセスできる必要はありません。

現在のところ、標準のRangeアダプタのview型のイテレータのその様なコンストラクタは、ものによってアクセスできたりできなかったりします。

int main() {
  auto base = std::views::iota(0);
  auto filter = base | std::views::filter([](int) { return true; });

  // 内部イテレータ型のコンストラクタが呼べる(場合もある)
  auto begin = decltype(filter.begin())(filter, base.begin()); // ok
  auto end   = decltype(filter.end()  )(filter);               // ok
}

この提案は、この様なコードはエラーとなるべきで、標準のRangeアダプタのview型のイテレータのコンストラクタは一部を除いて公開されるべきではない、とするものです。

上記のコードは実はGCCにおいてはエラーになります。それは、GCCfilter_viewイテレータの実装が親のfilter_viewを参照ではなくポインタで受け取る様になっているためです。実装の観点からは、これによってfilter_viewbegin()内では構築時にthisを渡すだけですみ、イテレータ側もaddresof()の呼び出しを適用する必要がなくなります。

現在の規定に照らせばGCCのこの実装は間違っていますが、これはviewの実装詳細の部分であり、本来公開されるべきではないものが公開されていることによる副作用と見なすことができます。また、このGCCfilter_viewにおけるイテレータの実装は、chunk_viewイテレータにおいては規格でそのように指定されており一貫していません。このことからも、これらのコンストラクタは公開されないのをデフォルトにするのが望ましいと言えます。

この提案の対象はあくまで実装のために使用されるコンストラクタを非公開化するもので、デフォルトコンストラクタやムーブコンストラクタ、変換コンストラクタなどを非公開にしようとするものではありません。

P3060R0 Add std::ranges::upto(n)

0から指定した数の整数シーケンスを生成するRangeアダプタ、views::uptoの提案。

この提案のviews::upto(n)views::iota(0, n)と同じシーケンスを生成します

import std;

int main() {
  // iota(0, n)
  for (int i : std::views::iota(0, 10)) {
    std::println("{} ", i);
  }

  std::println("");

  // upto(n)
  for (int i : std::views::upto(10)) {
    std::println("{} ", i);
  }
}

どちらも0 1 2 3 4 5 6 7 8 9が出力されます。

このため実装はごく簡単に行うことができます

namespace std::ranges {
  // ranges::upto 実装例
  inline constexpr auto upto = [] <std::integral I> (I n) {
    return std::views::iota(I{}, n);
  };

  namespace views {
    using ::std::ranges::upto;
  }
}

これだけだとiotaで十分にしか見えませんが、uptoの意義は符号なし整数型で同じことをする場合の微妙な使用感の悪さを改善することにあります。

void f(const std::vector<int>& vec) {
  auto seq1 = std::views::iota(0, vec.size());  // ng
  auto seq2 = std::views::upto(vec.size());     // ok
}

整数値a, ba < bとして)によってviews::iota(a, b)の様にする場合、a, bの型は異なっていても構いませんが少なくとも符号有無は一致している必要があります(これは、iota_viewの推論補助の制約によって要求されます)。この様な制約は、符号有無が混在した整数型の比較が暗黙変換の結果として意図通りにならなくなる場合があり、それを防止するためのものです。

そのため、上記例のように符号有無が混在した整数値によって指定するとコンパイルエラーとなります。正しくはviews::iota(0u, vec.size())とすべきですが、出力されるエラーメッセージも難しくこの原因を推察するのは容易ではありません。

uptoはシーケンス終端の整数値を1つ指定するだけで、先頭の値はその整数型をデフォルト構築して(0が)補われるため、この問題を回避することができます。

また、同じシーケンスを生成する際にはわずかではありますがviews::iotaよりも短く書くことができ、その意図も明確になります。

P3061R0 WG21 2023-11 Kona Record of Discussion

2023年11月に行われたKona会議の全体会議における議事録。

会議期間中の各SGの作業報告や、LWG/CWGを通過した提案の投票の様子が記載されています。

P3062R0 C++ Should Be C++ - Presentation

P3023の紹介スライド。

EWG/LEWGのメンバに向けてP3023の主張を紹介したものです。

プレゼンテーション用のスライドなので、文章よりも行間が補われている部分があり、主張が分かりやすくなっています。

P3066R0 Allow repeating contract annotations on non-first declarations

関数の最初の宣言にのみ契約注釈を行えるという制限を撤廃する提案。

現在C++26に向けて議論が進められている契約プログラミング機能においては、関数に対する事前条件・事後条件は関数の最初の宣言でのみ行うことができ、たとえ内容が同じだったとしても再宣言で契約注釈を指定する(あるいは再宣言のみで契約を行う)ことはできません。

// 最初の宣言(ヘッダ内など)
int f(const int n)
  pre(n < 100)
  post(r: r == n);

int g();


// 再宣言、f()の定義(翻訳単位内)
int f(const int n)
  pre(n < 100)      // ng
  post(r: r == n)   // ng
{
  return n;
}

int g()
  post(r: -10 < r)  // ng
{
  return 20;
}

これは、同じ関数に対して異なる翻訳単位で異なる契約注釈が行われてしまうことを防止するための制限です。

関数の宣言と定義がヘッダファイルと実装ファイルに分割されている場合、多くのユーザーはヘッダに書いた宣言をコピペして実装ファイルにおける定義を書き始めますが、その関数に契約がなされている場合契約注釈を削除しないとコンパイルエラーになることになります。これは驚くべき動作かもしれません。

クラスのメンバ関数の定義など、関数の宣言と定義が離れていてそこで使用されるエンティティが直接的に見えていない場合、契約注釈がそれを表示しなおかつそれが繰り返されることでコードの可読性を向上させられる可能性があります。

// Widget.h
struct Widget {
  int f() const noexcept
    pre(i > 0);
  
  ...
  
  // much further below:
private:
  int i;
};


// Widget.cpp
int Widget::f() const noexcept
  pre(i > 0)
{
  return i * i; // using i here!
}

元々、C++20で一旦導入されていた契約プログラミング機能では、契約注釈のリストが同じであるという制約の下で再宣言でも契約注釈を行うことができました。初期のMVPにもこれは受け継がれていましたが、後ほど削除されました。なぜなら、当初の仕様では契約注釈のリストについての同一性の定義がなく、どのように同一であるとするのかが不明だったためです。GCCの契約機能の実験実装(C++20の機能ベース)では契約条件式のODRベースの同一性を判定して実装されている様です。

しかし後で、異なる翻訳単位で同じ関数の最初の宣言が複数含まれるプログラムがwell-formeddであるかを指定する必要が出てきたことで、結局契約注釈の同一性の定義を行わなければならなくなった様です。そのため、これは解決すべき問題としてリストアップ(P2896R0)されており、その解決はP2932R2で提案されています。

この提案は、P2932R2で提案されている契約注釈の同一性の定義を採用することで、最初の宣言にある契約注釈を後の宣言で繰り返すことができる様にしようとするものです。

P2932R2で提案されている契約注釈の同一性の定義は次の様なものです

関数宣言d1の契約注釈c1と関数宣言d2上の契約注釈c2は、仮引数名・戻り値名・テンプレートパラメータ名が異なることを除いて、 その述語(契約条件式)p1, p2がそれぞれ宣言d1, d2に対応する関数定義上に置かれた場合にODRを満たしているならば、 c1c2は同じ契約注釈である

この提案はこの定義を採用した上で、C++20時点の仕様だった、関数の後の宣言は最初の宣言と同じ契約注釈を指定するか契約注釈を省略するかのどちらかを行う、というものを復活させることを提案しています

この提案ではあくまでこのことだけを提案していて、最初の宣言で契約注釈を省略して後の宣言でのみ指定する、ことを可能にすることは提案していません。

P3070R0 Formatting enums

列挙型の値を簡単にstd::formatにアダプトさせるための、format_asの提案。

C++23時点のstd::format()std::print)で自前の列挙型を出力しようとする時、主に2つの方法があります。

namespace kevin_namespacy {

  // フォーマットしたい列挙型
  enum class film {
    house_of_cards, 
    american_beauty,
    se7en = 7
  };
}

// 1. フォーマッター特殊化を定義
template <>
struct std::formatter<kevin_namespacy::film> : formatter<int> {
  auto format(kevin_namespacy::film f, format_context& ctx) {
    return formatter<int>::format(std::to_underlying(f), ctx);
  }
};

int main() {
  using kevin_namespacy::film;

  film f = film::se7en;

  // 2. 基底の整数値を出力
  auto s = std::format("{}", std::to_underlying(f));
}

1つはその列挙型のためにstd::formatterを特殊化してフォーマット方法を定義することです。ただし、この例のように整数型のフォーマッタを再利用したとしてもそれなりの量のボイラープレートコードの記述が必要となります。また、フォーマット方法の定義を同じ名前空間で行うことができず、列挙型とそのフォーマッタの定義が空間的に別れてしまいます。

もう1つはstd::to_underlying()によって列挙値に対応する整数値を取得してそれを出力する方法です。これはフォーマット時に常にstd::to_underlying()の呼び出しが必要となります。

この提案は、これらの方法の欠点を改善したフォーマットのカスタマイズ方法を提案するものです。

この提案では、std::formatに対してformat_as()というカスタマイズポイントを導入することを提案しています。

namespace kevin_namespacy {
  // フォーマットしたい列挙型
  enum class film {
    ...
  };

  // filmのためのカスタムフォーマット定義
  auto format_as(film f) {
    return std::to_underlying(f);
  }
}

format_as()std::format()呼び出し内からADLによって発見される関数であり、フォーマット対象の値(ここでは列挙値)を受け取ってそれを既にフォーマット可能な他の型の値(整数値や文字列など)に変換して返すようにしておく必要があります。

この方法のメリット・目的は次の様なものです

  • 列挙型のためのフォーマットカスタマイズ方法の単純化
  • 列挙型のフォーマット効率を向上
    • 既にフォーマッタ特殊化が存在する場合、フォーマッタを経由しないことでフォーマットのパフォーマンスを向上させられる
  • 後方互換性を確保し、std::formatへの移行を促進する

提案より、他の例

enum class color {
  red,
  green,
  blue
};

auto format_as(color c) -> std::string_view {
  switch (c) {
    case color::red:   return "red";
    case color::green: return "green";
    case color::blue:  return "blue";
  }
}

auto s = std::format("{}", color::red); // s == "red"

この提案ではこれを列挙型に限って有効化することを提案していますが、この仕組みはより一般の型に対して拡張可能です。実際に、{fmt}ライブラリではこの仕組みが列挙型に限らず一般の型に対して有効化された上で出荷されています。

P3071R0 Protection against modifications in contracts

P3071R1 Protection against modifications in contracts

契約注釈内から参照されるローカル変数と関数引数は暗黙的にconstとして扱われるようにする提案。

現在のContracts MVP仕様では、契約注釈内での意図しないプログラム状態の変更に対する保護が欠けているため、それを追加しようとする提案です。次のようなことを提案しています

  • contract context(契約コンテキスト)は契約注釈内の条件式
    • その文法はP2961R2で提案されているnatural syntaxのもの
  • 契約コンテキストの部分式であり、オブジェクト型Tの自動変数、または自動変数に対するT型の構造化束縛を指名するid式は、const T型の左辺値(lvalue
  • 契約コンテキストの部分式であり、自動変数であるTの参照を指名するid式は、const T型の左辺値(lvalue
  • 契約コンテキストの部分式であるラムダ式がコピーによって非関数エンティティをキャプチャする場合、暗黙に宣言された(クロージャオブジェクトの)メンバ型はTだが、ラムダの本体内でそのようなメンバを指名するとラムダがmutableでない限りconst左辺値が返される(これは通常通り)
    • ラムダ式が参照によってそのようなエンティティをキャプチャする場合、その参照を指名するid式は、const T型の左辺値(lvalue
  • 契約コンテキストの部分式で現れるthis式はcv Xへのポインタを示すprvalue
    • cvconstと囲むメンバ関数のCV修飾(存在する場合)との組み合わせ
    • この場合の契約コンテキストの部分式には、非静的メンバ関数の本体内での暗黙変換の結果を含む
  • 契約コンテキストの部分式であるラムダ式Tへのポインタであるthisをキャプチャする場合、暗黙に宣言された(クロージャオブジェクトの)メンバ型はconst Tへのポインタ

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

int global = 0;

int f(int x, int y, char *p, int& ref)
  pre((x = 0) == 0)            // proposal: ill-formed、const左辺値への代入
  pre((*p = 5))                // OK
  pre((ref = 5))               // proposal: ill-formed、const左辺値への代入
  pre(std::same_as_v<decltype(ref), int&>)  // OK; 結果はtrue
  pre((global = 2))            // OK
  pre([x] { return x = 2; }())           // error: xはconst
  pre([x] mutable { return x = 2; }())   // OK, 関数引数xのコピーを変更する
  pre([&x] { return x = 2; }())          // proposal: ill-formed、const左辺値への代入
  pre([&x] mutable { return x = 2; }())  // proposal: ill-formed、const左辺値への代入
  post(r: y = r)               // error: yはconstではないので事後条件で使用できない
{
  contract_assert((x = 0));    // proposal: ill-formed、const左辺値への代入
  int var = 42;
  contract_assert((var = 42)); // proposal: ill-formed、const左辺値への代入

  static int svar = 1;
  contract_assert((svar = 1)); // OK
  return y;
}

struct S {
  int dm;

  void mf() /* not const */
    pre((dm = 1))                         // proposal: ill-formed、const左辺値への代入
    pre([this]{ dm = 1; }())              // proposal: ill-formed、const左辺値への代入
    pre([this] () mutable { dm = 1; }())  // proposal: ill-formed、const左辺値への代入
    pre([*this]{ dm = 1; }())             // error: ill-formed、const左辺値への代入
    pre([*this] () mutable { dm = 1; }()) // OK, *thisのコピーを変更
  {}
};

proposalとコメントされているところがこの提案によって動作が変更されるところです。メンバ関数の契約注釈からthisを使用する場合、それはconstメンバ関数内であるかのように扱われます。

この提案は既にSG21においてMVPに採用することに合意されているようです。

P3072R0 Hassle-free thread attributes

スレッドへの属性指定APIについて、集成体と指示付初期化によるAPIの提案。

P2019ではstd::thread/jthreadに対してスレッド属性(のうちスレッド名とスタックサイズ)を指定できるようにすることを提案しています。そのAPIについては揺れているようで、P2019のリビジョン毎に変化している他、P3022では既存のライブラリに倣った異なるAPIが提案されています。

この提案は、P2019とP3022とも異なるAPIを提案するものです。

P2019R4では、属性ごとに異なる型を用意して、スレッドのコンストラクタ先頭でそれを受け渡します。P3022では、1つのスレッド属性クラスに全てのスレッド属性をまとめて、それをコンストラクタ先頭で受け渡します。

この提案は、P3022のアプローチに近いものですが、P3022とは異なりスレッド属性クラスを集成体として、それをコンストラクタ先頭で渡します。

// P2019R4
std::jthread thr(std::thread_name("worker"),
                 std::thread_stack_size_hint(16384),
                 [] { std::puts("standard"); });

// P3022R0
std::jthread::attributes attrs;
attrs.set_name("worker");
attrs.set_stack_size_hint(16384);

std::jthread thr(attrs, [] { std::puts("standard"); });

// この提案
std::jthread thr({.name = "worker", .stack_size_hint = 16384},
                 [] { std::puts("standard"); });

この提案のAPIは、P3022の利点(属性を1つにまとめられる、既存の慣行に従っている)という点を受け継ぎながら、よりユーザーにとって使いやすい構文で属性を指定することができます。

この実装はまず、std::thread内部にスレッド属性クラスを定義したうえで

class thread {
  ...

public:

  // スレッド属性集成体
  struct attributes {
      std::string const &name = {};
      std::size_t stack_size_hint = 0;
  };

  ...
};

これを受け取るコンストラクタをstd::threadstd::jthreadに追加します

class thread {
  ...

  // 追加するコンストラクタ
  template<class Attrs = attributes, class F, class... Args>
    requires std::is_invocable_v<F, Args...>
  explicit thread(Attrs, F &&, Args &&...);

  ...
};

class jthread {
  ...

  // 追加するコンストラクタ
  template<class Attrs = thread::attributes, class F, class... Args>
    requires std::is_invocable_v<F, Args...>
  explicit jthread(Attrs, F &&, Args &&...);

  ...
};

このようにすることで、先程のようにコンストラクタ引数内での指示付初期化による属性指定が可能になります。さらに、ベンダ定義の独自属性指定を使用することもできます。

std::thread t5(__gnu_cxx::posix_thread_attributes{.schedpolicy = SCHED_FIFO},
               std::puts, "vendor extension");

また、将来属性が増えた場合は新しい属性クラスを用意して、これらのコンストラクタのデフォルトテンプレートパラメータを差し替えることでAPI/ABIの互換性を保ったまま拡張することができます。

P3074R0 constexpr union lifetime

定数式において、要素の遅延初期化のために共用体を用いるコードを動作するようにする提案。

この提案の動機は、現在議論中のstd::inplace_vectorを定数式でも利用できるようにしようとするもので、そこでの問題は次のようなものです

template <typename T, size_t N>
struct FixedVector {

  // 単一要素union、ストレージ領域のオブジェクトを構築しない
  union U {
    // storageの各要素の生存期間は外側のFixedVectorが管理する
    constexpr U() { }
    constexpr ~U() { }

    T storage[N]; // 要素が挿入されるまでは初期化したくない
  };

  U u;
  size_t size = 0;

  // note: we are *not* constructing storage
  constexpr FixedVector() = default;

  constexpr ~FixedVector() {
    std::destroy(u.storage, u.storage+size);
  }

  constexpr auto push_back(T const& v) -> void {
    std::construct_at(u.storage + size, v); // ng、u.storageはアクティブメンバではない
    ++size;
  }
};

constexpr auto silly_test() -> size_t {
  FixedVector<std::string, 3> v;
  v.push_back("some sufficiently longer string");
  return v.size;
}
static_assert(silly_test() == 1);

このコードは合法的に動作しないようです。例えば、MSVC/EDG/GCC13.21までは動作しますが、clangや最新のGCCはこれを拒否します。

問題は、共用体のコンストラクタがメンバを初期化していないため、その唯一のメンバstorageオブジェクトの生存期間が開始されていないため、その領域(の一部)を遅延初期化しようとすると非アクティブメンバに対するアクセスになってしまい、これが定数式で許可されていないためにエラーとなることです。

とはいえ、共用体のコンストラクタでstorageを初期化してしまうと、storageの全ての要素の初期化が必要になってしまいます。要素型がデフォルトコンストラクタを持たない場合はこれはエラーになります。また、共用体を利用せずにこのような遅延初期化を行うことはできません(aligned_storageのようなものはTではない別の型の領域を再利用する形になる)。

この提案は、このような共用体使用時の非アクティブメンバアクセスを定数式でも行えるようにしようとするもので、3つのアプローチを紹介しています。

  1. 上記のようなストレージのための特別扱いされたライブラリ型std::uninitialized<T>を提供する
    • まさに上記のU::storageを提供するための汎用型、要素型がimplicit-lifetime typeでないならば各要素の生存期間は自動的に開始されない
    • その初期化と破棄の管理は完全に利用者の責任
    • コンパイラの特別扱いなどにより、上記の問題を回避する
  2. 共用体の最初のメンバがimplicit-lifetime typeならば、共用体の生存期間開始時に暗黙的にそのメンバの生存期間を開始する
    • 上記例の場合、配列オブジェクトstorageの生存期間は開始されるが、各要素の生存期間は開始されず初期化もされない
  3. 初期化を伴うことなく、メンバの生存期間だけをユーザーが明示的に開始する方法を提供する
    • 2の方法の問題点を回避する

2の方法の問題は、共用体が複数のimplicit-lifetime typeメンバを持っていてコンテキストに応じて使い分けたい場合に先頭以外のメンバの生存期間(だけ)を開始する方法がないことです。

union U {
  T x[N];
  U y[M];
} u;

例えばこのような共用体の場合、2の仕様を有効化したとするとU::xだけは定数式で初期化せずに使用できますが、U::yは最初の問題と同じことに悩まされます。そのため、3の方法では暗黙的ではなく明示的に、共用体の特定のメンバの生存期間だけを開始するライブラリ関数を提供することでこれを解決します。

現在の標準ライブラリにはそれに近いことを行ってくれる関数std::start_lifetime_as()がすでに存在しています

template<class T>
T* start_lifetime_as(void* p) noexcept;

しかし、これが行うことは今回解決したい問題の解消とは少し異なっており、いくつか問題があります

  • constexpr指定されていない
  • 実行時に使用されないようにif constevalで囲う必要がある
  • 型名を指定しなければならない
  • 戻り値を使用しなければならない
    • 実装によって[[nodiscard]]が付加される場合警告されてしまう
template <typename T, size_t N>
struct FixedVector {
  union U { constexpr U() { } constexpr ~U() { } T storage[N]; };
  U u;
  size_t size = 0;

  // note: we are *not* constructing storage
  constexpr FixedVector() {
    if consteval {
      std::start_lifetime_as<T[N]>(&u.storage);
    }
  }
};

そこで、この提案では共用体の特定メンバの生存期間を開始することに特化した関数を改めて追加することを提案しています

template<class T>
constexpr void start_lifetime(T*);

ただし、この関数で生存期間を明示的に開始できるのはTimplicit-lifetime typeの場合に限ります。

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

template <typename T, size_t N>
struct FixedVector {
  union U { constexpr U() { } constexpr ~U() { } T storage[N]; };
  U u;
  size_t size = 0;

  // note: we are *not* constructing storage
  constexpr FixedVector() {
    std::start_lifetime(&u.storage);
  }
};

また、この関数を利用すると1の方法を簡単に実装することができるようになります。

この提案では、この3番目の方法をメインとして提案しています。

P3075R0 Adding an Undefined Behavior and IFNDR Annex

C++の規格書の付属としてコア言語の未定義動作のリストを追加する手続きについての提案。

P1705R0にてコア言語の未定義動作のリストを規格書に添付することが提案されています。そこでは、未定義動作を少なくとも1つのサンプルコードと共に例示しておくことで、言語の未定義動作を明確にするとともにそのリストをC++コミュニティに対して提供し、各未定義動作についての追跡を容易にすることを目的としていました。また、追加で診断不用のill-formed(IFNDR)のリストも同様に提供しようとしています。

この提案はその具体的なプロセスについてのもので、序文およびリストの各要素がどのような情報を伴うかについて提案するものです。

提案する文書構造としては、現在の規格書のコア言語のグループに準じた形でグループ化したうえで、その下にそのグループに属する未定義動作/IFNDRを要素として配置するものです。

各要素は次のようなレイアウトになります

  • 問題を簡潔にまとめたタイトル
  • メインの規格の関連する部分へのクロスリファレンス
  • 標準のnote形式の問題の概要テキスト
  • 問題が起こる場合を示すサンプルコード

転記はしませんが、UB/IFNDRのAnnexの序文も提案されています。

おわり

この記事のMarkdownソース