yohhoyの日記

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

C++26 Executionライブラリ:Cancellable Sender

C++2c(C++26)実行制御ライブラリ(execution control library)(→id:yohhoy:20250609)において、キャンセル可能なSenderの実装方法。

実行制御ライブラリにおける非同期操作のキャンセルは、Receiver側で発行された停止要求(stop request)をSender側が明示的に確認する協調的キャンセル(cooperative cancellation)によって実現する。Senderにおいて明示的に対応しない限りは、同Senderによる非同期操作は停止要求を無視して継続処理される。停止要求の発行/確認を行うクラス群は<stop_token>ヘッダで提供される。*1

関数オブジェクトfを引数にとり、下記動作を行うキャンセル可能なSenderMySender{f}の実装例:

  • 下流側で停止要求が行われていた場合、何もせず停止完了を送信する。
  • 関数呼び出し式f()が正常終了(return)したとき、戻り値を値送信する。
  • 関数呼び出し式f()が例外終了(throw)したとき、例外をエラー送信する。
// C++2c
#include <exception>
#include <stop_token>
#include <type_traits>
#include <execution>
namespace exec = std::execution;

// MySender接続結果Operation State
template<typename Rcvr, typename F>
struct OpState {
  using operation_state_concept = exec::operation_state_t;

  // exec::start(op)カスタマイズ
  void start() & noexcept {
    // 停止トークンをReceiver環境から取得
    auto st = std::get_stop_token(exec::get_env(rcvr_));
    if (st. stop_requested()) {
      // 停止要求が存在する場合は停止完了を送信
      exec::set_stopped(std::move(rcvr_));
      return;
    }
    try {
      // 関数呼び出しf()の戻り値で値完了
      exec::set_value(std::move(rcvr_), f_());
    } catch (...) {
      // 送出された例外でエラー完了
      exec::set_error(std::move(rcvr_), std::current_exception());
    }
  }

  Rcvr rcvr_;
  F f_;
};

template<typename F>
struct MySender {
  using sender_concept = exec::sender_t;
  using result_type = std::invoke_result_t<F>;

  // MySenderの完了シグネチャ集合を宣言
  using completion_signatures = exec::completion_signatures<
    exec::set_value_t(result_type),
    exec::set_error_t(std::exception_ptr),
    exec::set_stopped_t()
  >;

  template <class Self>  // P3557R3
  static consteval auto get_completion_signatures()
    { return completion_signatures{}; }

  // exec::connect(sndr, rcvr)カスタマイズ
  template<typename Rcvr>
  auto connect(Rcvr rcvr) {
    return OpState{std::move(rcvr), std::move(f_)};
  }

  F f_;
};

2025-08-20追記:P3557R3採択によってSender完了シグネチャ集合のカスタマイズ方式が当初提案P2300R10から変更され、メンバ型completion_signaturesではなく静的メンバ関数テンプレートget_completion_signaturesを定義する必要がある。→ id:yohhoy:20250823

C++標準ライブラリ提供のSenderアダプタ(→id:yohhoy:20250612)から生成されるSenderは、Receiver側での停止要求を無視するため協調的キャンセルを行わない。ただし、上流側Senderからの停止トークン取得問い合わせstd::get_stop_token(→id:yohhoy:20250609)を下流側Receiverへと転送する動作仕様となっており、タスクチェイン全体での非同期操作キャンセルを阻害しないよう設計されている。*2

例外として、全入力Sender完了を待機するexecution::when_allSenderアダプタでは、下流側Receiverでの停止要求検知もしくは入力Senderからのエラー完了受信をトリガとして停止要求を発行し、他の入力Senderに対して協調的キャンセルを促す。*3

// タスクチェインを構築
exec::sender auto snd1 = MySender{[] -> int { return 1; }};
exec::sender auto snd2 = MySender{[] -> int { throw 42; }};
exec::sender auto snd3 = MySender{[] -> int { return 3; }};
exec::sender auto sndr = exec::when_all(snd1, snd2, snd3);

try {
  auto tup = std::this_thread::sync_wait(sndr).value();
  // 1) snd1は戻り値1を値送信
  // 2) snd2は例外42をエラー送信
  // 3) when_all内部で停止要求を発行
  // 4) snd3で停止要求を検知し停止完了を送信
  // 5) sndr自体は例外42でエラー完了
  std::println("val={}", tup);
} catch (int e) {
  std::println("catch {}", e);
}
// "catch 42"

2025-07-22追記:P3284R4にてSenderアダプタexecution::unstoppableが追加される。上流側Senderからのstd::get_stop_token問い合わせに対してstd::never_stop_tokenを返すようReceiver環境を書き換えることで、タスクチェイン下流側からの非同期操作キャンセル伝搬を遮断する。上流側からの停止完了送信には影響しない。

自作Senderアダプタにおいて独自基準で非同期操作キャンセルを追加サポートする場合、下流側Receiver環境から取得した停止トークン(stop token)を確認する以外に下記対応が必要となる。

  • Operation Stateがstd::inplace_stop_source型の停止ソース(stop source)を内包する。*4
  • 下流側Receiver環境から取得した停止トークンに対して、自身の停止ソースへ停止要求を発行(request_stop)する停止コールバック(stop callback)を登録する。
  • 上流側Senderと接続されるReceiverの環境は、std::get_stop_token問い合わせに対して停止ソースから取得した停止トークンを返す。

複雑なキャンセルサポートの実装例として、提案文書P2300R10, §4.1 Asynchronous Windows socket recvなども参照のこと。

関連URL

*1:<stop_token>ヘッダはC++20標準ライブラリで協調的な中断処理をサポートする std::jthread クラスと同時に導入された。実行制御ライブラリの提案文書 P2300R10 では、同ヘッダに std::inplace_stop_{token,source} クラスが追加される。

*2:Senderアダプタ間で対象クエリオブジェクトによる問い合わせを転送するか否かは、別のクエリオブジェクト std::forwarding_query への問い合わせ結果(bool値)によって制御される。

*3:厳密には、下流側Receiverでの停止要求発行もしくは上流側Senderからのエラー完了/停止完了いずれかの初回受信タイミングで停止要求を発行する。いずれかの入力Senderがエラー完了した場合、初回受信したエラー完了値が when_all のエラー完了値となる。入力Senderが値完了/停止完了のいずれかであれば、when_all は停止完了を送信する。

*4:C++20から存在する std::stop_source でも問題ないが、動的メモリ確保が発生しない std::inplace_stop_source の方が好ましい。C++標準ライブラリ execution::when_all Senderアダプタの仕様定義でも std::inplace_stop_source が利用されている。

C++26 Executionライブラリ(コルーチン相互運用編)

