yohhoyの日記

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

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