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

文書の一覧

全部で43本あります。

P0009R14 MDSPAN

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

以前の記事を参照

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

  • std::spanと調和するように変更
  • レイアウトマッピングクラスの変換コンストラクタを、暗黙変換できないextentによってexplicitになるように修正
  • mdspanの変換コンストラクタを、保持するメンバとテンプレートパラメータが明示的に変換可能であるかによってexplicitになるように修正
  • submdspanの文言改善
  • レイアウトについての文言の一貫性改善
  • アクセサクラスとマッピングクラスからのデフォルト構築可能性を削除
  • layout_strideの変換コンストラクタを修正
  • extents/mdspanの整数値からの推論補助にexplicitを追加
  • 要素型とextentを含まないようにmdspanの制約を調整
  • 機能テストマクロの追加
  • submdspanのsubslice引数をpairの代わりにtupleを取るように変更
  • mdspan::unique_sizeを削除
  • 整数型のstride配列に対して柔軟になるように、layout_strideのコンストラクタを修正
  • extent/mdspanのコンストラクタがrank_dynamicまたは整数rank指定値(あるいはそのサイズの配列)のいずれかを受けられるように変更
  • mdspantrivially default constructibleであるという指定を削除
  • mdspanがnon-owningであるという単語を削除(例えば、shared_ptrをポインタとして使用する事ができる)
  • 1次元layout_leftからlayout_rightの相互変換を追加
  • ランク0のlayout_left, layout_right, layout_strideそれぞれの間の暗黙変換を許可

などです。

この提案はC++23入りを目指して、LEWGからLWGへ転送する採決を取るために12月のLEWG投票待ちをしています。

P0323R11 std::expected

エラーハンドリングを戻り値で行うための型、std::expected<T, E>の提案。

以前の記事を参照

このリビジョンでの変更は、std::unexpectedの変換コンストラクタを削除した事、制約と効果を全体的に合理化した事、expected<void, E>のために部分特殊化を定義した事、などです。

この提案は、C++23入りを目指してLWGでのビュー中です。

P0447R17 Introduction of std::hive to the standard library

要素が削除されない限りそのメモリ位置が安定なコンテナであるstd::hive(旧名std::colony)の提案。

以前の記事を参照

このリビジョンでの変更は、使用経験とconstexprの検討について追記した事です。

P0533R9 constexpr for cmath and cstdlib

<cmath><cstdlib>の一部の関数をconstexpr対応する提案。

以前の記事を参照

このリビジョンでの変更は、「non-constant library call」という用語を導入して、Cライブラリ関数がnon-constant library callとなる条件とnon-constant library callがコア定数式ではないことを追記する形で、提案する文言を改善したことです。

non-constant library callはFE_INEXACT以外の浮動小数点例外を発生させるようなC標準ライブラリ関数の呼び出しとして規定され、コア定数式となる(non-constant library callではない)C標準ライブラリ関数の呼び出しセマンティクスは、その呼び出しの引数型の浮動小数点数型に対して適用可能なC標準のAnnex Fで規定されたもの、として指定されています。また、math_errhandling & MATH_ERRNO == trueとなる場合のC標準ライブラリ関数の呼び出しもまたnon-constant library callとなります。

すなわち、浮動小数点環境に影響を与えるorの影響を受けるような<cmath><cstdlib>)の関数の呼び出しは、定数式で実行不可となるようになります。

P1413R3 Deprecate std::aligned_storage and std::aligned_union

std::aligned_storagestd::aligned_unionを非推奨とする提案。

std::aligned_storagestd::aligned_unionを含めた在野の類似のものは、次のような問題があるため本質的に危険だと思われます

  1. その使用は未定義動作を引き起こす
    • これらの型はストレージを提供するのではなく、指定されたサイズをもつ型を提供する
  2. 保証が間違っている
    • 規格では型が指定されたサイズ以上であることを指定しているだけで、サイズ上限が無い
  3. APIが適切ではない
  4. APIが適切ではないため、利用にあたって同じような事前作業が繰り返される

API選択の間違い

std::aligned_storagestd::aligned_unionは共通して次のような問題があります。

  1. その領域上に構築された値のアクセスにはreinterpret_castする必要がある
    • constexpr化できない
    • 未定義動作を容易に引き起こす
  2. ::typeが自動解決されない
  3. ::typeのサイズに上限が無い

2つ目の問題は意味が分かりづらいですが、std::aligned_storagestd::aligned_union)は::typeとして指定されたサイズとアライメントをもつ型を提供するものです。従って、std::aligned_storagestd::aligned_union)のオブジェクトを作成しても意味はなく、さらに間違ってそれを使用してしまうと悲惨なことになります。これは、std::aligned_storage_tstd::aligned_union_t)を使用すれば防止できますが、それが提供されていてもstd::aligned_storagestd::aligned_union)を直接使うという間違いを阻止する手段がありません。ここに1つ目の問題が重なり、間違って使用されても気づけない可能性があります。

3つ目は単に標準の規定の欠陥です。どちらも規定では::typeは少なくとも要求されたサイズ以上であることを指定しており、その上限は指定されていません。それによって、必要以上のメモリが想定外に使用される可能性があります(特にstd::aligned_storagestd::aligned_union)を配列の要素にした場合に影響が大きくなる)。

std::aligned_storageの問題

さらに、std::aligned_storageに固有の次のような問題があります

  1. テンプレート引数として構築したい型Tを直接取らない
  2. 第二引数(アライメント指定)にデフォルト引数が指定されている

std::aligned_storageはテンプレート引数として2つのstd::size_t値を取ります。1つ目は領域の最小サイズ指定、2つ名は領域のアライメント指定です。しかし、2つ目のアライメント指定が1つ目のサイズ指定と無関係に指定されることはまれであり、std::aligned_storageの用法を考えればむしろ構築したい型Tは固定で、std::aligned_storage<sizeof(T), alingof(T)>と指定するのが適切なはずです。

このように、現在のAPIは本来必要な構築したい型Tを取らないだけでなく、アライメント指定にはデフォルト値が指定されています。オーバーアラインされた型をサポートする必要はなく、デフォルト値が有効なのはそれが適正であることをたまたま利用者が知っている場合だけです。

Folly/Boost/Abseilの3つのライブラリにおけるaligned_storagestd::aligned_storagelikeなものも含む)の使用を調査したところ、95例のうち69例でaligned_storage<sizeof(T), alingof(T)>のように使用されていたようです。他にもインターネット上で検索可能なところでも同様の用法が支配的であることが確認できます。

std::aligned_unionの問題

std::aligned_unionにも固有の次のような問題があります

  1. 第一引数(サイズ指定)は無意味
  2. サイズとアライメントの推論がstd::aligned_storageと一貫していない

std::aligned_unionはサイズパラメータと可変長の型のリストを取り、それらの型の中の最大のサイズとアライメントを使用したストレージ型を用意します。第一引数のサイズ指定はstd::aligned_unionの最小サイズ指定であり、引数リストの全ての型のサイズがその値よりも小さい時でも、std::aligned_unionの提供する型のサイズはそれ(第一引数)よりも小さくなりません。

しかし、この最小サイズが必要になるのは非常にまれであり、ほとんどの場合はstd::aligned_union<0, Ts...>のように使用されます。この0の指定の意味はstd::aligned_unionを使い慣れていない場合には分かりづらく、その意図がサイズ0の型が欲しいのか単にAPIを満足するためだけに指定されているのか解読するのは困難です。

そして、std::aligned_unionが領域サイズとアライメントを勝手に計算してくれるのはいいことではありますが、そのことがstd::aligned_storageAPIと逆になっています。これはstd::aligned_union<0, T>の様な使用法(サイズとアライメントを自動で求めてほしい)につながり、このコードを書いた人以外の人が見た時に、std::aligned_storage<sizeof(T), alingof(T)>の代わり使用しているのか、将来型を追加することを見越しているのか、APIの矛盾という前提によってその意図を把握することは困難となります。

