コルーチン (C++20)
コルーチンは後の再開のために実行を中断できる関数です。 コルーチンはスタックレスです。 実行は呼び出し元に戻ることによって中断され、実行の再開に必要なデータはスタックとは別に格納されます。 これにより逐次的なコードを非同期に実行する (例えば明示的なコールバックなしにノンブロッキング I/O を処理する) ことが可能となり、遅延計算される無限シーケンスに対するアルゴリズムや他の用途もサポートされます。
定義が以下のいずれかを行う場合、その関数はコルーチンです。
- 再開まで実行を中断するために co_await 演算子を使用する。
task<> tcp_echo_server() { char data[1024]; for (;;) { size_t n = co_await socket.async_read_some(buffer(data)); co_await async_write(socket, buffer(data, n)); } }
- 値を返して実行を中断するために co_yield キーワードを使用する。
generator<int> iota(int n = 0) { while(true) co_yield n++; }
- 値を返して実行を完了するために co_return キーワードを使用する。
lazy<int> f() { co_return 7; }
すべてのコルーチンは後述する多数の要件を満たす戻り値を持たなければなりません。
目次 |
[編集] 制限
コルーチンは、可変長引数、通常の return 文、プレースホルダ戻り値型 (auto
または Concept
) を使用することはできません。
constexpr 関数、コンストラクタ、デストラクタ、 main 関数は、コルーチンにできません。
[編集] 実行
それぞれのコルーチンには以下のものが紐付けられます。
- プロミスオブジェクト。 コルーチンの内側から操作されます。 コルーチンはこのオブジェクトを通してその結果または例外を提示します。
- コルーチンハンドル。 コルーチンの外側から操作されます。 これはコルーチンの実行を再開またはコルーチンのフレームを破棄するために使用される非所有のハンドルです。
- コルーチン状態。 これは、内部的な、ヒープ確保される (確保が最適化により削除された場合は除きます)、以下のものを含むオブジェクトです。
- プロミスオブジェクト。
- 引数 (すべて値によってコピーされます)。
- 再開時にどこから継続するのかを知るため、および破棄時にどのローカル変数がスコープ内であったかを知るための、現在の中断点の何らかの表現。
- 生存期間が現在の中断点を含むローカル変数および一時オブジェクト。
コルーチンの実行を開始するときは、以下のことが行われます。
- operator new を用いてコルーチン状態オブジェクトを確保します (後述)。
- すべての関数引数をコルーチン状態にコピーします。 値渡しの引数はムーブまたはコピーされ、参照渡しの引数は参照のままです (そのため参照先のオブジェクトの生存期間が終了した後にコルーチンが再開した場合はダングリングになります)。
- プロミスオブジェクトに対してコンストラクタを呼びます。 プロミス型がコルーチンの引数をすべて取るコンストラクタを持つ場合は、コピー後のコルーチン引数を用いてそのコンストラクタが呼ばれます。 そうでなければ、デフォルトコンストラクタが呼ばれます。
- promise.get_return_object() を呼び、その結果をローカル変数に保持します。 コルーチンが最初に中断したとき、この呼び出しの結果が呼び出し元に返されます。 この段階まで (この段階を含む) に投げられたあらゆる例外は、プロミスに置かれるのではなく、呼び出し元に伝播されます。
- promise.initial_suspend() を呼び、その結果を co_await します。 一般的なプロミス型は、遅延開始するコルーチンのために suspend_always を返すか、先行開始するコルーチンのために suspend_never を返すかの、いずれかです。
- co_await promise.initial_suspend() が再開したとき、コルーチンの本体の実行を開始します。
コルーチンが中断点に達したとき、
- それまでに取得された戻り値オブジェクトが、必要に応じて戻り値の型への暗黙の変換の後、呼び出し元または再開元に返されます��
コルーチンが co_return 文に達したときは、以下のことが行われます。
- 以下の場合は promise.return_void() を呼びます。
- co_return;
- co_return expr で expr が void 型の場合。
- void を返すコルーチンの終わりに達した。 この場合、プロミス型が Promise::return_void() メンバを持たなければ、動作は未定義です。
- co_return expr で expr が非 void 型の場合は、 promise.return_value(expr) を呼びます。
- 自動記憶域期間のすべての変数を、作成されたのと逆順で破棄します。
- promise.final_suspend() を呼び、その結果を co_await します。
コルーチンがキャッチされない例外で終了したときは、以下のことが行われます。
- その例外をキャッチし、その catch ブロック内から promise.unhandled_exception() を呼びます。
- promise.final_suspend() を呼び、その結果を co_await します (継続を再開するまたは結果を公開するために)。 この点からのコルーチンの再開は未定義動作です。
コルーチンが co_return またはキャッチされない例外によって終了したために、またはハンドルを通して破棄されたために、コルーチン状態が破棄されるときは、以下のことが行われます。
- プロミスオブジェクトのデストラクタを呼びます。
- 関数引数のコピーのデストラクタを呼びます。
- コルーチン状態によって使用されたメモリを解放するために operator delete を呼びます。
- 呼び出し元または再開元に実行を戻します。
[編集] ヒープ確保
コルーチン状態は非配列の operator new を通してヒープに確保されます。
Promise 型がクラスレベルの置き換えを定義している場合は、それが使用され、そうでなければ、グローバルな operator new が使用されます。
Promise 型が追加の引数を取る配置形式の operator new を定義しており、その第1引数が (std::size_t 型の) 要求されたサイズであり、残りがコルーチンの関数引数にマッチする引数リストを持つ場合は、それらの引数が operator new に渡されます (これはコルーチンに対する先頭アロケータ規約の使用を可能とします)。
以下の場合、 operator new の呼び出しは最適化によって削除できます (たとえカスタムアロケータが使用される場合でも)。
- コルーチン状態の生存期間が呼び出し元の生存期間内に厳密にネストしており、
- コルーチンフレームのサイズが呼び出し元に既知である。
この場合、コルーチン状態は呼び出し元のスタックフレーム (呼び出し元が普通の関数の場合) またはコルーチン状態 (呼び出し元がコルーチンの場合) に埋め込まれます。
確保が失敗した場合は、プロミス型がメンバ関数 Promise::get_return_object_on_allocation_failure() を定義していなければ、 std::bad_alloc が投げられます。 そのメンバ関数が定義されている場合は、確保には nothrow 形式の operator new が使用され、確保失敗時は、 Promise::get_return_object_on_allocation_failure() から取得されたオブジェクトが呼び出し元に即座に返されます。
[編集] プロミス
プロミス型は、 std::coroutine_traits を用いて、コルーチンの戻り値の型から、コンパイラによって決定されます。
コルーチンが task<float> foo(std::string x, bool flag); のように定義されている場合、その Promise 型は std::coroutine_traits<task<float>, std::string, bool>::promise_type です。
コルーチンが task<void> my_class::method1(int x) const; のような非静的メンバ関数の場合、その Promise 型は std::coroutine_traits<task<void>, const my_class&, int>::promise_type です。
This section is incomplete |
[編集] co_await
単項演算子 co_await は、コルーチンを中断し、制御を呼び出し元に返します。 被演算子の式は、 operator co_await を定義する型か、現在のコルーチンの Promise::await_transform を用いてそのような型に変換可能な型の、いずれかでなければなりません。
co_await expr
|
|||||||||
まず、 expr が以下のように awaitable に変換されます。
- expr が初期中断点、最終中断点、または yield 式によって生成された場合は、 awaitable は expr そのままです。
- そうでなく、現在のコルーチンのプロミス型がメンバ関数 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))) の結果です。
- そうでなく、オーバーロード解決が operator co_await を発見できない場合は、 awaiter は awaitable そのままです。
- そうでなく、オーバーロード解決が曖昧な場合は、プログラムは ill-formed です。
上記の式が prvalue の場合は、 awaiter オブジェクトはそれから具体化された一時オブジェクトです。 そうでなく、上記の式が 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() に入る前に完全に中断されるため、この関数は追加の同期なしにコルーチンハンドルをスレッド間で自由に転送できることに注意してください。 例えば、非同期 I/O 操作が完了したときにスレッドプール上で実行されるようにスケジュールされたコールバック内にそれを置くことができます。 これは、未だ await_suspend() の内部を実行中に、並行的に、現在のコルーチンがそのスレッドプール上で再開し、終了する可能性があるため、 await_suspend() は、ハンドルが他のスレッドに公開された後、 awaiter (*this オブジェクト) がアクセス可能であることを期待するべきでない、ということも意味します。
ノート: awaiter オブジェクトは (その生存期間が中断点を含む一時オブジェクトとして) コルーチン状態の一部であり、 co_await 式が終了する前に破棄されます。 これは、非同期 I/O API が要求する操作ごとの状態を追加のヒープ確保に頼ることなく維持するために使用することができます。
標準ライブラリは2つのトリビアルな awaitable、 std::suspend_always および std::suspend_never を定義しています。
This section is incomplete Reason: examples |
[編集] co_yield
yield 式は呼び出し元に値を返し、現在のコルーチンを中断します。 これは再開可能なジェネレータ関数の共通のビルディングブロックです。
co_yield expr
|
|||||||||
co_yield braced-init-list
|
|||||||||
これは以下と同等です。
co_await promise.yield_value(expr)
一般的なジェネレータの yield_value は、その引数をジェネレータオブジェクト内に格納し (コピーまたはムーブするか、単にそのアドレスを格納します (引数の生存期間は co_await 内部の中断点を含むため))、 std::suspend_always を返して制御を呼び出し元または再開元に転送します。
This section is incomplete Reason: examples |
[編集] ライブラリサポート
This section is incomplete |