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

文書の一覧

全部で49本あり、SG22(C/C++相互互換性に関する研究グループ)のCの提案を除くと48本になります。

N4887 PL22.16/WG21 agenda: 7 June 2021, Virtual Meeting

2021年6月7日 11:00 (北米時間)に行われたWG21本会議のアジェンダです。

C++23のための3回目の全体会議です。

N4888 WG21 virtual meetings: 2021-06, and -10

今年のWG21全体会議の予定表。

次は10月に予定されています。これもオンラインで行われることが決定しています。

N4889 WG21 admin telecon meeting: 2021-09

10月の全体会議の直前に行われる管理者ミーティングの予定表。

N4890 WG21 2021-05 Admin telecon minutes

2021年5月24日に行われた、管理者ミーティングの議事録。

N4891 WG21 2021-06 Virtual Meeting Minutes of Meeting

2021年2月22日(米国時間)に行われた、WG21全体会議の議事録。

CWG/LWGの投票の様子などが記載されています。

N4892 Working Draft, Standard for Programming Language C++

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

N4893 Editors' Report - Programming Languages - C++

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

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

P0205R1 Efficient Seeding of Random Number Engines

乱数シードの生成ヘルパーであるstd::seed_adapterを追加する提案。

<random>の乱数エンジンを使用する際、次の様に初期化を行うのが非常に一般的です。

template <typename EngineT>
void seed_non_deterministically_1st(EngineT& engine) {
  std::random_device device{};
  engine.seed(device());
}

しかし、EngineTの内部状態が大きい場合、この初期化は適切ではありません。より良い乱数の生成のためには内部状態の初期値が全て偏りなく初期化されている必要がありますが、たとえばstd::mt19937の場合その内部状態は19968bitある一方で、random_deviceの戻り値型はunsigned intであり多くの環境で32bitの大きさしかなく、初期状態の選択に偏りが生じます。それによって、エンジンが生成する乱数値にも偏りが生じる事があリます。

この様な問題に対処するために、std::seed_seqを利用する事ができます。std::seed_seqはシード列を表現するための型で、イテレータ範囲などによって任意の数の整数値から初期化し、それによって生成される32bit整数によるシード列を.generate()メンバ関数から取得する事ができます。あるいは、エンジンの.seed()メンバ関数に渡すこともできます。

template <typename EngineT, std::size_t StateSize = EngineT::state_size>
void seed_non_deterministically_2nd(EngineT& engine) {
  using engine_type = typename EngineT::result_type;
  using device_type = std::random_device::result_type;
  using seedseq_type = std::seed_seq::result_type;

  constexpr auto bytes_needed = StateSize * sizeof(engine_type);
  constexpr auto numbers_needed = (sizeof(device_type) < sizeof(seedseq_type))
      ? (bytes_needed / sizeof(device_type))
      : (bytes_needed / sizeof(seedseq_type));

  // シード列のシード?を生成
  std::array<device_type, numbers_needed> numbers{};
  std::random_device device{};
  std::generate(std::begin(numbers), std::end(numbers), std::ref(device));

  // シード列によるエンジンの初期化
  std::seed_seq seedseq(std::cbegin(numbers), std::cend(numbers));
  engine.seed(seedseq);
}

このコードにはいくつか問題があります。

  • 複雑
  • 乱数エンジンはその状態サイズを公開していない(あるいは、そう規定していない)
  • 正確ではない(std::seed_seqは偏りをもたらしうる)
  • 非効率
    • std::random_device -> std::array(スタック) -> std::seed_seq(ヒープ)とコピーされる
    • std::random_deviceは必要となる乱数のサイズを取らないため、実装によっては乱数取得が非効率になる

この提案は、この様なシード列による乱数エンジンの初期化という作業を効率的かつ簡易に行うためのstd::seed_adapterを提案するものです。

template <typename EngineT>
void seed_non_deterministically_3rd(EngineT& engine) {
  std::random_device device{};
  std::seed_adapter adapter{device};
  engine.seed(adapter);
}

std::seed_adapterは余分なコピーや一時オブジェクトが不要で、動的メモリ確保を必要とせず、偏りを導入しないシード列を表現する型です。

std::seed_adapterは任意のUniform Random Bit Generatorクラス(例えばstd::random_device)の参照をラップし、その関数呼び出し演算子を呼び出す.generate()メンバ関数を提供するクラステンプレートです。

P1068R4が採択された場合、ラップしているGeneratorクラスの関数呼び出し演算子は一度だけ呼び出せば良くなります。採択されない場合でも、例えばstd::seed_adapterの実装に合わせた最適な呼び出しをサポートする事が可能です。

std::seed_adapterは例えば次の様な小さなクラス型になります。

template <uniform_random_bit_generator U>
class seed_adapter {
public:
  // types
  using result_type = typename U::result_type;

  // constructors
  explicit constexpr seed_adapter(U& gen) noexcept;

  // generating functions
  template <random_access_iterator It>
    void constexpr generate(const It f, const It l)
    requires __unsigned_integral_least32<typename iterator_traits<It>::value_type>;

private:
  U* m_gen;  // exposition only
};

この部分は以下の方によるご指摘によって成り立っています。

P0447R15 Introduction of std::hive to the standard library

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

以前の記事を参照

このリビジョンでの変更は、splice()が例外を投げる場合の詳細を追記したこと、reshape(), splice()の設計の選択についての追記、(common_rangeではない場合の)番兵を用いたassign()の追加等です。

そして、このリビジョンより、名前がstd::colonyからstd::hiveへ変更されています。これはP2332R0を受けてのものです。

P0533R7 constexpr for cmath and cstdlib

P0533R8 constexpr for cmath and cstdlib

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

以前の記事を参照

R7での変更は、ベースとなる規格ワーキングドラフトをN4878に変更した事と、std::lerpconstexprとして<cmath>に追加されたことについてのメモを追記し、LEWGのレビューを受けて文言を修正した事です。

R8での変更は、記載サンプルをいくつか改善・修正し、constexprにする関数選択基準を修正したことと、LEWGのレビューを受けて文言を修正した事です。

この提案は2021年2月の全体会議で投票にかけられましたが、反対が多く否決されました。そこでの主な反対意見は実装経験の少なさと実装可能性についてのものでした。それを受けて次の2つのガイダンスを採択しました。

  • <cmath>関数のcosntexpr評価において、math_errhandling & MATH_ERRNO == trueとなる場合にerrnoを設定するようなエラー発生した場合、コンパイルエラーとする
  • 提案している<cmath>関数のconstexpr評価を、適用可能なすべての浮動小数点数型についてCのAnnexFに従うようにする

これらの事をベースに、再びC++23導入を目指します。

P1018R11 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/05

EWG(コア言語への新機能追加についての作業部会)が2021/05に議論した提案やIssueのリストや将来の計画、テレカンファレンスの状況などをまとめた文書。

P1072R8 basic_string::resize_and_overwrite

std:stringに領域(文字長)を拡張しつつその部分を利用可能にする為のメンバ関数resize_and_overwrite()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加し、string::resize()の効果について曖昧な点を修正した事、ベースとなる規格ワーキングドラフトをN4885にしたことなどです。

この提案はC++23導入を目指してLEWGからLWGへ転送されました。

P1132R8 out_ptr - a scalable output pointer abstraction