これらの問題から、この提案ではstd::aligned_storagestd::aligned_unionを非推奨にしようとしています。

また、可能であればstd::aligned_storageは次のように置き換えることを推奨しています。

namespace std2 {
  template <typename T>
  using aligned_storage = alignas(T) std::byte[sizeof(T)];
}

ただし、alignas(T)の指定はusing宣言では意味がなく正しく機能しないため、この提案ではこのような代替を導入することは提案していません。代わりに、ユーザーに対して現在std::aligned_storagestd::aligned_unionを使用しているところを次のように置換することを推奨しています

template <typename T>
class MyContainer {
  // ...
  
private:
  //std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
  alignas(T) std::byte t_buff[sizeof(T)];
  
  // ...
};
template <typename T>
class MyContainer {
  // ...
  
private:
  //std::aligned_union_t<0, Ts...> t_buff;
  alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
  
  // ...
};

こうしたときでもreinterpret_castの使用は避けられませんが、既にそれが必要とされるところで引き続き必要になるだけなので、新規に導入するよりも悪影響は少ないはずです。

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

P1467R6 Extended floating-point types and standard names

P1467R7 Extended floating-point types and standard names

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

以前の記事を参照

R6での変更は、

  • SG22/EWGでの議論に基づいて、通常の算術変換(usual arithmetic conversion)のルールをC23の動作と一致するように変更
  • オーバーロード解決のセクションを大幅に変更し、提案する方向性を「最小の安全な変換を優先」から「同じ変換順位を優先」に変更
  • std::is_extended_floating_pointなどの型特性を削除
  • 拡張浮動小数点数型のエイリアスなどを配置するヘッダを<stdfloat>に変更
  • C23の_FloatN_t名に関する説明を追記
  • エイリアスとそのリテラルサフィックスについて予備的な文言を追加

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

  • 拡張浮動小数点数型のためのリテラルサフィックスを言語機能として定義するように変更
  • 機能テストマクロを追加
  • C23の_FloatN_t名がC++でも使用可能であることを要求しないことを決定
  • IEEE/IEC浮動小数点数標準を参考文献(bibliography)から参照規格(normative reference)へ移動
  • タイプエイリアスの順番を論理的な順序になるように並び替えた

などです。

P1494R2 Partial program correctness

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

現代のコンパイラは未定義動作を活用して(未定義動作が現れないことを前提として)積極的な最適化を行うことがあります。それが起こると、未定義動作を回避するためのチェックやテストのコードをコンパイラが排除することがあります。

#include <cstdio>
#include <cstdlib>

static void bad(const char *msg) {
  std::fputs(msg, stderr);
#ifdef DIE
  std::abort();
#endif
}

void inc(int *p) {
  if(!p) bad("Null!\n");  // #1
  ++*p;
}

このコードでは、DIEマクロを事前定義していない場合に#1の行を削除する最適化が可能です(ただし、現在これを行うコンパイラはないようです)。なぜなら、#1の分岐はtrue/falseどちらのパスを通ったとしても次の行の++*p;に結局は到達し、pnullptrである場合のデリファレンスは未定義動作であるため、コンパイラpnullptrとならないと仮定することができ、遡って#1if(!p)は常にfalseとなるためtrueの分岐は実行されない、と導くことが可能だからです。「pnullptrである場合のデリファレンスは未定義動作であるため、コンパイラpnullptrとならないと仮定することができ」のような無茶な導出を支えているのは、未定義動作を含むプログラムは全体が未定義動作になるという規定([intro.abstract]/5)により、それによって未定義動作が起こりうる時にそれが起こらないとみなしてプログラムを書き換える最適化が許可されます。

なお、DIEマクロが定義されていればbad()の実行は戻ることがなく、従ってif(!p)trueとなる分岐はそこで実行が終了するため先ほどのような推論はできなくなります。

このような最適化あるいは未定義動作は、C++20で導入されかけていた契約プログラミングサポートを取り下げさせた原因の一つとなりました

void f(int *p) [[expects: p]] [[expects: *p<5]];

C++20契約プログラミングでは、契約条件が破られている時でも実行を継続する継続モードという実行モードが規定されており、その場合には1つ目の契約条件の実行後に2つ目の契約条件がチェックされることになり、先程と同様に未定義動作を起こらないものと仮定して1つ目の契約条件は常にtrueとみなしてしまうことが可能となります。

C++20契約プログラミングには契約違反時の動作をカスタムするための違反ハンドラーというものが規定されており、違反ハンドラを最適化に対してブラックボックス扱い(すなわち、違反ハンドラは戻ってくるとは限らない)とすることでこのような問題に対処することが模索されていたようです(結局はその議論も含めて紛糾したためC++20から取り下げられました)。

一番最初の例のコードは、volatile変数を用いた次のようなテクニックによって最適化から保護することが可能となります。

inline void log(const char *msg) {
  std::fputs(msg, stderr);    // always returns
}

bool on_fire() {
  static volatile bool fire;  // always false
  return fire;
}

void f(int *p) {
  if (p == nullptr) log("bad thing 1");
  if (on_fire()) std::abort();  // #1
  if (*p >= 5) log("bad thing 2");
}

volatile変数の読み取りはC++仮想機械(実装が模倣すべき振る舞いをする仮想適正実装)が規定する観測可能な振る舞い(observable behavior)の一部であり、観測可能な振る舞いは最適化の後でも必ず実行される必要があります。#1ifの条件では関数呼び出しを介してvolatile変数の読み取りが行われており、その読み取りは最適化の対象となりません。そのため、trueパスのstd::abort()は到達しないことがわかっていてもon_fire()の実行およびif (on_fire())文を最適化によって除去することはできず、次の行の*pに全てのパスで到達すると仮定できないことから、先ほどのような最適化が抑止されます。

ただし、コンパイラpnullptrである場合にon_fire()trueを返さない限り未定義動作となることを推察することができ、その場合on_fire()よりも前にstd::abort()を持ってくることができます(未定義動作は起こらないのだから、on_fire()trueを返すと仮定してもよい + プログラムが未定義となる場合にはそのプログラムは観測可能な振る舞いを実行しなくても良い)。その場合は、未定義動作を実行することなくそれを検出することができます。

とはいえこのような分析に実装が従う必要はなく、このテクニックには保証がありません。

この提案はこのテクニックを一般化し、最適化抑止の保証を与えたstd::observable()を導入することで、これら因果関係を逆転するような最適化をコントロールできるようにしようとするものです。

namespace std {
  void observable() noexcept;
}

std::observable()の呼び出しは最適化における一種のブロックとして動作して、std::observable()によるある1つのブロックが未定義動作を含まずに完了した場合、そのブロックはブロック内に含まれる観測可能な振る舞いを示すことを要求します。ブロックが未定義動作を含む時に未定義となるのはそのブロック内に留まり、コード上の因果関係を遡って未定義化が波及することはありません。より正確には、std::observable()(およびプログラムの終了)は1つの観測可能なチェックポイント(observable checkpoint)として規定され、そのようなチェックポイントの後方に未定義動作がある場合でもチェックポイント前方の観測可能な振る舞いを実行しなければならない、のように規定されます。

先ほどのC++20契約プログラミングの例では次のように使用して、いかなる場合でも1つ目の契約条件が評価されることを保証できます。

void f(int *p) [[expects: p]] [[expects: (std::observable(), *p<5)]];

他にstd::observable()を適用可能な明らかな場所は、その成否にかかわらずリターンするI/O操作の後、エラーをハンドルするコードの中です。そのような場所では未定義動作が発生する可能性が高いはずです。

この提案は、EWGのレビューを通過しており、LEWG/SG1/SG22での確認を待ってCWGに転送される予定で、今のところC++23を目指しているようです。

