Github Actions上のDockerコンテナ上のGCCの最新版でテストを走らせたかった

Github Actionsで用意されているubuntu 20.04環境に用意されているソフトウェアを見るとGCCは9.3が用意されています(2020年7月24日現在)。しかしGCC9.3はC++20対応がほぼなされていないので使い物になりません(個人の感想です)。いつかは追加されるでしょうが、GCC10.2(もう出た)、GCC 11.1とか言いだすと永遠に待ち続けることになりそうです・・・

一方、Docker公式のGCCコンテナは既にGCC10.1が利用可能です(2020年7月24日現在)。

つまり、この2つを組み合わせれば最新のGCCを使いながらGithub Actionsできるはず!

※以下ではGithub ActionsとかDocker関係の用語が必ずしも正しく使われていない可能性があります、ご了承ください。

Dcokerコンテナを起動しその上でビルドとテストを走らせるActionを作成する

この資料にはDockerコンテナを使ってCIを走らせるための初歩初歩の事が書いてあります。ちゃんと読むと、Dockerコンテナを起動し何かするためのオリジナルのActionを作成し、それをテストしたいリポジトリのymlファイルから呼び出す形になる事が分かります。

(なお、私はちゃんと読まなかったのでわかりませんでした)

というわけで、まずはGCCのDockerコンテナを起動してビルドとテストを行うActionを作成します。

Dockerファイルの作成

まずは所望の動作を達成するためのDockerファイルを作ります。

From gcc:latest
RUN apt-get update
RUN apt-get install -y python3-pip
RUN pip3 install meson ninja
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

私はビルドシステムに専らmesonを使っているので、mesonもインストールしておきます。上記Dcokerfileの2-4行目はお好きなビルドシステムのインストールに置き換えてください。

ここのRUNで全部やっても良いのですがあまり汎用性が無くなるので、ここでは最低限の環境構築に留めて、メインの処理は別に書いたシェルスクリプト(ここではentrypoint.sh)に記述します。

5行目で用意したシェルスクリプトをDockerインスタンス上にコピーして、6行目で開始時に実行するように指示します。

シェルスクリプトの作成

このページにあるものを参考にして書きます。ただこのページだけ見ても、テストしたいリポジトリのクローンをどうするのかが分かりません。試してみると別にクローンされてはいないので自分でクローンしてくる必要があります。なので、そのために必要な情報を引数として受け取るようにします。

公式のActionにはcheckoutというのがありますが、今回はDocker上で実行するため利用できなさそうです。

#!/bin/sh -l

git clone $2
cd $1
meson build
meson test -C build -v

第一引数にリポジトリ名(=ディレクトリ名)、第二引数にリポジトリのURLを受けるようにしています。5-6行目はmeson特有のものなので、お使いのビルドシステムのビルドコマンドと成果物の実行コマンドに置き換えてください。

ファイル名は先程Dockerfileに書いたentrypoint.shと合わせます。chmod +x entrypoint.shで実行可能にすることを忘れすに、 Windowsの場合は次のページが参考になります。

Actionファイルの作成

次にActionとして呼ばれたときに何をするかをaction.ymlというファイルに記述します。試してないですがファイル名は多分固定だと思われます。

name: 'build & test'

inputs:
  name:
    description: 'Name of target repository'
    required: true
  uri:
    description: 'URI of target repository'
    required: true

runs:
    using: 'docker'
    image: 'Dockerfile'
    args:
    - ${{ inputs.name }}
    - ${{ inputs.uri }}

inputsで先程のシェルスクリプトに渡す引数を定義しています。上から順番に第一引数(name)、第二引数(uri)で内容は先ほどの通り。descriptionは説明で無くても良いです。requiredは引数が省略可能かを表しており、今回は省略してほしくないのでtrueに設定しておきます。

他にも出力を定義できたりします。出来ることは次のページを参照。

runsにActionとして呼ばれたときにやることを書いておきます。usingは何のアプリケーションを使用するのかを指示するもので、ここではDockerを指定します。imageには使いたいDockerイメージ名を指定します。Docker Hubにあるイメージ名を指定しても良いようですが、今回は先ほど用意したDockerfile(のファイル名)を指定します。
そして、argsで引数をどの順で受け取るかを指定しています。先程inputsで定義した引数名をinputs.nameの様に参照しています。シェルスクリプトにはここで書いた順番に引数が渡されることになります。

Actionリポジトリの作成

ここまで用意したDockerfile、やることを記述したシェルスクリプトentrypoint.sh、Actionを記述したaction.ymlの3つをGithub上の公開リポジトリに保存しておき、リリースタグを打っておくことで、他のリポジトリからActionとして参照できるようになります。
ここまではリポジトリのトップに全てをぶちまける事を前提に書いてありますので、今回はそうします。

私の場合は次のようになりました。

Publish this Action to Marketplaceなるメッセージが表示されていたら、きちんとActionとして認識されています。もし自作のActionをMarketplaceに公開したい場合はreadmeを適切に整えておく必要がありそうです。

テストしたいリポジトリのワークフロー中でActionを呼ぶ

こうして用意した自作Docker ActionをテストしたいリポジトリGithub Actions ワークフローから呼び出します。

name: Test by GCC latest  # ワークフローの名前
on: [push, pull_request]  # 何をトリガーにして実行するか

jobs:
  test:
    runs-on: ubuntu-latest  # Dockerを実行することになるベース環境
    name: run test
    steps:
    - name: run test in docker
      uses: onihusube/gcc-meson-docker-action@v6
      with:
        name: 'harmony'
        uri: 'https://github.com/onihusube/harmony.git'

実物がこちらです。今回対象のリポジトリは私が衝動で書いていたC++のライブラリです。

ベースは普通のGithub Actionsのワークフローを書くのと変わらないはず。今回重要なのは先程作成したDocker Actionを適切に呼び出すことです。

steps中のusesで任意のActionを呼び出すことができます。先程作成したActionのリポジトリ参照する形で作ったユーザー名/リポジトリ名@タグというように指定します(上記タグがv6とかなってるのは試行錯誤の名残です・・・)。
その次のwithで順番に引数を指定します。指定出来るのはActionのymlに書いたものだけで、ここに書いた引数がDockerインスタンス上で実行されるシェルスクリプトに渡されることになります。

でこれをプッシュするとDocker上のGCCでビルドされテストが実行されたので、どうやら目的を果たせたようです(参考の結果)。

参考文献

謝辞

この記事の8割は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース

[C++]Forwarding referenceとコンセプト定義

コンセプトを定義するとき、あるいはrequires節で制約式を書くとき、はたまたrequires式でローカルパラメータを使用するとき、その型パラメータがForwarding referenceから来ているものだと使用する際に少し迷う事があります。

// operator*による間接参照が可能であること
template<typename T>
concept weakly_indirectly_readable = requires(T& t /*👈ローカルパラメータ*/) {
  *t;
};

template<weakly_indirectly_readable T>
void f(T&& t) {
  auto v = *std::forward<T>(t);
}