C++2c(C++26)標準ライブラリに追加される実行制御ライブラリ(execution control library)についてメモ。別名:std::execution, Senders/Receivers(S/R)

2025年5月現在、ベースライン提案文書P2300R10までを採択済み。
2025-07-20追記:2025年6月会合にて関連する一連の提案文書P2079R10,
P3149R11, P3284R4, (PDF)P3433R1, P3481R5, P3552R3, P3557R3, P3570R2, (PDF)P3682R0が採択された。

コルーチンAwaitable==Sender

C++コルーチンにおいてco_await演算子オペランドに指定可能なAwaitableオブジェクトは、実行制御ライブラリのSenderとしてReceiverと接続(connect)できる。2025年5月現在はそのような標準Awaitable型は提案文書P3552にて検討段階にあり、他プログラミング言語における非同期処理の結果型(例:Task, Promise, Futureなど)に相当する。
2025-07-20追記:P3552R3にてSender互換コルーチン戻り値型として利用する非同期タスクexecution::task<T>クラスが追加された。同クラスはコルーチンAwaitableとして定義されるものの、直接的にSenderへアダプト(メンバ型sender_concept定義)するため下記機構は利用されない。

Awaitableオブジェクトの取り扱いは接続操作execution::connectCPOにて仕様規定されるため、Awaitable型を定義するコルーチンライブラリ実装側での対処は必要ない。(コルーチン側でSenderを利用したい場合は、コルーチンライブラリ実装側にも後述対応が必要。)

co_await Awaitableの戻り値型Tのとき、コルーチンAwaitableと接続するReceiverは下記の完了シグネチャを持つ必要がある。

  • 値完了:set_value_t(T)
  • エラー完了:set_error_t(std::exception_ptr)
  • 停止完了:set_stopped_t()
// C++2c
#include <coroutines>
#include <execution>
namespace exec = std::execution;

// Awaitableコルーチン戻り値型
template<typename T>
struct Lazy {
  struct promise_type;
  //...
  // co_await演算子オーバーロードを定義
  auto operator co_await() noexcept;
  // 式co_await Lazy<T>はT型の値を返す
}

// 自作Receiver型
struct DumpReceiver {
  using receiver_concept = exec::receiver_t;

  // 値完了     set_value_t(int)
  void set_value(int) noexcept;
  // エラー完了 set_error_t(exception_ptr)
  void set_error(std::exception_ptr) noexcept;
  // 停止完了   set_stopped_t()
  void set_stopped() noexcept;
};

// Awaitableオブジェクトを返すコルーチン(関数)
Lasy<int> async_work();

// Awaitableオブジェクトはsenderコンセプトを満たす
exec::sender auto task = async_work();  // OK

// Awaitable(Sender)とRecevierを接続
exec::receiver auto rcvr = DumpReceiver{};
auto op = exec::connect(task, rcvr);  // OK
exec::start(op);

Sender Awaitableコルーチン

C++コルーチンのPromise型が特定インタフェース要件を満たすとき、コルーチン内部においてSenderオブジェクトに対して式co_await sndrでAwait可能となる。実行制御ライブラリと相互運用可能なコルーチンライブラリ実装者向けに、Promise型Pの実装ヘルパとして基底クラスexecution::with_awaitable_senders<P>が提供される。
2025-07-20追記:execution::task<T>を戻り値型とするコールチンはSender Awaitableコルーチンとなる。[P3552R3]

Sender Awaitableコルーチン内部でSenderオブジェクトをco_await演算子に指定するとexecution::as_awaitableCPOが呼び出され、Senderとコルーチン接続用のAwaitableオブジェクトに自動変換される。同Awaitableオブジェクトはコルーチンを中断(suspend)してSenderを開始(start)し、非同期操作が完了すると送信値をco_await式の値としてコルーチンを再開(resume)する。非同期操作がエラー完了したときは例外送出が行われ、停止完了の既定動作としてプログラム異常終了(std::terminate())する。

co_await演算子オペランドに指定可能なSenderの制約として、値完了シグネチャを1種類だけ持つことが要請される。複数の値完了シグネチャを持つSenderの場合は、Senderアダプタexecution::into_variant等を適用する必要がある。

  • 2025-07-20追記:Senderがクエリオブジェクトexecution::get_await_completion_adaptorに対応していれば、co_await演算子の適用時に単一の値完了シグネチャへと自動変換される。この仕組みにより、Receiver接続時およびコルーチン利用時それぞれで最適なインタフェースを提供できる。[P3570R2]
// C++2c
#include <coroutines>
#include <execution>
namespace exec = std::execution;

// Sender Awaitableコルーチン戻り値型
template<typename T>
struct Lazy {
  struct promise_type : exec::with_awaitable_senders<promise_type> {
    void return_value(T);
    //...
  };
  // コルーチンco_return戻り値を取得するメンバ関数
  T get();
};

// Sender Awaitableコルーチン
Lazy<int> coro(int n)
{
  // n*3を計算するSenderチェイン構築
  exec::sender auto sndr =
    exec::just(n)
    | exec::then([](int m){ return m * 3; });

  // Senderを開始して値取得を待機
  int val = co_await sndr;  // OK

  co_return val * 7;
}

auto task = coro(2);
int value = task.get();  // 42==2*3*7

Sender Awaitableコルーチン動作のカスタマイゼーションポイントとして下記が定義される。

  • co_await sndr
    • execution::as_awaitableCPO経由のas_awaitableメンバ関数
    • co_awaitオペランド指定専用のSender型変換として、Sender属性へのexecution::get_await_completion_adaptor問い合わせ[P3570R2]
  • 停止完了ハンドラ:コルーチンPromsie型unhandled_stoppedメンバ関数
  • 停止完了の委譲:execution::with_awaitable_senders<P>::set_continuationメンバ関数によるコルーチンハンドラ設定

メモ:難解なC++20コルーチン仕様 × 複雑なC++2c実行制御ライブラリの相乗効果で気分は宇宙猫 (゚ω゚)


関連URL

C++26 Executionライブラリ(Senderアルゴリズム編)

C++2c(C++26)標準ライブラリに追加される実行制御ライブラリ(execution control library)についてメモ。別名:std::execution, Senders/Receivers(S/R)

2025年5月現在、ベースライン提案文書P2300R10までを採択済み。
2025-07-20追記:2025年6月会合にて関連する一連の提案文書P2079R10,
P3149R11, P3284R4, (PDF)P3433R1, P3481R5, P3552R3, P3557R3, P3570R2, (PDF)P3682R0が採択された。

Senderアルゴリズム

