yohhoyの日記

技術的メモをしていきたい日記

C++26 std::execution

本文こちら→C++ MIX #16に参加しました - yohhoyの日記(別館)

スライド資料:https://www.docswell.com/s/yohhoy/ZQXJ2E-cpp26-execution

関連URL

C++26 Executionライブラリ:非同期スコープ

C++2c(C++26)実行制御ライブラリ(execution control library)(→id:yohhoy:20250609)における非同期操作の早期開始と非同期スコープについて。

Sender型で表現されたタスクチェインに対して、非同期操作の開始要求と完了待機を一括実施する遅延開始(lazy start)と、非同期スコープ(async scope)との組み合わせによる開始要求と完了待機を分離した早期開始(eager start)を行うSenderアルゴリズム(→id:yohhoy:20250612)が提供される。

まとめ:

  • 遅延開始:Senderアルゴリズムthis_thread::sync_wait(_with_variant)により、非同期操作を開始して完了まで待機する。処理結果は戻り値/例外送出によって取得される。
  • 早期開始:Senderアルゴリズムexecution::spawn, execution::spawn_futureにより、非同期スコープと関連付けて非同期操作を早期開始する。完了待機には非同期スコープから生成した合流(join)Senderを利用し、非同期操作の結果はspawn_futureが返すSender経由で取得する。
    • spawnコンシューマ:空の値完了set_value_t()と停止完了set_stopped_t()のみ対応。エラー完了には対応しない。
    • spawn_futureアダプタ:任意の完了シグネチャ集合に対応。
  • Senderアルゴリズムexecution::associate:Senderを非同期スコープと関連付け、非同期操作の生存期間(lifetime)をトラッキングする。任意の完了シグネチャ集合に対応。

spawn/spawn_futrueアルゴリズム

// C++2c
#include <execution>
namespace exec = std::execution;

void proc(int) noexcept;  // 戻り値なし
int compute(int) noexcept; // 戻り値あり

void start_lazy()
{
  // タスクチェインを構成: システムスレッドプール上でproc(1)を呼び出す
  exec::sender auto work = exec::on(
    exec::get_parallel_scheduler(),
    exec::just(1) | exec::then(proc));

  proc(2);

  // 非同期操作を遅延開始し、処理完了まで待機する
  std::this_thread::sync_wait(std::move(work));

  proc(3);
  // proc(2)→proc(1)→proc(3)順に呼び出される
}

void start_eager()
{
  // タスクチェインを構成: システムスレッドプール上でproc(1)を呼び出す
  exec::sender auto work = exec::on(
    exec::get_parallel_scheduler(),
    exec::just(1) | exec::then(proc));

  // 非同期スコープに関連付けて非同期操作を早期開始する
  exec::counting_scope scope;
  exec::spawn(std::move(work), scope.get_token());

  proc(2);

  // 非同期スコープに関連付けられた全ての非同期操作完了を待機する
  std::this_thread::sync_wait(scope.join());

  proc(3);
  // {proc(1) | proc(2)}→proc(3)順に呼び出される
  // proc(1)とproc(2)は同時並行に実行されうる
}

void start_eager_future()
{
  // タスクチェインを構成: システムスレッドプール上でcompute(1)を呼び出す
  exec::sender auto work = exec::on(
    exec::get_parallel_scheduler(),
    exec::just(1) | exec::then(compute));

  // 非同期スコープに関連付けて非同期操作を早期開始する
  exec::counting_scope scope;
  exec::sender auto future =
    exec::spawn_future(std::move(work), scope.get_token());

  int r2 = compute(2);

  // 非同期スコープに関連付けられた全ての非同期操作完了を待機する
  auto result = std::this_thread::sync_wait(
                  exec::when_all(std::move(future), scope.join()));
  auto [r1] = result.value();

  // compute(1)とcompute(2)は同時並行に実行されうる
}

Senderチェイン内で入れ子の非同期スコープを自動管理するSenderアダプタexecution::let_async_scopeは提案文書P3296にて検討中。*1

