Coroutines (C++20)
コルーチンは、実行を一時停止して後で再開できる関数です。コルーチンはスタックレスです:実行を一時停止するために呼び出し元に戻り、実行を再開するために必要なデータはスタックとは別に保存されます。これにより、(明示的なコールバックなしで非ブロッキングI/Oを処理するなど)非同期に実行される逐次的なコードが可能になり、遅延評価される無限シーケンスに対するアルゴリズムやその他の用途もサポートします。
関数は、その定義に以下のいずれかが含まれている場合、コルーチンとなります:
- the co_await 式 — 再開されるまで実行を中断する
task<> tcp_echo_server() { char data[1024]; while (true) { std::size_t n = co_await socket.async_read_some(buffer(data)); co_await async_write(socket, buffer(data, n)); } }
- the co_yield expression — 値を返して実行を中断する
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- the co_return statement — 値を返して実行を完了する
lazy<int> f() { co_return 7; }
すべてのコルーチンは、以下に示すいくつかの要件を満たす戻り値の型を持たなければなりません。
目次 |
制限事項
コルーチンは
可変引数
、通常の
return文
、または
プレースホルダー戻り値型
(
auto
または
Concept
) を使用できません。
Consteval関数 、 constexpr関数 、 コンストラクタ 、 デストラクタ 、および main関数 はコルーチンにできません。
実行
各コルーチンは関連付けられています
- promise オブジェクト - コルーチン内部から操作されるオブジェクト。コルーチンはこのオブジェクトを通じて結果または例外を送出する。promise オブジェクトは std::promise とは一切関係ない。
- コルーチンハンドル - コルーチン外部から操作される非所有ハンドル。コルーチンの実行再開やコルーチンフレームの破棄に使用される。
- コルーチン状態 - 内部的な動的確保ストレージ(最適化により確保が省略される場合を除く)であり、以下のものを含むオブジェクト
-
- promiseオブジェクト
- パラメータ(すべて値でコピーされる)
- 現在のサスペンションポイントの何らかの表現。これにより、resumeは継続する場所を認識し、destroyはスコープ内のローカル変数を把握できる
- 現在のサスペンションポイントをまたいで生存期間が続くローカル変数と一時オブジェクト
コルーチンが実行を開始すると、以下の処理を実行します:
-
operator newを使用して コルーチン状態オブジェクトを動的に確保します。 - すべての関数パラメータをコルーチン状態にコピーします:値渡しパラメータは移動またはコピーされ、参照渡しパラメータは参照のまま維持されます(したがって、参照先オブジェクトの寿命が終了した後にコルーチンが再開されると、ダングリング参照になる可能性があります — 例については後述)。
- promiseオブジェクトのコンストラクタを呼び出します。promise型がすべてのコルーチンパラメータを受け取るコンストラクタを持つ場合、そのコンストラクタがコピー後のコルーチン引数で呼び出されます。それ以外の場合はデフォルトコンストラクタが呼び出されます。
- promise. get_return_object ( ) を呼び出し、結果をローカル変数に保持します。この呼び出しの結果は、コルーチンが最初に停止したときに呼び出し元に返されます。このステップまでにスローされた例外はすべて、promiseに配置されず、呼び出し元に伝播します。
-
promise.
initial_suspend
(
)
を呼び出し、その結果を
co_awaitします。一般的なPromise型は、遅延開始コルーチンの場合は std::suspend_always を、即時開始コルーチンの場合は std::suspend_never を返します。 - co_await promise. initial_suspend ( ) が再開されると、コルーチンの本体の実行を開始します。
パラメータがダングリング状態になる例のいくつか:
#include <coroutine> #include <iostream> struct promise; struct coroutine : std::coroutine_handle<promise> { using promise_type = ::promise; }; struct promise { coroutine get_return_object() { return {coroutine::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; struct S { int i; coroutine f() { std::cout << i; co_return; } }; void bad1() { coroutine h = S{0}.f(); // S{0} 破棄済み h.resume(); // 再開されたコルーチンは std::cout << i を実行、解放後の S::i を使用 h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // 返されたコルーチンは解放後の使用を犯さずに再開できない } void bad3() { coroutine h = [i = 0]() -> coroutine // コルーチンでもあるラムダ { std::cout << i; co_return; }(); // 即時呼び出し // ラムダ破棄済み h.resume(); // 解放後の(匿名ラムダ型)::i を使用 h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // i をコルーチンパラメータにする { std::cout << i; co_return; }(0); // ラムダ破棄済み h.resume(); // 問題なし、i は値渡しパラメータとしてコルーチンフレームにコピー済み // フレームは値渡しパラメータとして h.destroy(); }
コルーチンが中断ポイントに到達したとき
- 必要に応じて、コルーチンの戻り値の型への暗黙的な変換が行われた後、以前に取得した戻り値オブジェクトが呼び出し元/再開元に返されます。
コルーチンが co_return 文に到達した場合、以下の処理を実行します:
- calls promise. return_void ( ) を呼び出します
-
- co_return ;
- co_return expr ; ここで expr は void 型を持つ
- promise. return_value ( expr ) を呼び出す( co_return expr ; の場合。ここで expr は非void型を持つ)
- 自動ストレージ期間を持つすべての変数を、作成された逆順で破棄する
- promise. final_suspend ( ) を呼び出し、その結果を co_await する
コルーチンの末尾から抜け出すことは、
co_return
;
と同等であるが、
Promise
のスコープ内で
return_void
の宣言が見つからない場合、動作は未定義となる。関数本体に関連するキーワードが一切含まれない関数は、その戻り値の型に関わらずコルーチンではなく、末尾から抜け出した場合の動作は、戻り値の型が(CV修飾された可能性のある)
void
でない場合は未定義となる。
// taskが何らかのコルーチンタスク型であると仮定 task<void> f() { // コルーチンではないため、未定義動作 } task<void> g() { co_return; // OK } task<void> h() { co_await g(); // OK、暗黙的なco_return }
コルーチンが捕捉されない例外で終了した場合、以下の処理を実行します:
- 例外をキャッチし、catchブロック内から promise. unhandled_exception ( ) を呼び出す
- promise. final_suspend ( ) を呼び出し、結果を co_await する(例:継続を再開する、または結果を公開する)。この時点からコルーチンを再開することは未定義動作である。
コルーチンの状態が破棄されるとき、それが co_return による終了、捕捉されなかった例外、またはハンドルを通じた破棄によって発生した場合、以下の処理を実行します:
- promiseオブジェクトのデストラクタを呼び出します。
- 関数パラメータのコピーのデストラクタを呼び出します。
- coroutine状態で使用されていたメモリを解放するために operator delete を呼び出します。
- 実行を呼び出し元/再開元に戻します。
動的割り当て
コルーチンの状態は非配列の operator new によって動的に割り当てられます。
Promise
型がクラスレベルの置換を定義している場合、それが使用されます。それ以外の場合はグローバルな
operator new
が使用されます。
Promise
型が追加のパラメータを受け取る配置形式の
operator new
を定義しており、それらが最初の引数が要求サイズ(型
std::size_t
)で残りがコルーチン関数の引数である引数リストと一致する場合、それらの引数は
operator new
に渡されます(これによりコルーチンで
先頭アロケータ規約
を使用することが可能になります)。
operator new の呼び出しは、以下の場合に最適化されて除去される可能性があります(カスタムアロケータが使用されている場合でも):
- コルーチン状態の生存期間は呼び出し元の生存期間に厳密にネストされており、
- コルーチンフレームのサイズは呼び出し元で既知です。
その場合、コルーチンの状態は呼び出し元のスタックフレーム(呼び出し元が通常の関数の場合)またはコルーチン状態(呼び出し元がコルーチンの場合)に埋め込まれます。
メモリ確保が失敗した場合、コルーチンは
std::bad_alloc
を送出します。ただし、
Promise
型がメンバー関数
Promise
::
get_return_object_on_allocation_failure
(
)
を定義している場合は例外です。このメンバー関数が定義されている場合、メモリ確保は
operator new
のnothrow形式を使用し、メモリ確保が失敗した場合、コルーチンは直ちに
Promise
::
get_return_object_on_allocation_failure
(
)
から取得したオブジェクトを呼び出し元に返します。例:
struct Coroutine::promise_type { /* ... */ // 例外を投げないoperator-newの使用を保証 static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // または、return Coroutine(nullptr); } // カスタムの例外を投げないnewオーバーロード void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // アロケーション失敗 } };
Promise
Promise
型は、コルーチンの戻り値型からコンパイラによって決定され、
std::coroutine_traits
を使用します。
形式的には、以下を定義する
-
RおよびArgs...はそれぞれコルーチンの戻り値型とパラメータ型リストを示します、 -
ClassTは非静的メンバ関数として定義されている場合のコルーチンが属するクラス型を示します、 - cv は非静的メンバ関数として定義されている場合の 関数宣言 で宣言されたCV修飾を示します、
its
Promise
typeは以下によって決定されます:
- std:: coroutine_traits < R, Args... > :: promise_type 、コルーチンが 暗黙オブジェクトメンバー関数 として定義されていない場合
-
std::
coroutine_traits
<
R,
cvClassT & , Args... > :: promise_type 、コルーチンが右辺値参照修飾されていない暗黙オブジェクトメンバー関数として定義されている場合 -
std::
coroutine_traits
<
R,
cvClassT && , Args... > :: promise_type 、コルーチンが右辺値参照修飾された暗黙オブジェクトメンバー関数として定義されている場合
例:
| コルーチンが以下のように定義されている場合... |
その
Promise
型は...
|
|---|---|
| task < void > foo ( int x ) ; | std:: coroutine_traits < task < void > , int > :: promise_type |
| task < void > Bar :: foo ( int x ) const ; | std:: coroutine_traits < task < void > , const Bar & , int > :: promise_type |
| task < void > Bar :: foo ( int x ) && ; | std:: coroutine_traits < task < void > , Bar && , int > :: promise_type |
co_await
単項演算子 co_await はコルーチンを中断し、呼び出し元に制御を返します。
co_await
式
|
|||||||||
co_await 式は、通常の 評価される可能性のある式 内でのみ、通常の 関数本体 ( ラムダ式 の関数本体を含む)内に現れることができ、以下の場合には現れることはできません
- ハンドラ 内で、
- 宣言 文内で(ただしその宣言文の初期化子内に現れる場合を除く)、
-
単純宣言
の
初期化文
内で(
if、switch、forおよび[[../range- for |range- for ]]を参照)、ただしその 初期化文 の初期化子内に現れる場合を除く、 - デフォルト引数 内で、または
- 静的またはスレッド ストレージ期間 を持つブロックスコープ変数の初期化子内で。
| (C++26以降) |
まず最初に、 expr は以下のようにawaitableに変換されます:
- expr が初期サスペンドポイント、最終サスペンドポイント、またはyield式によって生成された場合、awaitableは変更なしで expr となる。
-
それ以外の場合、現在のコルーチンの
Promise型がメンバ関数await_transformを持つならば、awaitableは promise. await_transform ( expr ) となる。 - それ以外の場合、awaitableは変更なしで expr となる。
次に、awaiterオブジェクトを以下のように取得します:
- operator co_await に対するオーバーロード解決が単一の最適なオーバーロードを返す場合、awaiterはその呼び出しの結果となります:
-
- awaitable. operator co_await ( ) メンバーオーバーロードの場合、
- operator co_await ( static_cast < Awaitable && > ( awaitable ) ) 非メンバーオーバーロードの場合。
- それ以外の場合、オーバーロード解決が演算子 co_await を見つけられない場合、待機可能オブジェクトはそのままアウェイターとなります。
- それ以外の場合、オーバーロード解決が曖昧な場合、プログラムは不適格となります。
上記の式が prvalue の場合、awaiterオブジェクトはそこから materialized された一時オブジェクトです。それ以外の場合、上記の式が glvalue の場合、awaiterオブジェクトはそれが参照するオブジェクトです。
その後、 awaiter. await_ready ( ) が呼び出されます(これは、結果が準備完了しているか同期的に完了できることが既知の場合、サスペンションのコストを回避するためのショートカットです)。その結果が bool への文脈的変換で false の場合、
- コルーチンは中断されます(そのコルーチン状態にはローカル変数と現在の中断ポイントが保存されます)。
-
awaiter.
await_suspend
(
handle
)
が呼び出されます。ここでhandleは現在のコルーチンを表すコルーチンハンドルです。この関数内では、中断されたコルーチン状態はそのハンドルを通じて観察可能であり、この関数の責任は、何らかのエグゼキューターで再開するようにスケジュールするか、破棄することです(falseを返すことはスケジュールとしてカウントされます)
-
await_suspendが void を返す場合、制御は直ちに現在のコルーチンの呼び出し元/再開元に戻ります(このコルーチンは中断されたままです)。それ以外の場合 -
await_suspendが bool を返す場合、
-
- 値 true は制御を現在のコルーチンの呼び出し元/再開元に戻します
- 値 false は現在のコルーチンを再開します
-
await_suspendが他のコルーチンのコルーチンハンドルを返す場合、そのハンドルが再開されます( handle. resume ( ) の呼び出しによって)(これは連鎖的に最終的に現在のコルーチンが再開される原因となる可能性があります) -
await_suspendが例外をスローした場合、例外は捕捉され、コルーチンは再開され、例外は直ちに再スローされます
-
最後に、 awaiter. await_resume ( ) が呼び出され(コルーチンが中断されたかどうかに関わらず)、その結果が全体の co_await expr 式の結果となります。
コルーチンが co_await 式で中断され、後で再開された場合、再開ポイントは awaiter. await_resume ( ) の呼び出し直前に位置します。
コルーチンは awaiter. await_suspend ( ) に入る前に完全に中断されていることに注意してください。そのハンドルは他のスレッドと共有され、 await_suspend ( ) 関数が戻る前に再開される可能性があります。(デフォルトのメモリ安全性ルールが依然として適用されるため、ロックなしでコルーチンハンドルがスレッド間で共有される場合、awaiterは少なくとも release semantics を使用し、再開側は少なくとも acquire semantics を使用する必要があります。)例えば、コルーチンハンドルをコールバック内に配置し、非同期I/O操作が完了したときにスレッドプールで実行されるようにスケジュールすることができます。その場合、現在のコルーチンが既に再開され、それに伴ってawaiterオブジェクトのデストラクタが実行されている可能性があるため、 await_suspend ( ) が現在のスレッドで実行を継続している間の全ての並行処理において、 await_suspend ( ) は * this が破棄されたものとして扱い、ハンドルが他のスレッドに公開された後はこれにアクセスすべきではありません。
例
#include <coroutine> #include <iostream> #include <stdexcept> #include <thread> auto switch_to_new_thread(std::jthread& out) { struct awaitable { std::jthread* p_out; bool await_ready() { return false; } void await_suspend(std::coroutine_handle<> h) { std::jthread& out = *p_out; if (out.joinable()) throw std::runtime_error("Output jthread parameter not empty"); out = std::jthread([h] { h.resume(); }); // 未定義動作の可能性: 破棄された可能性のある*thisへのアクセス // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // こちらは問題ありません } void await_resume() {} }; return awaitable{&out}; { struct task { struct promise_type { task get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; task resuming_on_new_thread(std::jthread& out) { std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // awaiterはここで破棄されます std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; } int main() { std::jthread out; resuming_on_new_thread(out); }
出力例:
Coroutine started on thread: 139972277602112 New thread ID: 139972267284224 Coroutine resumed on thread: 139972267284224
注意: awaiterオブジェクトはコルーチンの状態の一部であり(サスペンションポイントを跨ぐ一時オブジェクトとして)、 co_await 式が完了する前に破棄されます。これは、追加的な動的メモリ確保に頼ることなく、一部の非同期I/O APIで必要とされる操作ごとの状態を維持するために使用できます。
標準ライブラリは2つの自明なアウェイタブルを定義しています: std::suspend_always と std::suspend_never 。
|
このセクションは不完全です
理由: 例 |
| promise_type promise_type :: await_transform およびプログラム提供awaiterのデモ |
|---|
例
このコードを実行
#include <cassert> #include <coroutine> #include <iostream> struct tunable_coro { // コンストラクタのパラメータによって「準備完了」状態が決定されるawaiter。 class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // 3つの標準awaiterインターフェース関数: bool await_ready() const noexcept { return ready_; } static void await_suspend(std::coroutine_handle<>) noexcept {} static void await_resume() noexcept {} }; struct promise_type { using coro_handle = std::coroutine_handle<promise_type>; auto get_return_object() { return coro_handle::from_promise(*this); } static auto initial_suspend() { return std::suspend_always(); } static auto final_suspend() noexcept { return std::suspend_always(); } static void return_void() {} static void unhandled_exception() { std::terminate(); } // ユーザーが提供する変換関数。カスタムawaiterを返す: auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); } void disable_suspension() { ready_ = false; } private: bool ready_{true}; }; tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); } // 簡潔さのため、これら4つの特殊関数を削除済みとして宣言する: tunable_coro(tunable_coro const&) = delete; tunable_coro(tunable_coro&&) = delete; tunable_coro& operator=(tunable_coro const&) = delete; tunable_coro& operator=(tunable_coro&&) = delete; ~tunable_coro() { if (handle_) handle_.destroy(); } void disable_suspension() const { if (handle_.done()) return; handle_.promise().disable_suspension(); handle_(); } bool operator()() { if (!handle_.done()) handle_(); return !handle_.done(); } private: promise_type::coro_handle handle_; }; tunable_coro generate(int n) { for (int i{}; i != n; ++i) { std::cout << i << ' '; // co_awaitに渡されたawaiterはpromise_type::await_transformに渡され、 // 初期状態で中断を引き起こすtunable_awaiterの問題(呼び出し元に戻る) // 各イテレーションでmainが実行されますが、disable_suspension呼び出し後はサスペンションは発生しません // 発生し、ループが終了まで実行され、main() に戻らない。 co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // 最初の要素 == 0 のみを出力する for (int k{}; k < 4; ++k) { coro(); // 1 2 3 4 を各反復ごとに1つずつ出力する std::cout << ": "; } coro.disable_suspension(); coro(); // 末尾の番号 5 6 7 を一度に出力する } 出力: 0 1 : 2 : 3 : 4 : 5 6 7 |
co_yield
co_yield
式は呼び出し元に値を返し、現在のコルーチンを中断します:これは再開可能なジェネレータ関数の基本的な構成要素です。
co_yield
式
|
|||||||||
co_yield
波括弧初期化リスト
|
|||||||||
これは以下と同等です
co_await promise.yield_value(expr)
典型的なジェネレータの
yield_value
は、その引数を(引数の寿命が
co_await
内部のサスペンションポイントを跨ぐため、コピー/ムーブするか単にアドレスを保存して)ジェネレータオブジェクトに格納し、
std::suspend_always
を返して、呼び出し元/再開元に制御を移します。
#include <coroutine> #include <cstdint> #include <exception> #include <iostream> template<typename T> struct Generator { // クラス名 'Generator' は任意の選択であり、コルーチンに必須ではありません // マジック。コンパイラは 'co_yield' キーワードの存在によってコルーチンを認識します。 // 'MyGenerator'(または任意の他の名前)という名前を使用できます。ただし、含める限り // 'MyGenerator get_return_object()' メソッドを持つネストされた構造体 promise_type。 // (注: コンストラクタとデストラクタの宣言を調整する必要があります // リネーム時に使用する。 struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type // required { T value_; std::exception_ptr exception_; Generator get_return_object() { return Generator(handle_type::from_promise(*this)); } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { exception_ = std::current_exception(); } // 保存 // 例外 template<std::convertible_to<T> From> // C++20 concept std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // 結果をpromiseにキャッシュ return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // コルーチンが完了したかどうかを確実に判断する唯一の方法は、 // 次の値が生成されるかどうか (co_yield) // C++ゲッター(下記のoperator ())を介したコルーチンでの実行/再開 // 次のco_yieldポイントまでコルーチンを継続(または終了まで実行)。 // 次に、結果をpromiseに保存/キャッシュして、ゲッター(下記のoperator())でアクセスできるようにします // コルーチンを実行せずに取得するため。 return !h_.done(); } T operator()() { fill(); full_ = false; // 以前にキャッシュされたものを移動します // プロミスを再度空にするための結果 return std::move(h_.promise().value_); } private: bool full_ = false; void fill() { if (!full_) { h_(); if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_); // 呼び出し元コンテキストでコルーチン例外を伝播 full_ = true; } } }; Generator<std::uint64_t> fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw std::runtime_error("フィボナッチ数列が大きすぎます。要素がオーバーフローします。"); co_yield 0; if (n == 1) co_return; co_yield 1; if (n == 2) co_return; std::uint64_t a = 0; std::uint64_t b = 1; for (unsigned i = 2; i < n; ++i) { std::uint64_t s = a + b; co_yield s; a = b; b = s; } } int main() { try { auto gen = fibonacci_sequence(10); // uint64_t がオーバーフローする前に最大94 for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "例外: " << ex.what() << '\n'; } catch (...) { std::cerr << "不明な例外。\n"; } }
出力:
fib(0)=0 fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34
注記
| 機能テスト マクロ | 値 | 標準 | 機能 |
|---|---|---|---|
__cpp_impl_coroutine
|
201902L
|
(C++20) | コルーチン (コンパイラサポート) |
__cpp_lib_coroutine
|
201902L
|
(C++20) | コルーチン (ライブラリサポート) |
__cpp_lib_generator
|
202207L
|
(C++23) | std::generator : 範囲のための同期コルーチンジェネレータ |
キーワード
co_await 、 co_return 、 co_yield
ライブラリサポート
コルーチンサポートライブラリ は、コルーチンに対するコンパイル時および実行時サポートを提供するいくつかの型を定義します。
不具合報告
以下の動作変更の欠陥報告書は、以前に公開されたC++規格に対して遡及的に適用されました。
| DR | 適用対象 | 公開時の動作 | 正しい動作 |
|---|---|---|---|
| CWG 2556 | C++20 |
無効な
return_void
によりコルーチンの終端からの
フォールスルー動作が未定義となっていた |
この場合プログラムは
不適格となる |
| CWG 2668 | C++20 | co_await がラムダ式内で使用できなかった | 許可される |
| CWG 2754 | C++23 |
明示的オブジェクトメンバー関数のpromiseオブジェクト
構築時に * this が取得されていた |
この場合
*
this
は
取得されない |
関連項目
|
(C++23)
|
同期
view
を表す
coroutine
ジェネレータ
(クラステンプレート) |
外部リンク
| 1. | Lewis Baker, 2017-2022 - 非対称転送。 |
| 2. | David Mazières, 2021 - C++20コルーチンに関するチュートリアル。 |
| 3. | Chuanqi Xu & Yu Qi & Yao Han, 2021 - C++20コルーチンの原理と応用。(中国語) |
| 4. | Simon Tatham, 2023 - カスタムC++20コルーチンシステムの作成。 |