スマートポインタとポインタのポインタを取るタイプのC APIとの橋渡しを行う、std::out_ptrの提案。

以前の記事を参照

このリビジョンでの変更はtypo修正がメインです。

この提案は2021年6月の全体会議で投票にかけられ、C++23入りが決定しています。

P1202R3 Asymmetric Fences

非対称なフェンスの提案。

並行処理における多くの同期アルゴリズム(ハザードポインタ、RCUなどなど)では遅いが稀にしか実行されない実行パスを保護するために、通常の実行パスに対してフェンスの挿入が必要となります。そのフェンスはあまり実行されないパスが実行された際にもデータレースを起こさないようにするためにあり、それによって通常の実行パスのパフォーマンスが低下します。現在のところC++には、これを回避するための簡易な(ライブラリユーザーの手間を必要としない)代替案が用意されていません。

例えばデッカーのアルゴリズムの例で見てみると

// グローバル変数
std::atomic_int x{0}, y{0};
int r1, r2;

// thread1(いつも実行される)
x.store(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
r1 = y.load(std::memory_order_relaxed);

// thread2(たまにしか実行されない)
y.store(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
r2 = x.load(std::memory_order_relaxed);

// 2つのスレッドが終了した後で、このアサートが発動することはない
assert(!(r1 == 0 && r2 == 0));

この場合のスレッド1とスレッド2の実行の頻度が大きく異なる場合、頻繁に実行される方の処理だけを見ればフェンスは必要ないはずですが、あまり実行されない処理との間で同期を取るために、両方にフェンスの挿入が必要になります。これによって、頻繁に実行される方の処理のパフォーマンスが低下する可能性があります。

この場合に、頻繁に実行される処理では軽いフェンスを、あまり実行されない処理では重い(普通の)フェンスを使用して同期を取ることができれば、頻繁に実行される処理におけるパフォーマンス低下を回避することができます。

この提案は、その意味で非対称なフェンスであるstd::asymmetric_thread_fence_light()std::asymmetric_thread_fence_heavy()の導入を目指すものです。

先ほどの例は次のように書くことができます。

// グローバル変数
std::atomic_int x{0}, y{0};
int r1, r2;

// thread1(いつも実行される)
x.store(1, std::memory_order_relaxed);
std::asymmetric_thread_fence_light(std::memory_order_seq_cst);   // コンパイラによるストア/ロードの入れ替えを防止する
r1 = y.load(std::memory_order_relaxed);

// thread2(たまにしか実行されない)
y.store(1, std::memory_order_relaxed);
std::asymmetric_thread_fence_heavy(std::memory_order_seq_cst);   // 通常のフェンス
r2 =  x.load(std::memory_order_relaxed);

// 2つのスレッドが終了した後で、このアサートが発動することはない
assert(!(r1 == 0 && r2 == 0));

std::asymmetric_thread_fence_heavy()std::atomic_thread_fence()とほぼ同等で、フェンスとしてのフル機能を持ちますが、std::asymmetric_thread_fence_light()atomic変数の読み書きが前後したり統一したりする事を防止する程度の事しかしないため軽量となります。そしてこの時でも、この2つのフェンスを介してatomic変数の読み書きに順序付けを行う(strongly happens before関係を与える)ことができます。

これを用いることによって、このような問題をライブラリの内部で解決することができるようになり、ユーザーはこのことについて何も気にしなくても良くなります。

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

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

前回の記事を参照

このリビジョンでの変更は、意図せず抜け落ちていたstd::addressofを対象に追加したことです。

この提案はLEWGからLWGに転送され、議論されています。

P1664R4 reconstructible_range - a concept for putting ranges back together

viewによって別のrangeに変換されてしまった範囲を、元のrange(と同じ型)に戻す操作、std::ranges::reconstructと関連するコンセプトの提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言を修正した事と、常にADLを使用するように設計を変更した事などです。

P1675R2 rethrow_exception must be allowed to copy

std::rethrow_exceptionの同じ例外オブジェクトを再スローするという規定を変更する提案。

std::current_exceptionの規定は注意深く書かれており、例外オブジェクトをスタック上に構築する実装(MSVC ABI)とヒープ上に構築して取り回す実装(Itanium C++ ABI)の両方をサポートし、例外オブジェクトをstd::exception_ptrの保持するメモリ領域にコピーするなどの実装の自由を与えています。

一方で、std::rethrow_exceptionはそうではなく、引数のstd::exception_ptrの参照している例外オブジェクトと同じオブジェクトを再スローする、と規定しています。これは、スタック上に例外オブジェクトを構築しているMSVC ABIでは実装不可能です(例外ハンドラ毎に例外オブジェクトのデストラクタを呼ぶため)。

MSVCの現在の実装は、std::rethrow_exceptionに割り当てられたメモリ領域に引数で指定されたstd::exception_ptrの参照する例外オブジェクトをコピーし、現在のアクティブな例外を指すTLSの値をその場所に更新してからthrowすることで、あたかもそれを再スローしているかのように実装されています。これは同じオブジェクトを再スローしていないため規格違反となっています・・・

この提案は、MSVCを含めた再スロー時に例外オブジェクトを区別する必要のあるABIのためにstd::rethrow_exceptionの規定を、例外オブジェクトのコピーとそのための追加のメモリ領域の使用を許可するように変更することで、この問題を解決しようとするものです。

次のようなコードにおいて

struct X : std::exception {
  // コピーが例外を投げる可能性のあるメンバを持つとする

  X() { }

  X(X const&) { 
    if(oh_no()) throw 42;
    /* else success */
  }
};

int main() {
  try {
    std::exception_ptr eptr;

    try { 
      throw X();
    }
    catch(X& x) {
      std::cout << "caught X with address " << (void*)&x;
      eptr = std::current_exception(); 
    }

    std::rethrow_exception(eptr);
  }
  catch(X& x) {
    std::cout << " caught X with address " << (void*)&x;
  } catch(int) {
    std::cout << " caught int";
  }
}

この提案の主張は次の3つの場合のいずれも起こりうることを許可することです

  1. caught X with address xxxx caught X with address xxxx
  2. caught X with address xxxx caught X with address yyyy
  3. caught X with address caught int

MSVCは1のケースを実装できないため、2か3を選ぶことになりますが、コピーが例外を投げることが許可されないために2と3の実装は許可されていません。この提案の目的は、この場合の2,3を許可することです。

P1689R4 Format for describing dependencies of source files

C++ソースコードを読み解きその依存関係をスキャンするツールが出力する依存関係情報のフォーマットを定める提案。

以前の記事を参照

このリビジョンでの変更はいくつかのキーの追加・削除など修正と、サンプルの更新などです。

P1708R5 Simple Statistical Functions

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

以前の記事を参照

このリビジョンでの変更は、分位数(中央値)・最頻値について統計的な問題が提起されており別の提案に分離されることになったこと、stats_error例外が削除されたこと、イテレータの要素(のプロジェクション)型に応じて戻り値型を選択するstats_result_tの導入、いくつかの統計式エラーの修正、関数やクラス名、引数名を意味のあるものへ変更、rangeexecution_policyにまつわるいくつかのエラーの修正、などです。

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

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善、依存関係スキャナー実装者や#embed実装者からのコメントを受けての構文の変更、名前付引数の実装拡張機能のサポートの追加などです。

このリビジョンでは、#embedが追加のパラメータ(及び属性指定)を取れるようになり、長さの指定方法が変更されました。

#embedの追加のパラメータは読み込むファイル名の後に指定します。ここでは。limit, prefix, suffix,emptyの4つが提案されています。

// limit引数、読み込みサイズを指定する
const int please_dont_oom_kill_me[] = {
    #embed "/dev/urandom" limit(512)
};
// sizeof(please_dont_oom_kill_me) == 16

// prefix引数、suffix引数
// リソース(ファイル)が空でない場合に指定したプリフィックスとサフィックスを付加する
const unsigned char null_terminated_file_data[] = {
    #embed "might_be_empty.txt" \
        prefix(0xEF, 0xBB, 0xBF, ) /* UTF-8 BOM */ \
        suffix(,)
    0 // always null-terminated
};

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

以前の提案では読み取るサイズをファイル名の前に指定していましたが、このリビジョンからはlimit引数によってそれを指定します。limit(n)のように指定し、nは読み取る長さの最大長(バイト数)であって、初期化しようとする配列の要素数ではありません。

prefix引数、suffix引数は読み込むリソースが空であることを検知しやすくするためのものです。リソースが空ではない時読み取ったデータの先頭にprefixに指定されたものを展開し、データの末尾にsuffixで指定されたものを展開します。
empty引数は逆に、リソースが空であるときに指定されたものを展開し代わりのデータとします。

P2164R5 views::enumerate

元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrateの提案。

以前の記事を参照

このリビジョンでの変更は以下のものです

  • views::enumrateの間接参照結果の型である集成体enumerate_resultについて、P2165R2の変更を仮定し単純な集成体のままとするとともにタプルインターフェースを追加した
  • ↑に伴って、enumrate_viewの参照型(reference)はenumerate_result、値型(value_type)はtupleとした(zipcartesian_prodcutとの一貫性向上)
  • P2165R2によれば、タプルライク型の各要素間にcommmon_referenceが存在していれば、そのタプルライク型とstd::tupleの間にもcommon_referenceが存在することを保証している
  • メンバ型count_typeindex_typeに変更

P2165R2を前提とすることで、R3で問題となったenumrate_viewの参照型(reference)と値型(value_type)のcommon_referenceについての問題を解決しています。

P2165R2 Compatibility between tuple, pair and tuple-like objects

std::pairと2要素std::tuple及びtuple-likeな型の間の非互換を減らし比較や代入をできるようにする提案。

前回の記事を参照

R1では、提案の対象はstd::pairと2要素std::tupleの間の非互換だけを対象にしていましたが、このリビジョンからはそれらと他のtuple-likeな型との間の非互換も対象にするようになりました。

tuple-like, pair-likeという説明専用のコンセプトを導入し、std::tuple, std::pairはそのコンセプトを満たす型のオブジェクトから構築・代入ができるようにします。pair-likeは2要素のtuple-likeとして定義され、ここでstd::tuplestd::pairの互換性が表現されます。

そして、tuple-like型に対してbasic_common_referencecommon_typeの特殊化を提供しておくことで、tuple-like型はstd::tuple, std::pairも含めた別のtuple-likeな型との間にcommon_referencecommon_typeを自然に持つようになります(対応する要素型についてcommon_referencecommon_typeが定義されている必要があります)。

constexpr std::pair p {1, 3.0};
constexpr std::tuple t {p}; // OK、現在できない

std::pair<int, double> pp{t}; //OK、現在できない

static_assert(std::tuple(p) == t);

// 2要素tupleとpairの間の比較が可能になる
static_assert(p == t);
static_assert(p <=> t == 0);

std::tuple<int,int> t = std::array {1, 2};  // OK、現在できない

std::tuple<int> t = std::array {1, 2};  // NG、サイズが異なる

std::map m{t, u};

static_assert(same_as<std::tuple<int>, range_value_t<decltype(views::zip(v))>>);
static_assert(same_as<std::tuple<int,int>, range_value_t<decltype(views::zip(v, v))>>);

auto x = true ? tuple{0,0} : pair{0,0}; // NG、2つの型は非互換

これらの変更は主に、rangesライブラリの拡張においてpairあるいはpeir-likeな型のシーケンスの各要素をtupleとみなして扱うことが容易にできるようにすることを念頭に置いています。例えば先程のenumrate_viewzip_viewなどがあります。

ただし、この変更はAPIの破壊的変更を伴う部分があります。

P2290R1 Delimited escape sequences

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

前回の記事を参照

このリビジョンでの変更は、提案する文言の古い注意事項を削除した事です。

EWGの議論では、提案する文言について改善が必要であるもののC++23に向けてこの提案を採択する方向で合意が取れているようです。

P2295R4 Support for UTF-8 as a portable source file encoding

C++コンパイラが少なくともUTF-8をサポートするようにする提案。

以前の記事を参照

このリビジョンでの変更は、SG16のガイダンスに従って提案する文言を改善した事です。

P2299R3 mdspans of All Dynamic Extents

提案中のstd::mdspanのCTAD対応についての問題を報告する文書。

以前の記事を参照

以前のリビジョンが問題の周知と解決案を募るものであったのに対して、このリビジョンからは提案する文言と設計や考慮事項を説明した提案になっています。

そしてこの提案では、mdspanbasic_mdspanエイリアスである現状を修正し、basic_mdspanmdspanにリネームすることを提案しています。それが分かれているのはシンプルで使いやすいインターフェースを提供するためでしたが、そもそもそれが決定されたのはC++17以前のCTADが存在していない時代でした。mdspanの各要素を指定するためのdextentの導入とCTADによって、basic_mdspanmdspanの分割は必要なくなったという主張です。

この提案による変更の例。

mdspan<T> m(data, 16, 64, 64);  // 現在
mdspan<T> m(data, 16, 64, 64);  // この提案

mdspan<T, dynamic_extent, dynamic_extent, dynamic_extent> f();  // 現在
mdspan<T, dextents<3>> f();  // この提案

mdspan<T, 3, 3> m;           // 現在
mdspan<T, extents<3, 3>> m;  // この提案

mdspan<T, 3, 3> f();           // 現在
mdspan<T, extents<3, 3>> f();  // この提案

mdspan<T, 16, dynamic_extent, 64> m;           // 現在
mdspan<T, extents<16, dynamic_extent, 64>> m;  // この提案

mdspan<T, 16, dynamic_extent, 64> f();           // 現在
mdspan<T, extents<16, dynamic_extent, 64>> f();  // この提案

P2300R0 std::execution

P0443R14のExecutor提案をベースにした、任意の実行コンテキストで任意の非同期処理を実行するためのフレームワークの提案。

これは、Executor提案にあるscheduler, sender, recieverという3つの抽象をベースとして、そのうえで任意の非同期処理を構成することができるようにするためのライブラリの提案です。

非同期アルゴリズム自体はP0443でも紹介されており、それは別の提案(P1897R3)に委ねられていましたが、P0443R14のLEWGにおけるレビューによっていくつかの設計変更が行われており、それに伴ってschedulersenderといった抽象の役割が変化したため、それを反映した非同期アルゴリズムを提案するとともにその設計詳細を記述しています。

基本的なサンプル

using namespace std::execution;

scheduler auto sch = get_thread_pool().scheduler();                           // 1

sender auto begin = schedule(sch);                                            // 2
sender auto hi_again = then(begin, []{                                        // 3
    std::cout << "Hello world! Have an int.";                                 // 3
    return 13;                                                                // 3
});                                                                           // 3
sender auto add_42 = then(hi_again, [](int arg) { return arg + 42; });        // 4

auto [i] = std::this_thread::sync_wait(add_42).value();                       // 5
  1. (例ではスレッドプールから)schedulerを取得する。schedulerは実行リソースを表現する軽量ハンドル。
  2. あるschedulerで一連の作業を開始するにはstd::execution::schedule()を呼び出す。これによって、そのscheduler上で処理を完了するsenderが得られる。
    • senderは非同期作業を記述し、その作業完了時にreciever(複数可)にシグナル(値、エラー、キャンセル)を通知する
  3. 非同期アルゴリズムによってsenderを生成し、非同期作業を構成する。std::execution::thenは入力senderinvocablefを受け取り、入力senderからのシグナルによってfを呼び出すsenderアダプタ。返されるsenderはその呼び出しの結果を通知する。
    • 例での入力senderschedule()からの直接のものなので値はなく(戻り値void)、受け取るinvocableは引数を取らない。生成されたsenderintを返す(完了時にint値を通知する)。
  4. ここでは、作業チェーンにさらに作業を追加している。ここのinvocableには前の処理が返したint値が送信され、ここではその値に42を加えて返している。結果はまたsenderとして得られるため、さらに任意の処理を任意の個数チェーンさせることができる。
  5. 構成した非同期パイプライン(非同期作業)全体を実行リソースに送信して、作業の完了を待つ準備が整った。ここまでの全ての作業は非同期であり、作業はまだ開始されていないかもしれない。作業を開始し完了をその場で待機するために、std::this_thread::sync_wait()を使用する。その結果は、std::optional<std::tuple<...>>で得られ、最後のsenderが値を返した場合はそれを有効値として保持し、キャンセルされた場合は空になり、エラーの場合は例外を送出する。

ここの例ではsenderを関数呼び出しによってチェーンさせていますが、rangeライブラリlikeにパイプライン演算子|)によって中間オブジェクトを省略しつつ直感的にチェーンさせることもできます。

非同期inclusive_scanのサンプル。

using namespace std::execution;

sender auto async_inclusive_scan(scheduler auto sch,                          // 2
                                 std::span<const double> input,               // 1
                                 std::span<double> output,                    // 1
                                 double init,                                 // 1
                                 std::size_t tile_count)                      // 3
{
  std::size_t const tile_size = (input.size() + tile_count - 1) / tile_count;

  std::vector<double> partials(tile_count + 1);                               // 4
  partials[0] = init;                                                         // 4

  return transfer_just(sch, std::move(partials))                              // 5
       | bulk(tile_count,                                                     // 6
           [=](std::size_t i, std::vector<double>& partials) {                // 7
             auto start = i * tile_size;                                      // 8
             auto end   = std::min(input.size(), (i + 1) * tile_size);        // 8
             partials[i + 1] = *--std::inclusive_scan(begin(input) + start,   // 9
                                                      begin(input) + end,     // 9
                                                      begin(output) + start); // 9
           })                                                                 // 10
       | then(                                                                // 11
           [](std::vector<double>& partials) {
             std::inclusive_scan(begin(partials), end(partials),              // 12
                                 begin(partials));                            // 12
             return std::move(partials);                                      // 13
           })
       | bulk(tile_count,                                                     // 14
           [=](std::size_t i, std::vector<double>& partials) {                // 14
             auto start = i * tile_size;                                      // 14
             auto end   = std::min(input.size(), (i + 1) * tile_size);        // 14
             std::for_each(output + start, output + end,                      // 14
               [=] (double& e) { e = partials[i] + e; }                       // 14
             );
           })
       | then(                                                                // 15
           [=](std::vector<double>& partials) {                               // 15
             return output;                                                   // 15
           });                                                                // 15
}
  1. これは、doubleのシーケンスを入力として、結果をdoubleのシーケンスに出力するもの
  2. 実行コンテキストを指定するschedulerを受け取る。schの指す実行コンテキスト上で処理を実行する。
  3. tile_countは生成される実行エージェントの数を制御する(すなわち、並列数)。
  4. 最初にアルゴリズムの実行に必要な作業領域を確保する。1つの実行エージェント毎にdouble1つ分の領域が必要。
  5. std::execution::transfer_justによって最初のsenderを作成する。このsenderは先程確保した作業領域を後続の作業に転送し、後続の作業を受け取ったscheduler(引数で渡されたsch)の指す実行コンテキスト上で実行することを指定する。
  6. sendersenderアダプタはパイプライン演算子|)による構成をサポートする(ほとんどのsenderアダプタは|によって構成可能)。std::execution::bulkによってtile_countで指定された数の実行エージェントを生成し次の作業を接続する。
  7. それぞれの実行エージェントは1つの(2引数)invocableを実行する。1つ目の引数は実行エージェントのインデックス(例えばスレッドプール内のスレッドインデックスなど、この例では[0, tile_count)内の単一整数値)、2つ目の引数は先程確保し転送された作業領域。
  8. まず、実行エージェントのインデックスに基づいて、このエージェントが担当する入力と出力の範囲を計算する。
  9. 次に、要素に対して順次std::inclusive_scanを実行する。すべての要素の合計である最後の要素を割り当てられた作業領域に保存する。
  10. 1つ目のbulk()でのすべての計算が完了すると、生成された実行エージェントそれぞれが担当範囲の要素の合計を作業領域に保存している。
  11. 次に作業領域の範囲に対してstd::inclusive_scanを実行する。これは単一の実行エージェントによって実行され、std::execution::thenによってその実行エージェントを作成する。
  12. then()sender|に隠蔽されている)とinvocableを受け取って、そのsenderから送信された値を用いてinvocableを実行する。ここでは、前段の処理結果である作業領域を表すstd::vector<double>が送信されてくる。
  13. ここでの処理の結果として、次の作業に渡すものを返す。ここではstd::inclusive_scan実行済みの作業領域を次に渡す。
  14. 1つ目のbulk()と同じ形状(実行エージェント数)で別の処理をバルク実行する。ここでは、他のタイル(別の実行エージェント)による結果(和)を統合するために部分和の値をスキャンしinclusive_scanを完了する。
  15. 最後に、async_inclusive_scan()は出力範囲であるstd::span<double>を送信するsenderを返す。このアルゴリズムの利用者は、呼び出し側でさらに別の処理を(ここで見たのと同様の方法で)チェーンさせることができ、あるいは任意のrecieverによって処理結果を得ることもできる。この関数が帰った時、処理全体は完了していないかもしれないし、始まってすらいないかもしれない。

