C#(Unity) エンジニアから見た Coroutine
と言う体で C++20 Coroutine を解説していきます
生粋の C++ エンジニアで Coroutine って何?どんな挙動をするの?
という部分は割愛します
C#(Unity) で Coroutine を普段から使用していて
且つ C++20 で Coroutine 使える様になったんだ!
でも なんか C# と違うぞ? どう使うんだ? と言う人向けです
C++ 日本語 リファレンス
リンク先のリファレンスでは C++23 の説明になってしまっていますが
基本は C++20 ベースでの話と思って下さい
(私の環境が C++20 なので C++23 は動作させた事がない)
cppcoro
cppcoro と言う有名な Coroutine ライブラリがある様なので
C# みたく使いたいだけなんだけど?って方は 是非 検討すると良いかもです
https://github.com/lewissbaker/cppcoro
C++23
C++23 では std::generator と言う機能が増えて
この記事の co_yield(後述) に関する部分がスッキリしますが
あくまで C++20 前提でいきます
なぜ C++20 なのか?
Visual Studio 2022(2025/08)では C++14 がデフォルトの指定ですが
Unreal Engine(2025/08 UE5.6)でもエンジンは C++20 でビルドされている様ですし
C++17 >> C++20 と直ぐに世の中のデフォルトになると思われるので
今から(既に遅いのか?) C++20 の機能を使って慣れよう的なノリです
(C++23 は もう少し先になってしまうのかな?という所感もあり)
ここで C#(Unity) の場合
// C# の比較サンプル
// 非同期メソッド
public IEnumerator FuncA() {
// 何か時間のかかる処理
yield return null;
}
// 非同期メソッド
public IEnumerator FuncB() {
// 何か時間のかかる処理
yield return null;
}
// 非同期メソッド
public IEnumerator FuncAll() {
yield return FuncA();
yield return FuncB();
}
public void StartFunc() {
// 完了復帰ではない非同期メソッドを呼び出して次に進む呼び出し
StartCoroutine(FuncAll);
}
C# だったら こんな感じで手軽に 実装 & 動作 させられますよね
C++20 の Coroutine
構文と言えるものは
あるにはあるのですが単純に使うだけでは Coroutine の挙動をしません
いろいろと準備(自身の実装)をしないとだめなんです
C++20 で Coroutine になる条件 1
メソッドの中で下記のいずれかのワードが使用されている
(これが構文といえば構文)
-
co_return: 完了 -
co_await: 中断と再開 -
co_yield: 値を生成と中断
目的の補足
-
co_await: 単一の非同期な結果を待つ -
co_yield: 一連の値を順番に生成(yield)する
C++20 で Coroutine になる条件 2-1
Coroutine メソッドの戻り値の型(仮で Task 型とする)内で
(C++ の struct は ほぼ class と同等なので class が良い方は class に置き換えて)
promise_type という名前(固定名)の Promise 型が実装されている
(インナーである必要はない)
// Task 型は サンプル用の仮の型
struct Task {
// この例ではインナー
struct promise_type {
};
};
C++20 で Coroutine になる条件 2-2
promise_type 型の中で(最低限)下記のメソッドが実装されている
(自身の Coroutine で動作させたい挙動に必須 & 必要な定義)
// 必須実装なメソッド群
// Coroutine がコールされた際に Coroutine の戻り値型を返すメソッド
// それぞれの実装により型は決定する
T get_return_object() noexcept;
// Coroutine 処理の直後に処理を即時実行するか遅延実行(サスペンド)するかを決定するメソッド
// 戻り値は std::suspend_never(即時実行), std::suspend_always(遅延実行)
// もしくは Awaitable オブジェクト(*後述)
std::suspend_always/*例*/ initial_suspend() const noexcept;
// Coroutine 処理の最後の振る舞い(破棄処理)を決めるメソッド
// 戻り値は std::suspend_never(即時破棄), std::suspend_always(手動破棄)
// もしくは Awaitable オブジェクト(*後述)
std::suspend_always/*例*/ final_suspend() noexcept;
// Coroutine 内でキャッチされなかった例外が発生した際にコールされるメソッド
// 実は必須ではないが 実装をしておいた方が良い
void unhandled_exception();
// co_return が どの様な戻り値かによって 下記のどちらかを実装する
void return_void() const noexcept; // 値を返さない Coroutine
void return_value(T value) noexcept; // 値 T を返す Coroutine
// co_yield を使う Coroutine では実装が必要
// 戻り値は もしくは Awaitable オブジェクト
std::suspend_always/*例*/ yield_value(T value) noexcept;
例
#include <coroutine> // std::suspend_never, std::suspend_always
#include <stdexcept> // std::runtime_error
#include <format> // std::format : C++20
#include <source_location> // std::source_location : C++20
struct Task {
struct promise_type {
Task get_return_object() noexcept {
return Task{};
}
std::suspend_never initial_suspend() const noexcept {
return std::suspend_never{}; // Coroutine は即時実行
}
std::suspend_never final_suspend() noexcept {
return std::suspend_never{}; // 破棄は自動
}
void unhandled_exception() { // 適当な例外を出す
const auto& location = std::source_location::current();
throw std::runtime_error(std::format("{0}:{1}", location.file_name(), location.line()));
}
void return_void() const noexcept {}
};
};
ここまでで お腹一杯ですよね
でも まだまだ続くんです
上記の状態では メチャクチャ最低限で まともな Coroutine の動作まで届きません
(コンパイラに怒られない程度 & 説明の為の意味の無い実装)
co_await や co_yield といった中断や再開が出来ないんです
// 現状で出来る事
Task FuncA() {
// 何か時間のかかる処理
co_return;
}
Task FuncB() {
// 何か時間のかかる処理
co_return;
}
// ただの同期メソッドを連続で呼んでいるのと同じ
void FuncAll() {
FuncA();
FuncB();
}
そして C++ は何故 Coroutine を動作させるまでの道程が遠いのでしょうか
実は C++20 の Coroutine は コア機能のみが実装 されている状況なんです
(要は一般プログラマーが手軽に使える機能では無いって事みたい)
簡単に例えると
- C# : 高レベル制御機能 / オートマチック楽ちん
- C++ : 低レベル制御機能 / マニュアル全開
C++ はヒープのメモリ使用状況にいたるまでプログラマが制御できるようにと
この様な超絶細かい仕様になっているのです
と言うか、この先のバージョンで使い易い Coroutine 機能を実装するための準備的な?
(C++26 で C# レベルの高レベル機能が実装されると期待)
co_await を使える様にする
C# で言うと async / await が比較対象になります
(非同期処理の完了を待って 次の処理を行うと言う観点です)
// C# の比較サンプル
// 仮に戻り値は int とした非同期メソッド
async Task<int> FuncA() {
// 何か時間のかかる処理
return 0;
}
// FuncA の完了をまって 戻り値をもらう
int result = await FuncA();
相変わらず C# は 簡単 & 簡素 に記述できますね
ここから C++ は ちょっと複雑になっていきます
Awaiter オブジェクト
co_await を使える様にするには Awaiter オブジェクトという
下記の 3 つの実装をもったオブジェクトが必要になります
(そして その実装で co_await を制御する)
// co_await が実行された際に 最初にコールされ 戻り値により Coroutine の挙動を決める
// true : 待機不要 コルーチンは中断しない await_resume() が呼ばれる
// false : 待機が必要 コルーチンは中断される可能性あり await_suspend() が呼ばれる
bool await_ready();
// コルーチンが中断する直前の処理を実装する
// 引数は 中断されるコルーチンのハンドルで 中断を再開する resume() を呼ぶ用
// 戻り値は void or bool
// void : 制御を呼び出し元に戻す
// bool : true void と同じ, false コルーチンを中断せず即座に実行を再開
void/*bool*/ await_suspend(std::coroutine_handle<>);
// コルーチンが再開された後にコールされる
// co_await 式 全体の結果として返したい値を返す実装をする
T await_resume();
Awaiter オブジェクトに加えて std::coroutine_handle なんて出てきましたね
だんだん マニュアル全開になっていきます
std::coroutine_handle
Coroutine を指すハンドル(ポインタみたいなもの -> 所有権はない)
実は std::coroutine_handle は
promise_type::get_return_object()
で リターンされた 戻り値型のオブジェクト
(このオブジェクトは Coroutine ではない事に注意)
が 必ずもっているものとなります
(プログラマが使う実装をしていなくても)
この std::coroutine_handle を介して
-
Coroutine の再開 :
resume() -
Coroutine の破棄 :
destroy() -
Coroutine の完了確認 :
done()
と Coroutine を制御できます
更に std::coroutine_handle は
Promise オブジェクト(promise_type 型) から生成できます
(後述のサンプルコードで解説)
std::coroutine_handle は 2 種類あります
-
std::coroutine_handle<>: ジェネリック
どの Coroutine にも使える汎用的なハンドル
基本的な制御のみ可能 -
std::coroutine_handle<PromiseType>: 特化
特定の Promise型を持つ Coroutine 専用のハンドル
promise()が使えて promise オブジェクトにアクセスできます
それにより Coroutine の 外部と内部で 値のやりとりが可能になります
Awaitable オブジェクト
規格的な話になりますが co_await の右辺におくオブジェクトが
Awaitable オブジェクトと呼ばれます
これは 前述の似た名称の Awaiter オブジェクトと違うものとされていて
ここが 冗長な話になるところなんですが、簡単に言うと
co_await に対して 最終的に
Awaiter オブジェクトを渡してあげるオブジェクトを
Awaitable オブジェクトと呼んでいるだけです
つまり、Awaitable オブジェクトが Awaiter オブジェクトの場合もあり
はたまた Awaiter オブジェクトを内包や返す機能をもっている場合もあり
と、co_await には最終的に Awaiter が必要なんですが
co_await の右辺におくオブジェクトは Awaiter とは限らない
なので Awaitable オブジェクトと言うって感じです
co_await で使える Task 型にしてみる
Task を Awaitavle にするサンプルコードです
設計(方針)は Task 自身が Awaiter になる実装です
#include <coroutine> // std::suspend_never, std::suspend_always
#include <stdexcept> // std::runtime_error
#include <format> // std::format : C++20
#include <source_location> // std::source_location : C++20
#include <utility> // std::exchange : C++14
struct Task {
struct promise_type; // 前方宣言
using handle_type = std::coroutine_handle<promise_type>; // エイリアスで簡素にする
// 自身の coroutine_handle を保持するためのメンバ
handle_type handle;
// コンストラクタ
explicit Task(handle_type h = nullptr) noexcept : handle(h) {}
~Task() {
if (handle) {
handle.destroy();
}
}
Task(const Task&) = delete; // コピー禁止
Task& operator =(const Task&) = delete;
// ムーブ
Task(Task&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
Task& operator =(Task&& other) noexcept {
if (this != &other) {
if (handle) {
handle.destroy();
}
handle = std::exchange(other.handle, nullptr);
}
return *this;
}
struct promise_type {
// 待機している(後続処理の) Coroutine ハンドルを保持する
std::coroutine_handle<> continuation = nullptr;
Task get_return_object() noexcept {
return Task{handle_type::from_promise(*this)};
}
std::suspend_never initial_suspend() const noexcept {
return std::suspend_never{}; // Coroutine は即時実行
}
auto final_suspend() noexcept {
// 完了時に後続タスクを再開させるための Awaiter を返す
// Task 型が Awaiter になる事とは 別の処理なので注意
struct FinalAwaiter {
// final_suspend(内でのFinalAwaiter) では 必ず中断
bool await_ready() noexcept { return false; }
// 中断時に 後続タスク を再開する
std::coroutine_handle<> await_suspend(handle_type h) noexcept {
// 待機しているコルーチンがあれば、そのハンドルを返して実行を移す
if (auto c = h.promise().continuation; c) { // C++17 初期化子付き if
return c;
}
// 待機しているものがなければ停止する
return std::noop_coroutine();
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
void unhandled_exception() {
const auto& location = std::source_location::current();
throw std::runtime_error(std::format("{0}:{1}", location.file_name(), location.line()));
}
void return_void() const noexcept {}
};
// Awaiter 化に必要な実装
bool await_ready() const noexcept {
return !handle || handle.done(); // タスクが完了済みなら待つ必要はない
}
void await_suspend(std::coroutine_handle<> await) const noexcept {
// この Task の promise 型(promise_type)に 後続処理(await)を保持する
handle.promise().continuation = await;
}
void await_resume() const noexcept {} // このTaskは値を返さないので何もしない
};
どうですか? 一気に複雑化して来ましたね
- Task を Awaiter にする為 以下のメンバを実装
・Task::await_ready
・Task::await_suspend
・Task::await_resume -
Coroutine 処理を制御するため
std::coroutine_handleのメンバ実装は必須レベル -
std::coroutine_handle自体をやりとりするために ムーブもあると良い
・逆に コピーコンストラクタ と コピー代入 は禁止 - Task::promise_type::final_suspend にて ネスト して Awaiter(FinalAwaiter) を実装
・Task の Awaiter 化とは関係ないstd::coroutine_handleの連携の為
実際の co_await で Task 型が どう挙動するかの説明
// 超簡素な C++ イメージ
Task FuncA() {
// 実際は suspend_always ではなく、何か時間がかかる処理
co_await std::suspend_always{};
}
Task FuncB() {
// 実際は suspend_always ではなく、何か時間がかかる処理
co_await std::suspend_always{};
}
// 実は C++20 では この FuncAll を呼び出して完了を待つコードですら
// サンプル提示が躊躇われるほど簡素に書けない
// (はい、逃げました)
// 通常 コルーチンの実行を管理するイベントループや
// cppcoro::sync_wait の様な 上位で完了を同期的に待つ関数を用意する必要がある
Task FuncAll() {
co_await FuncA();
co_await FuncB();
}
1 FuncA の呼び出し
co_await FuncA(); で Coroutine が作成され promise_type オブジェクトが作られる
2 get_return_object の呼び出し
Task::promise_type::get_return_object が呼び出される
std::coroutine_handle<promise_type>::from_promise(*this)
により std::coroutine_handle が作られる
(Task 型 オブジェクトの作成)
3 initial_suspend の呼び出し
Task::promise_type::initial_suspend が呼び出される
std::suspend_never なので中断しない
4 await_ready の呼び出し
Task::await_ready が呼び出される
Task は実行中なので handle.done は false
5 await_suspend の呼び出し
Task::await_suspend が呼び出される
引数には FuncAll で作られた std::coroutine_handle が渡ってくるので
promise_type.continuation にセットする
6 FuncA の処理
FuncA のタスクが実行される
7 final_suspend の呼び出し
タスクの処理が終わったら
Task::promise_type::final_suspend が呼び出される
(FinalAwaiter を返す)
8 FinalAwaiter::await_ready の呼び出し
FinalAwaiter::await_ready が呼び出される
必ず中断の理由は Coroutine が生存している必要があるため
9 FinalAwaiter::await_suspend の呼び出し
FinalAwaiter::await_suspend が呼び出される
ここで Task::promise_type.continuatio
つまり FuncAll で作られた std::coroutine_handle
が 戻る事になり 再開処理が可能になる
10 await_resume の呼び出し
Task::await_resume が呼び出される
(何もしない)
11 破棄
Task 型オブジェクトが破棄されると
デストラクタで handle.destroy が呼び出される
(co_await FuncA(); で作られた Coroutine の破棄)
ここまでの所感
ここまで、シンプル & 最低限で 分かり易く Coroutine の
co_await 実装について説明したつもりですが
自分で見ても 何だか伝わらないな、、、と思えます
C++20 の Coroutine は外部ライブラリか
誰か出来る人が構築したコードをライブラリ化 or 改造してつかう
くらいにしかならなそうですね
WEB で検索しても C++20 Coroutine の情報が少ないわけだ
これ 会社・組織・プロジェクト はともかく
個人で理解して使ったろ!ってプログラマがどれだけいるのか、、、
co_yield を使える様にする
promise_type::yield_value の実装が必須になります
実は構文的には これだけなのですが
この後は 便利に使う拡張の話が追加されていきます
Template 化
必須では無いのですが
値の型を決め打ち実装では汎用性がないので Template 化すると良いです
#include <coroutine> // std::suspend_never, std::suspend_always
#include <stdexcept> // std::runtime_error
#include <format> // std::format : C++20
#include <source_location> // std::source_location : C++20
#include <utility> // std::exchange : C++14
#include <optional> // std::optional : C++17
// T は co_yield される値の型
template<typename T>
struct Task {
struct promise_type; // 前方宣言
using handle_type = std::coroutine_handle<promise_type>; // エイリアスで簡素にする
// 自身の coroutine_handle を保持するためのメンバ
handle_type handle;
// コンストラクタ
explicit Task(handle_type h) : handle(h) {}
~Task() {
if (handle) {
handle.destroy();
}
}
Task(const Task&) = delete; // コピー禁止
Task& operator =(const Task&) = delete;
// ムーブ
Task(Task&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
Task& operator =(Task&& other) noexcept {
if (this != &other) {
if (handle) {
handle.destroy();
}
handle = std::exchange(other.handle, nullptr);
}
return *this;
}
// 手動の 値 取得用メソッド群(メソッド名は それらしい感じで)
bool move_next() {
if (!handle || handle.done()) {
return false;
}
handle.resume();
return !handle.done();
}
T current_value() const { // move_next が true 前提でコールする設計
return handle.promise().value.value();
}
struct promise_type {
// std::optional にすることで T 値の無効状態を処理できる
std::optional<T> value;
Task get_return_object() noexcept {
return Task{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() const noexcept {
return std::suspend_always{}; // 呼び出し元が begin() を呼んでから実行を開始する
}
std::suspend_always final_suspend() noexcept {
return std::suspend_always{}; // 破棄は自動
}
void unhandled_exception() {
const auto& location = std::source_location::current();
throw std::runtime_error(std::format("{0}:{1}", location.file_name(), location.line()));
}
void return_void() const noexcept {}
// co_yield に必須のメンバ
std::suspend_always yield_value(T value) {
this->value = std::move(value); // co_yield された値を保持
return std::suspend_always{}; // Coroutine を中断して呼び出し元に戻る
}
};
};
キモは WEB で良く見る Generator という ワード や実装を使わない所です
move_nextcurrent_value
というメソッドを追加 & それを明示的に呼び出して 値を取得する設計にしています
こうする事により 複雑な話や実装を省けます
(主眼は co_yield を使うための最小の説明なんで)
// 使い方サンプル
Task<int> FuncA() {
co_yield 10;
co_yield 20;
co_yield 30;
}
auto task = FuncA();
// 専用に実装したメソッドで明示的に操作する
while (task.move_next()) {
auto value = task.current_value();
}
Generator 化
co_yield で中断した Coroutine から
どうやって 値を取り出すか におけるデザインパターンとして
Generator 化という手法があります
これが WEB だと当然の実装の様に説明されてるやつですね
(そして前述では 意図的に避けた部分)
おそらく 便利 スマート モダン 定石 と C++ なら そうしとけってやつです
(まったく否定はないです、むしろ好きな方向)
具体的な Generator 化をすると どうなるかと言うと
range-based for などで 1 つずつ値を取り出せる 動作 & 実装出来る様になります
さらに 標準ライブラリのアルゴリズム(std::copy, std::ranges::... など)と
シームレスに連携できる様になり コードがモダンに書けます
Generator 化に必要な事
-
Task::beginとTask::endの実装を行う
つまり そのbeginとendが返す イテレータの実装を行う
という事が必要になります
下記は、実は 冒頭で紹介した C++23 std::generator の C++20 版になります
#include <coroutine> // std::suspend_never, std::suspend_always
#include <stdexcept> // std::runtime_error
#include <format> // std::format : C++20
#include <source_location> // std::source_location : C++20
#include <utility> // std::exchange : C++14
#include <optional> // std::optional : C++17
// T は co_yield される値の型
template<typename T>
struct Task {
struct promise_type; // 前方宣言
using handle_type = std::coroutine_handle<promise_type>; // エイリアスで簡素にする
// イテレータの定義
struct Iterator {
handle_type handle;
// std::iterator_traits 用
// range-based for 以外にも適応させる
using iterator_category = std::input_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
using pointer = T*;
using reference = T&;
Iterator& operator ++() {
handle.resume();
return *this;
}
void operator ++(int) {
(void)operator++();
}
const T& operator *() const {
return handle.promise().value.value();
}
bool operator !=(std::default_sentinel_t) const {
return !handle.done();
}
};
// 自身の coroutine_handle を保持するためのメンバ
handle_type handle;
// コンストラクタ
explicit Task(handle_type h) : handle(h) {}
~Task() {
if (handle) {
handle.destroy();
}
}
Task(const Task&) = delete; // コピー禁止
Task& operator =(const Task&) = delete;
// ムーブ
Task(Task&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
Task& operator =(Task&& other) noexcept {
if (this != &other) {
if (handle) {
handle.destroy();
}
handle = std::exchange(other.handle, nullptr);
}
return *this;
}
// Generator 化に必要な実装
Iterator begin() {
if (handle) {
handle.resume(); // 最初の値を取得するために一度再開する
}
return Iterator{handle};
}
std::default_sentinel_t end() {
return std::default_sentinel_t{};
}
struct promise_type {
// std::optional にすることで T 値の無効状態を処理できる
std::optional<T> value;
Task get_return_object() noexcept {
return Task{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() const noexcept {
return std::suspend_always{}; // 呼び出し元が begin() を呼んでから実行を開始する
}
std::suspend_always final_suspend() noexcept {
return std::suspend_always{}; // 破棄は自動
}
void unhandled_exception() {
const auto& location = std::source_location::current();
throw std::runtime_error(std::format("{0}:{1}", location.file_name(), location.line()));
}
void return_void() const noexcept {}
// co_yield に必須のメンバ
std::suspend_always yield_value(T value) {
this->value = std::move(value); // co_yield された値を保持
return std::suspend_always{}; // Coroutine を中断して呼び出し元に戻る
}
};
};
// 使い方サンプル
Task<int> FuncA() {
co_yield 10;
co_yield 20;
co_yield 30;
}
// Generator 化していると色々便利に記述出来る
for (auto value : FuncA()) {
// value が co_yield が返す値
}
C++23 std::generator
C++23 になると、上記 Task 型の実装が不要で下記のサンプルのみで行けます!
// 使い方サンプル
#include <generator>
std::generator<int> FuncA() {
co_yield 10;
co_yield 20;
co_yield 30;
}
for (auto value : FuncA()) {
// value が co_yield が返す値
}
co_await と co_yield の使い分け
いままでの解説をみて頂けると分かると思いますが
co_await でも co_yield でも使える汎用的な(型)実装は 無理がありそうですよね
co_await を行いたいのに Generator 化している必要はないわけです
値を 1 つだけ返す co_await 的な使いか方とか出来なくもなさそうですが
プログラム実装の基本は、シンプル & 分かり易い のはずです
無理やり使用する方向なんて設計より、専用のシンプルな実装を押す派ですねー
つまり co_await と co_yield 用の それぞれの専用設計で実装をすると良い様な気がします
promise_type::initial_suspend の挙動における使い分け
co_await では std::suspend_never を返しており
これは ホットスタート と呼ばれ
Coroutine は呼び出されると直ぐに実行を開始します
非同期処理の様な 呼び出したら直ぐに処理を始めて欲しい場合に使う感じです
co_yield では std::suspend_always を返しており
これは コールドスタート と呼ばれ
Coroutine は呼び出されると実行はされず 中断状態で開始します
begin() や move_next() が呼ばれて初めて resume() されて実行が開始されます
Generator の様に呼び出し側が値を要求する場合に使う感じです
最後に C++20 Coroutine 日本語リファレンス
リンクを貼っておきます
読める、読めるぞ! となって頂ければ幸いです
まあ、実際は 濃霧で全く前が見えない >> 霧雨で見通し悪い
くらいになってくれる程度で良いかも、と途中で諦めました(笑)