P1774R4 Portable assumptions

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

プログラマーはあるコードについて特定の仮定が成立する事を知っている場合があり、そのような情報をコンパイラに伝えることができれば、コンパイラの最適化の一助となる可能性があります。そして、全ての主要なC++処理系はその手段を提供しています。

  • clang : __builtin_assume(expr)
  • MSVC/ICC : __assume(expr)
  • GCC : if (expr) {} else { __builtin_unreachable(); }
int divide_by_32(int x) {
  __builtin_assume(x >= 0); // 仮定を伝える
  return x/32;
}

この例では、コンパイラは通常符号付整数の可能な全ての入力で正しく動作するコードを出力しますが、__builtin_assume(x >= 0)によってxが負およびゼロのケースを考慮しなくても良いことがわかるため、コンパイラは正の場合のみで正しく動作するコード(5ビット右シフト)を出力します。

このように高い有効性が期待できますが、各実装の独自拡張でありその意味論や振る舞いも微妙に異なっているなどポータブルではありません。この提案はこの既存の慣行となっている機能を標準化するとともに、既存実装とC++標準の両方にうまく適合するように統一された構文と意味論を与えようとするものです。

構文は__builtin_assume(expr)をベースとした属性構文[[assume(expr)]]を提案しています。

int divide_by_32(int x) {
  [[assume(x >= 0)]];
  return x/32;
}

この属性はどこにでも書けるわけではなく、空のステートメントに対して([[fallthrough]]と同様)のみ指定でき、かつ関数内部でのみ使用できます。[[assume(expr)]]exprは評価されないオペランドであり、副作用を持つ式を指定することもできますが、決して実行されません。そして当然ですが、expr == falseとなるような入力に対しては未定義動作となるため、この仮定が満たされるようにするのはプログラマの責任となります。

このような仮定は、契約プログラミングにおける事前条件とよく似たものに思えます。しかし、契約の目的は事前条件と事後条件をコード上で記述できるようにするとともに、実行時にチェックすることでバグを発見するものであり、インターフェースなどAPIの境界の部分で使用されるものです。この機能(仮定の伝達)の目的はコードの特定の部分における事前条件(不変条件)をコンパイラに伝えるもので、特定の実装の詳細として使用されます。また、誰もが広く使用するものではなく、パフォーマンスが必要となるところで専門家だけが使用するものです。

また、契約の事前条件を仮定とみなすことでパフォーマンスが向上するということを示した調査はなく、むしろ低下させるか全く変化がないことを示した調査は存在しています。そのため、事前条件のアサーションと仮定を同じ言語機能で表現すべきではなく、提案中の契約プログラミングの構文とは異なったものをここでは提案しています。また、将来的に契約プログラミングに仮定の能力を与える場合でも、この機能をベースとしてそれを指定することができます。

P1854R2 Conversion to literal encoding should not lead to loss of meaning

文字列リテラルエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。

以前の記事を参照

このリビジョンでの変更は、マルチキャラクリテラルの各要素が基本文字集合のメンバに限定されるのではなく、1つのコード単位として表現可能なように文言を修正したこと、マルチキャラクリテラル関連の変更が視覚的な曖昧さを避ける為だけのものであることを強調するように文章を変更したことなどです。

P1899R1 stride_view

範囲を等間隔の要素からなる範囲に変換するRangeアダプタstride_viewの提案。

stride_viewのような機能はSTLに存在しておらず、C++20のRangeライブラリにもこれを簡単に合成する方法はありません。それによって次のような処理のforループからアルゴリズムへの移行が妨げられています。

// 2つ飛ばしの代入
for (auto i = 0; i < std::ssize(v); i += 2) {
  v[i] = 42; // fill
}

// 3つ飛ばしの変換
for (auto i = 0; i < std::ssize(v); i += 3) {
  v[i] = f(v[i]); // transform
}

// 3つ飛ばしの選択ソート
for (auto i = 0; i < std::ssize(v); i += 3) {
  for (auto j = i; j < std::ssize(v); i += 3) {
    if (v[j] < v[i]) {
      std::ranges::swap(v[i], v[j]);
    }
  }
}

stride_viewによってこれらの処理は次のように書くことができるようになります。

// 2つ飛ばしの代入
std::ranges::fill(v | std::views::stride(2), 42);

// 3つ飛ばしの変換
auto strided_v = v | std::views::stride(3);
std::ranges::transform(strided_v, std::ranges::begin(strided_v), f);

// 3つ飛ばしの選択ソート
stdr::stable_sort(strided_v);

C++23にstride_viewがない場合、必要とするユーザーはそれを得ようとして自作を試み、filter_viewが最適だと思うかもしれません。

auto bad_stride = [](auto const step) {
  return views::filter([n = 0, step](auto&&) mutable {
    return n++ % step == 0;
  });
};

この実装は少なくとも次の2つの問題があり、間違っています

  • filter_viewに渡す述語はstd::predicateのモデルでなければならず、副作用は認められない。
  • このラムダは後方への移動を考慮しておらずbidirectional_rangeの入力rangeに対して動作しない。
    • ラムダがstd::predicateのモデルとなっておらず、それによって出力rangebidirectional_rangeのモデルにもならないため、これは診断不要の未定義動作となる。

stride_viewは利便性が高いく、欠けていればこのように誤った実装をされる可能性が高いため、<ranges>追加しなければならないということで、C++23に向けて追加しようとする提案です。

提案されているstride_viewは、入力範囲のrandom_access_rangeを継承するようになっています。その際問題となるのは、指定された数で割り切れない長さを持つ範囲に対するstride_viewの後退時で、ナイーブな実装(指定された数飛ばしてイテレータを進行/後退する実装)だと終端に到達した時に正しく後退することができません。

// ここでのviews::strideはナイーブ実装のものとする

// 3で割り切れる長さの入力範囲
auto x = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

// prints 0 3 6 9
std::ranges::copy(std::views::stride(x, 3), std::ostream_iterator<int>(std::cout, " "));

// prints 9 6 3 0
std::ranges::copy(std::views::stride(x, 3) | std::views::reverse, std::ostream_iterator<int>(std::cout, " "));

// 3で割り切れない長さの入力範囲
auto y = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// prints 0 3 6 9
std::ranges::copy(std::views::stride(y, 3), std::ostream_iterator<int>(std::cout, " "));

// prints 8 5 2、reverseすると異なる範囲になる
std::ranges::copy(std::views::stride(y, 3) | std::views::reverse, std::ostream_iterator<int>(std::cout, " "));

ナイーブな実装だと、stride_view(stride)reverseした時には終端イテレータからのstride飛ばしの後退をすることになりますが、入力範囲がstrideで割り切れない長さの場合先頭から進行した時と異なる要素をイテレートすることになります。

これを防ぐためには、ステップ数を記憶しておき、それを利用して正しい位置を求めるようにします。

// 次のものはstride_view::iteratorのメンバ変数
// n : 進める距離
// stride_ : stride_view(n)のn
// step_ : 

iterator& advance(difference_type n) {
  if (0 < n) {
    step_ = ranges::advance(current_, n * stride_, ranges::end(underlying_range_));
    return *this;
  }
  //...
}

この形式のranges::advanceは指定された距離(n * stride_)に対して進めなかった距離を返します。すなわち、入力範囲の終端以外のところではstep_はゼロです。入力範囲の終端かつ入力範囲長がnで割り切れない場合のみstep_は非ゼロ(正)になります。進行時はこのadvance(n)を使用して、最後に進めなかった距離をstep_に記録しておきます。

iterator& advance(difference_type n) {
  //...

  if (n < 0) {
    auto stride = step_ == 0 ? n * stride_
                             : (n + 1) * stride_ - step_;
    step_ = ranges::advance(current_, -stride, ranges::begin(underlying_range_));
  }
}

