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

文書の一覧

全部で36本あります。

N4912 2022-11 Kona hybrid meeting information

2022年11月のWG21全体会議の周知文書。

2022年11月の全体会議が対面都オンラインの両方同時開催になることが決定されたようで、主にその開催場所についての情報が記載されています。

N4913 PL22.16/WG21 agenda: 25 July 2022, Virtual Meeting

2022年6月の全体会議のアジェンダ

P0543R1 Saturation arithmetic

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

C++の整数型による計算時、その計算結果がオーバーフロー(その型の最大値/最小値を超えた値になる)する場合、符号付き整数型は未定義動作となり、符号なし整数型は2Nを法としたモジュロ演算によって結果が取得されます。

int main() {
  std::uint32_t un1 = std::uint32_t(4294967295) + 1u; // 0
  std::uint32_t un2 = 0u - 1u;  // 4294967295

  std::int32_t sn1 = std::int32_t(2147483647) + 1; // UB
  std::int32_t sn2 = -2147483648 - 1;  // UB
}

いずれの場合でも、場合によってはオーバーフローするときに上限・下限値で値を止めてほしいときがあります。そのためのよく知られた方法が飽和演算(saturation arithmetic)で、計算結果がオーバーフローする場合に表現可能な範囲内に収める形で結果を返します。

C++には今までこの飽和演算を行う標準的な方法がありませんでしたが、この提案はそれを追加しようとするものです。

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

#include <saturation> // 新ヘッダに配置

int main() {
  int x1 = add_sat(3, 4);               // 7
  int x2 = sub_sat(INT_MIN, 1);         // INT_MIN
  unsigned char x3 = add_sat(255, 4);   // 3、引数はint型
  unsigned char x4 = add_sat<unsigned char>(255, 4);   // 255
  unsigned char x5 = add_sat(252, x3);  // error、2つの引数の型は同一でなければならない
  unsigned char x6 = add_sat<unsigned char>(251, x1);  // 255、2つ目の引数x1はint -> unsigned charへ変換されている
}

追加されるのは四則演算に対応する4つの関数と、飽和的なキャストを行うstd::saturate_cast()の5つです。

namespace std {
  template<class T>
  constexpr T add_sat(T x, T y) noexcept;

  template<class T>
  constexpr T sub_sat(T x, T y) noexcept;

  template<class T>
  constexpr T mul_sat(T x, T y) noexcept;

  template<class T>
  constexpr T div_sat(T x, T y) noexcept;

  template<class T, class U>
  constexpr T saturate_cast(U x) noexcept;
}

std::saturate_cast()は整数型Tから整数型Uへのキャストを行うものですが、Tの値がUで表現できない(オーバーフローする)場合に最大値か最小値に丸めて値を返す形でキャストします。

また、これらの関数テンプレートは任意の整数型(符号有無にかかわらず)において使用可能です。

P0792R9 function_ref: a non-owning reference to a Callable

Callableを所有しないstd::functionであるstd::function_refの提案。

以前の記事を参照

このリビジョンでの変更は、プライマリテンプレートを可変長テンプレートで宣言するようにしたこと(将来の拡張のため)と、function_refのオブジェクトをconstinitで初期化できるようにした(Callableを参照で受け取るコンストラクタをconstexpr化した)ことなどです。

この提案は現在、LEWGからLWGへ進むための投票待ちをしています。

P0901R9 Size feedback in operator new

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

以前の記事を参照

このリビジョンでの変更は、CWGのレビューなどを受けて提案する文言を変更したことです。ただし、それによってEWGの確認が必要な問題が発生しているようです。

P1021R6 Filling holes in Class Template Argument Deduction

C++17で導入されたクラステンプレートの実引数推定(CTAD)について、欠けている部分を埋める提案。

ここで提案されているのは、集成体初期化時のCTAD、エイリアステンプレートでのCTAD、継承コンストラクタからのCTADの3つで、最初の二つはC++20で可能になっています。

この提案のこのリビジョンでの変更は、最後の継承コンストラクタからのCTADの提案を行う文書へのリンクを追記したことです(P2582R0、下の方で解説)。

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

std::optionalやポインタ等のmaybeモナドな対象を、その状態によって要素数0か1のシーケンスに変換するRangeアダプタviews::maybeの提案。

以前の記事を参照

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

  • リスト内包表記サポートのために対象を全てのオブジェクト型に拡張
  • borrowed_rangeに関する議論の追記
    • オブジェクトを値で保持していない場合はviews::meybeborrowed_rangeになる
      • ポインタあるいはreference_warapperで保持している場合
  • パイプラインで参照が使用される例を追加
  • プロクシリファレンスのサポートの追加
  • const伝播に関する議論のセクションを追加(未決定)

などです。

P1642R9 Freestanding Library: Easy [utilities], [ranges], and [iterators]

[utility]<ranges><iterator>から一部のものをフリースタンディングライブラリに追加する提案。

前回の記事を参照

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

  • 提案する文言の改善
  • hidden friendをフリースタンディングとして扱うように
  • std::bindプレースホルダをフリースタンディングに含むことを明確化
  • allocation_resultallocate_at_leastを追加
  • wistream_viewviews::istreamを対象から削除
  • <memory>サポートのため<algorithm>からin_out_resultを追加

などです。

P1673R8 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

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

  • コレスキーTSQRの例でTriangleR[0,0]を修正
  • BLASが入力行列にUPLOを適用するのに対して、返還される可能性のある入力行列に対してTriangleを適用する理由の説明の追加
  • 必要ない場合にlayout_transposeを使用しないように、既知のすべてのレイアウトに対してtransposedを最適化
  • symmetric_matrix_rank_k_updatehermitian_matrix_rank_k_update制約とmandates内の行列のエクステントを修正
  • transposedの戻り値のconst性の曖昧さの解消
  • scaledの戻り値のconst性の曖昧さを解消し、その要素型を元のmdspanの要素型に戻すのではなく、積の型にする
  • accessor_conjugateaccessor_scaledからdecay()メンバ関数を削除(mdspanの要件ではなくなったため)
  • accessor_conjugateconjugatedがユーザー定義複素数型に対して正しく機能することを確認し、その戻り値型のconst性の曖昧さを解消した
  • typoの修正

などです。

P1674R2 Evolving a Standard C++ Linear Algebra Library from the BLAS

C++標準ライブラリに提案する線形代数ライブラリの設計に関して記述した文書。

このリビジョンでの変更は、P1673の最新のリビジョンの内容を反映したことと、参照文献リストの追加・更新などです。

P1774R7 Portable assumptions

コンパイラにコードの内容についての仮定を伝えて最適化を促進するための[[assume(expr)]]の提案。

以前の記事を参照

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

  • P2507の内容を反映したこと
  • CWGのレビューを受けて提案する文言を改善したこと
  • いくつかのデイスカッションを追加したことです

この提案で追記されたディスカッションは、ほぼコンパイラが標準属性を無視する場合の無視の仕方に関してのものです。コンパイラが標準属性を無視する場合、無視するというのは属性の効果なのか、属性のパースそのものなのかは特に規定されていません。これまでは式を取るような属性がなかったためあまり問題になりませんでしたが、[[assume(expr)]]は任意の式を取るためそれが問題になります。

つまり、属性の無視の仕方が未規定のままだと、[[assume(expr)]]と書いてある時にテンプレートがインスタンス化されるかどうか、ラムダがキャプチャするかどうかなどが変わることになります。それはともすればABI破壊を招きます。

この提案では[[assume(expr)]]を無視するにしても無視するのはその効果だけであり、式のパースやインスタンス化は行われるということで、CWGではコア言語もそのように変更する方向で議論が進んでいるようです。

この提案は、すでにCWGのレビューを終えており、次の全体会議で投票にかけられる事が決まっています。

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

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

以前の記事を参照

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

  • フィードバックに基づく構文の変更
  • WG21向け文書とWG14向け文書の分離
  • __has_embedの構文の変更
    • ファイルが存在していてそのファイルが空であり、指定されたembed引数が正しい場合、2を返すようにされた
    • 以前は未規定だった
  • typo修正や文言の修正
  • limit引数では、少なくとも1回はマクロを展開するように文言を調整

などです。

P2286R8 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整のみ(文字のエスケープまわり)です。

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

P2429R0 Concepts Error Messages for Humans

C++コンパイラの出力するエラーメッセージの改善についての報告書。

C++エラーメッセージが時に何を言っているのかわからなくなりがちなのは良く知られていることですが、C++20でコンセプトが導入されてそれが改善されるかと思いきやより意味がわからなくなっているなど、事態は深刻さを増しています。

この文書はそのような現状を報告するとともに、特にコンセプト関連のエラーメッセージに対しての改善可能性を示し、C++エラーメッセージの改善の議論の呼び水となること目的としたものです。

P2445R1 forward_like

クラス型のメンバ変数について、const性も含めた正しい完全転送を行うstd::forward_likeの提案。

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

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

P2460R1 Relax requirements on wchar_t to match existing practices

wchar_tエンコーディングについての実態になじまない制約を取り除く提案。

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

この提案は、以前の全てのバージョンに対するDefect Reportとすることで合意されているようです。

P2472R3 make function_ref more functional

function_refユーザビリティと安全性を向上させるコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、追加するコンストラクタにconstexprを付加したことです。

P2510R3 Formatting pointers

std::formatについて、ポインタ型のフォーマットを充実させる提案。

以前の記事を参照

このリビジョンでの変更は、ポインタ型に対する#のフォーマット指定(16進数値に対する0xプリフィックスのように、数値形式を明確化する指定)を提案から削除した事などです。

P2513R2 char8_t Compatibility and Portability Fix

char8_tの非互換性を緩和する提案。

以前の記事を参照

このリビジョンでの変更は、u8""文字列リテラルから変換可能な配列からsigned charを除外したことなどです。

新たに掲載されたサンプルコードより

                                    // pre C++20 | C++20 | This proposal  
const char* ptr0 = u8"";            //    ✅        💔         💔
const unsigned char* ptr1 = u8"";   //    ❌        ❌         ❌
const char arr0[] = u8"";           //    ✅        💔         ✅
const unsigned char arr1[] = u8"";  //    ✅        💔         ✅

✅ : ok、💔 : 破壊的変更、❌ : ng

// C++20以前はok, C++20でng
constexpr const char* resource_id () {
  return u8"o(* ̄▽ ̄*)o";
}

// この提案後、こう書けばok
constexpr const char* resource_id () {
  const char res_id[] = u8"o(* ̄▽ ̄*)o";
  return res_id;
}

P2542R2 views::concat

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

このリビジョンでの変更は、concat-indirectly-readableコンセプトに追加の意味論制約(等しさを保持しない*iter_moveの禁止)を追加した事などです。

P2551R1 Clarify intent of P1841 numeric traits

P1841R2で提案されている数値特性(numeric traits)取得ユーティリティについて、実装経験からの疑問点を報告する文書。

このリビジョンでの変更は、回答が得られて解決された疑問を削除した事、reciprocal_overflow_thresholdについてのオプションを追記した事です。

P2558R1 Add @, $, and ` to the basic character set

@ $ `の3種類の文字をソースコードの基本文字集合に追加する提案。

このリビジョンでの変更は、これによる影響についての説明を追記した事です。

P2577R1 C++ Modules Discovery in Prebuilt Library Releases

P2577R2 C++ Modules Discovery in Prebuilt Library Releases

ビルド済みモジュールライブラリ配布のための規則についての提案。

R1での変更は、リンカ引数から変換して取得する例に標準ライブラリモジュールを含めるようにしたことです(標準ライブラリも他のライブラリと同様の方法で提供するモジュールを取得できる)。

R2での変更は、提案しているルールはどこでも使用すべきというものではないことを明確にしたこと(?)などです。

P2580R0 Tuple protocol for C-style arrays T[N]

生配列にタプルインターフェースのサポートを追加する提案。

タプル-likeな型という観点からは、T[N]std::array<T, N>は同一の性質を持っているはずです。しかし、std::array<T, N>はタプルインターフェースを備えているのに対して、T[N]はそうではありません。

T[N]にタプルインターフェースのサポートを追加すれば、タプルを渡せるところ(std::applystd::make_from_tupeなど)に生配列を渡すことができるようになります。他にも、std::array<T, N>よりもT[N]の方が好まれるような場合においても、生配列からstd::arrayを一時的に構築しなくてもタプル操作を利用できるようになるなどのメリットがあります。

#include <array>

// サイズの自動推定
void auto_size_deduct() {
  int c_arr[] = {1, 2, 3};          // ok、要素数3
  std::array<int> arr = {1, 2, 3};  // ng
}


// C APIライブラリ関数
struct ReferenceFrame;
int get_origin(struct ReferenceFrame* frame, double (*pt)[3]);

// C APIとの相互運用性
std::optional<Point> get_origin(ReferenceFrame& frame) {
  // この提案の後では、このように書けるようになる

  double pt[3] { };

  if (get_origin(&frame, &pt) != 0) {
    return { };
  }

  return std::make_from_tuple<Point>(pt);
}

// コンパイル時の境界チェック
void ctbc() {
  int c_arr[42]{};

  c_arr[42] = 42;           // UB
  std::get<42>(c_arr) = 42; // コンパイルエラー
}

この提案は、これらのメリットのために、生配列に対してタプルインターフェースサポートを追加しようとするものです。

タプルインターフェースの実体は、次の3つの標準ライブラリ機能からなっています。

  • std::tuple_size<T> クラステンプレート
    • タプルの長さを求める
  • std::tuple_element<T> クラステンプレート
    • タプルの要素型を求める
  • std::get<I> 関数
    • タプルの要素を引き当てる

この提案では、これらのものに対して生配列T[N]の(部分)特殊化を追加することで生配列に対するタプルサポートを有効にしようとしています。

その際、T const [N]に対して新規追加するものと既存のもの(tuple_size, tuple_element)がバッティングするため、T const [N]に対する特殊化も同時に追加しています。また、コンセプトを使用した実装によって、そのようなconst問題を回避できる他std::getにコンセプトによるインデックス制約をする実装も可能とのことです(現在の提案ではありません)。

P2581R0 Specifying the Interoperability of Binary Module Interface Files

ビルド済みモジュールを扱う際に、ビルドシステムがそのビルドズムモジュールファイルを直接扱うことができるかどうかを調べられるようにする提案。

バイナリモジュールインターフェースファイル(BMI)はモジュールのインターフェース部分をビルドしたものであり、モジュールをパースし直したりビルドしなおしたりすることなくモジュールのインターフェースを調べたり、モジュールがビルドされた時の情報を取得したりするのに使用されます。その形式は実装定義であり、clangとGCCの間では相互運用可能であるようですが、基本的にはコンパイラによって独自の形式です。

ビルド済みモジュールをライブラリとして配布する場合、そこに含まれるBMIがその環境のコンパイラによって使用可能であったとしても直接使用することができない場合があります(ビルドされたときの設定と使用時の設定が異なるなど)。

SG15のコンセンサスとしては、モジュールの探索にはメタデータを使用する方向性が決まっています。その探索の方法やフォーマットなどはまだ議論中ですが、モジュールライブラリがBMIを提供しそれが使用可能かどうかをコンパイラに知らせる必要があることは確実です。そして、それはビルドシステムによって認識される必要があり、現在その方法はまだありません。

この提案は、ビルドシステムがBMIを読んだりコンパイラを呼び出したりすることなくそれを認識できるようにするためのメカニズムを確立しようとするものです。

この提案では、モジュール探索時に使用するメタデータ内に、BMIの場所とともにそのフォーマットを示す識別子を必ず含むようにすることを提案しています。その識別子は少なくとも以下のものを含んでいる必要があります

  • ビルドに使用されたコンパイラ種別とバージョン
  • 言語バージョン
  • ISA/ABIに関するオプション

また、そもそもBMIを生成する時や配布されたBMIが使用可能ではなかったとき、その環境とビルドコンテキストで使用可能なBMIを生成する必要があり、そのためにこの識別子を取得可能である必要があります。そこで、コンパイラがそのようなインターフェース(コマンドラインオプションによってコマンドライン出力するなど)を備えるようにすることも提案しています。

P2582R0 Wording for class template argument deduction from inherited constructors

クラステンプレートの実引数推定(CTAD)を継承コンストラクタからでも行えるようにする提案。

CTADはC++17で導入され、その時は非集成体のクラステンプレートそのものでしか使用できませんでした。C++20では集成体テンプレートとエイリアステンプレートに拡大されましたが、継承コンストラクタからCTADはできないままでした。

この提案はそれを解消し、継承コンストラクタをもちいてCTADできるようにしようとするものです。

template <typename T>
struct Base {
  Base(T&&);
};

template <typename T>
struct Derived : public Base<T> {
  using Base<T>::Base;  // Baseのコンストラクタを継承
}

Derived d(42); // この提案後ok、Derived<int>と推論される

この手順は、派生クラスに対する基底クラスをエイリアステンプレートのように扱って、エイリアステンプレートからのCTADのアルゴリズムを流用するとともに、継承コンストラクタから生成された推論補助(関数テンプレート)を派生クラスで直接生成された推論補助よりもオーバーロード順で優先することで行われています。

最初の推論補助の抽出は、クラステンプレートCの基底クラスにクラステンプレートBが含まれている場合、Cのテンプレートパラメータを持ち右辺がBであるようなエイリアステンプレートを生成し、このエイリアステンプレートを用いて推論補助を取得します。

上記の例では次のようにエイリアステンプレートとそこからの推論補助が抽出されます。

// 生成された、仮想的なエイリアステンプレート
template <typename T>
using D = Base<T>;

// ↑から生成された推論補助
template<typename T>
Derived(T&&) -> Derived<T>;

この提案は既にR1が存在するらしく、R1が次の全体会議で投票にかけられることが決まっています。

P2584R0 A More Composable from_chars

std::from_charsrange対応させつつ使いやすくする提案。

std::from_charsは文字列から数値への変換を行うものです。引数には入力文字範囲の先頭と終端を取り(C++17までのイテレータペアを受け取るインターフェースと同様)、結果は引数にとった出力用変数への参照に出力されます。戻り値には変換結果のステータスが返されます。

template<std::size_t N>
int to_int(char(&input)[N]) {
  using namespace std::ranges;

  int result;

  // 文字列 -> int値へ変換
  auto [p, err] = std::from_chars(begin(input), end(input), result);

  // エラーチェック
  if (err == std::errc{}) {
    return result;
  } else {
    return 0;
  }
}

これはこれでシンプルで使いやすいインターフェースではあるのですが、入力に範囲(range)を取れないことや、結果を受けるために一度ローカル変数を用意しなければらないなど、少し面倒なところがあります。例えば、任意の文字範囲(range)で使用するには次のように書く必要があります

template<std::ranges::contiguous_range R>
int to_int(R&& input) {
  using namespace std::ranges;

  int result;

  // 文字列 -> int値へ変換
  auto [p, err] = std::from_chars(std::to_addres(begin(input)), std::to_addres(end(input)), result);

  // エラーチェック
  if (err == std::errc{}) {
    return result;
  } else {
    return 0;
  }
}

contiguous_rangeは必ずしもsized_rangeではなく、イテレータは必ずしもポインタとは限らないため、このように書くのが最適です。

この提案は、std::from_charsオーバーロードを追加してrange対応させると共に、戻り値で変換結果を返すようにするものです。次のようなオーバーロードを追加することを提案しています

namespace std {
  // 新しいオーバーロードの戻り値型
  template <typename T>
  struct from_chars_result_range {
    T value;  // 変換結果
    std::errc ec;
    std::span<const char> unparsed;
  };

  // 整数型用
  template <integral T>
    requires (!std::same_as<bool, T>)
  constexpr from_chars_result_range<T> from_chars(std::span<const char> rng, int base = 10);

  // 浮動小数点数型用
  template <floating_point T>
  from_chars_result_range<T> from_chars(std::span<const char> rng, chars_format fmt = chars_format::general);
}

入力範囲としてstd::span<const char>を取るのは、std::string_viewrangeコンストラクタがexplicitされそうなことと、より一般の入力文字範囲は必ずしも文字列ではないと考えられるためです。

テンプレートパラメータTには変換先の数値型を指定します。例えば先ほどのコードは次のようになります

template<std::ranges::contiguous_range R>
int to_int(R&& input) {
  // 文字列 -> int値へ変換
  auto [result, p, err] = std::from_chars<int>(input);

  // エラーチェック
  if (err == std::errc{}) {
    return result;
  } else {
    return 0;
  }
}

提案文書より、views::splitと組み合わせる場合の比較例。

現在 この提案
std::string s = "1.2.3.4";

auto ints =
  s | std::views::split('.')
    | std::views::transform([](const auto & v){
        int i = 0;
        std::from_chars(std::to_address(v.begin()),
                        std::to_address(v.end(), i);
        return i;
      });
std::string s = "1.2.3.4";

auto ints =
  s | std::views::split('.')
    | std::views::transform([](const auto & v) {
        return std::from_chars<int>(v).value;
      });

提案では、別の実装としてstd::expected<std::from_chars_result_range<T>, std::errc>を返すインターフェースに関しても言及されていますが、今の所はそちらはサブ案です。

P2585R0 Improving default container formatting

std::formatのコンテナに対するフォーマットを改善する提案。

std::formatのコンテナに対するフォーマットはC++23に向けて提案中(P2286)です。そこでは、コンテナ(range)の要素型がフォーマット可能であれば、コンテナの要素列をコンテナごとに適した形式でフォーマットします。

コンテナ 出力
std::vector<std::pair<int, int>>{{1, 2}, {3, 4}} [(1, 2), (3, 4)]
std::set<std::pair<int, int>>{{1, 2}, {3, 4}} {(1, 2), (3, 4)}
std::map<int, int>{{1, 2}, {3, 4}} {1: 2, 3: 4}

これはそれぞれのコンテナ型ごとにstd::formatterを特殊化することで行われており、非標準のコンテナに対してはデフォルトのコンテナフォーマット(std::vectorと同様のもの)しか提供されません。

コンテナ 出力
boost::container::flat_set<int>{1, 2, 3} [1, 2, 3]
absl::flat_hash_map<int, int>{{1, 2}, {3, 4}} [(1, 2), (3, 4)]

{fmt}では、formatterは特定の標準コンテナに対して特殊化されているのではなく、map-like(::mapped_typeの存在によって判定)、あるいはset-like(key_typeの存在によって判定)なより広い型に対して特殊化されています。したがって、{fmt}では非標準の連想コンテナに対しても標準のものと同様のフォーマットを行うことができます。

この提案は、{fmt}がコンテナフォーマットのためにformatterで行なっていることとほぼ同様のことをP2286のコンテナフォーマットに対しても適用するものです。

提案では、format_kindというフォーマット対象の種別を表す変数テンプレート(整数or列挙値)を用意して、input_rangeに対してはrange_format_kindという列挙値をformat_kindとして特殊化します。rangeformat_kindでは、rangeの種別(maporset、それ以外など)によってrange_format_kindの値を変化させます。

namespace std {

  // range型の種別を表す列挙値
  enum class range_format_kind {
    disabled,
    map,
    set,
    sequence,
    string,
    debug_string,
  };

  // フォーマット種別を指定する任意の値
  template <class R>
  inline constexpr auto format_kind = unspecified;

  // format_kindのrange型に対する特殊化
  template <input_range R>
  inline constexpr range_format_kind format_kind<R> = []{
    if constexpr (requires { typename R::key_type; typename R::mapped_type; }
                  and is-2-tuple<range_reference_t<R>>) {
      return range_format_kind::map;  // map-likeの判定
    } else if constexpr (requires { typename R::key_type; }) {
      return range_format_kind::set;  // set-likeの判定
    } else {
      return range_format_kind::sequence; // その他のコンテナ(範囲)
    }
  }();
}

input_rangeに対するformatter特殊化では、共通実装であるdefault-range-formatterという型を継承するようにした上で、default-range-formatterにこのformat_kindを渡すようにします。

namespace std {

  // range型に対するフォーマッター共通実装
  template<range_format_kind K, ranges::input_range R, class charT>
    struct default-range-formatter; // exposition only

  // range型に対するformatter特殊化
  template<ranges::input_range R, class charT>
      requires (format_kind<R> != range_format_kind::disabled)
                && formattable<ranges::range_reference_t<R>, charT>
    struct formatter<R, charT> : default-range-formatter<format_kind<R>, R, charT> { };
}

このようにして、std::formatterシグネチャを変更することなく、そのフォーマッター実装に対してこのformat_kind<R>を伝え、default-range-formatterでは各range_format_kindの値に対して特殊化してフォーマットを調整します

namespace std {
  // デフォルトのコンテナフォーマット
  template <ranges::input_range R, class charT>
  struct default-range-formatter<range_format_kind::sequence, R, charT> {
    ...
  };

  // map-likeコンテナに対するフォーマット
  template <ranges::input_range R, class charT>
  struct default-range-formatter<range_format_kind::map, R, charT> {
    ...
  };

  // set-likeコンテナに対するフォーマット
  template <ranges::input_range R, class charT>
  struct default-range-formatter<range_format_kind::set, R, charT> {
    ...
  };
}

これによって、現在のstd::set/std::map専用のフォーマットをそれに近い(同等な)型へと広げることができます。

また、format_kindrange以外の型に対してはその値を未規定としておくことで、将来的に他の型に対してこのようなフォーマット調整を行える余地を残しています。

P2587R0 to_string or not to_string

std::to_string浮動小数点数出力を修正する提案。

例えば次のコードの出力は

auto loc = std::locale("uk_UA.UTF-8");
std::locale::global(loc);
std::cout.imbue(loc);
setlocale(LC_ALL, "C");

std::cout << "iostreams:\n";
std::cout << 1234 << "\n";
std::cout << 1234.5 << "\n";

std::cout << "\nto_string:\n";
std::cout << std::to_string(1234) << "\n";
std::cout << std::to_string(1234.5) << "\n";

setlocale(LC_ALL, "uk_UA.UTF-8");

std::cout << "\nto_string (uk_UA.UTF-8 C locale):\n";
std::cout << std::to_string(1234) << "\n";
std::cout << std::to_string(1234.5) << "\n";

以下のようになります

iostreams:
1 234
1 234,5

to_string:
1234
1234.500000

to_string (uk_UA.UTF-8 C locale):
1234
1234,500000

std::to_stringの整数型オーバーロードは、グローバルCロケールを使用しますがグルーピングを行わず、実質的にローカライズされません。一方、浮動小数点数オーバーロードは、グローバルCロケールを使用して小数点を取得し、グルーピングを行いません。そのため、このような結果になります。

また、浮動小数点数オーバーロードでは固定小数点形式を常に使用するため、その出力が有用なのは限られた範囲の数値に対してのみです。

std::cout << std::to_string(std::numeric_limits<double>::max()) << "\n";
std::cout << std::to_string(-1e-7);
179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000
-0.000000

1つ目の出力では最初の17桁だけが有効で残りの桁は有用ではない値です。さらに、小数点以下に無意味なゼロがあります。2つ目の出力も、小数点以下の0は無意味です。

これらのことは、std::to_stringsprintfを用いて定義されているためにiostreamの振る舞いと異なっていることが原因です。

この提案はこれらの問題の修正のために、std::to_string(v)std::format("{}", v)で定義し直そうとするものです。

先ほどの例の出力は次のように修正されます

iostreams:
1 234
1 234,5

to_string:
1234
1234.5

to_string (uk_UA.UTF-8 C locale):
1234
1234.5
1.7976931348623157e+308
-1e-7

この変更では整数型の出力は従来通りのままで、浮動小数点数型の出力だけが変更されます。

P2588R0 Relax std::barrier phase completion step guarantees

std::barrierのバリアフェーズ完了時処理が、同じバリアで同期する任意のスレッドから起動できるようにする提案。

std::barrierはFork-Joinモデルのような並行処理の実装に活用することができる繰り返し使用可能な動機機構です。典型的には、同期に参加する全スレッドが並列実行される部分と一つのスレッドだけで実行される同期部分から構成される処理単位の繰り返しのような処理になり、その1つの処理単位のことをバリアフェーズと呼びます。


Fork-Joinモデルのイメージ(Wikipediaより)

std::barrierはテンプレート引数として完了関数(CompletionFunction)の型を受け取って、そのオブジェクトを保持しておくことでバリアフェーズの最後にどこか一つのスレッドでそれを実行してから、待機しているスレッドを再開します。

// 複数のスレッドで呼ばれる処理
template<typename CF>
void fork_proc(std::barrier<CF>& sync) {
  // キャンセルされるまで行われる連続処理
  // ループごとに全スレッドで同期しつつ実行される
  while(/*キャンセルの検出*/) {

    // メインの処理
    ...

    // 全スレッドはここで待ち合わせる(1ループの処理の完了を同期する)
    // バリアフェーズに参加する全てのスレッドがここに到達した時、CFの処理を実行してから次のバリアフェーズを開始する
    sync.arrive_and_wait(); // 同期ポイント
  }
}

int main() {
  // 並列数
  constexpr std::size_t N = 10;

  // スレッド数でバリアを初期化
  // 同時に、同期ポイントで再開直前に実行する完了関数を指定
  std::barrier sync{N, [] {
    // 処理対象データの更新など、同期が必要なシングルスレッド処理
    ...
  }};

  for ([[maybe_unused]] auto i : std::views::iota(0, N)) {
    std::thread{[&sync]{
      fork_proc(sync);
    }}.detach();
  }

  // 完了を待機する処理など
  ...
}

バリアで同期する(バリアフェーズに参加する)各スレッドは.arive()(同期ポイント到達通知)と.wait()(他スレッドの待機)もしくは.arrive_and_wait().arive().wait()の複合操作)を呼び出すことで同期を取ります。完了関数は、バリアフェーズに参加しているスレッドが全て同期ポイントに到達した後、バリアフェーズに参加するスレッドのいずれかで実行され、その実行が完了した後で次のバリアフェーズが開始されます。

現在の標準の規定では、この完了関数がどのスレッドで実行されるかは実装の自由として明確に規定していません。しかし、std::barrierの保証やメンバ関数の規定などの相互作用から生じる意図しない制約によって、実質的に同期ポイントに最後に到達したスレッドで実行する実装しか取れないようになっているようです。

それによって、ハードウェア(GPUなど)が持っているスレッド同期機構やそのサポート機構を用いてstd::barrierを効率的に実装することが妨げられており、この提案は、この意図しない制限を取り払うことで、std::barrierの効率的な実装を許可しようとするものです。

これによる変更は規定の変更のみですが、保証内容や意味論が変化するために破壊的変更となります。提案ではハードウェアによる効率化に最も適した選択肢を選んでいますが、この振る舞いはstd::barrier提案者やそれを既に使用している人によっても意外なものであり、まだ入ったばかりなこともあり影響を受けるコードは存在しないと思えるため破壊的変更の影響はメリットを上回ると主張しています。そして、この変更をC++20に対するDRとすることを提案しています。

詳細

この意図しない制限というのは、.arive()(同期ポイント到達通知)と.wait()(他スレッドの待機)の操作が分かれている事に原因があります。.arive()は戻り値としてarrival_tokenというものを返し、.wait()はそのarrival_tokenを受け取って待機します。すなわち、この2つの操作が分割されていることによって、バリア同期ポイント通知とバリア同期の待機を異なるスレッドで行うことができるようになっています。

これによって、恣意的ではありますが、次のようなことが起こります。

// 参加スレッド数2で初期化、完了関数cfをセット
std::barrier<CF> b{2, cf};

// arrival_tokenの型
using tok_t = decltype(b.arrive());

void thread() {
    new tok_t(b.arrive());      // A: 同期ポイント到達を通知するが、返されるトークンを消費しない(リークしてる
}                               // B: スレッド終了

// C: 2つのスレッドを起動
auto t0 = std::thread(thread);
auto t1 = std::thread(thread);

// D: 2つのスレッド終了待機
t0.join();                      
t1.join();

// E: 完了関数cfが呼ばれていることが保証される

この例では、2つのarrival_tokenが有効であり続けながらstd::barrier.wait()は呼ばれておらず、バリアフェーズに参加している(完了を待機している)スレッドが居なくなっています。現在の標準の規定はこの時でも完了関数が呼ばれていることを保証しており、それをこの場合でも確実に達成するためには、最後に.arive()したスレッドで完了関数を実行するという実装を取らざるを得ません。

これは、現在のstd::barrier仕様が保証している次の2つのことの相互作用の結果です

  • スレッドが.wait()を呼び出さなくてもいい自由
  • 完了関数(バリアフェーズ完了ステップ)がいつどこで実行されるかの保証

数百万のHWスレッドを持つアーキテクチャでは、わずかな並列化されていない処理によって大きく並列化のスケーラビリティが制限されます(アムダールの法則)。.arive().wait()を分割しているAPIはその影響は削減するためのもので、完了関数が実行するシングルスレッド処理とstd::barrierの同期の処理を並列化し、同期をさらに別のスレッドやHWアクセラレータ(NVIDIA GPUはそれを持っている)で実行することで同期のコストを完了関数の処理の背後に隠してしまうことを目的とするものです。しかし、上記の問題によってそのような実装は現在可能となっていません・・・

この提案では、std::barrierの保証を次のように変更することでこの問題の解決を図ります

  • 累積性は以下の2つのところで確立される
    • バリアフェーズに参加しているすべてのスレッドと完了関数を実行するスレッドの間
    • 完了関数を実行するスレッドと.wait()の呼び出しによってバリアフェーズ完了を待機しているすべてのスレッドの間
  • バリアフェーズ完了を待機するスレッドが無い場合、完了関数が実行されるかは未規定
  • 完了関数はバリアフェーズに参加している(参加していた)スレッドの一つ、もしくは新しいスレッドで実行される
  • 完了関数を実行するスレッドは未規定であり、既存のバリアと無関係なスレッドも含まれ、そのスレッドの任意の時点でコードが実行される

累積性(Cumulativity)とは、2つの処理(A, B)を接続するある地点(A -> C -> BのC)について、AがCに到達してからBが実行されるという順序関係を言うもので、特にマルチスレッド処理の場合にはAの並行処理がすべて終わってから(各スレッドの完了がCに累積してから)、Bの処理開始が実行(累積)されることを表現しています

これらの変更によって、完了関数の実行とstd::barrierによる同期は累積性が切り離されており、それによってその2つの処理を並列に実行することが許可され、なおかつそれらの処理を実行するスレッドはバリアフェーズと無関係なスレッドでも良くなり、同期ポイントでバリアフェーズに参加しているスレッドが無い場合には必ずしも完了関数を実行しなくても良い(してもいい)、となるようになります。

P2589R0 static operator[]

添え字演算子operator[])を静的メンバ関数として定義できるようにする提案。

この提案のモチベーションはP1169と同じです。

operator[]P2128によって任意個数の引数を取ることができるようになっており、operator()との違いはほぼ演算子の見た目だけになっています。従って、operator[]operator()と同等に扱えるようにすることで構文と意味論の一貫性が向上し、使いやすさや教えやすさの向上につながります。

operator()ではラムダ式であったりstd::functionの様なコールラッパを考慮しなければならなかったりしますが、operator[]の場合はそれらの考慮は必要ないため、変更はとても小さく最小限で済みます。

P2590R0 Explicit lifetime management

メモリ領域上にあるトリビアルな型のオブジェクトの生存期間を開始させるライブラリ機能の提案。

例えば、次のようなコードはC++17までは厳密には未定義動作でした

struct X { int a, b; };

X* make_x() {
  X *p = (X*)malloc(sizeof(struct X));
  p->a = 1; // UB
  p->b = 2; // UB
  return p;
}

なぜなら、mallocで確保したメモリ領域にはXのオブジェクトが構築されておらず、そのためXのポインタは生存期間外のオブジェクトを参照している不正なポインタであり、それを介したアクセスは未定義動作となるためです。

とはいえこのようなコードはCでは問題なく、またC++においても低レベルな操作でよく使用されるものであります。そのためC++20では、特定のライブラリ関数によって操作されるメモリ領域上にトリビアルな型(implicit-lifetime type)のオブジェクトを暗黙的に構築するようにされました。その対象はmallocmemcpystd::bit_castなどです。

これ以外にも実装定義なものが含まれていますが、ユーザー定義の関数はそこには含まれません。例えば、メモリプールからメモリを割り当てる関数など、ライブラリ作成者がC++コードで記述するメモリ割り当て関数を上記例のmallocの代わりに使えば、相変わらず未定義動作です。

P0593R6では、この場合に明示的にオブジェクトの生存期間を介するための関数std::start_lifetime_as<T>を提案として含んでいました。それはLEWGのレビューを通過していましたが、LWGの時間不足のためにC++20には間に合いませんでした。

この提案は、そのstd::start_lifetime_as<T>を標準ライブラリに含めるためのものです。

struct X { int a, b; };

X* make_x() {
  // pの領域上でXのオブジェクトの生存期間を開始
  X *p = std::start_lifetime_as<X>(malloc(sizeof(struct X)));
  p->a = 1; // ok
  p->b = 2; // ok
  return p;
}

