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

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

C++標準化委員会の論文(提案文書)公開がコロナウィルスの影響もあって月1になり量がお手頃になったので、4/20公開の提案文書をさらっと見てみます。

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

提案文書で採択されたものは今回はありません。

N4858 : Disposition of Comments: SC22 5415, ISO/IEC CD 14882

C++20 CD (committee draft)の投票時に各国委員会およびそのメンバーから寄せられたコメントとその対応および理由の総覧です。

N4859/N4860/N4861

N4860/N4861のN4849との差分を記したEditors' Report。新たに採択された提案文書の一覧、解決されたIssueの一覧、Github上での軽微な修正コミットの一覧、などが載っています。

C++20のDIS (draft international standard)。この後FDIS (final draft international standard)を経てIS (international standard)へと至ります。

残念ながら委員会のメンバーしか見られないようです・・・

C++23のWD (working draft)第一弾。でもC++23向けに導入されたものはないはず。

N4860との差異は、表紙とヘッダ、フッダ、C++17規格とのクロスリファレンスの有無(無い)だけのようで、内容としてはDIS(N4860)と同一とのこと。C++17(N4659)も最終的に公開されているのはDIS相当のWDなので、これがC++20規格として参照されることになりそうです。

P0533R6 : constexpr for <cmath> and <cstdlib>

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

<cmath>からは、logb()modf()scalbn()abs()/fabs()ceil(),floor()等丸め系関数、fmod()copysign(), nextafter()fmax()/fmin()/fdim()fma()fpclassify(),isunordered()等数値分類・数値比較系関数、等が対象です。
<cstdlib>はなぜかそっちに含まれている数学関数(abs()とかdiv())だけが対象です。

筆者は、<cmath>の関数群は全てconstexpr指定できるはずだけど、コンパイラ/標準ライブラリベンダーの過度な負担とならない一部だけをconstexpr対応させる、と述べています。その一部には、sin(),cos()等の数学関数は含まれません・・・

また、これらの関数はerrnoや丸めモードなどグローバルフラグに依存し、またそれらを更新します。errnoをセットすべき時(定義域エラーやゼロ割)は単にコンパイルエラーを発生させ、グローバルフラグの状態はコンパイル時には変更しない、と言うようにすれば良いのですがC++17以前ではそれは少し難しい実装を要求していました。
しかし、C++20にてstd::is_constant_evaluated()が導入されたことでこの問題は解決されるため、単純な実装によって多くの<cmath>関数を追加でconstexpr対応させられるようになりました。

丸めモードに関しては色々議論があるようで、この提案では丸めモードへの依存が強い(変更することで精度が1%以上変化しうる)関数を除外しています。

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

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

これは例えば、std::optionalstd::variantのようなラッパー型において、縮小変換が起こる場合に変換や構築を禁止する制約をかけるのに利用できます。

意図しない縮小変換の発生は実行時において浮動小数点数の精度低下などの発見しづらいバグにつながります。縮小変換(発生の可能性)をコンパイル時に検出し禁止しておくことで、C++の型システムをユーザーの手によって多少ロバストにして運用することができます。

提案されている宣言。

namespace std {
  template <class From, class To>
  struct is_narrowing_convertible;

  template <class From, class To>
  inline constexpr bool is_narrowing_convertible_v = is_narrowing_convertible<From, To>::value;
}

これは例えば、次のように実装できます(提案文書より)。

// そもそも変換不可能な型のペアのためのプライマリテンプレート
template<class From, class To>
inline constexpr bool is_narrowing_convertible_v = false;

// 縮小変換を検出する
// To t[] = { std::declval<From>() };のような式がエラーとなるかによって縮小変換が起こるかを調べている
template<class T, class U>
concept construct_without_narrowing = requires (U&& x) {
  { std::type_identity_t<T[]>{std::forward<U>(x)} } -> std::same_as<T[1]>;
};

// 変換可能な型のペアはこちらを利用
template<class From, class To> requires std::is_convertible_v<From, To>
inline constexpr bool is_narrowing_convertible_v<From, To> =
  !construct_without_narrowing<To, From>;

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

P1255R6 : A view of 0 or 1 elements: views::maybe

std::optionalやポインタ等のmaybeモナドな対象を、その状態によって要素数0か1のシーケンスに変換するRangeアダプタviews::maybeの提案。

例えば、std::optionalのシーケンスを無効値を持つかによってフィルタする処理をわざわざ書く必要がなくなったり、std::optionalの状態をチェックして中身を取り出して・・・といったお決まりのコードを隠蔽することができます。

{
  auto&& opt = possible_value();  // optionalを返す関数
  if (opt) {
      
      // 数十行の処理が挟まっていたとすると・・・

      use(*opt); // ここでのデリファレンスは有効かがすぐに分からなくなりがち
  }
}

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

for (auto&& opt : views::maybe(possible_value())) {
  
  // 数十行の処理が挟まっていたとしても・・・

  use(opt); // すでにデリファレンスされており、有効値が得られている
}

views::maybeを通した場合、possible_value()が無効値を返した場合はループが実行されません(シーケンスが空なので)。

std::vector<int> v{2, 3, 4, 5, 6, 7, 8, 9, 1};

auto test = [](int i) -> std::optional<int> {
  switch (i) {
    case 1:
    case 3:
    case 7:
    case 9:
      return i;
    default:
      return {};
  }
};


auto&& r = v | ranges::views::transform(test)
             | ranges::views::filter([](auto x){return bool(x);})
             | ranges::views::transform([](auto x){return *x;})
             | ranges::views::transform(
                [](int i) {
                  std::cout << i;
                  return i;
                }
               );

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

auto&& r = v | ranges::views::transform(test)
             | ranges::views::transform(views::maybe) //0か1要素のシーケンスのシーケンスになる
             | ranges::views::join                    //シーケンスのシーケンスを1つのシーケンスに平滑化する
             | ranges::views::transform(
                [](int i) {
                  std::cout << i;
                  return i;
                }
               );

P1315R5 : secure_clear

特定のメモリ領域の値を確実に消去するための関数secure_clear()の提案。

パスワード等のセキュアなデータを扱う場合、不用になったらすぐにその内容を消し去り、コアダンプ等によってキャプチャ可能な時間を少しでも短くする必要があります。このことは、近年の脆弱性(MeltdownやSpectre等)の影響によって重要度が増しています。

しかし、単純にメモリ領域をクリアするだけの処理はその領域がその後使用されない事からコンパイラの最適化によって削除される可能性があります。

void f()
{
  constexpr std::size_t size = 100;
  char password[size];

  // セキュアなデータの取得
  getPasswordFromUser(password, size);

  // 取得したデータの仕様
  usePassword(password, size);

  // 取得したデータの削除
  std::memset(password, 0, size);
}

この様な問題(すなわちコンパイラ最適化)を回避するのにはいくつもの方法がありますが、それらの方法は非自明であったり、移植性が無く容易に利用できるものではなかったりします。

そのような機能を標準によって提供しポータブルかつ容易に利用できるようにするために、secure_clear()関数を提案しています。

namespace std {
  template <class T>
    requires is_trivially_copyable_v<T>
        and (not is_pointer_v<T>)
  void secure_clear(T & object) noexcept;
}

効果は上に示した通り、受け取った参照先のオブジェクトの占めるメモリ領域をゼロクリアします。

なお、この提案は同時にC標準に対しても行われているようです(N2505)。

void secure_clear(void * data, size_t size);

こちらはポインタとゼロクリアする領域サイズを取ります。

C++からはstd::secure_clear()としてこの2つのオーバーロードが利用可能になります(採択されれば)。

P1641R3 : Freestanding Library: Rewording the Status Quo

フリースタンディング処理系に要求されるライブラリ機能についての文言を変更する提案。

現在はライブラリヘッダ毎にフリースタンディングで要求されるかを規定していますが、ヘッダの一部分の機能および対応する機能テストマクロを個別にフリースタンディング指定することができるように文言を追加・変更しようというもの。主に<cstdlib>の文言を改善するのが目的っぽい?

P1654R1 : ABI breakage - summary of initial comments

ABIの破損問題について、委員会メンバからのコメントをまとめた報告書。

C++標準がABI破損を伴う変更を受け入れるのか、どのように受け入れるのかについて、次の4つのケースが考えられます。

  1. ABIを絶対に壊さない
    • 実行パフォーマンスが低下するが、もっとも安定している。何も懸念がなければこれを選択すべき
  2. ケースバイケース(例 : std::stringのSSO)
    • 以前に行ったことがあるが、ユーザーはそれが行われることを予測できない
  3. 特定のリリースを境界としてABI破壊を許可する(例えば12年毎など)
    • 試したことはない、適切な期間とは何か?
  4. 任意のタイミングで自由にABIを破壊する
    • 一番素早く動けて実行パフォーマンスを高められるが、安定性がもっとも低い

どれを選ぶのかを慎重に検討するために委員会メンバからのコメントを募集し、過去に行われたABI破壊や、ABI破壊を理由に採択されなかった提案について、また将来必要になるかもしれないABI破壊などについてまとめられています。

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

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