後退時はadvance(-n)のように使用して、step_がゼロであれば端点の考慮は必要なたいめn * stride_分(これは負になる)入力範囲のイテレータcurrent_)を現在位置から後退させます。step_が非ゼロなら元の範囲の終端を超えた位置まで進行しようとしていたことがわかるので、(n + 1) * stride_ - step_のようにして終端位置から後退する距離を調整します。

これらの工夫によって、stride_viewは入力範囲のrandom_access_rangeを継承できるようになります。

P2071R1 Named universal character escapes

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

C++11から、基本文字集合に含まれない任意のユニコード文字をポータブルに表すためにユニバーサル文字名を使用できます。

// UTF-32 character literal with U+0100 {LATIN CAPITAL LETTER A WITH MACRON}
char32_t c = U'\u0100';
// UTF-8 string literal with U+0100 {LATIN CAPITAL LETTER A WITH MACRON} U+0300 {COMBINING GRAVE ACCENT}
char8_t c = u8"\u0100\u0300";

とはいえ、16(8)進エスケープシーケンスではそれがどの文字を指すのか直感的ではありません。この提案は、16進数値列の代わりにユニコードの規定する文字の名前を使用してユニバーサル文字名を構成できるようにしようとするものです。

char32_t c = U'\N{LATIN CAPITAL LETTER A WITH MACRON}'; // Equivalent to U'\u0100'
char8_t c = u8"\N{LATIN CAPITAL LETTER A WITH MACRON}\N{COMBINING GRAVE ACCENT}"; // Equivalent to u8"\u0100\u0300"

16(8)進エスケープシーケンスによるユニバーサル文字名では、その内容を表示するために同時にコメントを追記する場合、時間経過とともにコードとコメントの同期が取れなくなりがちですが、このように文字の名前(エイリアス)で指定する事でそのようなコミュニケーション手段を取る必要がなくなります。

この機能は名前付文字エスケープ(Named character escape)と呼ばれます。構文は、文字/文字列リテラル中で\N{...}の中に文字の名前を指定する形です。

使用可能な文字名はユニコード規格によって提供され、ユニコード規格に追随しており安定性が保証されている次のものを参照します。

ユニコードが将来追加しうる名前との衝突を回避するために、この提案では使用可能な文字名を実装が拡張することを許可しないようにしています。

名前指定のマッチングについてはUAX44-LM2を参照しており、これによって大文字と小文字を区別しない、ハイフンの省略、アンダースコアのスペースへの置換など、柔軟な指定が可能となっています。例えば、次の名前は全てU+200B {ZERO WIDTH SPACE}を示すものとして扱われます

ZERO WIDTH SPACE
ZERO-WIDTH SPACE
zero-width space
ZERO width S P_A_C E

この提案はSG16およびEWGのレビューを通過し、C++23導入を目指してCWGへ転送されるための投票待ちをしています。

P2093R10 Formatted output

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

以前の記事を参照

このリビジョンでの変更は、ベースとするドラフトを最新のものに更新し、それに伴ってP2418の変更を適用したことです。

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

P2198R3 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

以前の記事を参照

このリビジョンでの変更は、(この提案がC++23で入るとして)フリースタンディングと示される機能テストマクロの値を更新するものをC++20以前の機能のみに限定したこと(その機能の導入・更新とフリースタンディング化を区別するため)、C++23のライブラリ機能でフリースタンディング指定可能なものに対応する機能テストマクロを追加したこと(__cpp_lib_byteswap, __cpp_lib_constexpr_typeinfo, __cpp_lib_invoke_r, __cpp_lib_is_scoped_enum, __cpp_lib_ranges_zip, __cpp_lib_to_underlying)などです。

P2249R3 Mixed comparisons for smart pointers

スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGのレビューを受けて設計上の決定を明確にするために説明を改善したこと、std::unique_ptr/std::shared_ptrから派生したクラスを除外するための制約を追加したことです。

P2273R3 Making std::unique_ptr constexpr

std::unique_ptrを全面的にconstexpr対応する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けて提案する文言のフォーマットを調整したこと、unique_ptr同士の比較演算子を対象に追加したことなどです。

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

P2278R2 cbegin should always return a constant iterator

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

以前の記事を参照

このリビジョンでの変更は、views::as_constviews::all_constに名前を変更したこと、エイリアステンプレート(std::ranges::const_iterator_t/std::ranges::range_const_reference_t)と機能テストマクロを追加したことなどです。

この提案はこのリビジョンを持ってLEWGでのレビューを完了し、LWGに送るためのLEWGの投票待ちをしています。

P2286R3 Formatting Ranges

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

以前の記事を参照

このリビジョンでの変更はP2418の変更を適用したこと、文字列を適切に引用符で括る方法や連想コンテナをフォーマットする方法について議論と機能を拡充したこと、あらゆるフォーマット指定子の紹介とそれをより広く機能させるための議論の追加、設計に集中するために提案文言を一旦削除、などです。

P2302R1 std::ranges::contains

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

このリビジョンでの変更は、std::basic_string_view/std::basic_string.contains()メンバ関数削除を提案しなくしたこと、命名についての説明を追加したことなどです。

P2338R2 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

以前の記事を参照

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

  • strtokC++のフリースタンディングライブラリとして追加
  • <ratio>への誤った言及を削除
  • <cinttypes>の提案からの削除完了
  • その他C向けの変更

などです。

P2361R4 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなる規格ドラフトを更新したことです。

P2388R4 Minimum Contract Support: either No_eval or Eval_and_abort

契約が破られた時に継続しないコントラクトサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、不適切な契約によって契約チェックが無限ループに陥る問題について追記したこと、関数の戻り値型がvoidである場合に事後条件に名前(戻り値のキャプチャ)を導入できないように文言を修正したことなどです。

この提案による契約サポートは事前条件と事後条件のみを対象としていて、クラス不変条件についてのサポートは欠けています。その場合、クラス不変条件とはすべてのパブリックメンバ関数の事前条件である、と考える人がいるかもしれません。すると、例えば次のようなコードが生まれる可能性があります

class Container {
  // ...
public:
  bool invariant() const { return (size() == 0) == empty(); }

  int size() const [[pre: invariant()]];
  bool empty() const [[pre: invariant()]];
};

このクラスのsize()/empty()はどちらの関数を呼んだとしても、自身及び片方が事前条件invariant()のチェックで呼び出されるため、無限ループに陥ります。この問題の考えられる解決策は、これが危険であることを周知・教育しこのようなコードが書かれないことを信頼する、あるいは契約のチェック時には再帰的に契約チェックを行わないようにする、の二つが考えられます。

2つ目のアプローチでは、事前条件の中でそれ自体が事前条件をなしている関数を呼び出すことはできず、すべての事前条件が広い契約(wide contract)を持たなければならないことを示しています。

2つ目のアプローチはBoost.Contracts、1つ目のアプローチはD言語でそれぞれ実装されています。

P2407R1 Freestanding Library: Partial Classes

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

このリビジョンでの変更は、機能テストマクロを追加したこと、std::optionalmonadic oeprationstd::string_view::containsについても同様にフリースタンディング化することにしたことなどです。

P2408R3 Ranges iterators as inputs to non-Ranges algorithms

P2408R4 Ranges iterators as inputs to non-Ranges algorithms

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

以前の記事を参照

R3での変更は、プロクシイテレータを正しくハンドルできるように変更したことです。

これによって、Cpp17XXXIterator要件からイテレータコンセプトを使用してイテレータチェックを行うように変更するのは、constant iteratorに対してのみになります(以前はすべてのイテレータ)。プロクシイテレータでは、*it = vがユーザーの期待通りに振舞うように設計されていますが、*it = std::move(v)swap(*it1, *it2)などの変異操作は予期しない振る舞いをする可能性があります。なぜなら、通常のイテレータdecltype(*it)T&であるのに対して、プロクシイテレータのそれは別のもの(おそらくprvalue)となるためです。例えば、zip_view::iteratorの場合はdecltype(*it)は参照型のstd::pair/std::tupleであってstd::pair/std::tupleの参照ではありません。