テンプレート引数のTimplicit-lifetime typeと呼ばれる型(トリビアル型や集成体型など)でなければなりません。

std::start_lifetime_as<T>(p)は単一オブジェクトのためのものですが、このような場合実際には配列を作成することの方が多いと思われます。そのために、std::start_lifetime_as_array<T>(p, n)も提案されています。

void process(Stream *stream) {
  unique_ptr<char[]> buffer = stream->read();

  // stream->read()内でオブジェクト構築していたとしても、どちらかのパスは未定義動作となる
  if (buffer[0] == FOO) {
    process_foo(reinterpret_cast<Foo*>(buffer.get())); // UB
  } else {
    process_bar(reinterpret_cast<Bar*>(buffer.get())); // UB
  }

  // start_lifetime_as_arrayを使用すれば未定義動作を回避できる
  // ただし、要素数nが必要
  if (buffer[0] == FOO) {
    process_foo(std::start_lifetime_as_array<Foo>(buffer.get(), n)); // ok
  } else {
    process_bar(std::start_lifetime_as_array<Bar>(buffer.get(), n)); // ok
  }
}

std::start_lifetime_as_array<T>(p, n)pの領域でT[n]の配列の生存期間を開始するものです。

これらの関数は実際にはコンストラクタを呼び出したりするものではなく、あくまで未定義動作回避のためにコンパイラにアピールするものです。従って、実際には何もしない関数となるでしょう。

P2591R0 Concatenation of strings and string views

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

現在は、std::stringstd::string_viewの間で+を使用できません。

std::string calculate(std::string_view prefix)
{
  return prefix + get_string(); // ERROR
}

しかし、このことはstd::stringの他のAPIと一貫していません。.apend()などの他のAPIは、すでにstring_view(も含めたview型)を受け取れるようになっているためです。

std::string str;
std::string view;

// Appending
str + view;              // ERROR
str + std::string(view); // OK, but inefficient
str + view.data();       // Compiles, but BUG!

std::string copy = str;
copy += view;            // OK, but tedious to write (requires explicit copy)
copy.append(view);       // OK, ditto


// Prepending
view + str;              // ERROR

std::string copy = str;
str.insert(0, view);     // OK, but tedious and inefficient

また、このことは生の文字列とも一貫していません。

std::string str;

str + "hello";    // OK
str + "hello"sv;  // ERROR

"hello"   + str;  // OK
"hello"sv + str;  // ERROR

この提案はstd::stringstd::string_viewの間のoperator+を追加することでこれらの問題を解決しようとするものです。

そもそも、C++17でこの+が他のものと一緒に導入されなかった理由は、std::stringstd::string_viewの間のoperator+がそれを遅延して結合するような中間オブジェクトを返す、string builder的な実装を将来的に考慮するものだったようです(C++17時点で、LLVMstring_viewに近いクラスの実装がそうなっていた様子)。しかし、そのような提案は現在でもなく、それを考慮したとしても生の文字列との間でそうなっていないので+を選択するのは間違っている、と筆者の方は述べています。

P2592R0 Hashing support for std::chrono value classes

<chrono>の時間や日付を表す型に対してハッシュサポートを追加する提案。

<chrono>に定義されている時間や日付を表す型にはハッシュサポートがなく、そのためそれらの型を非順序連想コンテナのキーとして使用することができません。

std::unordered_set<std::chrono::milliseconds> unique_measurements;

ただし、それらの型の実体は整数型等の数値型であるため、ハッシュを定義することは容易です。

truct duration_hash
{
    template <class Rep, class Period>
    constexpr auto operator()(const std::chrono::duration<Rep, Period> &d) const
        noexcept(/* ... */)
    {
        std::hash<Rep> h;
        return h(d.count());  // 
    }
};

std::unordered_set<std::chrono::milliseconds, duration_hash> unique_measurements; // OK

<chrono>にあるほとんどの型は同様にしてハッシュサポートを用意に追加できるため、標準がこれを提供しない理由はないと考えられるため、このようなサポートを追加しようとする提案です。

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

  • std::chrono::duration
  • std::chrono::time_point
  • std::chrono::day
  • std::chrono::month
  • std::chrono::year
  • std::chrono::weekday
  • std::chrono::weekday_indexed
  • std::chrono::weekday_last
  • std::chrono::month_day
  • std::chrono::month_day_last
  • std::chrono::month_weekday
  • std::chrono::month_weekday_last
  • std::chrono::year_month
  • std::chrono::year_month_day
  • std::chrono::year_month_day_last
  • std::chrono::year_month_weekday
  • std::chrono::year_month_weekday_last
  • std::chrono::zoned_time
  • std::chrono::leap_second

time_zonetime_zone_linkragularではない(コピーできない)ことから、ハッシュサポートは必要ないと考えられるため除外されています。

P2593R0 Allowing static_assert(false)

static_assert(false)がテンプレートの実体化前にエラーとならないようにする提案。

if constexprfalseとなるブロックでstatic_assert(false)を使用したとき、意図通りにならず常にコンパイルエラーになってしまう問題はC++17で導入された当初から割と有名な罠でした。

template<typename T>
void f(T t) {
  if constexpr (sizeof(T) == 4) {
    ...
  } else {
    static_assert(false); // 常にコンパイルエラー
  }
}

これは、constexpr if文が条件付きのテンプレート実体化抑制を行う機能であり、static_assert(false)はテンプレートパラメータに依存していないことからテンプレート実体化前に評価されてしまうために起きていることです。

より正確には、これは診断不要のill-formedとなりほぼ未定義動作となっていますが、現在にコンパイラはどれもこれをエラーとして診断しています。

N4861 [temp.res]/8より

The validity of a template may be checked prior to any instantiation. [ Note: Knowing which names are type names allows the syntax of every template to be checked in this way. — end note ] The program is ill-formed, no diagnostic required, if: - no valid specialization can be generated for a template or a substatement of a constexpr if statement within a template and the template is not instantiated, or - ...

ここでは、インスタンス化前にテンプレートの有効性をチェックできることを規定していて(いわゆるtwo-phase name lookupの一段階目に行われる)、その中でも検出されたら診断不要のill-formedとなる事項を列挙しています。その一番最初の項目が

テンプレートまたはテンプレート内constexpr ifのサブステートメントに対して有効な特殊化を生成できない事が分かっていて、テンプレートがインスタンス化されない場合(は診断不要のill-formed)

とあり、static_assert(false)はテンプレートパラメータTと無関係にstatic_assertが発動する事がわかるため、まさにこれに該当しています。

この問題の回避策には例えば

  • static_assert(sizeof(T) == 0);
  • static_assert(sizeof(T*) == 0);
  • []<bool flag=false>(){ static_assert(flag); }();
  • static_assert([]{return false;}());

などが知られています。しかし、先ほどの規定に照らせばどれも診断不要のill-formedから脱し切れておらず(少なくとも上3つは)、これらのワークアラウンドはたまたま動いているに過ぎません。

この問題のよく知られている正しい回避策は、always falseと呼ばれるイディオムです。

template<typename T>
inline constepxr bool always_false = false;

template<typename T>
void f(T t) {
  if constexpr (sizeof(T) == 4) {
    ...
  } else {
    static_assert(always_false<T>); // Tのサイズが4以外の時にだけエラー 
  }
}

これを標準ライブラリにいれる提案もありましたが、テンプレートパラメータが値である場合にそのまま使用できず、static_assert<T>のようなものは覚えるべきことを増やして本質的な問題に蓋をしているに過ぎません。

この提案では、テンプレート中のstatic_assert(false)がテンプレートインスタンス化時にのみ効果を持つようにすることで、この問題の解決を図るものです。

おわり

この記事のMarkdownソース

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

文書の一覧

全部で36本あります(SG22のWG14からのものは除きます)。

P0429R8 A Standard flat_map

キーの検索をstd::map比で高速に行える連想コンテナ、flat_mapの提案。

std::mapはノードベースの連想コンテナであり、個々の要素はノードと呼ばれる単位(key-valueペア及びアロケータ、親と子ノードへのポインタなどをまとめて格納しているもの)でメモリ上に存在しており、多くの場合はノード同士はメモリ上で連続せずに分散して存在しています。そのため、キャッシュ局所性が悪く、キーの検索(ノードの引き当て)のパフォーマンスが悪いことが問題となっていました。

flat_mapの基礎的なアイデアは、シーケンスコンテナ(std::vectorなどメモリ連続なコンテナ)上に二分木を構成して使用することで、各要素をメモリ上で連続させてキャッシュ局所性を向上させようとするものです。要は、更新時もソート済みであることが保証されるソート済std::vectorです。したがって、要素の更新はO(N)(ほぼ常に再配置が発生)、要素の検索はO(logN)(ソート済み配列上での二分探索)の計算量となります。

この提案のflat_mapではさらに、キーと値をそれぞれ別のシーケンスコンテナ(デフォルトはstd::vector)に保持することでキーのキャッシュ局所性をさらに向上させ、検索のパフォーマンス向上の最大化を図っています。そして、この提案のflat_mapはコンテナではなくコンテナアダプタとなり、イテレータはランダムアクセスイテレータ(ただし、C++17以前に対しては入力イテレータ)となります。

namespace std {
  // std::flat_mapの宣言例
  template <class Key, class T, class Compare = less<Key>,
            class KeyContainer = vector<Key>,
            class MappedContainer = vector<T>>
  class flat_map {

    ...

    struct containers {
      KeyContainer keys;
      MappedContainer values;
    };

    ...

  private:
    containers c; // exposition only
  };
}

また、flat_mapstd::mapとのインターフェース互換を意識しているため、ほぼ同じインターフェースによって使用可能となっています。

flat_mapはその構造上、要素の挿入(insert())や削除(remove())が遅く検索(find/operator[])及びイテレートが早い連想コンテナです。従って、適した用途は要素の更新よりも参照回数の方が多くなる場合であり、最大のパフォーマンスメリットを得るには最初に一度構築した後は検索しかしないような使い方をする必要があります。

また、flat_mapはノードベースコンテナではないため要素ごとにアロケータや親子ポインタなどを保持する必要がなく、std::mapに比べて要素あたりの空間コストを削減することができます。そのため、パフォーマンスメリットが得られない場合でも省メモリ目的で使用することもできます。

flat_mapC++20に向けて議論されていましたが、キーと値のコンテナを別々に持つことからそのイテレータのためにzip_viewが必要とされ、zip_viewはその値型(std::pair<T&, U&>/std::tuple<Ts&...>)のcommon_referenceの問題(std::pair<T&, U&><->std::pair<T, U>のような変換ができない)やswapの問題(参照pair/tupleconst-assignableにすると正しくswapされない)の解決のためにC++23に延期されたたため、flat_mapもそれを待たねばなりませんでした。今のところ、C++23に向けてレビューされています(現在LWGでレビュー中)。

P0957R7 Proxy: A Polymorphic Programming Library

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

以前の記事を参照

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

  • proxy::type(), proxy::cast()proxy::reflect()で置き換えた(静的リフレクションを意識して?)
  • bad_proxy_castを削除
  • proxy::operator=の例外指定を変更
  • BasicFacade(名前付き要件)の指定を変更

などです。

P1061R2 Structured Bindings can introduce a Pack

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

std::tupleとパラメータパックは任意の異なるオブジェクトのシーケンスという点でよく似ています。現在、パラメータパックからstd::tupleへ変換することは簡単にできますが、その逆(std::tuple->パラメータパック)は少し面倒です。

template<typename... Ts>
void pack_to_tuple(Ts&&... ts) {
  std::tuple<Ts...> t(std::forward<Ts>(ts)...);
}

template<typename... Ts>
void tuple_to_pack(std::tuple<Ts...> t) {
  std::apply([](auto&&... elems) {
    // ここでtupleから変換したパックが得られる
  }, t);
}

これは単一のパックの単純な使用においてはそこまで複雑ではありませんが、特定のオーバーロードを解決したいとか、それによって戻り値を返したいとか、複数のtupleを扱いたいなどしてくると、急速に複雑化します。たとえば、2つのtuple内積を求めるようなコードの場合

// std::applyを使用した例
template <class P, class Q>
auto dot_product_apply(P p, Q q) {
    return std::apply([&](auto... p_elems){
        return std::apply([&](auto... q_elems){
            return (... + (p_elems * q_elems));
        }, q)
    }, p);
}

// std::index_sequenceを使用した例
template <size_t... Is, class P, class Q>
auto dot_product_idxseq(std::index_sequence<Is...>, P p, Q, q) {
    return (... + (std::get<Is>(p) * std::get<Is>(q)));
}

template <class P, class Q>
auto dot_product_idxseq(P p, Q q) {
    return dot_product_idxseq(
        std::make_index_sequence<std::tuple_size<P>::value>{},
        p, q);
}

どちらもコード自体の短さとは裏腹に、恐ろしいほどの複雑さが詰め込まれています(慣れてるとそう見えなくなってしまうのですが・・・)。そして、このコードはstd::tuple(あるいは互換インターフェースを備えた型)のみに制限されています。構造化束縛宣言で使用可能なタプルっぽく思える型をここに入れることはできません。

この提案はこれらの複雑さと非一貫性を取り払うために、構造化束縛宣言を拡張してパック導入ができるようにしようとするものです。これによって、std::tupleからパラメータパックへの変換が簡単になるとともに、それをタプルlikeな任意の型へと拡張することができます。

std::tuple<X, Y, Z> f();

auto [x,y,z] = f();          // OK today
auto [...xs] = f();          // proposed: xsは長さ3のパック、X,Y,Zのオブジェクトを含んでいる
auto [x, ...rest] = f();     // proposed: xはXのオブジェクト、restは長さ2のパック(Y,Z)
auto [x,y,z, ...rest] = f(); // proposed: restは空のパック
auto [x, ...rest, z] = f();  // proposed: xはXのオブジェクト、restは長さ1のパック(Y)、zはZのオブジェクト
auto [...a, ...b] = f();     // ill-formed: 複数パックへの展開は決定不可能

サンプルコード

std::applyの実装

現在 この提案
namespace detail {
    template <class F, class Tuple, std::size_t... I>
    constexpr decltype(auto) apply_impl(F &&f, Tuple &&t,
        std::index_sequence<I...>)
    {
        return std::invoke(std::forward<F>(f),
            std::get<I>(std::forward<Tuple>(t))...);
    }
}

template <class F, class Tuple>
constexpr decltype(auto) apply(F &&f, Tuple &&t)
{
    return detail::apply_impl(
        std::forward<F>(f), std::forward<Tuple>(t),
        std::make_index_sequence<std::tuple_size_v<
            std::decay_t<Tuple>>>{});
}
template <class F, class Tuple>
constexpr decltype(auto) apply(F &&f, Tuple &&t)
{
    auto&& [...elems] = t;
    return std::invoke(std::forward<F>(f),
        forward_like<Tuple, decltype(elems)>(elems)...);
}

std::applyを使用した2つのタプルの内積

現在 この提案
template <class P, class Q>
auto dot_product(P p, Q q) {
    return std::apply([&](auto... p_elems){
        return std::apply([&](auto... q_elems){
            return (... + (p_elems * q_elems));
        }, q)
    }, p);
}
template <class P, class Q>
auto dot_product(P p, Q q) {
    // no indirection!
    auto&& [...p_elems] = p;
    auto&& [...q_elems] = q;
    return (... + (p_elems * q_elems));
}

std::index_sequenceを使用した2つのタプルの内積std::index_sequenceにタプルインターフェースを追加したとする)

現在 この提案
template <size_t... Is, class P, class Q>
auto dot_product(std::index_sequence<Is...>, P p, Q, q) {
    return (... + (std::get<Is>(p) * std::get<Is>(q)));
}

template <class P, class Q>
auto dot_product(P p, Q q) {
    return dot_product(
        std::make_index_sequence<std::tuple_size_v<P>>{},
        p, q);
}
template <class P, class Q>
auto dot_product(P p, Q q) {
    // no helper function necessary!
    auto [...Is] = std::make_index_sequence<
        std::tuple_size_v<P>>{};
    return (... + (std::get<Is>(p) * std::get<Is>(q)));
}

この拡張による実装は簡潔であるだけでなく、これらのタプルに限定されていたコードを構造化束縛で利用可能な型に拡張します。たとえば、上記のstd::apply実装ならば、ユーザー定義型で利用可能となります。

struct Point {
    int x, y, z;
};

Point getPoint();
double calc(int, int, int);

double result = std::apply(calc, getPoint()); // 現在はng、この提案による実装ではok

ただし、これを実装すると。非テンプレートコンテキストにおいてもパラメータパックの出現を考慮しなければならなくなるため、実装の複雑さと通常コードへのコンパイル時間の増大等の影響が予想されます。

// 非テンプレートでのパック導入
auto sum_non_template(SomeConreteType tuple) {
    auto [...elems] = tuple;
    return (... + elems);
}

この提案では、この提案以外の提案(主にリフレクション関係)によって任意の場所にパックを導入するものがいくつかあるということと、この提案によるパックは構造化束縛宣言によってのみ導入され、展開される前に必ず宣言されていることから任意の場所で突然パック展開が出現することはない(ためにすべてのコンテキストでパック展開出現を考慮する必要が無い)、等の事から実装と他コードへの影響は大きくないとしています。

P1169R4 static operator()

関数呼び出し演算子operator())を、静的メンバ関数として定義できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整のみのようです。

この提案はCWGのレビューを終えていますが、ライブラリ部分についてLEWGでの投票待ちをしています。

P1222R3 A Standard flat_set

キーの検索がstd::set比で高速に行える連想コンテナ、flat_setの提案。

この提案のモチベーションやメリット、及び設計はほとんど先ほどのflat_mapと共通しています。flat_setはほぼ、常にソート済みであることが保証されるソート済みvectorであり、要素はメモリ上で連続して配置されています。それによって、検索やイテレート時のキャッシュ局所性が向上しそれらの操作を高速に行うことができ、ノードベースではないことから空間コストも削減することができます。

flat_mapの実装がキーと値のコンテナを別々に持つなど複雑だったのに対して、flat_setの場合はキーのコンテナ1本のラッパとなるのでかなり単純になります。flat_setflat_map同様にコンテナアダプタであり、そのイテレータはプロクシイテレータかつC++20イテレータとしてはランダムアクセスイテレータとなります(C++17イテレータとしては入力イテレータ)。

namespace std {

  // flat_setの宣言例
  template<class Key, class Compare = less<Key>, class Container = vector<Key>>
  class flat_set {
    
  private:
    container_type c; // exposition only
  };

}

この提案はおそらくflat_mapと足並みを揃えるために遅れており、現在はC++23に向けてLWGでレビュー中です。

P1223R4 find_last

指定された値をシーケンスの後ろから探索するfind_lastアルゴリズムの提案。

このリビジョンの変更はfind_lastファミリの戻り値としてsubrangeを返すようにしたことと、それに伴う提案全体の書き直しなどです。

以前の提案のfind_lastの戻り値は見つけた位置を指すイテレータのみでしたが、この提案では範囲の終端を指すイテレータを含めたsubrangeを返すように変更されました。これはLWGのレビューによるもので、そのAPIの変更の確認のためにSG9およびLEWGでレビューと投票を行っており、現在LEWGでの投票待ちです。

P1467R9 Extended floating-point types and standard names

C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。

以前の記事を参照

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

この提案は、このリビジョンでもってCWG/LWGのレビューを終え、C++23に向けて次の全体会議で投票にかけられる予定です。

P1642R8 Freestanding Library: Easy [utilities], [ranges], and [iterators]

[utility]<ranges><iterator>から一部のものをフリースタンディングライブラリに追加する提案。

前回の記事を参照

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

  • move_only_functionout_ptrを含めないようにした
  • invoke_r, zip, zip_transform, adjacent, adjacent_transform, to_underlying, unreachable, views::chunk_by, views::chunk, views::slide, views::join_with, ranges::to及びP2387をフリースタンディングライブラリ機能として含めるようにした

などです。

この提案はC++23を目指してLWGのレビュー待ちをしています。

P1673R7 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更は、タイポの修正と提案する文言の調整などです。

P1674R1 Evolving a Standard C++ Linear Algebra Library from the BLAS

C++標準ライブラリに提案する線形代数ライブラリの設計に関して記述した文書。

これはP1673(1つ上)の設計について記述した文書でもあります。

BLASをベースに、それをC++のインターフェスによってラップし、C++のイディオムやコアガイドラインに沿うように抽象化していくとともに、その際に生じた問題やその解決について述べられています。

P1684R2 mdarray: An Owning Multidimensional Array Analog of mdspan

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

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

  • 全体的なtypoや指定漏れの修正
  • rangeコンストラクタの追加
  • デフォルトのコンテナ型をstd::vectorとした
  • 内部コンテナへのアクセス関数を削除(勝手にresize()等ができてしまうため)
  • mdspanからの変換コンストラクタを追加
  • mdarrayの領域へのmdspanを返す.view()メンバ関数の追加

などです。

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

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

以前の記事を参照

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

  • 実装者などからのフィードバックにより、構文の変更
  • 提案する文言の改善
  • WG14への提案とWG21への提案を分離した
  • __has_embedの変更
    • 空のリソースに対しては1ではなく2を返す
  • limitパラメータの引数について、最低でも1回はマクロ展開が行われるようにした

などです。

このリビジョンでは、emptyパラメータがis_emptyに変更されています。

// empty引数
// リソースが空の場合に指定されたpp-tokenのリストを展開する
constexpr const char x[] = {
#embed "empty_file.dat" \
    is_empty((char)-1)
};
// sizeof(x) == 1
// x[0] == -1 or 255

__has_embed<header-name>header-nameに指定されたリソースが使用可能であるかを問い合わせるもので、__has_includeに対応するものです。その結果は

  • 0 : リソースが見つからない、もしくは、指定された追加のパラメータが利用可能でない場合
  • 1 : リソースが存在し空ではなく、追加のパラメータが利用可能な場合
  • 2 : リソースが存在し空であり、追加のパラメータが利用可能な場合

となります。

また、これにより#embed#__has_embedの間でTOCTOU問題が発生しますが、同じことは#include__has_includeの間でも発生しており、それは現在コンパイラによって回避されています(一度読んだファイルをキャッシュすることで回避されている)。#embedも既存コンパイラのそうした実装に乗っかることで問題を回避できます。

P2071R2 Named universal character escapes

ユニバーサル文字名として、16進エスケープシーケンスの代わりにユニコードの規定する文字名を使用できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、CWGからのフィードバックに基づく提案する文言の改善です。

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

P2093R14 Formatted output

std::formatによるフォーマットを使用しながら出力できる新I/Oライブラリstd::printの提案。

以前の記事を参照

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

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

P2174R1 Compound Literals

C99から存在している複合リテラルcompound literal)をC++でもサポートする提案。

このリビジョンでの変更は、複合リテラルの生成するオブジェクトの値カテゴリを調整した事です。

以前の提案では複合リテラル(式)の値カテゴリはprvalueとすることを提案していました。この提案では複合リテラルで使用可能な型をtrivially destructibleな型のみに制限したうえで、複合リテラルの結果はそのスコープに導入される新しい変数を参照するlvalueとなるようになりました。

これによって、C++でも複合リテラルによって生成した匿名のバッファを安全に使用できるようになるなど、C言語での複合リテラルとほぼ同等に使用できるようになり、C言語への後方互換性が向上します。

char *ptr = strcat((char [100]){0}, "like this"); // ok、安全

P2198R5 Freestanding Feature-Test Macros and Implementation-Defined Extensions

フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。

以前の記事を参照

このリビジョンでの変更は、__cpp_lib_bind_back, __cpp_lib_ranges_chunk, __cpp_lib_ranges_chunk_by, __cpp_lib_ranges_join_with, __cpp_lib_ranges_slide, __cpp_lib_ranges_to_container, __cpp_lib_reference_from_temporary, __cpp_lib_unreachableをフリースタンディングとして追加したことなどです。

この提案は再びC++23に向けて作業されており、現在はLEWGでの最終投票をパスしています。

P2266R3 Simpler implicit move

return文における暗黙のムーブを改善する提案。

以前の記事を参照

このリビジョンでの変更は、この提案の内容に関してAnnexCに追記した事などです。

この提案の採択によって、ローカル右辺値参照をそのまま返そうとするときの型が変更されます。

decltype(auto) f(int&& x) { return (x); }  // 戻り値型はint&&になる、以前はint&
int& g(int&& x) { return x; }  // ill-formedになる、以前はwell-formed

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

P2278R3 cbegin should always return a constant iterator

std::ranges::cbegin/cendを拡張して、常にconst_iteratorを返すようにする提案。

以前の記事を参照

このリビジョンでの変更は、views::all_constviews::as_constに名前を戻したことなどです。

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

P2280R4 Using unknown references in constant expressions

定数式での参照のコピーを許可する提案。

以前の記事を参照

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

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

P2286R7 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整のみのようです。

P2300R5 std::execution

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

以前の記事を参照

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

  • start_detachedは引数にvoidsenderを要求するようにした
  • recieverコンセプトはエラーチャネルにexception_ptrを使用しないようになった
  • sender_ofコンセプトとconnectカスタマイゼーションポイントでは、receiverがすべての完了を受信できる必要があることが要求されるようになった
  • get_completion_signaturescompletion_signaturesdependent_completion_signaturesのどちらかを返すようにした
  • make_completion_signaturesをよりジェネリックにした
  • receiver_adaptorは派生クラスの.get_env()メンバ関数を考慮するようになった
  • just, just_error, just_stopped, into_variantはカスタマイゼーションポイントととして再指定された

などです。

この提案は現在LWGに転送するためのLEWGでの最終投票待ちをしています。

P2302R4 std::ranges::contains

新しいアルゴリズムとしてstd::ranges::containsを追加する提案。

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

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

P2322R6 ranges::fold

rangeアルゴリズムであるranges::foldの提案。

以前の記事を参照

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

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

P2400R3 Library Evolution Report: 2021-09-28 to 2022-01-25

2021年9月から2022年1月にかけての、LEWGでのミーティングについてのまとめ。

どれくらいミーティングを行ったのか、おおまかな機能単位についての進行状況、レビューを行った提案についての議論の状況などが記載されています。

P2408R5 Ranges iterators as inputs to non-Ranges algorithms

非Rangeアルゴリズムイテレータに対する名前付き要件を、イテレータコンセプトで置き換える提案。

以前の記事を参照

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

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

P2472R2 make function_ref more functional

function_refに適応的に型消去させるためのヘルパ関数make_function_ref()を追加する提案。

以前の記事を参照

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

  • 提案する文言の追加
  • function_refに、参照の代わりにポインタを受け取る3つ目のコンストラクタを追加
  • function_refの推論補助を追加

などです。

この提案はC++23に向けて作業されることになったようです。

P2505R2 Monadic Functions for std::expected

std::optionalのMonadic interfaceをstd::expectedにも導入する提案。

以前の記事を参照

このリビジョンでの変更は、CGG(libstdc++)での実装と例へのリンク追加と提案する文言の修正などです。

この提案はバグフィックスであるとしてC++23をターゲットにすることになったようです。

P2510R2 Formatting pointers

std::formatについて、ポインタ型のフォーマットを充実させる提案。

以前の記事を参照

このリビジョンでの変更は、例(比較表)におけるフォーマットエラーを修正したこと、参照実装へのリンクを追加した事です。

P2538R1 ADL-proof std::projected

C++20 Rangeアルゴリズムが不必要な型の完全性要求をしないようにする提案。

このリビジョンでの変更は、コンパイルエラーの例を追記したことなどです。

この提案はC++23に向けて、LEWGの最終投票待ちをしています。

P2539R1 Should the output of std::print to a terminal be synchronized with the underlying stream?

提案中のstd::printP2093)が出力するストリームについて、同じストリームに対する他の出力との同期を取るようにする提案。

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

P2542R1 views::concat

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

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

  • 元の範囲が!common_range && random_access_range && sized_rangeであるときにconcat_viewcommon_rangeとするのをやめた
  • concat可能な型を示す説明専用のコンセプトconcatableを追加
    • 主に入力範囲の値型の互換性に関する要求をまとめたもの

などです。

P2546R1 Debugging Support

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

以前の記事を参照

このリビジョンでの変更は、std::breakpoint()の何もしない可能性がある実装についてのコメントを削除、SG15における投票結果を追記。

この提案はC++26に向けて、LEWGでのレビュー中です。

P2559R0 Plan for Concurrency Technical Specification Version 2

Concurrency TS v2発効に向けた作業計画書。

現在のConcurrency TS v2に向けては次の2つの提案が採択されています

次の提案は、Concurrency TS v2入りを目指してレビュー中です

予定では、2022年中にLEWGでのレビューを完了し、2023年後半に正式発効することを目指しています。

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

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

関数のdelete指定はC++11で導入され、特に、クラスの特殊メンバ関数の暗黙生成を抑制するために対応する宣言をプライベートで宣言だけしておくイディオムを置き換えることが目的でした。しかし、= delete;は特殊メンバ関数だけではなくあらゆる関数に拡張され、さらに強力な機能となりました。

導入から10年が経ち振り返ってみれば、この機能はライブラリ関数の誤使用を防止するC++11の重要な改善の1つだったと言ってよく、Andrew's C/C++ Token Count Dataset 2019.で調べてみるとその利用は4万件を超えており、標準ライブラリ内部でもより広いコミュニティ全体でも広く採用されています。

しかしその診断メッセージには問題があり、delete指定された関数が呼ばれた場合には単に削除されているとしか表示されず、なぜ削除されているのかがユーザからは不明瞭になっています。これと同様の問題は、[[deprecated]][[nodiscard]]でも報告され、これらはそれぞれC++14とC++20で理由を含むメッセージを指定できるように拡張されました。

この提案は、それらと同様に= deleteもメッセージを付加できるように拡張しようとするものです。

提案している構文は= delete("...");という形のもので、delete()に渡す文字列で理由を指定し、コンパイラはその関数が選択された場合のエラーメッセージにその文字列を出力します。

提案文書より、利用例

// フリー関数
void newapi();
void oldapi() = delete("This old API is outdated and already been removed. Please use newapi() instead.");

// 関数テンプレート
template<typename T>
struct A {/* ... */};

template<typename T>
A<T> factory(const T&) {/* process lvalue */}

template<typename T>
A<T> factory(const T&&) = delete("Using rvalue to construct A may result in dangling reference");

// メンバ関数
struct MoveOnly
{
    // ... (with move members defaulted or defined)
    MoveOnly(const MoveOnly&) = delete("Copy-construction is expensive; please use move construction instead.");
    MoveOnly& operator=(const MoveOnly&) = delete("Copy-assignment is expensive; please use move assignment instead.");
};

std::unique_ptrでの利用例

// [unique.ptr.single.general]
namespace std {
    template<class T, class D = default_delete<T>> class unique_ptr {
    public:
        // ...
        // disable copy from lvalue
        unique_ptr(const unique_ptr&) = delete(
            "unique_ptr<T> resembles unique ownership, so copy is not supported. Use move operations instead.");
        unique_ptr& operator=(const unique_ptr&) = delete(
            "unique_ptr<T> resembles unique ownership, so copy is not supported. Use move operations instead.");
    }
}

// [memory.syn]
namespace std {
    // ...
    template<class T>
        constexpr T* addressof(T& r) noexcept;
    template<class T>
        const T* addressof(const T&&) = delete("Cannot take address of rvalue.");

    // ...
    template<class T, class... Args> // T is not array
        constexpr unique_ptr<T> make_unique(Args&&... args);
    template<class T> // T is U[]
        constexpr unique_ptr<T> make_unique(size_t n);
    template<class T, class... Args> // T is U[N]
        unspecified make_unique(Args&&...) = delete(
            "make_unique<U[N]>(...) is not supported; perhaps you mean make_unique<U[]>(N) instead?");
}

// [basic.string.general]
namespace std {
    template<class charT, class traits = char_traits<charT>,
             class Allocator = allocator<charT>>
    class basic_string {
    public:
        // ...
        basic_string(nullptr_t) = delete("Construct a string from a null pointer is undefined behavior.");
}

これによって、ライブラリ開発者は今までの「あなたがやろうとしていることはわかるしそれは間違っている」というメッセージに加えて、「なぜ間違っているのか、推奨される方法を示すこともできる」という選択肢を手に入れることができ、ユーザーエクスペリエンスを改善することができます。

C++23の設計凍結時期は既に過ぎており、原則としてこの提案をC++23に含めることはできません。しかし、この提案の内容は既存機能の改善であり、=delete;のより広い採用と既存ライブラリの使いやすさの向上に不可欠なものであり後2年ほど待たせるべきでは無く、実装も簡単(clangのフォークで実装済み)である、と筆者の方は主張しておりC++23に向けて提案しています。

P2574R0 2022-05 Library Evolution Polls

2022年の5月に予定されている、LEWGでの全体投票の予定表。

次の提案が、LWGに進むための投票にかけられます。

最後の3つ以外はすべてC++23を目指しています。

P2577R0 C++ Modules Discovery in Prebuilt Library Releases

ビルド済みモジュールライブラリ配布のための規則についての提案。

これは、P2473R1の代替案となる提案です。P2473R1では、モジュール名とファイル名が対応していることを提案していましたがこの点についてコンセンサスを得られなかったようで、ライブラリ自体により近いメタデータファイルを読み取ることができるより高いレベルの抽象化が必要であるという指針が示されたようです。ただし、それは結局メタデータファイルの依存関係やビルドに必要なメタデータファイルを決定する方法などの問題を生みますが、これをどのように実現するかについてはまだコンセンサスを得られていないようです。

この提案は、ビルドシステムがビルド済みモジュールライブラリを使用するために、まず最初にそのライブラリにまつわるメタデータファイルを発見する方法についてのものです。

この提案では、今日のビルドシステムがビルド済みライブラリを利用する際に主にそのリンカ引数を取得するためにパッケージマネージャーと対話し、ビルド済みバイナリを正しく使用する方法をパッケージマネージャから取得していることに着目しています。

この実装そのものは完全に実装定義であり取得する情報(リンカ引数)の形式や意味も実装定義ですが、ビルドシステムがビルド済みライブラリを利用する際にそれらの情報を必要とし、取得しているという点は共通しています。また、これらの情報は実装定義の方法によってファイルに保存されることも共通しています。

この提案は、ビルド済みモジュールのメタデータファイルを決定論的に命名し、ビルドシステムはリンカ引数取得プロセスにおいて同時にそのメタデータファイルを取得し(リンカ引数からメタデータファイルへの変換を行うことでメタデータファイル名を得る)、それをパースすることでビルド済みモジュールライブラリの利用に必要な情報(バイナリの場所と名前、コンパイル時引数、依存関係など)を取得するというプロセス、及びそのメタデータファイルが現在のパッケージマネージャが配布するリンカ引数等の情報と同様に配布されることを提案しています。

具体的な配布形態や配布手段、メタデータがビルドシステムに伝達される手段などは実装定義となりますが、これは現在C++ビルド済みライブラリを使用するための要件でもあり、既に実装され広く使用されているはずです。

この提案によるアプローチのメリット

  • ODR違反の緩和
  • 既存のセマンティクスのみを使用する
    • この提案のほとんどの部分は現在の実装の上に構築可能であり、変更が必要なのはモジュールメタデータファイル周りのみ
    • モジュールメタデータファイルの依存関係を取り扱う必要がない

提案では、GNU/Linux環境(GNU Linkerを使用)を例にこれらのことの実装例を解説しています。この環境の場合、ライブラリはpkg-config名によって指定され、pkg-configファイルによってライブラリおよび依存ライブラリのリンカ引数が取得されます。

GNU/Linux環境の場合リンカ引数からモジュールメタデータファイルへの変換は簡単で、例えば取得されたライブラリファイル名(libfoo.a, libbar.so)の拡張子を変更してモジュールメタデータファイル(libfoo.meta-ixx-info, libbar.meta-ixx-info)を取得します。メタデータファイルがない場合はそのライブラリはモジュールを提供しないものと見做せばよく、メタデータファイルにはコンパイル時引数(コンパイラとリンカへの引数)およびモジュール依存関係グラフが含まれており(いる必要があり)、これを全て結合することでモジュールを正しく使用するために必要な情報の全てを取得することができます。

Windows環境におけるMT/MDオプションのように、的リンクと動的リンクでオプションが変化するような場合でも、現在のビルド設定からまずそのオプジョンを判別し、それに従って最初に探しに行くライブラリを変化させることで後の処理を共通化し、ビルド条件分岐を簡易に処理することができます。

おわり

この記事のMarkdownソース

[C++] rangesのパイプにアダプトするには

C++20の<ranges>のパイプ(|)に自作のview(Rangeアダプタ)を接続できるようにするにはどうすればいいのでしょうか?その方法は一見よくわからず、特に提供されてもいません。それでもできないことはないので、なんとかする話です。

パイプの実態

rangesのパイプは言語組み込みの機能ではなく、ビット論理和演算子|)をオーバーロードしたものです。そのため、単純には|オーバーロードを自作のviewに対して提供すれば良さそうに思えます。