Senderアルゴリズム(sender algorithm)はSenderを引数/戻り値とする関数であり、 下記3つのカテゴリに区分される。C++2c標準ライブラリでは、Senderアルゴリズムは全てCPO(Customization Point Object)として定義される。

区分 引数 戻り値
Senderファクトリ
(sender factory)
任意個の引数
(非Sender)
Sender execution::
just
Senderアダプタ
(sender adaptor)
1個以上のSender
+任意個の引数
Sender execution::
then
Senderコンシューマ
(sender consumer)
1個以上のSender
+任意個の引数
任意戻り値
(非Sender)
this_thread::
sync_wait
パイプライン記法

一部のSenderアダプタは|演算子によるパイプライン記法をサポートする。(標準RangesライブラリのRangeアダプタと同様)

#include <execution>
namespace exec = std::execution;

auto calc(int, int) -> int { /*...*/ }

// 関数呼び出し
exec::sender auto snd0 = exec::just(2, 3);
exec::sender auto snd1 = exec::then(snd0, calc);
// パイプライン記法
exec::sender auto sndr = exec::just(2, 3) | exec::then(calc);

パイプライン記法をサポートするSenderアダプタ(例:then)はパイプ可能Senderアダプタオブジェクト(pipeable sender adaptor object)と定義され、第1引数(Sender)以外を束縛したオブジェクト(例:then(calc))はパイプ可能Senderアダプタクロージャオブジェクト(pipeable sender adaptor closure object)となる。後者は|演算子の右オペランドや、Senderアダプタexecution::onの引数*1としてSenderチェイン構築時に利用される。

非同期処理フレームワークを拡張するライブラリ実装者向けに、パイプ可能Senderアダプタクロージャオブジェクト型Dの実装ヘルパとして基底クラスexecution::sender_adaptor_closure<D>が提供され、独自Senderアダプタのパイプライン記法対応がサポートされる。*2

Senderチェイン

SenderアダプタやSenderコンシューマに指定した入力Senderは生成Senderオブジェクトのメンバ変数として保持されるため、ライブラリ仕様では子(child)Senderと呼ばれる。同様に、Senderアルゴリズムが生成するSenderオブジェクトは親(parent)Senderと呼ばれる。

  • 子Sender = Senderチェインの上流側
  • 親Sender = Senderチェインの下流
snd1() | snd2() | snd3()  // パイプライン記法
snd3(snd2(snd1()))        // 関数呼び出し
// 生成Senderオブジェクトは下記の入れ子構造
snd3{ snd2 { snd1{} } }

Senderアルゴリズムが生成する親Senderは子Senderの接続先となるため、Senderオブジェクト内部ではReceiverインタフェースを定義する。Sender-Receiver間の接続(connect)はSenderコンシューマ適用時にSenderチェイン上で親→子の順序で実行され、Sender同様に入れ子構造をとるOperation Stateを生成する。親Operation Stateに対して開始(start)操作を行うと子Operation State=Senderチェイン上流側の処理が開始(start)され、以降は子Operation Stateが親SenderのReceiver完了操作を呼び出すことで上流→下流の順に非同期操作全体が実行される。

std::this_thread::sync_wait( exec::just(2, 3) | exec::then(calc) );
// Sender構造: sync_wait{ then{ just{ {}, {2, 3} }, calc } }
  • 1) sync_waitコンシューマ内部で、子Sender-親SenderのReceiver間を接続(connect)する。
  • 2) sync_waitコンシューマ内部で、Operation Stateを開始(start)する。
  • 3) justのOperation Stateが、送信値2, 3thenのReceiverハンドラを呼び出す。
  • 4) thenのReceiverハンドラ受信値2, 3calcに適用し、結果値でsync_waitのReceiverハンドラを呼び出す。
  • 5) sync_waitのReceiver受信値を戻り値として返す。

Senderファクトリ

名前空間std::executionにて下記Senderファクトリが提供される。

ファクトリ 呼び出し形式 概要
just (values...) 値完了を送信
just_error (err) エラー完了を送信
just_stopped () 停止完了を送信
schedule (sch) Scheduler上で非同期処理を開始
read_env (q) 接続先Receiver環境への問合せ

ノート:

  • schedule:指定Schedulerschを値完了Scheduler(value completion scheduler)として、空の値を送信する。
  • read_env:指定クエリオブジェクトqを用いて、非同期操作の開始(start)時に後続Receiver環境へ問い合わせて得られた結果を値送信する。Senderオブジェクト構築時やReceiver接続(connect)時には何もしない。

Senderアダプタ

名前空間std::executionにて下記Senderアダプタが提供される。※列はパイプライン記法の対応可否を表す。

アダプタ 呼び出し形式 概要
write_env (sndr, env) Receiver環境を上書き[P3284R4]
unstoppable (sndr) 下流側の停止要求を無視[P3284R4]
start_on (sch, sndr) Scheduler上でSender開始
continues_on (sndr, sch) Scheduler上で後続継続
on (sch, sndr) 指定Senderのみ別Scheduler上で実行
on (sndr, sch, closure) 指定非同期操作のみ別Scheduler上で実行
schedule_from (sch, sndr) continues_on実装補助*3
then (sndr, f) 値完了の後続処理アタッチ
upon_error (sndr, f) エラー完了の後続処理アタッチ
upon_stopped (sndr, f) 正常完了の後続処理アタッチ
let_value (sndr, fn) 値完了をネスト非同期操作へ変換
let_error (sndr, fn) エラー完了をネスト非同期操作へ変換
let_stopped (sndr, fn) 停止完了をネスト非同期操作へ変換
bulk (sndr, policy, n, f) 指定関数を複数回実行*4
bulk
_chunked
(sndr, policy, n, f) 指定関数をグループ単位で複数回実行[P3481R5]
bulk
_unchunked
(sndr, policy, n, f) 指定関数を個別に複数回実行[P3481R5]
split (sndr) P3682R0で削除*5
when_all (sndrs...) 全Sender完了待機+値結合
when_all
_with_variant
(sndrs...) 全Sender完了待機+値結合
複数値完了シグネチャ対応版
into_variant (sndr) 複数値完了シグネチャvariant型に統合
stopped_as
_optional
(sndr) 停止完了をoptional値完了に変換
stopped_as
_error
(sndr, err) 停止完了をエラー完了に変換
associate (sndr, token) 非同期スコープに関連付け[P3149R11]
spawn_future (sndr, token, env?) 非同期スコープでSender早期開始[P3149R11]*6
affine_on (sndr, sch) コルーチンでのSchedulerアフィニティ指定[P3552R3]