// P3296
std::this_thread::sync_wait(
  exec::just(1)
  | exec::let_async_scope(
    [] (auto token, int n) {
      // 非同期スコープに関連付けて非同期操作を早期開始する
      exec::spawn(exec::on(
        exec::get_parallel_scheduler(),
        exec::just(n) | exec::then(proc)
      ), token);

      proc(2);
    }
  ) | then([] {
      proc(3);
    }
  ));
// {proc(1) | proc(2)}→proc(3)順に呼び出される
// proc(1)とproc(2)は同時並行に実行されうる

非同期スコープ: Counting Scope

C++2c標準ライブラリは、カウンタ値に基づく非同期スコープ実装クラスを提供する。これらの非同期スコープ型はコピー/ムーブ不可。

  • execution::simple_counting_scope:非同期操作の早期開始でカウンタをインクリメントし、非同期操作の完了時にカウンタをデクリメントする。
    • get_token操作:非同期スコープトークン(scope_token)を取得する。
    • close操作:以降は非同期操作との関連付けを抑止し、早期開始操作(spawn, spawn_future)では非同期操作をスキップする。spawn_future戻り値Senderでは停止完了(set_stopped)が即時送信される。
    • join操作:カウンタ値が0になるまで合流待機(join)するSenderを生成する。合流Senderの完了シグネチャ集合=空の値完了(set_value_t())+任意のエラー完了(set_error_t(E))+停止完了(set_stopped_t()) *2
  • execution::counting_scopesimple_counting_scope+非同期キャンセル(request_stop)のサポート。

Counting Scopeクラスは内部状態を管理しており、各操作をトリガとして状態遷移が行われる。

  • オブジェクト構築後の初期状態:unused
  • オブジェクト破棄時の事前条件:unused/joined/unused-and-closedのいずれか

操作assocはSenderとの関連付け成功、操作disassocは非同期操作の完了によりカウンタ値が0となる最終関連付けの解除、操作start-joinは合流Senderの開始を表す。

状態\操作 assoc disassoc start-join close
unused open - joined unused-and-closed
open (+1) - open-and-joining closed
open-and-joining (+1) joined (open-and-joining) closed-and-joining
closed - - closed-and-joining -
unused-and-closed - - joined -
closed-and-joining - joined (closed-and-joining) -
joined - - (joined) -

表中の下線付き状態はオブジェクト破棄可能であることを、括弧は内部状態が変化しない自己遷移を表す。"(+1)" はカウンタのインクリメントのみ行われ、状態遷移しない。

associateアルゴリズム

入力Senderに対して非同期スコープとの関連付けを行い、関連付けされた(associated)Senderを返す。同Senderがそのまま破棄されるか、開始(start)された非同期操作の完了までを非同期スコープを介して追跡する。

exec::counting_scope scope;

// タスクチェインを構成し、非同期スコープに関連付ける
exec::sender auto work =
  exec::just(1)
  | exec::then(proc)
  | exec::associate(scope.get_token());

// タスクを別スレッド上で開始 or そのまま破棄する操作へ委譲
async_start_or_discard_task(std::move(work));

// (自スレッドの処理)

// 非同期操作の完了 or タスク破棄を待機
std::this_thread::sync_wait(scope.join());

associateアルゴリズムが関連付けに失敗すると関連付けの無い(unassociated)Senderを返し、同Senderの開始操作は上流側Senderを開始せず即座に停止完了を送信する。Counting Scopeに対してclose操作を行うと以降の関連付けが失敗するようになり、別スレッドからの非同期タスクの開始をの受付停止機構を実現できる。

exec::scheduler auto sch = /*Scheduler*/;
exec::counting_scope scope;

// 非同期要求の受付停止とリソース解放
void finish()
{
  // 非同期スコープをcloseし、以降の非同期タスク開始を抑止する
  scope.close();
  // 関連付けが行われた全タスクの完了(or破棄)を待機する
  std::this_thread::sync_wait(scope.join());

  // (リソース解放処理)
}