しかし、よくあるパイプライン記法による記述を見てみると、それではダメそうなことがわかります。

int main() {
  using namespace std::views;

  auto seq = iota(1) | drop(5)
                     | filter([](int n) { return n % 2 == 0;})
                     | transform([](int n) { return n * 2; })
                     | take(5);
}

このチェーンの起点となっているのはiota(1)であり、これは入力となるrangeを生成しています。このiotaiota_viewというviewを返していて、このように引数から何かviewを生成しているものをRangeファクトリと呼びます。Rangeファクトリはこのiotaのようにパイプの最初で使用して入力となるrangeを生成するタイプのものです。今回どうにかしたいのはこれではありません。

iota(1)の後ろで、|で接続されているの(drop, filterなど)がRangeアダプタと呼ばれるもので、これはviewを入力として何かしらの変換を適用したviewを返すもので、これは必ず|の右辺に来ます。今回どうにかしたいのはこれであり、これはview型とは別のもので、どうやら型ではなさそうです。

ここで注意すべきなのは、Rangeファクトリの戻り値型は常にviewであるのに対して、Rangeアダプタの戻り値型はそうではないことです。例えばdrop(5)の戻り値型は引数に与えられた5を保持した何かを返しています。その後、|によってrangeを入力することでようやくviewを生成します(例えば、iota(1) | drop(5)の結果はdorp_viewになる)。

RangeファクトリをRF、RangeアダプタをRA、与える0個以上の引数をArgsとして、コンセプトっぽい書き方で表すと次のようになっています

  • Rangeファクトリ : RF(Args) -> view
  • Rangeアダプタ : RA(Args) -> ??
  • パイプライン : view | RA(Args) -> view

viewviewコンセプトを満たす型であることを表します。

この性質から分かるように、パイプライン演算子|)を提供しているのはRangeアダプタの戻り値型(上記の??)です。そして、自作のRangeアダプタをパイプにチェーンしたければこれらと同じことをする必要があります。

Rangeアダプタオブジェクト/Rangeアダプタクロージャオブジェクト

Rangeアダプタは関数のように見えますがそうではなく、カスタマイゼーションポイントオブジェクト(CPO)と呼ばれる関数オブジェクトの一種です。そのため、Rangeアダプタの実体のことをRangeアダプタオブジェクトと呼びます。

Rangeアダプタオブジェクトとは、1つ目の引数にviewable_rangeを受けて呼出可能なCPOでありその戻り値型はviewとなる、みたいに規定されています。その中でも、1引数のRangeアダプタオブジェクトのことを特に、Rangeアダプタクロージャオブジェクトと呼びます。

このRangeアダプタクロージャオブジェクトには規格によって変な性質が付加されています。

RangeアダプタクロージャオブジェクトCと入力のrange(正確には、viewable_range)オブジェクトrがあった時、次の2つの記述は同じ意味と効果を持ちます

C(r);   // 関数記法
r | C:  // パイプライン記法

ようはRangeアダプタクロージャオブジェクトに入力rangeを関数呼出とパイプラインの2つの方法で入力できるということです。先ほど見たように、この戻り値型はviewとなります(でなければなりません)。コンセプトを用いて書いてみると次のようになります

// 入力のrange(viewable_range)オブジェクト
viewable_range auto r = ...;

// この2つの呼び出しは同じviewを返す
view auto v1 = C(r);
view auto v2 = r | C ;

さらに、別のRangeアダプタクロージャオブジェクトDに対して、C | Dが有効である必要があり、その戻り値型はまたRangeアダプタクロージャオブジェクトである必要があります。

auto E = C | D;  // EはRangeアダプタクロージャオブジェクト

// これらの呼び出しは同じviewを返す
view auto v1 = r | C | D;
view auto v2 = r | (C | D) ;
view auto v3 = r | E ;
view auto v4 = E(r) ;

つまりは、Rangeアダプタクロージャオブジェクト同士もまた|で(事前に)接続可能であり、|は右結合となるということです。そしてその結果もRangeアダプタクロージャオブジェクトとなり、入力に対して順番に接続した時と同じ振る舞いをしなければなりません。ただし、Rangeアダプタクロージャオブジェクト同士の事前結合においては関数記法は求められていません。

auto E = D(C);  // これはできる必要はない(できない)

Rangeアダプタクロージャオブジェクトは1引数ですが、Rangeアダプタオブジェクトの中には追加の引数を受け取る者もいます(というかそっちの方が多い)。その場合、引数を渡してからrangeを入力しても、rangeと一緒に引数を渡しても、ほぼ同等な振る舞いをします。

view auto v1 = r | C(args...);
view auto v2 = C(r, args...);
view auto v3 = C(args...)(r);

つまりは、Rangeアダプタオブジェクトにその追加の引数args...をあらかじめ渡すことができて、その結果(C(args...))はRangeアダプタクロージャオブジェクトとなります。

ここまでくると、Rangeアダプタクロージャオブジェクトとは、このように追加の引数を全て部分適用して、あとは入力のrangeを受け取るだけになったRangeアダプタオブジェクト(1引数で呼出可能なRangeアダプタオブジェクト)、であることがわかります。そして、パイプで使用可能なRangeアダプタオブジェクトとはRangeアダプタクロージャオブジェクトのことです。

なお、事前結合が可能なのはRangeアダプタクロージャオブジェクトだけなので、そうではないRangeアダプタオブジェクトを事前に|で接続することはできません。

実例

int main() {
  auto seq = iota(1) | std::views::take(5);
}

ここでは、std::views::takeはRangeアダプタオブジェクトですがまだRangeアダプタクロージャオブジェクトではありません。take(5)によって必要な引数が満たされ、Rangeアダプタクロージャオブジェクトとなり、これで|で使用可能となります。そして、iota(1) | take(5)の結果はviewを生成します。

標準にあるRangeアダプタクロージャオブジェクトには例えばviews::commonがあります。

int main() {
  auto seq = iota(1) | std::views::common;
}

views::commonはすでにRangeアダプタクロージャオブジェクトであるので追加の引数を渡す必要がなく、そのままパイプで接続可能です。iota(1) | commonの結果はviewを生成します。

Rangeアダプタの事前適用は次のようになります

int main() {
  using namespace std::views;

  auto adoptor = drop(5)
               | filter([](int n) { return n % 2 == 0;})
               | transform([](int n) { return n * 2; })
               | take(5);

  auto seq = iota(1) | adoptor;
}

drop, filter, transform, take, adoptorは全てRangeアダプタオブジェクトであり、引数を与えて呼び出すことでRangeアダプタクロージャオブジェクトを生成しています。それらを|で接続して生成されたadopterもまたRangeアダプタクロージャオブジェクトであり、まだrangeは入力されていません。そして、iota(1) | adoptorviewを生成し、冒頭の全部まとめているコードと同じ振る舞いをします(ただし、ここではまだ処理を開始していないので何も始まっていません)。

自作のRangeアダプタ(view)でパイプを使用可能にするとは、そのviewのためのRangeアダプタオブジェクトを定義した上で、それそのものあるいはその呼出がRangeアダプタクロージャオブジェクトを返すようにし、そのRangeアダプタクロージャオブジェクト型に対して|オーバーロードし、なおかつ上記のRangeアダプタ(クロージャ)オブジェクトの性質を満たすようにしなければなりません。

標準ライブラリ実装による実装

やるべきことはわかりましたたが、そこはかとなく面倒臭そうですしどのように実装すれば適切なのかもよくわかりません。そこで、主要なC++標準ライブラリ実装がRangeアダプタをどのように実装しているのかを見てみます。

GCC 10

例えば、filter_viewview型)とviews::filter(Rangeアダプタオブジェクト)を見てみると、次のように定義されています

namespace std::ranges {

  ...

  template<input_range _Vp,
           indirect_unary_predicate<iterator_t<_Vp>> _Pred>
    requires view<_Vp> && is_object_v<_Pred>
  class filter_view : public view_interface<filter_view<_Vp, _Pred>>
  {
    ...
  };

  ...

  namespace views
  {
    inline constexpr __adaptor::_RangeAdaptor filter
      = [] <viewable_range _Range, typename _Pred> (_Range&& __r, _Pred&& __p)
      {
        return filter_view{std::forward<_Range>(__r), std::forward<_Pred>(__p)};
      };
  } // namespace views

}

また、Rangeアダプタクロージャオブジェクトviews::commoncommon_viewは次のように定義されています。

namespace std::ranges {

  ...

  template<view _Vp>
    requires (!common_range<_Vp>) && copyable<iterator_t<_Vp>>
  class common_view : public view_interface<common_view<_Vp>>
  {
    ...
  };

  ...

  namespace views
  {
    inline constexpr __adaptor::_RangeAdaptorClosure common
      = [] <viewable_range _Range> (_Range&& __r)
      {
        if constexpr (common_range<_Range>
                  && requires { views::all(std::forward<_Range>(__r)); })
          return views::all(std::forward<_Range>(__r));
        else
          return common_view{std::forward<_Range>(__r)};
      };

  } // namespace views
}

Rangeアダプタの実体型は__adaptor::_RangeAdaptor、Rangeアダプタクロージャオブジェクトの実体型は__adaptor::_RangeAdaptorClosureであるようです。省略しますが、他のRangeアダプタに対してもこれらと同様の実装方針が採られています。

まずはRangeアダプタの実装を見てみます。

template<typename _Callable>
struct _RangeAdaptor
{
protected:
  [[no_unique_address]]
   __detail::__maybe_present_t<!is_default_constructible_v<_Callable>, 
                               _Callable> _M_callable;

public:

  constexpr
  _RangeAdaptor(const _Callable& = {})
   requires is_default_constructible_v<_Callable>
  { }

  constexpr
  _RangeAdaptor(_Callable __callable)
   requires (!is_default_constructible_v<_Callable>)
   : _M_callable(std::move(__callable))
  { }

  template<typename... _Args>
    requires (sizeof...(_Args) >= 1)
    constexpr auto
    operator()(_Args&&... __args) const
    {
     // [range.adaptor.object]: If a range adaptor object accepts more
     // than one argument, then the following expressions are equivalent:
     //
     //   (1) adaptor(range, args...)
     //   (2) adaptor(args...)(range)
     //   (3) range | adaptor(args...)
     //
     // In this case, adaptor(args...) is a range adaptor closure object.
     //
     // We handle (1) and (2) here, and (3) is just a special case of a
     // more general case already handled by _RangeAdaptorClosure.
     if constexpr (is_invocable_v<_Callable, _Args...>)
       {
          static_assert(sizeof...(_Args) != 1,
                  "a _RangeAdaptor that accepts only one argument "
                  "should be defined as a _RangeAdaptorClosure");
          // Here we handle adaptor(range, args...) -- just forward all
          // arguments to the underlying adaptor routine.
          return _Callable{}(std::forward<_Args>(__args)...);
       }
     else
       {
          // Here we handle adaptor(args...)(range).
          // Given args..., we return a _RangeAdaptorClosure that takes a
          // range argument, such that (2) is equivalent to (1).
          //
          // We need to be careful about how we capture args... in this
          // closure.  By using __maybe_refwrap, we capture lvalue
          // references by reference (through a reference_wrapper) and
          // otherwise capture by value.
          auto __closure
            = [...__args(__maybe_refwrap(std::forward<_Args>(__args)))]
              <typename _Range> (_Range&& __r) {
                // This static_cast has two purposes: it forwards a
                // reference_wrapper<T> capture as a T&, and otherwise
                // forwards the captured argument as an rvalue.
                return _Callable{}(std::forward<_Range>(__r),
                          (static_cast<unwrap_reference_t
                                           <remove_const_t<decltype(__args)>>>
                            (__args))...);
              };
          using _ClosureType = decltype(__closure);
          return _RangeAdaptorClosure<_ClosureType>(std::move(__closure));
       }
   }
};

template<typename _Callable>
  _RangeAdaptor(_Callable) -> _RangeAdaptor<_Callable>;

めちゃくちゃ複雑なので細かく解説はしませんが、ここではRangeアダプタオブジェクト(not クロージャオブジェクト)の追加の引数を事前に受け取って保持しておくことができる、という性質を実装しています。

view auto v1 = r | C(args...);  // #1
view auto v2 = C(r, args...);   // #2
view auto v3 = C(args...)(r);   // #3

このクラスは何か呼出可能と思われるもの(_Callable)を受け取って、それがデフォルト構築不可能な場合のみメンバ(_M_callable)に保存しています。最初に見た使われ方では、ラムダ式によって初期化されていて、そのラムダ式で対象のviewに合わせたRangeアダプタの処理が実装されていました。

Rangeアダプタの性質を実装しているのはoperator()内で、ここでは上記#2, #3の2つのケースを処理していて、#1はRangeアダプタクロージャオブジェクト(_RangeAdaptorClosure)のパイプライン演算子に委ねています。

operator()内、constexpr iftrue分岐では、C(r, args...)を処理しています。この場合は引数列__argsの1つ目に入力rangeを含んでおり、残りの引数を保存する必要もないため、それらをそのまま転送して_Callableを呼び出し、それによってRangeアダプタを実行します。この場合の戻り値はviewとなります。

constexpr iffalse分岐では、C(args...)(r)を処理しています。この場合は引数列__argsに入力rangeは含まれておらず、それは後から入力(|or())されるので、渡された引数列を保存して後から入力rangeと共に_Callableの遅延呼び出しを行う呼び出し可能ラッパを返しています。それはラムダ式で実装されており、引数の保存はキャプチャによって行われています。この場合の戻り値はRangeアダプタクロージャオブジェクトであり、引数と_Callableを内包したラムダ式_RangeAdaptorClosureに包んで返しています。

どちらの場合でもメンバに保存した_M_callableを使用していませんが、この#1, #2のケースの場合はどちらも_Callableがデフォルト構築可能であることを仮定することができます。なぜなら、この二つの場合にわたってくる_Callableは状態を持たないラムダ式であり、C++20からそれはデフォルト構築可能であり、それはRangeアダプタオブジェクト定義時に渡されるものだからです。_M_callableを使用する必要があるのは実はC(args...)相当の部分適用をおこなった場合のみで、それはRangeアダプタクロージャオブジェクト(_RangeAdaptorClosure)において処理されます。

次はそのRangeアダプタクロージャオブジェクトの実装を見てみましょう。

template<typename _Callable>
struct _RangeAdaptorClosure : public _RangeAdaptor<_Callable>
{
  using _RangeAdaptor<_Callable>::_RangeAdaptor;

  template<viewable_range _Range>
    requires requires { declval<_Callable>()(declval<_Range>()); }
  constexpr auto
  operator()(_Range&& __r) const
  {
    if constexpr (is_default_constructible_v<_Callable>)
      return _Callable{}(std::forward<_Range>(__r));
    else
      return this->_M_callable(std::forward<_Range>(__r));
  }

  // 1. range | RACO -> view
  template<viewable_range _Range>
    requires requires { declval<_Callable>()(declval<_Range>()); }
  friend constexpr auto
  operator|(_Range&& __r, const _RangeAdaptorClosure& __o)
  { return __o(std::forward<_Range>(__r)); }

  // 2. RACO | RACO -> RACO
  template<typename _Tp>
  friend constexpr auto
  operator|(const _RangeAdaptorClosure<_Tp>& __x,
            const _RangeAdaptorClosure& __y)
  {
    if constexpr (is_default_constructible_v<_Tp>
                  && is_default_constructible_v<_Callable>)
      {
        auto __closure = [] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | decltype(__x){} | decltype(__y){};
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
    else if constexpr (is_default_constructible_v<_Tp>
                       && !is_default_constructible_v<_Callable>)
      {
        auto __closure = [__y] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | decltype(__x){} | __y;
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
    else if constexpr (!is_default_constructible_v<_Tp>
                       && is_default_constructible_v<_Callable>)
      {
        auto __closure = [__x] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | __x | decltype(__y){};
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
    else
      {
        auto __closure = [__x, __y] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | __x | __y;
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
  }
};

template<typename _Callable>
  _RangeAdaptorClosure(_Callable) -> _RangeAdaptorClosure<_Callable>;

まず見て分かるように、_RangeAdaptorClosure_RangeAdaptorを継承していて、受けた呼出可能なものの保持などは先ほどの_RangeAdaptorと共通です。そして、2つのoperator|オーバーロードが定義されています。この実装方法はHidden friendsと呼ばれる実装になっています。

Rangeアダプタクロージャオブジェクトは1つのrangeを関数呼出によって入力することができ、それはoperator()で実装されています。ここで、_Callableがデフォルト構築可能かによって_RangeAdaptor::_M_callableを使用するかの切り替えが初めて行われており、_Callableがデフォルト構築可能ではない場合というのは、Rangeアダプタに追加の引数を部分適用した結果生成されたRangeアダプタクロージャオブジェクトの場合のみで、それは_RangeAdaptor::operator()constexpr iffalseパートの結果として生成されます。

1つ目のoperator|オーバーロードは追記コメントにあるように、左辺にrangeを受けて結合する場合の|オーバーロードです(range | RACO -> view)。この場合は先ほどの関数呼び出しと同じことになるので、operator()に委譲されています。わかりづらいですが、2つ目の引数の__o*thisに対応しています。

2つ目のoperator|オーバーロードは残った振る舞い、すなわちRangeアダプタクロージャオブジェクト同士の事前結合を担っています。なんかifで4分岐しているのは、引数の_RangeAdaptorClosureオブジェクトの_Callableがデフォルト構築可能か否かでメンバの_M_callableを参照するかが変化するためで、それが引数2つ分の2x2で4パターンの分岐になっています。実際の結合処理は1つ目の|に委譲していて、その処理はラムダ式で記述して、そのラムダ式のオブジェクトを_RangeAdaptorClosureに包んで返すことで戻り値は再びRangeアダプタクロージャオブジェクトになります。ifの分岐の差異は必要な場合にのみ引数__x, __yを返すラムダにキャプチャしていることです。

GCC10の実装では、Rangeアダプタとしての動作はステートレスなラムダ式で与えられ、Rangeアダプタオブジェクトはそれを受けたこの2つの型のどちらかのオブジェクトとなり、ややこしい性質の実装はこの2つの型に集約され共通化されています。<ranges>のパイプライン演算子_RangeAdaptorClosureに定義されたものが常に使用されています。

GCC 11

GCC11になると、この実装が少し変化していました。

namespace std::ranges {

  // views::filter
  namespace views
  {
    namespace __detail
    {
      template<typename _Range, typename _Pred>
          concept __can_filter_view
            = requires { filter_view(std::declval<_Range>(), std::declval<_Pred>()); };
    } // namespace __detail

    struct _Filter : __adaptor::_RangeAdaptor<_Filter>
    {
      template<viewable_range _Range, typename _Pred>
        requires __detail::__can_filter_view<_Range, _Pred>
        constexpr auto
        operator()(_Range&& __r, _Pred&& __p) const
        {
          return filter_view(std::forward<_Range>(__r), std::forward<_Pred>(__p));
        }

      using _RangeAdaptor<_Filter>::operator();
      static constexpr int _S_arity = 2;
      static constexpr bool _S_has_simple_extra_args = true;
    };

    inline constexpr _Filter filter;
  } // namespace views

  // views::common
  namespace views
  {
    namespace __detail
    {
      template<typename _Range>
        concept __already_common = common_range<_Range>
          && requires { views::all(std::declval<_Range>()); };

      template<typename _Range>
        concept __can_common_view
          = requires { common_view{std::declval<_Range>()}; };
    } // namespace __detail

    struct _Common : __adaptor::_RangeAdaptorClosure
    {
      template<viewable_range _Range>
        requires __detail::__already_common<_Range>
          || __detail::__can_common_view<_Range>
        constexpr auto
        operator()(_Range&& __r) const
        {
          if constexpr (__detail::__already_common<_Range>)
            return views::all(std::forward<_Range>(__r));
          else
            return common_view{std::forward<_Range>(__r)};
        }

      static constexpr bool _S_has_simple_call_op = true;
    };

    inline constexpr _Common common;
  } // namespace views
}

filtercommonだけを見ても、_RangeAdaptorClosureとかの名前そのものは変わっていなくてもその使い方が大きく変わっていることがわかります。どちらも継承して使用されていて、_RangeAdaptorはCRTPになっています。それらに目を向けてみると

// The base class of every range adaptor non-closure.
//
// The static data member _Derived::_S_arity must contain the total number of
// arguments that the adaptor takes, and the class _Derived must introduce
// _RangeAdaptor::operator() into the class scope via a using-declaration.
//
// The optional static data member _Derived::_S_has_simple_extra_args should
// be defined to true if the behavior of this adaptor is independent of the
// constness/value category of the extra arguments.  This data member could
// also be defined as a variable template parameterized by the types of the
// extra arguments.
template<typename _Derived>
struct _RangeAdaptor
{
  // Partially apply the arguments __args to the range adaptor _Derived,
  // returning a range adaptor closure object.
  template<typename... _Args>
    requires __adaptor_partial_app_viable<_Derived, _Args...>
    constexpr auto
    operator()(_Args&&... __args) const
    {
      return _Partial<_Derived, decay_t<_Args>...>{std::forward<_Args>(__args)...};
    }
};
// The base class of every range adaptor closure.
//
// The derived class should define the optional static data member
// _S_has_simple_call_op to true if the behavior of this adaptor is
// independent of the constness/value category of the adaptor object.
struct _RangeAdaptorClosure
{

  // 1. range | RACO -> view  ※説明のため追記
  // range | adaptor is equivalent to adaptor(range).
  template<typename _Self, typename _Range>
    requires derived_from<remove_cvref_t<_Self>, _RangeAdaptorClosure>
      && __adaptor_invocable<_Self, _Range>
    friend constexpr auto
    operator|(_Range&& __r, _Self&& __self)
    { return std::forward<_Self>(__self)(std::forward<_Range>(__r)); }

  // 2. RACO | RACO -> RACO ※説明のため追記
  // Compose the adaptors __lhs and __rhs into a pipeline, returning
  // another range adaptor closure object.
  template<typename _Lhs, typename _Rhs>
    requires derived_from<_Lhs, _RangeAdaptorClosure>
      && derived_from<_Rhs, _RangeAdaptorClosure>
    friend constexpr auto
    operator|(_Lhs __lhs, _Rhs __rhs)
    { return _Pipe<_Lhs, _Rhs>{std::move(__lhs), std::move(__rhs)}; }
};

この二つのクラスの実装そのものはかなりシンプルになっています。_RangeAdaptor::operator()でRangeアダプタの性質(追加の引数を部分適用してRangeアダプタクロージャオブジェクトを生成する)を実装していて、_RangeAdaptorClosure::operator|でパイプライン演算子を実装しているのも先ほどと変わりありません。

ただし、どちらの場合もその実装詳細を_Partial_Pipeという二つの謎のクラスに委譲しています。これらのクラスの実装は複雑で長いので省略しますが、_PartialはRangeアダプタの追加の引数を保存してRangeアダプタクロージャオブジェクトとなる呼び出し可能なラッパ型で、_PipeはRangeアダプタクロージャオブジェクト2つを保持したRangeアダプタクロージャオブジェクトとなる呼び出し可能なラッパ型です。

_Partial_Pipeはどちらも部分特殊化を使用することで、渡された追加の引数/Rangeアダプタクロージャオブジェクトを効率的に保持しようとします。_RangeAdaptor/_RangeAdaptorClosureを継承するクラス型に_S_has_simple_call_opとか_S_has_simple_extra_argsだとかの静的メンバが生えているのは、これを適切に制御するためでもあります。

実装が細分化され分量が増えて利用方法も変化していますが、基本的にやっていることはGCC10の時と大きく変わってはいません。

これらの変更はおそらく、P2281の採択とP2287を意識したものだと思われます(どちらもC++23では採択済)。

MSVC

同じように、MSVCの実装も見てみます。view型とそのアダプタの関係性は変わらないので、以降はRangeアダプタだけに焦点を絞ります。

views::filter(Rangeアダプタオブジェクト)

namespace views {
    struct _Filter_fn {
        // clang-format off
        template <viewable_range _Rng, class _Pr>
        _NODISCARD constexpr auto operator()(_Rng&& _Range, _Pr&& _Pred) const noexcept(noexcept(
            filter_view(_STD forward<_Rng>(_Range), _STD forward<_Pr>(_Pred)))) requires requires {
            filter_view(static_cast<_Rng&&>(_Range), _STD forward<_Pr>(_Pred));
        } {
            // clang-format on
            return filter_view(_STD forward<_Rng>(_Range), _STD forward<_Pr>(_Pred));
        }

        // clang-format off
        template <class _Pr>
            requires constructible_from<decay_t<_Pr>, _Pr>
        _NODISCARD constexpr auto operator()(_Pr&& _Pred) const
            noexcept(is_nothrow_constructible_v<decay_t<_Pr>, _Pr>) {
            // clang-format on
            return _Range_closure<_Filter_fn, decay_t<_Pr>>{_STD forward<_Pr>(_Pred)};
        }
    };

    inline constexpr _Filter_fn filter;
} // namespace views

views::common(Rangeアダプタクロージャオブジェクト)

namespace views {
    class _Common_fn : public _Pipe::_Base<_Common_fn> {
    private:
        enum class _St { _None, _All, _Common };

        template <class _Rng>
        _NODISCARD static _CONSTEVAL _Choice_t<_St> _Choose() noexcept {
            if constexpr (common_range<_Rng>) {
                return {_St::_All, noexcept(views::all(_STD declval<_Rng>()))};
            } else if constexpr (copyable<iterator_t<_Rng>>) {
                return {_St::_Common, noexcept(common_view{_STD declval<_Rng>()})};
            } else {
                return {_St::_None};
            }
        }

        template <class _Rng>
        static constexpr _Choice_t<_St> _Choice = _Choose<_Rng>();

    public:
        // clang-format off
        template <viewable_range _Rng>
            requires (_Choice<_Rng>._Strategy != _St::_None)
        _NODISCARD constexpr auto operator()(_Rng&& _Range) const noexcept(_Choice<_Rng>._No_throw) {
            // clang-format on
            constexpr _St _Strat = _Choice<_Rng>._Strategy;

            if constexpr (_Strat == _St::_All) {
                return views::all(_STD forward<_Rng>(_Range));
            } else if constexpr (_Strat == _St::_Common) {
                return common_view{_STD forward<_Rng>(_Range)};
            } else {
                static_assert(_Always_false<_Rng>, "Should be unreachable");
            }
        }
    };

    inline constexpr _Common_fn common;
} // namespace views

雰囲気はGCC11の実装に似ています。Rangeアダプタオブジェクトでは、rangeを受け取る方の呼び出しをoperator()でその場(Rangeアダプタ型内部)で定義し外出し(共通化)しておらず、追加の引数を部分適用してRangeアダプタクロージャオブジェクトを返す呼出では_Range_closureという型に自身と追加の引数をラップして返しています。

Rangeアダプタクロージャオブジェクトでは、_Pipe::_Baseといういかにもな名前の型を継承しています。どうやら、パイプライン演算子はそこで定義されているようです。

まずはRangeアダプタの引数の部分適用時に返されるラッパ型_Range_closureを見てみます。

template <class _Fn, class... _Types>
class _Range_closure : public _Pipe::_Base<_Range_closure<_Fn, _Types...>> {
public:
    // We assume that _Fn is the type of a customization point object. That means
    // 1. The behavior of operator() is independent of cvref qualifiers, so we can use `invocable<_Fn, ` without
    //    loss of generality, and
    // 2. _Fn must be default-constructible and stateless, so we can create instances "on-the-fly" and avoid
    //    storing a copy.

    // Types(追加の引数)は参照やconstではないこと
    _STL_INTERNAL_STATIC_ASSERT((same_as<decay_t<_Types>, _Types> && ...));
    // _Fn(Rangeアダプタ型 not クロージャ型)はステートレスクラスかつデフォルト構築可能であること
    _STL_INTERNAL_STATIC_ASSERT(is_empty_v<_Fn>&& is_default_constructible_v<_Fn>);

    // clang-format off
    template <class... _UTypes>
        requires (same_as<decay_t<_UTypes>, _Types> && ...)
    constexpr explicit _Range_closure(_UTypes&&... _Args) noexcept(
        conjunction_v<is_nothrow_constructible<_Types, _UTypes>...>)
        : _Captures(_STD forward<_UTypes>(_Args)...) {}
    // clang-format on

    void operator()(auto&&) &       = delete;
    void operator()(auto&&) const&  = delete;
    void operator()(auto&&) &&      = delete;
    void operator()(auto&&) const&& = delete;

    using _Indices = index_sequence_for<_Types...>;

    template <class _Ty>
        requires invocable<_Fn, _Ty, _Types&...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) & noexcept(
        noexcept(_Call(*this, _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(*this, _STD forward<_Ty>(_Arg), _Indices{});
    }

    template <class _Ty>
        requires invocable<_Fn, _Ty, const _Types&...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) const& noexcept(
        noexcept(_Call(*this, _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(*this, _STD forward<_Ty>(_Arg), _Indices{});
    }

    template <class _Ty>
        requires invocable<_Fn, _Ty, _Types...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) && noexcept(
        noexcept(_Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{});
    }

    template <class _Ty>
        requires invocable<_Fn, _Ty, const _Types...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) const&& noexcept(
        noexcept(_Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{});
    }

private:
    template <class _SelfTy, class _Ty, size_t... _Idx>
    static constexpr decltype(auto) _Call(_SelfTy&& _Self, _Ty&& _Arg, index_sequence<_Idx...>) noexcept(
        noexcept(_Fn{}(_STD forward<_Ty>(_Arg), _STD get<_Idx>(_STD forward<_SelfTy>(_Self)._Captures)...))) {
        _STL_INTERNAL_STATIC_ASSERT(same_as<index_sequence<_Idx...>, _Indices>);
        return _Fn{}(_STD forward<_Ty>(_Arg), _STD get<_Idx>(_STD forward<_SelfTy>(_Self)._Captures)...);
    }

    tuple<_Types...> _Captures;
};

やたら複雑ですが、4つあるoperator()は値カテゴリの違いでムーブしたりしなかったりしているだけで、実質同じことをしています。テンプレートパラメータの_Fnviews::filterの実装で見たように、まだクロージャではないRangeアダプタ型です。追加の引数はTypes...で、静的アサートにも表れているように参照やconstを外すことでコピー/ムーブして(メンバのtupleオブジェクトに)保持されています。

このクラスはRangeアダプタオブジェクトとその追加の引数をラップしてRangeアダプタクロージャオブジェクトとなるものなので、operator()がやることは入力のrangeを受け取って、ラップしているRangeアダプタに同じくラップしている追加の引数とともに渡してviewを生成することです。その実態は_Call()関数であり、_Arg(入力rangeオブジェクト)->_Captures(追加の引数列)をこの順番で_Fn(Rangeアダプタ型)の関数呼び出し演算子に渡しています。_Fnは常にデフォルト構築可能であること強制することで追加のストレージを節約しており、_Fnの関数呼び出し演算子_Argと共に呼び出すとそこで直接定義されている入力rangeを受け取る処理が実行されます。例えばviews::filterの場合は1つ目のoperator()がそれにあたり、filter_viewの生成を行っています。

_Range_closureもまた、_Pipe::_Baseを継承することで|の実装を委譲しています。次はこれを見てみます。

namespace _Pipe {
  // clang-format off
  // C | R = C(R)の呼び出しが可能かを調べるコンセプト
  template <class _Left, class _Right>
  concept _Can_pipe = requires(_Left&& __l, _Right&& __r) {
      static_cast<_Right&&>(__r)(static_cast<_Left&&>(__l));
  };

  // Rangeアダプタクロージャオブジェクト同士の結合の要件をチェックするコンセプト
  // 共に、コピーorムーブできること
  template <class _Left, class _Right>
  concept _Can_compose = constructible_from<remove_cvref_t<_Left>, _Left>
      && constructible_from<remove_cvref_t<_Right>, _Right>;
  // clang-format on

  // 前方宣言
  template <class, class>
  struct _Pipeline;

  // Rangeアダプタクロージャオブジェクト型にパイプラインを提供する共通クラス
  template <class _Derived>
  struct _Base {
      template <class _Other>
          requires _Can_compose<_Derived, _Other>
      constexpr auto operator|(_Base<_Other>&& __r) && noexcept(
          noexcept(_Pipeline{static_cast<_Derived&&>(*this), static_cast<_Other&&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<_Derived&&>(*this), static_cast<_Other&&>(__r)};
      }

      template <class _Other>
          requires _Can_compose<_Derived, const _Other&>
      constexpr auto operator|(const _Base<_Other>& __r) && noexcept(
          noexcept(_Pipeline{static_cast<_Derived&&>(*this), static_cast<const _Other&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<_Derived&&>(*this), static_cast<const _Other&>(__r)};
      }

      template <class _Other>
          requires _Can_compose<const _Derived&, _Other>
      constexpr auto operator|(_Base<_Other>&& __r) const& noexcept(
          noexcept(_Pipeline{static_cast<const _Derived&>(*this), static_cast<_Other&&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<const _Derived&>(*this), static_cast<_Other&&>(__r)};
      }

      template <class _Other>
          requires _Can_compose<const _Derived&, const _Other&>
      constexpr auto operator|(const _Base<_Other>& __r) const& noexcept(
          noexcept(_Pipeline{static_cast<const _Derived&>(*this), static_cast<const _Other&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<const _Derived&>(*this), static_cast<const _Other&>(__r)};
      }

      template <_Can_pipe<const _Derived&> _Left>
      friend constexpr auto operator|(_Left&& __l, const _Base& __r)
#ifdef __EDG__ // TRANSITION, VSO-1222776
          noexcept(noexcept(_STD declval<const _Derived&>()(_STD forward<_Left>(__l))))
#else // ^^^ workaround / no workaround vvv
          noexcept(noexcept(static_cast<const _Derived&>(__r)(_STD forward<_Left>(__l))))
#endif // TRANSITION, VSO-1222776
      {
          return static_cast<const _Derived&>(__r)(_STD forward<_Left>(__l));
      }

      template <_Can_pipe<_Derived> _Left>
      friend constexpr auto operator|(_Left&& __l, _Base&& __r)
#ifdef __EDG__ // TRANSITION, VSO-1222776
          noexcept(noexcept(_STD declval<_Derived>()(_STD forward<_Left>(__l))))
#else // ^^^ workaround / no workaround vvv
          noexcept(noexcept(static_cast<_Derived&&>(__r)(_STD forward<_Left>(__l))))
#endif // TRANSITION, VSO-1222776
      {
          return static_cast<_Derived&&>(__r)(_STD forward<_Left>(__l));
      }
  };


  // Rangeアダプタクロージャオブジェクト同士の事前結合を担うラッパ型
  template <class _Left, class _Right>
  struct _Pipeline : _Base<_Pipeline<_Left, _Right>> {
      /* [[no_unique_address]] */ _Left __l;
      /* [[no_unique_address]] */ _Right __r;

      template <class _Ty1, class _Ty2>
      constexpr explicit _Pipeline(_Ty1&& _Val1, _Ty2&& _Val2) noexcept(
          is_nothrow_convertible_v<_Ty1, _Left>&& is_nothrow_convertible_v<_Ty2, _Right>)
          : __l(_STD forward<_Ty1>(_Val1)), __r(_STD forward<_Ty2>(_Val2)) {}

      template <class _Ty>
      _NODISCARD constexpr auto operator()(_Ty&& _Val) noexcept(
          noexcept(__r(__l(_STD forward<_Ty>(_Val))))) requires requires {
          __r(__l(static_cast<_Ty&&>(_Val)));
      }
      { return __r(__l(_STD forward<_Ty>(_Val))); }

      template <class _Ty>
      _NODISCARD constexpr auto operator()(_Ty&& _Val) const
          noexcept(noexcept(__r(__l(_STD forward<_Ty>(_Val))))) requires requires {
          __r(__l(static_cast<_Ty&&>(_Val)));
      }
      { return __r(__l(_STD forward<_Ty>(_Val))); }
  };

  template <class _Ty1, class _Ty2>
  _Pipeline(_Ty1, _Ty2) -> _Pipeline<_Ty1, _Ty2>;
} // namespace _Pipe

_Pipe::_Baseには2種類6つのoperator|が定義されています。_Can_composeコンセプトで制約されている最初の4つがRangeアダプタクロージャオブジェクト同士の事前結合を行うパイプ演算子で、4つあるのは値カテゴリの違いで*thisを適応的にムーブするためです。このクラスはCRTPで利用され、_Derived型は常にRangeアダプタクロージャオブジェクト型です。views::commonのように最初からRangeアダプタクロージャオブジェクトである場合は_Derivedはステートレスですが、views::filterのように追加の引数を受け取る場合は_Derivedは何かを保持しています。この結果は再びRangeアダプタクロージャオブジェクトとなるため、_Pipeline型がそのラッピングを担っています。_Pipeline_Pipe::_Baseを継承することで|の実装を省略しています。その関数呼び出し演算子ではrange | __l | __rの接続が__r(__l(range))となるように呼び出しを行っています。

残った2つがrange | RACO -> viewの形の接続(|によるrangeの入力)を行っているパイプ演算子で、この場合の_Left型の__lが入力のrangeオブジェクトです。__r*thisであり、パラメータを明示化していることで不要なキャストやチェックが省略できています(ここにはDeducing thisの有用性の一端を垣間見ることができます)。この場合は__l | __rの形の接続が__r(__L)の呼び出しと同等になる必要があり、そのような呼び出しを行っています。
なぜこっちだけHidden friendsになっているかというと、この場合はthisパラメータが|の右辺に来るように定義する必要があるため非メンバで定義せざるを得ないからです(メンバ定義だと常に左辺にthisパラメータが来てしまう)。

GCCがRangeアダプタオブジェクトのoperator()(引数を部分適用する方)の実装をも共通クラスに外出ししていたのに対して、MSVCはそうしていません。そのおかげだと思いますが、実装がだいぶシンプルに収まっています(値カテゴリの違いで必要になる4つのオーバーロードから目を逸らしつつ)。

どうやらMSVCは早い段階からこのような実装となっていたようで、P2281P2287の二つの変更はいずれもMSVCのこれらの実装をモデルケースとして標準に反映するものでした。

clang

views::filter

namespace views {
namespace __filter {
  struct __fn {
    template<class _Range, class _Pred>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Range&& __range, _Pred&& __pred) const
      noexcept(noexcept(filter_view(std::forward<_Range>(__range), std::forward<_Pred>(__pred))))
      -> decltype(      filter_view(std::forward<_Range>(__range), std::forward<_Pred>(__pred)))
      { return          filter_view(std::forward<_Range>(__range), std::forward<_Pred>(__pred)); }

    template<class _Pred>
      requires constructible_from<decay_t<_Pred>, _Pred>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Pred&& __pred) const
      noexcept(is_nothrow_constructible_v<decay_t<_Pred>, _Pred>)
    { return __range_adaptor_closure_t(std::__bind_back(*this, std::forward<_Pred>(__pred))); }
  };
} // namespace __filter

inline namespace __cpo {
  inline constexpr auto filter = __filter::__fn{};
} // namespace __cpo
} // namespace views

views::common

namespace views {
namespace __common {
  struct __fn : __range_adaptor_closure<__fn> {
    template<class _Range>
      requires common_range<_Range>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Range&& __range) const
      noexcept(noexcept(views::all(std::forward<_Range>(__range))))
      -> decltype(      views::all(std::forward<_Range>(__range)))
      { return          views::all(std::forward<_Range>(__range)); }

    template<class _Range>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Range&& __range) const
      noexcept(noexcept(common_view{std::forward<_Range>(__range)}))
      -> decltype(      common_view{std::forward<_Range>(__range)})
      { return          common_view{std::forward<_Range>(__range)}; }
  };
} // namespace __common

inline namespace __cpo {
  inline constexpr auto common = __common::__fn{};
} // namespace __cpo
} // namespace views

clangの実装はMSVCのものにかなり近いことが分かるでしょう。Rangeアダプタの共通実装は提供しておらず、Rangeアダプタクロージャオブジェクトの共通実装は__range_adaptor_closure_t__range_adaptor_closureというCRTP型を使用しています。

初期コミット時のメッセージによれば、P2287をベースとした実装であり、P2287はMSVCの実装を参考にしていたので、結果として似た実装となっているようです。

// CRTP base that one can derive from in order to be considered a range adaptor closure
// by the library. When deriving from this class, a pipe operator will be provided to
// make the following hold:
// - `x | f` is equivalent to `f(x)`
// - `f1 | f2` is an adaptor closure `g` such that `g(x)` is equivalent to `f2(f1(x))`
template <class _Tp>
struct __range_adaptor_closure;

// Type that wraps an arbitrary function object and makes it into a range adaptor closure,
// i.e. something that can be called via the `x | f` notation.
template <class _Fn>
struct __range_adaptor_closure_t : _Fn, __range_adaptor_closure<__range_adaptor_closure_t<_Fn>> {
    constexpr explicit __range_adaptor_closure_t(_Fn&& __f) : _Fn(std::move(__f)) { }
};

template <class _Tp>
concept _RangeAdaptorClosure = derived_from<remove_cvref_t<_Tp>, __range_adaptor_closure<remove_cvref_t<_Tp>>>;

template <class _Tp>
struct __range_adaptor_closure {
    template <ranges::viewable_range _View, _RangeAdaptorClosure _Closure>
        requires same_as<_Tp, remove_cvref_t<_Closure>> &&
                 invocable<_Closure, _View>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    friend constexpr decltype(auto) operator|(_View&& __view, _Closure&& __closure)
        noexcept(is_nothrow_invocable_v<_Closure, _View>)
    { return std::invoke(std::forward<_Closure>(__closure), std::forward<_View>(__view)); }

    template <_RangeAdaptorClosure _Closure, _RangeAdaptorClosure _OtherClosure>
        requires same_as<_Tp, remove_cvref_t<_Closure>> &&
                 constructible_from<decay_t<_Closure>, _Closure> &&
                 constructible_from<decay_t<_OtherClosure>, _OtherClosure>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    friend constexpr auto operator|(_Closure&& __c1, _OtherClosure&& __c2)
        noexcept(is_nothrow_constructible_v<decay_t<_Closure>, _Closure> &&
                 is_nothrow_constructible_v<decay_t<_OtherClosure>, _OtherClosure>)
    { return __range_adaptor_closure_t(std::__compose(std::forward<_OtherClosure>(__c2), std::forward<_Closure>(__c1))); }
};

__range_adaptor_closure_tのテンプレートパラメータ_FnはRangeアダプタ型で、__range_adaptor_closure_t_Fn__range_adaptor_closureを基底に持ち、operator|__range_adaptor_closureで定義されています。

__range_adaptor_closureもまたCRTPで、operator|は2つともHidden friendsであり、1つ目がrangeを入力する方、2つ目がRangeアダプタクロージャオブジェクト同士の接続をする方、に対応しています。どちらでも、_Closure型の方がthisパラメータで_Tpと同じ型となることが制約されています。

Rangeアダプタ型(の部分適用operator())で使用される場合_Tpは一つ上の__range_adaptor_closure_tとなり、Rangeアダプタクロージャオブジェクト型で(継承して)使用される場合は_TpはそのRangeアダプタクロージャオブジェクト型となります。__range_adaptor_closure::operator|での*thisとは使われ方に応じてそのどちらかの型であり、Rangeアダプタの処理は_Fnの関数呼び出し演算子に実装されていて(__range_adaptor_closure_t<_Fn>の場合は_Fnを継承することで実装していて)、thisパラメータ__closureはそれらを呼び出すことができます。2つ目のoperator|で使用されている__compose(f, g)f(g(arg))となるように関数合成を行うラッパ型のようです。

filterの実装で__range_adaptor_closure_tの初期化に使用されている__bind_back()std::bind_frontと同じことを逆順で行うもので、Rangeアダプタの実装簡略化のためにP2287で提案されているものでもあります。

自作viewのアダプト

各実装をみて見ると、割とそこそこ異なっていることが分かります。従って、自作viewをrangesの|にアダプトするには各実装に合わせたコードが必要になりそうです(共通化も可能だと思いますが、考えがまとまっていないので次回以降・・・)。

Rangeアダプタオブジェクトとか知らねえ!とりあえず|で繋げればいい!!っていう人向け

Rangeアダプタの性質を知り色々実装を見てみると、|につなぐだけなら簡単なことに気付けます。Rangeアダプタは必ず|の右辺に来て、左辺はrangeviewable_range)オブジェクトとなります。複数チェーンしている時でも、1つのrange | RAの結果はviewになります。つまり、rangeを左辺に受ける|の実装においては左辺のオブジェクトは常にviewable_rangeとなります。

それを例えば自作のxxx_viewに実装すると

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  namespace views {
    namespace detail {

      struct xxx_view_adoptor {

        // Rangeアダプタの主処理
        template<typename R>
        [[nodiscard]]
        constexpr auto operator(R&& r) const {
          // xxx_viewの生成処理
        }

        // range | RA -> view なパイプライン演算子
        template <std::ranges::viewable_range R>
          requires requires(R&& r, const xxx_view_adoptor& self) {
            self(std::forward<R>(r));
          }
        [[nodiscard]]
        friend constexpr std::ranges::view auto operator|(R&& r, const xxx_view_adoptor& self) noexcept(noexcept(self(std::forward<R>(r)))) {
          return self(std::forward<R>(r));
        }
      };

    }

    inline constexpr xxx_view_adoptor xxx;
  }
}

省略した部分を適切に整えさえすれば、このoperator|定義は全ての実装でrangesのパイプラインチェーンにアダプトすることができます(多分)。

ただしこのxxx_view_adoptorはRangeアダプタとして必要なことを何もしていないので、それ以外の保証はありません。未定義動作にはならないと思いますが、標準のRangeアダプタ/Rangeアダプタクロージャオブジェクトと同等の振る舞いはできないので本当にとりあえずの実装です。

GCC10

GCC10の場合は、_RangeAdaptorClosure/_RangeAdaptorを適切にラムダ式などで初期化し、そのラムダ式内にview生成処理を記述します。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  namespace views {

    // xxx_viewのRangeアダプタクロージャオブジェクト
    inline constexpr std::views::__adaptor::_RangeAdaptorClosure xxx
      = [] <viewable_range _Range> (_Range&& __r)
      {
        // xxx_viewの生成処理
      };

    
    // xxx_viewのRangeアダプタオブジェクト
    inline constexpr std::views::__adaptor::_RangeAdaptor xxx
      = [] <viewable_range _Range, typename _Pred> (_Range&& __r, _Pred&& __p)
      {
          // xxx_viewの生成処理
      };
  }
}

GCC11

GCC11の場合も_RangeAdaptorClosure/_RangeAdaptorを使用するのですがラムダ式は使用できず、別にRangeアダプタ(クロージャ)型を定義してそこで継承して使用する必要があります。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor : std::views::__adaptor::_RangeAdaptor<xxx_adoptor>
      {
        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        // Rangeアダプタの部分適用共通処理を有効化
        using _RangeAdaptor<xxx_adoptor>::operator();

        // よくわからない場合は定義しない方がいいかもしれない
        static constexpr int _S_arity = 2;  // 入力rangeも含めた引数の数
        static constexpr bool _S_has_simple_extra_args = true;
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : std::views::__adaptor::_RangeAdaptorClosure  // これはCRTPではない
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }

        // _S_arityはクロージャオブジェクトの場合は不要らしい

        static constexpr bool _S_has_simple_call_op = true;
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

GCC10と11の間で使用法が結構変わっているのが地味に厄介かもしれません。GCCの場合はどちらでもRangeアダプタの引数事前適用を実装する必要がありません。

MSVC

MSVCの場合は_Pipe::_Baseを使用します。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor {

        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        template <typename Arg>
            requires std::constructible_from<std::decay_t<Arg>, Arg>
        constexpr auto operator()(Arg&& arg) const {
          // Rangeアダプタの引数事前適用処理
          return std::ranges::_Range_closure<xxx_adoptor, std::decay_t<Arg>>{std::forward<Arg>(arg)};
        }
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : public std::ranges::_Pipe::_Base<xxx_adoptor_closure>
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

MSVCの場合、Rangeアダプタの追加の引数を事前適用する処理を自分で記述する必要があります。そこでは_Range_closureを使用することでほぼ省略可能です。Rangeアダプタ型の場合はこれだけでよく、Rangeアダプタクロージャオブジェクト型の場合は_Pipe::_Baseを継承する必要があります。

clang

clangの場合、MSVCとほぼ同じ記述となり、使用するものが異なるだけです。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor {

        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        template <typename Arg>
            requires std::constructible_from<std::decay_t<Arg>, Arg>
        constexpr auto operator()(Arg&& arg) const {
          // Rangeアダプタの引数事前適用処理
          return std::__range_adaptor_closure_t(std::__bind_back(*this, std::forward<Arg>(arg)));
        }
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : public std::__range_adaptor_closure<xxx_adoptor_closure>
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

clangもMSVCの場合同様に、Rangeアダプタの追加の引数を事前適用する処理を自分で記述する必要があります。とはいえ内部実装を流用すればほぼ定型文となり、Rangeアダプタクロージャオブジェクト型の場合も__range_adaptor_closureを継承するだけです。

C++23から

これら実装を見ると、自作のview型を標準のものと混ぜてパイプで使用することはあまり想定されていなかったっぽいことが察せられます。そもそもviewを自作することってそんなにある?ということは置いておいて、この事態はあまり親切ではありません。

これまでもちらちら出ていましたが、この状況はP2287の採択によってC++23で改善されています。それによって、MSVC/clangの実装とほぼ同等に使用可能なユーティリティstd::ranges::range_adaptor_closurestd::bind_backが用意されます。これを利用すると次のように書けるようになります。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor {

        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        template <typename Arg>
            requires constructible_from<decay_t<Arg>, Arg>
        constexpr auto operator()(Arg&& arg) const {
          // Rangeアダプタの引数事前適用処理
          return std::ranges::range_adaptor_closure(std::bind_back(*this, std::forward<Arg>(arg)));
        }
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : public std::ranges::range_adaptor_closure<xxx_adoptor_closure>
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

これはC++23以降の世界で完全にポータブルです。ただし、MSVC/clang同様に、Rangeアダプタの追加の引数を事前適用する処理を自分で記述する必要があります。とはいえそれはやはりほぼ定型文まで簡略化されます。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で27本あります(SG22のWG14からのものは除きます)。

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

次期標準ライブラリ機能候補の実装経験を得るためのTSである、Library Fundamental TS v3の文書。

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

↑の変更点を記した文書。

この版での変更は、LWG Issue 3649を反映したことです。

N4910 Working Draft, Standard for Programming Language C++

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

N4911 Editors’ Report - Programming Languages - C++

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

2月の会議で採択された提案とコア言語/ライブラリのIssue解決が適用されています。

P0009R16 MDSPAN

多次元配列に対するstd::spanである、mdspanの提案。

以前の記事を参照

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

P0957R6 Proxy: A Polymorphic Programming Library

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

以前の記事を参照

このリビジョンでの変更は、ODR違反防止のためにstd::dispatch宣言の構文を再設計したこと、それに伴って、std::dispatch関連の文言を修正したことです。

以前の提案では、std::dispatchはテンプレート引数として1つめに関数型、2つめにそれに合うCallable(ディスパッチ処理)をNTTPで受けていました。このリビジョンでは、2つめのパラメータを無くして、std::dispatchの実装クラスのメンバ関数operator())としてディスパッチ処理を記述するようにしました。

// 以前の提案
struct Draw : std::dispatch<
    void(), [](const auto& self) { self.Draw(); }> {};
struct Area : std::dispatch<
    double(), [](const auto& self) { return self.Area(); }> {};

// この提案
struct Draw : std::dispatch<void()> {
  template <class T>
  void operator()(const T& self) { return self.Draw(); }
};
struct Area : std::dispatch<double()> {
  template <class T>
  double operator()(const T& self) { return self.Area(); }
};

ラムダ式がNTTPに渡されている時でもそのクロージャ型は宣言ごとに固有の型をもつため、ヘッダに宣言されたstd::dispatchの特殊化を複数の翻訳単位から参照するとODR違反を起こします。この変更はおそらくこれを防止するためのものです。

/// foo.h
template <auto>
struct foo { };

// 2つ以上の翻訳単位から参照されると未定義動作(ODR違反)
extern foo<+[]() {}> x;
inline foo<+[]() {}> y;

/// tu1.cpp
#include "foo.h"

/// tu2.cpp
#include "foo.h"

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

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

以前の記事を参照

このリビジョンでの変更は、aligned_object_storage<T>Tはオブジェクト型でなければならないことを規定したこと、aligned_object_storage<T>TがCV修飾される可能性を考慮するようにしたこと、Library Foundermental TSの参照をv2からv3へ更新したことです。

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

P1684R1 mdarray: An Owning Multidimensional Array Analog of mdspan

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

std::mdspanP0009)は別に確保された領域を適切に参照することで、所有権を保持せずに多次元配列を使用するものです。ユースケースによっては(小さい固定サイズの配列を使用する場合など)、多次元配列を所有した状態で同様に使用したい場合があります。それによって、std::mdspanでは回避できない間接参照のコストを削減でき、サイズが小さい場合は参照局所性の向上や最適化によるレジスタへの配置などを期待できます。この提案のmdarrayはその目的のための多次元配列クラスです。

// mdspanを使用する場合

void make_random_rotation(mdspan<float, std::extents<3, 3>> output);

void apply_rotation(mdspan<float, std::extents<3, 3>>, mdspan<float, 3>);

void random_rotate(mdspan<float, std::extents<dynamic_extent, 3>> points) {
  float buffer[9] = { };
  auto rotation = mdspan<float, 3, 3>(buffer);
  make_random_rotation(rotation);

  for(int i = 0; i < points.extent(0); ++i) {
    apply_rotation(rotation, subspan(points, i, std::all));
  }
}
// mdarrayを使用する場合

mdarray<float, std::extents<3, 3>> make_random_rotation();

void apply_rotation(mdarray<float, std::extents<3, 3>>&, const mdspan<float, std::extents<3>>&);

void random_rotate(mdspan<float, std::extents<dynamic_extent, 3>> points) {
  auto rotation = make_random_rotation();

  for(int i = 0; i < points.extent(0); ++i) {
    apply_rotation(rotation, subspan(points, i, std::all));
  }
}

この提案によるmdarrayは、mdspanとそれが参照するメモリ領域(を保持するコンテナ)を1つのクラスにまとめたような設計になっています。特に、そのインターフェースはmdspanと共通しており、mdspanの利用に慣れたユーザーがほぼそのままmdarrayを利用できるようになっています。

// mdarrayのクラス定義の一部例
template<class ElementType,
         class Extents,
         class LayoutPolicy = layout_right,
         class Container = ...>
class mdarray {
  ...

  using mapping_type = typename layout_type::template mapping<Extents>;

  ...
  
private:
  container_type ctr_;
  mapping_type map_;
};

mdarrayはその内部領域を保持するコンテナ型を一番最後のテンプレートパラメータで変更することができます。デフォルトでは、Extentsの一部がdynamic_extentであればstd::vector<ElementType>Extentsが全て静的であればstd::array<ElementType, N>NExtentsの値の積)となります。手動で指定する場合、Containercontiguous container要件を満たしている必要があります。

LayoutPolicyのデフォルトは、1次元の配列を多次元配列として(行優先で)アクセスするときのインデックス変換を行うものです。その変更は、たとえばその要素へのアクセスにアトミックアクセスが必要である場合などに使用することができます。

ただし、mdarrayは配列を所有するものであるため、mdspanとは異なりコンテナセマンティクスを持ちます。特に、mdarraydeep constnessを持っており、mdarray事態のconst性は(そのconstメンバ関数を通して)各要素まで伝播されます。これはmdspanとは異なり、std::vectorなどとのコンテナと共通する性質です。

P1708R6 Simple Statistical Functions

標準ライブラリにいくつかの統計関数を追加する提案。

以前の記事を参照

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

  • stats_result_tの削除
  • Accumulator objectを並列実装が可能となるように改善
  • stat_accumweighted_stat_accumの削除
  • より多くのデータ型を許可するために一部の制約を緩和
  • Projectionの削除(必要なら、views::transformによって行えるため)
  • 多くの名前に変更

などです。

P1839R4 Accessing Object Representations

reinterpret_cast<char*>によるオブジェクト表現へのアクセスを未定義動作とならないようにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言内のオブジェクト表現のNについて曖昧な使用を修正したことです。

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

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

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

  • Macos Mojaveでシステムヘッダとしてのassertの利用経験から、assert.hのを修正
  • WG14がこの提案を採択し場合、なにがCにとって必要かを示すために、C++の文言を明示化した

などです。

WG14は、C23に向けてこの提案を採択しています。C++に向けてはC++26を目指して作業しています。

P2290R3 Delimited escape sequences

文字・文字列定数中の8進・16進エスケープシーケンスおよびユニバーサル文字名について、その区切りが明確になるような形式を追加する提案。

前回の記事を参照

このリビジョンでの変更は、conditional-escape sequenceのリストから\oを取り除いたことです。

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

P2465R3 Standard Library Modules std and std.compat

標準ライブラリモジュールについて最小のサポートをC++23に追加する提案

以前の記事を参照

このリビジョンでの変更は、CWGのフィードバックを受けての文言の修正です。

この提案はすでにLWG/CWGでのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2510R1 Formatting pointers

std::formatについて、ポインタ型のフォーマットを充実させる提案。

以前の記事を参照

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

P2511R1 Beyond operator(): NTTP callables in type-erased call wrappers

std::move_only_fuctionを、呼び出し可能なNTTP値を型消去するCallable Wrapperへ拡張する提案。

以前の記事を参照

このリビジョンでの変更は、寄せられたコメントへの返答、std::functionのための文言を追加した事などです。

このリビジョンでは、std::functionに対してもnontype_tからの構築を行うコンストラクタが追加されました。

P2521R2 Contract support - Working Paper

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

以前の記事を参照

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

  • 事前条件と事後条件の評価順序について提案を追加
    • 引数初期化 -> 事前条件評価 -> 関数本体の評価 -> ローカル変数のデストラクタ実行 -> 事後条件評価、の順を提案
  • 事後条件で使用される非参照の関数引数はconstでなければならない、とした
  • 関数の仮引数の代わりに、関数の実引数の値を事後条件で使用したとしても、事後条件で非参照引数を利用する際の問題を解決できないことに関する説明を追記
  • 副作用の除去を提案する動機を更新

などです。

P2539R0 Should the output of std::print to a terminal be synchronized with the underlying stream?

提案中のstd::printP2093)が出力するストリームについて、同じストリームに対する他の出力との同期を取るべきという意見についての報告書。

std::print(及びfmt::print)では、文字化け防止のためにコンソール出力時にストリームバッファをバイパスして書き込んでおり、その際に環境ネイティブのユニコードAPIを使用する場合があります。

P2093のレビューでは、std::printをその基礎となるストリーム(出力先ストリーム、デフォルトではstdout)と同期させることが移行のために有益だろうという意見が出たようです。この文書は、その提案についての報告書です。

printf("first\n");
std::print("second\n");

この単純なコードは、次のように出力されることが期待されます。

first
second

しかし実際には、この順番は入れ替わる可能性があります。なぜなら、printfはバッファリングするのに対して、std::printはそれを行わないためです。

これを期待通りに出力するためには、std::printが書き込み動作をする前にストリームバッファをflushする必要があります。ただし、それをすると小さくない追加のコストがかかります。そして、{fmt}でもRustのコンソール出力でも、現在そのような同期は行われていません。

この文書は何かを提案するものではなく、問題の周知や別の解決策・対案を得るためのものです。

P2540R1 Empty Product for certain Views

viewの空積は空のタプルになるべきという提案。

以前の記事を参照

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

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

P2553R1 Make mdspan size_type controllable

P0009で提案中のstd::mdspanについて、そのsize_typeを制御可能とする提案。

以前の記事を参照

このリビジョンでの変更は、size_typeを符号付き整数型に制限しないようにしたこと、それに伴ってextentsのコンストラクタに事前条件(0以上の値であること)を追加したこと、size_typeの変換が縮小変換となる場合にexplicitになるようにしたことなどです。

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

P2555R1 Naming improvements for std::execution

P2300で提案されている、senderファクトリ及びsenderアダプタの一部のものについて、名前を改善する提案。

以前の記事を参照

このリビジョンでの変更は、文字化けを修正したことです。

P2558R0 Add @, $, and ` to the basic character set

@ $ `の3種類の文字をソースコードの基本文字集合に追加する提案。

これらの文字はAsciiに含まれているものですが、ソースコードの基本文字集合basic character set)には含まれていませんでした。基本文字集合C/C++の基本的な文法を記述するために使用される文字の集合であり、そこに追加しておけば将来的な構文拡張の際に使用可能となります。

この3つの文字は、C++では基本文字集合にこそ含まれていなかったものの、1バイト文字として翻訳文字集合Translation character setソースコードを記述するのに使用可能な文字の集合。C++23で導入された概念)には含まれていました。Cには翻訳文字集合の概念はなく、この3つは基本文字集合に含まれていませんでしたが、C23でこの3つはCの基本文字集合に追加されました。CはC++よりさらに保守的(後方互換に敏感)ですが、この3つの文字の将来的な利用に障害がないことを確認した上で基本文字集合に追加しています。

