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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で34本あります。

P1068R4 : Vector API for random number generation

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

既存の分布生成器はスカラーAPI(1度に1つの乱数しか取得できないAPI)しか備えておらず、乱数エンジンが状態を持つことと合わさってコンパイラによる最適化を妨げており、1度に多量の乱数を取得する場合に効率的ではなくなることがあります。一方で、既存の擬似乱数生成アルゴリズムには多数の乱数生成の際にSIMDを利用するような形に書き換えて効率化する事ができるものが多数あります。
そのような用途のためのベクターAPI(1度に多数の乱数を取得できるAPI)を別途追加し、実装がその中で各種分布生成器やエンジンの特性によって最適な実装を行えるようにし、またコンパイラに最適化の機会を提供可能にすることがこの提案の目的です。

当初はSIMDタイプのAPIも同時提案していたようですが、途中でそれは別の提案に分離されたため、この提案ではイテレータによるベクターAPIの追加のみを提案しています。

std::array<float, N> array; 

std::mt19937 gen(777);  // 乱数エンジン
std::uniform_real_distribution dis(1.f, 2.f); // 分布生成器

// 範囲を乱数列で初期化する、現在はこう書くしかない
for (auto& el : array) {
  el = dis(gen);
}

// この提案によるAPI、イテレータ範囲を乱数列で初期化する
dis(array.begin(), array.end(), gen);

これは必ずしも対応するforループによる範囲の要素毎の初期化コードと同じ効果とはならず、より効率的に実装される可能性があります。

P1184R2 : A Module Mapper

モジュール(特にそのインターフェース)の名前とその中間生成物の間の名前のマッピングやモジュールのビルドについてGCCのTS実装に基づいて書かれた報告書。

C++20のモジュールはそのモジュール名とファイル名の間には何ら関連付けの規則が無く、ビルドの際に必要とされるコンパイル済みモジュールインターフェース(モジュール名とexportされているエンティティ名を記録した中間生成物)の命名に関しても当然何ら規則がありません。この論文はGCCのモジュールTSに基づく実装経験から得られたモジュール名と中間生成物の名前のマッピングのための1つのプロトコルについて説明されているものです。

P1272R3 : Byteswapping for fun&&nuf

バイトスワップ(バイトオーダー変換)のための関数std::byteswap()の提案。

C++20より<bit>ヘッダが導入されpopcountやbit rotation等の基本的なビット操作のための関数が標準で用意されるようになりました。しかし、そこにはバイトスワップ(バイトを逆順にする操作)はありません。現代のほとんどのCPUはバイトスワップのための命令を持っており、それが無い環境でも使用可能なビット演算手法が存在しています。それらを標準で用意しておくことで、よりポータブルかつ効率的に(結果として1命令以下で)バイトスワップを行えるようにしようという提案です。

// 提案されている宣言
namespace std {
  constexpr auto byteswap (integral auto value) noexcept;
}

int main() {
  auto r = std::byteswap(0x1234CDEF);
  // r == 0xEFCD3412
}

LWGでの合意は反対無しで取れていてほとんどC++20入りが決まっていたようでしたが、パディングビット(C++標準としては1byte=8bitではなく、奇数幅整数型の実装がありうる)の扱いについてCWGでの議論が必要となりその文言調整に手間取った結果C++20には間に合わなかったようです。おそらくC++23には入りそうです。

P1478R4 : Byte-wise atomic memcpy

アトミックにメモリのコピーを行うためのstd::atomic_load_per_byte_memcpy()/std::atomic_store_per_byte_memcpy()の提案。

この提案では、主にSeqlockと呼ばれるロック手法におけるユースケースを主眼にそこで有用となるアトミックなmemcpyについて提案しています。

// Seqlockにおける読み手の処理の一例
// ロック対象のshared_dataをdataに読み取る
do {
  // クリティカルセクション開始時点のシーケンス番号取得
  int seq1 = seq_no.load(std::memory_order_acquire);

  // データの読み取り
  // shared_dataがatomic変数ではない場合、この読み取り操作はフェンスを越えてseq2の後に変更されうる
  data = shared_data;

  atomic_thread_fence(std::memory_order_acquire);  // seq2の読み取り順序はここより前に変更される事はない

  // クリティカルセクション終了時点のシーケンス番号取得
  int seq2 = seq_no.load(std::memory_order_relaxed);

  // 開始時点と終了時点のシーケンス番号が異なる、あるいは開始時点のシーケンス番号が奇数であるならクリティカルセクション中に書き込みが起きている
} while (seq1 != seq2 || seq1 & 1);

// dataはshared_dataからアトミックにロード完了している事が期待されるが・・・

shared_dataはロック対象である事もあり普通はatomicでは無いはずで、その場合はこのループが正常に終了した場合でもdata変数の内容はデータ競合を起こした結果を読み取っている可能性があります。つまり、データ競合を回避するためにクリティカルセクション中で読み取られたデータは書き込みが起きている場合に破棄されるようになっているにも関わらず、このクリティカルセクション中の読み取り操作はアトミックに行われなければなりません。