C++20 Rangeアルゴリズムはこのようなプロクシイテレータを正しく扱うことができるように設計されています。例えばムーブ/swapiter_moveiter_swapCPOを使用することで、プロクシイテレータによってカスタムされたムーブ/swap操作を呼び出します。非Rangeアルゴリズム(つまり従来のアルゴリズム)ではその考慮はされておらず、イテレータデリファレンス結果に直接ムーブ/swap操作を呼び出します。

ただし、プロクシイテレータconstant iterator(要素アクセスのみ可能で書き換えができないイテレータ)としては正しく動作します。問題となるのはプロクシイテレータように設計されておらず、かつmutable iterator(要素の書き換えができるイテレータ)を必要とするアルゴリズムだけです。したがって、Cpp17XXXIterator要件を非Rangeアルゴリズムに渡されるmutable iteratorのために維持すれば、そこでプロクシイテレータを使用することは未定義動作となります。

R3ではこれに対処して、ForwardIterator要件が要求されるところでは、mutable iteratorが必要となる場合はCpp17ForwardIterator要件を満たす、そうでない場合はforward_iteratorのモデルとなる、というように変更しています。

R4(このリビジョン)での変更は、実装経験を収集し追記したこと、それに基づいてこの提案の既存実装への影響セクションを更新したことです。

筆者の方はこの提案の内容をGCC(libstdc++)、MSVC(MSVC STL)に適用したうえでテストを行い、その結果いくつかの問題は見つかったもののこの提案の内容を実装可能であると報告しています。

P2441R1 views::join_with

パターンによってrangerangeとなっているようなシーケンスを接合して平坦化するRangeアダプタ、views::join_withの提案。

以前の記事を参照

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

P2443R1 views::chunk_by

指定された述語によって元のシーケンスの可変個の要素を組にした要素からなるシーケンスを生成する、views::chunk_byアダプタの提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映したことです。

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

P2446R1 views::all_move

入力シーケンスの各要素をstd::moveするようなviewである、views::moveの提案

以前の記事を参照

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

P2454R0 2021 November Library Evolution Polls

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

以下の13の提案が投票にかけられる予定です。

LEWGでの作業を完了してLWG(CWG)へ転送することを確認するための投票です。

P2461R1 Closure-based Syntax for Contracts

属性likeな構文に代わるコントラクト構文の提案。

以前の記事を参照

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

  • タイポ修正と単語や文書の改善
  • 文法の変更
    • preconditionspostconditionsassertionsに分割し明確化
    • postconditionsの戻り値パラメータをオプションにした
  • []の指定が採用されない場合、メンバ関数におけるキャプチャデフォルトを[&, this]となるように変更
  • 値によるキャプチャでは、対応するラムダ本体は常にmutableであることを明確にした
  • サンプルの拡張
  • 文書から「lambda」を削除し、「closure」に統一した
    • 提案する文法はラムダクロージャの構文に依存しているが、ラムダ本体の構文には依存していないため
  • 「future extensions」のセクションを副節に分割
  • 副作用消去の異なるモデルを紹介し、このテインのモデルよりも緩いものを採用すると何が失われるかを説明
  • 戻り値の破棄の例を意味のあるものに変更
  • attribute-specifier-seqの場所を変更して、契約指定そのものに対する属性指定を許可
  • コントラクト仕様そのものに対するテストとファジングについてのセクションを追加
  • 省略形ラムダとの関連を追記
  • 「capture design space」セクションを追加
  • 検討、否定されたアイデアに関するセクションを追加
  • 契約指定がチェックされないモードでもODR-usedであることを明確化

などです。

P2467R0 Support exclusive mode for fstreams

fstreamに排他モードでファイルオープンするフラグ、std::ios_base::noreplaceを追加する提案。

C11ではfopenで書き込みモードでファイルを開く際のフラグにxを追加できるようになりました。これによって、ファイルが排他モードでオープンされ、既存のファイルが存在する場合はオープンに失敗するようになります。

FILE *fp = fopen("foo.txt", "wx"); // w+x,wbxなど、書き込みモードのみxが有効

これはいわゆるTime of Check, Time of Use(TOCTOU)という問題に対処するためのものです。

FILE *fp = fopen("foo.txt","r"); 
if(!fp) { 
  // file does not exist
  fp = fopen("foo.txt", "w");

  //...

  fclose(fp); 
} else { 
  // file exists
  fclose(fp); 
} 

このようなコードにおいて、1-2行目のファイルの存在チェックから3行目の書き込み用ファイル作成(オープン)までの間にその名前のファイル(あるいはシンボリックリンクなど)が作成されてしまうと、4行目以降の処理において意図しないところに書き込みを行ってしまう可能性があります。xを追加した排他モードの書き込みファイルオープンでは、fopenにおいてファイルの存在チェックとファイル作成を同時に行うことでTOCTOUに対処し、既存ファイルが存在する場合に上書きを行わないようになります。

C++はC11を参照しているのでfopenに対するxフラグはすでにサポートされていますが、std::fstreamで同じことをする標準的な手段はなく、TOCTOUを回避しようとする場合に使用することができません。xフラグはglibcで早期からサポートされており、時期POSIX標準でも導入される予定です。また、C++の初期(標準化以前)のstd::ofstreamではnoreplaceフラグがサポートされていました(これはおそらくPOSIXO_EXCLから来ており、C90との互換のために標準化されませんでした)。また、MSVCではios_base::_Noreplaceとしてサポートされています。

これらの理由から、C++std::ofstream)でも排他モードの書き込みファイルオープンをサポートすべき、という提案です。

排他モードフラグはstd::ios_base::noreplaceとして追加されます。

int main() {
  // 書き込みモードかつ排他モード(ファイルが無い場合のみファイル作成)でオープン
  std::ofstream ofs("file.txt", std::ios_base::out | std::ios_base::noreplace); 

  if (!ofs) {
    // file.txtが存在する場合失敗する
    std::cout << "file exist\n";
    return -1;
  }
}

P2477R1 Allow programmer to control and detect coroutine elision by static constexpr bool must_elide() and

P2477R2 Allow programmer to control and detect coroutine elision

コルーチンの動的メモリ確保を避ける最適化を制御し、起こったことを検出するAPIを追加する提案。

以前の記事を参照

R1での変更は

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

  • must_elide()の戻り値型をboolから3要素のenumへ変更
  • 背景をさらに追記
  • must_elide()を非constexprとすることについて議論を追加

must_elide()で動的メモリ確保省略を強制できないケース(コルーチン本体が見えていない時)をどうするかなどの問題について議論が必要であるため、この提案はまだ方向性を決定していません。

P2483R0 Support Non-copyable Types for single_view

single_viewがコピー不可なクラスのオブジェクトでも保持できるようにする提案。

現在のsingle_viewstd::copy_constructibleコンセプトによって要素型を制約しているため、コピー構築不可能な型を要素とすることができません。

// fooはムーブオンリーな型とする
foo make_foo();

std::views::single(make_foo()) // error

この制約はどうやら、最初期のview定義(copyableかつmovable)を満たすための制約のようですが、途中でviewの定義は変更され現在はmovableであればよくcopyableは必ずしも求められていません。したがって、single_viewのこの要件はstd::move_constructibleまで弱める事ができるはず、という提案です。

これによって冒頭のサンプルのような、ムーブオンリーな型を要素とするsingle_viewが作成可能となります。

P2484R0 Extending class types as non-type template parameters

クラス型を非型テンプレートパラメータ(NTTP)の条件にアダプトするための新しい構文operator template()の提案。