C++ではCほどの重要性はないものの、将来的な構文のために使用可能にしておくことは有益であるとして、C23での決定を踏襲してC++でも@ $ `を基本文字集合へ追加しておこうとする提案です。

P2560R0 Comparing value- and type-based reflection

値ベースのリフレクションと型ベースのリフレクションの比較スライド。

現在のリフレクション機能の候補には型ベースのものと値ベースの2種類があります。型ベースはリフレクションの結果(メタオブジェクト、メタ情報を格納するもの)が型で得られるのに対して、値ベースはリフレクションの結果が値(コンパイル時の値)で得られます。型ベースはTMPとの親和性に、値ベースはconstexpr処理との親和性に優れています。

Reflection TSは現在型ベースのリフレクションで設計されていますが、リフレクションの方向性としては値ベースのものにしていこうとしているようです。

このスライドはその両者を比較するもので、ユーザビリティでは型ベースが優れており、コンパイル時間への影響は値ベースの方が小さいことを報告しています。その結論としては、値ベースリフレクションにはコンパイル時間の利点しかなく、それだけを重視して値ベースのリフレクションを採用するのか?という疑問を投げかけています。

P2562R0 constexpr Stable Sorting

std::stable_sortとそのファミリをconstexpr対応する提案。

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

  • std::stable_sort
  • std::stable_partition
  • std::inplace merge
  • これらのranges

これらのものはどれも安定ソート(同値なものの相対順序が保たれるソート)によって並べ替えやマージを行うものですが、rangesのものも含めてどれもconstexprではありません。その原因は、入力範囲の長さによって追加のメモリ領域を使用するかしないかを動的に分岐する実装になっているためです。

このことは規格によっても指定されていて、例えばstd::stable_sortなら「計算量はN log2(N)(回の比較)、ただし追加のメモリが使用できる場合はN log N」のように規定されています。追加のメモリが使用できる場合がいつなのかは指定されていませんが、実装は入力範囲が一定の長さを超える場合にその場でメモリを確保して使用します。

この追加のストレージを使用する経路が、stable_sortとそのファミリのconstexpr対応を妨げていたようです。ただし、追加のメモリを使用しない経路の実装は現在でもconstexpr対応が可能であり、こちらの経路はそのままconstexpr指定することができます。

このような実行経路による定数実行可能性の問題は、std::is_constant_evaluated()によって解決されます。これによって、定数式で実行可能なパスの選択を静的に行うことができるため、std::stable_sortとそのファミリをconstexpr対応させることができます。

また、C++20からは動的メモリ確保が定数式で行えるようになり、std::vectorを定数式で使用可能となります。C++23からは同様にstd::unique_ptrも定数式で使用可能となるため、これらを用いた実装ならばすべてのパスでconstexpr対応させることが可能です。

P2564R0 consteval needs to propagate up

consteval関数をconstexpr関数の中で呼び出すことができない問題を軽減する提案。

値ベースのリフレクション(P1240)では、std::meta::infoという単一の型によってリフレクションの結果を表現することが提案されています。そして、多くのところではstd::meta::infoのシーケンス(std::vector<std::meta::info>など)を扱うことになることが予想されます。すると、自然に既存のアルゴリズム<algorithm>)を使用したくなります。

namespace std::meta {
  // std::meta::infoの仮の実装とする
  struct info {
    int value;
  };

  // std::meta::infoがある条件を満たしているかを調べる関数の一例
  consteval auto is_invalid(info i) -> bool {
    // 偶数を許可しない
    return i.value % 2 == 0;
  }
}

// std::meta::infoのシーケンス
constexpr std::meta::info types[] = {1, 3, 5};

std::meta::infoの実装は現在ないので、このような仮想的な実装で実験をしてみます(ここでの問題にはこれで十分です)。ここで注意するのは、std::meta::is_invalid()consteval関数であることです。

// NG
// is_invalid()の呼出(none_of内)は即時関数コンテキストではない
static_assert(std::ranges::none_of(
  types,
  std::meta::is_invalid
));

// NG
static_assert(std::ranges::none_of(
  types,
  [](std::meta::info i) {
    return std::meta::is_invalid(i);  // is_invalid()の呼出は即時呼出となるが、iが定数式ではないためNG
  }
));

// NG
// is_invalid()をラップするラムダの呼出(none_of内)は即時関数コンテキストではない
static_assert(std::ranges::none_of(
  types,
  [](std::meta::info i) consteval {
      return std::meta::is_invalid(i);
  }
));

// NG
// 即時関数コンテキストではないところで、consteval関数の関数ポインタを取れない
static_assert(std::ranges::none_of(
  types,
  +[](std::meta::info i) consteval {
      return std::meta::is_invalid(i);
  }
));

次に、アルゴリズム(ここではnone_of)の結果を直接static_assertに渡す代わりに、consteval関数の内部で使用してみます。

// OK(ただし言語ルール的には)
// 標準ライブラリのルールでは、規定されていない限りそのアドレスをとることができない
consteval auto all_valid() -> bool {
  return std::ranges::none_of(
    types,
    std::meta::is_invalid
  );
}
static_assert(all_valid());

// NG
consteval auto all_valid() -> bool {
  return std::ranges::none_of(
    types,
    [](std::meta::info i) {
      return std::meta::is_invalid(i);  // iは定数式ではない
    }
  );
}
static_assert(all_valid());

// NG
// is_invalid()をラップするラムダの呼出(none_of内)は即時関数コンテキストではない
consteval auto all_valid() -> bool {
  return std::ranges::none_of(
    types,
    [](std::meta::info i) consteval {
      return std::meta::is_invalid(i);
    }
  );
}
static_assert(all_valid());

// OK
consteval auto all_valid() -> bool {
  return std::ranges::none_of(
    types,
    +[](std::meta::info i) consteval {
        return std::meta::is_invalid(i);
    }
  );
}
static_assert(all_valid());

一番最後の方法(consteval関数の中で、consteval関数をconstevalラムダでラップして、関数ポインタを取得する)だけが、完全に合法的にconsteval関数を既存のアルゴリズムで使用する方法です。

この問題の原因は、consteval関数を呼び出すことができるのは即時関数コンテキスト(immediate function context)と呼ばれる文脈の中だけであることから起きています。特に、他の関数内でconsteval関数を呼び出そうとすると、その関数もまたconstevalでなければなりません。そうするとconsteval関数は呼び出せなくなる気がしますが、呼出が完全に定数式であれば(引数が定数式であれば)どこの関数内でも呼び出すことができ、その場合のことを即時呼出(immediate invocation)と言います。

consteval f(int i) {
  return i;
}

constexpr void g(int i) {
  // g()の内部は即時関数コンテキストではない

  f(0); // ok、即時呼出
  f(i); // ng、iは定数式ではない
}

consteval void h(int i) {
  // h()の内部は即時関数コンテキスト

  f(0); // ok
  f(i); // ok
}

std::ranges::none_ofをはじめとする標準アルゴリズムは、constexpr指定されているものはあってもconsteval指定されているものはありません。そのため、関数を受け取って何かをするアルゴリズムの場合、その内部が即時関数コンテキストではないことからconsteval関数を受け取って実行することができません。

これと同様の問題はstd::is_constant_evaluated()でも問題となっていて、C++23ではその解決のためにif constevalが導入されました(P1938R3)。if constevaltrueブロック(定数式で実行されるべきブロック)を即時関数コンテキストとすることで、定数式でしか実行されないtrueブロックにおいてconsteval関数を自然に呼び出せるようにします。これを利用すると、この問題の解決を図れそうです。

// OK
static_assert(std::ranges::none_of(
  types,
  [](std::meta::info i) {
    if consteval {
      return std::meta::is_invalid(i);  // ここは即時関数コンテキスト
    }
  }));

// NG
// none_of内は即時関数コンテキストではない
static_assert(std::ranges::none_of(
  types,
  [](std::meta::info i) consteval {
    if consteval {
      return std::meta::is_invalid(i);
    }
  }));

それでもなお、アルゴリズムが直接呼び出す関数はconstevalであってはいけません。そのため、非constevalラムダによるラップが必要です。根本的な解決を図るには、ライブラリ関数の変更が必要です。

template <ranges::input_range R, class Pred>
constexpr auto my::none_of(R&& r, Pred pred) -> bool {
  auto first = ranges::begin(r);
  auto last = ranges::end(r);
  for (; first != last; ++first) {
    if consteval {
      // 即時関数コンテキスト
      if (pred(*first)) {
        return false;
      }
    } else {
      // 即時関数コンテキストではない
      if (pred(*first)) {
        return false;
      }
    }
  }
  return true;
}

if constevalの半目的外使用に目を瞑れば、このような変更でそのままpredconsteval関数を渡せるような気がします。ただしこれは実際には機能しません。なぜなら、predconsteval関数の場合、if constevalfalseブロックで結局同じ問題が発生するからです。

ライブラリ側で解決を図るには、もっと大元でif constevalで分岐しておくことで、定数式(即時関数)用の処理と実行時処理を分岐する必要があります。そして、その分岐先の処理(コード)は全く同じになるでしょう。しかし統一することはできません・・・

無論そのような醜い解決策は解決とはいえないため、この問題の解決には言語レベルでの対処が必要となります。

問題を単純化してみてみると

constexpr auto pred_bad(std::meta::info i) -> bool {
  return std::meta::is_invalid(i);    // NG、即時関数コンテキストではない
}

constexpr auto pred_good(std::meta::info i) -> bool {
  if consteval {
    return std::meta::is_invalid(i);  // OK、即時関数コンテキスト
  }

  // UB、実行時にリターンしない
}

pred_bad()は今日ではNGであり、consteval関数が確実にコンパイル時にのみ呼ばれるという性質を保証する1つの方法の結果です。pred_good()consteval関数の呼出がif constevalのブロック(即時関数コンテキスト)にのみ現れているため、問題ありません。ここで、pred_good()constexprとマークされていますが、実際には定数式でのみ呼び出すことができます(何もリターンしないため実行時にはUB)。従って、これも優れた解決策であるとはいえません。

pred_good()コンパイル時にのみ呼出可能なようにすることができれば問題は解決しますが、それは結局consteval関数に行き付き堂々巡りになります。

最良の方法はどうやら、pred_bad()が実行時に呼び出されないことを保証すればよさそうです。pred_bad()の問題点はconsteval関数が確実に呼ばれるのにconstexpr関数であることから実行時にも呼び出すことができうる点にあります。したがって、pred_bad()の呼出がコンパイル時にのみ行われることを保証できれば、pred_bad()がNGである必要は無くなります。

この提案は、constexpr関数がコンパイル時にのみ呼出可能である場合に、それらの関数を暗黙consteval関数とすることで上記の問題の解決を図るものです。そのために、次のようなルールを提案しています

  1. constexpr関数が即時関数コンテキスト外のconsteval関数呼出を含み、その呼出が定数式ではない場合、そのconstexpr関数は暗黙的にconsteval関数となる。
  2. 式が、1と同様のコンテキストで即時呼出ではないconsteval関数を呼び出す場合、そのコンテキストも暗黙的にそのコンテキストも暗黙的にconsteval関数(即時関数コンテキスト)となる。
  3. manifestly constant evaluatedコンテキスト(std::is_constant_evalueted()trueとなるコンテキスト)は即時関数コンテキストと見做される。

これらの変更によって、先ほどNGだったstd::meta::infoのシーケンスに対する標準アルゴリズムの呼出は、全てコンパイルが通り、(直接渡すものもラムダによるものも)意図通りに実行されるようになります。

P2565R0 Supporting User-Defined Attributes

ユーザー/ベンダー定義の属性について、全てのコンパイラでサポートされる(警告が出ない)属性構文の提案。

C++標準では、「規格で指定されていない属性は実装(コンパイラ)によってサポートされ、実装が認識できない属性については無視される」のように規定されており、非標準の属性をサポートしています。コンパイラは別のベンダーやユーザー定義の属性については無視することでサポートすべきですが、現在主要なコンパイラは属性名のタイプミス検出のために不明な属性について警告を発します。

// deprecatedのタイプミス
[[ edprecated("Broken. Use `bravo`.") ]]
double banana(double num);
// Clang 13.0.1
<source>:6:4: warning: unknown attribute 'edprecated' ignored
[-Wunknown-attributes]
[[ edprecated("Broken. Use `bravo`.") ]]
   ^~~~~~~~~~

// GCC 11.2
<source>:7:25: warning: 'edprecated' attribute directive ignored [-Wattributes]
  7 | double banana(double num);
    |

// MSVC v19.30
<source>(6): warning C5030: attribute 'edprecated' is not recognized

これはユーザー定義属性(何かしらの手段で実装したとする)に対しても同様です。

// deprecated属性の拡張、2つ目の引数に重要度を受け取る
[[ bespoke::deprecated("Broken. Use `charlie`.", "warn") ]]
double carrot(double num);
// Clang 13.0.1
<source>:9:4: warning: unknown attribute 'deprecated' ignored
[-Wunknown-attributes]
[[ bespoke::deprecated("Broken. Use `charlie`.", "warn") ]]
   ^~~~~~~~~~~~~~~~~~~>

// GCC 11.2
<source>:10:25: warning: 'bespoke::deprecated' scoped attribute directive ignored [-Wattributes]
  10 | double carrot(double num);
     |

// MSVC v19.30
<source>(9): warning C5030: attribute 'bespoke::deprecated' is not recognized

そして、ベンダー定義属性についても、各コンパイラが互いに同様の報告をします。

[[ clang::no_sanitize("undefined") ]]
[[ gnu::access(read_only, 1) ]]
[[ msvc::known_semantics ]]
double daikon(double num);
// Clang 13.0.1
<source>:13:4: warning: unknown attribute 'access' ignored [-Wunknown-attributes]
[[ gnu::access(read_only, 1) ]]
   ^~~~~~~~~~~
<source>:14:4: warning: unknown attribute 'known_semantics' ignored [-Wunknown-attributes]
[[ msvc::known_semantics ]]
   ^~~~~~~~~~~~~~~~~~~~~

// GCC 11.2
<source>:15:21: warning: 'clang::no_sanitize' scoped attribute directive ignored [-Wattributes]
  15 | int* daikon(int* num);
     | ^
<source>:15:21: warning: 'msvc::known_semantics' scoped attribute directive ignored [-Wattributes]
Compiler returned: 0

// MSVC v19.30
<source>(12): warning C5030: attribute 'clang::no_sanitize' is not recognized
<source>(13): warning C5030: attribute 'gnu::access' is not recognized

このことは、ベンダー定義であってもユーザー定義であっても、非標準の属性は実質的にサポート(警告もない完全な無視)されていないことを意味しています。この問題の回避のために、規模の大きなプロジェクトやライブラリでは、プリプロセッサとマクロによってコンパイラを検出し適切に非標準属性の表示を切り替えることが行われています。しかし、その方法は複雑であり、正しく書くのは難しいものがあります。

この提案は、現在のこの挙動(標準属性タイプミスの検出のための警告)を受け入れつつ、属性構文を拡張することによって、ユーザー/ベンダー定義の属性を全てのコンパイラでサポートされる(無視されるなら警告されない)ようにすることを提案するものです。例えば次のような構文を提案しています

// タイポではない意図的な属性であることを表す
[[ extern gnu::access(...) ]];

この提案ではどのような構文やメカニズムを採用するかについては確定しておらず、これ以外の代替案を募集するものでもあります。

P2568R0 Proposal of std::map::at_ptr

.at()を提供する標準コンテナに、.at_ptr()メンバ関数を追加する提案。

.at_ptr()はその名の通り、.at()して見つかった要素のポインタを返すものです。もし要素が見つからない場合はnullptrを返し、例外を投げません。

int main() {
  std::map<int, int> m = /*...*/;

  if (int* v = m.at_ptr(42)) {
    // 要素が見つかった場合の処理

  } else {
    // 要素が見つからなかった場合の処理
  }

}

このように、コンテナ内の特定要素を検索し存在すればそれに対して何かする、という処理はさまざまなプログラムやアルゴリズムで非常によく見られる処理です。しかし、.at()関数は要素が見つからない場合に例外を投げ、[]は要素が見つからない場合にデフォルト構築して返すなど使いづらい部分があり、要素の検索と引き当てを別々に書く必要がありました。そのため、そのような典型的なパターンを1まとめにしてこの.at_ptr()と同じことを行う関数がさまざまなライブラリやプロジェクトに存在しています。この提案は、そのような既存の慣行を標準コンテナのメンバ関数として標準化するものです。

この提案ではすでに.at()を持っているコンテナのみを対象としています

  • std::string
  • std::string_view
  • std::array
  • std::vector
  • std::map
  • std::unorderd_map
  • std::deque

また、std::optionalは参照型を保持することが(現在は)できないため、この用途に適しておらず、この提案ではポインタを返すことを提案しています。

P2569R0 *_HAS_SUBNORM==0 implies what?

*_HAS_SUBNORMを削除する提案。

*_HAS_SUBNORMとは、<float.h>/<cfloat>に定義されているマクロで、float, double, long double型における非正規化数のサポート状況を取得するためのものです。このマクロの値が1であれば非正規化数がサポートされており、-1の場合はその判定ができないことを表しています。

0の場合は非正規化数がサポートされていないことを表していますが、その場合の振る舞いは明確ではありません。例えば、fpclassify()に非正規化数を渡した場合の動作は不明瞭です。

また、ARM, x86, NVIDIAGPUなど、非正規化数の扱いに関する制御ビットを持つアーキテクチャが存在しています。その制御ビットの値によって、次のように非正規化数の扱いを変化させることができます

  • 非正規化数のオペランドをゼロとして扱う
  • 非正規化数の結果をゼロとして扱う(フラッシュ)

ただし、x86は丸めの後に非正規化数を検出し、ARMは丸めの前に検出するなどその振る舞いは実装によって変化しており、さらにはその制御ビットを実行時に変化させる実装が提供されていることもあります。つまりは、*_HAS_SUBNORMの値はコンパイル時定数ではなく、どちらかといえば実行時に変化する値です。

非正規化数はCの浮動小数点モデルの一部ではありますが必須の要素ではなく、IEC 60559を除いて標準仕様としてその動作を指定するものがありません。非正規化数をサポートする実装は様々に異なる方法で非正規化数を扱います。現在の*_HAS_SUBNORMは実装や利用の上で曖昧であり、これを解決する方法を見つけることができていません。したがって、この提案では将来の改定のためにも*_HAS_SUBNORMを削除することを提案しています。

ただし、非正規化数のフラッシュ(非正規化数の結果をゼロにする)は最適化などの点から有用であり広く使用されていることからC標準はそれをサポートし続ける必要があるため、フラッシュされた非正規化数の振る舞いを明確に規定することも同時に提案しています。

これはC標準に対するものですが、SG22を通してC++にも提案されています。

おわり

この記事のMarkdownソース

[C++]クラス/変数テンプレートとコンセプト

クラス/変数テンプレートに対する制約

クラス/変数テンプレートに対するコンセプトによる制約は、後置requires節が使用できないことを除いて関数テンプレートと同様に行うことができます。

// クラステンプレートに対する制約の例

template<typename T>
struct S1 {
  static constexpr int N = 1;
};

template<std::integral T>
struct S1<T> {
  static constexpr int N = 2;
};

template<typename T>
struct S2 {
  static constexpr int N = 1;
};

template<typename T>
  requires std::integral<T>
struct S2<T> {
  static constexpr int N = 2;
};
// 変数テンプレートに対する制約の例

template<typename T>
constexpr int v1 = 1;

template<std::integral T>
constexpr int v1<T> = 2;

template<typename T>
constexpr int v2 = 1;

template<typename T>
  requires std::integral<T>
constexpr int v2<T> = 2;

クラス/変数テンプレートではオーバーロードを表現するためには部分特殊化を使用する必要がありますが、ほぼ関数テンプレートと同じような雰囲気で使用可能です。

プライマリテンプレートへの制約

先程の例では、プライマリテンプレートに制約を行わず、部分特殊化に対してのみ制約を行っていました。

ここでそれを逆にしてみましょう

// プライマリテンプレートに制約してみる

template<std::integral T>
struct S1 {
  static constexpr int N = 2;
};

template<typename T>
struct S1<T> {
  static constexpr int N = 1;
};

template<std::integral T>
constexpr int v1 = 2;

template<typename T>
constexpr int v1<T> = 1;

謎のエラーが出ました。インスタンス化しない場合のエラーを見てみると

prog.cc:10:8: error: class template partial specialization does not specialize any template argument; to define the primary template, remove the template argument list
struct S1<T> {
       ^ ~~~
prog.cc:9:10: error: type constraint differs in template redeclaration
template<typename T>
         ^
prog.cc:4:15: note: previous template declaration is here
template<std::integral T>
              ^
prog.cc:18:15: error: variable template partial specialization does not specialize any template argument; to define the primary template, remove the template argument list
constexpr int v1<T> = 1;
              ^ ~~~

クラステンプレートの部分特殊化はどのテンプレート引数も特殊化していない、みたいに言われていて、変数テンプレートもほぼ同じメッセージです。

インスタンス化した場合のエラーメッセージには少しヒントを見ることができます。

prog.cc:22:16: error: constraints not satisfied for class template 'S1' [with T = float]
  std::cout << S1<float>::N << '\n';
               ^~~~~~~~~
prog.cc:4:15: note: because 'float' does not satisfy 'integral'
template<std::integral T>
              ^
/opt/wandbox/clang-13.0.0/include/c++/v1/concepts:198:20: note: because 'is_integral_v<float>' evaluated to false
concept integral = is_integral_v<_Tp>;
                   ^
prog.cc:24:16: error: constraints not satisfied for variable template 'v1' [with T = float]
  std::cout << v1<float> << '\n';
               ^~~~~~~~~
prog.cc:14:15: note: because 'float' does not satisfy 'integral'
template<std::integral T>
              ^
/opt/wandbox/clang-13.0.0/include/c++/v1/concepts:198:20: note: because 'is_integral_v<float>' evaluated to false
concept integral = is_integral_v<_Tp>;

floatで特殊化した場合に、std::integralが満たされずにエラーが出ているようです。いやそもそも、std::integralが満たされない場合は部分特殊化が選択されてほしいはずなのですが・・・?

部分特殊化の制限

クラステンプレートでも変数テンプレートでも、部分特殊化のテンプレートパラメータリスト(型名の後の<Ts...>)に対して次の制限がかけられています([temp.spec.partial]/9

  1. 非型テンプレート引数が特殊化されるとき、その型が部分特殊化のテンプレートパラメータに依存してはならない
  2. 部分特殊化は、プライマリテンプレートよりも特殊化(more specialized)されていなければならない
    • more specializedの判定には部分特殊化の半順序ルールが適用される
    • 部分特殊化の半順序は、クラス/変数テンプレートを関数テンプレートに変換して関数テンプレートの半順序を適用する
  3. 部分特殊化のテンプレートパラメータリストはデフォルト引数を含んではならない
  4. 実引数には未展開のパラメータパックを含めることはできない。実引数がパック展開の場合、テンプレート引数リストの最後に来なければならない

先程のエラーの原因となっていたのは、この2つ目の規則に抵触しているからです。

単純には、プライマリテンプレートのパラメータリストに対して、部分特殊化のパラメータリストの数の対応が取れていて、部分特殊化のパラメータのうち1つでも型が確定していれば、プライマリテンプレートよりも特殊化されています。これは部分特殊化の最も基本的な使用法に対応しています。

型が確定していなくても、SFINAEやコンセプトによってプライマリテンプレートが無効化される場合は部分特殊化の方がより特殊化されています。今回の例はこちらに対応しています。つまり、先程の謎のエラーが起きていたのは、部分特殊化よりもプライマリテンプレートの方がより特殊化されていたためです。

その判定では、最終的に関数テンプレートのオーバーロード解決のルールが適用されますが。今回の場合はそこに影響する要素はコンセプトによる制約のみです。従って考慮すべきはコンセプトの半順序となります。コンセプトの半順序ルールは複雑ですが、少なくとも制約されていない候補(ここでは部分特殊化)よりも制約されている候補(ここではプライマリテンプレート)の方が優先順位が高くなります。

このように、プライマリテンプレートに制約を行って部分特殊化に何も制約を行わない場合、部分特殊化はプライマリテンプレートよりも特殊化されなくなり(むしろプライマリテンプレートがより特殊化され)、部分特殊化としての基本要件を満たさないためコンパイルエラーとなっていたわけです。

このことは直感的にも、プライマリテンプレートのTstd::integralを要求するのに、部分特殊化の対応するTが無制約なのは特殊化になっていなくない?と気づくことができるかもしれません。私は気付きませんでした。

コンセプトによって順序を付ける

エラーになっていた原因がコンセプトによる優先順位付けにある事が分かったので、その解決のためにはコンセプトによって順序付けを正しく行えばよいわけです。

template<std::integral T>
struct S1 {
  static constexpr int N = 2;
};

template<std::signed_integral T>
struct S1<T> {
  static constexpr int N = 1;
};

template<std::integral T>
constexpr int v1 = 2;

template<std::signed_integral T>
constexpr int v1<T> = 1;

とはいえ、コンセプトの順序を適切に判断するのも難しいものがあるので、プライマリテンプレートには制約を行わないことが推奨されます。それは一番最初の部分特殊化にのみ制約を行う例であり、そちらのほうがより簡単に目的を達成できるはずです。

プライマリテンプレートと部分特殊化の順序

クラステンプレートでも変数テンプレートでも、オーバーロード(部分特殊化とプライマリテンプレート)の解決は関数テンプレートの半順序ルールを応用して行われます。そのルールは

  1. 一致する部分特殊化が一つだけ見つかった場合は、それを選択
  2. 一致する部分特殊化が複数見つかった場合は、部分特殊化の半順序ルールにより最も特殊化されている部分特殊化を選択する。最も特殊化された部分特殊化が複数ある場合はコンパイルエラー
  3. 一致する部分特殊化が見つからなかった場合、プライマリテンプレートが選択される

となっています。すなわち、プライマリテンプレートと部分特殊化の間では部分特殊化の方が優先順位が高くなっています。

つまり、コンセプトによってクラス/変数テンプレートをオーバーロードする場合、適切な順序付けを行うためには、まずコンセプトによる順序付けによって部分特殊化をより制約されている状態にして、プライマリテンプレートよりも部分特殊化が優先されるという事情を理解して、そのうえで部分特殊化の間で優先順序を制御する必要があります(このことはSFINAEでも同様ですが)。

言うまでもなくこれは、関数テンプレートのオーバーロード時よりも考えることが増えており複雑です。そのため、クラス/変数テンプレートのオーバーロードにおいては、プライマリテンプレートに制約をせず無効化(定義なし)したうえで部分特殊化だけを用いてオーバーロードを定義することが推奨されます。そうしておけば、すくなくともプライマリテンプレート周りの順序を気にする必要が無くなります。このような運用は、<numbers>の変数定義とその実装に見ることができます(おそらく)。

おまけ : 変数テンプレートでできること

変数テンプレートはクラステンプレートとほぼ同じことができるのですが、そのことはあまり一般的に知られていないようです。特に、部分特殊化周りの仕様はクラステンプレートと共有されています。

// detection idiom 普通の実装
template<class, template<class> class, class = void>
struct detect : std::false_type {};

template<class T, template<class> class Check>
struct detect<T, Check, std::void_t<Check<T>>> : std::true_type {};

// detection idiom 変数テンプレート版
template<class, template<class> class, class = std::void_t<>>
inline constexpr bool detect_v = false;

template<class T, template<class> class Check>
inline constexpr bool detect_v<T, Check, std::void_t<Check<T>>> = true;

// チェッカー実装
template<class T>
using check_equality_comparable = decltype(std::declval<const T&>() == std::declval<const T&>());

template<class T>
using check_iterator_type = typename T::iterator;

// 何も満たさない型
struct S {
  int n;
};


int main() {
  std::cout << std::boolalpha;

  std::cout << detect<int, check_equality_comparable>::value << '\n';
  std::cout << detect_v<int, check_equality_comparable> << '\n';
  
  std::cout << detect<S, check_equality_comparable>::value << '\n';
  std::cout << detect_v<S, check_equality_comparable> << '\n';
  
  std::cout << detect<std::vector<int>, check_iterator_type>::value << '\n';
  std::cout << detect_v<std::vector<int>, check_iterator_type> << '\n';
  
  std::cout << detect<S, check_iterator_type>::value << '\n';
  std::cout << detect_v<S, check_iterator_type> << '\n';
}

どちらが良いかはともかく、このことがあまり知られていない(ように思える)のは、以前の変数テンプレートの仕様が曖昧で、特に部分特殊化についての仕様がはっきりしていなかったせいだと思われます。この問題はP2096R2によってC++23でようやく解決されました。

P2096R2では、変数テンプレートの部分特殊化の扱いがクラステンプレートと同じになるように仕様を調整しており、これはC++14当時の仕様を明確化することを意図しています。そのため、これらの事は本来、C++14時点からクラステンプレートと共通だったはずです。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で66本あります(SG22のWG14からのものは除きます)。

N4903 PL22.16/WG21 agenda: 7 February 2022, Virtual Meeting

2022年2月7日(北米時間)に行われたWG21全体会議のアジェンダ

C++23のための4回目の全体会議です。

N4904 WG21 admin telecon meetings: 2022 summer and autumn (revision 1)

次回以降のWG21の各作業部会の管理者ミーティングの予定表。

次は2022年7月11日(北米時間)に予定されています。

N4905 WG21 2022-01 Admin telecon minutes

WG21の各作業部会の管理者ミーティングの議事録。

前回から今回の会議の間のアクティビティの報告がされています。

N4906 Transactional Memory TS2

P2066の最小トランザクショナルメモリのTechnical Specifications。

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

N4907 WG21 2022-02 Virtual Meeting Minutes of Meeting

2022年2月7日(北米時間)に行われた、WG21全体会議の議事録。

CWG/LWG/LEWGの投票の様子などが記載されています。

P0009R15 MDSPAN

多次元配列に対するstd::spanである、mdspanの提案。

以前の記事を参照

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

  • mdspan::rank[_dynamic]の型がsize_tになった
  • strideを考慮するようにlayout_strideの比較演算子を修正
  • layout_striderequired_span_sizeへのマッピングを修正
  • レイアウトマッピングクラスのoperator()の効果でindex_sequenceandのみを使用するようにした(stride(P())によるエラーを回避)
  • extents<>を処理するために、単項畳み込み式を二項へ置き換えた
  • mdspanstd::arrayを受け取るコンストラクタがExtentstd::arrayコンストラクタを呼び出すように修正
  • 提案する文言の調整・改善

などです。

この提案は現在、LWGにおいてレビュー作業中です。

P0323R12 std::expected

エラーハンドリングを戻り値で行うための型、std::expected<T, E>の提案。

以前の記事を参照

このリビジョンでの変更はよくわかりません。

この提案は2022年2月の全体会議で承認され、C++23入りが決定しています。

P0447R19 Introduction of std::hive to the standard library

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

以前の記事を参照

このリビジョンでの変更は、Introduction節の修正、sort()によってイテレータが無効化されることを追記、記載されていたQ&Aの削除、constexprについての説明の移動、などです。

P0561R6 An RAII Interface for Deferred Reclamation

deferred reclamationを実現するためのより高レベルAPIを標準ライブラリに追加する提案。

以前の記事を参照

このリビジョンでの変更は、<snapshot><experimental/snapshot>に変更した事、機能テストマクロを追加した事です。

この提案はConcurrency TS v2に向けて作業されているようです。

P0792R7 function_ref: a non-owning reference to a Callable

P0792R8 function_ref: a non-owning reference to a Callable

Callableを所有しないstd::functionであるstd::function_refの提案。

以前の記事を参照

R7での変更は、関数と関数ポインタを同じようにハンドルすることを明確にしたことです。

R8での変更は

  • メンバポインタのサポートをやめた
  • コピー代入を維持しつつ、関数ポインタ以外のCallableの代入を削除
  • 提案する文言をよりよい用語を用いて改善

などです。

P0957R5 Proxy: A Polymorphic Programming Library

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

多態性Polymorphism)はプログラムコンポーネントを分離し、拡張性を向上させるために不可欠です。ただし、そのコストとして実行時のパフォーマンスを低下させます。

現在の標準には仮想関数による継承ベースの方法とライブラリのポリモルフィックラッパ型(std::function, std::any, std::pmr::polymorphic_allocatorなど)の2つの多態性サポートがありますが、ライブラリのポリモルフィックラッパ型は特定の用途のために拡張性が制限されており、多態的なプログラミングには通常継承ベースの方法の使用が避けられません。

この提案の"Proxy"は自由に拡張可能で効率的な多態性を実現するためのユーティリティであり、多くの部分を静的に解決することによって従来のC++におけるOOPとFPの使いやすさとパフォーマンスの制限を取り払うことを目指すものです。

図形クラスによるサンプル。

// Drawableのインターフェースクラス
class IDrawable {
public:
  virtual void Draw() const = 0;    // 図形の描画
  virtual double Area() const = 0;  // 面積の取得
};

// 長方形クラス
class Rectangle : public IDrawable {
public:
  void Draw() const override;
  void SetWidth(double width);
  void SetHeight(double height);
  void SetTransparency(double);
  double Area() const override;
};

// 円クラス
class Circle : public IDrawable {
public:
  void Draw() const override;
  void SetRadius(double radius);
  void SetTransparency(double transparency);
  double Area() const override;
};

// 点クラス
class Point : public IDrawable {
public:
  void Draw() const override;
  double Area() const override { return 0; }
};

// Drawableな対象を操作する
void DoSomethingWithDrawable(IDrawable* p) {
  printf("The drawable is: ");
  p->Draw(); // .Draw()の呼び出し
  printf(", area = %f\n", p->Area());  // .Area()の呼び出し
}

// 文字列による指定からDrawableを構築する
auto MakeDrawableFromCommand(const std::string&) -> std::unique_ptr<IDrawable>;

int main() {
  std::unique_ptr<IDrawable> p;

  p = MakeDrawableFromCommand("Rectangle 2 3");
  DoSomethingWithDrawable(p.get());

  p = MakeDrawableFromCommand("Circle 1");
  DoSomethingWithDrawable(p.get());

  p = MakeDrawableFromCommand("Point");
  DoSomethingWithDrawable(p.get());
}

このよくある継承ベースのポリモルフィズムのサンプルは、Proxyによって次のように書くことができます。

#include <proxy>

// Drawableに要求される2つの操作の定義(図形の描画と面積の取得)
struct Draw : std::dispatch<
    void(), [](const auto& self) { self.Draw(); }> {};
struct Area : std::dispatch<
    double(), [](const auto& self) { return self.Area(); }> {};

// DrawableのFacadeクラスの定義
struct FDrawable : std::facade<Draw, Area> {};

// 長方形クラス
class Rectangle {
 public:
  void Draw() const
      { printf("{Rectangle: width = %f, height = %f}", width_, height_); }
  void SetWidth(double width) { width_ = width; }
  void SetHeight(double height) { height_ = height; }
  void SetTransparency(double);
  double Area() const { return width_ * height_; }

 private:
  double width_;
  double height_;
};

// 円クラス
class Circle {
 public:
  void Draw() const { printf("{Circle: radius = %f}", radius_); }
  void SetRadius(double radius) { radius_ = radius; }
  void SetTransparency(double);
  double Area() const { return std::numbers::pi * radius_ * radius_; }

 private:
  double radius_;
};

// 点クラス
class Point {
 public:
  Point() noexcept { puts("A point was created"); }
  ~Point() { puts("A point was destroyed"); }
  void Draw() const { printf("{Point}"); }
  constexpr double Area() const { return 0; }
};

// Drawableな対象をstd::proxy経由で呼び出す
void DoSomethingWithDrawable(std::proxy<FDrawable> p) {
  printf("The drawable is: ");
  p.invoke<Draw>(); // .Draw()の呼び出し
  printf(", area = %f\n", p.invoke<Area>());  // .Area()の呼び出し
}

// 文字列による指定からDrawableを構築する
auto MakeDrawableFromCommand(const std::string&) -> std::proxy<FDrawable>;

int main() {
  std::proxy<FDrawable> p;

  p = MakeDrawableFromCommand("Rectangle 2 3");
  DoSomethingWithDrawable(std::move(p));

  p = MakeDrawableFromCommand("Circle 1");
  DoSomethingWithDrawable(std::move(p));

  p = MakeDrawableFromCommand("Point");
  DoSomethingWithDrawable(std::move(p));
}

この提案のProxyの中核は、std::dispatch, std::facade, std::proxyの3つのクラスです。

std::dispatchは関数型と関数呼び出し可能なもの(Callable)をテンプレートパラメータに受け取る空のクラスで、第二引数のCallableにはディスパッチ処理を記述します(ここでは、メンバ関数Draw(), Area()の呼び出し)。std::facadestd::dispatchによって定義されたディスパッチ要件の列を受け取る空のクラスです。この2つのクラス定義ではこれ以外のことをする必要はありません(多分usingでもいいはず)。

std::proxystd::dispatch, std::facadeによって定義した要件にアダプトした任意の型のオブジェクトを保持する型消去ラッパーです。テンプレートパラメータにディスパッチ要件を指定したstd::facade<...>を受け取ります。構築及び代入では柔軟な変換によって任意のオブジェクトを受け取り(std::anyの振る舞いに近い)、.invoke<D>()メンバ関数によって保持するオブジェクトを使用してstd::dispatchD)に指定した方法によって関数の呼び出しを行います。.invoke<D>()は追加の引数を受け取って呼び出される関数まで転送することもできます。

std::proxyはポリモルフィックなオブジェクトを外部から受け取って内部に保持するもので、そのオブジェクトの構築は任意に行うことができます。それによって、それらポリモルフィックなオブジェクトのライフタイム管理の戦略をも柔軟にカスタマイズすることができます。例えば、上記例のMakeDrawableFromCommand()は次のように実装されます。

std::proxy<FDrawable> MakeDrawableFromCommand(const std::string& s) {
  // 引数をパースする(ParseCommandは別に定義されているとする)
  std::vector<std::string> parsed = ParseCommand(s);

  if (!parsed.empty()) {
    if (parsed[0u] == "Rectangle") {
      if (parsed.size() == 3u) {
        // polymorphic_allocatorとプールによるアロケートとライフタイムのカスタマイズ
        static std::pmr::unsynchronized_pool_resource rectangle_memory_pool;
        std::pmr::polymorphic_allocator<> alloc{&rectangle_memory_pool};

        auto deleter = [alloc](Rectangle* ptr) mutable { 
          alloc.delete_object<Rectangle>(ptr);
        };

        Rectangle* instance = alloc.new_object<Rectangle>();
        std::unique_ptr<Rectangle, decltype(deleter)> p{instance, deleter};

        p->SetWidth(std::stod(parsed[1u]));
        p->SetHeight(std::stod(parsed[2u]));
        
        return p; // unique_ptr -> proxyへ暗黙変換(unique_ptrを内部で保持することで間接所有する)
      }
    } else if (parsed[0u] == "Circle") {
      if (parsed.size() == 2u) {
        // ローカル(スタック)変数
        Circle circle;
        circle.SetRadius(std::stod(parsed[1u]));

        return std::make_proxy<FDrawable>(circle); // コピーして直接保持、SBO(Small Buffer Optimization)が適用される
      }
    } else if (parsed[0u] == "Point") {
      if (parsed.size() == 1u) {
        // グローバルシングルトンオブジェクト
        static Point instance;

        return &instance; // ポインタを保持
      }
    }
  }
  throw std::runtime_error{"Invalid command"};
}

このような柔軟で安全かつ効率的(SBOによる)なライフタイム管理は、従来の継承ベースの手法では困難だったものです。std::proxyはここにさらにインターフェースのディスパッチサポートが追加されていることで、継承ベースの手法と同等の多態性サポートを行うとともに、安全性と効率性や使いやすさを向上させています。

継承ベースとstd::proxyのメモリレイアウトの比較図

P1018R15 C++ Language Evolution status - pandemic edition – 2022/01-2022/02

2022年1月から2022年2月の間のEWGの活動についてのレポート。

2022年1月に行われた次の提案についてのEWGの電子投票の結果が記載されています。

否決されていない提案はC++23入りを目指してCWGでレビューされますが、否決されたものはC++23には間に合わず、C++26以降を目指して作業していくことになります。

P1202R4 Asymmetric Fences

非対称なフェンスの提案。

以前の記事を参照

このリビジョンでの変更は、どうやら文書内容の整理(承認されている部分について省略)のようです。

この提案は現在Concurrency TS v2を目指してLEWGで作業されています。

P1223R3 find_last

指定された値をシーケンスの後ろから探索するfind_lastアルゴリズムの提案。

これはstd::findの逆を行うものです。現在それを書こうとすると、単純なループかstd::reverse_iteratorを使用するかのどちらかになるでしょう。

template<std::bidirectional_iterator I, typename T>
  requires std::indirect_binary_predicate<ranges::equal_to, I, const T*>
auto find_last1(I first, I it, const T& x) {
  // ループを使った探索
  while (it-- != first) {
    if (*it == x) {
      // Use it here...
    }
  }

  return it;
}

template<std::bidirectional_iterator I, typename T>
  requires std::indirect_binary_predicate<ranges::equal_to, I, const T*>
auto find_last1(I first, I it, const T& x) {
  // reverse_iteratorとfindを使った探索
  auto rfirst = std::make_reverse_iterator(it);
  auto rlast = std::make_reverse_iterator(first);
  
  auto it2 = std::find(rfirst, rlast, x);
  // Use it here...

  return it2;
}

しかし、やりたい事の単純さと比較してこれらの記法はどちらも煩わしさがあります。理想的には次のようにかけるといいはずです。

auto it2 = std::find_last(first, it, x);

この提案はこのfind_lastをはじめとした各種ファミリー(find_last_if, find_last_if_not)の標準ライブラリへの追加を目指すものです。

namespace std::ranges {
  // イテレータペアを受け取る
  template<forward_iterator I, sentinel<I> S, class T, class Proj = identity>
    requires indirect_binary_predicate<ranges::equal_to, projected<I, Proj>, const T*>
  constexpr I find_last(I first, S last, const T& value, Proj proj = {});

  // rangeを受け取る
  template<forward_range R, class T, class Proj = identity>
    requires indirect_binary_predicate<ranges::equal_to, projected<iterator_t<R>, Proj>, const T*>
  constexpr borrowed_iterator_t<R> find_last(R&& r, const T& value, Proj proj = {});

}

現在の提案ではこれらの関数は全て対象の要素を発見した位置のイテレータを返すようになっていますが、find_last()はその実行に際してまず範囲の終端を求める必要があります(rangeを受け取る方の場合)。したがって(その他のアルゴリズム、特にranges版がそうであるように)、その有用な情報をユーザーに返す必要があります。そこで、subrangeを返すように変更することが議論されています。

namespace std::ranges {
  // イテレータペアを受け取る
  template<forward_iterator I, sentinel<I> S, class T, class Proj = identity>
    requires indirect_binary_predicate<ranges::equal_to, projected<I, Proj>, const T*>
  constexpr subrange<I> find_last(I first, S last, const T& value, Proj proj = {});

  // rangeを受け取る
  template<forward_range R, class T, class Proj = identity>
    requires indirect_binary_predicate<ranges::equal_to, projected<iterator_t<R>, Proj>, const T*>
  constexpr borrowed_subrange_t<R> find_last(R&& r, const T& value, Proj proj = {});
}

この場合、指定されたものを見つけたら[it, last)subrangeを、何も見つからなかったら[last, last)subrangeを返します。

この設計の問題点は、この戻り値がfindなどと一貫していないこと、多くの場合ユーザーは終端情報(last)を使用しないので捨てるべき余分な情報が増えるだけと思われることです。しかし、筆者の方はこれを改善であると認識していて、その方向で議論が進んでいるようです。

P1478R7 Byte-wise atomic memcpy

アトミックにメモリのコピーを行うためのstd::atomic_load_per_byte_memcpy()/std::atomic_store_per_byte_memcpy()の提案。

以前の記事を参照

このリビジョンでの変更はP2396R0の変更を適用したことです。

この提案は、Concurrency TS v2に向けてLWGに転送済です。

P1664R7 reconstructible_range - a concept for putting ranges back together

viewによって別のrangeに変換されてしまった範囲を、元のrange(と同じ型)に戻す操作、std::ranges::reconstructと関連するコンセプトの提案。

以前の記事を参照

このリビジョンでの変更は、設計に関する説明の改善、提案する文言の改善などです。

この提案は、P2516R0std::string_viewrangeコンストラクタ削除)が採択された場合にviews::splitの内側rangeの使用を改善するのに役立つ可能性があります。

std::string_view s = "12.22.32.41";

auto ints =
    s | views::split('.')
      | views::transform([](auto v) {
        int i = 0;
        // vはstring_viewオブジェクト
        if (v.ends_with('2')) {
            std::from_chars(v.data(), v.size(), &i);
        }
        return i;
    });

P1774R6 Portable assumptions

コンパイラにコードの内容についての仮定を伝えて最適化を促進するための[[assume(expr)]]の提案。

以前の記事を参照

このリビジョンでの変更は、SG22での投票の結果を追記したことなどです。

SG22の投票では、この提案をCに対しても提案することを推奨することに合意が取れています。

この提案は、EWGからCWGに送られ、レビュー待ちをしています。

P1839R3 Accessing Object Representations

reinterpret_cast<char*>によるオブジェクト表現へのアクセスを未定義動作とならないようにする提案。

この提案の解決する問題は、キャストとポインタ演算という2つの操作によって発生します。

int a = 420;

char b = *reinterpret_cast<char*>(&a);  // UB

このreinterpret_cast<char*>(&a)static_cast<char*>(static_cast<void*>(&a))と同じ効果となり([expr.reinterpret.cast]/7)、そのようなstatic_castの指定するところ([expr.static.cast]/13)によってそのポインタ値(アドレス値)は変化しません。そのため、このキャスト後のポインタは元のオブジェクトを指しています。

この時、bの初期化式にlvalue-to-rvalue conversionを適用するとその結果はintの値(420)となり、それはcharで表現できる値ではないので未定義動作となります([expr.pre]/4)。もしこの時、charにその表現が存在する場合でも、[basic.types]/4の規定するところのオブジェクト表現は配列ではなくunsgined charのオブジェクトの列となってしまい、現在のオブジェクトモデルの下ではポインタ演算に適していません。

この問題はC++17でP0137R1が採択されたことによって発生した問題のようです。この提案では、ポインタが単なるメモリのアドレス表現ではなくオブジェクトを指すものという形でポインタの動作方法の変更を行なっており、そこでは、ポインタを介してオブジェクトのオブジェクト表現へアクセスする方法についての考慮がおろそかになっていたようです。

この提案では、次のような変更によってこの問題の解決を図ります。

  • ある型のオブジェクト表現が連続したストレージを占有している場合、そのオブジェクト表現はunsigned charの配列とみなされるようにする
  • unsigned char, char, std::byteのオブジェクトとその配列は、それ自身のオブジェクト表現とする(定義の再帰防止)
  • unsigned char, char, std::byte以外の型のオブジェクト表現の各要素の値は未規定であり、unsigned char, char, std::byteのオブジェクト表現の要素の値はそれらのオブジェクト表現の値とする
  • オブジェクト表現へのポインタを、unsigned char, char, std::byteへの(ポインタ)キャストによって取得できるようにする
  • オブジェクト表現へのポインタは、キャストを用いて元のオブジェクトへのポインタに戻せるようにする
  • std::launderが、オブジェクト表現の要素ではないオブジェクトへのポインタを返すことを優先するように規定
    • 複数のオブジェクトが同じストレージを占めている時、そのような領域へのポインタに対するstd::launderが返すポインタはプログラム定義のオブジェクトを指すものとする
    • おそらく、その時生存期間内にあるオブジェクトへのポインタを返すことを意図していると思われる、たぶん・・・
  • 式の型がunsigned char*, char*, std::byte*の場合、オブジェクト表現の要素に対してポインタ演算できるようにする

これらの変更は新しい機能を導入するものではなく、既存の慣行を標準化するものです。

現在 この提案
using T = unsigned char*;
int a = 0;
T b = reinterpret_cast<T>(&a);
// ポインタ値は変更されない
// bはaを指す

T c = ++b;
// UB、式の型は要素型と異なる
using T = unsigned char*;
int a = 0;
T b = reinterpret_cast<T>(&a);
// bはa(int)のオブジェクト表現の
// 最初の要素(unsigned char)を指す

T c = ++b;
// cはa(int)のオブジェクト表現の2番目の要素を指す

++(*c); // OK
現在 この提案
using T = unsigned char*;
int a[5]{};
T b = reinterpret_cast<T>(&a);
// ポインタ値は変更されない
// bはaを指す

for (int i = 0; i < sizeof(int) * 5; ++i){
  b[i] = 0; // UB、式の型は要素型と異なる
}
using T = unsigned char*;
int a[5]{};
T b = reinterpret_cast<T>(&a);
// bはa(int[5])のオブジェクト表現の
// 最初の要素(unsigned char)を指す

for (int i = 0; i < sizeof(int) * 5; ++i){
  b[i] = 0; // OK
}

P1841R3 Wording for Individually Specializable Numeric Traits

std::numeric_limitsに代わる新たな数値特性(numeric traits)取得方法を導入する提案。

以前の記事を参照

このリビジョンでの変更は、CWG/LWGのレビュー結果の反映などです。

P1885R10 Naming Text Encodings to Demystify Them

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

以前の記事を参照

このリビジョンでの変更はP2498R1で提起された懸念とSG16の提案への対応を行ったことです。

P2498ではエンコーディングとしてIANAレジストリのものを参照しそれにほぼ固定されていることを問題としていました。SG16およびLEWGでの議論の結果、

  • IANAレジストリはデフォルトとして妥当
  • 他のものを追加しようと思えば可能
  • 設計変更を行うとC++23に間に合わなくなる

などの理由によってP2498の提案を採用しないことで合意されました。

P2093R13 Formatted output

std::formatによるフォーマットを使用しながら出力できる新I/Oライブラリstd::printの提案。

以前の記事を参照

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

P2214R2 A Plan for C++23 Ranges

C++23に向けてのrangeライブラリの拡張プランについてまとめた文書。

前回の記事を参照

このリビジョンでは、R1以降の作業の進行を反映しいくつかの提案へのリンクを追加した事です。

C++23に向けて、次のものはすでに採択済です

  • ranges::to
  • views::adjacent
  • views::adjacent_transform
  • views::chunk
  • views::chunk_by
  • views::join_with
  • views::slide
  • views::zip
  • views::zip_transform
  • ranges::iota
  • ranges::shift_left
  • ranges::shift_right
  • P2321R2(views::zipのためのpair/tupleの調整)

さらに、次のものはC++23に向けて、LWGのレビュー中です

  • views::as_const
  • views::as_rvalue
  • views::cartesian_produc
  • views::stride
  • views::repeat
  • ranges::fold
  • ranges::contains
  • P2248R4(値を指定するRangeアルゴリズム{}を使えるようにする)
  • P2408R4(C++20イテレータC++17アルゴリズムで使用可能にする)
  • P2494R1(いくつかのRangeアダプタがムーブオンリーイテレータをサポートする)

おそらくこれ以上このリストに追加されることはなく、LWGでレビュー中のものもC++23に間に合わない可能性があります。

P2416R2 Presentation of requirements in the standard library

現在の規格書の、要件(requirement)の記述方法を変更する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映したことです。

この提案は次の全体会議で投票にかけられることが決まっています。

P2438R2 std::string::substr() &&

右辺値std::stringからのsubstr()を効率化する提案。

以前の記事を参照

このリビジョンでの変更は、フィードバックに基づく提案文言の変更、Annex Cセクションの追記、機能テストマクロの追加、などです。

この提案は次の全体会議で投票にかけられることが決まっています。

P2441R2 views::join_with

パターンによってrangerangeとなっているようなシーケンスを接合して平坦化するRangeアダプタ、views::join_withの提案。

以前の記事を参照

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

この提案は2022年2月の全体会議にて承認され、すでにC++23に向けたWDに適用されています。

P2446R2 views::as_rvalue

入力シーケンスの各要素をstd::moveするようなviewである、views::moveの提案

以前の記事を参照

このリビジョンでの変更は、名前をviews::as_rvalueへ変更したこと、提案する文言の修正です。

P2448R2 Relaxing some constexpr restrictions

constexpr関数がすべての引数について定数実行不可能となる場合でも、コンパイルエラーにしないようにする提案。

以前の記事を参照

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

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

P2452R0 2021 October Library Evolution and Concurrency Polls on Networking and Executors

2021年10月ごろにLEWGとSG1のメンバーを対象に行われた、P2300とNetworking TSの方向性決定のための全体投票の周知文章。

この投票はすでに終了しており、なぜ今出てきたのかはわかりません・・・。

詳細はこちら

P2453R0 2021 October Library Evolution Poll Outcomes

2021年10月ごろにLEWGとSG1のメンバーを対象に行われた、P2300とNetworking TSの方向性決定のための全体投票(↑)の結果。

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

P2458R1 2022 January Library Evolution Polls

2022年の1月に予定されている、LEWGでの全体投票の予定表。

以前の記事を参照

P2459R0 2022 January Library Evolution Poll Outcomes

2022年の1月に予定されている、LEWGでの全体投票の結果。

次の13の提案が投票にかけられ、P2300を除いてLWGへ転送されています。また、投票に当たって寄せられた賛否のコメントが記載されています。

P2465R2 Standard Library Modules std and std.compat

標準ライブラリモジュールについて最小のサポートをC++23に追加する提案

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善とそれに伴う文言関連のQ&Aの削除などです。

この提案はすでにLWG/CWGでのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2467R1 Support exclusive mode for fstreams

fstreamに排他モードでファイルオープンするフラグ、std::ios_base::noreplaceを追加する提案。

以前の記事を参照

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

この提案はC++23を目指してLWGでレビューされています。

P2472R1 make function_ref more functional

function_refに適応的に型消去させるためのヘルパ関数make_function_ref()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、function_ref単体でのメンバポインタサポートを追求しないようにしたことから、std::nontypeを使用してfunction_refを初期化できるように拡張するようにへ変更した事です。

以前の提案は、std::make_function_reffunction_refに1つコンストラクタを追加して、メンバポインタサポートを追加することを提案していましたが、この提案ではP2511の方向性を採用したうえで、function_refにそれを受け取るためのコンストラクタを追加することによって{}だけからfunction_refをメンバポインタのために初期化できるようにすることを提案しています。

struct cat {
    void walk() {
    }
};

void leap(cat& c) {
}

void catwalk(cat& c) {
    c.walk();
}

struct callback {
    cat* c;
    void (*f)(cat&);
};

cat c;

// メンバ関数の呼び出しを部分適用して引数なしにする例
void mem_func() {
  // callback構造体の利用
  callback cb = {&c, [](cat& c){c.walk();}};
  callback cb = {&c, catwalk};

  // ラムダでラップ
  auto temp = [&c](){c.walk();};  // ダングリング防止のため中間オブジェクトが必要
  function_ref<void()> fr = temp;

  some_function([&c](){c.walk();}); // some_functionの呼び出し中でだけ使われるなら直接渡せる

  // この提案
  function_ref<void()> fr = {std::nontype<&cat::walk>, c};
}

// フリー関数の呼び出しを部分適用して引数なしにする例
void free_func() {
  // callback構造体の利用
  callback cb = {&c, [](cat& c){leap(c);}};
  callback cb = {&c, leap};

  // ラムダでラップ
  auto temp = [&c](){leap(c);};
  function_ref<void()> fr = temp;

  some_function([&c](){leap(c);});

  // この提案
  function_ref<void()> fr = {nontype<leap>, c};
}

同じことをしているはずですが、function_ref使用時は場合によって2パターンの初期化方法に分かれています。このことはfunction_refを使用しづらくしており、安全性を損ねています。同じfunction_refを複数回使いたい場合、一度変数に受けてから使いまわすことはおそらくよく行われるでしょう。その場合、ダングリングを回避するには追加の(余分な)コードが必要となります。

この提案によるソリューションでは、関数に渡すときと変数を初期化する時で異なる構文を使用する必要はなく、より便利で安全です。また、コア言語のサポート(callback構造体のような)と一貫しています。

std::string_view sv = "hello world"s; // 次の行(`;`)以降ダングリング
auto cs = "hello world"s;
std::string_view sv = cs;             // ダングリングではない

function_ref<void()> fr = [&c](){c.walk();};  // 次の行(`;`)以降ダングリング
function_ref<void()> fr = [&c](){leap(c);};   // 次の行(`;`)以降ダングリング

function_ref<void()> fr = {nontype<&cat::walk>, c}; // ダングリングではない
function_ref<void()> fr = {nontype<leap>, c};       // ダングリングではない

P2495R0 Interfacing stringstreams with string_view

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

std::stringstreamstd::stringによる文字列を受け取ってその文字列によるストリームを構成するものです。C++20から.view()メンバ関数によってストリームの中身へのstd::string_viewを取得することができるようになっていますが、ストリームの最初の内容をコンストラクタで指定する際にはstd::stringしか渡せません。また、C++20から.str()メンバ関数によって後からストリームの中身を置き換えることができるようになっていますが、ここでもstd::stringしか渡せません。

std::stringstreamに文字列リテラルおよびstd::string_viewを渡そうとすると、std::stringの一時オブジェクトを作成してからそれをコンストラクタに渡さなければなりません。しかも、std::stringstreamはそこからさらに内部のstd::stringを構築するために、コピー/ムーブすることになります。

この提案はこの問題を解決するために、コンストラクタと.str()の両方がstd::string_viewを受けとれるようにするものです。

その際問題となるのが、文字列リテラルを渡した時にstd::stringを受け取るオーバーロードstd::string_viewを受けとるオーバーロードで曖昧になることです。いくつかの解決が考えられますが、この提案では文字列リテラル用のオーバーロードconst char*を受け取る)も同時に追加することでそれを解消しています。

この提案による変更の例

const ios_base::openmode mode;
const allocator<char> alloc;
const string str;
// mystringはstring_viewに暗黙変換可能だとする
const mystring mstr;

stringstream s0{""};                  // ok
stringstream s1{"", alloc};           // ng -> ok
stringstream s2{"", mode, alloc};     // ng -> ok

stringstream s3{""sv};                // ng -> ok
stringstream s4{""sv, alloc};         // ng -> ok
stringstream s5{""sv, mode, alloc};   // ng -> ok

stringstream s6{""s};                 // ok
stringstream s7{""s, alloc};          // ok
stringstream s8{""s, mode, alloc};    // ok

stringstream s9{str};                 // ok
stringstream s10{str, alloc};         // ok
stringstream s11{str, mode, alloc};   // ok

stringstream s12{mstr};               // ng -> ok
stringstream s13{mstr, alloc};        // ng -> ok
stringstream s14{mstr, mode, alloc};  // ng -> ok

stringstream s15;
s15.str("");      // ok
s15.str(""sv);    // ng -> ok
s15.str(""s);     // ok
s15.str(str);     // ok
s15.str(mstr);    // ng -> ok

P2502R1 std::generator: Synchronous Coroutine Generator for Ranges

Rangeライブラリと連携可能なT型の要素列を生成するコルーチンジェネレータstd::generator<T>の提案。

以前の記事を参照

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

  • generator::yielded(型)を説明専用ではなくし、publicにした
  • 機能テストマクロの追加
  • generator<T&&>のコルーチンでは、左辺値をyieldできるようにした(コピーしたxvalueが生成される)
    • これによって、co_yield auto(lvalue)の代わりにco_yield lvalueと書けるようになった
  • テンプレートパラメータの順番を再度並べ替え(Allocatorを最後に)
    • 議論の結果、Allocatorパラメータがテンプレートパラメータリストの最後に無いことは標準ライブラリの他の部分と一貫していないという合意が得られた
  • elements_ofをシンプルな2要素構造体にした
    • elements_of再帰したrangegenerator)をyield式で1要素づつ生成していくような変換を行うもの
  • P2529R0の設計に従って、generator<T>generator<T&&>のように動作するようにした

この提案は、C++23に向けて現在LWGのレビュー中です。

P2505R1 Monadic Functions for std::expected

std::optionalのMonadic interfaceをstd::expectedにも導入する提案。

以前の記事を参照

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

  • transform_orの追加
  • expected<void, E>transformを追加
  • サンプルコードの改善
  • or_elseの欠けていたオーバーロードを追加
  • エラー値に対する関数(or_else, transform_or)での、戻り値型の変更に関する議論の追加

などです。

transform_orはエラー値に対してtransformするもので、参照実装ではmap_errorと呼ばれていたようです。要するに、expected<T, E> -> expected<T, E2>の変換を行うものです(or_elseとの違いは、エラー状態からの脱却ができないこと)。

この提案は、C++26に向けて議論されることになりました。

P2506R0 std::lazy: a coroutine for deferred execution

コルーチンによって非同期タスクを表現するためのユーティリティ、std::lazyの提案。

std::lazyC#などの他の言語ではTaskという名前のクラスであることが多いものです。

これはコルーチンによって非同期処理の実行と待機、継続処理の実行を自動化するためのユーティリティ型で例えば次のように使用します。

// 何かのデータ型
struct record {
  int id;
  std::string name;
  std::string description;
};

// recordをどこかから読み出す非同期処理
std::lazy<record> load_record(int id);
// recordをどこかに保存する非同期処理
std::lazy<> save_record(record r);

// recordの更新処理
std::lazy<void> modify_record() {
  // これらの処理はこの順番に実行される
  record r = co_await load_record(123);     // 1. レコードの読み出し
  r.description = “Look, ma, no blocking!”; // 2. 1の実行後rが初期化されてから実行
  co_await save_record(std::move(r));       // 3. レコード保存。2の実行後に実行される
  std::cout << "modify end.\n";             // 4. 3の実行が完了してから実行される
}

このmodify_record()をコルーチンではなくfutureとコールバックによって同じ実行順となるように書くと次のようになります

// modify_record()の非コルーチン版
void modify_record_async() {
  auto f1 = load_record_async(123, [](record r) {
    r.description = “Look, ma, no blocking!”;
    auto f2 = save_record_async(std::move(r), [](){
      std::cout << "modify end.\n"
    });

    f2.wait();
  });

  f1.wait();
  // f1, f2はstd::future相当のものとする
}

このコールバックコードで手動でやっているようなことを、co_await構文を使用することで隠蔽・自動化するためのコルーチン制御を担っているのがstd::lazy型です。

std::lazyは元々は別の著者によって別の提案(P1056R1)で議論されていましたが、そちらは長期間止まっていたため、この提案が引き取った形です。そのため、ほとんどの部分の設計は変更されていません。筆者の方は、std::lazyを含めた基本的なコルーチンユーティリティがC++20に含まれていないのは間違いであり、C++23に含まれないのはもっと間違いだ、と述べていますが、この提案は今の所C++23に向けたものではありません(C++23の設計フェーズはすでに終了しているため)。

std::lazyは次のようなとてもシンプルで小さいクラスです

template<class T, class Allocator = void>
class [[nodiscard]] lazy {
public:
  lazy(lazy&& that) noexcept;
  ~lazy();

  unspecified operator co_await();

  T sync_await();
};

おそらくこれらのメンバ関数を明示的に呼び出すことはないでしょう。テンプレートパラメータTはコルーチンから返す値の型(move_constructibleであること)を指定し、何も返さない場合はvoidを指定することができます。2つ目のAllocatorは、コルーチンステートのために必要となるヒープアロケーションをカスタマイズするために指定します。

std::lazyはコルーチンとその呼び出し元の境界で暗黙的に使用されるものであって、上記サンプルコードにあるように、呼び出し側のco_awaitを除いてコルーチン側でもこの型に対して何か明示的な操作をする必要はありません。

C++26 Executorライブラリ(P2300)(予定)では、senderとコルーチンの相互変換などの仕組みが用意されており、std::lazyも別の提案によって後ほどP2300にアダプトする作業が行われる予定です。

なお、std::lazystd::taskという名前ではないのは、std::taskという名前をここで消費してしまうことを嫌ってのことのようです。

この部分の7割は以下の方のご指摘によって成り立っています。

P2513R1 char8_t Compatibility and Portability Fix

char8_tの非互換性を緩和する提案。

以前の記事を参照

このリビジョンでの変更は、タイポやタイトルの修正、配列を含む構造体の集成体初期化時のオーバーロード解決について説明の追加、文言の追加と集成などです。

この提案はSG16からEWGへ転送され、C++20へのDRとすることを目指してるようです。

P2521R1 Contract support -- Working Paper

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

以前の記事を参照

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

  • 関数の再宣言とオーバーライド時のコントラクト注釈を禁止
  • コントラクトの条件式に仮想関数呼び出しが含まれている場合に起こることを明確化
  • MVPに事後条件を含めることに関する投票結果の追記
  • 翻訳単位毎にコントラクトチェックを有効にする/しないを混ぜてコンパイルするモード(mixed translation mode)の必要性について追記
    • 現在のmixed translation modeは診断不用のill-formed
  • 関数本体内に事前条件チェックを実装する必要がある事を示唆する記述の削除
  • 仮想関数に関する特殊なケース(基底クラスがテンプレートパラメータによって指定されている時や複数の関数をオーバーライドするとき)についての説明の追加
  • フリースタンディング環境でも使用可能とすることを目指していることを明確化
  • コントラクト注釈内のnoexceptと似たルックアップを定義しないようにした

などです。

P2528R0 C/C++ Identifier Security using Unicode Standard Annex 39

セキュリティ上の問題のある文字を識別子に使用できなくする提案。

C++20では(DRなので以前のバージョンに対してもですが)P1949の採択によって不可視の文字や絵文字など、識別子(変数名やクラス名など)として使用すると問題のある一部の文字が識別子に使用できなくなりました。そこでは、ユニコード標準のAnnex 31というものを参照して識別子として使用可能な文字の集合を指定(制限)していました。

ユニコード標準にはAnnex 39としてユニコード文字のセキュリティ上の問題を解決するためのメカニズムを提供しています。P1949では、UAX31とUAX39の両方を実装するのは困難であるとして、UAX31による制限だけを提案していました。この提案は、筆者の方の経験(libu8identの実装)やRustやJavaなどでUAX31とUAX39が正しく実装されていることから、両方を実装するのは可能であるとしてそれを提案するものです。

UAX31に加えてUAX39を実装するとユニコード識別子に関する既知のセキュリティ上の問題のほとんどを修正することができます。残った問題は、文字列やコメント中のbidiオーバーライド(文字列中での双方向テキストの切り替えによる攻撃)ですが、これは識別子に対する制限だけでは解決できないため、この提案の範囲ではありません。それでも、最近GCCに実装されたように、トークナイザやプリプロセッサの警告によって発見することができます。

提案していることは以下の事です

  • UAX39 #5.2 Mixed-Scripts Moderately Restrictive profileに部分的に準拠
  • UAX31 #Table_7(限定文字)とUAX31 #Table_4(除外文字)をすべて使用しない
  • UAX39 #Table_1のRecommended, Inclusion, Technicalに分類される文識別子タイプのみを許可
  • NFCの正規化を要求する
    • すべてのcomposable sequence(結合文字のこと?)を拒否する
  • 混合スクリプト (SCX)を使用した不正な形式の混合マークシーケンス(Sk, Cf, Mn, Me)を拒否する
  • オプション
    • #pragma unicode <LongScript>によって、ソースファイルごとに除外スクリプトを指定できるようにする

ここに出てくるスクリプトというのは、特定の文字の集合みたいな意味です(日本語では用字)というらしい)。

提案では、拒否する文字が識別子に現れた場合はコンパイルエラーとすることを提案しています(ただし、それは政治的な決断になるかもしれないと言っています)。

この部分の7割は以下の方のご指摘によって成り立っています。

P2529R0 generator should have T&& reference_type

提案中のstd::generator<T>reference_typeT&&であるべきとする提案。

std::generatorは現在P2502で議論されていますが、そこではstd::generator<T>についてそのreference_typeが何であるべきかが主な論点になっているようです。現在の提案ではconst T&が提案されています。この提案は、それはT&&とするように推奨しその根拠を述べるものです。

前提として、誰もが合意するであろうことは

  • std::generator<U>UT&, const T&, T&&である場合
    • reference_typeUvalue_typeremove_cvref_t<U>
    • promise_type::yield_valueシグネチャyield_value(U)
  • std::generator<U>Uconst T, const T&&、またはvolatileである場合を考慮する必要はない
    • これらを機能させるために時間をかける価値はない
  • std::generator<T>TがCV・参照修飾されていない場合
    • value_typeのデフォルトはT
    • reference_typeT, const T, const T&&ではない
      • TはP2168R3で提案されていたがその時点での著者はそれが正しい選択ではないことに同意している
      • これによって、T, const T, const T&&は候補から外れる
    • co_yield EXPRをサポートする、この時のEXPRは左辺値もしくは右辺値
      • これは右辺値のみをco_yieldできるstd::generator<T&&>では異なる
      • co_yieldに渡される左辺値とconst右辺値は関数の外部から変更できないようにする必要がある
      • co_yieldに渡された非const右辺値は変更されるか、少なくともムーブされる
        • 必ずそうしなければなら無いという事ではなく、右辺値が変更されることに対する異議があるとは思っていないということ

そして、問題となっているのは

  • std::generator<T>TがCV・参照修飾されていない場合
    • reference_typeT&&const T&のどちらか
      • const T& : P2502R0
      • T&& : この提案
      • T&は既存のジェネレータで採用されており有効なオプションだが、std::generatorに対して提案した人はいない
    • co_yieldに左辺値が渡された時にどうすべきか?
      • P2502R0はconst参照によって直接返すことを提案している
      • この提案では、コピーしてその右辺値を返すことを提案する

この提案では、次の4つの理由によってT&&とこの振る舞いが最適であると説明しています。

パフォーマンスとムーブオンリー型のサポート

make_gen()std::generator<T>を返す関数であるとして、

`T&&` `const T&`
ranges::copy(generator, output); // no copies

make_gen() | to<vector>; // no copies

for (auto s : make_gen()) {} // no copies

for (auto&& s : make_gen()) {
    consume(s); // copies (as it should!)
    consume(std::forward<decltype(s)>(s)); // no copies
}
ranges::copy(generator, output); // copies

make_gen() | to<vector>; // copies
make_gen() | views::move | to<vector>; // still copies
for (auto s : make_gen()) {} // copies

for (auto&& s : make_gen()) {
    consume(s); // copies (as it should!)
    consume(std::forward<decltype(s)>(s)); // copies
    consume(std::move(s)); // still copies;
}

この2つの問題は異なる問題ですが、同じサンプルコードで表現できます。この場合のTがムーブオンリーであった場合、上記の全てのコピーはコンパイルエラーとなります。

reference_typeconst T&であることで、あらゆる場合に要素型のムーブが妨げられることによってこの違いが出ています。generatorが自分で変更できない場合、この問題を解決することはできません。

言語の他の部分との一貫性

  • std::generator<T>を使用するコルーチン内部
    • co_yield EXPR;は、fTをとる時のf(EXPR)Tを返す関数の内部でreturn EXPR;した時と同様に考えるのが良い。どちらの場合も、EXPRが左辺値の時はコピーされるが、右辺値の時はそうではない。
  • Tを取る関数の引数の場合
    • 同等のものは、T&&const T&を取るオーバーロードであると認識されている。xvalue引数の最終的な状態は呼び出された側に依存するため厳密には等価ではないが、std::move()して渡した後の変数の状態は通常気にされないので無視できる。
    • さらに、ムーブ不可能なxvalueをT&&/const T&をとる関数に渡すことはできても、Tを取る関数に渡すことはできない。

これは、std:generator<T>::promise_type::yield_value()の振る舞いについてのこと(及び提案)です。寿命が短すぎるためTを直接取ることはできないため、次善の策としてT&&を取るようにしておき(これはTが参照の場合にも正しい動作をする)、copyable(かつmovableでない)非参照の型のためにconst T&オーバーロードを提供して渡されたオブジェクトのコピーを作成できるようにします。

ジェネレータのユースケースに最適

std::generator<T>はその名前が示すように、複数のTのオブジェクトを生成して返す場合に最適なツールです。一方で、既存のデータ構造に対してイテレータを作成するためのツールにはなり得ません。一般的にその用途にはforward_rangeが求められますが、std::generatorは必然的にinput_rangeにしかなりえません。

そのため、範囲の各要素は高々1度しかアクセスされず、std::generatorを使用するコルーチンが新しいオブジェクトを生成する場合にそれを一々コピーさせるのは明らかに無駄です。

筆者の方のコードベースでは、このジェネレータのようなものを使用していませんが、似たようなパターンを色々な場所で使用しているようです。そこでは、連続した値の生成のためにSTLスタイルのイテレータではなくstd::optional<T>を返すgetNext()のようなものを使用しています。そして、それを使用するほぼすべての場所では、返されるのは右辺値であるか、ムーブが安全なローカル変数です。そのようなAPIは当初、呼び出し側が所有するオブジェクトを返すためにT*を返すことができましたが、誰も使用しなかったようです。

シンプルさ

この提案の主なポイントは、std::generator<T&&>::iterator::operator*()は常にT&&を返す必要がある、ということです。

これは、言語の参照の折り畳みルールを使用して、Tが参照型である場合も含めてすべての場合に正しい振る舞いをします。それによって、reference_type等の導出にconditional_tを用いるなど複雑なことをしなくても良くなります。

これらの理由により、co_yieldの引数型およびoperator*の戻り値型として非参照のTを使用することはいいアイデアではなく、常にT&&を使用するようにすることを提案しています。

この提案は既にLEWGのレビューを終えて、P2529への適用に合意が取れています。これによって、std::generator<T>std::generator<T&&>と同等の振る舞いをするようになります。

P2530R0 Why Hazard Pointers should be in C++26

標準ライブラリにハザードポインタサポートを追加する提案。

これは現在Concurrency TS v2(未発行)に含まれているハザードポインタ関連のユーティリティのサブセットをC++26の標準ライブラリに含めようとする提案です。以前はP1121で議論されていたため、詳細はそちらの解説記事を参照してください。

この提案の動機と根拠は、P1121R3のハザードポインタの設計が2018年頃にはほぼ固まっており変更されていない事、FacebookのFollyというライブラリで2016年頃から実装され、Facebook社内で2017年頃から実利用されていること、などによります。提案しているのはConcurrency TS v2にあるハザードポインタ関連のすべてではなく上記の経験に基づく安定している一部であり、文言の変更も軽微であるため少ない労力で導入できると述べています。

P1121R3およびConcurrency TS v2から含まれていないのは次の2点です

  • カスタムドメイン
    • ドメインとはハザードポインタを参照(使用)する範囲(主にスレッド)のこと
    • Follyの経験では、カスタムドメインなしでもデフォルトのドメインを拡張・改良可能だった
    • これはカスタムドメインの必要性を否定するものではないが、一般的なユースケースに必要であるかは不透明であるため、この提案には含めない
  • グローバルクリーンアップ関数(hazard_pointer_clean_up()
    • グローバルクリーンアップ関数が呼び出されると、ハザードポインタによってロックされているすべてのリソースが再利用可能となる(つまりロックが解放される)。これをsynchronous reclamationと呼ぶ。
    • Follyでは、グローバルクリーンアップよりも効率的なsynchronous reclamationcohort-based synchronous reclamation)を実装しており、2018年頃から利用されている。こちらを導入させたいためグローバルクリーンアップはこの提案に含めない。
    • ただし、cohort-based synchronous reclamationに関する文言は複雑となることが予想され(Concurrency TS v2にも含まれていない)、ハザードポインタの一般的なユースケースのサポートを優先するためにこちらも将来の提案とする。

結果として、提案されているのはハザードポインタクラスそのもの及びそのファクトリ関数(とswap()特殊化)のみです。ただしこのドロップされたものも含めて将来の拡張の計画があるようで、それを意識した文言になっています。

P2531R0 C++ Standard Library Issues to be moved in Virtual Plenary, Feb. 2022

今回(2022/02)の会議で採択された標準ライブラリについてのIssue報告とその解決。

  1. 3088. forward_list::merge behavior unclear when passed *this
  2. 3471. polymorphic_allocator::allocate does not satisfy Cpp17Allocator requirements
  3. 3525. uses_allocator_construction_args fails to handle types convertible to pair
  4. 3601. common_iterator's postfix-proxy needs indirectly_readable
  5. 3607. contiguous_iterator should not be allowed to have custom iter_move and iter_swap behavior
  6. 3610. iota_view::size sometimes rejects integer-class types
  7. 3612. Inconsistent pointer alignment in std::format
  8. 3616. LWG 3498 seems to miss the non-member swap for basic_syncbuf
  9. 3618. Unnecessary iter_move for transform_view::iterator
  10. 3619. Specification of vformat_to contains ill-formed formatted_size calls
  11. 3621. Remove feature-test macro __cpp_lib_monadic_optional
  12. 3632. unique_ptr "Mandates: This constructor is not selected by class template argument deduction"
  13. 3643. Missing constexpr in std::counted_iterator
  14. 3648. format should not print bool with 'c'
  15. 3649. [fund.ts.v3] Reinstate and bump __cpp_lib_experimental_memory_resource feature test macro
  16. 3650. Are std::basic_string's iterator and const_iterator constexpr iterators?
  17. 3654. basic_format_context::arg(size_t) should be noexcept
  18. 3657. std::hash<std::filesystem::path> is not enabled
  19. 3660. iterator_traits<common_iterator>::pointer should conform to §[iterator.traits]
  20. 3661. constinit atomic<shared_ptr> a(nullptr); should work

P2532R0 Removing exception_ptr from the Receiver Concepts

P2300について、receiverコンセプトからexception_ptrを取り除く提案。

P2300のreceiverの定義は次の2つのコンセプトからなります。

template <class T, class E = exception_ptr>
concept receiver =
  move_constructible<remove_cvref_t<T>> &&
  constructible_from<remove_cvref_t<T>, T> &&
  requires(remove_cvref_t<T>&& t, E&& e) {
    { execution::set_stopped(std::move(t)) } noexcept;
    { execution::set_error(std::move(t), (E&&) e) } noexcept;
  };

template<class T, class... An>
concept receiver_of =
  receiver<T> &&
  requires(remove_cvref_t<T>&& t, An&&... an) {
    execution::set_value(std::move(t), (An&&) an...);
  };

ここで要求されているのは、receiverオブジェクトに対してはset_stopped(), set_error(), set_value()の3つの関数(tag_invokeによるCPO)による呼び出しが可能であることです。この3つはreceiverの提供する3つの通知チャネルであり、接続されたsenderによってその処理の実行後にいずれか1つが呼び出されることが保証されています。3つはそれぞれ、完了(キャンセル)・エラー・成功時の結果の通知を担うチャネルです。

この提案が問題としているのはset_error(R, E)チャネルのデフォルトのエラー型で、上記定義(現在)ではexception_ptrが設定されています。これについて、LEWGでのP2300レビュー中に次のような問題が提起されました

  1. set_value()は例外を投げる可能性があり(何が飛んでくるかわからず)、sendercompletion signatureset_error(), set_value()チャネルで送信される値の型)を計算するように要求された時でもそれがわからないことから、ほとんどのsenderは実際にその経路が使用されるかに関わらずexception_ptrによって完了するようなcompletion signatureを返してしまう可能性がある。
    • これによって、使用されないにも関わらず、exception_ptrを考慮するエラー処理経路のインスタンス化を引き起こす可能性がある。
  2. receiverRexception_ptrによるエラーチャネルを提供しなければ、receiver<R>, receiver_of<R, Args...>といったコンセプトを満たすことができない。
    • exception_ptrstd::shared_ptrと比較すると重いエラーチャネルである。このチャネルの存在を要求してしまうと不必要なコード生成を引き起こす可能性が高い
    • 例外処理のサポートが無い場合が多いフリースタンディング環境でP2300の機能の何が使用可能かが不透明となる

この提案では、これらの懸念に対応するために次のような変更を行っています

  • get_envカスタマイゼーションポイントのデフォルト実装を削除
  • receiver_ofコンセプトはreceiver型とcompletion_signatures<>型を取るようにする
  • receiverによるset_valueのカスタマイズはnoexceptである必要がある
  • sender_to<S, R>コンセプトでは、Rreceiver型)がSsender型)の全ての完了通知を受け入れる必要がある
  • connect(s, r)では、rreceiverオブジェクト)がssenderオブジェクト)の全ての完了通知を受け入れることを要求する
  • get_completion_signaturescompletion_signaturesクラステンプレートのインスタンスを返す必要がある
  • make_completion_signaturesをより一般的になるように調整

これらの変更は、sendercompletion signatureから得られる情報を用いた型チェックをライブラリ側でより行うようにするものです。これらの変更がなぜこのタイミングでなされたのかというと、元々のsenderは必ずしもcompletion signatureを提供しておらず、その情報を前提とすることができなかったからです。P2300R4(2022年1月発行)での変更によって、そのようなuntyped senderのサポートが削除され、senderはデフォルトかつ全てがtyped sendercompletion signatureを提供するsender)となりました。この提案はP2300R4で開かれたこの新しい情報を用いてreceiver(およびP2300全体)のexception_ptrへの依存を減らすものです。

この提案はすでにP2300へ適用されることが決定しています。

P2533R0 Core Language Working Group "ready" Issues for the February, 2022 meeting

今回(2022/02)の会議で採択されたコア言語についてのIssue報告とその解決。

  1. 1726. Declarator operators and conversion function
  2. 2494. Multiple definitions of non-odr-used entities
  3. 2499. Inconsistency in definition of pointer-interconvertibility
  4. 2502. Unintended declaration conflicts in nested statement scopes
  5. 2506. Structured bindings and array cv-qualifiers
  6. 2509. decl-specifier-seq in lambda-specifiers
  7. 2511. cv-qualified bit-fields

P2534R0 Slides: function_ref in the wild (P0792R7 presentation)

function_refP0792R7)の紹介スライド。

内容としてはむしろ、function_refで問題となっている関数ポインタの右辺値を参照するとUBになる可能性があるという問題の紹介がメインです。

struct retry_options {
  // 実行する処理を受け取るfunction_ref
  function_ref<payload()> action;
  ...
};

// optに指定された事を成功するまで繰り返すような関数とする
auto retry(retry_options opt) -> payload;


// retryに渡すオプション
auto opt = default_strategy();
// 実行する処理の指定、downloadは引数なしで呼び出してpayloadを返す関数とする
opt.action = &download; // 関数ポインタの右辺値を渡している

auto result = retry(opt);

function_refの一つの実装として、関数ポインタを渡されたときに関数ポインタへのポインタを保持して呼び出しに使用する、というものがあります。その場合、この例のように関数ポインタの右辺値を渡してしまうと一時オブジェクトへのポインタを保持する事でUBになってしまいます。その場合でも、関数そのものやデリファレンスして関数参照を渡すとUBにはなりません。

opt.action = download;      // ok
opt.action = &download;     // UB
opt.action = *(&download);  // ok

現在の提案では(そしてこのスライドの結論では)、function_refを関数と関数ポインタで初期化した時の違いがないようにする方向で設計されているようです。

類似の問題として、メンバ関数ポインタを渡した時の振る舞いについての問題があります。

function_ref<void(Ssh&)> cmd = &Ssh::connect; // UB?

Ssh ssh{};
cmd(ssh); // ok?

この振る舞いを認めると、内部でメンバポインタを保持するためにunionを使用しなければならなくなるなど、実装が複雑になります。現在の提案では(そしてこのスライドの結論では)、この振る舞いはサポートせず、別の方法(P2511など)によってfunction_refにアダプトする方向性のようです。

function_ref<void(Ssh&)> cmd = nontype<&Ssh::connect>;  // ok

P2535R0 Message fences

対象オブジェクトを限定するフェンス、メッセージフェンスの提案。

現在C++で利用できるメモリフェンスにはstd::atomic_thread_fenceがあります。これはstd::atomicオブジェクトへの読み書きの操作がそのフェンスを越えて前後しないようにするためのもので、フェンスが置かれているとそのスレッドの全てのstd::atomicオブジェクト(あるいは間接的に他の変数)の読み書きの順序に影響を与え、フェンスの効果はすべてのスレッドにわたって保証されます。

メモリモデルの説明によく見られる2つのスレッド間でのメッセージパッシングの例のように、特定の少数のスレッドの間でのやり取りのみでフェンスの効果が必要である場合、フェンスのもたらす効果をその他多くのスレッドの間でも保証することは場合によっては(特殊なメモリファブリックやメモリ構造を持つハードウェアなど、例えばGPU等のアクセラレータで)とても高価になります。

メッセージフェンスでは、特定のオブジェクトに対してのみフェンスの効果を適用することでフェンスの効果が及ぶスレッドを限定し、そのようなユースケースにおいてさらなる最適化を図るものです。そのようなメッセージフェンスを標準化することで、ハードウェアや環境固有のより効率的なメカニズムをポータブルかつ容易に利用できるようになります。

現在 この提案
x = 1;
atomic_thread_fence(memory_order_release);
a.store(1, memory_order_relaxed);
while(a.load(memory_order_relaxed) != 1);
atomic_thread_fence(memory_order_acquire);
assert(x == 1);
x = 1;
atomic_object_fence(memory_order_release, x);
a.store(1, memory_order_relaxed);
while(a.load(memory_order_relaxed) != 1);
atomic_object_fence(memory_order_acquire, x);
assert(x == 1);

提案されているのは、std::atomic_message_fence()std::atomic_object_fence()の2つです。std::atomic_thread_fence()はその存在がstd::atomicオブジェクトへのアクセスに対してのみ影響を及ぼし、他の変数へは直接的な影響を持ちません。std::atomic_message_fence()はそれと同様に、そのフェンスが介在するstd::atomicオブジェクトに対して暗黙にメッセージフェンスを適用するもので、std::atomic_object_fence()は指定された変数(非std::atomicも含む)に対してメッセージフェンスを適用するものです。

std::atomic_message_fence()をメッセージフェンスと呼び、std::atomic_object_fence()をオブジェクトフェンスと呼んでいます。オブジェクトフェンスは同時にメッセージフェンスでもあり、std::atomic_thread_fence()の事をスレッドフェンスと呼びます。この提案では次のような規定によってメッセージフェンスの効果を指定しています

オブジェクトOと任意のフェンスXYが存在していて

  • Oに対する評価(アクセス)AXの出現よりも前に順序づけられ(sequenced before)、かつ
  • XがオブジェクトフェンスならばOとともに起動され、かつ
  • YがオブジェクトフェンスならばOとともに起動され 、かつ
  • Yの出現は、Oに対する別の評価Bよりも前に順序づけられている(sequenced before

とき、Oに対する評価Aと同じオブジェクトOに対する別の評価Bの間にhappens before関係が定義されます。

さらに、XYが共にスレッドフェンスであるならば、XYと同期(X synchronize with Y)します。

なお、この2つのメッセージフェンスに対してはmemory_order_seq_cstを指定することができません。メッセージフェンスは2つのスレッドの間でのメッセージパッシングが成立する程度の順序保証しか与えないため、その順序が別のスレッドからどう観測されるかについて何の保証も与えません。ある特定のメモリ領域への読み書きの推移的な順序に関する保証もなく、sequential consistencyのような強い順序保証を提供するものではないからです。

この部分の9割は以下の方のご指摘によって成立しています。

P2536R0 Distributing C++ Module Libraries with dependencies json files.

P2473R1で提案されているビルド済みモジュールの配布のための、別のアプローチについての提案。

この提案のシナリオは次のようなものです

  1. あるグループAはC++モジュールとしてライブラリAを構築し、BMIBinary Module Interface、ビルド済みのモジュールインターフェース)として配布したい。Aはそのパッケージに含まれていない別のモジュールに依存している。
  2. グループBはライブラリAを使用したい。互換性のあるコンパイラとツールを使用しているのでビルド済みのライブラリやモジュールを使用できる。
  3. 静的解析(あるいはBMI形式を理解しない他のツール)は、グループBのソース・ビルドにおいてモジュールのソースとそれをビルドするために使用されたコンパイルオプションを見つける必要があり、ここに2つのシナリオがある
    1. . メインビルドが成功した後でモジュールを再ビルドする。つまり、グループBのビルドで生成された情報を利用できる
    2. . メインビルドが行われる前にモジュールを再ビルドする。Visual Studioのインテリセンスなど

この時、2と3においてビルド済みモジュールを活用するために必要な情報は何で、ライブラリAから何が必要で、どう見つかる必要があるか?を考え、それらの必要な情報をJSONファイルとしてコンパイラが出力するようにすることを提案するものです。

以下、提案の説明の訳。

ビルド済みのモジュールを使用するにはどのような情報が必要か?

  • 2 : モジュールAを利用するとき
    • 現在のビルド設定と一致するモジュールAのBMIの場所
    • モジュールAの依存するすべての依存関係のBMIの場所
  • 3 : 静的解析など、モジュールAのインターフェースをリビルドするとき
    • グループBのPC上にある、モジュールAのインターフェースのソースファイルの場所
    • グループBのPC上にある、すべての依存インクルードファイルの場所
    • すべてのモジュール依存関係(上2つと同じ情報など)
    • 現在のPCに依存しない(場所以外の)ビルドオプション

メインビルドによって生成された依存関係情報使用してモジュールをリビルドする(シナリオ3.1)

コンパイラはモジュールそのものも含めた翻訳単位をビルドするために、使用しているすべてのBMIの場所を認識している必要があります。したがって、この情報を他の情報(#include等の依存関係情報)と共に出力しておくことができます。できれば、現在MSVCが/sourceDependenciesに対して生成しているような、簡単に解析可能なjson等の形式であることが望ましいです。

インクリメンタルビルドをサポートするにはすべてのソース依存関係に関する情報が必要であるため、これらの情報の必要性はこの提案の前提シナリオ固有のものではありません。

例えば、グループBの次のようなソースファイル(source.cpp)に対しては

/// source.cpp
import ModuleA; // ライブラリAのモジュール, AのパーティションとモジュールCに依存する
import ModuleB; // グループBの所有しているモジュール

...

次のようなソース依存関係ファイル(source-dependencies)を出力するものとします

{ 
  "Version": "1.1", 
  "Data": { 
    "Source": "C:\\PartyB\\sources\\source.cpp", 
    “Includes” = [], 
    "ImportedModules": [ 
      { 
        "Name": "ModuleA", 
        "BMI": "C:/Path/To/LibraryA/x64/SpecialConfig/ModuleA.ixx.ifc" 
      }, 
      { 
        "Name": "ModuleA:PartitionA", 
        "BMI": " C:/Path/To/LibraryA/x64/SpecialConfig/ModuleA-PartitionA.ixx.ifc" 
      }, 
      { 
        "Name": "ModuleC", 
        "BMI": " C:/Path/To/LibraryC/x64/Release/ModuleC.ixx.ifc" 
      }, 
      { 
        "Name": "ModuleB", 
        "BMI": "C:/Path/To/Outputs/x64/MyConfig/ModuleB.ixx.ifc" 
      }, 
    ], 
    "ImportedHeaderUnits": [] 
  }
}

ModuleB.ixx.ifcファイルはグループBのビルドの一部として同じルールの下でビルドされており、そのsource-dependenciesファイルはifcファイルと同じ場所に同じ名前であるはずです。したがって、C:/Path/To/Outputs/x64/MyConfig/ModuleB.ixx.ifc.jsonが存在し、次のような内容となります

{ 
  "Version": "1.1", 
  "Data": { 
    "Source": "C:\\PartyB\\sources\\ModuleB.ixx", 
    "ProvidedModule": "ModuleB", 
    “Includes” = [], 
    "ImportedModules": [] 
    "ImportedHeaderUnits": [] 
  }
}

C:/Path/To/LibraryA/x64/SpecialConfig/ModuleA.ixx.ifcおよび他のifcファイルの近くにこれらと同様のjsonファイルがある事で、source.cppのビルドに使用したすべての依存関係(インクルードファイル、モジュール名およびそのソース)が判明したことになります。また、すべての依存関係の正確な場所も同時に把握することができます。

このようなsource-dependencies jsonファイルによってすべての依存関係の場所を把握できるため、メインビルドが成功した後で任意のモジュール(モジュールAを含めて)をリビルドできるようにする(3.1のシナリオ)ために、あとはコマンドラインオプションなどの場所を示すものではないオプション(#defineなど)だけが必要です。

dependencies jsonファイルによるライブラリAのパッケージ(シナリオ1)

ライブラリのパッケージ構造は任意ですが、少なくとも次のものを含んでいる必要があります

  • モジュールのソース
  • 依存関係jsonファイル(<bmi name>.d.json)。BMIの近くにあること

例えばライブラリA(モジュールAとそのパーティション、依存するモジュールC)の場合次のようなディレクトリ構造となり

LibraryA 
  Sources 
    ModuleA.ixx 
    ModuleA-PartitionA.ixx 
  x64 
    SpecialConfig 
      ModuleA.ifc 
      ModuleA.ifc.d.json 
      ModuleA-PartitionA.ifc 
      ModuleA-PartitionA.ifc.d.json 
      LibraryA.lib 
  x64 
    Release 
  … 

x64/SpecialConfig/ModuleA.ifc.d.jsonは次のようになります

{
  "Version": "1.1", 
  "Data": { 
    "Source": "../../sources/ModuleA.ixx", // このファイルの場所からの相対パス
    "ProvidedModule": "ModuleA", 
    “Options”=[], // 構成固有の、場所ではないオプション
    "ImportedModules": [ 
      { 
        "Name": "ModuleA:PartitionA", 
        "BMI": ". /ModuleA-PartitionA.ixx.ifc" // このパッケージ依存関係
      } 
      { 
        "Name": "ModuleC", 
        "BMI": "" // このパッケージの外の依存関係
      }
    ]
  }
}

ライブラリのビルドで(Bのビルド時と同様の)source-dependencies jsonファイルが出力される場合、このファイルはそこから簡単に作成できます。

モジュールAをグループBのビルドで使用し(シナリオ2)、そのビルド情報を使用せずにAをリビルドする

モジュールAをグループBのビルドで使用するにはAのBMIの場所をBのビルド構成に外から与える必要があります。これはBMIの名前が常に定まっていないためで、パッケージマネージャが行うかユーザーが手動で行う(コンパイラの検索パスのどこかにBMIを配置する)ことで可能です。BMIとモジュールを対応付けるためには、BMI名とモジュール名の対応が事前に分かっている必要があります。これはユーザーが手動で対応を指示する、モジュール名をBMI名にエンコードするなどの方法で可能となります。

モジュール名をBMI名にエンコードする場合、BMI名がコンパイラの期待するエンコードと一致する必要があります。例えば:windowsではファイル名に使用できないため別の文字に置き換える必要があります。このことは、BMIコンパイルしたコンパイラと互換性のある環境を使用していれば可能かもしれませんが、そうではない場合にはうまく働かない可能性があります。

仮にモジュール名のBMI名へのエンコードに関して標準化できたとしても、それを要求してしまうとモジュール名が変更されるたびにBMI名/場所のビルドオプションの変更が必要となります。これは、ユーザー/IDEの両方に負担をかけることになります(BMIファイルはコンパイラ固有のビルド成果物であるため、パースに向くものではないものと考える)。

しかし、上記のd.jsonファイルがBMIの近くにあれば、ビルドにモジュール検索パスだけが指定されている時でも、このパスですべてのd.jsonファイルを発見することができ、それらを読み込むことでモジュール名とBMIパスの対応を記録したビルド前モジュールマップを作成することができます。

この情報は、グループBのソースでインポートされているすべてのモジュールの依存関係を解決するのに十分であり、必要なライブラリがすべて同一PC上に存在している場合はすべてのライブラリモジュールを解決することができます。また、BMIやモジュールインターフェースの名前にモジュール名をエンコードする必要もなくなり、異なるコンパイラがモジュールをビルドするために必要なすべての情報を提供することができます。

提案の概要

インクリメンタルビルドのサポートには翻訳単位のコンパイルで使用されるすべてのファイルのリストが必要となります。現在、ほとんどのコンパイラはインクリメンタルビルドをサポートしているため、それらの#include情報やソースの場所を含む依存関係ファイルを作成することができるはずです。

モジュールのBMI#includeするファイルと同様の依存関係です。コンパイラは翻訳単位をコンパイルするために、それらの正確な場所を知っていなければならず、依存関係としてそれらをリストアップすることができるはずです。

従って、この提案では、コンパイラは次のものを含む依存関係の情報をJSON形式(解析や修正が簡単にできる)で出力できるようにすることを提案します

  • モジュール名(モジュールインターフェースをビルドする場合)
  • コンパイルオプション(コンパイラの内部的なものも含む可能性がある)
  • モジュールの依存関係

モジュール依存関係情報はP1689R4でソーススキャンについて提案されたもの(つまり、ビルド前に生成される)と似ている部分もありますが、ここではBMIとソースファイルの場所が追加されています。

また、ビルド済みモジュールを配布するすべてのライブラリは、以下の情報を含めるようにする事も提案します

  • モジュールインターフェースソース
  • BMIの近くにある依存関係jsonファイル(d.json

BMIはビルド時の設定(おそらくリリースビルドなどのこと)に依存しているため、d.jsonファイル内の依存関係やコンパイルオプションも同様の設定に依存します。ビルド時にどの設定を選択するかについては曖昧さはなく、d.jsonファイルにはモジュールインターフェースソースの場所とそのすべての依存関係の名前(と場所)が含まれており、モジュール名をファイルシステムエンコードする必要はありません。

それぞれのBMIの近くにd.jsonファイルがあれば、すべてのモジュールをリビルドするシナリオにおいて十分な情報を提供できます。

この提案では各JSONファイルの形式について、現在MSVCが/sourceDependenciesに出力しているものと同様のものを使用していますが、実際のフィールド名や追加の内容については標準化委員会における合意が必要であり、これから決めることです。

P2537R0 Relax va_start Requirements to Match C

可変長引数関数を0個の引数で宣言できるようにする提案。

C23で古いK&Rスタイルの関数宣言が削除されたことで、それを有効活用していたCから他言語の関数を呼び出すためのトリックが使用できなくなります。

// C23以前のK&R Declaration
// 実装は別言語によってどこかで実装されている
// 引数0の関数宣言、では無い
double compute_values();

int main () {

    // C: K&Rルールで許可された宣言、C23で削除
    // C++: ill-formed、関数は0個の引数をとる
    double x = compute_values("with_pi", 2.4, 2.7, true);

    return (int)x;
}

compute_values()の実装は他言語で行うことができ、実体との接続等を適切に整えた上でC側でこのように関数宣言だけを用意しておけば、それをCから呼び出すことができました。しかし、C23でK&Rスタイルの関数宣言が削除されたことで、このトリックは使用不可能となります。

C++では元々この形の宣言は引数0個の関数宣言であリ、f(...)と言う関数宣言によってK&Rスタイルの関数宣言をAPI/ABI両面で近似できていました。しかし、Cでは可変長引数関数がその可変長引数の利用のために可変長引数の前に最低1つの名前のある引数を必要とすることから、Cでは逆にf(...)のような関数宣言を使用できません。これは、va_startがその呼び出しに際してva_listと可変長引数の直前の引数名の2つのパラメータを取るためです。

void f(int arg1, ...) {
  // 可変長引数リスト
  va_list args;

  // 可変長引数の使用を開始
  va_start(args, arg1);

  ...
}

C23では、最初の例のような他言語関数呼び出しのユースケースを保護するためにf(...)の形式の関数宣言を許可しており、同時にva_startが1つの引数(va_list)のみで呼び出すことができるようにされました。C++ではf(...)の形式の関数宣言は元々可能でありC23のそれと同じ意味を持っていますが、va_startの定義はCに合わせる形で2つの引数を要求するようになっていました。この提案は、C++側でもC23でのその修正に従って、va_startが1引数で呼び出すことができるように修正するものです。

この提案によって、最初の例(をC23に合わせて修正したコード)はCとC++で一貫した意味を持つコードとなります。

// C23からok、C++は以前からok
// 定義は多言語など、このソースの外でなされている
double compute_values(...);

int main () {
    // C/C++両方でポータブル
    double x = compute_values("with_pi", 2.4, 2.7, true);

    return (int)x;
}

P2538R0 ADL-proof std::projected

C++20 Rangeアルゴリズムが不必要な型の完全性要求をしないようにする提案。

まず、説明のために不完全型(定義の無い型)をテンプレートパラメータに受け取るとコンパイルエラーを起こすクラスを用意します。

// 完全性を要求される文脈で、Tに不完全型を指定されるとエラーを起こすクラス
template<class T>
struct Holder { T t; };

// 不完全型
struct Incomplete;


Holder<Incomplete> h; // error

Holder型の完全性(定義の存在)が要求されるとテンプレートパラメータTがメンバとして実体化し同様に完全性が要求されるため、Tが不完全型だとコンパイルエラーを起こします。

不完全型はその使用が制限されており、完全型(定義を備えた型)が要求される文脈で使用されるとコンパイルエラーを起こします。そのような操作の1つにはADLがあります。

Holder<Incomplete> *p;      // ok、ポインタとしての利用は完全性を要求しない
int f(Holder<Incomplete>*); // ok

int x = f(p);   // error、ADLでは完全型が必要
int x = ::f(p); // ok、ADLは行われない

不完全型はポインタとして宣言することはでき、ネイティブのポインタ型T*ではTの完全性を要求しません。また、ネイティブポインタに対する多くの操作はADLをトリガーしません。

Holder<Incomplete> *a[10] = {}; // 10個のnullptr
Holder<Incomplete> **p = a;                 // ok
p += 1;                                     // ok
assert(*p == nullptr);                      // ok
assert(p == a+1);                           // ok
assert(std::count(a, a+10, nullptr) == 10); // ok

libc++(clangの標準ライブラリ実装)のテストスイートには、STLアルゴリズムが不必要に型の完全性を要求しないことをチェックするテストが含まれており、それらのテストの事をADL-proofingテストと呼んでいます。libc++実装者たちは、これは標準への適合性の問題ではなくかなり重要な実装品質の問題だと考えているようです。なぜなら、早期の不必要な型の完全性要求はユーザーにとって困難なハードエラーの原因となるほか、ODRの問題を引き起こす可能性があるためです。

C++20 Rangeアルゴリズムではまさにこのことが問題となり、ADLから保護されていません。

Holder<Incomplete> *a[10] = {}; // 10個のnullptr
assert(std::count(a, a+10, nullptr) == 10);         // ok
assert(std::ranges::count(a, a+10, nullptr) == 10); // error

より詳しく見てみると

using T = Holder<Incomplete>*;

static_assert(std::equality_comparable<T>); // ok

bool x = std::indirectly_comparable<T*, T*, std::equal_to<>>; // error
bool y = std::sortable<T*>; // error

std::indirectly_comparable<T*, T*, Pred>std::indirect_binary_predicate<Pred, projected<T*, identity>, projected<T*, identity>>で定義されており、それはstd::projected<T*, identity>型のイテレータitに対する*itが有効な式であるかどうかをチェックしています。ADLは*itという式で発動しており、それによってprojected<T*, identity>の関連するすべての型(Holder<Incomplete>も含む)に対して完全性が要求され、Holder<Incomplete>の完全性が要求されてエラーが発生しています。

ADLでは、関数に渡された引数の型から関連する型を抽出し、その型の関連する名前空間(その型を囲う最も内側の名前空間)を取得します。引数型がクラステンプレートの特殊化である場合、クラステンプレートのテンプレート引数(非型テンプレートパラメータおよびテンプレートテンプレートパラメータを除く)を関連する型として抽出してしまうため、テンプレートパラメータの型に対して完全性の要求が発生します。

これによって、std::sortstd:countなどはHolder<Incomplete>のようなADLで危険な型に対して安全に使用できるのに対して、rangesの対応するものはそうではない、という問題が生じています。

この問題を解決し、すべてのRangeアルゴリズムをADLから保護するためには、Tprojected<T*, identity>の関連する型(associated type)にならないようにすればよく、これはADLに対するファイアウォールを導入することで実現可能です。

// 現在のstd::projected定義
template<class Associated>
struct projected {
  ...
};

// この提案によるstd::projected定義
template<class T>
struct __projected_impl {
  struct type {
    ...
  };
};

template<class NonAssociated>
using projected = __projected_impl<NonAssociated>::type;

ADLは派生クラスに対する基底クラスを関連する型として関連付けますが、ネストしたクラスにおける包含されたクラスは関連付けません。そのため、__projected_impl<NonAssociated>::typeprojected<NonAssociated>の関連する型ではなくなります。__projected_impl<NonAssociated>::type::は不要なADLに対するファイアウォールとして機能していると見ることができます。

このような考慮はP2300R4では既に文言として存在しており、同様の指定をstd::projectedに行えれば理想です。しかし、std::projectedC++20機能として出荷済みであるため、ここでは後方互換性を取った方法(__projected_implusingによるエイリアス)を提案しています。

P2540R0 Empty Product for certain Views

viewの空積は空のタプルになるべきという提案。

この提案の対象は明示されていませんが、おそらくviews::zipファミリやviews::cartesian_productを対象にしています。これらのviewは入力の2つ(以上)のrangeを1つのrangeに変換するものであり、入力のrangeが1つもない場合は共にstd::empty_view<std::tuple<>>となります。この提案は、この場合の結果はstd::tuple<>になるべきというものです。ただし、このことはzipに対して拡張しない(有用ではないため)としています。

以下、提案の説明の訳。

2つのものに対する積を3以上のものの積に拡張するのは自然なことで、$P=A×B$から$P = \prod_{i=0}^n a_i = a_1 ... a_n$へと拡張されます。多くの積では、空積の値は単位元に設定され、空積は積の恒等式となるようにしています。これによって、他の演算との整合性がとれるなど色々と都合がよくなります。

空積を恒等式要素に設定することで、fold 0はより健全な足場を得ます。基本ケースが自動的に単位元を提供するため、単位元を指定する必要はありません。

デカルト積は集合管の全ての関係の全体とみなすことができます。実際、集合間の関係はしばしばデカルト積の部分集合として定義され、集合間の関係はその関係に含まれる集合の要素同士を関連付けます。集合の無いデカルト積(空積)は$\empty \to \empty$から、ちょうど1つの関数、空関数が存在しています。このことは、集合の無いデカルト積の結果の基数(濃度)が0ではなく1であるべきことを示しています。

特に、zipはインデックス付き集合の内結合であり、デカルト積の対角線上の集合であるという性質を持っています。しかし、zip恒等式要素は空のタプルの集合であり、空のタプルの無限回の繰り返しによる範囲となります。もし空の範囲のzipをその単位元にすることを許可したら、概念的に同じものを2つの異なる形式で表現し異なる結果を返すという矛盾をシステムに持ち込むことになり、これは良くないことです。

これらのことからこの提案では、空の範囲のデカルト積は1つの要素の範囲であり、それは空のタプル(std::tuple<>)であることを提案していますが、それをzipにまで拡張しないことを提案しています。空の範囲のzipデカルト積の対角線になるはずですが、それはzipを消滅させるもので、実際には有用ではありません。空の範囲に対するほかの操作がそうであるように、これは未定義にしておくべき、と述べています。

P2542R0 views::concat

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

これは引数として受け取った任意個数の範囲を繋げて1つの範囲として扱うものです。

std::vector<int> v1{1,2,3}, v2{4,5}, v3{};
std::array  a{6,7,8};
auto s = std::views::single(9);

std::cout << fmt::format("{}\n", std::views::concat(v1, v2, v3, a, s)); 
// output:  [1, 2, 3, 4, 5, 6, 7, 8, 9]

これはRangeアダプタではなくRangeファクトリなので、パイプ記法(|)の右辺で用いることはできません。

views::concatによって連結可能な範囲は、その全ての参照型(range_reference_tおよびrange_rvalue_reference_t)が共通の参照型(common_reference)を持ち、全ての値型(range_value_t)が共通の型(common_type)を持っている必要があり、さらに入力の範囲それぞれの参照型・値型間にcommon_referencerange_value_tの同様の要求と、変換可能性の要求があります。複雑ですが、とりあえず全部同じ型であれば意図通りに動作します。

なお、0引数views::concat()コンパイルエラーとなり、1引数views::concat(r)views::all(r)と等価となります。

入力のrangeの列をRs...とすると、concat_viewの諸特性は次のようになります

  • reference : common_reference_t<range_reference_t<Rs>...>
  • value_type : common_type_t<range_value_t<Rs>...>
  • rangeカテゴリ
    • Rsが全てrandom_access_range && sized_range : random_access_range
    • Rsの最後の範囲がbidirectional_rangeでありそれ以外の範囲がconstant-time-reversibleとなる場合 : bidirectional_range
      • rangeRconstant-time-reversibleとなるのは次のいずれかの場合
        • bidirectional_range<R> && common_range<R>
        • sized_range<R> && random_access_range<R>
    • Rsが全てforward_range : forward_range
    • それ以外 : input_range
  • common_range : 次のいずれかの場合
    • Rsが全てcommon_range
    • Rsが全てrandom_access_range && sized_range
  • sized_range : Rsが全てsized_range
  • const-iterable : Rsが全てconst-iterableであり、const Rsが全て連結可能(views::concat可能)である場合
  • borrowed_range : ×

bidirectional_rangeとなるときは少し複雑ですが、少なくとも全ての範囲はbidirectional_rangeでなければなりません。その上で、Rsの最後の範囲(n番目とすると)の最初の要素から--するときのことを考えると、n-1番目の範囲のend() - 1の位置に移動しなければなりません。それが可能となるのは、n-1番目の範囲Rが、単にbidirectional_rangeであるときはcommon_rangeでなければならず(そうでないと--end()ができない)、Rrandom_access_rangeならばsized_rangeでもある必要があります(common_rangeを要求せずにend() - 1の位置に行くにはサイズが必要となり、サイズを定数時間で求めるためにはsizedである必要がある)。そして、これは最後(n番目)の範囲を除いたRsの残りの全てについて同じことが言えます。そのため、views::concatの入力Rsの最後の範囲はbidirectional_rangeであることしか求められておらず、それ以外の全ての範囲はその条件をエンコードしたconstant-time-reversibleという制約を満たす必要があるわけです。

concat_viewは、入力の全ての範囲がborrowed_rangeであればborrowed_range(入力範囲のイテレータ有効性はその範囲オブジェクトのライフタイムから切り離されているrange)とすることができます。しかしその場合、concat_viewオブジェクトおよびconcat_view::iteratorは全ての入力範囲のイテレータを自身の内部に保存しておかねばならなくなります(concat_viewが構築されたその瞬間しか、入力の範囲の生存を仮定できないため)。これはイテレータサイズの肥大化を招き、実装が複雑になります。concat_viewが常にborrowed_rangeでないとすると実装はより単純になり、イテレータは常にアクティブな1つだけを保持し、境界を接続するためだけにconcat_viewオブジェクトを参照すれば良くなります。Range-v3の経験ではconcat_viewborrowed_range性は重要とされないため、Range-v3およびこの提案ではborrowed_range性をドロップすることで安価な実装を選択しています。

P2544R0 C++ exceptions are becoming more and more problematic

C++の例外について、マルチスレッド環境のパフォーマンスおよび代替手段との比較についての報告。

ここでは、次のようなコードの入力データに確率的に例外を投げる値を挿入することで、例外発生確率ごとの全タスク実行時間を計測しています。

struct invalid_value {};

void do_sqrt(std::span<double> values) {
  for (auto& v : values) {
      if (v < 0) throw invalid_value{};
      v = std::sqrt(v);
  }
}

// このようにして10万回実行
unsigned exceptionsSqrt(std::span<double> values, unsigned repeat) {
  unsigned failures = 0;
  for (unsigned index = 0; index != repeat; ++index) {
      try {
         do_sqrt(values);
      } catch (const InvalidValue& v) { ++failures; }
  }
  return failures;
}

// マルチスレッド時はexceptionsSqrt()を複数スレッドで走らせる

この入力(values)は基本的には全て1.0の100個の配列ですが、設定した確率に応じてランダムに-1が挿入されます。パフォーマンス計測のため、この関数を10万回実行します。

例外発生確率\スレッド数 1 2 4 8 12
0.0% 19ms 19ms 19ms 19ms 19ms
0.1% 19ms 19ms 19ms 19ms 20ms
1.0% 19ms 19ms 20ms 20ms 23ms
10% 23ms 34ms 59ms 168ms 247ms

傾向として、例外発生確率が高くなると実行時間が長くなっています。そして、スレッド数が増えるほど発生確率の上昇に対するパフォーマンスの低下が顕著に見られるようになります。

この数値はGCC11.2を使用したLinux環境で計測されたものですが、Clang13やWindowsのMSVCにおいても同じ傾向だったとのことです。また、上記計測はRyzen 9 5900Xを使用したもので、さらにスレッド数の多いEPYC 7713(128C/256T)で実行したのが次の結果です

例外発生確率\スレッド数 1 2 4 8 16 32 64 128
0.0% 24 26 26 30 29 29 29 31
0.1% 29 29 29 29 30 30 31 105
1.0% 29 30 31 34 58 123 280 1030
10% 36 49 129 306 731 1320 2703 6425

(単位は全てms)

この場合、1スレッドしか使用していないときでも0.1%の例外発生率からパフォーマンスの劣化が目立ち始めています。このことは、実際に使用しているスレッド数ではなく使用可能なスレッド数が増加することで例外処理のパフォーマンスが劣化することを示しています。

一方で、C++の例外は、例外が発生しない限りはオーバーヘッドが(ほぼ)ゼロであるという利点があります。このことは、呼び出しごとに余分な作業をほとんど行わない関数を大量に呼び出すようなコードで確認することができます。

struct invalid_value {};

unsigned do_fib(unsigned n, unsigned max_depth) {
   if (!max_depth) throw invalid_value();
   if (n <= 2) return 1;
   return do_fib(n - 2, max_depth - 1) + do_fib(n - 1, max_depth - 1);
}

このコードをn = 15、確率的にmax_depthが13に設定される条件で1万回実行し計測した結果は次のようになります

例外発生確率\スレッド数 1 2 4 8 12
0.0% 12 12 12 14 14
0.1% 14 14 14 14 15
1.0% 14 14 14 15 15
10% 18 20 27 64 101

(単位は全てms)

スレッド数が増えるほど/例外発生確率が増えるほど、パフォーマンスが低下するという傾向に変化はありません。

これから比較するC++例外の代替案では、sqrtよりもこのfibの方がコストがかかるため、例外の利点も合わせた比較のためにこちらの値を使用します。

この提案では、上記のような傾向を生じさせるC++例外の問題点として次の2つをあげています

  1. 例外オブジェクトは、継承構造のため、あるいはstd::current_exception()のような非ローカル性のため、動的メモリに確保される。これはthrowgotoに最適化することを妨げている他、メモリ不足の状況でthrowが例外を投げるという問題がある。
  2. 現代のC++コンパイラが使用するテーブル駆動型のアンワインド(スタック巻き戻し)ロジックが、テーブルを同時変更から保護するためにグローバルミューテックスを取得するため、例外のアンワインド処理は実質的にシングルスレッド実行になる。これは、コア数の多い環境では特に問題となり、例外をほとんど使用できなくなる。

throw;(再スロー)やstd::current_exception()のような構文はプログラムのどの場所にも存在する可能性があり、特に、キャッチブロック内で呼び出される関数内(特に、インライン化されていない関数内)で呼び出される可能性があるため、例外オブジェクトの構築を単純に省略したりすることができません。そして、この(1つ目の)問題は言語仕様を変更しないと回避できません。
2つ目の問題は実装によって回避可能かもしれませんが(提案中ではGCC実装を変更することでこの問題が解決可能であることを確認しているが)、それは確実に大きなABIの変更をもたらすため、共有ライブラリを含めた関連するコンポーネント全てにおいて慎重な調整が必要となります。

C++に対しては、いくつかの例外を代替するものが提案されています。ここでは、次のものについて先ほどのsqrt, fibの計測を行なっています。

  • std::expected
  • boost::LEAF
  • throwing values(P0709R4

それぞれ結果は次のようになります(これらの手法においてはマルチスレッド環境でのパフォーマンス劣化要因がないため、マルチスレッドの結果はシングルスレッドと同じとして省略します)

  • std::expected
計種別\例外発生確率 0.0% 0.1% 1.0% 10%
sqrt 18 18 18 16
fib 63 63 63 63
  • boost::LEAF
計種別\例外発生確率 0.0% 0.1% 1.0% 10%
sqrt 18 18 18 16
fib 23 23 23 22

このテストにおいては、-fno-exceptionsを使用した結果となっています。これがない(例外を有効にした)場合、例外が全く投げられない場合のfibケースは最低29msかかります。これは例外が真にゼロオーバーヘッドではないことを示しています(try-catchブロックによるものではなく、標準ライブラリなどのあらゆる場所で例外が投げられることを考慮したコードとなることによるオーバーヘッド)。

これは提案中の機能で、任意の例外を許可せず、代わりに2つのレジスタだけを使用して効率的に受け渡すことのできる特定の例外クラス型を使用するものです。純粋なC++で実装できないため、ここではC++でエミュレートした場合(例外オブジェクトは最大でもポインタサイズ)と、インラインアセンブラによってハードコートされた実装の2つをテストしています。

C++エミュレーション

計種別\例外発生確率 0.0% 0.1% 1.0% 10%
sqrt 18 18 18 16
fib 19 18 18 18

インラインアセンブラ実装

計種別\例外発生確率 0.0% 0.1% 1.0% 10%
sqrt 18 18 18 16
fib 13 13 13 13

これらの代替案はsqrtケースでは例外処理を上回る一方で、fibケースではパフォーマンスにオーバーヘッドが見られます。これは、これらの代替案によって単純に例外機構を置き換えることができないことを示しています。

ただし、throwing values(P0709R4)はfibケースにおけるオーバーヘッドはかなり小さく、C++エミュの場合は25%、アセンブラ実装の場合は10%程度で済んでいます。失敗率が高くなると例外処理を上回り、マルチスレッド時のパフォーマンス劣化もありません。

throwing values(P0709R4)は、現在の例外機構の代替として非常に理想的なものですが、そのメカニズムを既存のコード、特に標準ライブラリに統合する方法が問題となります。新しいメカニズムを使用するためにソースの再コンパイルが必要という点は問題ではなく、ソースレベルの互換性をどう確保するかという点が問題となります。コンパイラフラグによる切り替え(例えば、stdからstd2への切り替えなど)はODR関連の問題を引き起こす可能性があり危険です。

したがって、例外機構代替の最善の戦略というものは現時点ではまだ明らかではありません。しかし、AMD/intelのCPUロードマップを見てもCPUコア数は今後確実に増加していくため、何か手を打つ必要があります。そうしなければ、例外を有効にすることによるパフォーマンス劣化を回避するために-fno-exceptionsとともに自前のソリューションを使用することが避けられなくなります。

P2545R0 Why RCU Should be in C++26

標準ライブラリにRead-Copy-Update(RCU)サポートを追加する提案。

これは現在Concurrency TS v2(未発行)に含まれているRCU関連のものをそのままC++26の標準ライブラリに含めようとする提案です。以前はP1122で議論されていたため、詳細はそちらの解説記事を参照してください。

基本的な主張は「P2530R0 Why Hazard Pointers should be in C++26」と同様で、FacebookのFollyというライブラリでこの提案のスーパーセットが実装され、実製品での使用経験があり、インターフェースは安定してるため、C++26に追加しようというものです。

参照カウンタ・ハザードポインタ・RCUの性質の比較。

性質 Reference Counting Hazard Pointers RCU
リーダー(読み取り処理) 遅くスケーラブルでない 高速でスケーラブル 高速でスケーラブル
未回収オブジェクトの利用 制限あり 制限あり 制限なし
Traversal Retries? オブジェクトが削除された場合 オブジェクトが削除された場合 しない
回収にかかるレイテンシ 高速 遅い 遅い

提案では、ハザードポインタは参照カウンタのスケーラブルな代替品であり、RCUはリーダー・ライターロックのスケラーブルな代替品であり、競合するものではないと述べられています。

P2546R0 Debugging Support

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

この提案は、以前のP2514P2515をマージした後継となるものです。

それぞれについては以前の記事を参照。

この提案での変更は

  • std::breakpoint()の文言を変更して、デバッガの有無などの環境条件に左右されないようにした
  • デバッガが接続されている時だけブレークポイントを有効化するstd::breakpoint_if_debugging()の追加
  • 機能テストマクロの追加
  • 実装経験の拡充
  • std::is_debugger_present()が即時クエリであることを明記

などです。

P2547R0 Language support for customisable functions

カスタマイゼーションポイントの言語サポートの提案。

C++20のCPOと以前のADLベースカスタマイゼーションポイントの問題点についてはこちら

その上で、CPOには次のような問題がまだあります

  • 定義のためのボイラープレートが多い
  • 名前衝突の可能性がある
  • 値で引数を渡した時、コピー省略されない
  • CPOをラップして転送できるようなジェネリックラッパを書けない

コピー省略されないという問題は、CPOが引数を転送した上で内部で関数呼び出し行う2段階の関数呼び出しをしているために起きています。

// foo CPOの実体
namespace _foo {
  // Poison-pill オーバーロード
  void foo();

  struct _fn {
    template<typename T, typename V>
    requires requires (T& obj, V&& value) {
      foo(obj, (V&&)value);
    }
    void operator()(T& obj, V&& value) const {
      foo(obj, (V&&)value);
    }
  };
}

// foo CPOの定義
inline namespace _foo_cpo {
  inline constexpr _foo::_fn foo{};
}


struct my_type {
  // foo CPOへのアダプト、stringを値で受け取る
  friend void foo(my_type& self, std::string value);
};

void example() {
  my_type t;
  foo(t, std::string{"hello"}); // std::stringのムーブコンストラクタが1回呼ばれる
}

my_type::foo()Hidden friend関数)を直接呼び出した場合、2つ目の引数のstd::stringはコピー省略保証によって関数定義内で直接構築されるため、ムーブコンストラクタの呼び出しすら省略できます。しかし、CPOは引数を転送しているため(prvalueであることが忘れられることで)、それができません。

CPOの一部の問題を解決したアプローチとしてtag_invokeが提案されており、現在のP2300(Executor提案)はtag_invokeによるカスタマイゼーションポイントを多用しています。しかし、これにもまだ次のような問題があります

  • CPOから
    • 定義のためのボイラープレートが(まだ)多い
    • 値で引数を渡した時、コピー省略されない
  • tag_invoke固有
    • カスタマイズのための構文がわかりづらい
      • アダプトするにはtag_invokeという名前の関数を定義しなければならない
      • どのカスタマイズポイントにアダプトしてるのかわかりづらくなる
    • オーバーロードセットが膨れ上がる可能性がある
      • 複数のカスタマイズポイントにアダプトしていても関数名は全部tag_invokeとなるため、オーバーロード候補が多くなりがち

P2279R0は、(上記のように)tag_invokeですらカスタマイゼーションポイントとして十分ではなく、理想的なカスタマイゼーションポイントのためには言語メカニズムが必要性であるとするものです。

この提案はそれに応えるもので、P2300で多用されているtag_invokeを置き換えるべく、P2300とともにC++26への導入を目指しています。

この提案では、既存のカスタマイゼーションポイントメカニズムに対して、次のような改善を行います

  • 名前を広く予約することなく、カスタマイゼーションポイントを名前空間スコープに配置できる
  • この提案の機能によって定義されるカスタマイゼーションポイントオブジェクト(CPO)を、汎用型消去ラッパや一部の操作をカスタマイズして他の操作を通過させるようなアダプタなどのラッパを通して、汎用的に転送できる
  • CPOの定義と、それにアダプトする型にカスタマイズを追加する為の簡潔な構文
  • カスタマイゼーションポイントに値として渡される引数のコピー省略サポート
  • tag_invokeと比較して、コンパイル時間が短縮される
    • 3層のテンプレートインスタンス化を回避し、実装関数を分離する為のSFINAEが不要になる
  • tag_invokeと比較して、エラーメッセージの改善
    • tag_invokeはカスタマイゼーションポイントを区別しない(名前が全部tag_invokeになる)為、大量のオーバロードが発生しうる

これらのことを、次のような構文の導入によって達成します

  • 名前空間スコープ関数宣言に対するvirtual指定による、カスタマイゼーションポイント関数の宣言
    • これを、Customisable Function Prototype(CFP)、あるいはCustomisable Functionと呼ぶ
    • = 0;で宣言することで、デフォルト実装を省略する
    • default指定子付きでデフォルト実装を定義できる
  • CFPの宣言は同名のCPOを導入する
  • 特定のCFPにアダプトするためには、同名の関数をoverride指定子付きで関数を定義する
  • 特定のCFPに対するアダプトを明示するために、CFPのフルスコープ名による関数定義構文
  • ジェネリックな転送サポートのために、CFP(CPO)そのものを推論する機能
    • template <auto cpo, typename T> auto cpo(T&& object) override;のような構文
namespace std::execution {
  // execution::operation_state CFPの宣言
  template<sender S, receiver R>
  virtual operation_state auto connect(S s, R r) = 0;
}

CFPを宣言するには、名前空間スコープでvirtualで関数を宣言します。デフォルト実装が必要ない場合は最後に= 0;を付加します。これらの構文は、クラスの仮想関数と同様です。デフォルト実装が必要な場合は、default指定とともに定義します。

namespace std::ranges {
  // ranges::contains CFPの宣言およびデフォルト実装
  template<input_range R, typename Value>
    requires equality_comparable_with<range_reference_t<R>, Value>
  virtual bool contains(R&& range, const Value& v) default {
    for (const auto& x : range) {
      if (x == v) return true;
    }
    return false;
  }
}

このcontains()をカスタマイズするためには、override指定とともに同じ名前(スコープ名)で関数を定義します

namespace std {
  template<class Key, class Compare, class Allocator>
  class set {
    // ...
  private:

    // ranges::containsへのアダプト(カスタマイズ)、Hidden friend
    template<typename V>
      requires requires(const set& s, const V& v) { s.contains(v); }
    friend bool ranges::contains(const set& s, const V& v) override {
      return s.contains(v);
    }
  };
}

このカスタマイズは名前空間スコープ(クラス定義外)でも同様に行えます

namespace std {
  template<class Key, class Hash, class KeyEq, class Allocator>
  class unordered_set { ... };

  // クラス定義外でのカスタマイズ
  template<class Key, class Hash, class Eq, class Allocator, class Value>
    requires(const unordered_set<Key,Hash,Eq, Allocator>& s, const Value& v) {
      s.contains(v);
    }
  bool ranges::contains(const unordered_set<K,H,Eq,A>& s, const Value& v) override {
    return s.contains(v);
  }
}

このように宣言・定義されたCFPはフルスコープ名で普通に呼び出すことができ、引数に応じたカスタマイズを自動で(かつCPOのような2段階ディスパッチを行うことなく)解決してくれます

void example() {
  std::set<int> s = { 1, 2, 3, 4 };

  for (int x : { 2, 5 }) {
    if (std::ranges::contains(s, x)) // std::setでカスタマイズされたものを呼び出す。
    std::cout << x << " Found!\n";
  }
}

CFPはCPOと同様に、ADL以前の名前探索で見つかっているとADLを無効化します

void lookup_example() {
  std::set<int> s = { 1, 2, 3 };
  
  contains(s, 2); // ADLによる探索が行われる
                  // std::ranges::contains()は見つからない

  using std::ranges::contains;

  contains(s, 2); // containsの非修飾名探索でstd::ranges::contains()が見つかる
                  // これはADLの代わりにCFPのオーバーロード解決ルールに従う
}

CFPは同名のオブジェクトを導入しており、これは空のオブジェクトで値で渡すことができます(C++20 CPOと同様の性質を持つ)。このオブジェクトを関数呼び出しすると、対応するCFPのすべてのオーバーロードの集合から解決されます(すなわち、CFPを呼び出した時と同様の扱い)。

// frobnicate CFPの宣言
template<typename T>
virtual void frobnicate(T& x) = 0;

struct X {
  // frobnicateのカスタマイズ
  friend void frobnicate(X& x) override { ... }
};

void example() {
  std::vector<X> xs = ...;

  std::ranges::for_each(xs, frobnicate); // "frobnicate"は対応するCFPの宣言に順じたCallabkeオブジェクトとして使用できる
}

クラス内では、名前空間スコープの汎用カスタマイズ定義によって、CFPの集合全体に対してカスタマイズを定義することができます

// 特定のメンバ変数型を取得する
template<typename Obj, typename Member>
using member_t = decltype((std::declval<Obj>().*std::declval<Member Obj::*>()));

// InnerでうけたCFPの呼び出し前にログ出力する
template<typename Inner>
struct logging_wrapper {
  Inner inner;

  // 呼び出されているCPO名を出力した後、Argsを使用してinnerで呼び出し可能なら
  // 最初の引数をlogging_wrapperとみなし、self.innerにargsを転送して呼び出す
  template<auto cpo, typename Self, typename... Args>
    requires std::derived_from<std::remove_cvref_t<Self>, logging_wrapper> &&
             std::invocable<decltype(cpo), member_t<Self, Inner>, Args...>
  friend decltype(auto) cpo(Self&& self, Args&&... args) noexcept(/* ... */) override {
    std::print("Calling {}\n", typeid(cpo).name());
    return cpo(std::forward<Self>(self).inner, std::forward<Args>(args)...);
  }
};

void test(int x) {
  logging_wrapper log{ .inner = std::ranges::contains };
  std::set<int> s = { 1, 2, 3, 4 };

  std::ranges::contains(log, s, x);
  // logging_wrapperでカスタムされたcpo()を呼び出す
  // ログ出力後、std::ranges::contains(s, x);が呼び出される
}

これ(logging_wrappercpo())は、非型テンプレートパラメータcpoを呼び出されたCFPに推論する事で可能となり、overrideキーワードがその目印となっています。

テンプレートなCFPは、明示的なテンプレートパラメータを使用して宣言することもできます(これを、template customisable functionと呼ぶ)。この場合、このテンプレートパラメータは明示的に与えられなければならず、CFPの特殊化それぞれに対して個別にCPOが作成されます。これは、std::getのようなカスタマイゼーションポイントを定義するのに使用できます

// CFPによるstd::getの実装例
namespace std {
  template<typename T, typename Obj>
  virtual auto get<T>(Obj&& obj) = 0;
  
  template<size_t N, typename Obj>
  virtual auto get<N>(Obj&& obj) = 0;

  template<size_t N, typename Obj>
  virtual auto get(Obj&& obj, std::integral_constant<size_t, N>) default -> decltype(auto) {
    return std::get<N>(std::forward<Obj>(obj);
  }
}

struct my_tuple {
  int x;
  float y;

  // 型名指定に対するカスタマイズ
  friend int& std::get<int>(my_tuple& self) noexcept override { return self.x; }
  friend float& std::get<float>(my_tuple& self) noexcept override { return self.y; }

  // インデックス指定に対するカスタマイズ
  friend int& std::get<0>(my_tuple& self) noexcept override { return self.x; }
  friend float& std::get<1>(my_tuple& self) noexcept override { return self.y; }
};

変数や変数テンプレートと異なり、CFPの提供するCPOは比較可能ではないため、cpo-name<T>は明確にテンプレート名であり、<が比較演算子として認識されてしまうことはありません。これによって、CPOとCPO-templateは同じスコープに共存することができます。

P2279R0の比較軸に基づくと次のようになります

性質 この提案
Interface visible in code ✔️
Providing default implementations ✔️
Explicit opt-in ✔️
Diagnose incorrect opt-in ✔️
Easily invoke the customization ✔️
Verify implementation ✔️
Atomic grouping of functionality
Non-intrusive ✔️
Associated Types 🤷
Customization Forwarding ✔️
  • Interface visible in code
    • カスタマイズ可能な(あるいはその必要がある)インターフェース(関数など)がコードで明確に識別できる
  • Providing default implementations
    • デフォルト実装を提供し、なおかつオーバーライド可能
  • Explicit opt-in
    • インターフェースを明示的にオプトインできる(インターフェースへのアダプトが明示的)
  • Diagnose incorrect opt-in
    • インターフェースに意図せずアダプトしない
  • Easily invoke the customization
    • カスタマイズされたものを簡単に呼び出せる
    • デフォルト実装がある場合、必ずカスタマイズされたものを呼び出す
  • Verify implementation
    • ある型がインターフェースを実装していることを簡単に確認できる(機能がある)
  • Atomic grouping of functionality
    • インターフェースにアダプトするために必要な最小の機能グループを提示でき、早期にそれを診断できる
  • Non-intrusive
    • 非侵入的(その型を所有していない人が後からカスタマイズできる)
  • Associated Types
    • 関連する型をまとめて扱える(個別の型ごとにインターフェースにアダプトする必要が無い)
    • 例えば、イテレータ型に対するカスタマイゼーションポイントを提供する時、イテレータの要素の型ごとにカスタマイズ処理を書く必要が無い。

「Atomic grouping of functionality」および「Associated Types」に関しては意図的に含まれていません。それは、コンセプトを用いることによって解決することができ、組み合わせればRustのTraitsと同等の機能となります(提案中にはRustとの比較コードがあります)。また、この提案が将来的にそのような拡張をする余地をなくすものではないため、将来そのような方向性を探ることもできます。

P2549R0 std::unexpected should have error() as member accessor

std::unexpectedのエラー値取得関数をerror()という名前にする提案。

std::unexpected<E>std::expected<T, E>Eについてのラッパ型です。std::unexpected<E>std::expected<T, E>に暗黙変換することができ、主にstd::expectedを返すときにエラー値を返していることを明示するために使用します。

std::expected<int, std::errc> svtoi(std::string_view sv) {
  int value{0};
  auto [ptr, ec] = std::from_chars(sv.begin(), sv.end(), value);

  if (ec == std::errc{}) {
    // 正常終了
    return value;
  }

  // エラー
  return std::unexpected(ec);
}

std::unexpected<E>を直接使用することはあまりなさそうですが、一応保持するEの値を取り出せるようになっており、それは.value()メンバ関数で行います。しかし、それをstd::expected<T, E>に変換するとその値は.error()で取り出すことができ、.value()は正常値Tを取り出す関数となります。このように、std::unexpected<E>の値アクセスはstd::expected<T, E>のものと矛盾しているため、std::unexpected<E>::value()error()にリネームするべき、という提案です。

P2550R0 ranges::copy should say output_iterator somewhere

ranges::copyをはじめとする出力系のアルゴリズムが、出力イテレータ型をoutput_iteratorという言葉を含むようなコンセプトで制約するようにする提案。

例えば、ranges::copyイテレータペアを受け取るオーバーロードは次のように宣言されています。

template <input_iterator I, sentinel_for<I> S, weakly_incrementable O>
  requires indirectly_copyable<I, O>
constexpr ranges::copy_result<I, O> ranges::copy(I first, S last, O result);

このうち出力操作を制約しているのはindirectly_copyableコンセプトで、それがイテレータっぽく使えることをweakly_incrementableが要求しています。とはいえ、この2つのコンセプトはoutput_iterator等よく知られた性質に比べると知名度がなく、診断(コンパイルエラーなど)に表示されてもあまり役立ちそうにありません。この提案は、これをoutput_iteratorという言葉を含むようなコンセプトに置き換えようとするものです。

単純には、output_iteratorを使用して次のように書き換えてしまえばこの問題は解決します。

template <input_iterator I, sentinel_for<I> S, output_iterator<iter_reference_t<I>> O>
constexpr ranges::copy_result<I, O> ranges::copy(I first, S last, O result);

この二つの制約はoutput_iteratorに含まれている*i++ = std::forward<T>(t);(引数名を使って書くと、*result++ = *first;)という制約を除いて、同じことを制約しています。今日のranges::copy(およびその他出力系アルゴリズム)はこの構文を使用していないためこの要件を必要とせず、そのためoutput_iteratorを使用していません。

筆者の方の調査によれば、rangesの出力操作を行うアルゴリズムのうちoutput_iteratorを使用するのはranges::replace_copy, ranges::replace_copy_if, ranges::fill, ranges::fill_nの4種類だけだったようです。また、ranges::fill, ranges::generateの2つのrangeRを受け取るオーバーロードではoutput_range<R>コンセプトが使用されていましたが、ranges::generateではイテレータペアに対して同様の制約がなされていなかったようです。

indirectly_copyable<I, O>コンセプトはIindirectly_readableであり、Oindirectly_writableであることを要求するもので、この2つを組み合わせただけのコンセプトです。出力系アルゴリズムでは、Iはすでにinput_iteratorで制約されており、ここにはindirectly_readable<I>がすでに含まれています。したがって、indirectly_copyable<I, O>indirectly_writableに置き換えることができます。

template<input_iterator I, sentinel_for<I> S, weakly_incrementable O>
  requires indirectly_writable<O, iter_reference_t<I>>
constexpr ranges::copy_result<I, O> ranges::copy(I first, S last, O result);

今は2022年であり、C++20に対する変更を行うには遅すぎます。そのため、output_iteratorコンセプトから*i++ = std::forward<T>(t);を取り除くことも、出力系アルゴリズムoutput_iteratorを使用するように変更することもできません(後方互換を損ねるため)。この提案では、代わりにより弱いoutput_iteratorコンセプトを導入して、出力系アルゴリズムがそれを使うようにすることで、出力系アルゴリズムの出力イテレータに対する要件をoutput_iteratorという言葉を含んだ1つのコンセプトに統一することを提案しています。

ここまでの考察から、出力系アルゴリズムに必要なのはindirectly_writableであることと、既存の出力系アルゴリズムの制約がoutput_iteratorコンセプトと異なる点は*i++ = std::forward<T>(t);だけだということがわかっているので、これらの要件をまとめて最小のコンセプトに抽出したweak_output_iteratorコンセプトによってoutput_iteratorを書き直します

template<class I, class T>
concept weak_output_iterator =
  input_or_output_iterator<I> &&
  indirectly_writable<I, T>;

template<class I, class T>
concept output_iterator =
  weak_output_iterator<I, T> &&
  requires(I i, T&& t) {
    *i++ = std::forward<T>(t);        // not required to be equality-preserving
  };

weak_output_iteratorコンセプトは既存の出力系アルゴリズムの出力イテレータに対する制約と同等であり、かつoutput_iteratorという言葉を含んでいることで要求事項がより知名度のある概念によって表されています。これを用いて、ranges::copyは次のように書き直せます

template <input_iterator I, sentinel_for<I> S, weak_output_iterator<iter_reference_t<I>> O>
constexpr ranges::copy_result<I, O> ranges::copy(I first, S last, O result);

この提案は、このweak_output_iteratorを追加しoutput_iteratorを書き換えた上で次のような変更を行います

  • weak_output_rangeコンセプトの新設とoutput_rangeコンセプトのそれを用いた変更
  • mergeableコンセプトをweak_output_iteratorを使用するように変更
  • 全ての出力系アルゴリズムの出力操作に関する制約をweak_output_iteratorを使用するように変更
    • ほとんどの場合はweakly_incrementableindirectly_writableを再指定するだけ(コンセプト名が変わるだけ)
    • 一部のアルゴリズムでは、output_iteratorから変更されることで要件が弱まる(既存コードを壊すことはない)

これによって、全ての出力系アルゴリズムではweak_output_iteratorコンセプトを出力操作の制約に一貫して使用するようになり、output_iteratorという言葉を表示するようになります。

P2551R0 Clarify intent of P1841 numeric traits

P1841R2で提案されている数値特性(numeric traits)取得ユーティリティについて、実装経験からの疑問点を報告する文書。

P1841の内容については以前の記事を参照。

この提案は、P1841の内容を実際に実装した経験より生じた、設計上の疑問点を報告するものです。

  1. ある数値型に対して、ある特性が無効となる(定義されない)のはどのような場合か?
    • 現在の文言は、定数の表現が存在する場合は常にvalueを定義することを意図している
    • 文言は、動作の側面からそれが重要な役割を持つのかを明確にする必要がある
  2. boolを数値型として扱い、特性を提供するのかを明確にすべき
    • 現在の文言は、boolを数値型としてstd::numeric_limits<bool>と一致する特性を有効化している
    • 一般に、boolは数値型として使用されない(コア言語では算術型扱いされているが)
  3. 数値特性のほとんどは浮動小数点数から来ており、整数型では意味をなさないものが多い。次の数値特性は整数型でも有効であることを意図しているか?
    • denorm_min
    • epsilon
    • norm_min
    • reciprocal_overflow_threshold
    • round_error
    • max_exponent
    • max_exponent10
    • min_exponent
    • min_exponent10
  4. reciprocal_overflow_thresholdT(1)/xがオーバーフローしない最も小さいTの正の値)の値はIEC559の型について、非正規化数をゼロとして扱う場合にどのように変化するか?
    • ハードウェアが非正規化数を0として扱う場合、1/0 -> infとなりオーバーフローする。この場合は規定と一致しない。
    • この特性は実行時のプロセッサの状態に依存することがあり、その場合コンパイル時定数として正しい値は何か?
  5. numeric_limits::max_digits10は整数型では0となる。対応するmax_digits10_v<int>digits10_v<int> + 1を返すべきか?浮動小数点数型に限定されるべきか?

この提案では、これらのことに関連した投票をLEWGで行い設計を決定するための投票項目のリストも記載しています。

P2552R0 On the ignorability of standard attributes

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

新しい属性や属性likeな構文が提案される際には、ほぼ確実に不明な属性が無視可能であるという性質に言及がなされます。しかし、その性質については明文化されておらず、それが正確にどういう意味を持つのかについての一般的な理解はなく、現在ある種の紳士協定の上に成り立っているようです。

例えば、[[fallthrough(42)]]のような正しくない属性指定を無視するべきでしょうか?これはコンパイルエラーとするべきです(全ての主要なコンパイラはエラーとします)。このことは、属性は「無視できる」というよりも「属性を含めてプログラムがwell-definedであるならば、属性を無視してもプログラムの観測可能な振る舞いは変化しないはず」という性質を持っている事を示唆しています。例えば、ある実装が[[no_unique_address]]を実装していれば型のレイアウトが変更可能であるので、その効果は明らかに観測可能です(すなわち、無視できない)。

この提案は、このような属性の無視可能という性質についてその意味を明確に定義し規定しようとするものです。それによって、将来的に新規構文を属性とするべきか否かを判断する際の一助となるはずです。また、属性はC(C23)とC++の共通する機能であり、Cには標準属性の共通セマンティクスについての明確な規定があるため、その互換性を保つようにすることも意識しています。

この提案では、次のような「無視可能性」規定を追加する事を提案しています

well-definedな振る舞いをするwell-formedなプログラムは、特定の標準属性の全ての出現を省略すると、全ての観測可能な振る舞いが元のプログラムの正しい実行と一致するwell-formedなプログラムとなる。

このルールを適用するには、プログラムが属性を使用した上でwell-formedである必要があります。したがって、ill-formedな標準属性([[fallthrough(42)]]など)を無視できません。言い換えると、属性はそれ自体がwell-definedである場合、つまり属性が許可されている場所に正しい構文で現れている場合にのみ上記ルールによって無視可能となります。

例えば、[[maybe_unused]][[nodiscard]]は無視されてもされなくてもプログラムの観測可能な振る舞いに影響を与えないため、コンパイラはいつでも無視することが許可されます。[[no_unique_address]]は観測可能な振る舞いに影響を与えるため実装済みのコンパイラでは無視できず、未実装のコンパイラではそれは効果を持たないため観測可能な振る舞いに影響を与えず、無視することができます。

一方で、このルールを適用するためにはプログラムがその属性によって未定義動作をしないようにしなければなりません。つまり、属性の使用によって未定義動作を導入することは許可されますが、属性の削除(無視)によって未定義動作が導入されるのは許可されません。これは[[noreturn]][[assume(expr)]]などを許可する一方で、未定義動作を取り除くような属性(現在はない)が許可されない事を意味します。

P2553R0 Make mdspan size_type controllable

P0009で提案中のstd::mdspanについて、そのsize_typeを制御可能とする提案。

std::mdspansize_typeExtentsテンプレートパラメータのsize_typeから継承されるもので、Extents<Is...>mdspanの次元と要素数を指定するものです(sizeof...(Is)で次元を、各Isの値で要素数を指定)。そして、Extents::size_typeはその要素数を指定する値の型です。

Extents<Is...>::size_typestd::mdspanのレイアウトマッピングクラスでまで伝播し、レイアウトマッピングの処理において各Isの値が使用されることになります。例えば2次元配列なら、std::mdspan<double, std::extents<Width, Height>>のように指定し(Extentsstd::extents<Width, Height>)、レイアウトマッピングクラスでは、インデックス(x, y)に対してi = y * Width + xのようなインデックス計算が行われます。

ここで問題となるのは、std::extents::size_typeの型がsize_t(多くの環境で64bit符号なし整数)に固定されていることで、これによって、64bit整数による計算が32bit整数の計算よりも遅い環境でパフォーマンス低下の原因となります。そのような環境には現在の多くのGPUが当てはまり、GPUでは32bit整数の64bit整数に対するパフォーマンス比は2~4倍(高速)である事が多いです。そして、mdspanGPUが使用されることが多いHPCや機械学習、グラフィックスにおいて使用されることを想定しています。

この提案は、GPUmdspanの主戦場でパフォーマンス低下を防ぐために、mdspan(ひいてはExtents)のsize_typeをユーザーが指定できるようにしようとするものです。

提案では、CUDAでmdspan(のリファレンス実装)を使用して3次元ステンシル計算を行うコードでループインデックスの型を変更したときのベンチマークを取っています。

// mdspan
for(size_t i = blockIdx.x+d; i < s.extent(0)-d; i += gridDim.x) {
  for(size_t j = threadIdx.z+d; j < s.extent(1)-d; j += blockDim.z) {
    for(size_t k = threadIdx.y+d; k < s.extent(2)-d; k += blockDim.y) {
      value_type sum_local = 0;
      for(size_t di = i-d; di < i+d+1; di++) {
      for(size_t dj = j-d; dj < j+d+1; dj++) {
      for(size_t dk = k-d; dk < k+d+1; dk++) {
        sum_local += s(di, dj, dk); // インデックス計算が必要になるところ
      }}}

      o(i,j,k) = sum_local; // インデックス計算が必要になるところ
    }
  }
}

// 生ポインタ
for(size_t i = blockIdx.x+d; i < x-d; i += gridDim.x) {
  for(size_t j = threadIdx.z+d; j < y-d; j += blockDim.z) {
    for(size_t k = threadIdx.y+d; k < z-d; k += blockDim.y) {
      value_type sum_local = 0;
      for(size_t di = i-d; di < i+d+1; di++) {
      for(size_t dj = j-d; dj < j+d+1; dj++) {
      for(size_t dk = k-d; dk < k+d+1; dk++) {
        sum_local += data[dk + dj*z + di*z*y];  // インデックス計算が必要になるところ
      }}}

      data_o[k + j*z + i*z*y] = sum_local;  // インデックス計算が必要になるところ
    }
  }
}

この例ではループインデックスの型をsize_tにしていますが、計測ではこれをunsigned(32bit符号なし整数)にした時とsize_tにした時で比較したところ、生ポインタの場合もmdspanの場合もunsigned : 31ms、size_t : : 56msのような結果が得られたようです。

また、ループインデックス型をsize_tのままでmdspanの要素アクセス()内でのインデックス計算において、extents.extent(r)(次元ごとの要素数取得)をunsignedにキャストするようにしたときでも、31msという結果が得られたようです。

使用したGPUについて記載はありませんが、この結果はExtents::size_typesize_tに固定するとパフォーマンス低下が起こる環境がある事を示しています。

mdspanExtents::size_typeを指定する方法はいくつか考えることができます

  1. std::extents<Is...>の各Iscommon_typesize_typeとして取得する
    • (レイアウトにも依存する)オーバーフローの検知が困難
  2. std::extentssize_typeを取るようにする
  3. std::extentsを変更せず、size_typeとextentを受け取るbasic_extentsを導入する
    • std::extentsbasic_extentsエイリアスとなる
    • Extent型とその値を推論するような関数テンプレートでの使用ができなくなる
  4. レイアウトポリシークラスがsize_typeを取るようにして、extent値をキャストする
    • オブジェクトのサイズが大きくなり、追加のキャストが必要となる

筆者の方は3番目の方法を推奨し、提案しています。

また、これは後から修正できないことからC++23に向けた提案としています。std::extentsエイリアステンプレートとしてしまうと次のような推論ができなくなり、後方互換を損ねることから3番の変更は不可能となります。

// std::extentsが単一のクラス型である現在はこれが可能
template<class T, class L, class A, size_t ... Extents>
void foo(std::mdspan<T, std::extents<Extents...>, L, A> a) {
  // ...
}

P2554R0 C-Array Interoperability of MDSpan

生配列からのmdspan構築時に、その要素数に関する情報が失われてしまうのを修正する提案。

現在のmdspanでは、生配列を参照する際にポインタとして扱ってしまうことから、その要素数の静的な情報が伝わらない問題があります。

int a[6] = {1,2,3,4,5,6};

// 1次元配列からの構築
std::mdspan b(a);
static_assert(decltype(b)::rank()==0);  // 何も参照しないmdspanができている

この提案は、これを修正し次のように正しく情報が伝播するようにするものです。

{
  int a[6] = {1,2,3,4,5,6};

  std::mdspan b(a);
  static_assert(decltype(b)::rank()==1);
  static_assert(decltype(b)::static_extent(0)==6);
}
{
  int a[6] = {1,2,3,4,5,6};

  std::mdspan b(a,3);
  static_assert(decltype(b)::rank()==1);
  static_assert(decltype(b)::static_extent(0)==dynamic_extent;
  // b.extent(0)==3
}
{
  int a[6] = {1,2,3,4,5,6};

  std::mdspan<int, std::extents<3>> b(a);
  static_assert(decltype(b)::rank()==1);
  static_assert(decltype(b)::static_extent(0)==3;
}

この問題は、現在のmdspanのコンストラクタおよび推論補助が生配列をポインタに変換(decay)して扱うために起こっています。そこでこの提案では、mdspanのコンストラクタと推論補助に生配列用のものを追加したうえで、制約によって単にポインタが渡された時に選択されないようにすることで、1次元配列からの構築時にその要素数が静的に伝達されるように修正しています。

また、別の問題として、mdspanは多次元配列からの構築ができないようにされています。これは、多次元配列をその要素型のポインタから操作することはUBとなるためそれを禁止するもので、ユーザーにとっては意外なことかもしれませんが正当な仕様です・

int a[3][2] = {1,2,3,4,5,6};

std::mdspan<int, std::dextents<2>> b(a,3,2);  // error!
std::mdspan<int, std::extents<3,2>> c(a);     // error!

ただし、このことは実際には少なくとも2つのコンパイラGCCとclang)で正しく動作しているようです。この提案ではこの問題の解決を含めてはいませんが、コア言語の変更によってUBとならなくすることで多次元配列でもmdspanを動作させることができるとしています。

P2555R0 Naming improvements for std::execution

P2300で提案されている、senderファクトリ及びsenderアダプタの一部のものについて、名前を改善する提案。

P2300のsender/receiverは3つのチャネルを使用して値やエラー、キャンセルなどの情報をやり取りします。あるsenderアルゴリズムの操作がそれら3つのチャネルすべてで使用可能である場合、一貫した方法で特定のチャネルを呼び出すようにすべきです。その時、valueチャネルがデフォルトであり明示的に名前を付ける必要が無いと考えられる場合は常に名前を付けるべきではありません。

現在のP2300の命名においては常にチャネル名を明示する方向性に見えますが、just/just_error/just_stoppedsenderチェーンの起点として各チャネルのシグナルを挿入する)とlet_value/let_error/let_stoppedsenderチェーンの途中で各チャネルのシグナルを挿入する)を比較すると矛盾していることが分かります。

また、thenのファミリ(各チャネルの継続をチェーンする、あるいは各チャネルの値を変換する)の場合はthen/upon_error/upon_stoppedとなっており、ここでも矛盾が見られます。

この提案は、これらの名前付けについて次の2つの事を提案するものです

  • just, just_error/just_stoppedset_value/set_error/set_stoppedへ変更
  • upon_error/upon_stoppedthen_error/then_stoppedへ変更することを検討する

また、let_*系の操作についてもforkspawnといった呼び方が好まれているように見える、transfer_justtransfer_when_allなどの複合操作は実装の最適化として残した方がよい、などの事も書いていますが、P2300に関する経験の不足からこれらの事については提案していません。

P2557R0 WG21 2022-02 Virtual Meeting Record of Discussion

2022年2月7日(北米時間)に行われたWG21全体会議の議事録。

各投票においての発言などが記録されています。

おわり

この記事のMarkdownソース

[C++]後から触られないグローバルRAIIラッパー

C言語のライブラリにたまにある、最初にグローバル状態を~init()で確保して、それを最後に~release()で解放するというインターフェースについて、C++から使うときはRAIIで自動化したい衝動に駆られます。そのとき問題となるのは、プログラムの最初で初期化したらあとはプログラム終了時まで誰にも触ってほしくない、という気持ちをどうやって実現するのか、という事です。

グローバルRAIIラッパー

単純にRAIIに従った、次のようなクラスを考えることができます。

#include <iostream>

// グローバル状態初期化/解放関数とする
void init() {
  std::cout << "init()\n";
}
void release() {
  std::cout << "release()\n";
}

// グローバルRAIIラッパ
struct global_raii_t {

  global_raii_t() {
    init();
  }

  ~global_raii_t() {
    release();
  }
};

// RAII実態
inline const global_raii_t raii_obj{};


int main() {
  std::cout << "main()\n";
}

これの問題点は、別の人がいくらでも新しいオブジェクトを作って勝手に確保/解放処理を走らせることができる点です。後コピーも普通にできてしまいます。

型を隠そう!

デフォルトコンストラクタを隠したりコピーを防ぐこともできますが、簡単には型名を知られなければいいので型を隠す方向で行ってみます。よく知られた型隠蔽手段としてローカルクラスがあります。

// RAII実態
inline const auto raii_obj = [] {
  // グローバルRAIIラッパ
  struct global_raii_t {

    global_raii_t() {
      init();
    }

    ~global_raii_t() {
      release();
    }
  };

  return global_raii_t{};
}();

これで一見型名に触れられなくなりました。ただこれもまたよく知られているように、C++にはdecltype()があります。

int main() {
  using C = std::decay_t<decltype(::raii_obj)>;

  C c1{};             // ok
  C c2 = ::raii_obj;  // ok
}

初期化処理を分ける+コピー禁止

RAII型を関数内部に移したので初期化処理はそこでやることにしましょう。すると、構築された時でも初期化処理が走らなくなります。同時にコピー/ムーブを禁止してしまいます。

// RAII実態
inline const auto raii_obj = [] {
  init();

  // グローバルRAIIラッパ
  struct global_raii_t {

    global_raii_t() = default;

    global_raii_t(const global_raii_t&) = delete;
    global_raii_t& operator=(const global_raii_t&) = delete;

    ~global_raii_t() {
      release();
    }
  };

  return global_raii_t{};
}();

こうすれば、型名を取られて初期化されてもinit()は走らないしコピーとかもできません(ムーブコンストラクタ/代入演算子は、宣言がなく対応するコピーコンストラクタ/代入演算子が宣言されている場合に暗黙deleteされています)。

int main() {
  using C = std::decay_t<decltype(::raii_obj)>;

  C c = ::raii_obj;  // ng
}

しかしお忘れではないですか?デストラクタのことを・・・

int main() {
  using C = std::decay_t<decltype(::raii_obj)>;

  C c{};  // ok
}

後から構築を禁止する

もはやグローバルraii_objは動かないので、デストラクタが勝手に走るのを抑止するには新しく構築されるのを防止すればいいわけです。じゃあデフォルトコンストラクタを削除しよう!となりますが、しかし最初の一度だけは構築可能である必要があります。そこで、コンストラクタになんか引数を与えて構築するようにします。

// RAII実態
inline const auto raii_obj = [] {
  init();

  // グローバルRAIIラッパ
  struct global_raii_t {

    global_raii_t(int){}

    global_raii_t(const global_raii_t&) = delete;
    global_raii_t& operator=(const global_raii_t&) = delete;

    ~global_raii_t() {
      release();
    }
  };

  return global_raii_t{1};
}();

こうすれば単純には構築できなくなりますが、必要な引数がintであることはコードを見ればわかります。ここでも型名が隠蔽された型が必要です。

// RAII実態
inline const auto raii_obj = [] {
  init();

  struct tag {};

  // グローバルRAIIラッパ
  struct global_raii_t {

    global_raii_t(tag){}

    global_raii_t(const global_raii_t&) = delete;
    global_raii_t& operator=(const global_raii_t&) = delete;

    ~global_raii_t() {
      release();
    }
  };

  return global_raii_t{tag{}};
}();

こうするとtag型を外から取得して構築する手段がなくなるので、これでglobal_raii_t型を外から構築する手段はなくなりました。やった!

int main() {
  std::cout << "main()\n";

  using C = std::decay_t<decltype(::raii_obj)>;

  C c{{}};  // ok!
}

型推論オーバーロード解決の前では、型名など飾りです。

これは、tag型が集成体であるため{}による集成体初期化が可能であることによって起こっています。これを防ぐにはtag型が非集成体であればいいわけです。その方法はいくつかありますが、一番簡単なのはexplicitコンストラクタを追加することです。

// RAII実態
inline const auto raii_obj = [] {
  init();

  struct tag {
    explicit tag() = default;
  };

  // グローバルRAIIラッパ
  struct global_raii_t {

    global_raii_t(tag){}

    global_raii_t(const global_raii_t&) = delete;
    global_raii_t& operator=(const global_raii_t&) = delete;

    ~global_raii_t() {
      release();
    }
  };

  return global_raii_t{tag{}};
}();

こうすれば先ほどのように{}を用いた構築はできなくなります。そして、もはやこのglobal_raii_t型を外から構築する手段はありません。

int main() {
  std::cout << "main()\n";

  using C = std::decay_t<decltype(::raii_obj)>;

  C c{{}};  // ng
}

なお、これはC++17以降から有効です。C++14以前ではexplicitデフォルトコンストラクタがあってもクラスは集成体となり得ます。C++14以前で集成体でなくするには、仮想関数を何か定義しておくか継承しておくのが楽かと思います。

いやまあ、正直ここまでやる必要性はあまり感じられません。ローカルクラスで包むだけでもいいと思いますし、何なら最初の素のRAIIラッパで十分な気がします・・・

さらなる懸念

まあ何とここからデストラクタを呼び出すことができてしまうんですね・・・

int main() {
  std::cout << "main()\n";

  std::destroy_at(&raii_obj); // oK
}

この場合std::destroy_atの実行以降は未定義動作の世界です。しかしこれはちょっと防止する方法が思いつきません。

参考文献

この記事のMarkdownソース