多くの場合Seqlock対象のデータはtrivially copyableなオブジェクトであり、その場合そのコピーにはmemcpyが使用されます。しかし、memcpyはアトミックでは無いので、対象オブジェクトをロックフリーなアトミックサブオブジェクトに分解した上でそれらを1つづつコピーすることになります。

// 上記クリティカルセクション中のコピーは例えばこうなる
for (size_t i = 0; i < sizeof(shared_data); ++i) {
  reinterpret_cast<char*>(&data)[i] =
      std::atomic_ref<char>(reinterpret_cast<char*>(&shared_data)[i]).load(std::memory_order_relaxed);
}
std::atomic_thread_fence(std::memory_order_acquire);

この提案によるatomic_load_per_byte_memcpyはこのようなコピー(およびフェンス)を1文で自明に行うものです。

Foo data;  // trivially copyableだとする

do {
  int seq1 = seq_no.load(std::memory_order_acquire);  // このロードより後のあらゆる読み書き操作はこの前に順序変更されない

  // データの読み取り
  std::atomic_load_per_byte_memcpy(&data, &shared_data, sizeof(Foo), std::memory_order_acquire); // このロードより後のあらゆる読み書き操作はこの前に順序変更されない

  int seq2 =  seq_no.load(std::memory_order_relaxed); // 順序変更について制約はないが、先行する2つのacquireロードを超えて順序変更されることはない
} while (seq1 != seq2 || seq1 & 1);

// dataはshared_dataからアトミックにロード完了している

atomic_load_per_byte_memcpyはメモリオーダー指定を受け取りコピー元に対してアトミックにアクセスする事以外はmemcpyと同様に動作します。これを用いると最初の問題のあるSeqlockの読み取り操作はより単純にそして正しく書く事ができるようになります。誤解の種でもあったatomic_thread_fenceを使用する必要も無くなります。

atomic_store_per_byte_memcpyはこのケースの読み取りに関して双対的なもので、memcpyにおいてコピー先へのアクセスをアトミック化します。Seqlockの書き手側の処理などで使用することを想定しているようです。

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

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

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、機能テストマクロが別の提案に分離されたこと、P1641によるフリースタンディング機能指定の文言への変更、uses-allocator構築に関する2つの機能の追加、などです。

P1659R1 : starts_with and ends_with

任意の範囲に対して動作するstd::ranges::starts_with/std::ranges::ends_withの提案。

C++20にてstd::stringstd::string_viewに対してstarts_with/ends_withメンバ関数が追加され、文字列の先頭(末尾)が指定の文字列で始まっている(終わっている)かをbool値で得られます。それらをより一般化して、任意の範囲について同じ事ができるようにするのがこの提案です。

// 共にtrueとなる
bool s = std::ranges::starts_with(std::views::iota(0, 50), std::views::iota(0, 30));
bool e = std::ranges::ends_with(std::views::iota(0, 50), std::views::iota(20, 50));

また、これらは第3引数に述語オブジェクトを渡す事で行われる比較をカスタマイズできます。例えば、先頭や末尾がある値以下であるか?のようなチェックができるようになります。

// 共にtrueとなる
bool s = std::ranges::starts_with(std::views::iota(0, 50), {-1, 0, 1, 2, 3}, std::ranges::greater);
bool e = std::ranges::ends_with(std::views::iota(0, 50), {46, 47, 48, 49, 50}, std::ranges::less);

また、さらにその後ろに各範囲の要素についての射影(Projection)を渡すこともできます。ただ残念なことに、これは普通の関数なので|で繋ぐことが出来ません。

この提案は既にLWGでのレビューをほとんど終えていて、どうやら次のリビジョンとそのレビューでもってC++23入りが確定する様子です。

P1679R3 : String Contains function

std::string, std::string_viewに、指定された文字列が含まれているかを調べるcontains()メンバ関数を追加する提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は機能テストマクロのの追加と大文字小文字の取り扱いについての説明の追記です。

また、LWGでの全会一致でC++23への導入が承認されたようなので何事もなければC++23に入りそうです。

P1726R4 : Pointer lifetime-end zap

Pointer lifetime-end zapと呼ばれる問題の周知とそれについてのフィードバックを得るための報告書。

Pointer lifetime-end zapとは、現在のC++のポインタセマンティクスが寿命が尽きたオブジェクトを指しているポインタの存在を認めない(その再利用が禁止されている)事で、これによってポインタ再利用を用いると効率的に書けるタイプのアルゴリズム(特に並行アルゴリズム)が未定義動作に陥っている問題の事のようです。

この文書は、Pointer lifetime-end zapは規格としてどのように表現されているかに始まり、なぜそうなっているのか?どこで問題になるのか?そして、この問題をどう解決するのか?を記したものです。
主目的は委員会のメンバに向けてこの問題の周知を図り問題そのものや解決についてのフィードバックを得ることで、何かを提案するものではないですが、この文書を受けてSG1(Concurrency Study Group)はこれを削除することを目指しており、LWGもこの問題の解決に興味を持っているようです。

(内容は私には難しいので識者の方には是非解読をお願いしたい所存)、

P1864R0 : Defining Target Tuplets

ビルドシステムがそのターゲットとなる環境を決めるための名前の列であるTarget Tupletsについて、基礎的な定義を標準に追加する提案。