これらの非同期アルゴリズムは特定の型に依存するものではなくscheduler, sender, recieverといった抽象にのみ依存しており、それらはコンセプトによって定義されています。これによって、単純な1スレッドやスレッドプール、GPU等の実行リソースを表現するschedulerさえ用意すれば、これらの例のような共通の操作によって任意の実行コンテキスト上で実行可能な非同期処理を構成することができます(GPUなど外部アクセラレータ上での実行はコンパイラの特別扱いが必要ではありますが)。

P2301R1 Add a pmr alias for std::stacktrace

std::basic_stacktracestd::pmrエイリアスstd::polymorphic_allocatorを用いるエイリアス)を追加する提案。

以前の記事を参照

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

この提案は既に議論を完了しており、次の全体会議で投票にかけられることが決まっています。

P2321R2 zip

<ranges>zip_view, adjacent_view, zip_transform_view, adjacent_transform_viewを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGのフィードバックを受けて設計や文言を改善した事と、difference_type, size_typeinteger-like-typeを考慮するようにしたことなどです。

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

P2322R3 ranges::fold

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

以前の記事を参照

このリビジョンでは、LEWGのレビューを受けて関数名と戻り値型についての議論を追加しています。

関数名については以前のリビジョンがfold_left/fold_rightとしていましたが、LEWGにおける投票で合意が取れなかったようで、いくつかの候補を提示しています。筆者の方は、メインの左畳み込みと右畳み込みの関数をfoldl, foldr、それらの初期値を取る関数をfoldl1, foldr1とすることを推しています。