現在C++では識別子に使用可能なUnicode文字列をコードポイントの範囲として規定していますが、その中にはゼロ幅文字など人間の目で見て区別できない文字が含まれてしまっており、万が一使用されればバグの元となりえます。そのため、それらの使用を禁止しそのような文字列が使用されていた場合はコンパイルエラーにすることを提案しています。

Unicode Standard Annex 31というのはどうやら、プログラミング言語において汎用的に識別子として使用可能な文字列および文字列の形式を定めたものです。C++11時点ではこれは安定しておらず使用されませんでしたが、現在は安定しており後々のUnicodeの規格で破壊的な変更が行われないことが保証されるようになっているようです。

そのため、それを参照して識別子の構文を規定することで識別子として適切な文字だけが使用できるように標準を変更します。

Unicode Standard Annex 31で規定されている識別子の構文規則(EBNF)は次のようになります。

<Identifier> := <Start> <Continue>* (<Medial> <Continue>+)*

ここで、<Start>XID_Startという特定の文字(コードポイント)の集合、<Continue>XID_Continueという特定の文字の集合、<Medial><Continue>の文字の間に現われることができる文字の集合です。

C++では、<Start>_(U+005F、アンダーバー)を追加し<Medial>は空になります(<Continue>はそのまま)。上記の文法に照らせば、次のようになります。

<Identifier> := <Start> <Continue>*
<Start> := XID_Start + U+005F
<Continue> := <Start> + XID_Continue

XID_Startにどんな文字が含まれているのか及びXID_Continueにどんな文字が含まれているのかは正直良く分からないくらい大量の文字がありますが、多分制御文字やゼロ幅文字はないはずで、絵文字も含まれていないようです。

また、採択されたとしたら、これらのことは欠陥報告としてC++20以前のバージョンに遡って適用されることになりそうです。

P2011R1 : A pipeline-rewrite operator

x |> f(y);f(x, y);と評価する新しい演算子|>の提案。

この演算子オーバーロード可能ではなく、右辺の値を左辺の関数呼び出しの第一引数に渡すように式全体を書き換えるだけです。

Unified Function Call Syntax(UFCS)に近いものに見えますが、この演算子による書き換えは常に非メンバ関数を呼び出します。

x->f(y);    // メンバ関数f()を呼び出す  
x.f(y);     // メンバ関数f()を呼び出す
x |> f(y);  // 非メンバ関数f()を呼び出す

一見すると何の意味があるのか分からない演算子ですが、Rangeのパイプライン演算子|にまつわる以下の様な諸問題を解決するためのものです。

Rangeのパイプライン演算子がやっていることは要するに、左辺の式の結果オブジェクトを右辺の関数の第一引数に渡すようなもので、それによって関数呼び出しのネストを分解しています。

#include <iostream>
#include <vector>
#include <ranges>

int main()
{
  std::vector v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  // 偶数を取り出して、2倍して、逆順にする
  // 適用順と逆順になるうえ、やることが多くなるとネストしまくり可読性がしぬ
  auto&& range = std::views::reverse(
                   std::views::transform(
                     std::views::filter(v, [](auto n){ return n % 2 == 0;}),
                     [](auto n) { return n * 2;}
                   )
                 );
  
  for (auto e : range)
  {
    std::cout << e << std::endl;
  }
}

//↑これを↓のように書ける

int main()
{
  std::vector v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  // パイプラインスタイル
  // 適用順と同じ順番で縦に並べられるので見やすい!
  for (auto e : v | std::views::filter([](auto n){ return n % 2 == 0;})
                  | std::views::transform([](auto n) { return n * 2;})
                  | std::views::reverse)
  {
    std::cout << e << std::endl;
  }
}

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

ここで、2つのスタイルの例に現れているfiltertransform等の関数はそれぞれ異なるオーバーロードが使用されています(例えば、filter(Rng&&, Pred&&)filter(Pred&&))。|演算子はあくまで演算子オーバーロードでありその呼び出しよりも引数に与えられた式の評価が先になるので、パイプラインスタイルの時に|演算子の右辺に来る関数(filter(Pred&&))は渡された関数オブジェクトを|に引き渡す為のラッパを生成するだけの処理になります。一方、第一引数にrangeオブジェクトが直接渡っている最初の例(filter(Rng&&, Pred&&))では受け取った処理の適用準備の済んだrange viewオブジェクトを返します。

どちらが読みやすいかを考えるとパイプラインスタイルの威力は圧倒的ですが、この裏側では大量の黒魔術が発動しています・・・

このような|の行なっていることを式の書き換えによって行う|>演算子を言語サポートすることで、パイプライン演算子を使用するためのそのような黒魔術コードを削減することができ、それに伴う諸問題の解決を図ることができます。

|>演算子でも|と同様に書けます。

#include <iostream>
#include <vector>
#include <ranges>

int main()
{
  std::vector v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  for (auto e : v |> std::views::filter([](auto n){ return n % 2 == 0;})
                  |> std::views::transform([](auto n) { return n * 2;})
                  |> std::views::reverse)
  {
    std::cout << e << std::endl;
  }
}

|>演算子の場合はこれを一番最初の関数呼び出しネストコードに書き換えることによって|演算子と同じことを達成します。これによって演算子オーバーロードもそれに対応するためにfilter等に不要なオーバーロードを追加する必要もなくなります。

また、|>の両辺は書き換え前に評価されません。つまり、他の演算子とは少し振る舞いが異なります。オーバーロード不可能とされているのはこの性質によります。

P2013R1 : Freestanding Language: Optional ::operator new

フリースタンディング処理系においては、オーバーロード可能なグローバル::operator newを必須ではなくオプションにしようという提案。

フリーストア(ヒープ)を持たないかその使用が著しいオーバーヘッドとなる環境では、意図しない::operator newが使用された場合にコンパイルエラー(リンクエラー)となってほしい場合があります。また、OSのカーネルの動作環境のように、メモリ割り当てを正しく行う方法が無い環境でも同様です。このような環境ではすでに::operator newの実装を提供できておらず、結果としてそれらの環境のC++ユーザーは::operator newを使わないか、(使うために)独自実装をするかの2択を迫られているのが現状です。そのため、あえて::operator newを定義しないという選択肢を標準化し、その場合の振る舞いを規定する必要がある、というのが要旨です。