C++20からNTTPの制限が緩和され、一部のクラス型をNTTPとして取れるようになりました。新たな制限はNTTPとなれる型をstructural typeとして制限しており、それは次のいずれかに該当するものです

  1. スカラ型
  2. 左辺値参照型
  3. 次の条件を満たすリテラル
    • 全ての基底クラス及び全ての非静的メンバ変数はpublicかつmutableではない
    • 全ての基底クラス型及び全ての非静的メンバ型はstructural typeであるか、配列型である

クラス型のNTTPは3つ目の条件を満たすものに限られています。これによってstd::pairstd::arrayなどの型をNTTPとする事ができるようになりますが、std::tuple, std::optional, std::variantstd::string, std::vectorなどの型をNTTPとすることはできず、これらの型をこの条件にアダプトさせることも困難です。

特に問題となるのは全メンバがpublicであるという制約です。この制約はテンプレートの等価性判定のための制約で、あるクラス型のNTTPのテンプレートとしての等価性判定をメンバ毎の比較で行える型を指定するものです。型として追加の意味論を持つためにそのような比較が適切ではない型では、そのメンバ変数はprivateとされる事が一般的です。

そういう意味論の下では、std::tuple, std::optional, std::variantなどはそのメンバの比較によるテンプレート等価性の判定が適切ではありますが、これらの型はそのメンバを全てpublicとして実装されません。これらの型をNTTPとして扱うために必要なことは、C++20で導入されたメンバ毎比較によるテンプレート等価性判定にアダプトするための仕組みだけです。

将来的にはstd::string, std::vectorなどの型もNTTPとして活用できると便利ですが、std::tuple, std::optional, std::variantと同じアプローチではこれは達成できません。例えば、std::stringはポインタ3つ(あるいはポインタ2つとサイズ2つ)をメンバとして実装される事が多いですが、単にそれらメンバの比較によってテンプレート等価性判定をしてしまうと次のような問題があります

// テンプレート等価性をメンバのポインタ値の比較によって可能としたとすると
template <std::string S>
struct X {
  bool operator==(X) const = default;
};

// この2つは同じ型となっていて欲しいが
X<"hello"> a;
X<"hello"> b;

a = b;  // error、型が合わない

異なるstd::stringオブジェクトのメンバのポインタ値は異なる領域を指すため、このa bは異なる型を持ちます。これはstd:string及びテンプレート等価性の意味論にそぐわないため、std::string, std::vectorなどの型をNTTPとして扱うためには別のアプローチが必要そうです。

この提案のアプローチはoperator template()という演算子を追加する事で、ある型のNTTPのテンプレート等価性を別のstructural typeに移譲するものです。

class A {
private:
  int m_i;

  struct Repr {
    int i;
  };

  constexpr A(Repr r) : m_i(r.i) {}

  constexpr auto operator template() const -> Repr { 
    return {m_i};
  }

public:
  constexpr A(int i) : m_i(i) { }
};

template <A a>
struct X { };

T::operator template()structural typeな型Rを返さなければならず、RTの表現として機能する必要があり、TRから構築可能である必要があります。この例では、A::operator template()の返す型A::RepeによってA::Repr{1} == A::Repr{1}となるため、A{1} == A{1}となります。

この例は説明的なもので、実際には次のようにより簡易化できます。

class A {
private:
  int m_i;

  constexpr auto operator template() const -> int { 
    return m_i;
  }

public:
  constexpr A(int i) : m_i(i) { }
};

template <A a>
struct X { };

intはすでにstructural typeなので、それをラップする型は必要ありません。

これをtuplelikeな型に対して書くのは非常に面倒な作業となるので、operator template()default実装可能です。

class A3 {
private:
  int i;
  int j;

  constexpr auto operator template() const = default;
public:
  constexpr A3(int i, int j) : i(i), j(j) { }
};

template <A3 a> struct X { };

default定義のoperator template()を持つ型は、C++20の集成体をNTTPとして使用した時と同様に、全ての基底クラス及び非静的メンバ変数についての比較によってテンプレートの等価性判定が可能であることを表明し、それによってテンプレート等価性判定が行われます。唯一の違いは、基底クラス及び非静的メンバ変数がprivateであっても構わない点です。ただし、これは再帰的ではなく、全ての基底クラス及び非静的メンバ変数がstructural typeである事が求められます。

// structural typeではない
class B {
  int i;  // プライベートメンバ
};

// structural typeとしたい
class D : B {
  int j;
  constexpr auto operator template() const = default;
};

template <D d> // error、BがstructuralではないためDもstructuralではない
struct Y { };

operator template()は関数のように見えますが、あくまでコンパイラがテンプレートの等価性判断(及びマングリング方法)をどうするかを指定する注釈にすぎません。したがって、実際にこれが呼び出される事はなく、呼び出された時の振る舞いなどは規定されず、defaultoperator template()の戻り値型を気にする必要はありません。ユーザー定義型Cをマングリングに参加させる(Cによってテンプレート等価性を判定する)には単にCを直接使用すればokです。

class A {
private:
  C c;  // ユーザー定義型(C++20のstructural typeかもしれないし、operator template()を持つかもしれない)
  D d;  // なんらかの理由によりマングリングに関与しないユーザー定義型

  struct Repr { C c; };
  constexpr auto operator template() const { return Repr{c}; }
  explicit constexpr A(Repr);
};

Cstructural typeであればその性質に到達する方法にかかわらず(Cintエイリアスであったりoperator template()を持っていたりにかかわらず)、A::Reprstructural typeでありCstructural性を正しく反映します。ここでも、operator template()を呼び出す必要はありません。

std::tuple, std::optional, std::variantはこのoperator template()を使用してメンバごとの比較によってテンプレート等価性を判定できるようになり、簡単にNTTPにアダプトする事ができます。一方、std::string, std::vectorは現在定数式での動的メモリ確保が一時的(実行時に持ち越せない)なため、operator template()を正しく定義したとしてもNTTPとして使用する事ができません。それを解決する提案は進行中ですがまだ採択されていないため、この提案ではこの2つの型に対しては何もしません。

定数式での非一時的な動的メモリ確保が許可されていないことから非defaultoperator template()を急ぐ必要はないため、この提案ではC++23に向けてクラス型でdefaultoperator template()を定義可能にし、そのクラス型は全ての基底クラス及び非静的メンバがstructural typeであれば自身もstructural typeとなるようにすることを提案しています。また、それをstd::tuple, std::optional, std::variantにも定義して、要素型が全てstructural typeであればこの3つの型もstructural typeとなるようにすることも提案しています。

ここまでの説明のように、この提案では非defaultoperator template()を使用してstd::string, std::vectorなどの型をNTTPとして扱えるようにする方向性が示されていますが、ここではそれは提案されません。将来的に定数式での非一時的な動的メモリ確保が許可された後で解禁する予定です。

P2485R0 Do not add value_exists and value_or to C++23

std::numeric_traitsに代わる数値特性クエリAPIとして提案されているP1841R1から、value_existsvalue_orを取り除く提案。

P1841R1に関しては以前の記事を参照

P1841R1に提案されているvalue_existsは数値特性TraitTについて利用可能かどうかを調べるもので、次のような定義になります。

template <template<class> class Trait, class T>
constexpr bool value_exists = requires { Trait<T>::value; };

これは例えば、value_exists<finite_max, int>のように使用しますが、LWGにおける議論の過程でTraitTを別々に受け取る設計について疑問が提起されたようです。

template <class T>
constexpr bool value_exists = requires { T::value };

// このようなAPIではないのはなぜ?
static_assert(value_exists<finite_max<int>>);

つまりこのように、Trait<T>の形で受けた上で静的メンバ::valueの存在チェックをする形の方が理解しやすく使いやすいのでは?という事です。