戻り値型について、以前のリビジョンでは計算結果の値を直接返していましたが、計算を終了した位置のイテレータが欲しい需要があったようです。提案では、そのような関数をfold_whileとして別途追加し、イテレータと計算結果の組(となる構造体)を返すようにしています。また、初期値をとらないタイプの関数では入力範囲が空の場合を考慮してstd::optionalを返すようになっています。

P2340R1 Clarifying the status of the "C headers"

現在非推奨とされているCヘッダを相互運用の目的で使用可能なようにする提案。

以前の記事を参照

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

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

P2347R0 Argument type deduction for non-trailing parameter packs

関数テンプレートの実引数型推論時に、引数リスト末尾にないパラメータパックの型を推論できるようにする提案。

この提案のモチベーションはsource_locationの使い勝手を改善させることにあります。

例えば、多くの巷のログ関数はフォーマット文字列とログ出力したいものを受けて、次のような形式になっていることが多いです。

void log(string_view formatString, auto&&...args);

C++20以降、これを自然に拡張してsource_locationを取れるようにするために、次のように書きたくなります。

void log(string_view formatString, auto&&...args, source_location loc = source_location::current());

しかし、現在のC++はパラメータパックがある場合は引数リストの末尾になければならず、そうでない場合パックの型を推論できないためコンパイルエラーとなります。