提案では、オーバーロード可能なグローバル::operator newの提供を実装定義とし、提供するならば全てのオーバーロードを提供する必要があるが、提供しない場合は全てのオーバーロードを提供しない、という規定を追加します。結果として、::operator newが提供されない場合、プログラム中でのそれらの使用はill-formedであり、おそらくリンカエラーを引き起こします。
ちなみにそのような場合には、<coroutine>ヘッダはグローバル::operator newに依存しているので存在そのものがill-formedになります(#include or importしなければok)。

なお、::operator deleteは仮想デストラクタにおいて参照されるのでそのままであり、constexpr newは使用可能となるように文言が調整されています。

記載されているEWG等での投票結果を見るに受け入れられそうな雰囲気です。

P2034R1 : Partially Mutable Lambda Captures

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

パフォーマンスが求められるコールバック関数オブジェクトではその内部に個別に利用するローカルメモリを持つことがあります。また、mutex等の参照をメンバに持つこともあるでしょう。それらは外部から観測不可能な内部状態であり、そのオブジェクトは意味論的にはimmutableです。

struct MyRealtimeHandler {
private:
  const Callback callback_;
  const State state_;
  mutable Buffer accumulator_;

public:
  void operator()(Timestamp t) const {
    callback_(state_, accumulator_, t);
  }
}; 

struct MyThreadedAnalyzer {
private:
  const State& state_;
  std::mutex& mtx_;

public:
  void operator()(Slice slice) const {
    std::lock_guard<std::mutex> lock{mtx_};
    analyze(state_, slice);
  }
};

例えばこの様な典型的な型はラムダ式を使えば定義を必要とせずに簡単に書くことができますが、現在はこの様に部分的にmutable/非constなメンバを持つようなラムダ式を書くことが出来ません。全部constが全部mutableかの二者択一です。

提案では、次のようにキャプチャを個別にmutable指定できるようにします。

auto a = [mutable x, y]() {}; 

// ↑は↓と等価

struct A {
  mutable X x;  // Xが参照型(xが参照キャプチャ)なら単に非const参照になる
  const Y y;

  void operator()() const {}
} a; 

この様に、その物理的な状態を変更したとしてもそのオブジェクトの論理的(意味論的)な状態を変更しないような不変オブジェクト(に対する操作)の事をlogical constと呼びます。

さらに、std::any_invocableというCV修飾やnoexceptを指定できるstd::functionが議論されており、それを踏まえるとlogical constラムダ式はより必要とされます。

また、この提案では更なる議論を前提としていて、他にも以下の様な書き方を可能にすることが提案されています。

// constキャプチャとmutable呼び出し
auto b = [x, const y]() mutable {}; 

// 参照のconstキャプチャ
auto b = [&x, const &y]() {};

// const呼び出し(コンパイルエラーにならないようにする)
auto c = [x]() const {}; 

// constキャプチャとconst呼び出し
auto c = [const x]() const {};

// mutableキャプチャとmutable呼び出し
auto c = [mutable x]() mutable {}; 

P2044R2 : Member Templates for Local Classes

ローカルクラスでメンバテンプレートを使用できるようにする提案。

ローカルクラスとは関数の中で定義されたクラスで、関数テンプレートと利用するとインターフェースの自動実装を行えたりとなかなか便利なやつです。ローカルクラスのスコープはその関数の中に閉じられ、関数外部からクラス名を参照することはできないため名前が衝突したりせず、インターフェースクラスを継承して自動実装する場合はアップキャストされるのを完全に防止できます。なお、ローカルクラスのメンバは普通に外から参照できます。

しかし、ローカルクラスでは囲む関数テンプレートのテンプレートパラメータなど囲む関数からアクセスできるものは全てアクセスできますが、メンバテンプレートを持つことができないなどいくつかの制限があります(ローカルクラス自体がテンプレートになることはできます)。

ローカルクラスがメンバテンプレートを持つことができるようになると、あるインターフェースを別のインターフェースへ変換するアダプタの自動生成などをローカルクラスで書くことができるようになります。

// operator()をcall()に変換するアダプタを生成する
template<typename Callable>
auto callable_to_call(Callable&& f) {
  // ローカルクラス
  struct call_impl {
    Callable m_f;

    // 今はこれができない・・・
    template<typename... Args>
    auto call(Args&&... args) {
      return m_f(std::forward<Args>(args)...);
    }
  };

  return call_impl{std::forward<Callable>(f)};
}

実装クラスを関数外部に持っておいてもいいのですが、クラス名が漏洩しないというのと、見た目的にも気持ち的にもコードがコンパクトになるのが個人的お好みポイントです。

MSVCの実装者はこの変更は問題ないと言っているそうですが、Clangは熱心にテンプレートの実体化を行う結果思わぬコンパイルエラーが起こる可能性があるとのことです。しかし、すでにジェネリックラムダが限定的とはいえ同じことが可能になっているのであまり壁は高くなさそうです。

P2096R1 : Generalized wording for partial specializations

変数テンプレートの部分特殊化を明確に規定するように文言を変更する提案。

現在の書き方だと変数テンプレートの部分特殊化についてが不透明なので、クラステンプレートの部分特殊化に関する文言を一般化して変数テンプレートの部分特殊化を規定するように文言を調整しています。これが通ったとしても多くのユーザーにとっては関係ない話です。パッと見では、クラステンプレートの部分特殊化やプライマリクラステンプレート、などと書かれていたところからクラステンプレートというワードが消されています。仮に採択された場合はこの辺を読むときは注意しないと分かりづらいかもしれません。

P2098R1 : Proposing std::is_specialization_of

std::complex<T>std::complex<double>のように、ある型Tが別の型Pの特殊化となっているかを調べるメタ関数is_specialization_of<T, P>の提案。

例えばテンプレートの文脈で、std::complex<T>std::optional<T>std::vector<T>など、その要素型はともかくとして型がその特殊化であるかを知りたい!という場合はよくあります。幸いこれを判定するメタ関数を書くのは難しくないのでテンプレート好きな人は多分1度は書いたことがあるでしょう。そのように、よく利用されるものであるので標準に追加しようという提案です。ただし、あるテンプレート毎に個別にそのようなメタ関数を追加するわけにはいかないので、より一般化した任意の型のペアの間でそれを判定するものを追加します。

template<class T, template<class...> Primary>
struct is_specialization_of;

template<class T, template<class...> Primary>
inline constexpr bool is_specialization_of_v = is_specialization_of<T,Primary>::value;

例えば次の用に使います。

// そのまま使う
static_assert(std::is_specialization_of_v<T, std::optional>);

// 特定のテンプレート用に特殊化
template< class T >
inline constexpr bool is_complex_v = is_specialization_of_v<T, std::complex>;

ただ、std::array<T, N>のように非型テンプレートパラメータを取るものは判定できません。それは諦めているようです。また、これはクラスの継承関係を判定するものではありません。

P2138R1 : Rules of Design<=>Wording engagement

(タイトルの<=>は宇宙船演算子ではありません)
CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。

C++標準会員会の作業プロセスの改善に関するお話なので、完全にユーザーには関係ありません。CWGとかEWGとかは次の図参照。

WG21 組織図

コア言語の提案はEWG(Evolution Working Group)で基礎設計が詰められてからCWG(Core Working Group)へ送られ、CWGでは標準としての文言の確認と調整を行います。その際、EWGである程度設計に基づく文言が整っていることが要求されますが、設計を文言が表現しきれていなかったり、議論していない文言が含まれていたり、とそうなっていない事があったようです。
そのため、EWGとCWGの間ではそう言う事が無いようにするためのルールが設けられていました。とはいえ、そのルールは文書化されたものではなかったためか、CWGに送られた段階でしっかりと文言が整っていない事がまだあるようです。

EWGが設計とそれを表現する文言を決定しCWGは文言を確認するだけ、という役割分担を明確にしCWGの時間を無駄にしないようにするためにルールを変更し、同じことをLWG(Libraly Working Group)とLEWG(Libraly Evolution Working Group)の間でも行うようにする。というのが提案の要旨です。

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

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

C++20現在、バイナリファイルのIOをやろうとするとiostreamを使用することになりますが、iostreamもベースにあるCのIO関数もテキストストリームへの入出力前提なものをバイナリモードという特殊な状態にしたうえでバイナリIOに使用することになるので、使いづらく、また非効率です。

#include <fstream>

int my_value = 42;
{
  std::ofstream stream{"test.bin", std::ios_base::out |    std::ios_base::binary};
  stream.write(reinterpret_cast<const char*>(&my_value), sizeof(my_value));
}

int read_value;
{
  std::ifstream stream{"test.bin", std::ios_base::in |  std::ios_base::binary};
  stream.read(reinterpret_cast<char*>(&read_value), sizeof(read_value));
}

assert(read_value == my_value)

これには以下の欠点があります。

  • std::byte非対応のため、reinterpret_cast<const char*>が必要
  • バイト数を明示的に指定しなければならない
  • バイトの読み書きにエンディアンを考慮してくれない(するようにできない)
  • std::char_traitsが使われるがバイナリIOには不要、std::ios::pos_typeは多くのIO操作に必要だが使いづらい。
  • バイナリIOに必要なのは常にstd::ios_base::binary、オープンモード指定は不用
  • ストリームオブジェクトはテキスト形式フラグをいくつも持っているが、バイナリIOには不要。メモリの無駄
  • デフォルトのストリームは例外を投げない。これはストリーム状態を調べて例外を発生させるラッパーコードを追加する手間の元
  • メモリ内で完結するIOのためにstd::stringを使用するstd::stringstreamが用意されているが、無駄なコピーが発生するなど使いづらい。バイナリデータはほとんどの場合std::vector<std::byte>が適当であり、spanで参照すれば十分
  • 現行のiostreamには、バイナリIOとシリアライズのためのカスタマイゼーションポイントが無い

これらの欠点をすべて解決したバイナリIOのための新ライブラリの導入を目指すのがこの提案です。

生バイト列のIOサンプル

#include <io>
#include <iostream>

int main() {
  
  // 書き込むバイト列
  std::array<std::byte, 4> initial_bytes{
    std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}
  };

  {
    // 書き込み用にファイルオープン
    std::io::output_file_stream stream{"test.bin"};
    // 書き込み
    std::io::write_raw(initial_bytes, stream); 
  } // RAIIによってストリームが閉じられる

  // 読み込み用バイト列
  std::array<std::byte, 4> read_bytes;
  
  {
    // 読み込みのためにファイルオープン
    std::io::input_file_stream stream{"test.bin"};
    // 読み込み
    std::io::read_raw(read_bytes, stream); 
  } // RAIIによってストリームが閉じられる

  // 読み込んだバイト列の比較
  if (read_bytes == initial_bytes) { 
    std::cout << "Bytes match.\n"; 
  } else { 
    std::cout << "Bytes don't match.\n"; 
  }
}

カスタマイゼーションポイントによる任意クラスのカスタムシリアライズエンディアン指定のサンプル。

#include <io>
#include <iostream>

struct MyType {
  int a; 
  float b;

  void read(std::io::input_stream auto& stream) {
    // ビッグエンディアンでメンバ変数にストリームから値を読み出す 
    std::io::default_context context{stream, std::endian::big};
    std::io::read(a, context);
    std::io::read(b, context);
  }

  void write(std::io::output_stream auto& stream) const {
    // ビッグエンディアンでメンバ変数の値をストリームに書き出す
    std::io::default_context context{stream, std::endian::big}; 
    std::io::write(a, context);
    std::io::write(b, context);
  }
};

int main() {
  MyType my_object{1, 2.0f};
  std::io::output_memory_stream stream;

  // std::io::writeはカスタマイゼーションポイントオブジェクト
  // メンバ関数か同じ名前空間の非メンバ関数のwrite()を探して呼び出す
  // 対になるstd::io::readも同様
  std::io::write(my_object, stream);

  // ストリームのバッファを取得し、内容をバイト列として書き出す
  const auto& buffer = stream.get_buffer();
  for (auto byte : buffer) {
    std::cout << std::to_integer<int>(byte) << ' ';
  }
  std::cout << '\n'
}