int main() {
  std::optional<int> opt{10};

  f(opt); // 左辺値を渡す
  f(std::optional<int>{20});  // 右辺値を渡す
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

さてこの時、weakly_indirectly_readableは右辺値に対してきちんと制約出来ているでしょうか?値カテゴリの情報まできちんと伝わっていますか??
残念ながらこれでは右辺値に対して適切に制約出来ていません。例えば右辺値の時は呼べないような意地の悪い型を用意してあげると分かります。

struct dereferencable_l {
  int n = 0;

  int& operator*() & {
    return n;
  }

  int&& operator*() && = delete;
};


int main() {
  dereferencable_l d{10};

  f(d); // 左辺値を渡す
  f(dereferencable_l{20});  // 右辺値を渡す、エラー
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

エラーにはなりますが、それは右辺値を渡したときにf()の定義内で発生するハードエラーです。コンセプトによる制約はすり抜けています。

ではこの時どのようにすれば左辺値と右辺値両方に対して適切な制約を書けるのでしょう・・・?

Forwarding referenceと参照の折り畳み

Forwarding referenceとは先程のf()の引数にあるT&&のような引数宣言の事です。これはそこに引数として左辺値が来ても右辺値が来ても、テンプレートの実引数推定時にいい感じの型に変化してくれる凄い奴です。

そのルールは単純で、Forwarding referenceに対応する引数の値カテゴリによって次のようになります。

  • 引数として左辺値(lvalue)が渡されたとき
    • テンプレートパラメータTは渡された型をTとしてT&になる
  • 引数として右辺値(xvalue, prvalue)が渡されたとき
    • テンプレートパラメータTは渡された型そのままのTになる
template<typename T>
void f(T&& t);

int n = 0;

f(n);  // intの左辺値を渡す、T = int&
f(1);  // intの右辺値を渡す、T = int

ではこの時、引数宣言のT&&はどうなっていて、引数のtはどうなっているのでしょう?
これは参照の折り畳み(reference collapsing)というルールによって、元の値カテゴリの情報を表現するように変換されます。

  • 引数に左辺値が渡されたとき
    • テンプレートパラメータはT -> T&となり、引数型はT&&& -> T&となる
    • 引数tは左辺値参照
  • 引数に右辺値が渡されたとき
    • テンプレートパラメータはT -> Tとなり、引数型はT&& -> T&&となる
    • 引数tは右辺値参照

これ以降の場所でこのTを使用する場合も同様になります。参照の折り畳みは別にForwarding reference専用のルールではないのでそれ以外の場所でも同様に発動します。
そして、参照の折り畳みによって右辺値参照が生成されるのは、T&&&&を付けた時だけです。それ以外はすべて左辺値参照に折りたたまれます。

using rawt = int;
using lref = int&;
using rref = int&&;

using rawt_r  = rawt&;   // int&
using rawt_rr = rawt&&;  // int&&
using lrefr   = lref&;   // int& & -> int&
using lrefrr  = lref&&;  // int& && -> int&
using rrefr   = rref&;   // int&& & -> int&
using rrefrr  = rref&&;  // int&& && -> int&&

最適解

Forwarding referenceに対して制約を行うコンセプトの定義内、あるいはrequires節の制約式では上記の様に折り畳まれた後の型が渡ってきます。つまりは、それを前提にして書けばいいのです。先程のweakly_indirectly_readableを書き直してみると次のようになります。

// operator*による間接参照が可能であること
template<typename T>
concept weakly_indirectly_readable = requires(T&& t) {
  *std::forward<T>(t);
};

template<weakly_indirectly_readable T>
void f(T&& t) {
  auto v = *std::forward<T>(t);
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

f()に左辺値を渡したとき、weakly_indirectly_readableに渡る型は左辺値参照型T&です。weakly_indirectly_readableのローカルパラメータtは参照の折り畳みによってT&& & -> T&となり、左辺値参照になります。std::forwardに渡しているTT&なので、ここではムーブされません。

f()に右辺値を渡したとき、weakly_indirectly_readableに渡る型は単にTです。weakly_indirectly_readableのローカルパラメータtはそのまま&&が付加されてT&&となり、右辺値参照になります。std::forwardに渡しているTTなので、ムーブされることになります。

これはコンセプト定義内ではなく、requires節で直接書くときも同様です。

template<typename T>
  requires requires(T&& t) {
    *std::forward<T>(t);
  }
void f(T&& t) {
  auto v = *std::forward<T>(t);
}

先程の意地の悪い例を渡してみてもコンセプトによるエラーが発生するのが分かります。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

この様に、Forwarding referenceと参照の折り畳みを考慮することで、値カテゴリを適切に処理しつつ制約を行うことができます。

値カテゴリを指定する制約

先程完璧に仕上げたweakly_indirectly_readableですが、それをあえて右辺値と左辺値パターンで分けて書いてみます。

template<typename T>
concept weakly_indirectly_readable = requires(T& t) {
  *t;             // 左辺値に対する制約
  *std::move(t);  // 右辺値に対する制約
};

これは先ほどのT&&std::forwardを使った制約と同じ意味になります。

あえてこのように書くことで、ローカルパラメータとその型の参照修飾を用いて制約式に値カテゴリの制約を指定している様を見ることができます。すなわち、weakly_indirectly_readableのモデルとなる型はoperator*()が右辺値・左辺値の両方で呼べること!という制約を表現しています。これを略記すると先ほどのT&&std::forwardを使った制約式になるわけです。

例えば制約したい対象の式(ここではoperator*)が左辺値でだけ呼べれば良いのであれば右辺値に対する制約は必要なく、逆に右辺値だけをチェックすればいい場合は左辺値に対する制約は必要ありません(ただし、そのようなコンセプト定義は適切ではないかもしれません)。

さらに、ローカルパラメータをCV修飾することで、値カテゴリに加えてCV修飾を指定した制約を行えます。

template<typename T>
concept weakly_indirectly_readable = requires(const T& t) {
  *t;             // const 左辺値に対する制約
  *std::move(t);  // const 右辺値に対する制約
};

ちなみにこの場合に、非constに対する制約も同時に行いたい場合は以下のようにします。

template<typename T>
concept weakly_indirectly_readable = requires(T& t) {
  *t;             // 左辺値に対する制約
  *std::move(t);  // 右辺値に対する制約
  *const_cast<const T&>(t);             // const 左辺値に対する制約
  *std::move(const_cast<const T&>(t));  // const 右辺値に対する制約
};

constのローカルパラメータを取ってconst_castします。requires式を分けても良い気がしますが、標準ライブラリのコンセプトはこの様に定義されるようです。

これらのように、requires式のローカルパラメータのCV・参照修飾を用いて制約式に対するCV修飾と値カテゴリの制約を表現する事ができます。そして、標準ライブラリのコンセプトは全てそのように書かれています。

結局

脱線しながら長々と語ってきましたが、コンセプトを定義するあるいはrequires節で制約をする際に意識すべきことは一つだけです。

  • コンセプト定義あるいはrequires節において、引数となる型パラメータは常にCV修飾無し、参照修飾無しの完全型が渡ってくると仮定して書く

これに近いことは規格書にも書いてあります。この仮定を置いてそれに従って書けば、結果的に適切なCV修飾と値カテゴリによって制約を行うことができます。これは実際に渡ってくる型が云々と悩むのではなく、そう思って書け!という事です。

標準ライブラリのコンセプトはそのように定義されており、これを意識の端っこに置いておけばそのようなコンセプトを利用しやすくなり、自分で定義する際もすっきり書くことができるようになるかもしれません。

参考文献

この記事のMarkdownソース

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

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

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

P1401R3 : Narrowing contextual conversions to bool

constexpr ifstatic_assertの引数でのみ、整数型からbool型への暗黙の縮小変換を定数式で許可する提案。

現在定数式での整数の暗黙変換では、文脈的なbool変換も含めて縮小変換が禁止されており次のようなコードはコンパイルエラーになります。

enum Flags { Write = 1, Read = 2, Exec = 4 };

template <Flags flags>
int f() {
  if constexpr (flags & Flags::Exec) // 縮小変換が起きるためコンパイルエラー
  // if constexpr (bool(flags & Flags::Exec)) とするとok
    return 0;
  else
    return 1;
}

int main() {
  return f<Flags::Exec>();  // コンパイルエラー
}
template <std::size_t N>
class Array {
  static_assert(N, "no 0-size Arrays"); // 縮小変換が起きるためコンパイルエラー
  // static_assert(N != 0); とするとok

  // ...
};

Array<16> a;  // コンパイルエラー

一方対応する実行時コードは普通にコンパイルでき、期待通りに動作します。

if (flags & Flags::Exec) // ok
  {}

assert(N); // ok

このような一貫しておらず直感的ではない挙動を修正するために、constexpr ifstatic_assertの条件式に限って、文脈的なbool変換時の縮小変換を許可しようというものです。

そもそもこれらの条件式でさえもboolへの縮小変換が禁止されていたのは、noexcept式での縮小変換を禁止した時に巻き込まれてしまったためのようです。関数f()の例外仕様が別の関数g()と同じ(あるいはそれに従う)場合、noexceptを二つ重ねて書きます。しかし、その場合に書き間違えて関数名だけを書いたり、1つにしてしまってg()constexpr関数だったりすると思わぬバグを生みます。

int f() noexcept(noexcept(g()));  // 書きたいこと、noexcept指定指の中にnoexcept式を書く

int f() noexcept(g);    // 関数ポインタからの暗黙変換、でもこう書きたさもある・・・
int f() noexcept(g());  // 定数評価の結果bool値へ変換されると・・・

このような些細な、しかし気づきにくいバグを防ぐために定数式での文脈的なbool変換の際は縮小変換を禁止することにしました。しかし、noexcpet以外のところではこれによって(最初に上げたような)冗長なコードを書くことになってしまっていました。

この欠陥報告(CWG 2039、C++14)を行ったRichard Smithさんによると、本来はnoexcept式にだけ適用するつもりで、static_assertには表現の改善のみで縮小変換禁止を提案してはいなかったそうですが、その意図に反して両方で縮小変換が禁止されてしまいました。結果、おそらくその文言を踏襲する形でconstexpr ifexplicit(bool)にも波及したようです。

P1450R3 : Enriching type modification traits

型の修飾情報などを操作するためのいくつかの新しいメタ関数の提案。

主に次の2種類のものが提案されています。

  • remove_all_pointers
  • 型に付いている情報をコピーするcopy_*メタ関数
using remove_ptr = std::remove_all_pointers_t<int************>;     // int
using copy_ptr1 = std::copy_all_pointers_t<int***, double>;         // double***
using copy_ptr2 = std::copy_all_pointers_t<int***, double*>;        // double****
using copy_r1 = std::copy_reference_t<int&, double>;                // double&
using copy_r2 = std::copy_reference_t<int&, double&>;               // double&&
using copy_r3 = std::copy_reference_t<int&, double&&>;              // double&
using copy_const1 = std::copy_concst_t<const int, double>;          // const double  
using copy_const2 = std::copy_const_t<const volatile int, double>;  // const double  
using copy_cvr = std::copy_cvref_t<const volatile int&&, double>;   // const volatile double&&

この他にもcopy_volatileとかcopy_extentcopy_pointerなどが提案されています。

copy_*系メタ関数の引数順は<From, To>になっており、全てに_t付きのエイリアスが用意されています。また、対象の修飾がコピー先にすでに付いている場合はそれはそのままに追加でコピーする形になり、コピー元に対象の修飾がない場合は何もコピーしません。

筆者の方々の経験からプロクシクラス作成やカスタムオーバーロードセットを構築するツールの実装に有用であった型特性を提案しているそうです。

P1467R4 : Extended floating-point types and standard names

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

機械学習(特にディープラーニング)では多くの場合それほど高い精度が求められないため、float(32bit浮動小数点数)よりもより小さい幅の浮動小数点型(時には整数型)を利用することでその時間的/空間的なコストを抑えることが行われており、それを支援する形でハードウェア(GPU/CPU)やソフトウェア(CUDA/LLVM-IR)でのサポートが充実してきています。

現在のC++には3種類の浮動小数点型だけが定義されておりそれ以外のものは何らサポートがあリません。そのため、拡張浮動小数点型は算術・変換演算子オーバーロードして組み込み型に近い挙動をするようなクラス型を定義することでサポートされています。しかし、そのような方法は完全ではなく面倒で、効率化のためにインラインアセンブラコンパイラ固有のサポートが必要とされます。
これらの問題はユーザー定義のライブラリで解決できるものではなく、コア言語でのサポートが必要です。そして、拡張浮動小数点型が求められる場所ではC++が使用される事が多くこれらの問題を解決するに足る価値(ポータビリティや効率性の向上など)があるので、拡張浮動小数点型の言語/ライブラリサポートを追加しようという提案です。

ただし、現在のところ拡張浮動小数点型のスタンダードとなるものは確定しておらず、将来どれが使われていく(あるいは廃れる)のか予測することは困難であるため、何がいくつ定義されるかは実装定義とされます。そのため、bfloat16とかfloat16みたいな具体的な型は提供されませんが、代わりに似た形のエイリアスが実装定義で提供されます。

変更は既存の浮動小数点型の振る舞いを保ったままで追加の浮動小数点型をより安全に利用可能かつ拡張可能である(ハードウェアに依存しない)ようにされています。

  • 拡張浮動小数点型はdoubleに昇格されない
  • 拡張浮動小数点型の関わる暗黙変換では縮小変換を許可しない
    • ただし、定数式では許可される
  • 式のオペランドとなっている2つの浮動小数点型を統一する算術型変換(Usual arithmetic conversions)では、どちらのオペランドももう片方の型に変換できない場合はill-formed
    • float, double等は従来のルール通りにより幅の広い型に自動昇格する
float f32 = 1.0;
std::float16_t f16 = 2.0;
std::bfloat16_t b16 = 3.0;

f32 + f16; // OK、f16はfloatに変換可能、結果の型はfloat
f32 + b16; // OK、b16はfloatに変換可能、結果の型はfloat
f16 + b16; // NG、2つの型の間に互換性がなく、どちらの型ももう片方の型に値を変更することなく変換できない

std::float16_t x{2.1};  // OK、2.1は2進浮動小数で表現可能ではないため縮小変換が発生している、定数式なのでok

// <charconv>が使用可能
char out[50]{};
if (auto [ptr, ec] = std::to_chars(out, std::end(out), f16); ec == std::errc{}) {
  std::cout << std::string_view(out, ptr - out) << std::endl;

  std::float16_t outv;
  if (auto [_, ec2] = std::from_chars(out, ptr, outv); ec2 == std::errc{}) {
    std::cout << outv << std::endl;
  }
}

// ユーザー定義リテラルが用意される
using namespace std::float_literals;

// complexも規定される
std::complex<std::bfloat16_t> z = {1.0bf16, 2.0bf16};

これら型エイリアスが定義されるヘッダには<fixed_float><stdfloat>という名前を提案しているようですが、筆者の方々はあまり気に入っていないようでより良い名前やふさわしい既存のヘッダを考慮中のようです。いい名前が思いついたらコントリビュートチャンスです。

P1468R4 : Fixed-layout floating-point type aliases

この提案文書は拡張浮動小数点型の既知のレイアウトとその名前についての提案でしたが、今回P1467R4(1つ前のの拡張浮動小数点型に対する提案)にマージされたため、内容は空です。それが行われたことを記しておくために存在しているようです。

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

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

[utility]は範囲が広いですが、ほとんどpair, tupleを対象にしています。また、動的メモリ確保を必要としたり例外を送出しうるもの、iostreamのようにOSのサポートを必要とするものは当然含まれていません。<utility>, <tuple>, <ratio>ヘッダの全てと、特筆する所ではstd::unique_ptrstd::functionのフリースタンディング化が提案されています。
<ranges><iterator>からは(i|o)stream_iterator(i|o)streambuf_iteratoristream_view等に関わるもの以外の全てを追加することが提案されています。

また、これらのものには機能テストマクロが用意されています。

筆者の方は、<optional><variant>などを今後別の提案で詳細に検討していくつもりのようです。

ライブラリ機能のフリースタンディング化に慎重な検討が必要になるのは、フリースタンディング処理系とホスト処理系とである関数呼び出し時のオーバーロードセットが変化することで暗黙のうちに動作が変わってしまうことを防ぐためです。変わったとしてもちゃんとエラーになるのかや選択されるオーバーロードが変化しないかなどを慎重に検討せねばならないようです。とても大変そうです・・・

P1944R1 : Add Constexpr Modifiers to Functions in cstring and cwchar Headers

<cstring><cwchar>の関数にconstexprを追加する提案。

これらのヘッダに定義されている文字列操作関数をconstexprにすることを意図しています。ただし、ロケールやグローバルオブジェクトの状態に依存したり、スレッドローカルな作業域を持つような関数は除外しています。また、std::memcpystd::memmoveなどのメモリ操作系の関数もconstexprにすることが提案されています。これらは引数にvoidポインタを取るためそのままだと定数式で使えないのですが、コンパイラマジックにより定数式で使用可能にしてもらうようです。

<cstring><cwchar>C++として独自実装している処理系とCのコードを流用している処理系が存在しているようですが、前者はそのままconstexprを付加し、後者はコンパイラの特別扱いによってABIを破損することなくconstexpr対応できるだろうということです。

P1949R4 : C++ Identifier Syntax using Unicode Standard Annex 31

識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

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

前回(R3)との変更点は文書にSummaryが追加されたことと、提案する字句トークン(プリプロセッシングトークン)のEBNF定義の修正だけのようです。

この提案はすでにCWGでの文言レビューを待つだけになっており、C++23に導入される可能性は高いです。その場合、C++においては識別子(クラス・変数・関数等の名前)に絵文字を使用できなくなります(欠陥報告になる可能性があるので以前のバージョンも含めて)。

P1990R1 : Add operator[] to std::initializer_list

std::initializer_listに添字演算子[]data()関数を追加する提案。

std::initializer_listは軽量な生配列のプロキシクラスではありますが、要素アクセスにはイテレータを使用するしかないなど少し使いづらい所があります。その解消のために、添字アクセスと先頭領域へのポインタ取得をサポートしようという提案です。

struct Vector3 {
  int x, y, z;

  // C++20現在
  Vector3(std::initializer_list<int> il) {
    x = *(il.begin() + 0);
    y = *(il.begin() + 1);
    z = *(il.begin() + 2);
  }

  // 提案
  Vector3(std::initializer_list<int> il) {
    x = il[0];
    y = il[1];
    z = il[2];
  }
};


void f(std::initializer_list<int> il) {
  // C++20 現在
  const int* head = il.begin();

  // 提案
  const int* head = il.data();
}

P2025R1 : Guaranteed copy elision for return variables

NRVO(Named Return Value Optimization)によるコピー省略を必須にする提案。

C++17からはRVO(Return Value Optimization)によるコピー省略が必須となり、関数内で戻り値型をreturnステートメントで構築する場合にコピーやムーブを省略し、呼び出し元の戻り値を受けている変数に直接構築するようになっています。一方、関数内で変数を構築してから何かしてその変数をreturnする場合のコピー省略(NRVO)は必須ではなく、コンパイラの裁量で行われます(とはいえ、主要なコンパイラは大体省略します)。

// コピーが重いクラス
struct Heavy {
  int array[100]{};

  Heavy() {
    std::cout << "default construct\n"
  }

  Heavy(const Heavy& other) {
    std::copy_n(other.array, 100, array);
    std::cout << "copy construct\n"
  }

  Heavy(Heavy&& other) {
    std::copy_n(other.array, 100, array);
    std::cout << "move construct\n"
  }

};

Heavy rvo() {
  return Heavy{}; // RVOが必ず行われる
}

Heavy nrvo() {
  Heavy tmp{};

  for (int i = 0; i < 100; ++i) {
    tmp.array[i] = i;
  }

  return tmp; // NRVOはオプション
}

int main() {
  Heavy h1 = rvo();   // 結果はh1に直接格納され、デフォルトコンストラクタが一度だけ呼ばれる
                      // コピーやムーブコンストラクタは呼ばれない。

  Heavy h2 = nrvo();  // NRVOが行われない場合、デフォルト・ムーブコンストラクタが一回づつ呼ばれる
                      // 正確にはデフォルト構築→コピー→ムーブとなるが、returnでのコピー後のprvalueはRVOの対象なので最後のムーブコンストラクタは省略される
                      // NRVOが行われた場合、デフォルトコンストラクタが一度だけ呼ばれる
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

(言語バージョンをC++14にすると完全にコピー省略のない世界を見ることができます。また、-fno-elide-constructorsを外すとコピー省略された結果を見ることができます。)

このような場合のnrvo()の呼び出しのようにNRVOが可能なケースではNRVOを必須にしよう、という提案です。NRVo可能なケースというのは簡単に言うと全てのreturn文が同じオブジェクトを返すことが分かる場合の事で、提案文書にはこの提案によっていつNRVOが保証されるかのいくつかのサンプルが掲載されています。

この提案はCWGでの文言調整フェーズに進んでおり、C++23に入る可能性が高そうです。

P2034R2 : Partially Mutable Lambda Captures

ラムダ式の全体をmutableとするのではなく、一部のキャプチャだけをmutable指定できるようにする提案。

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

前回との差分は一部のサンプルコードが変更されたことと初期化キャプチャ時パック展開でのmutable指定が提案に含まれた事、EWGIの議論で示された懸念事項が追記された事です。

C++20よりラムダ式の初期化キャプチャ時にパラメータパックをキャプチャ出来る様になっているので、そこでもmutableが出来るようにしようとしています。これを用いるとパラメータパックだけをラムダ式中で再ムーブする時に、全部をmutableにしなくても良くなります。

template <class... Args>
auto delay_invoke_foo(Args... args, State s) {
  return [s, mutable ...args = std::move(args)] {
    return foo(s, std::move(args)...);
  };
}

追加された懸念事項は、明示的なconstキャプチャをする場合に、ラムダ式のムーブで暗黙にコピーが行われるようになる事です。クラスのメンバにconstメンバがあってもムーブコンストラクタ自体は使用可能ですが、constメンバはコピーされます。コピーコンストラクタは多くの場合例外を投げうるので、これによって思わぬところで例外が発生するようになってしまう可能性があります。

auto l1 = [const str = std::string{"not movable"}](){return str;};
auto l2 = std::move(l1);  // キャプチャしたメンバstrはコピー構築される、場合によっては例外を投げうる

この提案によってもたらされるラムダ式の対称性と一貫性の向上による効用と、このような足を撃ち抜く可能性を導入することによる弊害のどちらがより大きいのかは解決されておらず、より議論が必要となりそうです。

P2037R1 : String's gratuitous assignment

std::stringの単一の文字代入を非推奨とする提案。

std::stringにはchar1文字を受け取る代入演算子が定義されています。

// char1文字を代入する
constexpr basic_string& operator=(charT c);

しかし、この代入演算子は特に制約されておらず、charに暗黙変換可能な型に代入を許します。その代表的なものはintdoubleの数値型です。

std::string s{};

s = `A`;  // s == A
s = 66;   // s == B
s = 67.0; // s == C

すなわち、intdoubleへの暗黙変換を実装している任意のユーザー定義型も代入可能です。

そもそもstd::stringchar1文字を代入できる必要があることが疑わしい上にコンストラクタのインターフェースとも一貫しておらず、この様な変換が起きることはほとんどの場合意図したものではなくバグの原因であるので非推奨にしようという主張です。
ただし、削除することまでは提案されていません。

ほかの選択肢としては

  • 削除する
    • その場合、nullptrの代入が可能になってしまうのでケアする必要がある
  • コンセプトによる制約を行う cpp template<same_as<charT> T> constexpr basic_string& operator=(T c) ;
  • intからの変換だけを許可するようにして、他の変換は不適格とする。
    • 筆者の方が見てきたこれらの変換が問題となっていたケースはほぼ全てintからの変換だったので解決策としては弱いだろう、とのこと

R0の際に行われたLEWGでの投票では、非推奨とすることに合意が取れていて、今回はそれを受けて標準のための文言を追加したようです。

P2093R0 : Formatted output

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

C++20で導入されたstd::formatはフォーマットを指定しつつ文字列を構成できるものですが、その結果はstd::stringで得られ出力機能は備えていません。そのままiostreamを使えば出力できますが、一時オブジェクトの確保が必要になる上、iostreamによってフォーマット済み文字列を再びフォーマットすることになり非効率です。

// 一時オブジェクトが作成され、内部で再フォーマットされ、バッファリングされうる
std::cout << std::format("Hello, {}!", name);

// nameはnull終端されていなければならない、型安全ではない
std::printf("Hello, %s!", name);

// 一時オブジェクトが作成される、c_str()と個別I/O関数の呼び出しが必要になる
auto msg = std::format("Hello, {}!", name);
std::fputs(msg.c_str(), stdout);

この提案では、このような場合に一時オブジェクトを作成せず、フォーマットとI/Oで別の関数を呼び出す必要もなく、より効率的な出力を行うstd::print関数を提案しています。

// 一時オブジェクトは作成されず、フォーマットは一度だけ、直ちに出力する
std::print("Hello, {}!", name);

これはすなわちiostreamに変わる新しい出力ライブラリとなります。このライブラリは次のことを目標にしています。

これはすでに{fmt}にて実装されていて、その実装により得られたベンチマーク結果が掲載されています。既存のI/Oと比較すると速度とバイナリフットプリントの両面で良好な結果を得られているようです(ただ、純粋なフットプリントだけはprintfに及ばないようです)。

P2138R2 : Rules of Design<=>Wording engagement

CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。

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

このリビジョンでの変更は、CWG/LWGにおける標準のための文言レビューと本会議での投票の間に、最終確認のためのTentatively Readyという作業フェーズを追加することを提案している点です。

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

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

この提案は規格書中のAnnex.Dというセクションに記載されている機能だけを対象としていて、そこにあるもの以外を削除するわけでもなく、そこに新しく追加する機能について検討するものでもありません。

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

P2141R0 : Aggregates are named tuples

集成体(Aggregate)を名前付きのstd::tupleであるとみなし、標準ライブラリにおけるstd::tupleのサポートを集成体に拡張する提案。

std::tupleは任意個数の型をひとまとめにして扱える大変便利なものではありますが、コア言語のサポートが無く全てをライブラリ機能によって実現しているため使いづらい事が多くあります。一方、集成体はC言語から引き継がれたいくつかの条件を満たした構造体で、std::tupleを利用するシーンでは集成体を利用した方が便利だったりする事が多々あります。

// 集成体
struct auth_info_aggreagte {
  std::int64_t id;
  std::int64_t session_id;
  std::int64_t source_id;
  std::time_t valid_till;
};

// std::tuple
using auth_info_tuple = std::tuple<
  std::int64_t,
  std::int64_t,
  std::int64_t,
  std::time_t
>;

template <class T>
constexpr bool validate() {
    static_assert(std::is_trivially_move_constructible_v<T>);
    static_assert(std::is_trivially_copy_constructible_v<T>);
    static_assert(std::is_trivially_move_assignable_v<T>);
    static_assert(std::is_trivially_copy_assignable_v<T>);
    return true;
}

// std::tupleは特殊メンバ関数をほぼ自前定義しているので、trivialではない
constexpr bool tuples_fail = validate<auth_info_tuple>();
constexpr bool aggregates_are_ok = validate<auth_info_aggreagte>();

ただ、集成体には言語サポート(集成体初期化、必然的なtrivial性など)がある代わりに、ほぼライブラリサポートがありません。std::getなどを利用できず、ジェネリックなコードにおいては少し使いづらい事があります。

namespace impl {
  // ストリームから読みだしたデータでtupleを初期化する
  template <class Stream, class Result, std::size_t... I>
  void fill_fileds(Stream& s, Result& res, std::index_sequence<I...>) {
    (s >> ... >> std::get<I>(res)); // 集成体はstd::getを使用できないため、コンパイルエラー
  }
}

template <class T>
T ExecuteSQL(std::string_view statement) {
  std::stringstream stream;

  // ストリームにデータを入力するステップ、省略

  T result;
  impl::fill_fileds(stream, result, std::make_index_sequence<std::tuple_size_v<T>>());
  return result;
}

constexpr std::string_view query = "SELECT id, session_id, source_id, valid_till FROM auth";

const auto tuple_result = ExecuteSQL<auth_info_tuple>(query); // ok
const auto aggreagate_result = ExecuteSQL<auth_info_aggreagte>(query); // error!

std::getstd::tupleに対するライブラリサポートはtuple-likeな型(例えばstd::pairstd::array)ならば利用可能であるので、一般の集成体をtuple-likeな型として利用可能にすることで集成体にライブラリサポートを追加しよう、という提案です。

tuple-likeな型の条件はstd::tuple_sizeによってその長さが、std::tuple_elementによってその要素型が、そしてstd::getによってインデックスに応じた要素を取得できる事です。標準ライブラリにおいて、任意の集成体に対してこれらを用意(あるいは自動生成?)しておくようにする事で集成体にライブラリサポートを追加します。コア言語に変更は必要ありませんが、コンパイラによるサポートは必要そうです。
そして、それによってstd::tupleを用いている既存のコードは一切変更する事なく集成体でも利用できるようになります。

constexpr std::size_t elems = std::tuple_size<auth_info_aggreagte>::value;  // 4
using e2_t = std::tuple_element_t<2, auth_info_aggreagte>;  // std::int64_t

auth_info_aggreagte a = { 1, 2345, 6789, {}};
auto& e3 = std::get<3>(a);  // 6789

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

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

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

このリビジョンでの変更は、いくつかの機能の追加とそれを用いた既存機能の修正などです。

P2152R0 : Querying the alignment of an object

alignofを型だけではなくオブジェクトに対しても使用出来るようにする提案。

alingasによってオブジェクトと型に対してアライメントを指定する事ができますが、alignofでアライメントを取得できるのは型だけです。この挙動は一貫しておらず、GCCではオブジェクトに対してもalignof出来るようになっているためC++標準としても正式に許可しようとする提案です。

struct alignas(32) S {};  // ok

int main () {
  alignas(64) S obj{};  // ok

  std::size_t type_alignment = alignof(S);    // ok、32
  std::size_t  obj_alignment = alignof(obj);  // 現在はエラー
}

さらに、既存のアライメントに関しての空白部分やC言語との非互換性を改善する提案も同時に行なっています。

オブジェクトの型のアライメントとオブジェクトのアライメント指定について。

// 32バイト境界にアラインするように指定 in C
typedef struct U U;
struct U {
}__attribute__((aligned (32)));
// C++での等価な宣言
// struct alignas(32) U {};

int main() {
  // C言語の挙動
  _Alignas(16) U u; // ng、型のアライメント要求よりも弱いアライメント指定
  _Alignas(64) U v; // ok
  _Alignof(v);      // GNU拡張、64

  // 等価なはずのコードのC++での挙動
  alignas(16) U u;  // GCCとMSVCはok、Clangはエラー (1)
  alignof(u);       // GCCのみok、16
  alignas(64) U v;  // ok
  alignof(v);       // GNU拡張、64 MSVCはエラー (2)
}
  • (1) : 型よりも弱いアライメントを指定するalignasではオブジェクトを定義できないはずだが、C++にはこの場合の規定がない
    • 型のアライメント要求よりも弱いアライメント指定はエラーと明確に規定する
  • (2) : オブジェクト型に対するalignofは現在許可されていない
    • この提案のメインの部分によって許可する

型のアライメントとメンバ変数のアライメントについて。

typedef struct V V;
typedef struct S S;
typedef struct U U;

struct V {} __attribute__((aligned (64)));
struct S {} __attribute__((aligned (32)));
struct U {
  S s;
  V v;
} __attribute__((aligned (16))); // GGCおよびclangはこのアライメント要求を無視する

/*
C++での等価な宣言
struct alignas(32) S {};
struct alignas(64) V {};
struct alignas(32) U {  // GCCはこのアライメント要求を無視、clangはエラー、MSVCは警告 (1)
  S s;
  U u;
};
*/

int main() {
  // C言語の挙動
  _Alignof(U);  // ok、64

  // 等価なはずのコードのC++での挙動
  alignof(U);   // GCCとMSVCではok、64  (2)
}
  • (1) : 型へのアライメント要求がそのメンバのアライメント要求よりも弱い場合の規定がC++にはない
    • 型へのアライメント要求がそのメンバのアライメント要求よりも弱い場合はエラーと明確に規定する
  • (2) : (1)の場合にアライメントをどうするのかの規定もない(ただし、構造体のアライメントはメンバのアライメントによって制限されるということを示す記述はある)
    • エンティティ(型)のアライメントはそのメンバと同じかそれよりも強くなければならない、と明確に規定する。

こうしてみると、C言語がしっかりとしている一方でC++は深く考えてなかった感があります・・・

P2161R1 : Remove Default Candidate Executor

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

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

このリビジョンでは、単純にassociated_executorからsystem_executorを削除してしまうと、Networking TS内にある別の機能である​defer, dispatch​, ​postが深刻な影響を受けてしまうようで、それについての問題点と対策が追記されています。他には、5月に行われたSG4でのレビューについて追記されています。

P2164R1 : views::enumerate

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

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

このリビジョンでは、インデックスの型の指定が変更されました。以前は1つ前の範囲の差分型(difference type)をインデックスの型に使用していましたが、1つ前の範囲のranges::size()の返す型が取得できる場合はそれを、できない場合は差分型と同じ幅の符号なし整数型を使用する、という風に変更されました。要は常に符号なし整数型を使用するようになったという事でしょう。

P2166R0 : A Proposal to Prohibit std::basic_string and std::basic_string_view construction from nullptr

std::stringstd::string_viewnullptrから構築できないようにする提案。

std::stringstd::string_viewにはconst char*を受けるコンストラクタ用意されており、nullptrを直接渡すとそのコンストラクタが選択され、未定義動作に陥ります。こんなコードは書かないだろうと思われるのですが、筆者の方の調査によればLLVMを含む少なくとも7つのプロジェクトでこのようなコードが発見されたそうです。

実装によっては実行時アサーションによってエラーにするものもあるようですが、std::nullptr_tを受けるコンストラクタをdeleteする事でそのような診断をコンパイル時に行おうとするものです。

P2176R0 : A different take on inexpressible conditions

契約プログラミングにおいて事前/事後条件をC++コードとして記述する際、チェックされない条件を記述する構文についての提案。

現在C++標準ライブラリでは処理の事前条件や事後条件を文章で指定していますが、契約プログラミングによってそれらをC++コードとして記述することが出来るようになります(予定)。その際、実行時であってもそのチェックが難しいか出来ない条件については、注釈という形で書いておくことが出来るようになっています。例えば、文字列のnull終端要求や、イテレータendへの到達可能性などがあります。

bool is_null_terminated(const char *); // 定義しない

// 文字列はnullでなくnull終端されている、という2つの事前条件が契約されている
void use_str(const char* s)
  [[expect: s != nullptr]]                  // この条件はチェックされる
  [[expect axiom: is_null_terminated(s)]];  // この条件は注釈であり、チェックされない

// 文字列はnullであるかnull終端されている、という事前条件が契約されている
void use_opt_str(const char* s)
  [[expect axiom: s == nullptr || is_null_terminated(s)]]; // この条件全体は注釈であり、チェックされない

この様に、axiomと指定された条件は注釈であり実行時にチェックされません。

この提案はこの構文を変更し、事前・事後条件に注釈であることを書くのではなく、関数宣言の方に注釈のためのものであることを表示するようにするものです。

// axiomをこっちに付ける
axiom is_null_terminated(const char *); // 定義なし

void use_str(const char* s)
  [[expect: s != nullptr]]           // この条件はチェックされる
  [[expect: is_null_terminated(s)]]; // この条件は注釈であり、チェックされない

void use_opt_str(const char* s)
  [[expect: s == nullptr || is_null_terminated(s)]]; // nullチェックは行われるが、is_null_terminatedは注釈でありチェックされない

このようにする事で、注釈となる条件とそうでないものを混ぜて書きながら実行可能な条件をチェックしてもらう事が出来るようになリます。OR条件の場合は条件を複数に分割して書く訳にもいかないので特に有用です。

axiomとマークされた関数は契約の構文の中でのみ使用でき、何らかの述語としてbool値を返す関数だがチェックが困難であることを表現し、実行時には単にtrueを返す条件として扱われます。それ以外はほとんど通常の関数と同様に扱えるものです。ただし、そのために記述する順番には制約がかかります。

void use_opt_str(const char* s)
  [[expect: is_null_terminated(s) || s == nullptr]];  // ng、チェック可能な条件を先に書く必要がある

P2178R0 : Misc lexing and string handling improvements

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

現在のC++の字句規則はユニコード以前の世界で定義されたもので、文字コードを具体的に指定せずに構成されています。しかし、それによって実装定義の部分が広くなり実装による差異が多く発生していたり、そもそも人間に理解しづらかったりしています。
この提案はそれらを改善しユニコードの振る舞いをより明確にしつつ、実装間の差異をなるべく縮小することを目指したものです。全部で12個の提案が含まれています。

C++コンパイラ書く人とかC++コンパイラになりたい人は読んでみると面白いかもしれません。

P2179R0 : SG16: Unicode meeting summaries 2020-01-08 through 2020-05-27

SG16(Unicode Study Group)のミーティングにおける議論の要旨をまとめた文書。

例えば先ほど出てきていたP1949: C++ Identifier Syntax using Unicode Standard Annex 31などの提案やIssue等についての議論の様子が記載されています。

P2181R0 : Correcting the Design of Bulk Execution

進行中のExecutor(P0443)提案中のbulk_executeのインターフェースを改善する提案。

bulk_executeはバルク実行のためのカスタマイゼーションポイントオブジェクトで、カスタムされたbulk executorを使用することによってハードウェアやOSが提供するバルク実行API(例えば、SIMDやスレッドプール)によって効率的なバルク処理を行う事を可能にするためのものです。

// P0443R13より、サンプル
// ex = executor, f = バルク処理, rng = バルク処理の対象となるデータのシーケンス
template<class Executor, class F, class Range>
void my_for_each(const Executor& ex, F f, Range rng) {
  // バルク実行を要求し、senerを取得する
  // ここで、exにカスタムbulk executorを渡せばバルク実行をカスタマイズできる
  sender auto s = execution::bulk_execute(ex, [=](size_t i) {
    f(rng[i]);
  }, std::ranges::size(rng));

  // 実行を開始し処理の完了を待機
  execution::sync_wait(s);
}

ただ、bulk_executeは提案の初期から存在しており、P0443は途中で遅延実行のためにsender/recieverによるアプローチを採用しましたが、bulk_executeはそれらの変更に追随しておらずインターフェースが一貫していませんでした。この提案はそれを解決するものです。主に以下の3点を変更します。

  • 既存のexecute(CPO)とセマンティクスを統一し、bulk_executeは与えられた作業を即座に実行する実行用インターフェースとする
  • 遅延実行用bulk_executeであるbulk_schedule(CPO)を導入する(executeに対するscheduleと同様)
  • bulk_scheduleによって返されるsenderに対する要件を制約し明確化するmany_receiver_ofコンセプトを導入する
    • このsenderではset_value()が繰り返し呼び出される事を許可する

これらの変更の提案はP0443R13に対してのもので、現在のExecutorライブラリの要件やコンセプト、セマンティクスを大きく変更しません。bulk_executeexecute/schedulesender/recieverとのセマンティクスの一貫性を改善し、Executorライブラリをより使いやすくするものです。

namespace std::execution {
  // bulk_executeの宣言
  void bulk_execute(executor auto ex,
                      invocable<executor_index_t<decltype(ex)> auto f,
                      executor_shape_t<decltype(ex)> shape);
}

// 任意のexecutorと処理対象データ列
auto executor = ...;
std::vector<int> ints = ...:

// intのvectorを変更する作業をexecutorに投入する、ただし実行タイミングは実装定義
bulk_execute(executor,
             [&](size_t idx) { ints[i] += 1; },
             vec.size());

// ここでintsを他の処理に使用する場合、同期等の配慮が必要になるかもしれない
namespace std::execution {
  // bulk_scheduleの宣言
  sender auto bulk_schedule(executor auto ex,
                            executor_shape_t<decltype(ex)> shape,
                            sender auto prologue);
}

// 任意のexecutorと処理対象データ列
auto executor = ...;
std::vector<int> ints = ...:

// intのvectorを変更する作業を構成する、まだ実行はされない
auto increment =
    bulk_schedule(executor, vec.size(), just(ints)) |
    transform([](size_t idx, std::vector<int>& ints) {
        ints[i] += 1;
    });

// ここでのintsの変更は安全

// 作業を開始する、ここでは処理をハンドルしないのでnull_receiverに接続する
execution::submit(increment, null_receiver{});

// ここでは処理はすべて終了している

P2182R0 : Contract Support: Defining the Minimum Viable Feature Set

C++20で全面的に削除されたContractsのうち、議論の余地がなく有用であった部分と削除の原因となった論争を引き起こした部分とに仕分けし、有用であった部分だけを最初のC++ Contractsとして導入する事を目指す提案。

C++20において最終的にContractsが削除されることになってしまったのは、主に以下の機能が議論を巻き起こし合意が取れなくなったためです。

  • 継続モード
  • ビルドレベル
  • 上記も含めた、制御がグローバルであること
  • Literal semantics(in-source controls)
    • 個々の契約に対して個別にチェックするか否かを指定したり、それがグローバルフラグの影響を受けないようにしていた
  • Assumption
    • (上記の事によって)axiomではないのにチェックされていない契約条件の存在が想定される

提案ではC++20Contractsからこれらの部分を除いた広く合意の取れていた有用な部分をMVP(Minimum Viable Product)と呼称し、MVPを最初のContractsとして導入し、そうでない部分(上記の5項目)についてはより時間をかけて議論し、追加の機能として導入していくことを提案しています。

P2184R0 : Thriving in a crowded and changing world: C++ 2006-2020

2020年6月のHistory Of Programming Languages (HOPL) で発表されるはずだったBjarne StroustrupさんによるC++の歴史をまとめた論文の紹介文書。

6月のHOPLカンファレンス延期されましたが論文は公開されているようです。英文PDF168Pの超大作ですが、とても興味深そうな内容です(翻訳お待ちしております)。

HOPLは15年毎に開催されるようで、C++はHOPLで3回紹介されたただ一つの言語になり、BjarneさんはHOPLで3回論文を書いたただ一人の人になったようです。次は2035年ですが、C++はそこでも登場することができるでしょうか・・・?

P2185R0 : Contracts Use Case Categorization

Contractsユースケースを「何のために使用するか」と「どうやって使用するか」2つにカテゴライズし、報告されている既存のユースケースをカテゴライズする文書。

これは提案文書ではなく、SG21(Contracts Studt Group)での議論のための報告書です。

P2187R0 : std::swap_if, std::predictable

新しい標準アルゴリズムであるstd::swap_ifstd::predictableの提案。

std::sortに代表される標準ライブラリ中の多くのアルゴリズムには次のような典型的な条件付きswapが頻出します。

if (*right < pivot) {
  std::swap(*left, *right);
  ++left;
} 

この様なswap-if操作は実装を少し変更するだけで、分岐予測のミスによるパイプラインストールを回避しパフォーマンスを2倍以上改善できるらしく、std::swap_ifはそのためのより効率的なswap-if操作を提供するものです。

次のような実装になるようです。

template <movable T>
bool swap_if(bool c, T& a, T& b) {
  T tmp[2] = { move(a), move(b) };
  b = move(tmp[1-c]), a = move(tmp[c]);
  return c;
}

bool値がfalse == 0true == 1であることを利用して、条件分岐を配列のインデックスに帰着させています。
これを用いると先ほどの典型的な操作は次のように書けます。

left += swap_if(*right < pivot, *left, *right);

ただし、現在のC++コンパイラはこの様なコードに対して必ずしも最適な(cmovを使った)コードにコンパイルすることができず、せいぜい次善のコードを出力する場合が多いようです。ただ、その場合でも通常のswap-ifによるstd::sortよりも高速なので、標準ライブラリとしてstd::swap_ifを規定し効率的な実装が提供されるだけでも典型的なswap-if操作の性能向上が図れます。

また、std::swap_ifを規定することはコンパイラによるのぞき穴最適化の機会を提供することに繋がり、将来的に多くのコンパイラが最善のコードを出力できるようになるかもしれません。

ただし、std::swap_ifの上記の様な実装は多くのケースでは高速ですが、特定のデータに対してはかえって低速になります(例えば、ほとんどソート済みの配列のようなデータ列など)。それが事前に予測できる場合、通常の分岐によるswap-if操作にフォールバックできる必要があります(現在のハードウェアでは、その閾値は90%以上の確度が必要)。

2つ目のstd::predictableはそのための述語ラッパー型です。

template <predicate Predicate, bool is = true>
struct predictable {
  std::remove_reference<Predicate>::type pred; // 名前は自由

  explicit predictable(Predicate&& p) : pred(p) {}

  template <typename... Args>
  constexpr bool operator()(Args&&... args) { 
    return ::std::invoke(p, args...);
  }
};

// predictableを検出する変数テンプレート
template <typename>
constexpr bool is_predictable = false;

template <predicate P, bool is>
constexpr bool is_predictable<predictable<P,is>> = is;

標準ライブラリの述語を引数に取るアルゴリズムでは、これを用いて述語をラップして渡し、アルゴリズム中でそれを検出してstd::swap_ifを使用するかをコントロールします。

auto v = std::vector{ 3, 5, 2, 7, 9 };

std::sort(v.begin(), v.end()); // swap_ifを使用する
std::sort(v.begin(), v.end(),  // swap_ifを使用しない
          std::predictable([](int a, int b) { return a > b; }));

std::predictableは単なる述語ラッパーであるため、従来の述語を取るアルゴリズムは何ら変更することなくこれを受け入れ、使用できます。一方で、std::swap_ifを使用しかつ述語を取るアルゴリズムでは、これを検出することで最適な実装を選択できるようになります。
これによって、標準ライブラリにstd::swap_ifを使用するかしないかを選択するための従来のアルゴリズム名それぞれに対応する新しい名前を導入したり、既存のアルゴリズムの規定を変更したりすることなく、標準アルゴリズムの多くでパフォーマンス向上と最適な実装の選択を同時に達成できるようになります。

P2188R0 : Zap the Zap: Pointers should just be bags of bits

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

[basic.stc] p4には、「Any other use of an invalid pointer value has implementation-defined behavior.(無効なポインタ値の他の使用には実装定義の振舞がある)」とあり、その注釈には「Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.(一部の実装では、無効なポインタのコピーを行うとシステム生成の実行時エラーが発生する、と定義している場合がある)」とあります(これらの規定のことをpointer zapと呼んでいるようです)。
一方でこの事は、[basic.types] p3にある規定及びポインタ型がtrivially copyableであることと明らかに矛盾しています。

提案はいくつかの例を示すとともに、これら規定を削除して無効なポインタはtrivially copyableであり比較可能と規定するか、ポインタ型はtrivially copyableではないと規定するか、どちらかを選択すべきと主張しています。提案としては前者が提案されています。

#include <assert.h>
#include <string.h>

int main() {
  int* x = new int(42);
  int* y = nullptr;
  
  // ポインタの値(参照先ではない)をx -> yへコピーする
  memcpy(&y, &x, sizeof(x));

 // ポインタyは有効化される
  assert(x == y);
  assert(*y == 42);
}

[basic.types] p3にある例をint*に特殊化したコードで、ポインタ型はtrivially copyableであるためこのコードは有効であり、yxと同じものを指すようになります。

#include <assert.h>
#include <string.h>

int main() {
  int* x = new int(42);
  int* y = nullptr;
  unsigned char buffer[sizeof(x)];

  //ポインタの値(参照先ではない)をbufferを介してx -> yへコピーする
  memcpy(buffer, &x, sizeof(x));
  memcpy(&y, buffer, sizeof(x));

  // ポインタyは有効化される
  assert(x == y);
  assert(*y == 42);
}

先ほどのサンプルを中間bufferを介して行ったもの。[basic.types] p2にあるように、ポインタ型はtrivially copyableであるためこのコードは有効です。

#include <assert.h>
#include <string.h>
#include <stdint.h>

int main() {
  int* x = new int(42);
  int* y = nullptr;

  // ポインタ値を対応する数値表現に変換したうえでx -> yにコピーする
  uintptr_t temp = reinterpret_cast<uintptr_t>(x);
  y = reinterpret_cast<int*>(temp);

  // ポインタyは有効化される
  assert(x == y);
  assert(*y == 42);
}

[expr.reinterpret.cast] p5にあるように、ポインタ値を整数型にキャストしてから再びポインタ値に戻した場合でもポインタとしては有効であり続けます。

他にも込み入った例が全部で10パターン紹介されています。しかしここで見ただけでもわかるように、標準は少なくとも有効なポインタから無効なポインタへのその値のコピーは有効であることを示しており、([basic.stc] p4にあるような)無効なポインターのコピーが実装定義であるという規定を削除すべきという主張のようです。

onihusube.hatenablog.com

この記事のMarkdownソース

[C++]カスタマイゼーションポイントオブジェクト(CPO)概論

C++20以降の必須教養となるであろうカスタマイゼーションポイントオブジェクトですが、その利便性の高さとは裏腹に理解が難しいものでもあります。これはその理解の一助となるべく私の頭の中の理解を書き出したメモ帳です。

C++17までのカスタマイゼーションポイントの問題点

C++17までにカスタマイゼーションポイントとなっていた関数(例えばstd::begin()/std:::end(), std::swap()など)にはアダプトして動作をカスタマイズするためのいくつかの方法が用意されており、より柔軟に自分が定義した型を適合できるようになっています。しかしその一方で、それによって使用するときに少し複雑な手順を必要としていました。例えばstd::begin()で見てみると

// イテレート可能な範囲を受けて何かする関数
template<typename Container>
void my_algo(Container&& rng) {
  using std::begin;

  // 先頭イテレータを得る
  auto first = begin(rng);
}

真にジェネリックに書くためにはこのように「std::begin()usingしてから、begin()名前空間修飾なしで呼び出す」という風に書くことで、std名前空間のもの及び配列にはstd::begin()が、ユーザー定義型に対しては同じ名前空間内にあるbegin()あるいはstd::begin()を通してメンバ関数begin()が呼び出されるようになります。しかし、手順1つ間違えただけでそのbegin()の呼び出しはたちまち汎用性を失います。これはstd:::end(), std::swap()等他のカスタマイゼーションポイントでも同様です。

C++17までのカスタマイゼーションポイントにはこのように、その正しい呼び出し方法が煩雑でそれを理解するにはC++を深めに理解する事が求められるなど、使いづらいという問題があります。

また、このようなカスタイマイゼーションポイントは標準ライブラリをよりジェネリックにするために不可欠な存在ですが、標準ライブラリはそのカスタマイゼーションポイントの名前(関数名)だけに着目して呼び出しを行うため、同名の全く異なる意味を持つ関数が定義されていると未定義動作に陥ります。特に、ADLが絡むとこれは発見しづらいバグを埋め込む事になるかもしれません。したがって、カスマイゼーションポイントを増やすと言う事は実質的に予約されている名前が増える事になり、ユーザーは注意深く関数名を決めなければならないなど負担を負うことになります。

C++20からのコンセプトはそのような問題を解決します。その呼び出しにおいてコンセプトを用いて対象の型が制約を満たしているかを構文的にチェックするようにし、カスタマイゼーションポイントに不適合な場合はオーバーロード候補から外れるようにする事で、ユーザーがカスタマイゼーションポイントとの名前被りを気にしなくても良くなります。結果的に、標準ライブラリにより多くのカスタマイゼーションポイントを設ける事ができるようになります。

しかし、コンセプトによって制約されたC++20カスタマイゼーションポイントの下では、先程のC++17までのカスタマイゼーションポイント使用時のベストプラクティスコードがむしろ最悪のコードになってしまうのです。

namespace std {

  // rangeコンセプトを満たす型だけが呼べるように制約してある新しいbegin()関数とする
  template<std::ranges::range C>
  constexpr auto begin(C& c) -> decltype(c.begin());  // (1)
}

namespace myns {

  struct my_struct {};

  // イテレータを取得するものではないbegin()関数
  bool begin(my_struct&);  // (2)
}


template<typename Container>
void my_algo(Container&& rng) {
  using std::begin;

  // 先頭イテレータを得る、はずが・・・
  auto first = begin(rng);  // my_structに対しては(2)が呼び出される
}

int main() {
  myns::my_struct st{};

  my_algo(st);  // ok、呼び出しは適格
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

このように、せっかくコンセプトで制約したにも関わらずADL経由で制約を満たさないbegin()が呼ばれています。別の見方をすれば、コンセプトによる制約を簡単に迂回できてしまっています。

これでは結局ユーザーはカスタマイゼーションポイント名を気にしてコードを書かなければならなくなるし、カスタマイゼーションポイントがコンセプトによって制約してあっても意味がなくなってしまいます・・・・

Customization Point Object(CPO)

カスタマイゼーションポイントオブジェクト(Customization Point Object)はこれら2つの問題を一挙に解決しつつ、将来的なカスタマイゼーションポイントの拡張も可能にしている素敵な魔法のようなすごいやつです!

例えば、これまでのstd::begin()に対応するカスタマイゼーションポイントオブジェクトであるstd::ranges::beginは次のように定義されます。

namespace std::ranges {
  inline namespace /*unspecified*/ {

    inline constexpr /*unspecified*/ begin = /*unspecified*/;
  }
}

unspecifiedなところは名前や型が規定されていない(実装定義である)ことを意味します。そして、このstd::ranges::beginは関数オブジェクトです。std::ranges::begin(E)のように呼び出してさも関数であるかのように使います。

std::ranges::begin(E)のように呼ばれた時、その引数の式Eによって以下のいずれかの処理を実行します(以下、TEの型、tは式Eの結果となる左辺値)。上から順番にチェックしていきます。

  1. Eが右辺値であり、std::ranges::enable_borrowed_range<remove_cv_t<T>> == falseならば、呼び出しは不適格。
  2. Tが配列型であり、 std::remove_all_extents_t<T>が不完全型ならば、呼び出しは不適格(診断不要)。
  3. Tが配列型であれば、std::ranges::begin(E)は式t + 0expression-equivalent
  4. decay-copy(t.begin())が有効な式であり、その結果の型がstd::input_or_output_iteratorコンセプトのモデルとなる(満たす)場合、std::ranges::begin(E)decay-copy(t.begin())expression-equivalent
  5. Tがクラス型か列挙型であり、decay-copy(begin(t))が有効な式であり、その結果の型がstd::input_or_output_iteratorコンセプトのモデルとなり、非修飾のbegin()に対する名前探索が以下2つの宣言だけを含むコンテキストでオーバーロード解決が実行される場合、std::ranges::begin(E)はそのコンテキストで実行されるオーバーロード解決を伴うdecay-copy(begin(t))expression-equivalent
// std::begin()を含まないコンテキストでオーバーロード解決をするということ
void begin(auto&) = delete;
void begin(const auto&) = delete;

「式Aは式Bとexpression-equivalent」というのは簡単に言うと式Aの効果は式Bと等価であり、式Aが例外を投げるかと定数実行可能かどうかも式Bと等価と言うことです。この場合の式Bは引数E由来なので、std::ranges::begin(E)の呼び出しが例外を投げるかどうかと定数実行可能かどうかは引数の型次第と言うことになります。

詳しく見ていくと、1,2番目の条件はまず呼び出しが適格ではない事が型レベルで分かるものを弾く条件です。enable_borrowed_rangeと言うのは右辺値のrangeであってもイテレータを取り出して操作する事が安全に行えるかを示すbool値です(たぶん)。
3番目以降がstd::ranges::beginの主たる効果です。3番目は配列の先頭のポインタを返します。t + 0というのは明示的にポインタにしてるようです。
4番目はメンバ関数として定義されたbegin()を呼び出します。標準ライブラリのほとんどの型がこれに当てはまります。
5番目はTと同じ名前空間にあるフリー関数のbegin()を探して呼び出すものです(Hidden friendsもここで探し出されます)。この時、std::begin()を見つけないようにするためにオーバーロード解決についての指定がなされています。

ユーザーがこのstd::ranges::beginにアダプトするときは、4番目か5番目に適合するようにしておきます。つまり、従来とやることは変わりません。一方、このstd::ranges::beginを使用する場合は逆に従来のような煩雑コードを書かなくてもよくなります。これまでやっていたことと同等(以上)のことを中で勝手にやってくれるようになります。

template<typename Container>
void my_algo(Container&& rng) {
  // using std::beginとかをしなくても、同じことを達成でき、よりジェネリック!
  auto first = std::ranges::begin(rng);
}

これによってまず、1つ目の問題(呼び出しが煩雑、使いづらい)が解消されている事がわかるでしょう。

さらに、ユーザー定義型に対しても行われうる4,5番目の処理では、戻り値型にコンセプトによる制約が要求されています。std::input_or_output_iteratorはインクリメントや間接参照等イテレータに要求される最小限のことを制約するコンセプトで、これによって使用されるbegin()イテレータを返さない場合にstd::ranges::begin(E)の呼び出しが不適格になります。そして、カスタマイゼーションポイントの呼び出しが診断可能な不適格となる場合は単にオーバーロード解決の候補から外れ、他に候補があれば別の適切な関数が呼び出されることになります。

namespace myns {

  struct my_struct {};

  // イテレータを取得するものではないbegin()関数
  bool begin(my_struct&);
}

int main() {
  myns::my_struct st{};

  std::ranges::begin(st);  // ng、戻り値型がinput_or_output_iteratorを満たさないためコンパイルエラー
}

こうして、2つ目の問題の一部(別の意味を持つ関数も呼び出してしまう)も解決されている事がわかりました。

関数オブジェクトとADL

最後に残ったのは、ADLによってカスタマイゼーションポイント呼び出しをフックできる、あるいは要求される型制約を無視できてしまう問題です。これはCPOが関数オブジェクトである事によって防止されます。

C++における名前探索では修飾名探索と非修飾名探索を行なった後、引数依存名前探索(ADL)を行いオーバーロード候補集合を決定します。この時、非修飾名探索の結果に関数以外のものが含まれているとADLは行われません。逆に言うと、ADLは関数名に対してしか行われません。つまり、関数オブジェクトに対してはADLは発動しません(6.5.2 Argument-dependent name lookup [basic.lookup.argdep])。

カスタマイゼーションポイントオブジェクトが関数オブジェクトであることによって、usingして使った時でも同名の関数によってADLでフックする事は出来なくなります。

namespace myns {

  struct my_struct {};

  // イテレータを取得するものではないbegin()関数
  bool begin(my_struct&); // (2)
}


template<typename Container>
void my_algo(Container&& rng) {
  using std::ranges::begin;

  // 先頭イテレータを得る
  auto first = begin(rng);  // std::ranges::beginが呼び出され、(2)は呼び出されない
                            // 戻り値型がinput_or_output_iteratorコンセプトを満たさないためコンパイルエラー
}

int main() {
  myns::my_struct st{};

  my_algo(st);  // ng
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

これらのように、カスタマイゼーションポイントオブジェクトではC++17までのカスタマイゼーションポイントに存在した問題が全て解決されている事が確認できたでしょう。

Template Method

遥か未来の世界で、イテレータを取得するのにbegin()だけではなく別の方法が追加された場合を考えてみます。例えば、first()関数が今のbegin()と同じ意味を持ったとします。その世界で統一的な操作としてstd::ranges::beginを使い続けるにはどうすればいいでしょうか?また、ユーザーは何をすべきでしょう?

答えは簡単です。先ほど5つほど羅列されていたstd::ranges::beginの条件にもう2つほど加えるだけです。標準ライブラリの実装は修正が必要ですが、それを利用するユーザーが何かをする必要はありません。first()関数がイテレータを返すようになった世界でもstd::ranges::beginを使い続けていれば何も変更する事なくイテレータを得る事ができます。

このように、C++20のカスタマイゼーションポイントオブジェクトはカスタマイゼーションポイントを追加する方向の変更に対して閉じています(そして、おそらく削除する変更は行われない)。ユーザー目線で見れば、そのような変更が行われたとしてもカスタマイゼーションポイントオブジェクトのインターフェースは常に安定しています。

このように、カスタマイゼーションポイントオブジェクトはよりジェネリックかつ静的なTemplate Methodパターン(あるいはNVI)だと見る事ができます。

inline名前空間

標準ライブラリのカスタマイゼーションポイントオブジェクトは、先ほど見たようになぜかinline名前空間に包まれています。

これはおそらく、将来行われうる変更に対してもABI互換性を維持するための布石です。

正しくは、CPOがstd名前空間にあるとき、標準ライブラリにあるクラスで定義されているHidden friends関数とCPOとで名前が衝突するため、それを回避するためのものです。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // swap CPO #1
  inline constexpr cpo_impl::swap_cpo swap{};
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

この例では#1と#2の異なる宣言が同じ名前空間にあるためにコンパイルエラーになっています。

この様な問題はメンバ関数との間では起こらず、非メンバ関数との間で起こります。正確にはswapなどほとんどのCPOはstd::ranges名前空間にありますが、Rangeライブラリのviewなど、Hidden friendsでCPOにアダプトする型との間で同様の問題が発生します。

この問題は、CPOをinline名前空間で囲むことによって解決されます。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // CPO定義をinline名前空間で囲う
  inline namespace cpo {
    // swap CPO #1
    inline constexpr cpo_impl::swap_cpo swap{};
  }
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

こうしてもmystd::swapという名前でswapCPOを参照できますし、CPOの内部からSに対するswapをADLによって正しく呼び出すことができます。しかし、#1と#2のswapは別の名前空間にいるために名前は衝突していません。

この様な事情から、標準ライブラリにあるCPOはほとんどのものがinline名前空間に包まれています。

その他の性質

標準ライブラリのカスタマイゼーションポイントオブジェクトは全て、リテラル型かつsemiregularであると規定されています。これはつまり、constexprにコピー・ムーブ・デフォルト構築/代入可能であると言う事です。

そしてそれら複数のインスタンスのカスタマイゼーションポイントオブジェクトとしての効果は呼び出しに使うインスタンスによって変化しない事も規定されています。

これらの性質によって、高階関数など関数オブジェクトを取るユーティリティでカスタマイゼーションポイントオブジェクトを自由に利用する事ができます。これはまた、従来のカスタマイゼーションポイント関数と比較した時のメリットでもあります(関数ポインタはジェネリックではいられないなど)。

実装してみよう!

言葉で語られても良く分からないのでここまで説明に使ってきたstd::ranges::beginを実装してみましょう。百聞は一見に如かずです。とはいってもstd名前空間ではなくオレオレ名前空間に書いてみます。

#include <concepts>

namespace mystd {
  
  namespace detail {
    
    // beginの実装型
    struct begin_impl {
      
      // 関数呼び出し演算子オーバーロードで定義していく
      template<typename E>
      constexpr auto operator()(E&&) const;
    };
  }
  
  inline namespace cpo {
    
    // ターゲットのカスタマイゼーションポイントオブジェクト begin
    inline constexpr detail::begin_impl begin{};
    
  }
}

概形はこんな感じで、detail::begin_implクラスの関数呼び出し演算子オーバーロードすることで実装していきます。

以下では、GCC10.1の実装を参考にしてます。

ケース1 右辺値

これは不適格な呼び出しとなり、該当する候補があるとハードエラーになってしまうので何も定義しません。ただし、右辺値かつstd::ranges::enable_borrowed_range<remove_cv_t<T>> == falseの場合は呼び出し不可でそうでない場合は呼び出し可能なのでそのようにしておきます。

struct begin_impl {

  template<typename T>
    requires std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>
  constexpr auto operator()(T&&) const;
};

右辺値かつstd::ranges::enable_borrowed_range<remove_cv_t<T>> == falseの時は呼び出しはill-formedにするので、その否定の場合は通すようにします。全体を否定してド・モルガンをした条件を制約しておきます。なぜ左辺値参照判定してるかというと、テンプレート引数推論の文脈ではT&&に右辺値が渡ってくるとTはそのままTに、左辺値が渡ってくるとTT&になって全体としてT&&& -> T&となるからです。つまり、この場合のテンプレートパラメータTは左辺値が渡された場合は左辺値参照となります。

この状態で右辺値vectorを入れると呼び出し可能な関数が無いとエラーになるので上手くいっていそうです。

ケース3 配列型、ケース2 不完全型の配列

先程の仮実装に配列判定を入れましょう。配列の要素型が不完全型である場合はここで弾いてやります。

// 不完全型の配列判定
template<typename T>
concept is_complete_array = requires {
  sizeof(std::remove_all_extents_t<std::remove_reference_t<T>>);
};

struct begin_impl {

  template<typename T>
    requires (std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>) and
              std::is_array_v<std::remove_reference_t<T>>
  constexpr auto operator()(T&& t) const noexcept {
    static_assert(is_complete_array<std::remove_all_extents_t<T>>, "Array element type is incomplete");
    return t + 0;
  }
};

不完全型の配列の時は診断不用とあり、SFINAEすることも求められないのでstatic_assertでハードエラーにします。

この状態で右辺値vectorを入れると適切にエラーになり、左辺値配列を入れると呼び出しは成功します。どうやら上手くいっているようです。

ケース4 ユーザー定義型のメンバbegin

コンセプトでメンバbeginが呼び出し可能かどうかを調べてやります。decay-copyauto戻り値型が勝手にやってくれるはず・・・

struct begin_impl {

  template<typename T>
    requires (std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>) and
             requires(T t) { {t.begin()} -> std::input_or_output_iterator; }
  constexpr auto operator()(T&& t) const noexcept(noexcept(t.begin())) {
    return t.begin();
  }
};

requires節の中のrequires式でメンバ関数beginが呼び出し可能かどうかをチェックします。ついでに戻り値型の制約もチェックしておきます。この場合でも右辺値でenable_borrowed_rangetrueならば呼び出しは可能(そうでなければSFINAEする)なので先程の条件を同時に指定しておく必要があります。

expression-equivalentというのもconstexpr指定と呼び出す式によるnoexcept二段重ねで自動化できます。

左辺値のvectorとかを入れてやるとエラーにならないので行けてそうですね。

ケース5 ユーザー定義型の非メンバbegin()

オーバーロードに関わる部分はstd::beginを含まないコンテキストで、という事なのでstd名前空間の外で実装するときには触らなくてよかったりします。それ以外は先ほどのメンバ関数ケースの時と同様に書けます。

struct begin_impl {

  // ケース5 メンバ関数begin
  template<typename T>
    requires (std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>) and
             (not requires(T t) { {t.begin()} -> std::input_or_output_iterator; }) and
             requires(T t) { {begin(t)} -> std::input_or_output_iterator; }
  constexpr auto operator()(T&& t) const noexcept(noexcept(begin(t))) {
    return begin(t);
  }
};

ただし素直にやると先ほどのケース4と曖昧になってしまうので、メンバ関数begin()を持たない場合、と言う条件を付け加えます(ケース4で追加した制約式の否定)。

適当にstd::vectorをラップしたような型を作って非メンバでbegin()を用意してやるとテストできます。大丈夫そうです。

完成!

#include <concepts>
#include <ranges>

namespace mystd {
  
  namespace detail {

    // 不完全型の配列判定
    template<typename T>
    concept is_complete_array = requires {
      sizeof(std::remove_all_extents_t<std::remove_reference_t<T>>);
    };
    
    struct begin_impl {
      
      // ケース3 配列型
      template<typename T>
        requires (std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>) and
                  std::is_array_v<std::remove_reference_t<T>>
      constexpr auto operator()(T&& t) const noexcept {
        // ケース2をエラーに
        static_assert(is_complete_array<std::remove_all_extents_t<T>>, "Array element type is incomplete");
        return t + 0;
      }
      
      // ケース4 メンバ関数begin
      template<typename T>
        requires (std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>) and
                 requires(T t) { {t.begin()} -> std::input_or_output_iterator; }
      constexpr auto operator()(T&& t) const noexcept(noexcept(t.begin())) {
        return t.begin();
      }
      
      // ケース5 非メンバ関数begin
      template<typename T>
        requires (std::is_lvalue_reference_v<T> or std::ranges::enable_borrowed_range<std::remove_cv_t<T>>) and
                 (not requires(T t) { {t.begin()} -> std::input_or_output_iterator; }) and
                 requires(T t) { {begin(t)} -> std::input_or_output_iterator; }
      constexpr auto operator()(T&& t) const noexcept(noexcept(begin(t))) {
        return begin(t);
      }
    };
  }
  
  inline namespace cpo {
    
    // オレオレstd::ranges::beginカスタマイゼーションポイントオブジェクト!
    inline constexpr detail::begin_impl begin{};
    
  }
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

正しいかはともかく、それっぽいものができました。カスタマイゼーションポイントオブジェクトの多くは大体この様に実装できます。コンセプトを使うと非常に簡単になるだけで、C++17以前の環境でもSFINAEを駆使するなどして実装することができます。この例ではオーバーロードに分けましたが、関数1つにまとめてconstexpr ifを使うと言う手もあります(GCCの実装はそっち)。

実装を知ってみれば、素敵な魔法というよりはそこそこ愚直な力業でできており、少し不透明さが取り払えることでしょう。

これまでのカスタマイゼーションポイント

本来は従来のカスタマイゼーションポイントとなっている関数をカスタマイゼーションポイントオブジェクトに置き換えたかったようですが、それは互換性の問題からできなかったようです。そのため、カスタマイゼーションポイントオブジェクトは別の名前空間に同名で定義されています。

ここまで見たことから分かるように、関数ではコンセプト時代のカスタマイゼーションポイントとしてはふさわしくないため、残っているのはほとんど後方互換のためでしょう。C++20以降だけをターゲットに出来るなら、それらの関数を使わずにカスタマイゼーションポイントオブジェクトを使うべきです。

C++20のカスタマイゼーションポイントオブジェクト

C++17のカスタマイゼーションポイントとC++20からのカスタマイゼーションポイントオブジェクトの対応と一覧を載せておきます。

C++17のカスタマイゼーションポイント関数 C++20のCPO 効果
std::begin() std::ranges::begin 範囲の先頭を指すイテレータを取得する
std::end() std::ranges::end 範囲の終端を指すイテレータを取得する
std::cbegin() std::ranges::cbegin 範囲の先頭を指すconstイテレータを取得する
std::cend() std::ranges::cend 範囲の終端を指すconstイテレータを取得する
std::rbegin() std::ranges::rbegin 逆順範囲の先頭を指すイテレータを取得する
std::rend() std::ranges::rend 逆順範囲の終端を指すイテレータを取得する
std::crbegin() std::ranges::crbegin 逆順範囲の先頭を指すconstイテレータを取得する
std::crend() std::ranges::crend 逆順範囲の終端を指すconstイテレータを取得する
std::size() std::ranges::size 範囲の長さを取得する
std::ssize() (C++20) std::ranges::ssize 範囲の長さを符号付き整数型で取得する
std::empty() std::ranges::empty 範囲が空であるかを取得する
std::data() std::ranges::data 範囲の領域先頭へのポインタを取得する
std::ranges::cdata 範囲の領域先頭へのconstポインタを取得する
std::swap() std::ranges::swap 二つのオブジェクトの内容を入れ替える
std::ranges::iter_move イテレータの指す要素をムーブする
std::ranges::iter_swap イテレータの指す要素をswapする
std::strong_order 全順序の上での三方比較を行う
std::weak_order 弱順序の上での三方比較を行う
std::partial_order 半順序の上での三方比較を行う
std::strong_order_fallback <=>が無い場合に< ==にフォールバックするstd::strong_order
std::weak_order_fallback <=>が無い場合に< ==にフォールバックするstd::weak_order
std::partial_order_fallback <=>が無い場合に< ==にフォールバックするstd::partial_order

もしかしたらほかにもあるかもしれません。

C++23以降の標準ライブラリ

カスタマイゼーションポイントを増やしづらかった時代はコンセプトとカスタマイゼーションポイントオブジェクトによって終わりを告げたため、これからのライブラリはそれらを中心として設計されるでしょう。提案文書のレベルでは、新規提案のほとんどが何かしらの形でコンセプトを用いており、規模の大きめな新規ライブラリ提案ではカスタマイゼーションポイントオブジェクトが普通に用いられています。

特にC++23に導入されるのがほぼ確実視されているExecutorライブラリは、現段階ですでにコンセプトとカスタマイゼーションポイントオブジェクトベースの非常にジェネリックな最先端のライブラリです。C++23とそれ以降の標準ライブラリではカスタマイゼーションポイントオブジェクトとコンセプトは空気のような存在になるでしょう。

onihusube.hatenablog.com

カスタマイゼーションポイントオブジェクトという言葉

カスタマイゼーションポイントオブジェクトの効果はそれぞれ異なりますが、おおよそ全般的に共通しているものがあり、単にCPOやカスタマイゼーションポイントオブジェクトと呼んだ時にはそのような性質を暗黙的に仮定していることがあります。

任意のカスタマイゼーションポイントオブジェクトの名前をcpo_nameとすると

  • (標準)ライブラリ側で用意されている関数オブジェクトである
  • カスタマイゼーションポイントオブジェクトによる処理は特定の型に限定されない
  • 呼び出しに当たっては引数あるいは戻り値型に(その文脈で)適切なコンセプトによる制約を行う
  • 少なくとも、cpo_nameと同じ名前のメンバ関数と非メンバ関数Hidden Friends含む)を捜索して呼び出すように定義される

これらの事を頭の片隅に入れておくと、カスタマイゼーションポイントオブジェクトが出て来た時にその意味を理解しやすくなるかもしれません。

参考文献

この記事のMarkdownソース

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

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

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

P0493R1 : Atomic maximum/minimum

std::atomicに対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()を追加する提案。

アトミックな数値演算は既に標準化されていますがmaximum/minimum操作はそうではなく、他フレームワークやハードウェアには既に実装があり、いくつかのマルチスレッドアプリケーションで有用であるため追加しようというものです。

#include <atomic>

std::atomic<int> a = 10;

int r1 = a.fetch_max(20);
// r1 == 10, a == 20

int r2 = a.fetch_min(5);
// r2 == 20, a == 5

これらの操作はread-modify-writeです。すなわち、現在の値と指定された値の大小関係に関わらず、値は常に更新されます。

std::atomic<int> a = 10;

int r = a.fetch_max(5);  // 値の入れ替えは起こらないが、書き込みは行われている

// 例えば、次のように実行される
int v = a.load();
int max = std::max(v, 5);
a.store(max);

int r = v;

この提案では今の所、std::atomic<T>の整数型とポインタ型の特殊化に対してだけfetch_max()/fetch_min()メンバ関数を追加しています。「P0020 : Floating Point Atomic」が採択されれば浮動小数点型の特殊化についても追加すると書かれていて、これはC++20に対して既に採択されているので、次のリビジョンくらいで浮動小数点型のstd::atomic<T>特殊化についても同様のものが追加されるかもしれません。

また、他のatomic操作に準ずる形で非メンバ関数版も用意されています。ただし、これらも整数型とポインタ型でのみ利用可能です。

namespace std {
  template<class T>
  T atomic_fetch_max(atomic<T>*, typename atomic<T>::value_type) noexcept;

  template<class T>
  T atomic_fetch_max_explicit(atomic<T>*, typename atomic<T>::value_type, memory_order) noexcept;

  // それぞれvolatileオーバーロードがある
  // fetch_min()も同様
}

P0870R3 : A proposal for a type trait to detect narrowing conversions

Tが別の型Uへ縮小変換(narrowing conversion)によって変換可能かを調べるメタ関数is_narrowing_convertible<T, U>を追加する提案。

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

このリビジョンでの主な変更は、機能テストマクロが追加された事と、配列を用いた実装がvoidや参照型、配列型など一部の型で機能しない事が明記された事です。

P1679R2 : String Contains function

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

同じことは既にあるfind()を使えばできますが、find()関数を使用する方法には少し問題があります。

if (str.find(substr) != std::string::npos)
  std::cout << "found!\n";
  • 含まれているかを調べているのに!=を使用する(書きづらい)
  • 調べているのは文字の位置なのか、含まれているかどうかなのか、含まれていないかどうかなのか、一見して分かりづらい(読みづらい)

対して、contains()というメンバ関数は意図が明確で書くときも読むときもこれらの問題は起こらず、初学者に対しても教えやすく使いやすいものです。また、他の言語の文字列型および、標準外のライブラリには既に対応する関数の実装があるため、標準ライブラリにも追加しよう、と言うのが要旨です。

if (str.contains(substr))
  std::cout << "found!\n";

オーバーロードstarts_with/ends_withを参考に以下の3つが提供されます。

constexpr bool contains(basic_string_view x) const noexcept;
constexpr bool contains(charT x) const noexcept;
constexpr bool contains(const charT* x) const;

P1841R1 : Wording for Individually Specializable Numeric Traits

std::numeric_limitsに代わる新たな数値特性(numeric traits)取得方法を導入する提案。

例えば数値型の最大値や最小値等、数値型の満たしている各種特性を取得するのに現在はstd::numeric_limitsが用意されています。これは少なくとも<type_traits>ヘッダにあるような型特性が見出されるよりも以前から存在しており、その設計は古くなっています。

ユーザー定義型に対する特殊化を追加する場合、ジェネリックな利用のために本来必要のない数値特性についてもそれっぽい値を返すように実装する必要があります。あるいは、ある数値特性を提供しているのかどうかを知る方法が提供されていません。
このことは、新たな数値特性を追加した場合には既存のユーザー定義型に対する特殊化を破壊する事を意味しており、そのためにstd::numeric_limitsは拡張可能ではなくなっています。

そこで、std::numeric_limitsにある各数値特性関数をそれぞれ個別のクラステンプレートと対応する変数テンプレートのペアに分解します。また同時に、一部の数値特性の名前と内容を調整します。

// 型Tの有限値のうちの最大値(numeric_limits<T>::max()相当
template <class T>
struct finite_max;

// 型Tの有限値のうちの最小値(numeric_limits<T>::min()相当
template <class T>
struct finite_min;

template <class T>
inline constexpr auto finite_max_v = finite_max<T>::value;

template <class T>
inline constexpr auto finite_min_v = finite_min<T>::value; 

ある型について任意の数値特性が定義されているかを調べるものも提供されます。

// 任意のTについて、数値特性Traitが定義されているかを調べる
template <template <class> class Trait, class T>
inline constexpr bool value_exists;

// 任意のTについて、数値特性が提供されていればその値を、いなければdefにフォールバックする
template <template <class> class Trait, class T, class R = T>
inline constexpr R value_or(R def = R()) noexcept;

これは例えば、次のように実装されます

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

template <template <class> class Trait, class T, class R = T>
constexpr R value_or( R def = R() ) noexcept {
  if constexpr(value_exists<Trait, T>s)
    return Trait<T>::value;
  else
    return def;
} 

これらのものは<numbers>ヘッダとstd::numbers名前空間に追加されます。

このように、数値型に対する数値特性が個別に分かれていることによって新しい数値特性を追加する際に既存のユーザー定義特殊化を壊してしまう事もありません。ユーザーが特殊化を追加する際も必要な数値特性についてだけ特殊化を行えばよくなります。

P1861R1 : Secure Networking in C++

Networking TS(簡単に言えば、ソケット通信ライブラリ)に対して、TLS/DTLSのサポートをデフォルトにする提案。

今日、ネットワークに接続すると言うことは悪意を持った攻撃に曝されることを意味します。それに対処するために、インターネットにおける通信はHTTPS(TLS)等を用いてセキュアにする事がデフォルトとなりつつあります。特に、WEBサーバの中にはHTTPを拒否しHTTPSでしか通信をしないものも増えてきています。
C++のネットワークライブラリがそれらの現代のWEBシステムと対話するために、また、ネットワークセキュリティの知識のない開発者がそれを意識せずとも一定のセキュリティを確保する事ができるように、そして、C++のネットワークライブラリを用いたプログラムが将来的にもセキュアなインターネットと連携していくために、Networking TSにおいてTLS/DTLSをデフォルトで使用するようにする、と言う提案です。

セキュリテイを確保するために追加のややこしい設定が必要になったりコードとその理解が複雑になることはセキュアなプログラムを書くことを妨げ、安全でない通信の利用を促進しかねないため、この提案では現在のNetworking TSのAPIを変更し、WEBアクセスなども意識した使いやすいAPIセットを提案しています。

コルーチンとstd::lazy<T>を用いたHTTPSクライアントのサンプル

#include <iostream>
#include <net>

std::lazy<void> run()
{
  net::workqueue queue(net::workqueue::main_queue());
  net::endpoint::host host("www.apple.com", 80);

  // ここの第二引数でTLS/DTLSを使用するかを制御する
  net::connection connection(host, net::parameters::tls(), queue);
  connection.start();

  std::cout << "Sending request" << std::endl;
  net::message message(net::buffer("GET / HTTP/1.1\r\nHost: www.apple.com\r\n\r\n"));
  auto sendResult = co_await connection.send(message);
  if (!sendResult) {
    std::cerr << "failed to send request" << std::endl;
    co_return;
  }

  std::cout << "Sent request, waiting for response" << std::endl;
  auto message = co_await connection.receive();
  if (!message) {
    std::cerr << "failed to receive response" << std::endl;
    co_return;
  }

  std::cout << "Received response" << std::endl;
  message->data().get([](const uint8_t *bytes, std::size_t size) {
    std::cout << std::string(reinterpret_cast<const char *>(bytes), size);
  });
  std::cout << std::endl;
  co_return;
}

int main(int, char**)
{
  auto lazy = run();
  net::workqueue::main();
}

この提案は将来的にQUIC等のプロトコルをサポートするための下準備も兼ねています。

P1897R3 : Towards C++23 executors: A proposal for an initial set of algorithms

Executorライブラリにいくつかの汎用非同期アルゴリズムを追加する提案。

現在のExecutor提案に含まれている非同期アルゴリズムはバルク処理のためのbulk_executeだけで、Executorを実用的にするためにもう少し多くの汎用非同期アルゴリズムを追加しよう、と言う提案です。

また、今後さらに多くの汎用非同期アルゴリズムを追加していくにあたって、より洗練された設計や文言を選択するために、個別に議論可能な(相互依存していない)最小のアルゴリズムのセットから提案を始めています。

追加されるものは以下のものです(引数のsは何か処理を示すsenderオブジェクト)。なおこれらのものは全てカスタマイゼーションポイントオブジェクトです。

  • just(v...)
    • v...を表現するsenderを返す
  • just_on(scheduler, v, ...)
    • onの効果とセットになっているjust()
    • schedulerの実行コンテキスト上でjust(v...)するsenderを返す
  • on(s, scheduler)
    • schedulerの実行コンテキスト上で実行されるsから、結果値かエラーを伝播するsenderを返す
  • sync_wait(s)
    • sを実行し、処理の結果を返すか、処理中の例外が送出されるか、どちらかによって完了するのを待機する
    • 戻り値はsの結果、sの実行に際する例外を送出する
  • when_all(s...)
    • 全てのs...の処理が完了するとその処理も完了するsenderを返す。全ての結果値が伝播される。
  • transform(s, f)
    • sの結果にf()を適用するか、エラーかキャンセルを伝播するsenderを返す。
  • let_value(s, f)
    • sの結果値が、別の非同期処理fの実行中利用可能となる非同期スコープを作成する
    • sのエラーやキャンセルは変更されずに伝播される
  • let_error(s, f)
    • sのエラー値が、別の非同期処理fの実行中利用可能となる非同期スコープを作成する
    • sの結果値やキャンセルは変更されずに伝播される
  • ensure_started(s)
    • 即座にsを実行コンテキストへ投入し、その他のコードと並行に実行されている可能性のあるsenderを返す

提案文書より、簡単なサンプル。

auto just_sender = just(3); // sender_to<int>

auto transform_sender = transform(
  std::move(just_sender),
  [](int a){return a+0.5f;}
); // sender_to<float>

// ここで処理をExecutorに投げ、結果を待機する
float result = sync_wait(std::move(transform_sender));
// result == 3.5

// パイプライン演算子を用いて中間オブジェクトを隠蔽する
float f = sync_wait(
  just(3) | transform([](int a){return a+0.5f;})
);

複数の処理(sender)を受けてそれらを直列化するwhen_allのサンプル。

auto just_sender = just(std::vector<int>{3, 4, 5}, 10); // sender_to<vector<int>>
auto just_int_sender = just(3); // sender_to<int>
auto just_float_sender = just(20.0f); // sender_to<float>

auto when_all_sender = when_all(
  std::move(just_sender),
  std::move(just_int_sender),
  std::move(just_float_sender)
);

auto transform_sender = transform(
  std::move(when_all_sender),
  [](std::vector<int> vec, int /*i*/, float /*f*/) {
    return vec; // 他の結果は捨てる
  }
);

vector<int> result = sync_wait(std::move(transform_sender));
// result = {3, 4, 5}

// パイプライン演算子の利用
vector<int> result_vec = sync_wait(
  when_all(
    just(std::vector<int>{3, 4, 5}, 10),
    just(3),
    just(20.0f)
  ) |
  transform([](vector<int> vec, int /*i*/, float /*f*/){return vec;})
);

P1898R1 : Forward progress delegation for executors

Executorにおける処理の前方進行と非同期処理グラフのモデルに関する提案。

Executorライブラリと非同期アルゴリズムによってワークチェーンを構成し実行する際にその実行リソース(実行コンテキスト、scheduler)がどのように伝播するのかを明確に定義するものです。

新しくscheduler_providerコンセプトとget_schedulerCPOの2つを追加します。scheduler_providerコンセプトは(receiverに対して)get_scheduler()によってschedulerを取得可能であることを求めます。senderconnect()されたscheduler_provider(なreceiver)からその実行コンテキストであるschedulerを取得する事で非同期タスクの下流から上流、あるいは上流から下流に向かってschedulerを伝播させることが可能になります。

複数の処理をチェーンするとき、個々の処理を示すsenderオブジェクトもその順番通りに内部で紐づいていき、最後にそれらの処理全体を示す1つのsenderオブジェクトが得られます。そこにその処理のコールバックとなるreceiverを接続(connect())して非同期処理の完了(成功、失敗、キャンセル)を待機できるようなoperation stateオブジェクトが得られます。そして、最後にoperation stateオブジェクトをstart()などで明示的に開始します。

senderreceiverconnect()の際は、渡されたreceiverオブジェクトはチェーンされたsender列の最後から先頭へ伝播していきます(実装によるかもしれません)。すなわち、チェーンされた処理を示す一連のsenderオブジェクトは全て同じ一つのreceiverオブジェクトを受け取ることになります。

// どこかのスレッドプールで実行してもらう
sender auto begin = then(
  std::execution::schedule( pool ),
  []{ return 1; }
);

// senderのチェーン
sender auto task = begin | then([](auto n){ return n + 1;})
                         | then([](auto n){ return n * 2;})
                         | then([](auto n){ return n * n});

receiver auto rec = /*任意のreceiverを取得*/;

// senderとreceiverを接続(コールバックの登録
// taskも含めてチェーンしているすべてのsenderにここで渡したreceiverが浸透する
operation_state auto state = std::execution::connect(task, rec);

// 実行開始!
std::execution::start(state);

この例では、最初のsenderに登録されたscheduler(どこかのスレッドプールとしている)が処理の上流から下流へ伝播するはずです。ただ、この例のように最初のsenderにいつも実行コンテキストが指定されるとは限りませんし、チェーンの途中でon()などによってschedulerを変更することができます。また、非同期アルゴリズムの種類によってはどのschedulerで実行するべきか不明な場合もあります。
そのような時、その一連のsender全体に渡っているreceiverオブジェクトを介してあるsenderから別のsenderschedulerをやり取りすることができると、適切なschedulerを選択できるかもしれません。

そのためにget_scheduler()を追加し、それを用いればscheduler_provider(なreceiver)からschedulerをチェーン上の任意の場所から任意の場所へ伝達できるようになります。もちろん、どのように伝達するのかはsenderの実装によることになります。

sender auto pool_sender = then(
  std::execution::schedule( pool ),
  []{ return 1; }
);

// pool_sender以外のsenderはどこで実行する?
// あるいは、後続のthenによる処理は??
sender auto task = when_all(
  pool_sender,
  just(1.0),
  just("executor")
) | then([](int, double, const char*) { return true; });

// この時、与えられたreceiverを介して適切なschedulerを設定できるかもしれない
operation_state auto state = std::execution::connect(task, rec);

P1974R0 : Non-transient constexpr allocation using propconst

コンパイル時に確保したメモリを実行時にも安全に参照するための要件と、そのためのより深いconst性を指定するpropconstの提案。

C++20からはconstexprな動的メモリ確保が可能になっていますが、Non-transientなメモリ確保(コンパイル時に確保したメモリを実行時にも参照すること)は許可されませんでした。

constexpr void f(std::initilizer_list<int> il) {
  std::vector<int> vec = il;  // これはok
}

int main() {
  constexpr std::vector<int> vec = {1, 2, 3, 4, 5}; // これはできない
}

Non-transientなメモリ確保が許可されていた以前の仕様の下では、クラス内部で確保されるメモリで条件を満たした場合にコンパイル時に解放されなかったメモリは実行時に静的ストレージに昇格されて参照可能でした。その際は通常のconstexpr変数と同様に実行時const変数になります。その条件とは以下のようなものでした。

  • Tは非トリビアルconstexprデストラクタを持つ
  • そのデストラクタはコンパイル時実行可能
  • そのデストラクタ内で、Tの初期化時に確保されたメモリ領域(Non-transient allocation)を解放する

すなわち、そのクラスのconstexprデストラクタによってコンパイル時に確保されたメモリがコンパイル時に解放可能であることです。これはコンパイラによるテスト要件であって、実際に解放が行われるわけではありません。

そして、その仕様の下では次のような問題が発生します。

// これはok(だった
constexpr std::unique_ptr<std::unique_ptr<int>> uui 
  = std::make_unique<std::unique_ptr<int>>(std::make_unique<int>());

int main() {
  std::unique_ptr<int>& ui = *uui; // これができてしまう
  ui.reset(); // 静的ストレージの領域をdeleteする?
}

このように、デストラクタを実行時に呼び出せてしまいますが、前述のテスト要件だけではこれを検出し防ぐことはできません。そのため、Non-transientなメモリ確保は最終的にリジェクトされました。

これが何故起こるかというと、std::unique_ptrdeep constな型ではないからです。すなわち、外側のstd::unique_ptrconst性が内部のstd::unique_ptrまで伝播していません。

そこで、以前のコンパイラによるデストラクタのテスト要件に次の条件を加えます。

  • デストラクタ呼び出し中に現れる全ての(メンバ)変数はconstであり、かつ
  • そのデストラクタは実行時に破棄されうるオブジェクトに対して呼び出されていない

これによって、上記のstd::unique_ptr<std::unique_ptr<int>>のような例をコンパイル時に正しくエラーにすることができます。ただこれにも問題がまだあります。

constexpr vector<vector<int>> vvi = {{1}};

int main() {
  vector<int>& vi = vi[0]; // 非const参照への変換になるのでng
  vi = vector<int>{}
}

このコードは以前の要件の下でもエラーになります。std::vectorconst修飾されたメンバ関数からその要素への非constな参照を取れないように巧妙に設計されているためです。これによってstd::vectorは多重ネストしてもstd::unique_ptrのように内部要素を解放されてしまうことは起こりえません。すなわち、std::vectordeep constな型です。

しかし、新たな要件によるデストラクタのテストはネストしたstd::vectorを許可しません。std::vectorconstメンバ関数の慎重な設計によってdeep constとなっているだけでそのメンバは非constのままであり、コンパイラはネストしたstd::vectordeep constであることを認識できません。

そこで、ユーザーに追加の作業を必要とせずにコンパイラが正しく型のdeep const性を認識するために、C++の型システムを拡張し新しいCV修飾子であるpropconstを導入することを提案しています。

propconstはポインタ型と参照型にのみ適用可能で、ポインタが不変である場合にconstに変換され、それ以外の場合は何もしません。参照型はポインタ型に置き換えた上で同様です。

非メンバのオブジェクトポインタ型に対してpropconst修飾している場合、その不変性はconst修飾の有無で決まります。メンバ変数に対してpropconst修飾している場合は呼び出すメンバ関数const修飾によってその不変性が決定されます。

int propconst* ip1;       // int* ip1;
int propconst* const ip2; // int const * const ip2;

struct S {
  int propconst *ppi;

  void f() const {
    // ここでは、ppiの型はint const * const
    // int const* ppi;と宣言されているように見える
  }

  void f() {
    //ここでは、ppiの型はint *
    // int * ppi;と宣言されているように見える
  }
};

最終的には、このpropconstと以下の条件でもってNon-transientなメモリ確保を許可することが提案されています。

  • constexprなデストラクタの呼び出し(テスト)中に現れた全ての変数は、他のmutableな(実行時)文脈から到達可能ではない
// 共にok
constexpr vector<vector<int>> vvi = {{1}};
constexpr vector<unique_ptr<int>> vui = {std::make_unique<int>()};

propconstはどこに現れるのかというと、std::vectorの実装に現れています。std::vectorの領域管理用のメンバ変数が全てpropconst修飾されていれば、その要素を外部から変更可能でないことが保証可能であるため、コンパイラstd::vectordeep constであることを認識可能です。つまり、普通のユーザーはpropconstを意識せずともNon-transientなメモリ確保を利用できるようになります。

P1985R1 : Universal template parameters

任意の型、テンプレートテンプレート...、非型テンプレートパラメータなど、テンプレートの文脈で使用可能なものを統一的に受けることのできるテンプレートパラメータ(Universal template parameter)の提案。

例えば高階メタ関数を書くときなど、引数として任意のものを受け取りたいことがよくあります。現状ではこれをするためにはそれぞれのテンプレートパラメータの種類毎の特殊化を行う必要があります。

// メタ関数Fに引数Argsを適用する
template <template <typename...> typename F, typename... Args>
using apply = F<Args...>;

template<typename X>
struct G {using type = X;};

using OK = apply<G, int>;  // ok、G<int>

// 部分適用する
template <template <typename> typename F>
using H = F<int>;

using NG = apply<H, G>; // ng
// applyの引数パラメータArgs...はテンプレートテンプレートパラメータではないため

これはまた、applyの2番目以降の引数として非型テンプレートパラメータを渡そうとしても同じことが起きます。このような場合に、そのパラメータの種類を指定せずにテンプレートパラメータを宣言できるととても便利です。

提案では、2種類の文法を提案しています。

// 簡単かつ使いやすい、template auto
template <template <template auto...> typename F, template auto... Args>
using apply = F<Args...>;

// 数学的に正しい、__ (+template auto)
template <template <__...> typename F, template auto... Args>
using apply = F<Args...>;

2番目の方法では同時に1つ目の方法も導入することになります。__はパターンマッチにおける制約のないパラメータのようなものであり(switch文のdefaultラベルのようなもの)、そのパラメータに名前をつけることが出来ません。
template autoはちょうど、C++17で導入されたautoによるユニーバーサル非型テンプレートパラメータの宣言と同じようなことをします。

また、このUniversal template parameterを取るクラステンプレートをプライマリテンプレートとして、テンプレートパラメータの種類毎に特殊化を行えるようにすることも提案されています。

// プライマリテンプレート
template <template auto>
struct X;

// 普通の型に対する特殊化
template <typename T>
struct X<T> {
  // T is a type
  using type = T;
};

// 非型テンプレートパラメータに対する特殊化
template <auto val>
struct X<val> : std::integral_constant<decltype(val), val> {
  // val is an NTTP
};

// テンプレートテンプレート(1引数メタ関数)に対する特殊化
template <template <typename> F>
struct X<F> {
  // F is a unary metafunction
  template <typename T>
  using func = F<T>;
};

P2066R2 : Suggested draft TS for C++ Extensions for Transaction Memory Light

現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。

トランザクショナルメモリはDBにおけるトランザクション処理の概念をDBをメモリに対応させ一般化したもので、トランザクション間においてメモリの一貫性を保証し、並行処理を容易に書くことができるようにするためのものです。

あるメモリ領域に対する1連の処理を1つのトランザクションとすると、そのトランザクションは成功するか失敗するかのどちらかであり、成功した場合にだけ結果がアトミックに書き込まれます。失敗した場合は進行中の処理は全てキャンセルされなされた変更はロールバックされるため、メモリ領域は一切変更されません。

このようなトランザクションはそれぞれがプログラム全体で1つの全順序に従うかのように実行され、あるトランザクションの実行中に外から処理中の状態を観測出来ず、1つのトランザクションは不可分の操作であるように実行されます。それによって、ユーザーはトランザクション間のデッドロックや同期などの心配を一切しないで並行処理を書くことが出来ます。

これらを標準機能として提供するために、トランザクショナルメモリTS仕様では2種類のトランザクション処理を定義するためのキーワード(transaction_relaxed/transaction_atomic)とトランザクションキャンセル時の挙動を指定する2種類の指定子やある関数がトランザクション中で安全に扱えるかを指定する2種類の関数指定子(commit_on_escape/cancel_on_escape/transaction_safe/transaction_unsafe)を定義していました。

2015年に現在のTS仕様が策定されていましたが実装もユーザー経験も少なく、機能がカバーする領域が広すぎると言う指摘もあり、標準への導入は見送られていました。そこでこの提案では、atomicトランザクション(以前のtransaction_atomic相当)とそのために必要な最低限の仕様変更だけをC++に導入することを提案しています。最終的にはTS仕様の全てを含めることを目指すために、まずは実装の負担にならない小さな変更から初めて行くつもりのようです。

導入されるキーワードはatomicだけで、上記4つの指定子は全てありません。プログラムの実行に当たって発生するトランザクションはプログラム中で一貫した全順序によって実行され、同じ式を評価する2つのトランザクションは、先に評価が開始されたトランザクションの終了にもう一つのトランザクションの開始が同期します。

int f()
{
  static int i = 0;

  // atomicステートメント、atomicトランザクションを定義
  // このブロックは全てのスレッドの間で1つづつ実行される
  atomic {
    ++i;
    return i;
  }
}

int main() {
  std::array<std::thread, 100> threads{};

  // 関数f()の呼び出し毎に一意の値が取得される
  // 同じ値を読んだり、更新中の値を読むことはない
  for (auto& th : threads) {
    th = std::thread([](){
      int n = f();
    });
  }

  for (auto& th : threads) {
    th.join();
  }
}

ただし、次のようにスレッド外部から観測可能な操作のatomicステートメント内部での実行は未定義動作とされています。

  • I/O操作
  • volatile領域へのアクセス
  • atomic操作
    • std::atomicなど

そして、atomicステートメントの中では次の行いは実装定義です。

  • asm宣言
  • 到達可能な定義をもつinline関数以外の関数の呼び出し
  • 仮想関数呼び出し
  • 関数名を指定しない後置式
    • a[], a++, a->b()など
  • throw
  • コルーチン関連
    • co_await, co_returnなど
  • スレッドローカルストレージ及び静的変数の動的初期化

トランザクションのキャンセルはどうやらサポートされず、例外送出=キャンセルと考えればそれは実装定義のようです。また、その実装がハードウェアによるのかソフトウェアによるのかも規定していません。ほとんど実装定義です・・・

この部分は以下の方のご指摘によって構成されています。

P2128R1 : Multidimensional subscript operator

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

行列や画像、位置情報など多次元のデータの1要素にアクセスするためには、ぞの次元に応じたインデックスが必要です。現状の添字演算子は1引数しか取ることができず、mdspanmdarrayなどの多次元データ型でその要素にアクセスするためには関数呼び出し演算子())を使用する必要があります。しかし、添字演算子と比べると明解ではなく少し混乱します。

そこで、添字演算子オーバーロードする際に1つ以上の任意の数の引数を取れるように変更しよう、と言うのが提案です。この場合、関数呼び出し演算子との差異は引数なしオーバーロードが可能かどうかだけになります。

template<class ElementType, class Extents>
class mdpan {

  // 多引数添字演算子オーバーロード
  template<class... IndexType>
  constexpr reference operator[](IndexType...);

  // 他実装略
};

int main() {
  int buffer[2*3*4] = { };
  auto s = mdspan<int, extents<2, 3, 4>> (buffer);

  // 添字演算子による多次元アクセス
  s[1, 1, 1] = 42;
  // 現在は関数呼び出し演算子を使用する必要がある
  s(1, 1, 1) = 42;
}

C++20では添字アクセスの際に[]の中にカンマを書くことが非推奨とされましたが、この提案の下では配列型の場合はC++17までのようなカンマによる式と常に認識し、クラス型の場合は添字演算子オーバーロードが見つからない場合にカンマによる式にフォールバックすると言う選択を取ることができ、C++17までの振る舞いをサポートし続けることが可能になります。

P2136R1 : invoke_r

戻り値型を指定するstd::invokeであるinvoke_rの提案。

C++17以前から、関数呼び出しという操作を規格上で統一的に表現するためにINVOKEという仮想操作があり、C++17ではそれに対応するライブラリ関数であるstd::invokeが追加されました。
また、指定した戻り値型RINVOKEするという操作もあり、対応するものとしてstd::invoke<R>のような形で提案されていましたが、不要であるとしてドロップされました。

しかし、INVOKE<void>(f, args...)のような呼び出しは戻り値を明示的に破棄するために便利です。また、std::is_invocable_rstd::is_nothrow_invocable_rは指定した戻り値型で呼び出せるかを調べられるようになっており、std::visitには戻り値型を指定するstd::visit<R>が用意されています。

このように、やっぱり戻り値型を指定するstd::invokeはあると便利なので追加しようという提案です。std::invoke_rstd::invokeと比較して次のような利点があります。

  • voidを指定すれば戻り値を破棄できる
  • callableオブジェクトの戻り値型を変換して呼び出しできる
    • 例えば、T&&を返す関数をTprvalueを返す関数に変換できる
  • 複数の戻り値型を返しうる呼び出しを指定した1つの型を返すように統一できる
    • 例えば、共変戻り値型をアップキャストする
namespace std {
  // 宣言
  template <class R, class F, class... Args>
  constexpr R invoke_r(F&& f, Args&&... args)
    noexcept(is_nothrow_invocable_r_v<R, F, Args...>);
}


[[nodiscard]] int f1(int);

// 戻り値の破棄
std::invoke_r<void>(f1, 0);

template<typename T>
T&& f2(T);

// 戻り値型の変換
int pr = std::invoke_r<int>(f2, 0);

struct base{};

template<typename F, typename... Args>
  requires std::derived_from<std::invoke_result_t<F, Args...>, base>
base f3(F&& f, Args&&... args) {
  // 共変戻り値型のアップキャスト
  return std::invoke_r<base>(std::forward<F>(f), std::forward<Args>(args)...);
}

効果としては、Rvoidが指定されたときは戻り値をstatic_cast<void>して、それ以外は暗黙変換する、という感じです。

名前の_rstd::invokeと間違えて使用しないようにするために付いています。

P2142R1 : Allow '.' operator to work on pointers

ポインタ経由のメンバアクセスの際に、->だけでなく.も使用できるようにする提案。

->.はほとんど同じことをするのに使い分けが必要なのは最初にCを学ぶ時の混乱する点の一つであり、他のモダンな言語におけるメンバアクセスはほとんど.で統一されています。また、ポインタと同等の振る舞いをする参照との間でのコードの非互換(コピペしたときに書き換えが必要)もあり、ポインタ経由のメンバアクセスに.を許可しよう、というものです。

struct S {
  int n;

  operator int() {
    return this.n;  // これも出来るようにする
  }
}

int main() {
  S  obj{.n = 10};
  S& ref = obj;
  S* ptr = &obj;

  ref.n = 20; // これは出来る
  ptr.n = 20; // これを出来るようにする

  obj->n = 20;  // これが出来るようになるわけではない
}

これまでポインタに対しての.コンパイルエラーとなっていたので、この変更によって後方互換性が損なわれることはありません。

これは同時にC標準に対しても提案されています。

P2145R0 : Evolving C++ Remotely

コロナウィルスの流行に伴ってC++標準化委員会の会議がキャンセルされている中で、リモートに移行しつつどのように標準化作業を進めていくのかをまとめた文章。

今後のテレビ会議のカレンダーとかリアル会議で何してるのかとか、リモートやメール等の代替手段でどう作業するかみたいなことが書いてあります。

には、C++23以降の優先度の高いライブラリと言語機能についての進捗等のまとめが書かれています。

今の所は「P0592R4 To boldly suggest an overall plan for C++23」によって示された予定の変更はないようですが、既に11月のニューヨークで行われる予定だった会議もキャンセルされているので、さすがに変更があるかもしれません・・・

P2159R0 : An Unbounded Decimal Floating-Point Type

Numbers TS (P1889R1)に対して、10進多倍長浮動小数点型std::decimalを追加する提案。

P1889R1は将来C++に導入することを目指した数値型関連の提案をまとめたもので、多倍長整数型などが提案されています。現状10進浮動小数点型は無いようなので追加しようということのようです。

P2160R0 : Locks lock lockables (wording for LWG 2363)

現在のMutexライブラリ周りの文言にはいくつか問題があるのでそれを修正するための提案。

主に以下のような問題に対処するものです。

  • std::shared_lock<Mutex>のパラメータMutexshared mutex要件を満たすことが要求されているが、その参照先はshared timed mutexになっている。この不一致によって、たとえユーザーが時間指定して待機する関数を呼ばなかったとしてもshared_lock<shared_mutex>は未定義動作となりうる。
  • std::shared_lockの現在の表現は内部定義(規格に表されていない?)を参照しているため、ユーザー定義の共有Mutex型の利用が許可されていない。これは明らかな欠陥。
  • Lock関連の操作全般の文言に横たわる問題として、ロック操作の事前条件を基礎となるロック可能な操作の事前条件と混同したり、ロック可能であることをミューテックスと混同する問題がある。

これらの問題は文言や要件の不足によるものなので、必要な要件を追加し文言を整理・調整する事で解決を図っています。

P2161R0 : Remove Default Candidate Executor

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

前回公開されたこの提案から派生したもののようです。

associated_executorは非同期処理の完了時に呼ばれるハンドラ(コールバック)に関連づけられたExeutorで、あるハンドラはそのassociated_executorで指定されたExecutor(および実行コンテキスト)で実行されます。これはユーザーによってカスマイズ可能にするために用意されており、デフォルトではsystem_executorが使用されることになっています。

ただ、system_executorはいくつかの特異な性質を持っています。例えば、system_contextは受け取った処理を任意の数並列実行することが許可されています(すなわち、スレッドプールを想定している)。これを使用するExecutorを選択する場合ユーザーには強い並行性要件が課されます。system_contextsystem_executorの実行コンテキストであり、暗黙のうちにこれにフォールバックすると静かにデータ競合(未定義動作)を引き起こします。

一方で、io_contex::run()など投入した処理は現在のスレッドをブロックして実行され、ユーザーが処理が実行されている時とされていない時を制御可能な実行コンテキストをデフォルトで使用するものもあります。これらはそれぞれ別々の場所で使用されており、ユーザーがそのつもりもないのにsystem_executorに静かにフォールバックする場合、全く意図しない偶発的なデータ競合を引き起こしてしまいます。

また、system_contextは投入されたワークアイテムの生存期間(lifetime)を任意に延長することが許されています。処理の前方進行を停止する方法も提供されてはいますが、ワークアイテムの寿命が確実に尽きることを保証する方法はありません。対照的に、ユーザーはワークアイテムの生存期間がいつ終了するかを制御可能なExecutorを使用することができます。その場合に意図せずsystem_executorにワークアイテムを投入してしまうと、あらゆる種類の生存期間にまつわるバグを引き起こす可能性があります。

特に、これらの性質のそれぞれはsystem_contextのシングルトンオブジェクトがグローバル変数であるということに由来しています。

これらの問題を抱えているものをデフォルトに据えておくのは明らかにバグの元であるので削除しよう、ということのようです。

P2162R0 : Inheriting from std::variant (resolving LWG3052)

std::variantを公開継承している型に対してもstd::visit()できるようにする提案。

namespace std {
  template <class R, class Visitor, class... Variants>
  constexpr R visit(Visitor&& vis, Variants&&... vars);
}

std::visitは上記のように宣言されていますが、例外指定の文言において「varsに含まれる全てのstd::variantが...」のように指定されていることから、Variantsパラメータパックに含まれてもいいのはstd::variantの特殊化だけ、となっています。
文言の調整によってこれを緩和し、std::varinat(の任意の特殊化)を曖昧でないpublicな基底クラスとして持つ型でも呼べるようにしようとしているものです。

これはすでにClang(libc++)とMSVCには実装済みのようなので、欠陥報告(C++17?)として採択されそうです。

P2163R0 : Native tuples in C++

言語サポートのあるより自然に使えるnative tupleを追加する提案。

native tupleは山かっこ<>のなかに型名を書くことによって導入し、{}braced init list)によって初期化されます。要素アクセスはnative tupleオブジェクトntに対してnt.<I>の様に行われ、これはstd::tupleのオブジェクトltに対するstd::get<I>(lt)と等価の働きをします(Iは型名でもok)。

<int, double> t1 = {1, 1.0};

int a = 0;

auto t2 = {a, "str"s};  // <int, string>
<int&, double> t3 = {a, 0.5}; // decayされずに転送

t2.<0> = 1; // aは変更されない
t3.<0> = 2; // aが変更される
auto d = t3.<double>;

スライシングと展開

<int, double, int, std::string> t = {1, 2.0, 3, "4.0"};

auto slice = t.<1..2>;  // sliceは<doube, int>
auto t2 = {...t...};    // パック展開

クラステンプレートの型推論で利用

// std::map<int, double>
std::map m = {
  {1, 2.3}, {3, 4.5}, {6, 7.8}
};

多値返却関数

auto f() -> <int, double> {
  return {1, 2.0};
}

// あるいは戻り値型指定を省略可能
auto f() {
  return {1, 2.0};
}

主に{...}std::initializer_listに推論されてしまう事から起きている一貫性の無さと不便さを解決することを目的としている様です。しかし、どう見てもstd::initializer_listと衝突しているのでさらに検討が必要そうです。

P2164R0 : views::enumerate

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

std::vector days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};

int index = 0;
for(const auto& d : days) {
  std::cout << std::format("{} {} \n", index, d);
  index++;
}

// ↑これが↓こう書ける

for(const auto& [index, d] : std::views::enumerate(days)) {
  std::cout << std::format("{} {} \n", index, d);
}

範囲forでインデックスが欲しい時は本当によくあるけれどそのままだと取れないため、外部スコープでインデックスを定義してインクリメントしたり普通のforループが使用されたりします。これは冗長でバグの元であるため、単純なライブラリ機能で解決が可能なviews::enumrateを追加しようというものです。また、すでにrange-v3boost::rangeには同等のものが実装されています。

参考実装が等価なforループと同等のコードを出力している結果が掲載されています。

https://godbolt.org/z/2Kxo8d

P2165R0 : Comparing pair and tuples

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

std::pairと2要素のstd::tupleは本質的に同じものであり多くのインターフェースを共有していますが、std::tupleからstd::pairへの代入ができなかったり互いに比較ができなかったりと非互換な部分があります。そうした非互換を取り除きよりstd::tuplestd::pairの一貫性を向上させるのが目的です。

constexpr std::pair p{1, 3.0};
constexpr std::tuple t{1.0, 3};

t = p;  // これは出来る

// 次の事を出来るようにする
p = t;
bool b1 = P == t;
bool b2 = (p <=> t) == 0;

これらの事は、既存のstd::tupleのコンストラクタと比較演算子を変更し、コンセプトによってtuple-likeなオブジェクトを受け入れ可能にすることで達成されます。そのため、std::pairでできるようになる上記の事はより一般のtuple-like(pair-like)な型でも同時に可能になります。

P2167R0 : Improved Proposed Wording for LWG 2114

contextually convertible to boolと言う規格上の言葉を、C++20で定義されたboolean-testableコンセプトを使用して置き換える提案。

純粋に規格の言葉の変更なので一般ユーザーには関係ないはずです。

contextually convertible to boolと言う要件はざっくりいえば「標準ライブラリが求めるときにboolに変換できること」みたいな意味で、比較演算子の戻り値型や述語関数の戻り値型に要求されるものです。この要件を言葉で式に対して定義するのが難しかったらしく、長年紛糾していたようです(LWG Issue 2114)。

C++20では当初あったbooleanコンセプトが置き換えられてboolean-testableという、まさにそれを表現するコンセプトが導入されました。そこで、これを使ってcontextually convertible to boolという要件を規定しようとしているようです。

P2168R0 : generator: A Synchronous Coroutine Generator Compatible With Ranges

Rangeライブラリと連携可能なT型の要素列を生成するコルーチンジェネレータstd::generator<T>の提案。

これは直接シーケンス列を生成するものではなくて、その様な処理をコルーチンによって書くときに使用可能なawaitableなクラスです。

// フィボナッチ数列を生成する
std::generator<int> fib (int max) {
  co_yield 0;

  auto a = 0, b = 1;
  for(auto n : std::views::iota(0, max)) {
    auto next = a + b;
    a = b, b = next;
    co_yield next;
  }
}

int answer_to_the_universe() {
  // 8項目までのフィボナッチ数列を生成(0, 1, 2, 3, 5, 8, 13, 21
  auto coro = fib(7) ;
  // 最初の5要素を捨てて集計(8, 13, 21
  return std::accumulate(coro | std::views::drop(5), 0);  // 42
}

この様な同期ジェネレータは多くのケースで有用であり基本的なコルーチンを書くためには必須だけど、正しく効率的に実装するのは難しいので標準で用意しよう、という提案です。

P2169R0 : A Nice Placeholder With No Name

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

// 宣言以降使われないため名前が必要ない変数
std::lock_guard _(mutex);

// 構造化束縛
auto [x, y, _] = f();

// パターンマッチ(提案中)
inspect(i) {
  1 => 0;
  _ => 1; // ワイルドカードパターン 
};

これは関数スコープでの_という変数名を少し特別扱いして、暗黙的に[[maybe_unused]]を付加し、再定義された時でも静かに実装定義の別名に置換した上で同じことをし名前探索の候補に上がらない様にするものです。

名前空間スコープでは基本的に使用できませんが、モジュールファイル実装単位(の本文)内でだけは関数スコープと同様に使用可能です。

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

namespace a {
  auto _ = f(); // Ok, [[maybe_unused]] auto _ = f();と等価
  auto _ = f(); // error: _の再定義
}

void f() {
  auto _ = 42; // Ok
  auto _ = 0;  // Ok、実装によって別の名前に置換される、ただしこの変数を参照できない

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

  assert( _ == 42 ); // Ok
}

使用可能な場所に制限はありますが、これによって既存のエンティティ名に_を使っているコードを壊さずにこの振る舞いを導入することが可能になるはずです。

P2170R0 : Feedback on implementing the proposed std::error type

静的例外のために追加されるstd::errorについて、実装経験に基づいた設計に関するフィードバックの文書。

実装リポジトリ
github.com

std::errorは任意のエラーを表現する型を型消去によって統一的に扱う型で、型消去したエラーオブジェクトとその復元のための情報を持つポインタで構成され、ポインタ2つ分を超えない程度のサイズを持ちます(std::string_viewと同等)。主にstd::exception_ptrを取り扱う事を想定していますが、std::shared_ptr等を用いてより大きなサイズを持つエラー型を扱う事も出来るようです。

この提案で主に述べられているのは従来のエラー型をstd::errorマッピングする際の問題点です。

  • std::error_codestd::errorマッピングするとそのサイズの都合(std::error_codeだけでポインタ2つ分のサイズがある)で効率的でなくなる。
  • std::errcはゼロ(デフォルト構築)相当の値がエラーなしを表現するがstd::errorは常にエラーだけを表現するためそこのマッピングをどうするか?
  • std::errorはエラー値の意味論的な等値比較が可能とされているがstd::errcと現在の標準の動的例外型を完全には対応付けできない。

等の問題についての報告がなされています。

P2171R0 : Rebasing the Networking TS on C++20

現在のNetworking TS(N4771)のベースとなっている規格はC++14なので、C++20ベースに更新する提案。

P2172R0 : What do we want from a modularized Standard Library?

標準ライブラリのモジュール化について急ぐべきか疑問を投げかける文書。

モジュールには主に以下の利点があります。

  • コンパイル時間の改善
  • ODR違反の緩和
  • 実装詳細の分離
  • マクロの分離

一方、標準ライブラリには次の制約があります。

  • #includeをサポートし続ける必要がある
  • ABIを破壊しない
  • 複数の言語バージョンをサポートする必要がある

この制約の下では標準ライブラリのモジュール化にはそれほど恩恵が無いため、モジュール化するにしても優先度は低く、とりあえずはヘッダユニットのインポートで済ませて他の事に時間を割いた方が良いのでは?というのが著者の主張です。

著者のおすすめ

  • hidden friendsの観点から、既存の演算子オーバーロードやカスタマイゼーションポイントを調整する作業を優先する。
  • その上で、大きめの粒度のモジュールを優先する。
  • フリースタンディングとモジュール化は直交しており、フリースタンディングは最適なモジュールの構成方法を示唆している。少なくとも部分的にフリースタンディングであるような機能は同じモジュールにあるべき。
  • 全てのヘッダをモジュール化する事を目指さない。条件付きコンパイルに使用される機能(<cassert>, <version>)は使用される際に明示的にインクルードされるべき。
  • コンパイル時間に関する提案のコストを決定するために標準ヘッダがインポートされるものとする

また、モジュールの大きさがどうあるべきかやC++23以降に追加される新しいライブラリ機能をモジュールにだけ追加するようにすることについても考察されています。
前者は、依存関係が多い場合には小さすぎると分割の意味をなさず、大きすぎると並列コンパイルの機会を喪失するが、大きい方がimportの数を減らせるためコンパイル時間削減には有利と述べられており、後者については、ABIの安定性の観点から難しいだろうと述べられています。

P2173R0 : Attributes on Lambda-Expressions

ラムダ式の関数呼び出し演算子に対して属性指定を出来るようにする提案。

関数オブジェクトを手で書いた場合は属性指定を行えるのだからラムダ式に対しても行えるようにするのは妥当なのでそのようにしよう、という提案です。

その場合問題になるのがどこに属性指定を置くかです。現在ラムダ式に対しての属性指定はnoexceptの後ろ、後置戻り値型の前に置くことができて、これは関数呼び出し演算子の型(メンバ関数型)に対して適用されています。文法的には、ラムダ導入子([])の前、ラムダ式全体の後ろ(;の前)、ラムダ導入子([])の直後という候補があるようですが、この提案ではラムダ導入子([])の直後を採用しています。

// 関数呼び出し演算子の型(メンバ関数型)に対して属性指定している(型に対して[[nodiscard]]は指定できないのでコンパイルエラー)
auto lm1 = [](int n) noexcept [[nodiscard]] -> int { return n; };

// この提案による関数呼び出し演算子に対する属性指定
auto lm2 = [][[nodiscard]](int n) noexcept -> int { return n; };

ラムダ導入子([])の前は構文に曖昧さをもたらすため不適切で、ラムダ式全体の後ろは将来的にクロージャ型そのものに対して属性指定できるようにする場合のために残しておくことにした結果、ラムダ導入子([])の直後が選ばれました。

この部分は以下の方のご指摘によって構成されています。

P2174R0 : Compound Literals

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

複合リテラル(型名){初期化子}の構文で指定された型のオブジェクトをその場に生成します。

int n = (int){10};
auto&& ar = (double[]){1.0, 2.0, 3.0};

C99の複合リテラルは常にlvalueを返すようですが、この提案ではprvalueを返すようにされています。既存のT{...}との一貫性を重視したもので、確かにC++的にはそちらの方が自然でしょう。

auto&       ref  = (int[]){0, 1, 2, 3, 4, 5};  // ng
const auto& cref = (int[]){0, 1, 2, 3, 4, 5};  // ok
auto&&      rref = (int[]){0, 1, 2, 3, 4, 5};  // ok

著者は、T{...}の型名をかっこで囲んで(T){...}としても同じ結果が得られるのは自然であり、CがすでにそれをサポートしているためGCCやclangでも使用可能であるが実装間に差異があるので、標準化することでより扱いやすくなるだろう、と述べています。

onihusube.hatenablog.com

この記事のMarkdownソース

[翻訳]P0443R13 A Unified Executors Proposal for C++

この記事は↑の一部を和訳しただけです。私には英語が読めないので怪しい訳を信用しないでください。

怪しい記述や間違いを見つけたらこの記事のMarkdownソースかコメント欄からお知らせください。

1 Design Document

1.1 Motivation

C++プログラムの未来を想像する時我々は、小さなスマートフォンから大きなスーパーコンピュータまで多様なハードウェアによってアクセラレートされエレガントにネットワーク化された非同期並列処理を思い描く。今、ハードウェアの多様性はかつてないほどに増しているが、C++プログラマには彼らが満足する並行プログラミングのためのツールがない。産業用の強力な並行処理プリミティブのようなものは強力だが危険であり、よく知られる問題に苦しめられている。また、C++標準のアルゴリズムライブラリは並列化対応されているが柔軟性に欠け、その他の並行処理機能(std::thread, std::atomic. std::async, std::futureなど)と組み合わせることができない。

これらの現在抱える課題に対処し思い描いた未来を築くためには、C++にプログラムの実行を制御するための基礎機能を整備しなければならない。まず、C++はある処理がいつどこで実行されるのかを制御するための柔軟な機能を提供する必要がある。本稿では、それらの機能の設計を提案する。SG1は多くの議論と協力の果てに2019年ケルン会議でこの設計を全員の合意の下で採択した。

1.2 Usage Example

この提案では実行のための2つの主要なコンポーネント(処理実行インターフェースと処理の表現)と、それらの間の相互関係についての要件を定義する。それぞれ、executorsenderreceiverと呼ばれる。

// この提案のAPIはstd::execution名前空間の下に定義される
using namespace std::execution;

// 任意の場所(たとえばスレッドプール)で処理を実行するexecutorを取得する
std::static_thread_pool pool(16);
executor auto ex = pool.executor(); // この記法はコンセプトによる変数の制約

// 高レベルのライブラリによる処理がどこで実行されるかを記述するためにexecutorを使用する
perform_business_logic(ex);

// あるいは、この提案によるよりプリミティブなAPIを直接使用することもできる

// スレッドプールに処理を投げ、すぐ実行する
execute(ex, []{ std::cout << "Hello world from the thread pool!"; });

// スレッドプールに処理を投げすぐ実行し、完了まで現在のスレッドをブロックする
execute(std::require(ex, blocking.always), foo);

// 依存性のある一連の処理を記述し、後で実行する
sender auto begin    = schedule(ex);
sender auto hi_again = then(begin, []{ std::cout << "Hi again! Have an int."; return 13; });
sender auto work     = then(hi_again, [](int arg) { return arg + 42; });

// 処理の最終結果を標準出力へ出力する
receiver auto print_result = as_receiver([](int arg) { std::cout << "Received " << std::endl; });

// 先ほど定義したworkによる処理をreceiverと組み合わせてスレッドプールで実行する
submit(work, print_result);

1.3 Executors Execute Work

軽量なハンドルとして、executorに実行コンテキストへの統一されたアクセスを課する。

executorは処理が物理的に実行されるハードウェアを抽象化することで、処理を作成するための統一的なインターフェースを提供する。先ほどのサンプルコードでの実行リソースはスレッドプールだった。そのほかには、SIMDユニットやGPU、単純な現在のスレッド、などが含まれる。そのような実行リソース一般を指して 実行コンテキスト(execution context と呼ぶ。

そのような実行リソースへの軽量なハンドルとして、executorには実行コンテキストへの統一されたアクセスを課する。統一性があることで、ライブラリインターフェースの背後で間接的に実行される場合でも(そこにexecutorを受け渡すことで)、処理が実行される場所を制御することができる。

基本的なexecutorインターフェースは、利用者が処理を実行するためのexecute()関数である。

// 何かしらのexecutorを取得する
executor auto ex = ...

// 処理を引数なしで呼び出し可能として定義する
invocable auto work = []{ cout << "My work" << endl; };

// 定義した処理workをexecuteカスタマイゼーションポイントを介して実行する
execute(ex, work);

execute()それ自体は基本的なfire-and-forgetスタイルのインターフェースで、引数なしで呼び出し可能な1つの呼び出し可能オブジェクトを受け入れ、作成した作業を識別・操作するための戻り値を返さない。このようにして、普遍性と利便性をトレードオフにしている。結果として、ほとんどのプログラマはより便利な高レベルのライブラリを介してexecutorを利用することになるだろう。我々が想定している非同期STLはそのようなライブラリの一例である。

std::asyncexecutorと相互運用可能なように拡張し、ユーザーが実行を制御できるようにする方法を考えてみる。

template<class Executor, class F, class Args...>
future<invoke_result_t<F,Args...>> async(const Executor& ex, F&& f, Args&&... args) {
  // 処理とその引数をパッケージングする
  packaged_task work(forward<F>(f), forward<Args>(args)...);

  // futureオブジェクトを取得
  auto result = work.get_future();

  // 与えられたexecutorで処理を実行
  execution::execute(ex, move(work));

  return result;
}

このように拡張することの利点は、ユーザーが複数のスレッドプールの中から1つを選択して、対応するexecutorstd::asyncに与えるだけでどのプールを使用するかを正確にコントロールでき、処理のパッケージングや処理のプールへの送出などの不便な部分はライブラリの仕事になる点にある。

Authoring executors

プログラマexecute()関数とともに型を定義することで、カスタムexecutorを定義することができる。

ユーザーの処理をその内部で実行するexecute()関数を持つexecutor実装を考えてみる。

struct inline_executor {
  // define execute
  template<class F>
  void execute(F&& f) const noexcept {
    std::invoke(std::forward<F>(f));
  }

  // enable comparisons
  auto operator<=>(const inline_executor&) const = default;
};

<=>による比較は、2つのexecutorが同じ実行リソースを参照しており、同じ意味論の下で処理が実行されるかを判断するものである。executor/executor_ofコンセプトはこれらを要約したもので、前者は個別のexecutorを検証し、後者はexecutorと処理の両方が利用可能な場合に検証する。

Executor customization

executorをカスタマイズし、実行をアクセラレートしたり新しい振舞を追加することができる。先ほどのサンプルコードは新しいexecutor型を定義するものだったが、より細かい/粗い粒度でのカスタマイズも可能である。それぞれ エグゼキュータープロパティ(executor propertie制御構造(control structure と呼ばれる。

Executor properties

executor propertieexecute()の最小の契約を超えてオプショナルな動作要件を実装に伝達する。本提案でもいくつかを規定する。エキスパートな実装者によってより高いレベルの抽象下の下でこれらの要件が課されることを想定している。

原則として、オプションの動的データメンバや関数引数はこれらの要件を伝達することができるが、C++にはコンパイル時にカスタマイズする機能が必要である。また、そのようなオプションのパラメータは組み合わせることによって多くの関数の変種を生み出してしまう

代わりに、statically-actionableなプロパティはそれらの要件を考慮し、エグゼキューターAPIの組み合わせ爆発を抑止する。例えば、ブロッキングを伴う処理の優先度付き実行のための要件を考えてみる。スケーラブルではない設計では、それぞれの要件を個別の関数に乗算することでオプションをexecute()のインターフェースに埋め込むことができるかもしれない(execute, blocking_execute, execute_with_priority, blocking_execute_with_priority, ...etc)。

本稿におけるexecutorでは、require/preferに基づくP1393のプロパティ設計を採用することによってこのような組み合わせ爆発を回避する。

// 何かしらのexecutorを取得する
executor auto ex = ...;

// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = std::require(ex, execution::blocking.always);

// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(blocking_ex, execution::priority(p));

// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);

それぞれのrequire/preferexecutorを要求されたプロパティを持つものに変換する。この例では、もしブロッキングエグゼキューターに変換できない場合はrequire()の呼び出しはコンパイルエラーとなる。prefer()はヒントを伝達するための弱い要求であり、その要求は無視される可能性があるため、コンパイルは常に成功する。

呼び出し元を決してブロックしないバージョンのstd::asyncを考えてみる。

template<executor E, class F, class... Args>
auto really_async(const E& ex, F&& f, Args&&... args) {
  using namespace execution;

  // 処理とその引数をパッケージングする
  packaged_task work(forward<F>(f), forward<Args>(args)...);

  // futureオブジェクトを取得
  auto result = work.get_future();

  // 指定されたexecutorでブロッキング無しで処理を実行
  execute(require(ex, blocking.never), move(work));

  return result;
}

このような拡張によって、よく知られたstd::asyncの危険性に対処できる。

// 戻り値のfutureは破棄されたためasyncの呼び出しはブロックする
std::async(foo);

// こちらは決してブロックしない
// futureがデストラクタで処理の完了を待機するのはstd::asyncが返したもののみであるため
really_async(foo);
Control structures

control structureexecutorがそれらをフックできるようにすることでより高い抽象化レベルでのカスタマイズを可能にし、特定の実行コンテキストにおいてより効率的な実装が可能な場合に有用である。本提案が最初に定義するcontrol structureは単一の操作で関数呼び出しのグループを作成するbulk_execute()である。このパターンは広範囲の効率的な実装を可能にし、C++と標準ライブラリにとって極めて重要なものである。

デフォルトのbulk_execute()は繰り返しexecute()を呼び出すだけであるが、個々の処理を繰り返し実行するのはスケールせず効率が悪い。そのため、多くのプラットフォームはそのようなバルク処理を明示的かつ効率的に実行するAPIを備えている。そのような場合、カスタムのbulk_execute()はそれらの高速化されたバルクAPIに直接アクセスすることで非効率的なプラットフォームとのやりとりを回避しスカラーAPIの使用を最適化することができる。

bulk_execute()は呼び出し可能オブジェクトと呼び出し回数を受け取る。可能な実装を考えてみる。

struct simd_executor : inline_executor { // 初めに、executor要件を満足するために、inline_executorを継承する

  template<class F>
  simd_sender bulk_execute(F f, size_t n) const {
    #pragma simd
    for(size_t i = 0; i != n; ++i) {
      std::invoke(f, i);
    }

    return {};
  }
};

bulk_execute()を高速化するために、simd_executorSIMDループを使用する。

bulk_execute()は一度に複数の処理が必要な場合に使用する。

template<class Executor, class F, class Range>
void my_for_each(const Executor& ex, F f, Range rng) {
  // バルク実行を要求し、senerを取得する
  sender auto s = execution::bulk_execute(ex, [=](size_t i) {
    f(rng[i]);
  }, std::ranges::size(rng));

  // 実行を開始し処理の完了を待機
  execution::sync_wait(s);
}

先程の例のsimd_executorによるbulk_execute()実装は熱心(即座)に実行されるが、bulk_execute()の意味論はそれを要求しない。上記my_for_each()が示すように、execute()とは異なりbulk_execute()はオプションで実行を延期可能な遅延操作の一例である。bulk_execute()が返すトークン(上記コード中のs)はユーザーが処理を開始したり、実行対象の処理と対話するために使用することができるsenderの一例である。例えば、senderを渡してsync_wait()を呼び出せば、呼び出し元の処理が継続される前にバルク処理が完了する事を保証する。senderreceiverは次のセクションの主題である。

1.4 Senders and Receivers Represent Work

executorコンセプトは指定された実行コンテキストで単一の操作を実行するという基本的なニーズに対応しているが、executorコンセプトの表現力は限られている。execute()はスケジュールされた処理へのハンドルを返すのではなくvoidを返し、executor抽象は操作をチェーンしてその結果の値やエラー、キャンセルシグナルを下流の処理に伝播させる汎用的な方法を提供しない。また、処理の登録から実行までの間に発生しうるスケジューリングエラーを処理する方法がなく、一連の操作に関連する状態オブジェクトのアロケーションとライフタイムを制御する便利な方法も提供されていない。

そのような制御方法を提供しないままでは、(Stepanovの意味で)汎用的な非同期アルゴリズムの効率的で機能的なデフォルト実装を定義することはできない。このギャップを埋めるために、本稿ではsenderreceiverの関連する2つの抽象を提案する。具体的な動機を以下に述べる。

1.4.1 Generic async algorithm example: retry

retry()senderreceiverが可能にする汎用アルゴリズムの一種であり、とても単純な意味論を持つ。実行コンテキストで処理をスケジュールし、恙なく成功した場合とユーザーがキャンセルした場合にその処理は完了したとみなし、それ以外、例えばスケジューリングエラーが発生した場合などには処理の実行を再試行する。

template<invocable Fn>
void retry(executor_of<Fn> auto ex, Fn fn) {
  // ???
}

executorだけではスケジューリングエラーをキャッチして対処するポータブルな方法がないため、このようなアルゴリズムの一般的な実装を妨げている。後程、senderreceiverによってこれがどのように実装されるのかを示す。

1.4.2 Goal: an asynchronous STL

retry()のような汎用非同期アルゴリズムの定義を後押しする適切に選択されたコンセプトは、効率的な非同期処理グラフの作成を簡素化する。ここに、我々の思い描いている非同期プログラムについて少しのサンプルコードを紹介する(P1897から借用している)。

sender auto s = just(3) |                                  // 即座に`3`を生成
                via(scheduler1) |                          // 実行コンテキストを遷移(変更)
                then([](int a){return a+1;}) |             // 継続処理をチェーン
                then([](int a){return a*2;}) |             // さらにもう一つ継続処理をチェーン
                via(scheduler2) |                          // 実行コンテキストを遷移(変更)
                handle_error([](auto e){return just(3);}); // エラーハンドル、デフォルト値を返すようにする
int r = sync_wait(s);                                      // 一連の処理の結果を待機

just(3)は、その戻り値の型が正しくコンセプトを満たしている非同期APIの呼び出しに置き換えても、このプログラムの正しさを維持することがことが可能であるべきである。when_allwhen_anyのような汎用アルゴリズムによってユーザーは、DAGを用いて並行処理のfork/joinを表現することが可能になる。STLイテレータ抽象と同様にコンセプト的な要件を満たすコストは、広く再利用と連携が可能なライブラリのアルゴリズムの表現力によって相殺される。

1.4.3 Current techniques

依存関係のある非同期実行のチェーンを作成するテクニックはいくつも存在している。普通のコールバックはC++でもそれ以外の場所でも長年にわたり成功を収めてきた。現代のコードベースは継続をサポートするfuture抽象のバリエーションに切り替わっている(例えばstd::experimental::future::then、他の所ではJavascriptのPromiseチェーンなど)。C++20以降はコルーチンがより標準的になり、非同期操作を起動するとawaitableオブジェクトが返されるようになることだろう。これらのアプローチにはそれぞれ長所と短所がある。

Futures

futureはこれまで実現されているように、共有状態とその同期のためにメモリの動的確保と管理を必要とし、通常は渡された処理と継続の型消去も必要とする。これらのコストの多くの部分はすでにスケジューリング済みの実行操作のハンドルとしてのfutureの性質に固有のもので、これらの費用は多くの用途で将来の抽象化を排除しており、汎用的なメカニズムの基礎としては不適切な選択である。

Coroutines

コルーチンも同様の問題を抱えているが、依存関係のある処理をチェーンさせる際に通常はサスペンドを開始するため、同期化を回避できる。多くの場合、コルーチンフレーム(コルーチンに関連するものを格納しておくメモリ領域)は動的確保が不可避である。そのため、組み込み環境やヘテロジニアスな環境では、その詳細に細心の注意を払う必要がある。協調動作しているコルーチンを素早く安全に終了させるためには満足のいかない解決策が必要となるため、コルーチンもまた非同期処理をキャンセル可能な候補とはならない。一方で、例外は非効率的であるため多くの環境で許可されず、また、co_yieldステータスコードを返すような扱いにくいアドホックカニズムは正しさの妨げになっている。これらの事はP1662で全容の解説がなされている。

Callbacks

コールバックは処理のチェーンを作成するための最も単純で強力かつ効率的なメカニズムであるが、それ自体に問題がある。コールバックは処理結果の値かエラーのどちらかを伝搬する必要があり、このシンプルな要件がいくつものインターフェースの可能性をもたらしている。しかし、それらインターフェースの標準的なものがないことが汎用的な設計を妨げている。さらに、それらのインターフェースの可能性の中には、ユーザーが上流の処理を停止してクリーンアップするよう要求した場合のキャンセル通知に対応しているものは殆どない。

1.5 Receiver, sender, and scheduler

前述の動機付けのように、結果値、エラー、キャンセル伝播の存在する場合の汎用非同期プログラミングの必要性に対応するためのプリミティブを導入する。

1.5.1 Receiver

receiverは特定のインターフェースと意味論を持つコールバックである。通常のコールバックは関数呼び出し構文と単一のシグネチャで成功とエラーの両方を処理するが、receiverには結果値、エラー、完了(あるいはキャンセル)の3つの個別のチャンネルがある。

これらのチャンネルはカスタマイゼーションポイントとして指定され、receiver_of<R,Ts...>のモデルとなる型Rはそれらをサポートする。

std::execution::set_value(r, ts...); // 成功を通知するがset_value自体が失敗する可能性はある
std::execution::set_error(r, ep);    // 失敗を通知(epはstd::exception_ptr)、これは失敗しない
std::execution::set_done(r);         // 停止を通知、失敗しない

これら3つの関数のうちのどれか一つだけが、receiverが破棄される前にそのreceiverによって呼び出されなければならない。これらの各インターフェースは処理の「終端」とみなされる。つまり、特定のreceiverは3つのうちのどれか1つが呼ばれた場合は残りの2つが呼ばれることはないと仮定することができる。唯一の例外はset_value()が例外を送出して終了した場合で、その時receiverはまだ完了していないため破棄する前に別の関数を呼び出す必要がある。set_value()の呼び出しが失敗した場合に正確性を保つには、続いてset_error/set_doneのどちらかの呼び出しが必要である。そのため、receiverset_value()の2回目の呼び出しがwell-formedであることを保証する必要はない。これらの要件を総称してreceiver contractと呼ぶ。

receiverのインターフェースは一見真新しく見えるかもしれないが、単なるコールバックであることに変わりはない。さらに、std::promiseset_value/set_exceptionが本質的に同じインターフェースを提供していることを考えれば、そのような真新しさは消えるだろう。このようなインターフェースと意味論の選択は、senderとともにretry()のような多くの有用な非同期アルゴリズムの汎用実装を可能にする。

1.5.2 Sender

senderは実行のスケジュールがまだされていない処理を表し、継続(receiver)を追加してからローンチするか、実行のためにキューに入れる必要がある。(senderに)connect()されたreceiverに対するsenderの義務は、3つのreceiver関数のうちのどれか一つが正常に完了することを保証しreceiver contractを履行することである。

この提案の以前のバージョンではこれらの2つの作業(継続のアタッチと実行のためのローンチ)を1つの操作submit()に集約していた。本稿ではそのsubmit()を、connect()ステップ(senderreceiverを1つのoperation stateにパッケージングする)とstart()ステップ(論理的に操作を開始し、操作が完了したときに呼ばれるreceiverの完了通知関数のスケジューリング)の2つの操作に分割することを提案する。

// P0443R12(以前のバージョン
std::execution::submit(snd, rec);

// P0443R13(このバージョン
auto state = std::execution::connect(snd, rec);
// ... 後からスタート
std::execution::start(state);

この分割は最適化のための興味深い機会を提供し、senderとコルーチンを調和させる

senderコンセプトそれ自身はsenderの処理が実行される実行コンテキストに対して何ら要件を課さない。その代わり、senderコンセプトのモデルとなる特定のsenderは、receiverの3つの関数が呼び出されるコンテキストについてより強い保証を提供することができる(senderコンセプトの意味論的な制約として要求されうる)。これは特に、schedulerによって作成されたsenderに当てはまる。

1.5.3 Scheduler

多くの汎用非同期アルゴリズムは、同じ実行コンテキストに対して複数の実行エージェントを作成する。したがって、既知の実行コンテキストで完了するsingle-shot senderを用いてそれらのアルゴリズムをパラメータ化するだけでは不十分である。むしろ、これらのアルゴリズムの方をsingle-shot senderのファクトリに渡す方が理にかなっている。そのようなファクトリはschedulerと呼ばれ、scheduleという単一の基本操作を持つ。

sender auto s = std::execution::schedule(sched);
// OK、sはschedの示す実行コンテキストで完了する何も返さないsingle-shot sender

executorと同様にschedulerも実行コンテキストへのハンドルとして機能するが、一方でexecutorとは事なりschedulerは処理の実行を遅延して実行コンテキストへ投入する。ただし、ある単一の型がexecutorコンセプトとschedulerコンセプトの両方のモデルとなる場合がある。schedulerコンセプトを包摂することによって、一定期間が経過するまで実行を延期する、またはキャンセルする機能が追加されることを想定している。

1.6 Senders, receivers, and generic algorithms

有用なコンセプトは汎用アルゴリズムを制約する一方で、それらコンセプトの基本操作によるデフォルト実装を許可する。以下に、これらsenderreceiverが一般的な非同期アルゴリズムの効率的な実装を提供する方法を示す。殆どの汎用的な非同期アルゴリズムsenderを受け取り、それによるconnect()の呼び出しがアルゴリズムのロジックを実装するアダプタをラップしたreceiverを返すように実装されていることを想定している。次のthen()アルゴリズムは、senderで継続関数をチェーンする簡単なデモである。

1.6.1 Algorithm then

次のコードはstd::experimental::future::thenのように、非同期処理の結果が利用可能な場合にその結果に適用される関数をスケジュールするthen()アルゴリズムを実装したものである。このコードはアルゴリズムがどのようにしてreceiverにアダプトしアルゴリズムのロジックをコード化するかを示している。

template<receiver R, class F>
struct _then_receiver : R { // 説明のために、Rからset_errorとset_doneを継承する
  F f_;

  // 呼び出し可能オブジェクトf_を呼び出し、その結果を基底クラスに渡すことでset_valueをカスタマイズする
  template<class... As>
    requires receiver_of<R, invoke_result_t<F, As...>>
  void set_value(Args&&... args) && noexcept(/*...*/) {
      ::set_value((R&&) *this, invoke((F&&) f_, (As&&) as...));
  }

  // f_の戻り値型がvoidの場合の対応など、省略
};

template<sender S, class F>
struct _then_sender : _sender_base {
  S s_; // sender
  F f_; // callble

  template<receiver R>
    requires sender_to<S, _then_receiver<R, F>>
  state_t<S, _then_receiver<R, F>> connect(R r) && {
      return ::connect((S&&)s_, _then_receiver<R, F>{(R&&)r, (F&&)f_});
  }
};

template<sender S, class F>
sender auto then(S s, F f) {
  return _then_sender{{}, (S&&)s, (F&&)f};
}

非同期senderを返すAPIasync_fooが与えられた場合、then()のユーザーはその非同期処理の結果が利用可能になったときに任意のコードを実行することができる。

sender auto s = then(async_foo(args...), [](auto result) {/* stuff... */});

この1文によって合成(チェーン)された非同期操作が構築される。ユーザーがこの操作の実行をスケジュールしたい場合、receiverconnect()し得られるoperation stateオブジェクトを用いてstart()を呼び出す。

then()を使用して実行コンテキストでの処理の実行スケジュールを設定することもできる。schedulerコンセプトを満たすstatic_thread_poolのオブジェクトpoolが与えられれば、ユーザーは次のようにすることができる。

sender auto s = then(
    std::execution::schedule( pool ),
    []{ std::printf("hello world"); } );

このコードでは、実行されるとスレッドプール内のスレッドからprintfを呼び出すsenderを作成している。

任意のコードを実行することができないようなヘテロジニアスコンピューティング環境が存在しており、その場合上記の様なthen()の実装は機能しないか(その環境にとって)未知のコードを実行するためにホストへの遷移コストが発生する。従って、then()自体とそのほかのいくつかの基本的なアルゴリズムプリミティブは実行コンテキスト毎にカスタマイズ可能である必要がある。

then()の動作例:https://godbolt.org/z/dafqM-

1.6.2 Algorithm retry

前述したように、retry()のアイデアは非同期操作の失敗時に再試行し、成功やキャンセル時は再試行をしない。retry()の正しい汎用実装の鍵はエラーが起きた場合とキャンセルされた場合を区別できることにある。then()アルゴリズムと同様に、retry()アルゴリズムアルゴリズムのロジックをretry()されるsenderconnect()されているカスタムreceiverに配置する。このカスタムreceiverにはシグナルを変更せずに渡すだけのset_value/set_doneメンバ関数が定義されている。一方、set_error()メンバ関数は元のsenderとカスタムreceiverの新しいオブジェクトを用いて再度connect()することで、その場でoperation stateを再構築する。そして、その新しいoperation stateが再びstart()され、実質的に元のsenderが再実行される。

付録retry()アルゴリズムソースコードが掲載されている。retry()アルゴリズムシグネチャは単純だ

sender auto retry(sender auto s);

操作を再実行する実行コンテキストはパラメータ化されていない。これは、指定された実行コンテキストでsenderを実行するようにスケジュールする関数の存在を仮定できるためである。

sender auto on(sender auto s, scheduler auto sched);

retry(on(s, sched));  // schedの実行コンテキストで再実行してもらう

これら2つの関数があれば、ユーザーはretry(on(s, sched))とすることで指定した実行コンテキストで処理を再実行することができる。

1.6.3 Toward an asynchronous STL

then()retry()の2つは、senderreceiverによって表現可能な多くの汎用非同期アルゴリズムのたった2つにすぎない。他の重要なアルゴリズムにはon()via()の2つがある。前者は指定したschedulerで実行されるようにsenderをスケジュールし、後者は指定したscheduler上でsenderconnect()されている継続を実行させる。このようにして、ある実行コンテキストから別の実行コンテキストへ遷移する非同期処理のチェーンを作成することができる。

そのほかの重要なアルゴリズムには、fork/joinセマンティクスをカプセル化するwhen_all/when_anyがある。これらのアルゴリズムやその他の仕組みを使用すれば、非同期処理全体のDAGを作成し実行できる。when_anyは汎用タイムアウトアルゴリズムを実装するために使用でき、一定時間スリープしてから「完了」シグナルを通知するsender実装とともに使用することでそのようなアルゴリズムを構成することができる。要するに、sender/receiverは汎用非同期アルゴリズムの豊富なセットをSTLにある既存のStepanovによるシーケンスアルゴリズムと同時に使用することができる。senderを返す非同期APIはこれらの汎用アルゴリズムで使用でき、再利用性が向上する。P1897ではそれらのアルゴリズムの初期セットが提案されている。

Summary

我々は、C++プログラマがエレガントな標準インターフェースを介して多様なハードウェアリソース上での非同期並行処理を表現できる未来を想像している。この提案は柔軟な実行のための基盤を提供し、その目標の実現に向けた最初の一歩である。executorは処理を実行するハードウェアをリソースを表現する。senderreceiverは遅延構築された非同期処理のDAGを表現する。これらのプリミティブは、処理がいつどこで行われるのかをプログラマが制御できるようにする。

2 Proposed Wording

あまりに長いので各ヘッダのsynopsisだけコピペして後は省略。

2.1.2 Header synopsis

namespace std {
namespace execution {

  // Exception types:

  extern runtime_error const invocation-error; // exposition only
  struct receiver_invocation_error : runtime_error, nested_exception {
    receiver_invocation_error() noexcept
      : runtime_error(invocation-error), nested_exception() {}
  };

  // Invocable archetype

  using invocable_archetype = unspecified;

  // Customization points:

  inline namespace unspecified{
    inline constexpr unspecified set_value = unspecified;

    inline constexpr unspecified set_done = unspecified;

    inline constexpr unspecified set_error = unspecified;

    inline constexpr unspecified execute = unspecified;

    inline constexpr unspecified connect = unspecified;

    inline constexpr unspecified start = unspecified;

    inline constexpr unspecified submit = unspecified;

    inline constexpr unspecified schedule = unspecified;

    inline constexpr unspecified bulk_execute = unspecified;
  }

  template<class S, class R>
    using connect_result_t = invoke_result_t<decltype(connect), S, R>;

  template<class, class> struct as-receiver; // exposition only

  template<class, class> struct as-invocable; // exposition only

  // Concepts:

  template<class T, class E = exception_ptr>
    concept receiver = see-below;

  template<class T, class... An>
    concept receiver_of = see-below;

  template<class R, class... An>
    inline constexpr bool is_nothrow_receiver_of_v =
      receiver_of<R, An...> &&
      is_nothrow_invocable_v<decltype(set_value), R, An...>;

  template<class O>
    concept operation_state = see-below;

  template<class S>
    concept sender = see-below;

  template<class S>
    concept typed_sender = see-below;

  template<class S, class R>
    concept sender_to = see-below;

  template<class S>
    concept scheduler = see-below;

  template<class E>
    concept executor = see-below;

  template<class E, class F>
    concept executor_of = see-below;

  // Sender and receiver utilities type
  namespace unspecified { struct sender_base {}; }
  using unspecified::sender_base;

  template<class S> struct sender_traits;

  // Associated execution context property:

  struct context_t;

  constexpr context_t context;

  // Blocking properties:

  struct blocking_t;

  constexpr blocking_t blocking;

  // Properties to allow adaptation of blocking and directionality:

  struct blocking_adaptation_t;

  constexpr blocking_adaptation_t blocking_adaptation;

  // Properties to indicate if submitted tasks represent continuations:

  struct relationship_t;

  constexpr relationship_t relationship;

  // Properties to indicate likely task submission in the future:

  struct outstanding_work_t;

  constexpr outstanding_work_t outstanding_work;

  // Properties for bulk execution guarantees:

  struct bulk_guarantee_t;

  constexpr bulk_guarantee_t bulk_guarantee;

  // Properties for mapping of execution on to threads:

  struct mapping_t;

  constexpr mapping_t mapping;

  // Memory allocation properties:

  template <typename ProtoAllocator>
  struct allocator_t;

  constexpr allocator_t<void> allocator;

  // Executor type traits:

  template<class Executor> struct executor_shape;
  template<class Executor> struct executor_index;

  template<class Executor> using executor_shape_t = typename executor_shape<Executor>::type;
  template<class Executor> using executor_index_t = typename executor_index<Executor>::type;

  // Polymorphic executor support:

  class bad_executor;

  template <class... SupportableProperties> class any_executor;

  template<class Property> struct prefer_only;

} // namespace execution
} // namespace std

2.5.1 Header <thread_pool> synopsis

namespace std {

  class static_thread_pool;

} // namespace std

2.6 Changelog

まあいいよね・・・

2.7 Appendix: Executors Bibilography

余力があったらそのうち・・・

2.8 Appendix: A note on coroutines

余力があったらいつか・・・

2.9 Appendix: The retry Algorithm

余力があったらきっと・・・

[C++]モジュールとプリプロセス

C++20より使用可能になるはずのモジュールは3つの新しいキーワードを用いて記述されますが、それらのキーワードは必ずしも予約語ではなく、コンパイラによる涙ぐましい努力によって特殊な扱われ方をしています。

たとえば、全部入りを書くと次のようになります。

module; // グローバルモジュールフラグメント
#include <iosream>
export module sample_module;  // モジュール宣言

// インポート宣言
import <vector>;
export import <type_traits>;

// エクスポート宣言
export int f();

// プライベートモジュールフラグメント
module : private;

int f() {
  return 20;
}

これはプリプロセス後(翻訳フェーズ4の後)に、おおよそ次のようになります。

__module_keyword;
#include <iosream>
__export_keyword __module_keyword sample_module;

__import_keyword <vector>;
__export_keyword __import_keyword <type_traits>;

// export宣言はプリプロセッシングディレクティブでは無い
export int f();

__module_keyword : private;

int f() {
  return 20;
}

これは実際には実装定義なのでどう置き換えられるのかは不明ですが、これら置換されているmodule, import, exportトークンの現れていた所とその行は実はプリプロセッシングディレクティブとして処理され、その結果としてこのような謎のトークンが生成されます。そして、C++のコードとしてはこれらの謎のトークンによるものをモジュール宣言やインポート宣言などとして扱います。

逆に、これらのプリプロセッシングディレクティブによって導入されるトークン置換後の宣言のみが、モジュール宣言やインポート宣言などとして扱われ、それ以外にそれらを直接記述する方法はありません。

何でこんなことをしているのかというと、ひとえに依存関係の探索を高速にするためです。

当初のモジュール宣言やインポート宣言はプリプロセス後にC++のコードとしての意味論の下で認識され、そこでようやくそのファイルがモジュールであるのか、またその依存関係を把握することができます。つまり、依存関係をスキャンしようとすればC++コードをある程度コンパイルせねばなりません。

対して、現在のC++における依存関係スキャンは相手にするのが#includeだけなのでプリプロセスさえ行えば依存関係を把握可能です。C++コンパイラは必要なく、プリプロセッシングディレクティブ以外の行は無視することができます。当然、こちらの方が圧倒的に高速かつ簡単です。

依存関係の把握はモジュールのビルドに関わってくる重要な問題であり、依存関係スキャンを高速に行えればプログラム全体のビルド時間を短縮できます。また、#includeに対して不利な点が増えればユーザーのモジュールへの移行を妨げてしまう事にもなりかねません。

そのため、モジュール宣言やインポート宣言をプリプロセッシングディレクティブによってのみ導入することでプリプロセスの段階でそれらの宣言を識別可能にし、#includeと同様にプリプロセスさえ行えば依存関係スキャンが可能かつ、プリプロセッシングディレクティブ以外の行を考慮しなくてもよくなるように変更されました。

また、両方ともマクロ展開よりも前に処理されるためマクロによって導入することができません(その名前は導入可能)。また、モジュール宣言は#if系ディレクティブよりも前に処理されるため、あるファイルがモジュールであるかどうかをマクロによって切り替えることもできません。これらのことによって結果的に、完全なプリプロセスを必要とせずにソースファイルの依存関係をスキャンすることができるようになっています。

ちなみに、VS2019 update5でMSVCはこれらのことを実装しているようです。

ディレクティブ導入トーク

ディレクティブ導入トークンはプリプロセッシングディレクティブを導入するトークン(文字列)です。従来のプリプロセッシングディレクティブでは、ある行の最初の非空白文字が#で始まる行がディレクティブ導入トークンとして扱われていました。そこに次の3つが追加され、新しいプリプロセッシングディレクティブとして扱われるようになります。

  • import : 以下のいずれかが同じ行で後に続くもの
    • <
    • 識別子(identifier
    • 文字列リテラル
    • :::とは区別される)
  • module : 以下のいずれかが同じ行で後に続くもの
    • 識別子
    • :::とは区別される)
    • ;
  • export : 上記2つの形式のどちらかの前に現れるもの

このディレクティブ導入トークンに該当するトークンで始まる行はプリプロセッシングディレクティブとして扱われ、対応する形式のプリプロセッシングディレクティブがあれば処理されます。もし対応するディレクティブが存在しない場合はコンパイルエラーとなります。

このディレクティブ導入トークンとしてみなされなかったmodule, import, exportトークンは通常の識別子として処理されます。このため、module, importはクラス名や変数名に使用できます(注意は必要ですが)。exportは元々予約語のため使用できません。

モジュールディレクティブ

モジュールディレクティブはモジュール宣言を導入するためのプリプロセッシングディレクティブです。EBNFは次のような形式です。

export(opt) module pp-tokens(opt) ; new-line

スペースの空いているところは任意個数の空白文字を含む事ができますが改行は含まれまず、(opt)とあるのはあってもなくても良いやつです。改行が入って良いいのは行末(new-line)だけなので、モジュールディレクティブはセミコロンまで含めて1行で書く必要があります。

pp-tokensはマクロによって置換される必要のあるトークン列を表していて、モジュール名が来る筈です。すなわち、モジュール名はマクロによって導入できます。また、(opt)とはありますが名前が無い場合はプリプロセスの後、C++構文解析時にコンパイルエラーになります。

このディレクティブの効果は、ディレクティブ中のexportmoduleトークンを実装定義のexport-keywordmodule-keywordに置換します。これによってこの行はプリプロセッシングディレクティブではなくなるため翻訳フェーズ4の終わりに削除されず、あとでモジュール宣言として処理されます。

// OKな例
export module module_name;

module module_name;

module module:part:partition_name;  // モジュールパーティションの宣言

export   module /*コメントは1つの空白と見なされるので間に入っても良い*/ module_name;

// NG例
export module;  // プリプロセスよりあとでコンパイルエラー

module 1module; // 通常の識別子同様、数字で始まってはならない

export
module
module_name;  // 1行で書く

module module_name
;

フラグメント導入ディレクティブ

moduleトークンはさらにグローバルモジュールフラグメントとプライベートモジュールフラグメントの2つの領域を導入します。それぞれEBNFは次のように定義されています。

// グローバルモジュールフラグメント
module ; new-line group(opt)

// プライベートモジュールフラグメント
module : private ; new-line group(opt)

groupには任意のコード列が入ります。(opt)とはありますが空になる事はほぼないでしょう。

これらのものもセミコロンまで含めて1行で書く必要があります。ディレクティブとしての効果はモジュールディレクティブと同様に、含まれるmoduleトークンをmodule-keywordに置換します。privateはそのままです。

モジュールファイルの識別

翻訳フェーズ4の開始時にプリプロセッシングディレクティブのパースを行う際、コンパイラはまず1行目が上記グローバルモジュールフラグメント導入ディレクティブかモジュールディレクティブのどちらかであるかによって現在のファイルがモジュールファイルであるのか通常のソースファイルであるのかを識別します。

上記モジュールディレクティブとフラグメント導入ディレクティブが処理されるのはモジュールファイルの中だけです。また、それらのディレクティブは正しく一回づつしか現れてはなりません。#if等で条件付きで導入することもできません。

通常のファイルとして処理が開始された場合はモジュールディレクティブやフラグメント導入ディレクティブは現れてはならず、現れればコンパイルエラーとなります。

従って、グローバルモジュールフラグメント導入ディレクティブおよびそれが無い場合のモジュールディレクティブはいかなるディレクティブの後にも書く事ができません(ただし、空白列やコメントはあってもokです)。必ずファイルの先頭に来ていなければなりません。

グローバルモジュールフラグメントがある場合でも、#include#ifdef等によって後続のモジュールディレクティブが導入されることはありません。これがなされた場合、コンパイルエラーとなります。

これらのことはEBNFとして表現され規定されています。

インポートディレクティブ

インポートディレクティブはインポート宣言を導入するためのプリプロセッシングディレクティブで、EBNFは次のようになります。

export(opt) import header-name pp-tokens(opt) ; new-line
export(opt) import header-name-tokens pp-tokens(opt) ; new-line
export(opt) import pp-tokens ; new-line

インポートディレクティブもセミコロンまで含めて1行で書かなければなりません。最初の2つはヘッダユニットのインポートに対応し、3つ目の形式がモジュールのインポートに対応します。これもまた、インポート対象のモジュール名やヘッダ名をマクロによって導入できます。

このディレクティブの効果はディレクティブ中のexportimportトークンを実装定義のexport-keywordimport-keywordに置換します。その後でインポート宣言として処理されます。
加えて最初の2つの形式では、指定されたヘッダ名に対応するヘッダユニットからマクロをインポートします。インポートされたマクロはディレクティブの末尾の改行の直後で定義されます。

// ok
import <vector>;
export import "mayhaader.hpp";
import module_name;
import module:part:partition;
import :partition;

// ng
export
import
module_name;  // 1行で書く

import <iostream>
;

export
import module_name; // プリプロセスよりあとでコンパイルエラー

なお、インポートディレクティブはimportあるいはexportがオブジェクトマクロ名として登録されているコンテキストで現れた場合コンパイルエラーになります。

import <vector>;  // この時点ではok

#define import export import

import <iostrema>; // error!

サンプルコード

OKな例

module;
#define m x
#define im anoter:module
export module m;  // モジュール名をマクロ展開するのはok

import im;  // モジュール名やヘッダ名をマクロ展開するのはok
// これらはプリプロセッシングディレクティブとして扱われない

::import x = {};
::module y = {};

import::inner xi = {};
module::inner yi = {};

void f(Import *import) {
  import->doImport();
}

ダメな例

// このファイルは常に非モジュールファイルとして扱われる
#ifdef INCLUDE_GUARD
#define INCLUDE_GUARD

export module mymodule; // モジュールディレクティブではない、コンパイルエラー

#endif
module;
#if FOO
export module foo;  // ここではモジュールディレクティブとみなされない、コンパイルエラー
#else
export module bar;  // ここではモジュールディレクティブとみなされない、コンパイルエラー
#endif
module;
#define EMPTY
EMPTY export module m;  // モジュールディレクティブではない、コンパイルエラー
                        // モジュール名以外の部分はマクロがあってはならない
export module m;

#ifdef COND_PRIVATE
module : private; // プライベートモジュールフラグメント導入ディレクティブとみなされない、コンパイルエラー
#endif

予めコンパイラオプションで-Dm="export module x;"などとしていたとして

m // モジュールディレクティブではない、マクロ展開後コンパイルエラー
module y = {};  // ファイル先頭にあるとモジュールディレクティブとしてみなされる
                // プリプロセス後にコンパイルエラー
namespace N {
  module a; // モジュールディレクティブではないが、プリプロセッシングディレクティブと認識される、コンパイルエラー
  import b; // インポートディレクティブ、プリプロセス後にコンパイルエラー
            // インポート宣言は他のあらゆる宣言の内部に来てはならないため
}

参考文献

この記事のMarkdownソース