value_or()は数値特性TraitTについて利用可能でない場合に、指定された値へフォールバックするためのものです。

template <template <class> class Trait, class T, class R = T>
inline constexpr R value_or(R def = R()) noexcept;

これは例えば、value_or<finite_max, int>(100)のように使用します。この問題は、value_or()の戻り値型は引数として渡した値の型Rであるため、Tと異なる可能性がある事です。例えば、value_or<finite_min, double>(1)int型の結果となり、これは想定される振る舞いではないでしょう。

これらの理由により、P1841からvalue_existsvalue_orを取り除き、他の数値特性のみを採用することを提案しています。これらのユーティリティが必要になったら、また後で議論をすれば良いとのことです。

P2486R0 Structured naming for function object and CPO values

CPOやniebloidなどの関数オブジェクトに対しての命名ルールに関する提案。

P2322R5 ranges::foldの議論の過程で、その命名に関する議論が起こりました。名前付についての議論は、名前が主観的になるとともにその名前の技術的な側面が主観的な側面によって曖昧になってしまうため、とても厄介な議論です。この提案は、そのような議論をなるべく回避するために、主として関数オブジェクトに対する命名についての標準的な方法を提案するものです。

物事のある集合に名前をつける際は、ネスト構造を追加することで簡単になります。この提案の言うStructured naming(構造化された名前付)とはそのようなネスト構造を適切に反映した命名のことであり、例えば次のようなものです。

構造化された名前 構造化されていない名前
std::vector<T>
std::vector<T>::value_type
std::list<T>
std::list<T>::const_iterator
std::chrono
std::chrono::steady_clock
std::chrono::steady_clock::time_point
std_vector<T>
std_vector_value_type<T>
std_list<T>
std_list_const_iterator<T>
std_chrono
std_chrono_steady_clock
std_chrono_steady_clock_time_point

この構造化されていない例の命名は多くの人が適切ではないと考えると思われますが、それは私たちが無意識下で想定している普遍的で適切な命名構造への期待に反しているからこその反応だと思われます。

現在LEWGで合意されたP2322R5の関数群は次のような命名となっています。

fold_left()                   // 左畳み込み
fold_left_first()             // 最初の要素を初期値とする左畳み込み
fold_right()                  // 右畳み込み
fold_right_last()             // 最初の要素を初期値とする右畳み込み
fold_left_with_iter()         // イテレータを返す左畳み込み
fold_left_first_with_iter()   // イテレータを返す最初の要素を初期値とする左畳み込み

ここに載っていないものも含めて、fold系操作はさらに増加する可能性があります。その際、このように構造化されていない命名は組合せ爆発とともに複雑化します。とはいえ将来的に追加されるものも含めて、それらの変種が別々のオーバーロードとして提供されるのは妥当なことであり、問題となるのはその命名のみです。

プレーンな関数名では構造化されていない命名を避けることは困難でしたが、このfoldは関数オブジェクトとして実装されることが示唆されており、他の提案でも関数オブジェクトやCPOの命名について構造化されていないものがあります。関数オブジェクトであれば、メンバとして関数オブジェクトをネストさせることができるはずです。それによって、非構造化名を使用せざるを得なかった関数に対しても構造化された命名をすることができます。

構造化された名前 構造化されていない名前
fold.left()
fold.left.with_iter()
fold.left.first()
fold.left.first.with_iter()
fold.right()
fold.right.last()
fold_left()
fold_left_with_iter()
fold_left_first()
fold_left_first_with_iter()
fold_right()
fold_right_last()

他のところでは、P2300のCPOに対してもこれを適用できそうです。

構造化された名前 構造化されていない名前
std::execution::receiver.set_value()
std::execution::receiver.set_error()
std::execution::receiver.set_done()
std::execution::sender.connect()
std::execution::set_value()
std::execution::set_error()
std::execution::set_done()
std::execution::connect()

ただし、senderrecieverは同じ名前空間でコンセプトとして提供されているため、実際にはこのような命名は行えません。この提案の構造化された命名の問題点は、コンセプト定義とのこのような衝突を回避すること(コンセプトの構造化された命名)ができなければコンセプトとCPOの命名についてLEWGの時間を無駄に消費してしまう点です。

この提案はP2322R5をブロックし命名を変更しようとするものではないですが、この方向性が受け入れられるならば事後的にP2322R5の命名を構造化されたものに変更することを目指しているようです。

P2487R0 Attribute-like syntax for contract annotations

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

現在、契約プログラミングのサポートの議論は「P2388R4 Minimum Contract Support: either No_eval or Eval_and_abort」にて行われており、そこではC++20の契約プログラミングの時からの属性に似た構文を採用しています。

int f(int i)
  [[pre: i >= 0]]
  [[post r: r >= 0]];

一方、それに対してラムダ式に似た構文の提案(「P2461R1 Closure-based Syntax for Contracts」)も出ています。

int f(int i)
  pre{i >= 0}
  post(r){r >= 0};

また、例えば次のような構文を容易に思いつくことができます(提案はされていません)

int f(int i)
  pre(i >= 0)
  post(r: r >= 0);

この提案は、現在の属性likeな契約構文が契約の指定にとって適しているのかを吟味するものです。主に以下のように分析しています。

  • 無視できる
    • 現在のC++の属性についての規定では、「無視する」の意味が曖昧
    • 属性の無視について規定しなおすことを提案している
  • 宣言的or命令的
    • 契約が数学的な意味での述語である(宣言的)なら属性構文は適している
    • 契約がチェックされる(命令的)なら、属性構文は直観に反する
  • 並べ替え可能
    • 1つの属性中の2つの属性([[A, B]])は並べ替え可能だが、2つの属性([[A]] [[B]])は並べ替えられない(意味が変わる)
    • これは契約指定と互換性がある(ショートサーキットされるかが変わるため契約は並べ替えられない)
  • 順序
    • 属性構文では、他の属性と契約の順序についての問題が発生する
int f1(int i)               // correct declaration?
[[pre: i > 0]]
[[using gnu: fastcall]]
[[post r: r > 0]]; 

int f2(int i)               // correct declaration?
[[using gnu: fastcall]]
[[pre: i > 0]]
[[post r: r > 0]]; 

int f3(int i)               // correct declaration?
[[pre: i > 0]]
[[post r: r > 0]]
[[using gnu: fastcall]]; 
  • コンテナとしての[[]]
    • 人々が抱いている(可能性のある)直感は、[[]]が0か1以上の属性をカンマ区切りで指定できるコンテナ(リスト)であるというもの
  • 型と効果の分析
    • 属性を使用して、型に対する効果の注釈を行うEffect systemをいくらでも考えられる
    • そのような型と効果の静的分析という観点からは、属性構文は自然に見える
  • 関数型に現れるか
    • 属性が関数型に影響を与えるのかを明確にする必要がある
  • 契約チェックとUB
    • 契約指定に違反したときはある種の未規定の動作となるが、その未規定の動作は実際にはコンパイラオプションによって制御されている。契約指定に違反しないプログラムのセマンティクスには影響を与えず、それは属性の無視可能な側面に合致する。
  • メタ注釈
    • 属性構文を採用しない場合、契約そのものに属性指定できる
  • リフレクションでの検出
    • 属性指定されたものをリフレクションで検出可能とするかどうか
  • コロンの使用
    • コロンだけでは契約なのか属性なのかを判別できない

総合的には属性は不利なのでは?と思わせる内容ですが、この文書はどちらを提案しているわけでもありません。

P2489R0 Library Evolution Plan for Completing C++23

C++23の設計完了に向けたLEWGの作業予定や進捗を示す文書。

C++23は2023年発行予定ですが、そこに向けた提案は2022年2月7日までに採択されなければなりません。LEWGのリソースは限られており、それをC++23に入る可能性のある提案に集中させる必要があり、この文書はそのような提案をリストアップしたものです。