他にも、spanやメモリのためのI/Oストリームが用意されていたり(これらはconstexpr対応!)、エンディアンを途中で切り替え可能だったり、整数型の特殊なフォーマット(LEB128など)をサポート可能だったり、ISO 60559以外もサポート可能な浮動小数点数バイナリフォーマット変換も考慮されていたり(ドロップされそうですが)、コンセプトベースだったりとイケてる雰囲気のライブラリです。

筆者の方が並行してリファレンス実装を作っています。なかなか本気のようです。

P2149R0 : Remove system_executor

Networking TSからsystem_executor​system_contextを削除する提案。

system_context::get_executor()はデフォルト構築したsystem_executor​を返して、そのメンバ関数であるsystem_executor::context()は静的記憶域期間に配置された(つまりグローバル変数の)system_context​オブジェクトへの参照を返します(これは必ずしもMeyer’s singletonではないかもしれない、つまり本物のグローバル変数かもしれない)。

しかも、そのようなグローバルなsystem_context​オブジェクトはmutableです。

グローバルなオブジェクトであるがゆえに、それを利用するユーザーのコンポーネントのRAIIとは無縁の所で動いています。プログラム、あるいはコンポーネントの終了時にそのグローバルsystem_context​に何かしなければいけないかどうかは、system_context​とやり取りをしたコンポーネントが自分も含めて存在しているかによって決まります。また、この様なグローバルなオブジェクトにはその構築と破棄の順序の不定性など様々な問題があります。

Networking TSの仕様ではsystem_contextを直接使用するのはsystem_executorだけで、system_executorassociated_executor(_t)の仕様においてフォールバックExecutorとして使用されています。

従って、グローバルな状態に依存しないような代わりのexecutorを用意して、現在のsystem_executor​system_contextを削除しよう、という事のようです(良く分かりません・・・)
本質的には、グローバル変数として複雑な状態を持ってしまっていることが問題のようです。

P2150R0 : Down with typename in the library!

標準ライブラリのパートから不用なtypenameを消し去る提案。

C++20からいくつかの場所でtypenameが不用になったのに伴って(P0634R3 : Down with typename!)、標準ライブラリの規定部分からも取り除こうという話です。

どこで不要になるかはこのページを参照。

P2155R0 : Policy property for describing adjacency

進行中のExecutor(簡単に言えばスレッドプールサポートライブラリ)に関するもので、NUMAのようなアーキテクチャ向けに、スレッドとそこで使用するメモリを同じノード内で確保しバインドするように指示するポリシーを追加する提案。

NUMAでは1つのプロセッサとそこに接続されたローカルメモリを1ノードとして、複数のノードで構成されることになりますが、そのシステム上での論理スレッド(OS上プロセスのスレッド)はOSによって任意のノードの物理スレッド(CPUコア)に割り当てられる可能性があり、また、そのスレッド内で確保し使用しているメモリはそのスレッドを実行している物理スレッドの属するノードとは別のノードに属するメモリを使用している可能性があります。

OSのスケジューリングによってこれはほとんど予測不可能となりますが、ノードを超えたスレッドスケジュールやメモリアクセスは当然ノード内で行われるよりも高コストになり、全体のパフォーマンスに影響を与えます。この様な実行スレッドに対する割り当てメモリの位置の事をメモリアフィニティ(memory afinity)、あるいは単にアフィニティと呼びます。

このようなことが起こりえる場合にもパフォーマンスを向上させるための1つの方法は、ある論理スレッドを物理スレッドとそのローカルメモリにバインドしスケジューリングやメモリ割り当てをあるノード内で完結するように強制してしまう事です。

NUMAの様なシステムにおいてC++開発者が現在および将来のアーキテクチャに渡って最高のパフォーマンスを得るためには、この様なスレッドとメモリの配置の制御をC++標準機能としてネイティブサポートする必要がある、というのが提案の要旨です。

次のようなadjacencyプロパティグループを定義しておき、これを実行ポリシーに与えることで、Excecutor実装に対してアフィニティ制御に関するヒントを提供できるようにします。

namespace std {
namespace experimental {
namespace execution {

  struct adjacency_t {
    struct no_implication_t;
    struct constructive_t;
    struct destructive_t;
  
    // デフォルト、普通にアフィニティ制御をしてほしい
    constexpr no_implication_t no_implication;

    // 以下二つは、隣接するワークアイテム(スレッド?)を離した上でアフィニティ制御を行うかを指定する
    // キャッシュラインの配置までコントロールするか否か?

    // 実行する処理はconstructive interferenceの恩恵を受けうる
    // すなわち、参照局所性が重要
    constexpr constructive_t constructive;

    // 実行する処理はdestructive interferenceの恩恵を受けうる
    // すなわち、false sharingが問題になる
    constexpr destructive_t destructive;

  };

  constexpr adjacency_t adjacency;

} // execution
} // experimental
} // std

このように、アフィニティ制御をどのように行うかを指定するポリシーを渡すことで実装へのヒントとし、実装の抽象化度を維持し移植性を持たせたまま必要なら高パフォーマンスな実装を選択できるようになります。