// sch上で実行される非同期スコープに関連付けたタスクを返す
//   finish前: 処理Xが実行され、戻り値を値送信する
//   finish後: 停止完了が送信される(処理Xは実行されない)
exec::sender auto create_new_task(int args);
{
  return exec::schedule(sch)
       | exec::then([args](){ return /*処理X*/; })
       | exec::associate(scope.get_token());
}

関連URL

*1:https://github.com/cplusplus/papers/issues/1948

*2:Receiver環境から取得したSchedulerに対するスケジュールSender(schedule sender)の完了シグネチャ集合に相当する。

*3:https://github.com/cplusplus/papers/issues/2315

*4:https://github.com/cplusplus/papers/issues/2335

式static_assert in C言語

プログラミング言語Cにおいて、式(expression)として扱えるコンパイル時アサートの実装例(C99以降)。*1 *2

#include <assert.h>

#define static_assert_expr(cexp_, msg_) \
  ((void)sizeof(struct {        \
    static_assert(cexp_, msg_); \
    int dummy_member;           \
  }))

C言語では空の構造体が禁止される(→id:yohhoy:20230430)ため、ダミーのメンバdummy_memberを追加している。C11 6.2.5/p20, 6.7.2.1/p1, 6.7.10/p1より一部引用(下線部は強調)。

Any number of derived types can be constructed from the object and function types, as follows:

  • (snip)
  • A structure type describes a sequentially allocated nonempty set of member objects (and, in certain circumstances, an incomplete array), each of which has an optionally specified name and possibly distinct type.
  • (snip)

Syntax
struct-or-union-specifier:
  struct-or-union identifieropt { struct-declaration-list }
  struct-or-union identifier
struct-or-union:
  struct
  union
struct-declaration-list:
  struct-declaration
  struct-declaration-list struct-declaration
struct-declaration:
  declaration-specifiers init-declarator-listopt ;
  static_assert-declaration
(snip)

Syntax
static_assert-declaration:
  _Static_assert ( constant-expression, string-literal ) ;

ノート:C++ではsizeof式での新しい型定義は明示的に禁止されている。*3

関連URL

*1:C11で導入された _Static_assert/static_assert は構文要素上は宣言(declaration)に区分されるため、そのままでは式(expression)の一部として利用できない。C17時点では<assert.h>ヘッダによる static_assert マクロ定義を必要とするが、C23現在は static_assert がキーワードに格上げされ、従前の _Static_assert 利用は非推奨となっている。

*2:C99時点ではLinuxの BUILD_BUG_ON マクロのように、負の要素数を持つ配列型によるHACK実装が行われていた。実装例:((void)sizeof(char[(cexpr_)?1:-1]))

*3:C++03 5.3.3/p5: "Types shall not be defined in a sizeof expression."

C++26 Executionライブラリ:Sender完了シグネチャ集合

C++2c(C++26)実行制御ライブラリ(execution control library)(→id:yohhoy:20250609)において、Sender型の完了シグネチャ集合(set of completion signatures)の宣言方法。[P3557R3採択後WD N5014*1準拠]

自作SenderクラスMySenderの完了シグネチャ集合は、コンパイル時評価される静的メンバ関数テンプレートMySender::get_completion_signaturesの戻り値型execution::completion_signatures<Sigs...>によって表現する。実行制御ライブラリからはexecution::get_completion_signaturesを経由したexecution::completion_signatures_of_tエイリアステンプレートによりSender完了シグネチャ集合にアクセスされる。

// C++2c
#include <execution>
namespace exec = std::execution;

// 非依存Senderクラス(詳細後述)
struct MySender {
  using sender_concept = exec::sender_t;

  // MySenderの完了シグネチャ集合を宣言
  // 値完了     set_value_t(int)
  // エラー完了 set_error_t(exception_ptr)
  // 停止完了   set_stopped_t()
  template <class Self>
  static consteval auto get_completion_signatures()
  {
    return exec::completion_signatures<
      exec::set_value_t(int),
      exec::set_error_t(std::exception_ptr),
      exec::set_stopped_t()
    >{};
  }