ノート:

  • then/upon_{error,stopped}:受信値args...に対して、f(args...)戻り値を値完了で送信する。
  • let_{value,error,stopped}:受信値args...に対して、fn(args...)戻り値Senderに後続Receiverを接続する。
  • bulk:受信値values...に対して、範囲[0, n)の各idxに対してf(idx, values...)が逐次的に呼び出される。
    • 2025-07-20追記:bulk動作はbulk_chunkedに移譲される。既定動作はP2300R10定義と同様に逐次実行。[P3481R5]
  • bulk_chunked:既定動作はbulkと同様に逐次実行。並列Scheduler上かつ実行ポリシーpolicyで並列処理を許可(execution::par)した場合、範囲[0, n)を分割した重複しない部分区間[begin, end)に対して複数スレッド上でf(begin, end, values...)が並列実行される。[P2079R10, P3481R5]
  • bulk_unchunked:既定動作はbulkと同様に逐次実行。並列Scheduler上かつ実行ポリシーpolicyで並列処理を許可(execution::par)した場合、範囲[0, n)の各idxに対して複数スレッド上でf(idx, values...)が並列実行される。[P2079R10, P3481R5]
  • when_all:入力Sender#nからの受信値vs_n...に対して、tuple<Vs_0..., ..., Vs_k...>型に統合して送信する。いずれかの入力Senderからエラー完了/停止完了を受信した場合、他の入力Senderに対して停止要求を行う。*7
  • when_all_with_variantwhen_all(into_variant(sndrs)...)相当。
  • into_variant:入力Senderから受信可能性のある値vs_m...に対して、variant<tuple<Vs_0...>, ..., tuple<Vs_j...>>型に統合して送信する。
  • spawn_future:任意の値完了シグネチャset_value_t(Vs...)をもつ入力Senderを非同期スコープトークtokenに関連付けて、非同期操作を即座に開始(start)する。[P3149R11] → id:yohhoy:20251002

Senderコンシューマ

名前空間std::this_threadにて下記Senderコンシューマが提供される。

コンシューマ 呼び出し形式 概要
sync_wait (sndr) Sender完了待機し結果取得
sync_wait
_with_variant
(sndr) Sender完了待機し結果取得
複数値完了シグネチャ対応版

名前空間std::executionにて下記Senderコンシューマが提供される。

コンシューマ 呼び出し形式 概要
spawn (sndr, token, env?) 非同期スコープでSender早期開始[P3149R11]

ノート:

  • sync_wait:値完了のときoptional<tuple<Vs...>>値を、停止完了のとき無効値(nullopt)を返す。エラー完了のときは例外送出する。
  • sync_wait_with_variant:ほぼsync_wait(into_variant(sndr))相当。*8
  • spawn:空の値完了シグネチャset_value_t()をもつ入力Senderを非同期スコープトークtokenに紐づけて、非同期操作を即座に開始(start)する。[P3149R11] → id:yohhoy:20251002
#include <execution>
namespace exec = std::execution;

// 値(123, 'X')を送信
exec::sender auto sndr = exec::just(123, 'X');
try {
  auto result = std::this_thread::sync_wait(sndr);
  if (result) {
    // 値完了
    auto [n, c] = *result;
    // n := int型/123, c := char型/'X'
  } else {
    // 停止完了
  }
} catch (...) {
  // エラー完了
}

実行ドメインとカスタマイゼーションポイント

サードパーティ・ライブラリによる柔軟な機能拡張を可能とするため、実行ドメイン(execution domain)を介したSenderアルゴリズムのカスタマイゼーションポイントを定義する。独自実行ドメインに関連付けた独自Senderと標準Senderアルゴリズムを組み合わせるとき、標準Senderアルゴリズムの動作セマンティクスを実現する独自のSender型へと変換することができる。標準ライブラリではデフォルト実行ドメイン(execution::default_domain)のみ定義し、標準Senderアルゴリズムは暗黙に同ドメインと関連付けられる。

カスタマイゼーションポイント実装側でSenderアルゴリズムを識別するため、大半の標準Senderアルゴリズムは同名タグ型(例:justアルゴリズムはSenderタグ型just_t)を持つ*9。また標準Senderアルゴリズムで作成されるSenderオブジェクトは構造化束縛(structured binding)に対応しており、カスタマイゼーションポイント実装側でSenderタグ型、オブジェクト構築時引数、子Senderを容易に取り出せる仕様となっている。

標準Senderアルゴリズムは下記CPO呼び出しを介したカスタマイゼーションポイントを規定する。一部の標準Senderアルゴリズム仕様では、これらのカスタマイゼーションポイントとデフォルト実行ドメイン動作に依存した仕様記述を行う。(動作を追うのが、とてもつらい。)

  • execution::transform_sender:Senderアルゴリズム呼び出しによるSenderオブジェクト構築時、および後続Receiverへの接続(connect)時のSender型変換*10。既定動作は無変換、一部SenderアルゴリズムではSenderタグ型へ処理ディスパッチ。
  • execution::transform_env:後続Receiver接続(connect)時の環境(environment)変換。既定動作は無変換。
  • execution::apply_sender:Senderコンシューマ適用時の動作。既定動作はSenderタグ型へ処理ディスパッチ。

例:starts_onアルゴリズムは後続Receiver接続(connect)時にschedulelet_valueの組合せへと変換される。以下は簡略化したstarts_on.transform_senderカスタマイゼーションポイント実装。

// 呼び出し式 execution::starts_on(sch, sndr)

// out_sndr := starts_onアルゴリズムで生成されるSenderオブジェクト
// OutSndr  := 上記Senderの型
auto&& [_, sch, sndr] = out_sndr;
// sch  := 非同期処理を実行するScheduler
// sndr := 実行させる非同期処理(Sender)
return let_value(
  schedule(sch),
  [sndr = std::forward_like<OutSndr>(sndr)]() mutable {
    return std::move(sndr);
  });


関連URL

*1:Senderアダプタ execution::on は2種類の呼び出し形式をサポートしており、パイプ可能Senderアダプタクロージャオブジェクトは on(sndr, sch, closure) 形式に対して指定する。

*2:C++23標準Rangesライブラリでも類似の ranges::range_adaptor_closure<D> が提供され、独自Rangeアダプタ実装者向けのパイプライン記法サポートが存在する。

*3:P2300R10: "schedule_from is not meant to be used in user code; it is used in the implementation of continues_on."

*4:2025-07-20追記:P3481R5にてbulkアルゴリズムCPO引数リストが (sndr, n, f) から (sndr, policy, n, f) へと変更された。P2079R10で導入される並列Schedulerと組み合わせてスレッド並列を実現する。

*5:P3682R0: "Deficiencies of std::execution::split, Dynamic Allocation, Shared Ownership, Eagerness, Naming"