C++23に向けて取り組む必要のある提案

優先度を高くする必要はないが、サイズが小さめで労力がかからなそうな提案

LEWGとしてはおそらく、これ以外の提案に(一時的に)リソースを割かなくなるため、これ以外の提案がC++23に入る可能性はほぼありません。なおこれは、ライブラリについての提案のみなので、コア言語に関してはまた別の話です。

P2490R0 Zero-overhead exception stacktraces

例外からのスタックトレースの取得の提案について、問題点とその解決策についての提案。

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

P2370R0では、任意の例外オブジェクトからスタックトレースを取得可能とすると多大な実行時コストがかかることを認めた上で、std::this_thread::capture_stacktraces_at_throw()によってその使用をスイッチできるようにすることを提案しています。しかし、そのアプローチはスレッドローカル変数へのアクセスを伴っており、スレッドローカル変数へのアクセスには実行時コストがかかります(参考)。現在の例外機構では例外そのもののコストに比べればスレッドローカル変数へのアクセスコストは無視できるものであるため問題とされないことが多いようですが、将来的に「P0709 Zero-overhead deterministic exceptions: Throwing values」が採用されるとそれが問題となることが予想されます。それを考慮しなくても、P2370の主張する最小の実行時コストはゼロではありません。

また、P2370の方法ではそれを有効化するのに再コンパイルとリンクが必要となりますが、独自の例外発生メカニズムを持つサードパティのライブラリの場合、再コンパイルとリンクされて再出荷されるのに年単位の時間がかかる可能性があります。さらに、例外を内部で使用しているライブラリではユーザーがこの機能を有効化した時に、ライブラリ内部で完全に捕捉されている例外であってもオーバーヘッドがかかることになるため忌避される可能性があります。その場合はライブラリから漏れる例外に対処する必要がないとすれば、APIのエントリポイントで無効化しておく、という方法がとられる可能性があります。

C++の例外処理は通常、言語に依存しない低レベルの機能の上に構築されており、それはWindowsでは構造化例外、Itanium ABIではLevel I Base ABIです。これらの低レベルの機能では通常、例外処理は2段階の過程を経て行われています。1段目は「検索」フェーズで、例外が投げられた地点から適切なハンドラを見つけるためにスタックを調べます。2段目は「巻き戻し」フェーズで、例外が投げられた地点から選択されたハンドラまでクリーンアップ(デストラクタ呼び出し)しながら戻ります。重要なのは次の2点です。

  • 検索フェーズではスタックの内容を変更しない
  • ハンドラの識別は動的であり、コンパイラ/ライブラリによって見つかった関数が呼ばれる

これらの点から、次のような代替メカニズムを考案できます

  1. ユーザーコードでは、特別な関数または新しい構文を利用して、特定のcatchブロックについてスタックトレースが必要である事をマークする
  2. コンパイラはそのようにマークされたcatchブロックを認識すると、そのcatchブロックが例外ハンドラとして選択された時にその選択の直前(検索フェーズ)でスタックトレースを取得するための適切なコード/データを発行できる
  3. ユーザーコードでは、巻き戻しフェーズの後の例外処理において、保存されたスタックトレースを取得することができる

このアプローチの利点は次の2つです

  1. 透明性
    • 例外を投げるコードを修正したり再コンパイル・リンクする必要がない
    • このメカニズムは例外をキャッチする側の変更のみに依存している
  2. ゼロコスト
    • 例外スロー時の検索フェーズでマークされたハンドラに到達しなければ、動作に影響がない

これを実現するための構文として次のいずれかを提案しています

  1. std::stacktrace::from_current_exception()の特別扱い
    • 欠点 : from_current_exception()の呼び出しがcatchブロックの外に意図せず移動すると機能しなくなる。
  2. catchブロックのデフォルトパラメータ
    • 欠点 : 新しい構文である事、キャッチする複数の型を指定するものとして勘違いされる可能性がある
  3. catch-with-init
    • 欠点 : 新しい構文である事、検索フェーズで一般的なユーザーコードを実行するのは危険
  4. 検索フェーズの露出
    • 欠点 : 新しい構文であり悪用される可能性がある、検索フェーズで一般的なユーザーコードを実行するのは危険
void f() noexcept(false);

int main() {
  // 1
  try {
    f();
  } catch (const std::exception& ex) {
    std::cout << ex.what() << "\n" << std::stacktrace::from_current_exception() << std::endl;
  }

  // 2
  try {
    f();
  } catch (const std::exception& ex, std::stacktrace st = std::stacktrace::from_current_exception()) {
    std::cout << ex.what() << "\n" << st << std::endl;
  }

  // 3
  try {
    f();
  } catch (auto st = std::stacktrace::current(); const std::exception& ex) {
    std::cout << ex.what() << "\n" << st << std::endl;
  }

  // 4
  try {
    f();
  } catch (const std::exception& ex) if (auto st = std::stacktrace::current(); true)  {
    std::cout << ex.what() << "\n" << st << std::endl;
  }
}

これは少なくともWindowsのABIとItanium ABIで実装可能である事を確かめているようですが、その他のABI(プラットフォーム)で実装可能かどうかは不明であり情報を求めています。

P2491R0 Text encodings follow-up

システムの文字エンコーディングを取得可能とする提案(P1885)に対して、設計の欠陥を指摘する提案。

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

要約すると、以下の3点を問題としてあげています

  1. "UTF-16"というエンコーディング名を別の目的で再利用しているため、混乱が生じている
  2. Windowsで以前に使用されていたUCS-2ワイドエンコーディングを適切に表現できない
  3. オブジェクト表現を指定しようとしており、C++抽象化を壊している

これに対して、次のような改善を提案しています。

  1. IANAレジストリに登録されているエンコーディング方式(encoding scheme)のオクテット(バイト)が、std::text_encodingにおけるコードユニットとみなされる事を規定する
  2. 現在または将来のIANAのエンコーディング割り当てとの衝突を避けるために負の列挙値を持つ追加のエンコーディングWIDE.UTF16, WIDE.UTF32, WIDE.UCS2, WIDE.UCS4)を規定する
  3. std::text_encoding::(wide_)literal()からは、sizeof(char_type) == 1でないとIANAのエンコーディング方式を返せない、という注意を追記
  4. CHAR_BIT == 8またはsizeof(wchar_t) > 1に関する制限の削除

  5. P2491 進行状況

P2492R0 Attending C++ Standards Committee Meetings During a Pandemic

WG21ミーティングへCOVID19パンデミックが与えた影響についての報告書。

筆者の方の個人的な経験を元にして書かれていて、WG21ミーティングがオンラインへ移行したことによって参加のためのコストが低下し、準備に時間を取れるようになったことで積極的に議論に参加できるようになったとのことです。筆者の方にとってはオンラインミーティングにはメリットしかなく、再び対面ミーティングに移行してしまうと参加のハードルが上がることから傍観者に追いやられてしまうことを危惧しているようです。

P2493R0 Missing feature test macros for C++20 core papers

コア言語機能について、忘れられていた機能テストマクロの更新を行う提案。

忘れられていたのは、P0848R3 Conditionally Trivial Special Member FunctionsP1330R0 Changing the active member of a union inside constexprの2つで、どちらもC++20のコア言語機能です。

P2231R1 Missing constexpr in std::optional and std::variantの実装にあたってこれらの機能が使用可能である必要があり、実装はそれを検知できる必要がありますが、その手段が提供されていない事がわかりました。

この提案では、__cpp_concepts__cpp_constexprの値を202002LC++20規格完成の年月)に指定します。C++20から時間が経ってしまっていますが、__cpp_conceptsの値は更新されておらず、__cpp_constexprの値は更新されている(P2242R3)ものの、GCCのみがこれを実装済みでかつP1330R0も実装済みであるので問題ないようです。

おわり