ビルドシステムの担う最も困難な仕事の一つは、あるプラットフォームに向けてコンパイルする際にどのアーキテクチャ、ツールチェーン、OS、そして実行環境を使用するのかを指定することです。この名前は実装によって好き勝手に選択されており、ツールのベンダーによって異なっています。
この提案は、それらの中でも多少広く使用されているclangの定めるTarget Tripleを基にしてその標準的なものを定義することを目指すものです。

Target Tupletsはビルドのために必要となる次の4つの情報の列として規定されます。

  • Architecture
    • CPUの命令セット(ISA)
    • x86_64, armv8など
  • Vendor
    • C++ツールチェーンを提供する事業体名(組織、個人、PC、なし)
    • apple, nvidia, noneなど
  • Operating system
    • linux, win32など
  • Environment
    • 実行ファイルの形式、ABI、ランタイムのいずれか
    • macho, elfgnu, eabiandroidなど

これらの具体的な名前をこの順番に-で繋いだ文字列がTarget Tupletsとなります。

  • Windows環境の一例 : x86_64-ms-win32-msvc
  • Linux環境の一例 : x86_64-none-linux-elf
  • CUDA環境の一例 : nvptx64-nvidia-cuda-cuda

ただし、標準規格としてTarget Tupletsを定義はせず、具体的なものは別のドキュメントに委ねるようです。規格書に記述してしまうと更新のために時間と手間がかかるためです。

P2000R2 Direction for ISO C++

C++の標準化の方向性を記した文書。

C++標準化の目的や課題、次のバージョンに向けて重視する機能など、より大域的な視点でC++標準化の道筋を示す文書です。今回コロナウィルス流行に伴う社会環境の変化を反映する形で変更されたようです。

P2029R2 : Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals

文字(列)リテラル中での数値エスケープ文字('\xc0')やユニバーサル文字名("\u000A")の扱いに関するC++字句規則の規定を明確にする提案。

この提案は以下の3つのIssueを解決するものです。

この提案では、既存の実装をベースとして字句規則をより明確に修正することによってこれらの問題の解決を図ります。

  • Core Issue 411
    • 単一の文字として表現できないユニバーサル文字名のエンコーディングは文字リテラルと文字列リテラルで異なる振る舞いをする、と規定する。
    • 例えば実行文字集合UTF-8である時、'\u0153'は(サポートされる場合)int型の実装定義の値を持つが、"\u0153"\xC5\x93\x00の長さ3の文字配列となる。
  • Core Issue 1656
  • Core Issue 2333
    • UTF-8リテラル中での数値エスケープ文字使用には価値があるので、明確に振る舞いを規定する。

問題範囲としては大きくないのですが、該当する文言をほとんど書き直しているため変更範囲が広くなっています。しかし、よく見るとより明確かつシンプルにこれらのことが表現されるようになっています。

P2075R1 : Philox as an extension of the C++ RNG engines

<random>に新しい疑似乱数生成エンジンを追加する提案。

疑似乱数生成エンジンは次のように様々な側面から特徴付けられます。

  • 乱数の質
  • 生成速度
  • 周期
  • 並列化(ベクトル化)可能性

例えば線形合同法std::linear_congruential_engine)は周期が短く乱数の質が低いのに対して、メルセンヌツイスターstd::mersenne_twister_engine)は周期が長く乱数の質は高いのですが、非常に大きなサイズの状態に依存しており並列化を妨げています。また、準モンテカルロ法をサポートできるような超一様分布を生成するエンジンもありません。

これらの観点から見ると現在のC++に用意されている乱数エンジンだけでは乱数のユースケースを満たすことは出来ず不十分であるので、最新の研究成果に基づいていくつかを検討し追加しようというのが(大本のP1932R0の)狙いです。

この提案ではPhiloxと呼ばれるGPU等のハードウェアによって並列化しやすい特性を持つエンジンに絞って提案されています。

Philoxはカウンタベースと呼ばれるタイプのエンジンで、カウンターベースのエンジンは総じて小さい状態と長い周期が特徴です(Philox4x32はそれぞれ40 [byte]2^130)。Philoxは状態が小さい事と演算が単純であることからSIMDGPUによる並列化が容易であり、intelnVidiaAMDによってそれぞれのハードウェアに最適化されたライブラリ実装が提供されており、それら実装は統計的なテストをパスしています。また、そのような特性を生かして大規模な並列乱数生成が求められる金融や非決定性有限オートマントンのシミュレーションなど、すでに広い使用実績があります。
このように、Philoxエンジンは導入および実装のハードルは低く標準化による利得は大きいため、標準に追加することを提案しています。

そして、Philoxエンジンを導入するに当たっては次の2つのAPIのどちらかを選択することを提案しています。

  • Philoxだけに着目したAPI
    • 既存の乱数エンジンを参考に、1つのPhiloxエンジンのベースとなるクラステンプレートと、そのパラメータ定義済みのいくつかのエイリアスstd::philoxNxM)だけを追加する。
  • カウンタベースエンジンのAPI

現状ではまだどちらを選択するかは議論中のようですが、どちらのAPIが選択されたとしてもユーザーからはstd::philox4x64std::philox4x64_rのような名前で既存の乱数エンジンと同様のインターフェースによって使用可能となります。