これ以外の所でも、次のようなメリットが想定できます

  • ragens::transformranges::margeにN個のrangeを受け取るオーバーロードを追加して、既存の1or2個のrangeを受け取るものと引数の順序を一致させられる
  • std::visitにおいて、任意個数のvariantを受け取るのが引数の先頭であるならば、より直感的に使用できる

など、パラメータパックを置ける位置の制限を取り払うと、APIの設計と使用の柔軟性向上が期待できます。

この提案ではこのような制限を撤廃し、関数テンプレートにパラメータパックが1つ(1つだけ)ある場合にそのパックの要素数は、推論されていない関数引数の数からパックより後ろにあるデフォルト引数を持たない引数の数を引いたもの、と推論されるようにします。

void f(auto a, auto...b, auto c, auto d);
void g(auto a, auto...b, auto c, int d = 0);
void h(auto a, auto...b, int c = 0);

f(0, 0, 0, 0);    // bのサイズは1と推論
f(0, 0, 0, 0, 0); // bのサイズは2と推論
f(0, 0, 0);       // bのサイズは0と推論
g(0, 0);          // bのサイズは0と推論
g(0, 0, 0, 0);    // bのサイズは2と推論
h(0, 0);          // bのサイズは1と推論
h(0, 0, 0);       // bのサイズは2と推論

この提案によるこのような推論規則の変更は純粋に制限の解除であって、オーバーロード解決や関数テンプレートの半順序を変更するものではありません。この推論は純粋に実引数と仮引数の数だけを使って行われます。

この提案の問題点は明らかに、パックの後にあるデフォルト引数を持つ引数に対して値を提供できないことですが、そこを何とかしようとして得られるメリットはそれによって導入されるデメリット(コンパイル時間の増大、テンプレートの引数推論とオーバーロード解決の境界が曖昧になる)を下回るようです。デフォルト引数を持つ引数に値を提供したい場合は、以下のような簡易なオーバーロードを追加することによって行うことができます。

template <typename... T>
void f(T&&... args, source_location loc = {}) requires (!(std::same_as<T, source_location>||...));

void f(auto&&... args, source_location loc);

P2361R1 Unevaluated string literals

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

以前の記事を参照

このリビジョンでの変更は、実装可能性と以前の同じ目的の作業について追記した事です。

P2368R1 2021 Spring Library Evolution Polls

2021年の春(4月から6月にかけて)に予定されている、LEWGでの全体投票の予定表。

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

基本的にはLEWGでの作業を完了してLWGへ転送することを確認するための投票です。

P2370R0 Stacktrace from exception

現在投げられている例外オブジェクトに基づくスタックトレースを取得できるようにする提案。

例えば次のように、一つのtryブロックで複数の例外を投げうる関数が呼ばれていると対応するcatchブロックではどこで例外が投げられたのか通常わかりません。

#include <iostream>
#include <stdexcept>
#include <string_view>

// 例外を投げうる関数
void foo(std::string_view key);
void bar(std::string_view key);

int main() {
  try {
    // どちらかが例外を投げたとすると
    foo("test1");
    bar("test2");
  } catch (const std::exception& exc) {
    std::cerr << "Caught exception: " << exc.what() << '\n';
    // 出力例
    // Caught exception: map::at
  }
}

C++23よりスタックトレースライブラリが標準ライブラリに導入されていますが、現在位置に対応するスタックトレースを取得することはできても、例外に応じたスタックトレースを取得することができません。

この提案は、それができるようなインターフェースをstd::basic_stacktraceに追加して、現在の例外ベースのスタックトレースを取得できるようにするものです。

#include <iostream>
#include <stdexcept>
#include <string_view>
#include <stacktrace>   // <---

void foo(std::string_view key);
void bar(std::string_view key);

int main() {
  try {
    foo("test1");
    bar("test2");
  } catch (const std::exception& exc) {
    std::stacktrace trace = std::stacktrace::from_current_exception();  // <---
    std::cerr << "Caught exception: " << exc.what() << ", trace:\n" << trace;
    // 出力例
    // Caught exception: map::at, trace:
    //  0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
    //  1# bar(std::string_view) at /home/axolm/basic.cpp:6
    //  2# main at /home/axolm/basic.cpp:17
  }
}

std::basic_stacktracestd::stacktrace)の静的メンバ関数としてfrom_current_exception()を追加し、それによって現在投げられている例外ベースのスタックトレースを取得できるようにします。後の扱いは通常のスタックトレースと変わりありません。これによって、デバッガを起動せずともどこで落ちたかがわかるようになります。

また、別の例として、terminate()が呼ばれた際にどこで呼ばれたのかの診断を容易にすることができます。

void broken_function() noexcept {
  std::unordered_map<std::string, int> m;
  [[maybe_unused]] auto value = m.at("non-existing-key");
}

int main() {
  std::set_terminate([] {
    auto trace = std::stacktrace::from_current_exception();
    if (trace) {
        std::cerr << "Terminate was called with an active exception:\n"
                  << trace << std::endl;
    }
  });

  broken_function();
}

特に、意図せず誤って実装されたnoexcept関数で例外が投げられた時など、一見どこで投げられているか発見することが難しい場合でも、デバッガレスでそれを知ることができます。