*6:P3149R11, §6.4: "To keep consistency with spawn() this paper doesn’t support pipe operator for spawn_future()."

*7:各Senderが停止要求を認識し、早期に停止完了を送信するか否かはSenderの実装に依存する。

*8:sync_wait_with_variantアルゴリズムの戻り値型は optional<variant<tuple<...>...>> となる。単純に sync_wait( into_variant(sndr) ) とすると、冗長なtuple層が追加された optional<tuple<variant<tuple<...>...>>> 型となってしまう。

*9:read_envアルゴリズムのみ例外的にSenderタグ型を規定しない(unspecified)。2025-07-20追記:write_env, unstoppableも同様にSenderタグ型を規定しない[P3284R4]。

*10:同一関数 transform_sender の呼び出し引数リストの差異により、2つのタイミングのカスタマイゼーションポイントを定義する。Senderオブジェクト構築時はSenderのみが、Receiver接続時はSenderと環境が実引数に渡される。

C++26 Executionライブラリ(基礎編)

C++2c(C++26)標準ライブラリに追加される実行制御ライブラリ(execution control library)についてメモ。別名:std::execution, Senders/Receivers(S/R)

2025年5月現在、ベースライン提案文書P2300R10までを採択済み。
2025-07-20追記:2025年6月会合にて関連する一連の提案文書P2079R10,
P3149R11, P3284R4, (PDF)P3433R1, P3481R5, P3552R3, P3557R3, P3570R2, (PDF)P3682R0が採択された。

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

// タスクチェインを構成
exec::sender auto work =
  exec::just(2, 3)
  | exec::then([](int a, int b) {
      return a * b * 7;
    });
// 呼び出しスレッド上で同期的にタスク実行
auto result = std::this_thread::sync_wait(work);
// 結果値を取り出す
auto [value] = result.value();
// value == int型の値42

CPU並列処理を実現するスレッドプール機構や、コルーチン非同期タスク型といった応用機能は検討進行中GPU/CUDA並列処理のようなベンダ拡張機能では、非同期処理フレームワークに則ったサードパーティ実装が提供される。*2

  • 2025-07-20追記:システムスレッドプールのSchedulerを返すexecution::get_parallel_scheduler関数[P2079R10]と連携してCPU並列処理を行うSenderアダプタexecution::bulkファミリ[P3481R5]、Sender互換コルーチン戻り値型として利用する非同期タスクexecution::task<T>クラス[P3552R3]が採択済み。

実行制御ライブラリ

C++2c実行制御ライブラリでは、非同期(asynchronous)・並行(concurrent)/並列(parallel)処理を統合的に扱うフレームワークを規定する。ライブラリ仕様は非同期処理を実現する枠組みとして記述され、並行/並列処理は具体的なScheduler実装によって実現される。(例:CPUスレッドプール、GPGPU/CUDA処理をサポートするScheduler)

  • <execution>ヘッダ(C++17追加ヘッダを拡張)
  • 基本コンセプト
    • execution::sender
    • execution::receiver
    • execution::operation_state
    • execution::scheduler
  • 基本操作(CPOとして定義)
    • execution::connect:(sender, receiver) -> operation_state
    • execution::start:(operation_state) -> void
    • execution::schedule:(scheduler) -> sender
    • execution::set_value:(receiver, values...) -> void
    • execution::set_error:(receiver, err) -> void
    • execution::set_stopped:(receiver) -> void
  • プロパティ問い合わせ(queries)
    • 標準クエリオブジェクト(CPOとして定義)
    • クエリ可能オブジェクト == Sender属性, Receiver環境, Scheduler
    • クエリ可能オブジェクトの実装ヘルパ型execution::env, execution::prop
  • Senderアルゴリズム(CPOとして定義)→ id:yohhoy:20250612
    • Senderファクトリ:(args...) -> sender
    • Senderアダプタ:(sender, args...) -> sender
    • Senderコンシューマ:(sender) -> result
    • 実行ドメイン == Senderアルゴリズムのカスタマイズ機構
    • Senderアルゴリズムの実装ヘルパ型execution::sender_adaptor_closure
  • C++コルーチンサポート → id:yohhoy:20250613
    • 任意のコルーチンAwaitable型をSenderとして扱える(execution::connectCPO内部動作)
    • Senderをコルーチン内で利用可能にする実装ヘルパ型execution::with_awaitable_senders

基本コンセプト: Sender, Receiver, Operation State

  • 非同期操作(asynchronous operation)
    • 明示的に作成され、1回だけ明示的に開始(start)でき、最終的に3種類いずれかの完了(completion)に到達する。
    • 値完了(value completion):非同期操作の “成功”。0個以上の任意型の値。
    • エラー完了(error completion):非同期操作の “失敗”。1個の任意型のエラー値。
    • 停止完了(stopped completion):非同期操作の “キャンセル”。付帯情報なし。
  • Operation State:execution::operation_state concept
    • 非同期操作に関連付けられる状態オブジェクト。
    • Operation Stateはコピー/ムーブともに不可。*3
    • メンバ型operation_state_conceptでタグ型operation_state_tを宣言。
    • 開始操作execution::startCPOから呼ばれるstartメンバ関数を定義。
  • Sender:execution::sender concept
    • 1つ以上の非同期操作のファクトリ。
    • メンバ型sender_conceptでタグ型sender_tを宣言。
      • 2025-07-20追記:LWG4202にてenable_sender変数テンプレート特殊化を用いて非侵襲的に任意の型をSenderにアダプト可能となる。
    • SenderとReceiverとを接続(connect)してOperation Stateを生成。
    • 接続操作execution::connectCPOから呼ばれるconnectメンバ関数を定義。
  • Receiver:execution::sender concept
    • 非同期操作の値完了/エラー完了/停止完了を受け取るハンドラの集合体。
    • メンバ型receiver_conceptでタグ型receiver_tを宣言。
    • 完了操作execution::set_{value,error,stopped}CPOから呼ばれる完了ハンドラset_{value,error,stopped}メンバ関数を定義。
  • 完了シグネチャ(completion signature) == 完了ハンドラの型情報*4
    • 値完了:execution::set_value_t(Values...)
    • エラー完了:execution::set_error_t(Err)
    • 停止完了:execution::set_stopped_t()
    • 完了シグネチャ集合をexecution::completion_signatures<Sigs...>で表現。
    • Sender/Receiverは少なくとも1個の完了シグネチャに対応する。Sender完了シグネチャ集合 ⊆ Receiver完了シグネチャ集合ならば、両者は接続(connect)可能。*5
    • 2025-08-23追記:P3557R3採択後のSender完了シグネチャ集合の宣言方法は id:yohhoy:20250823 を参照。