提案文書よりサンプルコード(Executor分からないから読めない

// bulk algorithmの各インデックスについて、そこで使用されるメモリ領域用のポインタ列
std::vector<std::unique_ptr<float>> data{}; data.reserve(SIZE); 
 
// NUMA対応Executorの作成
numa_executor numaExec; 
 
// bulk algorithmの各実行に与えるインデックス
auto indexRng = ranges::iota_view{SIZE}; 
 
// std::par実行ポリシーに加えてadjacency.constructiveプロパティを要求する新しい実行ポリシーを作成
// 実装に対して、実行する処理はconstructive interferenceの恩恵を受けうることをヒントとして与える
// adjacencyプロパティはここで指定する
auto adjacencyPar = std::execution::require(std::par, adjacency.constructive); 
 
// bulk algorithmの各実行毎に初期化を行うCallableオブジェクト
auto initialize = [=](size_t idx, std::vector<unique_ptr<float>> &value) {
  value[idx] = std::make_new<float>(0.0f);
}; 

// 実行する処理内容
auto compute = [=](size_t idx, std::vector<unique_ptr<float>> &value) {
  do_something(value[idx]);
};
 
// 入力となるdataを受けて、NUMA対応Executorを使用してスケジューリングし、
// indexed_forによって、まず初期化を行いその後で計算を行うsenderを作成
// 実行ポリシーはここで指定する
auto sender = std::execution::just(data)
            | std::execution::via(numaExec)
            | std::execution::indexed_for(indexRng, adjacencyPar, initialize)
            | std::execution::indexed_for(indexRng, adjacencyPar, compute); 
 
// senderをExecutorへ送り、結果を待機
std::execution::sync_wait(sender, std::execution::sink_receiver{}); 

P2156R0 : Allow Duplicate Attributes

属性指定時に同じ属性を重複して指定しても良いようにする提案。

現在の規定では、一つの属性指定[[]]の中で同じ属性が複数回現れることは出来ません。しかし、属性指定を複数に分割すれば同じ属性が何回重複してもokです。

// ng
[[noreturn, carries_dependency, deprecated, noreturn]]
void f();

// ok
[[noreturn]] [[carries_dependency]] [[deprecated]] [[noreturn]]
void g();

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

この挙動は一貫していないので、属性指定の重複を認める(上記NGの例f()を適格にする)方向に変更すべし、という提案です。

EWGの見解としては、属性指定を分ければ重複可能なのはマクロによって属性を条件付きで追加していくことをサポートするためのもので、一つの属性指定のなかでそれを行う事はレアケースなのでこの制限を解除する必要はない、という事。

しかし、これをそのままにしておくと、重複不可能な属性を標準に追加するたびにその旨を一々記述しておく必要があり、逆に重複可能な属性に対しては重複した時の振る舞いを記述しておく必要が生じます。これは明らかに標準を太らせ望ましくないので重複可能をデフォルトにするべき、というのが筆者の主張です。また、これは欠陥として過去のバージョンにさかのぼって適用されるのが望ましいとも述べています。

onihusube.hatenablog.com

この記事のMarkdownソース

[C++]to_chars()とfrom_chars()ってはやいの??

<charconv>ヘッダはC++17から導入されたヘッダで、ロケール非依存、動的確保なし、例外なげない、などを謳ういいことづくめで高速な文字列⇄数値変換を謳う関数が提供されています。現在フルで実装しているのはMSVCだけですが、実際速いってどのくらいなの?既存の手段と比べてどうなの??という辺りが気になったので調べてみた次第です。

計測環境

  • Core i7 7700T HT有効OCなしTBあり 16GBメモリ
  • Windows 10 1909 18363.778
  • VisualStudio 2019 update 6 preview 3

一応電源プランを高パフォーマンスにして、VS以外を終了させた状態で計測。ビルドは/std:c++latestを追加したリリースモードで行っています。

std::to_chars()

std::to_chars()は数値を文字列へ変換する関数です。出力はcharの文字列限定で、戻り値を調べることでエラーの有無と文字列長が分かります。

これとの比較対象は以下のものです。これらは同じく数値→文字列への変換を行います。

  • std::to_string()
  • std::stringstream
  • snprintf()

測定方法

100万件のランダムな数値を用意してそれを1つづつ全件変換にかけ、それにかかる時間を計測します。それを10回繰り返して各種統計量で見てみることにします。整数型(64bit整数型)と浮動小数点型(double)それぞれで実験を行います。

コードは以下のようになります。

#include <iostream>
#include <charconv>
#include <vector>
#include <random>
#include <chrono>
#include <type_traits>
#include <cassert>
#include <thread>
#include <numeric>
#include <string>
#include <sstream>

template<typename NumericType>
auto make_data(unsigned int N) -> std::vector<NumericType> {
  
  auto rng = []() {
    if constexpr (std::is_integral_v<NumericType>) {
      return std::uniform_int_distribution<NumericType>{};
    } else if constexpr (std::is_floating_point_v<NumericType>) {
      return std::uniform_real_distribution<NumericType>{};
    } else {
      static_assert([] { return false; }, "You have to specify a number type, right?");
    }
  }();
  
  std::mt19937_64 urbg{ std::random_device{}() };
  
  std::vector<NumericType> vec;
  vec.reserve(N);

  for (auto i = 0u; i < N; ++i) {
    vec.emplace_back(rng(urbg));
  }

  return vec;
}

template<typename Container>
void report(Container&& container) {
  const auto first = std::begin(container);
  const auto last = std::end(container);
  const auto N = std::size(container);

  using value_type = typename std::iterator_traits<std::remove_const_t<decltype(first)>>::value_type;

  std::sort(first, last);

  const auto max = *(last - 1);
  const auto min = *first;

  std::cout << "min : " << min.count() << " [ms]" << std::endl;
  std::cout << "max : " << max.count() << " [ms]" << std::endl;

  const auto medpos = first + (N / 2);
  std::cout << "median : " << (*medpos).count() << " [ms]" << std::endl;
  
  const auto sum = std::accumulate(first, last, value_type{});
  const auto ave = sum.count() / double(N);
  std::cout << "average : " << ave << " [ms]" << std::endl;

  const auto var = std::inner_product(first, last, first, 0ll, std::plus<>{},
    [](auto& lhs, auto& rhs) {return lhs.count() * rhs.count(); }) / N - (ave * ave);
  std::cout << "stddev : " << std::sqrt(var) << "\n" << std::endl;
}

template<typename NumericType, typename F>
void profiling(const char* target, F&& func) {
  using namespace std::chrono_literals;

  constexpr int trialN = 10;
  constexpr int sampleN = 1'000'000;

  std::chrono::milliseconds results[trialN]{};

  for (int i = 0; i < trialN; ++i) {
    //データの準備
    auto input = make_data<NumericType>(sampleN);

    //計測開始
    auto start = std::chrono::steady_clock::now();

    for (auto v : input) {
      func(v);
    }

    //計測終了
    auto end = std::chrono::steady_clock::now();
    results[i] = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::this_thread::sleep_for(200ms);
  }

  std::cout << target << std::endl;
  report(results);
}

int main()
{
  char buf[21];
  auto first = std::begin(buf);
  auto last = std::end(buf);

  profiling<std::int64_t>("to_chars() int64", [first, last](auto v) {
    auto [ptr, ec] = std::to_chars(first, last, v);
    if (ec != std::errc{}) throw new std::exception{};
  });
}

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

全部入りのコードはこちら

整数(std::int64_t

単位は全てms

方法 最小値 最大値 中央値 平均値 標準偏差
std::to_chars() 27 36 28 29.2 2.71293
std::to_string() 79 98 81 82.4 5.31413
std::stringstream 525 602 533 543.9 22.8427
snprintf() 236 246 240 240 2.44949

浮動小数点数double

方法 最小値 最大値 中央値 平均値 標準偏差
std::to_chars() 45 51 46 47.3 1.92614
std::to_string() 481 517 488 492.8 10.6846
std::stringstream 617 761 628 639.1 41.1241
snprintf() 245 264 250 250.4 4.98397

圧倒的じゃないか!というくらいにぶっちぎりでto_chars()最速です。

std::from_chars()

std::from_chars()は文字列から数値へ変換するものです。入力はcharの文字列限定で出力は引数に取った数値型変数への参照で返します。戻り値の扱いなどはto_chars()と似た感じです。

これとの比較対象は以下のものです。これらは同じく文字列→数値への変換を行います。

  • std::stoll()/std::stod()
  • std::stringstream
  • sscanf()
  • strtoll()/strtod()

測定方法

100万件のランダムな数値をto_chars()で文字列に変換しておき、それを全件変換にかけかかる時間を計測します。それを10回繰り返して各種統計量で見てみることにします。整数型(64bit整数型)と浮動小数点型(double)それぞれで実験を行います。

先ほどの処理と似たようなことになります。

#include <iostream>
#include <charconv>
#include <vector>
#include <random>
#include <chrono>
#include <type_traits>
#include <cassert>
#include <thread>
#include <numeric>
#include <string>
#include <string_view>

template<typename NumericType>
auto make_data(unsigned int N) -> std::pair<std::vector<char>, std::vector<std::string_view>> {
  
  auto rng = []() {
    if constexpr (std::is_integral_v<NumericType>) {
      return std::uniform_int_distribution<NumericType>{};
    } else if constexpr (std::is_floating_point_v<NumericType>) {
      return std::uniform_real_distribution<NumericType>{};
    } else {
      static_assert([] { return false; }, "You have to specify a number type, right?");
    }
  }();
  
  std::mt19937_64 urbg{ std::random_device{}() };
  
  std::vector<char> buffer(N * 21);
  auto* pos = buffer.data();
  std::vector<std::string_view> vec;
  vec.reserve(N);

  for (auto i = 0u; i < N; ++i) {
    const auto num = rng(urbg);
    const auto [end, ec] = std::to_chars(pos, pos + 21, num);
    if (ec != std::errc{}) {
      --i;
      continue;
    }
    
    const std::size_t len = end - pos;
    vec.emplace_back(pos, len);
    pos += (len + 1);
  }

  return {std::move(buffer), std::move(vec)};
}

/*
report()関数は変更ないので省略
*/

template<typename NumericType, typename F>
void profiling(const char* target, F&& func) {
  using namespace std::chrono_literals;

  constexpr int trialN = 10;
  constexpr int sampleN = 1'000'000;

  std::chrono::milliseconds results[trialN]{};

  for (int i = 0; i < trialN; ++i) {
    //データの準備
    auto [buf, input] = make_data<NumericType>(sampleN);

    //計測開始
    auto start = std::chrono::steady_clock::now();

    for (auto sv : input) {
      func(sv);
    }

    //計測終了
    auto end = std::chrono::steady_clock::now();
    results[i] = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::this_thread::sleep_for(200ms);
  }

  std::cout << target << std::endl;
  report(results);
}


int main()
{
  std::int64_t v;

  profiling<std::int64_t>("to_chars() int64", [&v](auto sv) {
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.length(), v);
    if (ec != std::errc{}) throw new std::exception{};
  });
}

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

全部入りのコードはこちら

整数(std::int64_t

単位は全てms

方法 最小値 最大値 中央値 平均値 標準偏差
std::from_chars() 24 32 26 26.3 2.07605
std::stoll() 129 159 132 134.5 8.23104
std::stringstream 409 438 412 416.5 9.26013
sscanf() 181 196 185 186.6 5.14198
strtoll() 53 57 55 54.7 1.38203

浮動小数点数double

方法 最小値 最大値 中央値 平均値 標準偏差
std::from_chars() 163 196 168 174.2 12.2213
std::stod() 287 316 297 296 8.42615
std::stringstream 445 531 456 464.6 23.5338
sscanf() 339 352 348 346.6 3.66606
strtod() 195 200 197 196.9 1.17898

以外にstrtoll()/strtod()が検討していますが、こちらもfrom_chars()最速です。しかし浮動小数点数変換は際どい・・・

なお、実装をチラ見するにstd::stoll()/std::stod()は対応するstrtoll()/strtod()によって変換を行っているだけなので、その速度差はstd::stringのオブジェクト構築と動的メモリ確保のオーバーヘッドから来るもののようです(std::stringstreamも結局は同じようにstrto~に投げていますが、こっちは更にもう少し色々してるみたいです)。

グラフで見てみる

先程のprofiling()を少し変更して処理時間をCSVに吐き出してグラフ化してみましょう。

template<typename NumericType, typename F>
auto profiling(const char* target, F&& func) -> std::vector<std::chrono::milliseconds> {
  using namespace std::chrono_literals;

  constexpr int trialN = 10;
  constexpr int sampleN = 1'000'000;

  std::vector<std::chrono::milliseconds> results(trialN, 0);

  for (int i = 0; i < trialN; ++i) {
    //データの準備
    auto [buf, input] = make_data<NumericType>(sampleN);

    //計測開始
    auto start = std::chrono::steady_clock::now();

    for (auto sv : input) {
      func(sv);
    }

    //計測終了
    auto end = std::chrono::steady_clock::now();
    results[i] = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::this_thread::sleep_for(200ms);
  }

  std::cout << target << std::endl;

  return results;
}

template<std::size_t N>
void output(const char* filename, std::vector<std::chrono::milliseconds>(&array)[N]) {

  std::ofstream ofs{ filename , std::ios::out | std::ios::trunc};
  //BOM付加
  unsigned char bom[] = { 0xEF, 0xBB, 0xBF };
  ofs.write(reinterpret_cast<char*>(bom), sizeof(bom));

  auto datanum = array[0].size();

  for (auto i = 0u; i < datanum; ++i) {
    for (auto j = 0u; j < N; ++j) {
      ofs << array[j][i].count() << ", ";
    }
    ofs << "\n";
  }
}

int main() {
  std::int64_t v;

  std::vector<std::chrono::milliseconds> res_array[5]{};

  res_array[0] = profiling<std::int64_t>("from_chars() int64", [&v](auto sv) {
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.length(), v);
    if (ec != std::errc{}) throw new std::exception{};
  });
  
  output("from_chars_int64.csv", res_array);
}

グラフは箱ひげ図で見てみます。
各箱は100万件の整数/浮動小数点数値に対する文字列⇄数値変換にかかった処理時間とばらつきを表しており、各箱に含まれるデータ数は10です。

箱の上辺は第三四分位点、下辺は第一四分位点、中央の線が中央値、×点が平均値を表しています。ひげの上辺は最大値、下辺は最小値を表し、それらから外れた孤立点は外れ値を表しています。

これによって、各処理方法毎のおおよその処理時間とそのばらつきを視覚的に確認・比較できます。

なお、データはここまでの実験とは別に取りなおしたものなので数値は上の表と一致していません。しかし、大まかな傾向に変化はないはずです。

std::to_chars()

std::to_chars()の計測結果

std::from_chars()

std::from_chars()の計測結果

to_chars()/from_chars()が他よりも明らかに速い事が改めて確認できます。また、全体の傾向として整数変換よりも浮動小数点数変換の方が重いことも分かります。

相対比較

代表値として平均値を採用し、to_chars()/from_chars()の処理時間を1としたときの他の方法の処理時間の比を見てみます。これによって、系統的な誤差要因を無視したうえで他の方法がto_chars()/from_chars()の何倍遅いか?を見ることができます。

データは上の表にある平均値を利用します。

std::to_chars()

方法 \ 数値型 int64_t double
std::to_chars() 1.00 1.00
std::to_string() 2.82 10.4
std::stringstream 18.6 13.5
snprintf() 8.21 5.29

std::from_chars()

方法 \ 数値型 int64_t double
std::from_chars() 1.00 1.00
std::stoll()/std::stod() 5.11 1.70
std::stringstream 15.8 2.67
sscanf() 7.09 1.99
strtoll()/strtod() 2.08 1.13

例えばこれをもって、std::stringstreamによる数値→文字列変換はstd::to_chars()に比べて、整数で18.6倍、浮動小数点数で13.5倍遅い!などという事ができます。

結論

使えるならto_chars()/from_chars()使いましょう!ちょっぱやです!!

注意点

これはあくまでMSVCの実装における結果なので、GCCやclang等ではまた違った結果になるかもしれません。C++標準はto_chars()/from_chars()の実装については何も規定していないためです・・・

GCCやclangは良い環境が手元に無いのと<charconv>浮動小数点型対応がまだなので実験していません。

参考文献

この記事のMarkdownソース

[C++]コンソール出力にchar8_t文字列を出力したい!

Windows

おそらくほとんどの場合、非Windows環境ではcharエンコードUTF-8なのでそのまま出力できるはずです。しかし、C++20では標準出力ストリームに対するchar8_t char16_t char32_toperator<<deleteされているため、そのままではコンパイルエラーになります。でもまあcharUTF-8なのですから、こう、ちょっとひねってやれば、無事出力できます・・・

#include <iostream>
#include <string_view>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  std::cout << reinterpret_cast<const char*>(u8str.data()) << std::endl;
}

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