この変更は例外オブジェクトにスタックトレースを仕込む必要があるため、ABI破壊をせねば達成できないように思われますが、どうやらABIの変更をせずとも実装できるようです。それをしているライブラリにlibsfeがあります。

ただし、この変更によって例外オブジェクトのサイズは増大するため、例外発生時のメモリ使用量が増大します。そのため、これを使用する/しないを切り替えられるようにするためにcapture_stacktraces_at_throw(bool enable = true);という関数が用意されています。

P2380R1 reference_wrapper Associations

Networking TSで用意されている、associated_allocatorassociated_executorに対して、reference_wrapper<T>の特殊化を追加する提案。

以前の記事を参照

このリビジョンでの変更は、associated_allocatorassociated_executorを指定する際の誤りを修正した事、associated_allocatorassociated_executortypeメンバを指定するためのエイリアスassociated_allocator_t, associated_executor_tを追加した事などです。

P2384R0 2021 Spring Library Evolution Poll Outcomes

2021年の春(4月から6月にかけて)に行われた、LEWGの全体投票の結果。

以下の9つの提案が投票にかけられ、どれもLWGに転送されることが可決されています。また、その際に寄せられたコメントが記載されています。

P2385R0 C++ Standard Library Issues to be moved in Virtual Plenary, June 2021

6月の会議で採択されたライブラリのIssue解決の一覧。

解決されたIssueは36件です。

  1. 2774. std::function construction vs assignment
  2. 2818. "::std::" everywhere rule needs tweaking
  3. 2997. LWG 491 and the specification of {forward_,}list::unique
  4. 3410. lexicographical_compare_three_way is overspecified
  5. 3430. std::fstream & co. should be constructible from string_view
  6. 3462. §[formatter.requirements]: Formatter requirements forbid use of fc.arg()
  7. 3481. viewable_range mishandles lvalue move-only views
  8. 3506. Missing allocator-extended constructors for priority_queue
  9. 3517. join_view::iterator's iter_swap is underconstrained
  10. 3518. Exception requirements on char trait operations unclear
  11. 3519. Incomplete synopses for <random> classes
  12. 3520. iter_move and iter_swap are inconsistent for transform_view::iterator
  13. 3521. Overly strict requirements on qsort and bsearch
  14. 3522. Missing requirement on InputIterator template parameter for priority_queue constructors
  15. 3523. iota_view::sentinel is not always iota_view's sentinel
  16. 3526. Return types of uses_allocator_construction_args unspecified
  17. 3527. uses_allocator_construction_args handles rvalue pairs of rvalue references incorrectly
  18. 3528. make_from_tuple can perform (the equivalent of) a C-style cast
  19. 3529. priority_queue(first, last) should construct c with (first, last)
  20. 3530. BUILTIN-PTR-MEOW should not opt the type out of syntactic checks
  21. 3532. split_view<V, P>::inner-iterator<true>::operator++(int) should depend on Base
  22. 3533. Make base() const & consistent across iterator wrappers that supports input_iterators
  23. 3536. Should chrono::from_stream() assign zero to duration for failure?
  24. 3539. format_to must not copy models of output_iterator<const charT&>
  25. 3540. §[format.arg] There should be no const in basic_format_arg(const T* p)
  26. 3541. indirectly_readable_traits should be SFINAE-friendly for all types
  27. 3542. basic_format_arg mis-handles basic_string_view with custom traits
  28. 3543. Definition of when counted_iterators refer to the same sequence isn't quite right
  29. 3544. format-arg-store::args is unintentionally not exposition-only
  30. 3546. common_iterator's postfix-proxy is not quite right
  31. 3548. shared_ptr construction from unique_ptr should move (not copy) the deleter
  32. 3549. view_interface is overspecified to derive from view_base
  33. 3551. borrowed_{iterator,subrange}_t are overspecified
  34. 3552. Parallel specialized memory algorithms should require forward iterators
  35. 3553. Useless constraint in split_view::outer-iterator::value_type::begin()
  36. 3555. {transform,elements}_view::iterator::iterator_concept should consider const-qualification of the underlying range

P2386R0 Core Language Working Group "ready" Issues for the June, 2021 meeting

6月の会議で採択されたコア言語のIssue解決の一覧。

解決されたIssueは9件です。

  1. 2397. auto specifier for pointers and references to arrays
    • 配列のポインタ/参照の宣言において、autoによる推論が効くようにした。 cpp int a[3]; auto (*p)[3] = &a; // OK、一部の実装では実装済 auto (&r)[3] = &a; // OK、一部の実装では実装済
  2. 2448. Cv-qualification of arithmetic types and deprecation of volatile
    • 算術型(arithmetic types)という用語が、整数型や浮動小数点数型のCV修飾も含むようにした。
    • 以前は含んでいなかったため、矛盾が生じていた。
  3. 2458. Value category of expressions denoting non-static member functions
    • 非静的メンバ関数のアドレスを取得する式の結果(&X::f)が左辺値となるようにした。
    • 以前は右辺値とされていた。
  4. 2465. Coroutine parameters passed to a promise constructor
    • コルーチンに渡された引数をコルーチン内部およびPromise型の初期化で使う際、コピーされていることを明確化した
    • 以前からなっていたが、Promise型の初期化についてカバーしきれていなかったのを修正
  5. 2466. co_await should be a single evaluation
    • co_awaitの呼び出しを並べ替えたり省略したりすることを禁止した。
    • 一つのco_await式は正確に一度評価される
  6. 2474. Cv-qualification and deletion
    • delete式において、その静的型と動的型がsimilarの関係であれば未定義動作を起こさなくなった
    • 以前の記述では、CV修飾の違いも未定義となっていた。
  7. 2477. Defaulted vs deleted copy constructors/assignment operators
    • コピー/ムーブコンストラクタがdefault定義される時と、delete定義される時の規定の矛盾の解消
  8. 2479. Missing specifications for consteval and constinit
    • consteval/constinitのキーワードについて、言及されるべき所で言及されていなかったのを正した
  9. 2481. Cv-qualification of temporary to which a reference is bound
    • コピー省略のためのtemporary materialization conversionに伴って、意図せずCV修飾が落とされていたのを修正

P2387R0 Pipe support for user-defined range adaptors

ユーザー定義のRangeアダプタに対して、パイプライン演算子|)サポートを簡単に行えるユーティリティを提供する提案。

標準のRangeアダプタは関数呼び出しによっても、パイプライン演算子によっても呼び出すことができ、その二つの呼び出しは入力となるrangeの受け取り方以外は同じ効果となることが規定されています。

// R, R1, R2を任意のRangeアダプタオブジェクトとする

// 入力のrange(viewable_range)
viewable_range auto vr = views::iota(1);

// この2つの呼び出しは同じ効果となる
view auto v1 = R(vr);
view auto v2 = vr | R ;

// Rangeアダプタが増えた時でも同様(以下3つは同じ効果)
// 適用順は右結合
view auto v3 = R2(R1(vr));
view auto v4 = vr | R1 | R2;
view auto v5 = vr | (R1 | R2);

これらのR, R1, R2のような、1引数のRangeアダプタのことをrange adaptor closure objectと呼びます。

Rangeアダプタはさらに追加の引数をとることができ、その時でも先ほどの例と同様の保証があります。この時、追加の引数だけを先に受け取って呼び出すこともでき(rangeを受け取らないで呼び出すことができ)、その結果はrange adaptor closure objectとなります。