  // ...
};

非依存Sender/依存Sender

Senderはその完了シグネチャ集合が確定するタイミングに応じて、下記2種類に区分される。

非依存Sender(non-dependent sender)
オブジェクト構築時点で完了シグネチャ集合が確定しているSender*2。例:justファクトリが生成するSender。非依存Senderを子Senderにもつ標準Senderアダプタが生成するSenderなど。
依存Sender(dependent sender)
Senderと後続Receiverとの接続時(connect)に、Receiver環境(environment)に依存して完了シグネチャ集合が型計算されるSender。execution::dependent_senderコンセプトを満たす。例:Receiver環境を読み取るread_envアルゴリズム(→id:yohhoy:20250612)が生成するSender。

Sender型Sndrの非依存Sender/依存Sender区分は、コンパイル時に静的メンバ関数テンプレートをSndr::get_completion_signatures<Sndr>()形式で呼び出したときにexecution::dependent_sender_error例外を送出*3する(依存Sender)か否か(非依存Sender)で判定される。

Senderクラスでは下記いずれかの静的メンバ関数テンプレートget_completion_signaturesを定義すればよい。いずれも空の引数リストをもち、戻り値型はクラステンプレートexecution::completion_signatures<Sigs...>とする*4。妥当な完了シグネチャ集合を定義できない場合は、任意の例外クラスを送出する*5ことでコンパイルエラーとできる。

// 非依存Senderとして定義
template <class Sndr>
static consteval auto get_completion_signatures();

// 依存Senderとして定義
template <class Sndr, class Env>
static consteval auto get_completion_signatures();

// 型情報に応じて非依存Sender/依存Senderを切替
template <class Sndr, class... Env>
static consteval auto get_completion_signatures();
// 依存Senderとして定義する場合は、Envが空の
// 呼び出し式 get_completion_signatures<Sndr>() に対して
// 例外 execution::dependent_sender_error を送出する

旧P2300R10仕様

提案文書P3557R3採択以前の実行制御ライブラリベース提案P2300R10時点では、Sender型Sndrの完了シグネチャ集合宣言には下記方式がとられていた。

  • 非依存Sender(相当)*6:メンバ型Sndr::completion_signaturesとして完了シグネチャ集合execution::completion_signatures<Sigs...>を定義する。
  • 依存Sender:戻り値型execution::completion_signatures<Sigs...>をもつメンバ関数テンプレートget_completion_signatures(Env)を定義する。SenderオブジェクトsndrとReceiver環境envに対して、decltype(sndr.get_completion_signatures(env))で完了シグネチャ集合をコンパイル時計算する。

P3557R1まではメンバ型completion_signatures定義方式も有効であったが、P3164R4を統合したP3557R2時点で冗長な仕様として削除された経緯がある。

Drop support for specifying a sender's completion signatures with a nested type alias.

P3557R3, Revision History, R2

Finally, since a sender can now use its get_completion_signatures() member function to provide its non-dependent senders, the nested completion_signatures type alias becomes redundant. This paper suggests dropping support for that.

P3164R4 Early Diagnostics for Sender Expressions

関連URL

*1:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5014.pdf

*2:型 Sndr が非依存Senderであることは、式 sender<Sndr> && !dependent_sender<Sndr> で判定可能。

*3:C++26からコンパイル時の例外送出+例外ハンドリング(try-catch)がサポートされる。P3068R6 参照。

*4:仮に適当な型を戻り値型としても execution::get_completion_signatures 仕様により未規定の例外送出動作へと変換される。これは同関数テンプレートの戻り値型制約を満たすための措置。

*5:依存Sender判定に用いられる execution::dependent_sender_error 以外を送出すること。

*6:P2300R10時点では “非依存Sender” 概念は定義されておらず、依存Senderと同様にReceiver接続時まで完了シグネチャ集合の計算および検査が遅延されていた。