P2093R1 : Formatted output

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

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、wchar_tおよびcharN_tオーバーロードの追加に関してを別の提案に分離して、ユニコードテキストのフォーマットについてと共に議論することが明記されたことです。

P2128R2 : Multidimensional subscript operator

多次元コンテナサポートのために添字演算子[])が複数の引数を取れるようにする提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、[]が少なくとも1つの引数を取る必要があるという制約を削除したことと、C配列に対するサポートや既存ライブラリに追加することを目指さないことを明言したことです。 C配列や既存のライブラリ機能(std::valarray)でそれらを使用したい場合は、C++26以降に向けて別の提案で議論することを推奨しています。

P2139R2 : Reviewing Deprecated Facilities of C++20 for C++23

C++20までに非推奨とされた機能をレビューし、標準から完全に削除するかあるいは非推奨を取り消すかを検討する提案文書。

まだ検討中で、削除が決まった物は無いようです。

P2146R2 : Modern std::byte stream IO for C++

std::byteによるバイナリシーケンスのI/Oのための新ライブラリ、std::ioの提案。

以前の記事を参照 onihusube.hatenablog.com onihusube.hatenablog.com

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

  • std::streambufやCのI/O APIの使用例を追加
  • design decisionsの書き直し
  • 参照文書の変更
  • エンディアンを考慮してRIFFファイルを読み取るサンプルコードの追加
  • std::filebufベンチマークを追加

などです。

P2161R2 : Remove Default Candidate Executor

Networking TSのassociated_executorからデフォルトのExecutorを取り除く提案。

以前の記事を参照 onihusube.hatenablog.com onihusube.hatenablog.com

このリビジョンでの変更は、1引数の​defer, dispatch​, ​postがその引数のCompletionTokenから(色々やって)型::executor_typeを取得できることを要求し、それが同様に(色々やって)取得できるget_executor()の戻り値型と一致することを要求するようになった事などです。

P2165R1 : Compatibility between tuple and tuple-like objects

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

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更はstd::tupleの推論ガイドを変更しないことにしたことと、std::tuple_catもより広いtuple-likeな型全般で使用可能となるようにしたことです。

以前のリビジョンではstd::tupleをtuple-likeな型からその要素を分解して構築できるように推論ガイドを変更していたのですが、std::tuple{std::array<int, 2>{}}(この型はstd::tuple<std::array<int, 2>>になるが、変更の下ではstd::tuple<int, int>になってしまう)の様な既存のコードの振る舞いを破壊する事になっていいたためです。ただ、std::pairは以前から特別扱いされていたので変更なしでも2要素tupleへ(現在でも)変換可能です。

P2169R1 : A Nice Placeholder With No Name

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は_がクラスメンバでも同じような意味をもつようにしたことと、同じスコープで複数回_を使う場合の扱いが変更されたことです。

結局、この特別な_は次の場所で使用することができます。

  • 変数名
  • クラスの非静的データメンバ名
  • 関数の引数名
  • ラムダ式のキャプチャ名
  • 構造化束縛宣言の変数名

そして、前回掲載したサンプルコードは次のように動作が微妙に変わります。

/// ここはモジュール実装単位内ではないとする

namespace a {
  auto _ = f(); // Ok, [[maybe_unused]] auto _ = f();と等価
  auto _ = f(); // NG: _の再定義、モジュール実装単位内ならばok
}

void f() {
  auto _ = 42; // Ok、[[maybe_unused]] auto _ = 42;と等価
  auto _ = 0;  // Ok、再定義(参照しない限りOK)

  {
    auto _ = 1; // Ok, 単に隠蔽する
    assert( _ == 1 ); // Ok
  }

  assert( _ == 42 ); // NG、再定義された名前の参照
}

ただし、この様な再定義が許可されるのは_という名前の変数だけです。他の名前の変数は今まで通り定義が一つでなければなりません。

P2178R1 : Misc lexing and string handling improvements

現在のC++の字句規則をクリーンアップし、ユニコードで記述されたソースコードの振舞を明確にする提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、FAQを追加した事と、生文字列リテラル末尾の空白やBOM、ill-formedなcode units sequence、文字列リテラルの連結についての追記が行われたことです。

P2183R0 : Executors Review: Properties

C++23への導入を目指してレビューが進んでいるExecutorライブラリ(P0443R13)におけるプロパティ指定(P1393R0)の利用に関する報告書。

P0688R0によってExecutorに対して導入された組み合わせ爆発を抑えるプロパティ指定の方法は、Executorにとどまらず将来のライブラリに対しても有用であると判断され、よりジェネリックなプロパティ指定のためのAPIとしてExecutorから分離されました(P1393R0)。
この提案は分離されたことによって若干変化したAPIをExecutorがどのように利用するのかをLEWGにおけるレビューのためにまとめたもので、boost.asioにおける先行実装を用いたサンプルコードがいくつか掲載されています。また、その際に発見された小さないくつかの問題についても同時に報告されています。

P2186R0 : Removing Garbage Collection Support

ガベージコレクタサポートのために追加された言語とライブラリ機能を削除する提案。