問題なのはWindowsさんです・・・

Windowsのコンソール出力と標準出力

C言語ではI/Oをファイルとそれに対するデータのストリームとして抽象化しています。ファイルの読み書きによって動作環境との通信(すなわちI/O)を制御し、規定しています。標準出力(stdout)や標準入力は(stdin)は標準によって予め開くファイルが規定されているストリームで、これらにおいてのファイルとはコンソール(端末)です。

C++もCからこれらのことを継承し、多くのI/O関数はCのものを参照しているため、この辺りの標準I/Oストリームに関することは共通しています。

従って、C/C++の範囲から見た標準出力とはとりあえずは何かのファイルに対する出力として考えることができます。特に、標準出力が受け取った文字列をどう表示するかというところはファイル出力の先の話であり、C/C++が感知するところではありません。

標準IOストリームのモード

C言語においてのファイルストリームには3つのモードがあり、ファイルオープンに使う関数種別fopen/wfopenおよびその引数、もしくはそのファイルストリームに対して最初に使用した関数で決定されます。

テキストモードではストリームへの入力データをテキストデータ(マルチバイト文字列 : char文字列)だとして処理します。改行コードの変換やロケール対応がここで行われます。標準入出力のデフォルトはこのモードです。この時、ワイド文字列版のI/O関数(std::wcoutなど)を使用すると、内部でマルチバイト文字列へ変換されたうえでストリームへ入力されます。

ユニコードモードではテキストモード時の入力データをワイド文字列(wchar_t文字列)として扱います。それ以外はテキストモードと同様ですが、マルチバイト文字列(char文字列)を処理するI/O関数(std::coutなど)が使用できなくなります。

バイナリモードはその名の通り入力データをバイト列として扱います。ワイド文字、マルチバイト文字版何れの関数でもその入力データをバイト列として扱い、何の変換も行われません。

ここでのマルチバイト文字列/ワイド文字列のエンコードは規定されていません。WindowsではそれぞれANSI/UTF-16になります。

出力だけに注目すると、最終的にファイルにはモードに応じて次のように書きこまれます。

  • テキストモード
    • char : そのまま書き込み
    • wchar_t : charに変換されて書き込み
  • ユニコードモード
    • char : 使用不可
    • wchar_t : そのまま書き込み
  • バイナリモード
    • 何れの場合もそのまま書き込み

コンソールのコードページ

ここからはWindowsのお仕事です。

Windowsにおける標準出力として設定されているファイルの中はコンソール出力へ繋がっています。C/C++のI/O関数としてはこのファイルに対してテキストモードではcharエンコード、すなわちWindows環境ごとのANSI(日本語ならShift-JIS)エンコードで文字列を書きこんでおり、ユニコードモードならばUTF-16で文字列を書きこんでいます。

文字列を表示するためには、その環境の言語毎に最適なエンコードを選択して文字列をそれに変換したうえで表示する必要があります。例えば、ANSI文字列と言っても言語設定によってその解釈に使用すべき文字コードは変化します。
Windowsのコンソールにおいてそれを指定しているのがコードページです。日本語環境ならばCP932というコードページがデフォルトであり、その文字コードはShift-JiSが利用されます。

おおよその場合デフォルトのコードページはその言語環境に合わせたANSIを示すコードページになっているはずなので、テキストモードではコードページに合わせた何かをする必要はありません。バイナリモードの場合も、ファイル出力されてきたバイト列をコードページに従ったエンコードで解釈するだけです。

しかし、ユニコードモード時はそのコードページに対応するエンコードへ変換する必要があります。無駄に思われるかもしれませんが、標準ストリームのモードとコードページは別なのです。コードページはOSで指定されているものなので、表示に当たってはそちらが優先されます。

そのため、ここのコードページを変更してやればユニコードモードにおいてはUTF-16を無変換で通すこともできるかもしれません。

スクリーンバッファ

コードページに従ったエンコードに変換された文字列は最後にスクリーンバッファに出力され、そのままフォントレンダラに渡され表示されます。

このスクリーンバッファ1文字辺りはCHAR_INFO構造体1つによって表現されます。定義を見るに、1文字はwchar_t1つかchar1つのどちらかです。これはおそらくVSプロジェクト設定にある文字セットの設定によってどちらが使われるか決定されると思われます。

あえて変更しなければ今時はユニコードになっているはずなので、スクリーンバッファの文字コードひいてはコンソール最終出力の文字コードUTF-16になっています。従って、コードページのエンコードからスクリーンバッファのエンコードへ再び変換され、スクリーンバッファへと出力・表示されることになります。

CHAR_INFO1つがコンソールスクリーン上の1文字に当たり、それはwchar_t1つ分なので、コンソール出力ではサロゲートペアや合字をそのまま扱えなさそうなことがうかがえます・・・

1. 素直に変換してstd::coutする

一番簡便かつ確実な方法は、UTF-8文字列をANSI(Shift-JIS)文字列へ変換してstd::coutへ出力することです。