クラステンプレート特殊化型判定 @ C++26

プログラミング言語C++において、与えられた型があるクラステンプレートの特殊化(specialization)か否かを判定する方法。id:yohhoy:20220122C++2c(C++26) Reflection(→id:yohhoy:20250305)バージョン。自明な実装で面白みはない...

// C++2c(C++26)
#include <atomic>
#include <array>
#include <complex>
#include <meta>

// rはプライマリテンプレートprimaryの特殊化?
consteval bool is_specialization_of(std::meta::info r, std::meta::info primary)
{
  return std::meta::has_template_arguments(r)
      && std::meta::template_of(r) == primary;
}

consteval bool is_complex(std::meta::info r)
  { return is_specialization_of(r, ^^std::complex); }

consteval bool is_stdarray(std::meta::info r)
  { return is_specialization_of(r, ^^std::array);}

consteval bool is_atomic(std::meta::info r)
  { return is_specialization_of(r, ^^std::atomic); }

static_assert( is_complex(^^std::complex<float>) );
static_assert( !is_complex(^^double) );

static_assert( is_stdarray(^^std::array<int, 5>) );
static_assert( !is_stdarray(^^int[5]) );

static_assert( is_atomic(^^std::atomic<int>) );
static_assert( !is_atomic(^^int) );

ノート:ADLを利用して単にhas_template_arguments(r), template_of(r)とも記述できる。言語組込み配列型(T[N])を判定したい場合は、標準ヘッダ<meta>にてリフレクション・メタ関数std::meta::is_array_type(r)が提供される。

関連URL

constexpr Two-Step

C++20からサポートされたコンパイル時動的確保データを、コンパイル時定数としてプログラム実行時へと効率的に持ち越すテクニック。

C++23現在の言語仕様では、コンパイルフェーズで動的確保(new)されたデータ領域はコンパイルフェーズにおいて解放(delete)されなければならない(“transient constexpr allocation”)。この制約によりstd::string*1std::vector型の定数初期化はコンパイルエラー(ill-formed)となる。データ領域の動的確保/解放を伴わないstd::array型ではプログラマの期待通りに定数初期化が行われる。

#include <array>
#include <string>
#include <vector>

constexpr std::string get_str()
{ return std::string(42, 'X'); }

constexpr std::vector<int> get_vec()
{ return {1, 2, 3}; }

constexpr std::array<int, 3> get_arr()
{ return {1, 2, 3}; }

static_assert(get_str() != "C++");  // OK
static_assert(get_vec()[0] == 1);   // OK
static_assert(get_arr()[0] == 1);   // OK

constexpr std::string str = get_str();  // NG: ill-formed
constexpr std::vector vec = get_vec();  // NG: ill-formed
constexpr std::array arr = get_arr();   // OK

前掲コードのうちstd::arrayの例が示す通り、コンパイル時に静的確保された領域ならば実行時へと値を持ち越せる。つまりコンパイル時に動的確保したデータを十分な要素サイズもつ静的領域へコピーしておき、後者を実行時に持ち越す実装方式が考えられる。同方式のデメリットとして必要な静的領域サイズを事前計算はできず、大きめの領域を確保する必要があるためにメモリ使用効率が犠牲になってしまう。

C++20/23: constexpr Two-Step技法

コンパイル時から実行時に持ち越す静的領域サイズを必要最小限とするため、0) コンパイル時に動的確保データを構築し、1) まず十分に大きい静的領域へとコピーしてから、2) 必要十分サイズの静的領域へとコピーする(“constexpr Two-Step”)。最後にconsteval関数テンプレートの非型テンプレートパラメータ*2に指定することで、静的領域を実行時にも参照可能とする。

#include <algorithm>
#include <array>
#include <string>
#include <string_view>

// コンパイル時にstd::stringを構築する関数
constexpr std::string my_data();

template <size_t N>
struct Storage {
  std::array<char, N> contents = {};
  size_t length = 0;