C++11にて、C++処理系としてGC実装を許可するための最小の文言とライブラリサポートが導入されました。2020年現在、BoehmGCというC/C++向けのGCライブラリ(not処理系)があるほか、主要ブラウザのJavascriptエンジンなどGCをサポートする言語がC++で実装され、GCにより管理されているオブジェクトとC++オブジェクトが密接に連携できるなど、C++から利用可能なGCの実装は一定の成功を収めています。ただしC++の処理系としてのGC実装はなく、これらのC++から利用可能なGC実装はC++11で導入されたGCのための仕組みを利用していません。

また、そのような標準で用意されているGC実装のための仕組みにはいくつも問題があり、実際のGC実装のためにはほとんど役に立ちません。実際、既存のC++から利用可能なGC実装が依存しているのは、現在の規定とは異なる要因に対してです。そして、GCのためのライブラリ機能を使用しているコードはGCCLLVM(つまり実装者)以外には見つからなかったそうです。

C++実装としてGCサポートを認めないわけではなく、少なくとも現在の仕様とライブラリ機能は役に立たないので削除しようという提案です。これらの文言と機能は実装される事も使用されることも無く、非推奨にすることには意味が無いためすぐに削除することを提案しています。

P2187R3 : std::swap_if, std::predictable

より効率的な条件付きswapを行うためのstd::swap_ifと、その使用を制御するstd::predictableの提案。

前回の記事を参照 onihusube.hatenablog.com

R1とR2は公開されておらず、前回のR0からいきなりR3に飛んでいます。

変更点はまず、入力となるTのサイズによってswap_ifを使うかどうかを静的に分岐するようにしたことです。そのための、is_trivially_swappable_vcheaply_swappableコンセプトを追加して以下の様な実装をします。

template <typename T>
constexpr bool is_trivially_swappable_v = std::is_trivially_copyable_v<T>;

template <typename T>
concept cheaply_swappable = (std::is_trivially_swappable_v<T> && sizeof(T) <= N); // ここのNは実装定義

template <typename T>
  requires (cheaply_swappable<T> || std::swappable<T>)
constexpr bool swap_if(bool c, T& x, T& y) noexcept(cheaply_swappable<T> || std::is_nothrow_invocable_v<decltype(std::swap<T,T>), T&, T&>) {
  if constexpr (cheaply_swappable<T>) {
    struct alignas(T) { char b[sizeof(T)]; } tmp[2];

    std::memcpy(tmp[0].b, &x, sizeof(x));
    std::memcpy(tmp[1].b, &y, sizeof(y));
    std::memcpy(&y, tmp[1-c].b, sizeof(x));
    std::memcpy(&x, tmp[c].b, sizeof(x));

    return c;
  }

  // Tのコピーコストが大きいとき、普通に条件分岐してswapする
  if (c) std::swap(x, y);
  return c;
} 

この様にすることで、Tのコピーコストが高くつく場合にも自動的に最適な実装に切り替えることができます。
また、is_trivially_swappable_vはカスタマイゼーションポイントになっていて、trivially copyableとみなされないが安全にmemcpyできるような型をswap_ifにアダプトできます。

そして最後に、イテレータに対して直接swap_ifするiter_swap_ifが追加されています。

このswap_ifが使用されるのはstd::sortなどのアルゴリズム中であり、そこではイテレータを介して実際の値にアクセスします。swap_ifの引数はイテレータを間接参照して渡す事になり、わずかではありますがボイラープレートが発生します。iter_swap_ifイテレータを直接受け取り内部で間接参照しつつswap_ifに渡す簡易なラッパーです。

template <typename Flag, typename I>
bool iter_swap_if(Flag c, I p, I q) {
  return swap_if(c, *p, *q);
}

P2188R1 : Zap the Zap: Pointers are sometimes just bags of bits

現在の標準の無効な(指すオブジェクトの生存期間が終了した後の)ポインタについての矛盾した規定を正す提案。

前回の記事を参照 onihusube.hatenablog.com

EWGでのレビューを受けて、不完全だった提案の文言をいったん削除し、代わりに著者の方が重視している側面について追記されています。

1. 等しいポインタは交換可能であるべき

ポインタはValue Semanticsを持っており、コピーすると元の値と同じになる。従って、2つの値が等しければ、それらは交換して扱うことができるはず。

// pとqは初期化されているとする
void f(int* p, int* q){
  *q = 99;
  bool same = false;

  if (p == q) {
    // ポインタの値が等しいならば、pとqは同じ
    *p = 42;
    same = true;
    assert(*q == 42);
  }

  assert(same ? (*q == 42) : (*q == 99));
}

このコードが動作しないとすれば、ポインタの等値性が壊れていることを意味する。

2. ポインタの等値性は一貫しているべき

同じ2つのポインタの等値性は比較される場所によらないはず、これはValue Semanticsの基本的な側面にすぎない。

bool compare(int* const p, int* const q){
  return p == q;
}

// pとqは初期化されているとする
void f(int* const p, int* const q){
  bool const same = (p == q);
  g(p, q);
  assert(same == (p == q));       // ここがtrueならば
  assert(same == compare(p, q));  // ここもtrue
}