exec::sender auto sndr = /* Senderオブジェクト */
exec::receiver auto rcvr = /* Receiverオブジェクト */
// SenderとReceiverを接続(connect)
exec::operation_state auto op = exec::connect(sndr, rcvr);
// 非同期操作の開始(start)
exec::start(op);
// 非同期操作完了時にexec::set_{value,error,stopped}を呼び出し、
// rcvrで定義する完了ハンドラのいずれか1つが呼び出される。
// (Receiverが受け取った結果値の取り出しは別機構で実現する。)

基本コンセプト: Scheduler

  • 実行エージェント(execution agent):タスクを並列実行する機構(例:CPUスレッドstd::threadGPU/CUDAスレッド)。
  • 実行リソース(execution resource):実行エージェントの集合を管理するエンティティ(例:CPUスレッドプール、CUDAスレッド管理)
  • Scheduler:execution::scheduler concept
    • 実行リソース上でタスク実行をスケジュールするためのインタフェースかつ軽量ハンドル。*6
    • メンバ型sender_conceptでタグ型sender_tを宣言。
    • スケジュール操作execution::scheduleCPOから呼ばれるscheduleメンバ関数を定義。
  • 完了Scheduler(completion scheduler)
    • 非同期操作の値完了/エラー完了/停止完了を実行するScheduler。完了種別毎に異なる可能性あり。
  • スケジュールSender(schedule sender)
    • execution::scheduleCPO呼び出しで得られるSender。Scheduler上で空の値を用いて値完了を実行する。
  • execution::run_loop
    • シングルCPUスレッドでのタスクFIFO処理を行う実行リソース。
exec::scheduler auto sch = /* Schedulerオブジェクト */
// スケジュールSenderを作成
exec::sender auto sndr = exec::schedule(sch);

exec::receiver auto rcvr = /* Receiverオブジェクト */
exec::operation_state auto op = exec::connect(sndr, rcvr);
exec::start(op);
// sch上でrcvrに対して値完了rcvr.set_value()が呼ばれる

クエリ可能オブジェクト/クエリオブジェクト

  • クエリ可能オブジェクト(queryable object)
    • 読み取り専用のKey−Valueデータ集合。Key==クエリオブジェクト、Valueは任意型。
    • Senderに関連付けられた属性(attributes)、Receiverに関連付けれた環境(environment)(execution::get_envCPO)
    • Schedulerオブジェクト自身
  • クエリオブジェクト(query object)
    • 問い合わせ(query)操作を行うCPO 兼 クエリ可能オブジェクトの Key。
    • Sender/Receiver/Schedulerに関連付けられたプロパティ(property)値を取得。
  • 標準クエリオブジェクト
    • forwarding_query:Senderアルゴリズム間のクエリ転送可否を問い合わせ。
    • get_allocator:メモリアロケータを問い合わせ。
    • get_stop_token:非同期キャンセル(→id:yohhoy:20250625)のための停止トークンを問い合わせ。
    • execution::get_domain:Sender動作カスタマイズのための実行ドメインを問い合わせ。
    • execution::get_(delegation_)scheduler:Receiverへ関連付けられたSchedulerを問い合わせ。
    • execution::get_forward_progress_guarantee:Schedulerへ前方進行保証を問い合わせ。
    • execution::get_completion_scheduler<completion-tag>:Senderへ完了Schedulerを問い合わせ。
    • execution::get_await_completion_adaptor:Senderへco_awaitオペランド指定時の型変換を問い合わせ。[P3570R2]
  • execution::env:複数のクエリ可能オブジェクトを合成。[P3325R5]
  • execution::prop:単一Key−Valueに対応した最小のクエリ可能オブジェクト。[P3325R5]
exec::receiver auto rcvr = /* Receiverオブジェクト */
// Receiver環境を取得
auto env = exec::get_env(rcvr);
// メモリアロケータを問い合わせ
auto alloc = std::get_allocator(env);


関連URL

*1:サードパーティ・ライブラリの拡張自由度を高めるためにCPOが多用されており、多数のカスタマイゼーションポイントを提供する。代償としてライブラリ仕様記述が複雑化し、難解な仕様となっているのは否めない。

*2:https://github.com/NVIDIA/stdexec

*3:P2300R10, §5.2: "An operation state is neither movable nor copyable"

*4:Receiverメンバ関数として定義する完了ハンドラの関数シグネチャとは異なり、戻り値型を “完了操作CPOの型” に置換した関数型として表現される。例:値完了シグネチャ execution::set_value_t(int) に対応するRcvrクラスメンバ関数は void Rcvr::set_value(int) となる。

*5:ある種類の完了シグネチャには対応しないケースや、同種の完了シグネチャで異なる引数リストに複数対応するケースもある。停止完了シグネチャは引数0個のため、実質 execution::set_stopped_t() の1種類のみ。

*6:P2300R10, §4.2: "A scheduler is a lightweight handle that represents a strategy for scheduling work onto an execution resource."

配列要素数の取得 in 標準C

プログラミング言語Cの次期標準C2yでは、配列要素数を安全に取得する_Countof演算子が導入される。オペランドに配列型を要求すること以外は、構文規則的にsizeof演算子とほぼ同じ。

