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