// Rangeアダプタが追加の引数を取るときも同様に書く事ができる
// その時でも、この3つは同じ効果となる
view auto v6 = R(vr, args...);
view auto v7 = R(args...)(vr);
view auto v8 = vr | R(args...)

range adaptor closure objectとこれら追加の引数を受け取るものを合わせて、range adaptor objectと呼びます。range adaptor objectはカスタマイゼーションポイントオブジェクトであり、1引数しか取らない場合はrange adaptor closure objectでもあります。

これらの保証はパイプラインで使用する時を考えるとユーザーにとっては便利なものですが、実際に実装しようとすると大変です。特に、range adaptor closure objectが右結合する所(R1 | R2range adaptor closure objectを返す)とか、追加の引数をとるrange adaptor objectに追加の引数だけを渡してもrange adaptor closure objectを返さねばならない所などが厄介です。

実際にはこれらのことはrange adaptor object全てに共通しているため、実装の大部分を共有することができます。Range-v3をはじめとする既存実装やGCC/MSVCなどの標準実装は、多少の差はあれど内部でそのようになっています。

この提案は、そのような共通実装の仕組みを標準化しユーザーに提供することで、ユーザー定義されたRangeアダプタに対してパイプラインサポートとそれに纏わる保証を簡易に実装できるようにしようとするものです。

問題は、range adaptor closure objectを実装する部分とrange adaptor object(からrange adaptor closure objectを除いた部分)を実装する部分の2つに分割することができます。

range adaptor closure objectの実装では、rangeを受け取った時の関数呼び出しとパイプライン演算子適用が同じ効果になり、かつrange adaptor closure objectを受け取った時に、それと自身を内包したrange adaptor closure objectとなるプロクシオブジェクトを返す必要があります。

残りのrange adaptor objectの実装では、追加の引数だけを受け取った時にそれら引数と自身をラップしたrange adaptor closure objectとなるプロクシオブジェクトを返し、それに対して関数呼び出しでrangeを渡した時と、最初にrangeと追加の引数をまとめて受け取った時で同じ効果となるようにしなければなりません。

この提案ではまず、range adaptor closure objectの実装のためにstd::ranges::range_adaptor_closure<T>というクラスを用意します。これはCRTPによって利用し、range adaptor closure object型はこれを継承することで、パイプライン演算子サポートを受けることができます(関数呼び出し演算子は実装しておく必要があります)。

namespace std::ranges {
  template<class D>
    requires is_class_v<D> && same_as<D, remove_cv_t<D>>
  class range_adaptor_closure { 

    // R | C == C(R) のサポート
    template <typename Range>
      requires /**/
    friend constexpr auto operator|(Range&& r, D& self) {
      return self(std::forward(r)); // Dの関数呼び出し演算子を使って実装
    }
    
    // C | D でrange adaptor closure objectを生成するサポート
    template <typename Lhs, typename Rhs>
        requires /**/
    constexpr auto operator|(Lhs lhs, Rhs rhs) {
      // range adaptor closure objectラッパで包んで返す
      return raco_proxy<Lhs, Rhs>(std::move(lhs), std::move(rhs));
    }
  };
}

例えばこんな感じの実装になります。

次に、range adaptor objectの実装のためにstd::bind_back()を用意します。std::bind_back(f, ys...)(xs...)のような呼び出しは、f(xs..., ys...)と等価になります。

これらを使用して、自前のRangeアダプタを例えば次のように実装できます。

namespace myns {

  // オレオレjoin_View
  template<std::ranges::view V>
  class join_view;

  // オレオレtransform_View
  template<std::ranges::view V, typename F>
  class transform_view;

  // オレオレjoin_Viewに対するrange adaptor closure object
  struct join_closure : std::ranges::range_adaptor_closure<join_closure> {

    template<std::ranges::viewable_range R>
    constexpr auto operator()(R&& r) const {
      return join_view{std::forward<R>(r)};
    }
  };

  // オレオレtransform_Viewに対するrange adaptor object
  struct transform_adopter : std::ranges::range_adaptor_closure<transform_adopter> {

    template<std::ranges::viewable_range R, typename F>
      requires std::invocble<F&, std::ranges::range_reference_t<R>>
    constexpr auto operator()(R&& r, F&& f) const {
      return transform_view{std::forward<R>(r), std::forward<F>(f)};
    }

    template<typename F>
    constexpr auto operator()(F&& f) const {
      return std::bind_back(*this, std::forward<F>(f));
    }
  };

  // range adaptor object定義
  namespace views {
    inline constexpr join_closure join{};

    inline constexpr transform_adopter transform{};
  }
}

この提案では、range adaptor objectの実装を簡易化するために最大限のことをしていません。例えば、別のラッパ型を用意してさらに簡易化することはできるはずです。これは将来的に言語機能や他のアプローチによって解決できるはずなのでここで導入しても将来的に不要となる可能性が高い、という判断のようです。

P2388R0 Abort-only contract support

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

C++20に導入が決定していた契約プログラミングコントラクト)サポートは、様々な問題があったため削除されました。その後、問題点を整理し機能を絞ったMVP (P2182R1)と呼ばれる部分を先んじて導入する事が提案されていました。

現在のコントラクトサポートの議論の方向性は、論争を引き起こしている部分の多くはコントラクトフレームワークの2次的な機能の部分であり、そのような部分が1次的な機能である「プログラマがプログラムのバグと思われるものを伝える機能」の追加を妨げたり妨げられる事がないように、コントラクトの最小の機能セットから導入を始めていこうとしています。

ただ、それを選別したはずのP2182R1の部分さえ論争を引き起こし合意が取れなかったようです。

この提案は、P2182R1からさらに機能を絞り込み、契約プログラミングの最小の機能を早期にC++に導入することを目指すものです。

この提案が導入を目指している機能は次のものです。

  1. [[pre: condition]]による事前条件
  2. [[post: condition]]による事後条件
  3. [[assert: condition]]による関数内アサーション
  4. 2つのモード
    • 全無視 : コンパイラは契約の式の有効性だけを検証し、実行時に影響を与えない
    • 実行時チェックとアボート : 全ての契約を実行時にチェックする。違反した場合契約違反ハンドラが呼び出され、その後std::abort()する
    • これらを翻訳単位ごとに切り替える実装が許可される

この提案には以前にあった、assertion levelcontinuation mode、契約違反ハンドラのカスタマイズなどは含まれていません(これらがまさに物議を醸していた部分です)。

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

int select(int i, int j)   // 最初の宣言
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]];      // `r`は戻り値
  
int select(int i, int j);  // 再宣言では、同じ契約を指定するか、何も指定しない
                          
int select(int i, int j)   // 定義
{
  [[assert: _state >= 0]];
  
  if (_state == 0) return i;
  else             return j;
} 

P2391R0 C23 Update Report

C23の変更点(と今後採択されうる変更)をまとめた文章。

SG22が設立されるなど、CとC++の相互互換性にはWG21とWG14双方が注意を払っており、それを受けてC23の変更点をWG21で周知するための提案です。

C23の変更点がわかりやすくまとまっているので、C23に興味のある人は覗いてみると良いかもしれません。

P2392R0 Pattern matching using “is” and “as”

現在のパターンマッチングをクリーンアップし、使用可能な所を広げる提案。