このコードはg()がなんであるかや、翻訳単位やインライン化の有無などに関わらず動作するはず。

3. コンパイラは特定の状況ではエイリアスが無いと仮定しなければならない

他の場所にその参照を渡されることがない関数内のローカル変数は、他の場所から変更されることは無いと仮定できるようにしなければならない。

void f(){
  int x = 42;
  g();
  assert(x == 42);  // 常にパスする
}

アサートが起動することはあってはならない。xへのポインタは存在しないので、g()の中で使用されるいかなるポインタと等しくなることはありえない。

4. 無効なポインタのデリファレンスは未定義動作

そのことを変更するつもりはない(ただし、次の場合は除く)。

5. ポインタの有効性は比較によって伝染する

ポインタpを、有効である事が分かっているポインタqと比較した結果等しければ、pも有効でなければならない(上記1に従う)。

void f(){
  int * const p = new int(42);
  delete p;
  int * const q = new int(99);

  if (p == q) {
    assert(*p == 99); // p == qならば指す先は同じ
  }
}

実装がこの場合のp == qが成り立たないようにすることを推奨するが、もしp == qであるならば、pqは同じオブジェクトを指していなければならない。

逆に言うと、現在のC++はこれらの事を必ずしも保証できていないという事です。

P2191R0 : Modules: ADL & GMFs do not play together well (anymore)

グローバルモジュールフラグメントのあらゆる宣言が、モジュール外からのテンプレートのインスタンス化時に参照されないようにする提案。

グローバルモジュールフラグメントとは、モジュール内部に用意される主としてヘッダをインクルードするための領域です。その領域にあるものがそのモジュール内から参照されなかった場合、モジュールのコンパイルの段階で破棄されます。モジュールのビルドはテンプレートを残す必要があるため完全に完了するわけではありませんが、破棄された宣言はその後のテンプレートインスタンス化時には一切参照できなくなります。これはモジュールのインターフェースの肥大化を抑えつつ、モジュール内で#includeを行うための仕組みです。

一方、従来の#includeディレクティブはヘッダユニットのインポートとして書くことができ、コンパイラによって自動的に置換される可能性があります。グローバルモジュールフラグメント内でもヘッダユニットのインポートが発生する可能性があり、その場合はヘッダファイルが一つのモジュールとしてインターフェース依存関係を構築するため、そのimportが破棄されることはありません。すると、同じヘッダを(グローバルモジュールフラグメント内で)#includeしたときと異なり、あらゆる宣言は破棄されません。

そして、モジュール内で定義されエクスポートされているテンプレートがモジュール外でインスタンス化されたとき、そのインスタンス化経路(テンプレートのインスタンス化に伴ってインスタンス化される関連テンプレートを繋いでいったグラフのようなもの)上からグローバルモジュールフラグメント内にあって外部リンケージを持ち破棄されていない宣言はADLによって参照することができます。
この時、コンパイラによってグローバルモジュールフラグメント内の#includeimportに置換されていた場合、ADLによって参照可能な宣言が異なることになります。どのようなヘッダをヘッダユニットとして扱い、いつ#includeimportに置換するのかは実装定義とされているため、このことはコンパイラによって異なる可能性があります。また、ユーザーがそれを制御する手段はありません。

他の問題として、このようなグローバルモジュールフラグメントの仕様によって、実装はモジュールの第一段階のコンパイル後にもグローバルモジュールフラグメントの中身と依存関係グラフを保持し続けなければならず、モジュールパーティションによって複数のグローバルモジュールフラグメントが間接的に外部にエクスポートされる時、それをどのように統合すべきか?という問題が生じます。また、それによってはモジュール内部のパーティションへの分割が外部から観測可能となってしまうことになり得ます。

/// header.hpp

#include <iterator> // このインクルードは実装によってimportに変換されるかもしれない
/// mymodule.cpp
module;
// グローバルモジュールフラグメント

#include "header.hpp" // このインクルードは実装によってimportに変換されるかもしれない

export module mymodule;

template<typename C>
export void f(C&& container) {
  auto it = begin(container);  // 依存名なのでここではstd::beginは参照されているとみなされない
}

// 説明のために実装単位などを省略
/// main.cpp
import mymodule;

#include <vector>

int main() {
  std::vector<int> v{};

  f(v);
  // mymoduleのグローバルモジュールフラグメント内の#includeがそのままならばこれはエラー
  // mymoduleのグローバルモジュールフラグメント内で#includeがimportになっているとコンパイルは通る
}

この提案では、これらの問題を解決するためにグローバルモジュールフラグメント内のあらゆる宣言は、モジュールに属するテンプレートのインスタンス化時のADLにおいて可視とならない、と規定を変更することを提案しています。
これによって、グローバルモジュールフラグメントは完全にモジュール内部で完結するものになり、モジュール外部からは気にしなくてもよくなります。また、実装はモジュールのコンパイル後にグローバルモジュールフラグメント内のものを残すかどうかを自由に選択できるようになります。

ただし、グローバルモジュールフラグメント内の宣言を参照しているようなモジュールに属するテンプレートの利用のためには、モジュール内でusingしておくか、そのモジュールを使う側で必要な宣言が含まれているヘッダをインクルードしなければならなくなります。

