yohhoyの日記

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

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

Expansion statement for C++

プログラミング言語C++の次期標準C++2c(C++26)向けに提案されている展開文(Expansion statement)について。template for構文をもちいて本体処理を反復的にコンパイル時展開する。

2025-06-22追記:2025年6月会合にてP1306R5が採択され*1C++2c言語仕様に展開文(Expansion statement)が導入される。あわせて P2996R13 C++ Reflection(→id:yohhoy:20250305)も採択済み*2

// P1306R3: destructurable expression
auto tup = std::make_tuple(0, 'a', 3.14);
template for (auto elem : tup)
  std::println("{}", elem);
// 下記コードに展開される
auto&& tup = std::make_tuple(0, 'a', 3.14);
{ auto elem = std::get<0>(tup); std::println("{}", elem); }
{ auto elem = std::get<1>(tup); std::println("{}", elem); }
{ auto elem = std::get<2>(tup); std::println("{}", elem); }
// P1306R3: expansion-init-list
template for (auto elem : {0, 'a', 3.14})
  std::println("{}", elem);
// 下記コードに展開される
{ auto elem = 0;    std::println("{}", elem); }
{ auto elem = 'a';  std::println("{}", elem); }
{ auto elem = 3.14; std::println("{}", elem); }

C++2c向けに同時検討されているC++ Reflection機能と組み合わせると、全C++プログラマの夢(?)であった「列挙型の値から文字列への変換関数」をジェネリックに実装できる。

// P1306R3 + P2996(Reflection) + P3491(define_static_*)
#include <meta>  // P2996+P3491
#include <string_view>
#include <type_traits>

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
  // リフレクション(reflection)演算子^^ + enumerators_ofメタ関数(P2996)
  //   列挙型Eの列挙子リストをstd::vector<std::meta::info>で返す
  // define_static_arrayメタ関数(P3491)
  //  コンパイル時vector<meta::info>から静的配列への参照(span)に変換
  template for (constexpr auto e :
    std::define_static_array(std::meta::enumerators_of(^^E)))
  {
    // リフレクション値(reflection value) eの型 == std::meta::info
    // スプライス式(splice-expression) [:e:] により列挙子の値を取得
    if (value == [:e:]) {
      // identifier_ofメタ関数
      //   列挙子eの識別子(名前)をstd::string_viewで返す
      return std::meta::identifier_of(e);
    }
  }
  return "<unnamed>";
}

2025-12-26追記:上記のenum_to_string関数実装にstd::define_static_arrayメタ関数(→id:yohhoy:20250629)利用を追記。StackOverflowの回答 What is `template for` in C++26? も参照のこと。

template for構文においても通常のfor構文と同じくbreak文/continue文をサポートする。コンパイル時展開はbreakcontinue制御によらず行われ、評価フェーズで実行パス分岐が行われることに注意。

// P1306R3
template for (auto v : {1,2,3,4,5,6,7,8,9}) {
  if (v % 2 == 0) continue;
  std::print("{} ", v);
  if (v % 5 == 0) break;
}
// 1 3 5

関連URL

サンプルコードによるC++23ジェネレータの紹介

サンプルコードによるC++23ジェネレータの紹介

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

スライド資料:https://www.docswell.com/s/yohhoy/KEXQPG-cpp23gen

関連URL