この提案はP1371R3で提案されている現在のパターンマッチングをベースに、パターンマッチング構文が局所的な言語サブセットにならないようにisas[]を用いた構文への変更とそれを他のところ(ifrequiresなど)でも使用できるようにするものです。

constexpr bool even (auto const& x) { return x%2 == 0; } // given this example predicate

// xは何でもいい
void f(auto const& x) {
  inspect (x) {
    i as int             => std::cout << "int " << i;
    is std::integral     => std::cout << "non-int integral " << x;
    [a, b] is [int, int] => std::cout << "2-int tuple " << a << " " << b;
    [_, y] is [0, even]  => std::cout << "point on x-axis and even y" << y;
    s as string          => std::cout << "string \"" + s + "\"";
    is _                 => std::cout << "((no matching value))";
  }
}

inspect式を使用しているところなど基本的なところに変更はありませんが、型に対するマッチングをis, asを用いて直感的に書く事ができる他、[]を用いて構造化束縛による分解とマッチングを同時に行う事ができます。_ワイルドカードパターンを表します。

  • x is C : 型や値を問わないパターンの表現に使用できる構文。Cは特定の型や値、型の述語(コンセプト)や値の述語を使用可能。
  • x as T : Tへのキャスト可能パターンの表現に使用できる構文。Tは特定の型か型の述語のいずれか。

そして、このis, asによるマッチング構文はinspect式の外側でも使用可能とされます。

void f(auto const& x) {
  if (auto i as int = x)                  { std::cout<<"int"<<x;}
  else if (x is std::integral)            { std::cout << "non-int integral " << x; }
  else if (auto [a, b] is [int, int] = x) { std::cout << "2-int tuple " << a << " " << b; }
  else if (auto [_, y] is [0, even] = x)  { std::cout << "point on x-axis and even y " << y; }
  else if (auto s as string = x)          { std::cout<<"string\""+s+"\"";}
  else                                    { std::cout << "((no matching value))"; }
}

あるいはrequires式(節)

void g(auto const& x) requires requires{x as int;}
                      { std::cout << "int " << x; }

void g(auto const& x) requires (x is std::integral)
                      { std::cout << "non-int integral " << x; }

void g(auto const& x) requires (x is [int, int])
                      { auto [a, b] = x; std::cout << "2-int tuple " << a << " " << b; }

void g(auto const& x) requires (x is [0, even])
                      { auto [_, y] = x; std::cout << "point on x-axis and even y " << y;

void g(auto const& x) requires requires{x as string;}
                      { auto s = x as string; std::cout << "string \"" + s + "\""; }

void g(auto const& x) { std::cout << "((no matching value))"; }

変数宣言

std::pair<int, std::pair<int,int>> data;

// 分解とマッチング
auto [a, [_, c]] = data;
if (data is [_, [1, _]] ) {...}

// 上記の複合構文
if (auto&& [a, [_, c]] is [_, [1, _]] = data) {...}
// is によるマッチがtrueとなれば、ifによる判定もtrueとなる


// C++20の変数宣言
int a  = f();
std::integral auto b = g();

// isを用いた変数宣言
auto a is int = f();  // f()の戻り値型がintである時有効
auto b is std::integral = g();


std::variant<std::pair<int,int>, std::string> v;

// vが1つ目の要素(std::pair<int,int>)を有している場合にキャスト可能
// そうでない場合、実行時例外
auto&& [a, b] as std::pair<int,int> = v;
// もう片側
auto&& s as std::string = v;

if (auto&& [a, b] as pair<int,int> = v) {...}
// as によるマッチがtrueとなれば、ifによる判定もtrue

// isを用いた変数宣言
auto a as int = f();  // f()の戻り値型がintに変換可能である時有効

// こうかくと
auto [a, b] = v as std::pair<int,int>;
// これと等価
auto [a, b] = std::get<0>(v);

この提案では、このようなネストしたパターン指定を可能にし構造化束縛と一貫させるために、同様の構文を構造化束縛においても許可することを提案しています。

このisas演算子でもあり、任意のクラス型に対して演算子オーバーロードによってその動作をカスタマイズする事ができます。

// std::variantでオーバーロードする例

template<typename... Ts>
constexpr auto operator is(std::variant<Ts...> const& x ) {
  return x.index();
}

template<size_t I, typename... Ts>
constexpr auto operator as(std::variant<Ts...> const& x ) -> auto&& {
  return std::get<I>( x );
}

これらのisasはグローバルに予約されたキーワードではなく文脈依存キーワードです。

筆者の方は既存の文法などを調査した上でこれらの構文を提案しているため、実現可能性は高そうです。

P2393R0 Cleaning up integer-class types

整数型とみなせる型を指すinteger-classの定義を修正する提案。

integer-class型は標準ライブラリのイテレータ/rangeの文脈で整数型として使用可能な型を指すものですが、その規定にはまだいくつかの問題があるようです。

  • zip_view, join_viewなど2つのrangeを合成するタイプのdifference_typeは、2つのrangedifference_typecommon_typeによって決定される、integer-class型同士はcommon_typeを持つことを要求されていない。
  • integer-class型同士は相互に変換可能ではなく、比較可能でもない。
    • ranges::equalなど、2つのrangeから取得した距離を比較する必要がある処理を実装できない
  • integer-class型の表現可能な値の範囲、および整数型への/からの変換は定義されていない
  • <ranges>/<iterator>/<algorithm>の多くの所ではrandom_access_iteratorの操作がdifference_typeでのみ動作するように規定されており、integer-class型を考慮していない
    • そのようなところでは、ranges::range_difference_t/iter_difference_tが使用されるために、integer-class型からそれらの整数型への変換が必要になる(がそれは定義されていない)。

この提案は、これらの問題の解決を図るものです。

  • integer-classは2の補数でありどの組み込み型よりも表現可能な値の幅が広い、と規定
  • integer-classは非クラス型となることを許可する
  • 2つのinteger-class型の間には常にcommon_typeが存在することを規定し、元のinteger-classが両方とも符号付整数型の場合はそのcommon_typeもまた符号付となることを規定
  • integer-class型は同じ符号性を持ちより大きな幅のinteger-class型に暗黙変換可能であり、全てのinteger-class型に明示的変換可能であることを規定する
  • 一方が他方に暗黙変換可能なinteger-class型の間での二項演算を許可する。

これらのことをベースに<ranges>/<algorithm>の規定に変更を加え、difference_typeinteger-class型が相互に変換しつつ自然に使用可能であるようにします。

これらの変更によって、問題の解決を図ります。

P2395R0 WG21 2021-06 Virtual Meeting Record of Discussion

6月の全体会議の議事録。

投票の際にどのような意見をだれが発言したかなどが記録されています。

P2396R0 Concurrency TS 2 fixes

COncurrency TSv2を目指しているいくつかの提案について、本題と関係の薄い微修正の提案。

  • 配置するヘッダの変更
  • 機能テストマクロの追加

いずれもまだexperimentalなものです。

P2397R0 SG16: Unicode meeting summaries 2021-04-14 through 2021-05-26

SG16(Unicode and text study group)のオンラインミーティングの議事録。

どのような議論においてだれがどんな発言をしたかが記録されています。

P2400R1 Library Evolution Report: 2021-02-23 to 2021-05-25

2021年2月後半から5月後半にかけての、LEWGでのミーティングについてのまとめ。

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