// C2y
int fibs[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
static_assert(_Countof(fibs) == 8);  // OK

// 標準ヘッダ<stdcountof.h>で定義されるマクロ利用すると
// 小文字キーワード風に countof(fibs) と記述可能となる
#include <stdcountof.h>
static_assert(sizeof(fibs) == countof(fibs) * sizeof(int));
// C2y
void func(int data[100]) {
  // 関数パラメータ部の配列表記 int data[100] は
  // 実際には単なるポインタ型 int* data と解釈される。
  constexpr size_t N = _Countof(data);  // NG: ill-formed

  // sizeofを用いたハック (sizeof(data)/sizeof(data[0])) では
  // 正しい要素数を算出できず問題が潜在化・深刻化しうる。
  // _Countof演算子であれば誤りをコンパイル時に検出できる。
}

ノート:constexpr変数*1static_assertキーワードのネイティブ化*2はC23標準で導入された機能。

キーワード名_Countof+マクロ名countof決定までに様々な候補*3が検討されたが、最終的にCプログラマへの大規模アンケート結果*4に従ったとのこと。C言語仕様策定の歴史のなかでは珍しいパターン?

C2y WD N3550, 6.5.4.1, 6.5.4.5/p1, p5より一部引用。

Syntax
unary-expression:
  postfix-expression
  ++ unary-expression
  -- unary-expression
  unary-operator cast-expression
  _Countof unary-expression
  _Countof ( type-name )
  sizeof unary-expression
  sizeof ( type-name )
  alignof ( type-name )

Constraints
1 (snip) The _Countof operator shall only be applied to an expression that has a complete array type, or to the parenthesized name of such a type. (snip)

Semantics
5 The _Countof operator yields the number of elements of its operand. The number of elements is determined from the type of the operand. The result is an integer. If the number of elements of the array type is variable, the operand is evaluated; otherwise, the operand is not evaluated and the expression is an integer constant expression.

おまけ:提案文書(PDF)N3313には_Generictypeof演算子(→id:yohhoy:20220912)を活用した、C23向けの型安全な要素数取得マクロ定義例が載っている。後続の(PDF)N3325例示では配列型判定にGNU C組込関数__builtin_types_compatible_pを利用している。

関連URL

*1:https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3018.htm

*2:https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2934.pdf

*3:WG14提案文書でのキーワード変遷:_Lengthof [N2529] → elementsof [N3313] → nelementsof [N3325] → _Lengthof [N3369] → _Countof [N3469]

*4:回答数は約1000件。アンケート選択肢:lenof, lengthof, countof, nelemsof, nelementsof, extentsof

Range挿入操作と要素オーバーラップ

C++23標準ライブラリのシーケンスコンテナ*1に対するRange挿入操作では、挿入先コンテナと挿入元Rangeとの要素オーバーラップが{append,prepend}_range操作でのみ許容される。それ以外のメンバ関数で要素オーバーラップしていた場合は未定義動作(undefined behavior)を引き起こす。

  • append_range:OK
  • prepend_range:OK
  • assign_range:NG
  • insert_range:NG
  • insert_range_after:NG

例示コードではstd::vectorに対して同コンテナの一部要素を逆順で末尾追加(append_range)している。*2

#include <ranges>
#include <string_view>
#include <vector>
using namespace std::literals;  // ""sv

std::vector<char> vec{std::from_range, "RACE"sv};
// vec=['R', 'A', 'C', 'E']

// OK: vecの先頭3要素を逆順でvec末尾に追加
vec.append_range(vec | std::views::take(3) | std::views::reverse);
// vec=['R', 'A', 'C', 'E', 'C', 'A', 'R']

C++23 24.2.4, 24.3.9.5よりシーケンスコンテナ操作の事前条件(Preconditions)を引用。

a.insert_range(p, rg)

41 Preconditions: T is Cpp17EmplaceConstructible into X from *ranges::begin(rg). For vector and deque, T is also Cpp17MoveInsertable into X, and T meets the Cpp17MoveConstructible, Cpp17MoveAssignable, and Cpp17Swappable requirements. rg and a do not overlap.

a.assign_range(rg)

62 Preconditions: T is Cpp17EmplaceConstructible into X from *ranges::begin(rg). For vector, if R models neither ranges::sized_range nor ranges::forward_range, T is also Cpp17MoveInsertable into X. rg and a do not overlap.

a.prepend_range(rg)

95 Preconditions: T is Cpp17EmplaceConstructible into X from *ranges::begin(rg). For deque, T is also Cpp17MoveInsertable into X, and T meets the Cpp17MoveConstructible, Cpp17MoveAssignable, and Cpp17Swappable requirements.

a.append_range(rg)

107 Preconditions: T is Cpp17EmplaceConstructible into X from *ranges::begin(rg). For vector, T is also Cpp17MoveInsertable into X.

template<container-compatible-range<T> R>
  iterator insert_range_after(const_iterator position, R&& rg);

18 Preconditions: T is Cpp17EmplaceConstructible into forward_list from *ranges::begin(rg). position is before_begin() or is a dereferenceable iterator in the range [begin(),end()). rg and *this do not overlap.

ノート:C++標準ライブラリ文字列型std::basic_stringassign_range, insert_range(およびappend_range, replace_with_range)操作を提供するが、要素オーバーラップに関する前提条件は存在しない。各関数の効果(Effects)によれば引数Rangeから一時オブジェクトを生成するため、要素オーバーラップしていても問題ない。

関連URL

*1:本記事の対象は std::vector, std::deque, std::list, std::forward_list の4種類。std::array もシーケンスコンテナの一種だが、要素数コンパイル時定数のため動的要素追加をサポートしない。

*2:std::vector コンストラクタ引数で std::string_view を経由するのは、文字列リテラル末尾に含まれるNUL終端文字を除外するため。類似ケース id:yohhoy:20161204 も参照のこと。

C++ Reflection(P2996R10)

次期C++2c(C++26)標準規格に向けて検討が進んでいる リフレクション(reflection) についてメモ。本記事の内容は提案文書 P2996R10 に基づく。

2025-07-18追記:2025年6月会合にてP2996R13[言語コア機能とライブラリ]、および一連の関連提案P3560R2[エラーハンドリング]、(PDF)P3096R12[関数パラメータ]、P3394R4アノテーション]、P3491R3[静的データ定義](→id:yohhoy:20250629)、P1306R5[展開文](→id:yohhoy:20250228)などが採択された。*1

要約:

  • 新しい演算子^^[::]の組
  • 新しい構文
    • リフレクト式:^^識別子^^型名
    • スプライサ構文:[:r:]
    • constevalブロック宣言:consteval { statement(s) }
  • 新しい標準ヘッダ<meta>
    • std::meta::info
    • 名前空間std::meta以下のリフレクション・メタ関数群
  • C++エンティティが持つあらゆる静的情報の問合せをサポート
    • 例1:列挙子の名前を取得(→id:yohhoy:20250228)、エンティティのソースコード位置source_locationを取得。
    • 例2:クラス型の全メンバ情報を取得、テンプレート引数情報を取得、エイリアス元情報を取得。
  • テンプレートパラメータの置換(substitute)操作をサポート
    • SFINAE(Substitution Failure Is Not An Error)エミュレーション動作を行える。と思う。
  • リフレクション情報からの集成体(aggregate)定義をサポート
    • クラスや共用体(union)の型情報をコンパイル時に組み立てるメタ・プログラミング。
  • テンプレート・メタ関数(<type_traits>)に対応するリフレクション・メタ関数(<meta>)
    • 例1:is_pointer_v<T>bool meta::is_pointer_type(info)
    • 例2:remove_cvref_t<T>info meta::remove_cvref(info)

リフレクション演算子

C++エンティティに対してリフレクション演算子(reflection operator)^^を適用したリフレクト式(reflect-expression)は、std::meta::info型のリフレクション値(reflection value; reflection)に評価される。リフレクション演算子^^オペランドには下記C++エンティティを指定できる。

  • 名前空間エイリアス(namespace alias)
  • 名前空間(namespace)
    • ^^:: == グローバル名前空間のリフレクション値
  • コンセプト(concept)
  • クラステンプレート(class template)
  • 関数テンプレート(function template)
  • 変数テンプレート(primary variable template)
  • エイリアステンプレート(alias template)
  • 型(type)
  • エイリアス(type alias)
  • 関数(function)
  • 変数(variable)
  • 構造化束縛(structured binding)
  • 列挙子(enumerator)
  • 非staticデータメンバ(non-static data member)