/// mymodule.cpp
module;
// グローバルモジュールフラグメント
// この提案の下では、ここの宣言は全て外部から参照されることは無い

#include <vector>
#include "header.hpp"

export module mymodule;

template<typename C>
export void f(C&& container) {
  using std::begin; // beginを後から使用するためにはこうする (1)

  auto it = begin(container);
}

// 説明のために実装単位などを省略
/// main.cpp
import mymodule;

#include <vector>
#include <iterator> // mymodule内部のstd::beginを有効化するために明示的に追加する (2)

int main() {
  std::vector<int> v{};

  f(v); // (1)か(2)のどちらかをしておかないとコンパイルエラー
}

P2192R0 std::valstat - function return type

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。

現在のC++にはいくつものエラー報告のための方法がありますが、エラーを報告する側と受け取り側にとって単純かつ柔軟な方法はなく、戻り値の形でエラー報告を行う際の標準的な1つの型もありません。

  • 例外機構
    • 例外を報告する最も簡易な方法ではあるが、受け取る側では何が飛んでくるか分からない
    • オーバーヘッドが大きい
  • errnoあるいはstd::errc
    • エラーしか報告できない
  • std::error_code
    • 動的メモリ確保を伴うなどいくつか問題がある(P0824R1
    • エラーしか報告できない
  • outcome<T>/expected<T, E>
    • これらは非常に複雑な型となり、必要以上の複雑さを導入することになる(例えば、Boost.outcomは7223行)
    • エラーかそうでないかの2つの情報しか表現できない

一方、C++標準としてはエラー報告については次のようなコンセンサスがあるようです。

  • 処理(関数)の結果はエラーかそうでないかの2値ではない
    • 例えば非同期処理のキャンセルなど
  • エラーかそうでないかのバイナリロジックを回避すれば複雑さが低減される
    • 早期リターンが失敗ではない場合のエラーがある
  • (エラー報告の結果の)戻り値を消費することは、必然的に複雑となる
    • 戻り値型は多様なので、そのような消費ロジックは複雑にならざるをえない
    • 統一的な戻り値消費処理を決定し、それに従うようにすることが有益

valstatはこれらのコンセンサスに基づいた、戻り値でエラーハンドリングを行うためのコンセプト(概念)とそれに従うものの総称です。次のように使用することができます。

// valstatは何か特定の戻り値型ではない
auto [ value, status ] = valstat_enabled_function();

// valstatは戻り値として4つの状態を表現する
if (   value &&   status )  { /* info */ }
if (   value && ! status )  { /* ok   */ }
if ( ! value &&   status )  { /* error*/ }
if ( ! value && ! status )  { /* empty*/ } 

valstat(に適合する戻り値型)は処理結果と処理のステータスのペアであり、その組み合わせによってMeta Stateを表現します。そして、その状態は!&&によって判別できます。

Meta Stateは処理結果と処理のステータスの有無によって次の4つの状態の1つを表現します

処理結果 \ 処理ステータス 有り 無し(空)
有り Info OK
無し(空) Error Empty
  • OK
    • 関数は期待される値を返し完了した
  • Error
    • 関数は期待通りに完了しなかった(失敗した)
    • ステータスには関連情報が格納される
  • Info
    • 関数は期待される値を返し完了した
    • ステータスには追加の情報が格納されている
  • Empty
    • 戻り値は空
    • (これがどのように使われるのかは提案文書に書いてない、多分重要なのは上3つ)

これはvalstatのコンセプトの一部であり、列挙値などで表現されるものではなく、valstatの任意の実体がこれを明示的に実装するわけではありません。valstatにアダプトした型は必然的にその状態によってこのMeta Stateを表現することになります。

この提案としてはこのようなコンセプトとそれを表現するための標準実装であるstd::valstatを標準ライブラリに導入することを提案しています。それは次のように実装可能です。

#include <optional>

namespace std {
  template<tpyename T, typename S>
  struct [[nodiscard]] valstat {
    using value_type = T;
    using status_type = S;

    optional<T> value;
    optional<S> status;
  };
}

ただし、valstatは特定の型に縛られるものではなく、std::valstatと同じように使用可能なvalstatコンセプトに適合する任意の型によって実装可能です。それは純粋なC言語においてもポインタを利用することで実装でき、そのようなライブラリをC++から利用するときにはvalstatコンセプトに従って統一的にエラー報告を処理できるようになります。

P2193R0 : How to structure a teaching topic

R1が同時に出ているので↓でまとめて。

P2193R1 : How to structure a teaching topic

C++を教えるにあたってのカリキュラムを書く際のガイドラインとなる文書。

ある1つのトピック(C++の言語・ライブラリ機能)についてそこに何をどのように書くか等を説明し、カリキュラム中のトピック説明においての共通の文書構造を提供するものです。

P2196R0 : A lifetime-extending forwarder

std::forwardのように与えられた引数を完全転送し、CV修飾を記憶しつつその生存期間を延長し、必要になった場所で完全転送しつつ取り出すことのできるクラステンプレートであるstd::forwarderの提案。

おおよそ次のように動作するものです(提案文書からの引用。一部正しくないコードが含まれていますが、イメージを掴む分には問題ないのでそのままにしています)。

// オブジェクトの値カテゴリによって呼ばれるメンバ関数が異なる
struct object {
  void test() const & {std::clog << "void test() const &\n";}
  void test() const && {std::clog << "void test() const &&\n";}
};

// Forwarding referenceで入力を受ける
template <class T>
auto function(T&& t) {
  // 引数tの型を記憶し生存期間を延長する
  auto fwd = forwarder<T>(t);
  
  /∗ ... ∗/

  return fwd;
}

int main(int argc, char∗ argv[]) {
  object x;
  auto fwd0 = function(x);
  auto fwd1 = function(object{});
  
  /∗ ... ∗/

  // 構築時の値カテゴリとCV修飾を復元して取り出す
  // (提案文書にはこのように書かれているが実際この呼び出しは出来ない)
  fwd0().test(); // void test() const &
  fwd1().test(); // void test() const &&
  return 0;
}

オブジェクトの値カテゴリとCV修飾を保存したまま持ち運び、必要な時にそれを復元しつつ取り出すことができます。ただし、提案ではメンバ関数は型変換演算子しか定義されておらず、operator.オーバーロード不可能なので実際には上記の.によるメンバ呼び出しみたいなことはできません。

筆者の方は別の提案であるP1772R1(いわゆるoverloadedの提案)の作業中にこのような完全転送と生存期間延長を行うラッパー型の必要性に遭遇し、それがより広範に適用可能なものであったため独立した提案として提出したとのことです。

P2198R0 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

P1641R3P1642R3の以前のリビジョンで同時に提案されていたものを分離して一つにまとめた提案です。

P2199R0 : Concepts to differentiate types

2つの型が同じではない、2つの型は類似の型(CV修飾を無視して同じ型)である、あるいは2つの型は類似していない、という事を表現する3つの新しいコンセプトを追加する提案。

それぞれ、std::different_from, std::similar_to, std::distinct_fromという名前が当てられています。

2つの型が異なる、CV修飾の違いを除いて異なるとき、のようなテンプレートパラメータに対する制約は標準ライブラリの中でも頻出しています(例えばstd::optionalstd::variantの変換コンストラクタや代入演算子など)。この様なテンプレートパラメータに対する制約はよくある物なのでユーザーコードでも当然頻出し、ユーザーは各々同じコンセプトを最発明することになります。標準でこれらを定義しておくことでそのような手間を省くことができ、また標準ライブラリに対する制約もこれらのコンセプトを直接用いて簡略化することができます。

提案ではそれぞれ次のように定義されます。

namespace std {
  template<typename T, typename U>
  concept different_from = not same_as<T, U>;

  template<typename T, typename U>
  concept similar_to = same_as<remove_cvref_t<T>, remove_cvref_t<U>>;

  template<typename T, typename U>
  concept distinct_from = not similar_to<T, U>;
}

この定義だけを見てもよく使いそうなのは分かるでしょう。

この提案では同時に、different_fromdistinct_fromによって置換可能な標準ライブラリ内の制約条件を書き換えることも行っています。
また、differentdistinctの使い分けが曖昧なので、LWG/LEWGの判断次第では名前が入れ替わる可能性があるとのことです。

P2201R0 : Mixed string literal concatenation

異なるエンコードプレフィックスを持つ文字列リテラルの連結を禁止する提案。

文字列リテラルが2つ並んでいるとき、それらはコンパイラによって連結され1つの文字列リテラルとして扱われます。

int main() {
  auto str = "abc" "def" "ghi"; // "abcdefghi"として扱われる
}

このとき、そのエンコード指定(u8 L u U)が異なっている場合の動作は実装定義で条件付きでサポートされるとされており、実際には主要なC++コンパイラはエラーとします。ただ、UTF-8文字列リテラルのその他のリテラルとの連結は明示的に禁止されています。

int main() {
  auto str1 = L"abc" "def" U"ghi";   // 実装定義
  auto str2 = L"abc" u8"def" U"ghi"; // u8がある場合はill-formed
}

ただ、SDCC(Small Device C Compiler)というCコンパイラはこれをサポートしており、最初のエンコード指定で連結するようです。

このような異なるエンコード指定の文字列リテラル連結のユースケースはないので、明示的にill-formedであると規定しようという提案です。

P2202R0 : Senders/Receivers group Executors review report

Executor提案(P0443R13)のsenderreciever周りのレビューの結果見つかった問題についての報告書。

Executor提案はレビューの効率化のために構成要素に分割したうえで、それぞれ別々の人々によってレビューされているようです。

P2203R0 : LEWG Executors Customization Point Report

Executor提案(P0443R13)のカスタマイゼーションポイント周りのレビューのまとめと、生じた疑問点についての報告書。

この文書は筆者(レビュワー)の方々がP0443のカスタマイゼーションポイント(オブジェクト)について読み込み、どんなカスタマイゼーションポイントがあるかやそれらの枠割についてまとめた部分が主となっています。Executorライブラリのカスタマイゼーションポイントについて比較的簡単にまとめられているので興味のある人は見てみるといいかもしれません。

onihusube.hatenablog.com

この記事のMarkdownソース