std::codecvtC++17で非推奨化してしまったので変換にはWinAPIを利用することにしますが、UTF-8 -> Shift-JISの変換を実はそのままできません。MultiByteToWideCharchar* -> wchar_t*へ、WideCharToMultiBytewchar_t* -> char*へ変換するので、どうしても型を合わせられないのです・・・

なのでこれらを連続適用して、UTF-8 -> UTF-16 -> Shift-JISという2段階変換することになります。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);
  
  std::wstring temp(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    temp.data(), temp.length());

  //UTF-16 -> Shift-JIS
  length = ::WideCharToMultiByte(CP_ACP, 0,
    temp.data(), static_cast<int>(temp.length()),
    nullptr, 0,
    nullptr, nullptr);

  std::string result(length, '\0');

  res = ::WideCharToMultiByte(CP_ACP, 0,
    temp.data(), static_cast<int>(temp.length()),
    result.data(), static_cast<int>(result.length()),
    nullptr, nullptr);

  std::cout << result;
}

出力結果

変換の実装を信用すれば、UTF-8 -> UTF-16の変換で文字が落ちることはありませんが、UTF-16 -> Shift-JISの変換では当然Shift-JISでは受けきれないものが出てきます(絵文字とか)。それはWideCharToMultiByteがシステムデフォルト値(どうやら??)で埋めてくれます。

後面倒なのでしてませんが、コード内resで受けてる変換結果が0だとエラーが起きてるのでケアした方が良いでしょう。

1.2 UTF-16に変換してstd::wcoutする

しかしとはいえ、二段階変換はさすがに気になりますし、途中でバッファ(wstring)を確保しなければいけないのも少し気になります。むしろ、std::wcoutUTF-16出力したくなりますよね。しかし、そのままだとなぜかAscii範囲外の文字が出力されません・・・

std::wcoutと言えども出力先はstd::coutと一緒です。すなわち、wchar_tを内部でcharに変換してから出力しています。そしてどうやら、std::wcoutのデフォルトはCロケールになっており、Cロケールでは変換時に非Ascii範囲の文字をスルーしてくれるようです。華麗です・・・

つまりは、明示的にロケールを指定してあげればいいのです。何を指定すればいいのかさっぱりですが、幸いWindowsではstd::locale("")とするとその環境のシステムデフォルトのロケールが取得できます。これはWindows限定でポータブルで、外国語環境に行っても適切にその環境のデフォルトロケールを取得することができます。後はこれをstd::wcoutにセットしてやればいいのです。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEANv
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);

  std::wstring result(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    result.data(), static_cast<int>(result.length()));

  // wcoutにシステムデフォルトのロケールを設定(Cロケールから変更
  std::wcout.imbue(std::locale(""));

  // 出力
  std::wcout << result;
}

出力結果

絵文字は消えましたが日本語出力は出来ているように見えます。しかし、これ以降同じプログラム内でstd::wcoutに何か出力しようとしても何も出てきません。

絵文字が消えているまさにそれが問題で、Shift-JISは絵文字を表現できないので絵文字の変換の際に内部でエラーとなってしまい、それ以降fail状態となり何も出てこなくなるのです。これはfail()によって検出でき、clear()によって回復できます。

  // wcoutにシステムデフォルトのロケールを設定
  std::wcout.imbue(std::locale(""));

  // 出力
  std::wcout << result;

  // fail状態なら状態を復帰する
  if (std::wcout.fail()) {
    std::wcout.clear();
  }

std::wcoutで出力したとしてもその内部でコードページに従った変換(結局Shift-JISへの変換)が走っているうえに、変換エラーによって出力できなくなるというのはこれはこれでイケてないですね・・・

2. UTF-16に変換してWriteConsoleW()する

Windowsにおいて、スクリーンバッファに直接出力するためのAPIWriteConsoleW()関数です。この関数はUTF-16文字列を受け取り、指定されたコンソールのスクリーンバッファに直接出力します。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);

  std::wstring result(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    result.data(), static_cast<int>(result.length()));

  // 出力
  ::WriteConsoleW(::GetStdHandle(STD_OUTPUT_HANDLE), result.data(), result.length(), nullptr, nullptr);
}

出力結果

WriteConsoleW()関数は指定されたコンソールのスクリーンバッファに対して指定されたUTF-16文字列を直接書き込む関数です。スクリーンバッファはコンソールの出力そのもので、ここに書き込まれているデータがフォントレンダラによって表示されます。

出力結果をコピペしてみると分かるのですが、絵文字列は表示出来ていないだけでコピペ先が表示できるもの(VSCodeとか)ならばちゃんと表示されます。すなわち、文字コードとしては出力までUTF-16で行われています。絵文字が出ないのはおそらくコンソールの表示部分がサロゲートペアを扱えないのに起因していると思われます。

WriteConsoleW()関数は名前の通りコンソール出力専用の関数なので、起動したプログラムにコンソールが割り当てられていない場合に失敗します。すなわち、この関数による出力ではリダイレクトができません。

3. 標準出力をユニコードモードにする

冒頭で説明したように、ユニコード出力だけを使うのであれば標準ストリームをユニコードモードにしてしまえばいいでしょう。Windowsでは_setmode()関数によってストリームのモードを後から変更できます。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);

  std::wstring result(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    result.data(), static_cast<int>(result.length()));

  // 標準出力をユニコードモードにする
  ::_setmode(_fileno(stdout), _O_U16TEXT);
  // 出力
  std::wcout << result;
}

出力結果

この方法実は、ユニコードモードといいつつユニコード直接出力出来ているわけではありませんので、コピペしてみると表示できないものは表示できない事が分かるでしょう。内部でUTF-16 -> Shift-JIS -> UTF-16変換が行われています。変換に失敗した文字列はスペースが当てられているのでしょうか。試してませんが、UTF-16コードページに変更すればあるいは・・・

なお、この方法だとstd::coutが使用できなくなります。出力するとエラー吐いて止まります・・・。他人の書いたライブラリを使っているときなどはログ出力にstd::coutが使用されている可能性があるので注意が必要です。

4. コンソールのコードページを変更してUTF-8バイト列を直接流し込む

C/C++I/O関数の範囲内においてはバイナリモードで出力しておき、コンソールのコードページをUTF-8に変更してしまえば、スクリーンバッファへの出力時のUTF-16変換一回で済みそうです。これならばUTF-8文字列をなるべく変換させず、文字が落ちることもほぼないはず・・・

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  // 出力先コンソールのコードページをUTF-8にする
  ::SetConsoleOutputCP(65001u);
  // 標準出力をバイナリモードにする
  ::_setmode(_fileno(stdout), _O_BINARY);
  // バイナリ列として直接出力
  std::cout.write(reinterpret_cast<const char*>(u8str.data()), u8str.length());
}

出力結果

最後の方がダブってるのはなんでしょうか、3バイト以上の文字が悪さをしているのでしょうか・・・

この方法でも、VSCodeなどにコピペしてみれば絵文字が正しく表示されるので意図通りになっているようです。また、Ascii範囲内の文字ならばstd::coutは依然として使用可能ですが、std::wcoutは文字化けします。

コードページを変更してあるので、コンソールはまず入ってきたバイト列をUTF-8文字列として解釈します。UTF-8はAscii文字と下位互換性があるのでstd::coutはAscii範囲内に限って使用可能となります。しかし、std::wcoutは通常wchar_tUTF-16)を受け付けますが、バイナリモードでは無変換でコンソール入力へ到達し、そこでのコードページに従った解釈の際、UTF-16文字列をUTF-8文字列だと思って処理してしまうため、文字化けします・・・

ただし、コンソールのスクリーンバッファへの出力は通常UTF-16なので、UTF-8がそのまま出力されているわけではなく、スクリーンバッファへの出力にあたってはUTF-8 -> UTF-16の変換が行われます。

5. Boost.Nowideを使用する

boost1.73から追加されたBoost.Nowide<iostream><fstream>UTF-8対応をポータブルにするライブラリです。非Windows環境に対してはcharエンコードUTF-8だと仮定しそのまま、Windows環境ではUTF-8 -> UTF-16変換してWriteConsoleW()などWindowsユニコード対応APIで出力します。

残念ながらchar8_t対応はされていない(おそらく厳しい)のですが、これを利用すれば一番最初に紹介した方法がポータブルになります。

#include <string_view>
#include <boost/nowide/iostream.hpp>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  boost::nowide::cout << reinterpret_cast<const char*>(u8str.data()) << std::endl;
}

試していないので出力がどうなるのかは分かりませんが、実装を見るにおそらくWriteConsoleW()を使用したときと同様になるかと思われます。

UTF-8の直接出力 in Windows

無理です。

絵文字の表示 in Windows

通常のコンソールでは無理ですが、Windows Terminalを使えば表示できます。

上記2の(WriteConsoleW()による)方法での出力 in Windows Terminal

出力結果

上記4の(コードページ変更とバイナリモードによる)方法での出力 in Windows Terminal

出力結果

まだ合字が表示できないみたいですが、今後に期待ですね。

検証環境

参考文献

謝辞

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

この記事のMarkdownソース

[C++]コンセプトの無言のお願い事