次のC++エンティティには適用できない。

  • 非型テンプレートパラメータ(non-type template parameter) *2
  • パック・インデクス式(pack-index-expression) *3

リフレクション値

フレクション値を表現するstd::meta::info型の特徴:

  • コンパイル時にのみ利用可能な型(consteval-only type)として提供される。
  • デフォルト構築はヌル・リフレクション(null reflection)値となる。
  • リフレクション値の等値比較(==, !=)をサポートする。
  • 単一の不透明(opaque)型として設計される。
    • <meta>ヘッダ:リフレクション値の問合せ(query)を行うリフレクション・メタ関数群を提供。
  • 新しいプライマリ型カテゴリ(→id:yohhoy:20141122)として追加される。
    • <type_traits>ヘッダ:std::is_reflectionテンプレート・メタ関数を追加。
  • リフレクション演算子では直接取得できない特殊なリフレクション値も存在する。
    • 無名ビットフィールド(unnamed bit-field):無名ビットフィールドを含むクラス型Tからmembers_of(^^T)[n]
    • データメンバ記述(data member description):data_member_spec(^^T, {.name="m1"})
    • 直接基底クラス関係(direct base class relationship):public/protected/private継承関係。派生クラス型Dからbases_of(^^D)[0]
#include <meta>
// 結果型は全て std::meta::info
constexpr auto r1 = ^^int;  // 型
constexpr auto r2 = ^^std::string;  // 型エイリアス
constexpr auto r3 = ^^std::malloc;  // 関数
constexpr auto r4 = ^^std::vector;  // クラステンプレート
constexpr auto r5 = ^^std::same_as;  // コンセプト
constexpr auto r6 = ^^std::linalg;  // 名前空間

// リフレクション・メタ関数は名前空間 std::meta に属する
// 引数型 std::meta::info からのADLによりスコープ指定は省略可能
static_assert( is_type(r1) );
static_assert( is_type(r2) && is_type_alias(r2) );
static_assert( is_function(r3) );
static_assert( is_template(r4) && is_class_template(r4) );
static_assert( is_template(r5) && is_concept(r5) );
static_assert( is_namespace(r6) );

ノート:属性(attribute)に対するリフレクションはP3385にて検討中*4属性と構文が似たユーザ定義アノテーション(annotation)導入はP3394にて検討中*5。関数パラメータ(function parameter)はP3096にて検討中*6

スプライサ構文

std::meta::info型のリフレクション値rからC++エンティティを得る、スプライス指定子(splice-specifier)[:r:]が新しい構文要素として追加される。

  • スプライス式(splice-expression):[:r:]template [:r:]で定数値を得る。
    • rがテンプレートを表すときtemplateキーワードが必要。
    • rがコンセプトを表すときはスプライス不可。*7
  • スプライス・型指定子(splice-type-specifier):[:r:]typename [:r:]で型を指定。
    • 型が自明に要求されるコンテキストではtypenameキーワード省略可能。
  • スプライス・スコープ指定子(splice-scope-specifier):[:r:]::名前空間を指定。
constexpr auto r_type = ^^int;
typename [:r_type:] x;  // int x;
         [:r_type:] y;  // int y;

namespace NS { void fn(); }
constexpr auto r_ns = ^^NS;
[:r_ns:]::fn();  // NS::fn();

template<int N> void fn();
constexpr int C = 42;
constexpr auto r_tfn = ^^fn;
constexpr auto r_var = ^^C;
template [:r_tfn:]<[:r_var:]>();  // f<42>();

集成体・共用体の定義

std::meta::define_aggregate関数とconstevalブロック宣言(consteval-block-declaration)を組み合わせて、クラス型や共用体型に対してリフレクション値からデータメンバを定義できる。std::meta::data_member_spec関数+std::meta::data_member_options*8によりデータメンバ記述・リフレクション値を生成し、定義対象型とデータメンバ記述リストを指定する。

#include <meta>

struct Point;
consteval {
  std::meta::define_aggregate(^^Point, {
    data_member_spec(^^float, {.name = "x"}),
    data_member_spec(^^float, {.name = "y"})
  });
}
// struct Point { float x; float y; };と等価
// P2996R10, §4.8 A Simple Tuple Type
#include <meta>

template<typename... Ts> struct Tuple {
  struct storage;
  consteval {
    // storageクラスに(無名)データメンバを追加する
    define_aggregate(^^storage, {data_member_spec(^^Ts)...});
  }
  storage data;

  Tuple(const Ts& ...vs): data{vs...} {}
};

// Tuple<int, char>から下記クラス(相当)を定義
struct Tuple<int, char>::storage {
  int  _u0;
  char _u1;
};
// _uN は無名データメンバのためメンバ名でのアクセス不可
// nonstatic_data_members_of関数とスプライス式を利用し
// constexpr auto ctx = std::meta::access_context::current();
// data.[:nonstatic_data_members_of(^^storage, ctx)[N]:] とする

2025-07-29追記:P2996R8時点で無名データメンバはNGに変更されており、上記§4.8引用コードのままではill-formedとなる*9。メンバ変数名を明示指定するか、data_member_spec(^^Ts, {.name="_"})*10に修正する必要がある。

関連URL

*1:https://herbsutter.com/2025/06/21/trip-report-june-2025-iso-c-standards-meeting-sofia-bulgaria/

*2:非型テンプレートパラメータ V に対して std::meta::reflect_value(V) とすればリフレクション値を得られる。2025-05-21追記:P2996R12で std::meta::reflect_constant に名称変更。

*3:C++2c言語仕様への採択が決定している args...[n] 式。(PDF)P2662R3 参照。

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

*5:https://github.com/cplusplus/papers/issues/2074

*6:https://github.com/cplusplus/papers/issues/1764

*7:リフレクション・メタ関数 std::meta::can_substitute を用いて、あるコンセプトを用いた制約が満たされる(satisfied)か否かを判定可能。例:can_substitute(^^std::integral, {^^int});

*8:データメンバの name, alignment, bit_width, no_unique_address を制御可能。

*9:P2996R13, [meta.reflection.define.aggregate]: "Constant When: if options.name does not contain a value, then options.bit_width contains a value;"

*10:https://cpprefjp.github.io/lang/cpp26/nice_placeholder_with_no_name.html