  template <class R>
  constexpr Storage(R&& r)
    : length(std::ranges::size(r))
  {
    std::ranges::copy(r, contents.data());
  }

  constexpr auto begin() const -> char const*
    { return contents.data(); }
  constexpr auto end() const -> char const*
    { return contents.data() + length; }
};

template <Storage V>
consteval auto promoted_value() {
  return std::string_view(V);  // C++23
  // C++20: std::string_view(V.begin(), V.end());
}

template <auto F>
constexpr std::string_view promote_to_static() {
  constexpr auto oversized_storage = Storage<255>(F());  // ★
  constexpr auto correctly_sized_storage =
    Storage<oversized_storage.length>(oversized_storage);
  return promoted_value<correctly_sized_storage>();
}

constexpr std::string_view s = promote_to_static<[]{ return my_data(); }>();

上記コード★箇所のバッファサイズ255は、コンパイル時に計算される文字列長よりも十分大きな値であればよい。バッファサイズ不足の場合はコンパイルエラーとして検出される。

C++2c Reflection

C++ Reflection(→id:yohhoy:20250305)の一環としてC++2c(C++26)へ採択*3された提案文書P3491R3では、コンパイル時の確保領域をプログラム実行時に参照可能な領域へと昇格させる関数群を追加する。これによりC++2c以降ではconstexpr Two-Step技法は不要となる。

// C++2c(C++26)
#include <meta>  // Reflection(P2996,P3491,...)

constexpr std::string my_data();

constexpr std::string_view s = std::define_static_string(my_data());  // OK

新しい標準ヘッダ<meta>に追加される関数*4

std::define_static_string
コンパイル時の文字型(CharT)Rangeから生成した文字列リテラルへのポインタ(const CharT*)を返すconsteval関数*5
std::define_static_array
コンパイル時の要素型(T)Rangeから生成した静的配列への参照(std::span<const T>)を返すconsteval関数
std::define_static_object
コンパイル時の構造的型(structural type)Tオブジェクトから生成した静的オブジェクトへのポインタ(const T*)を返すconsteval関数

2025-07-28追記:P3491で導入される上記の関数群は、P3617R0で導入されるより原始的なstd::meta::reflect_constant_{string,array}を用いて定義される。

提案文書P3491によれば、将来的にコンパイル時動的確保データを実行時へ直接持ち越し可能とする("Non-transient constexpr allocation”)仕様拡張や、std::stringstd::vectorを非型テンプレートパラメータ(non-type template parameter)指定可能に制約緩和されるまでの過渡的ソリューションと位置付けている*6。下記提案がC++2d(C++29)向けて検討中ステータスにある。

関連URL

*1:例示コードのうち get_str 関数で構築する std::string 文字列長が十分短いときはSSO(→id:yohhoy:20120428)が行われ、副次効果としてコンパイル時に構築した文字列を実行時に持ち越せるケースがある。SSO判定閾値C++標準ライブラリの実装依存となるため、この動作仕様に依存するプログラムは可搬性が損なわれる。

*2:採択済み提案文書(PDF)P2841R7により、C++2c仕様では旧来の "non-type template parameter" は "constant template parameter" へと名称変更される。https://github.com/cplusplus/draft/pull/7587

*3:https://github.com/cplusplus/papers/issues/2158

*4:P3491は文字型ポインタが文字列リテラルを指す否かを判定する std::is_string_literal 関数も追加するが、本文趣旨と無関係なためここでは省略。

*5:std::define_static_string 関数が生成する文字列リテラルはNUL文字終端されることを示すため、戻り値型が std::string_view ではなく const CharT* と定義されている。

*6:P3491: §1 "So having facilities to solve these problems until the general language solution arises is very valuable.", §3.5 "Given non-transient allocation and a std::string and std::vector that are usable as non-type template parameters, this paper likely becomes unnecessary."

*7:https://github.com/cplusplus/papers/issues/867

*8:https://github.com/cplusplus/papers/issues/1336

*9:https://github.com/cplusplus/papers/issues/2037