等しさの保持(equality preservation

ある式が等しい入力に対して等しい出力を返すとき、その式は 等しさを保持(equality-preserving しています。式とはわかりやすいところでは関数であり、演算子のことです。ある式をf()とするとa == bならばf(a) == f(b)となり、かつ常にこれが成り立つ時、f()は等しさを保持する式ということです。

この場合の入力とは、その式に直接与えられた引数全てのことであり、1つの引数は1つの式のことです。正確には以下のものだけを含む一番大きな部分式のことです。

  • id-expression(一次式)
  • std::move(), std::forward(), std::declval()の呼び出し

例えば、f(std::move(a), std::declval<T>(), c)みたいなコードでは、まずこの全体が1つの式です。この式の部分式とはstd::move(a), a, std::declval<T>(), cで、上記2つだけを含む最大の部分式=入力は、std::move(a), std::declval<T>(), cの3つです。
別の例では、a = std::move(b)と言う式の入力はa, std::move(b)の2つです。

式は最終的に結果となる1つの値になるので、等しさを保持する式の入力(の式)というのはつまりある1つの引数値と言う事です。

そして、出力とは式の結果(上記のf()ならその戻り値)および、その式の実行によって変更された引数の集合です(変更されなかった引数は含まれない)。

等しさを保持する式の入力と出力はこれら以外にあってはいけません。

安定(stable

あるオブジェクトを入力(引数)にとるある式の2回の評価において、そのオブジェクトの明示的な変更が介在しない限り等しい出力が得られる時、その式は 安定(stable な式です。等しさを保持する式は安定でなければなりません。

つまり、等しさを保持し安定である式は内部や外部の状態に依存してはならず、直接の引数以外に対して副作用を持ってはならないと言う事です。

そして、標準ライブラリにおけるコンセプト定義内の全ての制約式は、特に注釈がない限り等しさを保持し安定でなければなりません。これは、そのコンセプトを満たそうとする場合にユーザーコードに対しても要求されます。

例外的に等しさを保持することを要求されないコンセプトには例えばstd::invocableなどがあります。

定義域(domain

等しさを保持する式はその入力となりうる全ての値について有効である必要はありません。例えば、整数に対するa / bと言う式は等しさを保持する式ですが、b == 0の時この式は有効ではありません。
しかし、この様な入力を取り得たとしても、そのことはその式が等しさを保持することに影響を与えません。

ある等しさを保持する式の入力の全体から、この様な有効ではない入力を除いた集合をその式の 定義域(domain と呼びます。

この用語はコンセプトの意味論的な制約条件に出現することがあります(例えば、std:: equality_comparable_withなど)。

制約式の引数に対しての制約

標準ライブラリのコンセプト定義においては、あるrequires式内の各制約式が引数に対して副作用を及ぼしても良いかどうか(引数を変更することが許されるか)をそのrequires式の引数(ローカルパラメータ)のconst修飾によって表現しています。ローカルパラメータがconst修飾されている場合はそのパラメータを引数に取る制約式は対応する引数を変更してはなりません。逆に、const修飾されていなければ変更しても構いません。

このことも、コンセプトを満たそうとすれば自然にユーザーコードに対して要求されることになります。とはいえ、const修飾されたローカルパラメータが渡ってくるところでその引数を変更しようとするのは、const_castとかmutableとかなんかおかしなことをしない限りそれを破ることは無いでしょう・・・?

template<typename T>
concept C1 = requires(T a, T b) {
  f(a, b);
  a + b;
  // このC1コンセプトを満たす型は、f(T, T)とoperator+(T, T)の呼び出しが可能である必要がある
  // そして、そのような型に対するf()とoperator+の実装は、2つの引数に対して副作用を及ぼしても(引き数を変更しても)良い
};

template<typename T>
concept C2 = requires(const T a, T b) {
  f(a, b);
  a + b;
  // このC2コンセプトを満たす型Tは、f(T, T)とoperator+(T, T)の呼び出しが可能である必要がある
  // そのようなTに対するf()とoperator+の実装は、その第一引数は変更してはならない(`const`修飾されたローカルパラメータ`a`が渡されている)
  // ただし、第二引数は`const`修飾のないローカルパラメータ`b`が渡されているので、変更しても良い
};

この様に決めた上で、コンセプトの型パラメータTがCV修飾されていないオブジェクト型であり完全型と仮定すると、その定義内requires式ではそのローカルパラメータのCV/参照修飾から各ローカルパラメータの値カテゴリとCV修飾を確定することができます。 このようにCV修飾と値カテゴリを指定したローカルパラメータを利用すれば、各制約式が引数としてどのようなCV修飾でどの値カテゴリを受け取るべきなのか?という制約を表現することができます。

// このTがCV修飾されていないオブジェクト型であり完全型と仮定すると
template<typename T>
concept C = requires(T a, T&& b, const T& c) {
  // aの型はCV無しのTであり、左辺値
  // bの型はTの参照型であり、右辺値
  // cの型はconst T&であり、左辺値
  // というように、CV修飾と値カテゴリを指定できる

  f(a);             // 式f()はTのconst無し左辺値を受け取れる必要がある
  g(std::move(b));  // 式g()はTのconst無し右辺値を受け取れる必要がある
  h(c);             // 式h()はTのconst左辺値参照を受け取れる必要がある
};

requires式とrequires

C++20コンセプトではrequiresキーワードは、それを書く場所によってrequires-clauserequires節)とrequires-expressionrequires式)のどちらかとして扱われます。

また、requires節内にrequires式を書くこともできます。

template<typename T>
concept C1 =
  requires { T{}; };  // requires式

template<typename T>
concept C2 = 
  requires(T a) {     // requires式
    ++a; 
  };

template<typename T>
  requires C1<T> // requires節
void f(T t)
  requires C2<T> // requires節
{
  /*関数本体*/
}

template<typename T>
  requires ( requires { T{}; } )          // requires節とその中のrequires式
void g(T t)
  requires ( requires(T a) { t += a; } )  // requires節とその中のrequires式
{
  /*関数本体*/
}

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

requires式は任意の型に対する制約条件を表現する制約式となり、requires節はテンプレートにおいてそのテンプレートパラメータに対する制約を指定するものです。

ローカルパラメータを取れるのはrequires式だけなので、そのconst修飾による引数への副作用の制約表現のお話はrequires式だけの話です。

暗黙的な式のバリエーション(implicit expression variations

requires式ローカルパラメータのconst修飾によって制約式が引数を変更しないことを表明する場合、その制約式には非constの左辺値、右辺値、およびconst右辺値を取る追加の形式が暗黙に要求されます。これら暗黙の追加形式のことを 暗黙的な式のバリエーション(implicit expression variations と呼びます。

template<typename T>
concept C = requires(const T a, T b) {
  f(a, b);  // このf()は第一引数に対して暗黙的な式のバリエーションが要求される
};

// 明示的に書けば以下の様になる
template<typename T>
concept C = 
  requires(const T a, T b) { 
    f(a, b);
    f(std::move(a), b);
  } &&
  requires(T   a, T b) { f(a, b); } &&
  requires(T&& a, T b) { f(std::move(a), b); };

この様なf()は例えば次の様になります。

// 例えばT = intとすると

// ok
f(int n, int m);         // コピー・ムーブによって上記バリエーションの全てを受けられる
f(const int& n, int m);  // const左辺値参照は上記バリエーションの全てを受けられる

// ng
f(int& n, int m);  // 非const左辺値だけしか受けられない
f(int&& n, int m); // 非const右辺値だけしか受けられない

ただし、これら追加のバリエーションが制約式として明示的に書かれていない場合、それをどこまで構文的にチェックするのかは実装依存となります、

requires式ではありませんがこの様な追加の暗黙のバリエーションを明示的に書いているものには、std::copyablestd::copy_constructibleなどがあります。

コンセプトのモデルとなるために

「等しさの保持(かつ安定)」「引数への副作用の制約」「暗黙的な式のバリエーション」、これらの標準ライブラリのコンセプトが暗黙的に要求する事は構文的な制約ではなく意味論的な制約です。つまり、コンパイル時にチェックされる(あるいはできる)ものではありません。違反していたとしてもコンパイルエラーにはならないでしょう・・・

あるコンセプトCについて、型TCの要求する構文的な制約(制約式)を全て満たしていて、上記3つの暗黙的な制約も全て満たしており、かつCに追加で指定される意味論的な制約を全て満たしている時、型TはコンセプトCモデル(model であると言います。

型がコンセプトのモデルであることは、標準ライブラリのテンプレート(クラス・関数)の事前条件(Post Condition)において要求されます。このとき、そこに指定されているコンセプトのモデルとならない型の入力は診断不用(チェックも警告もされない)の未定義動作になります。
少なくとも標準ライブラリのものを利用するときは、コンセプトのモデルについて意識を向ける必要があるでしょう。

C++20で導入された<span>, <ranges>, <format><algorithm>, <iterator>の一部等は既にコンセプトを使用するように定義されているため、その事前条件においてはコンセプトのモデルであることを要求するようになっています。

参考文献

謝辞

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

この記事のMarkdownソース