コルーチン (C++20)
コルーチンとは、実行を一時停止して後で再開できる関数のことです。コルーチンはスタックレスです。呼び出し元に制御を返すことで実行を一時停止し、実行の再開に必要なデータはスタックとは別に保存されます。これにより、非同期に実行されるシーケンシャルコード(例えば、明示的なコールバックなしでノンブロッキングI/Oを処理するなど)が可能になり、遅延計算される無限シーケンス上のアルゴリズムなどもサポートされます。
関数が以下のいずれかを含む場合、その関数はコルーチンです。
- 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)); } }
- co_yield 式 — 値を返して実行を一時停止する
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- co_return 文 — 値を返して実行を完了する
lazy<int> f() { co_return 7; }
すべてのコルーチンは、以下に示すいくつかの要件を満たす戻り値の型を持たなければなりません。
目次 |
[編集] 制限
コルーチンは、可変引数、通常のreturn文、またはプレースホルダー戻り値の型(autoまたはConcept)を使用できません。
consteval関数、constexpr関数、コンストラクタ、デストラクタ、およびmain関数はコルーチンにできません。
[編集] 実行
各コルーチンには以下が関連付けられます。
- コルーチンの内部から操作されるプロミスオブジェクト。コルーチンはこのオブジェクトを通じて結果または例外を送信します。プロミスオブジェクトは
std::promiseとは一切関係ありません。 - コルーチンの外部から操作されるコルーチンハンドル。これは、コルーチンの実行を再開したり、コルーチンフレームを破棄したりするために使用される非所有ハンドルです。
- 内部的な動的に確保されたストレージであるコルーチン状態(アロケーションが最適化されない限り)。これは以下のオブジェクトを含みます。
- プロミスオブジェクト
- パラメータ(すべて値渡しでコピーされる)
- 現在のサスペンションポイントの何らかの表現。これにより、再開時にどこから続行するか、破棄時にどのローカル変数がスコープ内にあったかがわかります。
- 現在のサスペンションポイントをまたぐライフタイムを持つローカル変数および一時オブジェクト。
コルーチンは実行を開始すると、以下の処理を行います。
-
operator newを使用してコルーチン状態オブジェクトをアロケートします。 - すべての関数パラメータをコルーチン状態にコピーします。値渡しパラメータは移動またはコピーされ、参照渡しパラメータは参照のままです(したがって、参照先のオブジェクトのライフタイムが終了した後にコルーチンが再開された場合、ダングリングになる可能性があります — 例については以下を参照)。
- プロミスオブジェクトのコンストラクタを呼び出します。プロミス型がすべてのコルーチンパラメータを受け取るコンストラクタを持つ場合、そのコンストラクタがコピー後のコルーチン引数で呼び出されます。それ以外の場合はデフォルトコンストラクタが呼び出されます。
- promise.get_return_object()を呼び出し、その結果をローカル変数に保持します。この呼び出しの結果は、コルーチンが最初に一時停止したときに呼び出し元に返されます。このステップまででスローされた例外はすべて、プロミスには格納されず、呼び出し元に伝播します。
- 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} destroyed h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // returned coroutine can't be resumed without committing use after free } void bad3() { coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine { std::cout << i; co_return; }(); // immediately invoked // lambda destroyed h.resume(); // uses (anonymous lambda type)::i after free h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // make i a coroutine parameter { std::cout << i; co_return; }(0); // lambda destroyed h.resume(); // no problem, i has been copied to the coroutine // frame as a by-value parameter h.destroy(); }
コルーチンがサスペンションポイントに到達すると
- 以前に取得された戻りオブジェクトは、必要に応じてコルーチンの戻り値の型への暗黙的な変換の後、呼び出し元/再開元に返されます。
コルーチンがco_return文に到達すると、以下の処理を行います。
- 以下のためにpromise.return_void()を呼び出します。
- co_return;
- co_return expr;(exprがvoid型の場合)
- または、co_return expr;(exprが非void型の場合)のためにpromise.return_value(expr)を呼び出します。
- 自動記憶期間を持つすべての変数を、作成された順序とは逆順に破棄します。
- promise.final_suspend()を呼び出し、その結果をco_awaitします。
コルーチンの末尾に到達することは、co_return;と等価ですが、Promiseのスコープ内でreturn_voidの宣言が見つからない場合、動作は未定義です。関数本体に定義キーワードのいずれも含まれない関数は、その戻り値の型に関わらずコルーチンではなく、末尾に到達した場合に、戻り値の型が(cv修飾された可能性のある)voidでない場合、未定義の動作となります。
// assuming that task is some coroutine task type task<void> f() { // not a coroutine, undefined behavior } task<void> g() { co_return; // OK } task<void> h() { co_await g(); // OK, implicit co_return; }
コルーチンが未捕捉例外で終了した場合、以下の処理を行います。
- 例外をキャッチし、そのキャッチブロック内からpromise.unhandled_exception()を呼び出します。
- promise.final_suspend()を呼び出し、その結果をco_awaitします(例えば、継続を再開したり、結果を公開したりするため)。この時点からコルーチンを再開することは未定義の動作です。
コルーチン状態が、co_returnまたは未捕捉例外によって終了したため、またはそのハンドルを通じて破棄されたために破棄されると、以下の処理を行います。
- プロミスオブジェクトのデストラクタを呼び出します。
- 関数パラメータのコピーのデストラクタを呼び出します。
- コルーチン状態が使用するメモリを解放するために
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 { /* ... */ // ensure the use of non-throwing operator-new static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // or, return Coroutine(nullptr); } // custom non-throwing overload of new void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // allocation failure } };
[編集] プロミス
Promise型は、コンパイラによってコルーチンの戻り値の型からstd::coroutine_traitsを使用して決定されます。
正式には、
-
RとArgs...はそれぞれコルーチンの戻り値の型とパラメータ型リストを表し、 -
ClassTは、コルーチンが非静的メンバ関数として定義されている場合のクラス型を表し、 - cvは、コルーチンが非静的メンバ関数として定義されている場合の関数宣言で宣言されたcv修飾を表します。
そのPromise型は以下のように決定されます。
- コルーチンが暗黙のオブジェクトメンバ関数として定義されていない場合、std::coroutine_traits<R, Args...>::promise_type。
- コルーチンがrvalue参照修飾されていない暗黙のオブジェクトメンバ関数として定義されている場合、std::coroutine_traits<R,
cvClassT&, Args...>::promise_type。 - コルーチンがrvalue参照修飾された暗黙のオブジェクトメンバ関数として定義されている場合、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 expr |
|||||||||
co_await式は、通常の関数本体(ラムダ式の関数本体を含む)内の潜在的に評価される式の中にのみ現れることができ、以下の場所には現れません。
- ハンドラ内、
- 宣言文内。ただし、その宣言文の初期化子に現れる場合を除く。
- init-statementの単純宣言内(
if、switch、for、[[../range-for|range-for]]を参照)。ただし、そのinit-statementの初期化子に現れる場合を除く。 - デフォルト引数内、または
- 静的またはスレッドの記憶期間を持つブロックスコープ変数の初期化子内。
| (C++26以降) |
まず、exprは次のようにawaitableに変換されます。
- exprが初期サスペンションポイント、最終サスペンションポイント、またはyield式によって生成された場合、awaitableはexprそのものです。
- それ以外の場合、現在のコルーチンの
Promise型がメンバ関数await_transformを持っている場合、awaitableはpromise.await_transform(expr)です。 - それ以外の場合、awaitableはexprそのものです。
次に、アウェイターオブジェクトが次のように取得されます。
- operator co_awaitのオーバーロード解決により単一の最適なオーバーロードが与えられる場合、アウェイターはその呼び出しの結果です。
- メンバオーバーロードの場合、awaitable.operator co_await()。
- 非メンバオーバーロードの場合、operator co_await(static_cast<Awaitable&&>(awaitable))。
- それ以外の場合、オーバーロード解決でoperator co_awaitが見つからない場合、アウェイターはawaitableそのものです。
- それ以外の場合、オーバーロード解決が曖昧である場合、プログラムは不適格です。
上記の式がprvalueである場合、アウェイターオブジェクトはそこから実体化された一時オブジェクトです。そうでない場合、上記の式がglvalueである場合、アウェイターオブジェクトはそれが参照するオブジェクトです。
次に、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()関数が戻る前に再開される可能性があります。(ただし、デフォルトのメモリ安全規則は引き続き適用されるため、ロックなしでコルーチンハンドルがスレッド間で共有される場合、アウェイターは少なくともリリースセマンティクスを使用し、再開側は少なくともアクワイアセマンティクスを使用する必要があります。)例えば、コルーチンハンドルはコールバック内に配置され、非同期I/O操作が完了したときにスレッドプールで実行するようにスケジュールできます。その場合、現在のコルーチンが再開されてアウェイターオブジェクトのデストラクタが実行されている可能性があり、それは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(); }); // Potential undefined behavior: accessing potentially destroyed *this // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK } 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 destroyed here 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
注:アウェイターオブジェクトはコルーチン状態の一部であり(一時的なもので、ライフタイムがサスペンションポイントをまたぎます)、co_await式が終了する前に破棄されます。これは、一部の非同期I/O APIで必要とされる操作ごとの状態を、追加の動的アロケーションに頼ることなく維持するために使用できます。
標準ライブラリは、2つの自明なアウェイタブルを定義しています。std::suspend_alwaysとstd::suspend_neverです。
| このセクションは未完成です 理由: 例 |
| promise_type::await_transformとプログラム提供のアウェイターのデモ |
|---|
[編集] 例このコードを実行 #include <cassert> #include <coroutine> #include <iostream> struct tunable_coro { // An awaiter whose "readiness" is determined via constructor's parameter. class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // Three standard awaiter interface functions: 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(); } // A user provided transforming function which returns the custom 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); } // For simplicity, declare these 4 special functions as deleted: 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 << ' '; // The awaiter passed to co_await goes to promise_type::await_transform which // issues tunable_awaiter that initially causes suspension (returning back to // main at each iteration), but after a call to disable_suspension no suspension // happens and the loop runs to its end without returning to main(). co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // emits only one first element == 0 for (int k{}; k < 4; ++k) { coro(); // emits 1 2 3 4, one per each iteration std::cout << ": "; } coro.disable_suspension(); coro(); // emits the tail numbers 5 6 7 all at ones } 出力 0 1 : 2 : 3 : 4 : 5 6 7 |
[編集] co_yield
co_yield式は呼び出し元に値を返し、現在のコルーチンを一時停止します。これは、再開可能なジェネレータ関数の一般的な構成要素です。
co_yield expr |
|||||||||
co_yield braced-init-list |
|||||||||
これは以下と同等です。
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 { // The class name 'Generator' is our choice and it is not required for coroutine // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword. // You can use name 'MyGenerator' (or any other name) instead as long as you include // nested struct promise_type with 'MyGenerator get_return_object()' method. // (Note: It is necessary to adjust the declarations of constructors and destructors // when renaming.) 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(); } // saving // exception template<std::convertible_to<T> From> // C++20 concept std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // caching the result in promise return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // The only way to reliably find out whether or not we finished coroutine, // whether or not there is going to be a next value generated (co_yield) // in coroutine via C++ getter (operator () below) is to execute/resume // coroutine until the next co_yield point (or let it fall off end). // Then we store/cache result in promise to allow getter (operator() below // to grab it without executing coroutine). return !h_.done(); } T operator()() { fill(); full_ = false; // we are going to move out previously cached // result to make promise empty again 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_); // propagate coroutine exception in called context full_ = true; } } }; Generator<std::uint64_t> fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow."); 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); // max 94 before uint64_t overflows for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "Exception: " << ex.what() << '\n'; } catch (...) { std::cerr << "Unknown exception.\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: 範囲のための同期コルーチンジェネレータ |
[編集] キーワード
[編集] ライブラリサポート
コルーチンサポートライブラリは、コルーチンのコンパイル時および実行時サポートを提供するいくつかの型を定義しています。
[編集] 欠陥報告
以下の動作変更を伴う欠陥報告が、以前に公開されたC++標準に遡って適用されました。
| DR | 適用対象 | 公開された動作 | 正しい動作 |
|---|---|---|---|
| CWG 2556 | C++20 | 無効なreturn_voidにより、コルーチンの末尾に到達した場合の動作が未定義になった。コルーチンの末尾に到達した場合、動作は未定義になった。 |
この場合、プログラムは 不正となる |
| CWG 2668 | C++20 | co_awaitはラムダ式で現れることができなかった。 | 許可 |
| CWG 2754 | C++23 | 明示的なオブジェクトメンバ関数のプロミスオブジェクトを構築する際に、*thisが取得された。 明示的なオブジェクトメンバ関数のプロミスオブジェクトを構築する際に、*thisが取得された。 |
*thisは この場合取得されない。 |
[編集] 関連項目
| (C++23) |
同期的なコルーチンジェネレータを表すview(クラステンプレート) |
[編集] 外部リンク
| 1. | Lewis Baker, 2017-2022 - Asymmetric Transfer. |
| 2. | David Mazières, 2021 - Tutorial on C++20 coroutines. |
| 3. | Chuanqi Xu & Yu Qi & Yao Han, 2021 - C++20 Principles and Applications of Coroutine. (Chinese) |
| 4. | Simon Tatham, 2023 - Writing custom C++20 coroutine systems. |