[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ソース