C++20のコルーチンについて個人的にまとめたものです。内容には間違いが含まれている場合があります。
概要
- コルーチンとは「中断したり再開したりできる関数」のこと
- C++20のコルーチンは”スタックレス”
- 中断時のスタックの状態を保存しない方式
- 以下を満たす関数がコルーチンになる
- 以下のいずれかを含む:
co_await:式の評価が完了するまで関数を中断するco_return:関数を終了して、戻り値を返すco_yield:関数を中断して、戻り値を返す
- 戻り値の型と引数の型から
std::coroutine_traits<>を介してPromise型を取得できる
- 以下のいずれかを含む:
- コルーチンのステートは(基本的には)動的に割り当てられる
- Promiseオブジェクト、コルーチンのパラメータ、中断地点の情報、中断時に有効なローカル変数が保持される
- ただし、ステートの寿命が呼び出し元の寿命よりも短く、割当サイズが呼び出し側でわかっていれば、スタック上に割り当てられる
構成
- Promise:
- 内側からのコルーチン操作に用いるユーザー定義クラス
- 以下のメンバ関数を定義する:
- コンストラクタ:
- コルーチンのパラメータをすべて受け取るコンストラクタか、デフォルトコンストラクタのいずれかがあれば良い
get_return_object():- コルーチンの戻り値を作る
- 基本は、
std::coroutine_handle<Promise>::from_promise(*this)でハンドルを返す
- (任意)
static get_return_object_on_allocation_failure():- コルーチンステートの割当に失敗したときのコルーチンの戻り値を作る
- これが定義されていると、nothrow版のnewで割当を行うようになる
initial_suspend():- 初期化後に呼び出し元に戻るときに呼び出される
- この戻り値が
co_awaitされる
final_suspend():- 終了前に呼び出し元に戻るときに呼び出される
- この戻り値が
co_awaitされる
yield_value(expr):co_yieldされた値を引数にして呼び出される- この戻り値が
co_awaitされる
return_void(),return_value(expr):co_returnされた値を引数にして呼び出される
- (任意)
unhandled_exception():- コルーチン内で例外がキャッチされなかったときに、代わりにシステムがキャッチしてこれを呼び出す
- (任意)
await_transform(expr):co_awaitされた値をawaitableに変換する- これが定義されていなければ、
co_awaitされた値がそのままawaitableとして扱われる
- (任意)
operator new():- これが定義されていれば、ステートを格納するためのメモリを割り当てるときに呼び出される
size_tとコルーチンのすべてのパラメータを引数に取ることもできる
- コンストラクタ:
- Awaitable:
co_awaitされた値から変換された中間的なオブジェクトawaitable.operator co_await()かoperator co_await(Awaitable&&)でAwaiterに変換される- これらが定義されていなければ、awaitableがそのままawaiterとして扱われる
- Awaiter:
- 中断の挙動を定義するユーザー定義クラス
- 以下のメンバ関数が定義される:
await_ready():- 中断前に呼び出され、中断状態に入るか判断する
trueを返すと、そのまま再開後処理に合流するfalseを返すと、中断状態に移行する
await_suspend(handle):- 中断後に呼び出され、制御を返す先を決定する
- 戻り値が
voidかtrueの場合、呼び出し元に制御を返す - 戻り値が
falseの場合、現在のコルーチンを再開する - 戻り値がハンドルの場合、そのハンドルのコルーチンを再開する
- 例外を投げると、現在のコルーチンを再開して、そちらでrethrowする
await_resume():- 再開後に呼び出され、その戻り値が
co_awaitの結果として渡される
- 再開後に呼び出され、その戻り値が
- 定義済みのAwaiterとして、常に中断する
std::suspend_alwaysと常に中断しないstd::suspend_neverがある
- コルーチンの戻り値の型:
- 外側からのコルーチン操作に用いるユーザー定義クラス
std::coroutine_traits<>で対応するPromise型を取得できる必要がある- クラス内に
promise_typeという型を定義するか、std::coroutine_traits<>を特殊化するか
- クラス内に
- 基本は、
std::coroutine_handle<>を保持して、コルーチンの実行状態を管理する
動作
- 開始時:
- ステートをnewする
Promise::operator new()、Promise::operator new(size_t, Args...)、::operator new()のいずれかを呼ぶArgsはコルーチンの引数すべてと一致する必要がある
Promise::get_return_object_on_allocation_failure()を定義していれば、代わりにnothrow版を使う - 失敗時、この関数を呼び、その戻り値とともに呼び出し元に返る
- コルーチンの引数をステートにコピー・ムーブする
- 参照は参照のまま維持される
- Promiseをステート上に構築する
Promise::Promise(Args...)、Promise::Promise()のいずれかを呼ぶArgsはコルーチンの引数すべてと一致する必要がある- 渡される引数はステート上にある
promise.get_return_object()を呼ぶ- 初回中断時、この戻り値とともに呼び出し元に返る
promise.initial_suspend()を呼び、その戻り値をco_awaitする
- ステートをnewする
co_await時:- exprをawaitableに変換する
promise.await_transform(expr)があれば呼ぶ- なければ、exprがそのままawaitableになる
- awaitableをawaiterに変換する
awaitable.operator co_await()、operator co_await(Awaitable&&)のいずれかがあれば呼ぶ- なければ、awaitableがそのままawaiterになる
awaiter.await_ready()を呼ぶ- 戻り値が
trueなら、awaiter.await_resume()を呼び、その戻り値とともにco_awaitに返る - 戻り値が
falseなら、現在のコルーチンを中断する
- 戻り値が
awaiter.await_suspend(handle)を呼ぶhandleは現在のコルーチンを指すハンドル- 戻り値が
voidかtrueなら、呼び出し元に制御を返す - 戻り値が
falseなら、現在のコルーチンを再開し、awaiter.await_resume()を呼び、その戻り値とともにco_awaitに返る - 戻り値がハンドルなら、そのハンドルの指すコルーチンを再開する
- 例外を投げると、現在のコルーチンを再開し、その中でrethrowする
- コルーチンを再開すると、
awaiter.await_resume()を呼び、その戻り値とともにco_awaitに返る
- exprをawaitableに変換する
co_yield時:promise.yield_value(expr)を呼び、その戻り値をco_awaitする
co_return時:promise.return_void()、promise.return_value(expr)のいずれかを呼ぶpromise.final_suspend()を呼び、その戻り値をco_awaitする- ここで中断した場合、このコルーチンを再開することは未定義動作になるので、呼び出し元でハンドルをdestroyして処分する必要がある
- ステートをdeleteし、呼び出し元に制御を返す
- 例外がキャッチされなかった場合:
- 代わりにシステムがキャッチして、
promise.unhandled_exception()を呼び出す - ステートをdeleteし、呼び出し元に制御を返す
- 代わりにシステムがキャッチして、
注意点
- コルーチンのパラメータは参照を参照のまま保持するので、コルーチンが動いている間に参照先の変数の寿命を切らさないようにしなければならない
std::threadのように、参照渡しした引数をdecay-copyしたりしない
- クラスのメンバー関数をコルーチンにする場合には、コルーチンが動いている間にそのインスタンスの寿命を切らさないようにしなければならない
- メンバー関数をコルーチンにしても、そのインスタンスがコルーチンステートに保存されたりしない
- クロージャーをコルーチンにする場合も同様に、そのクロージャーオブジェクトの寿命に注意しなければならない
final_suspend()で中断しなかった場合、呼び出し元はそのハンドルを操作してはいけない- 呼び出し元に制御が戻された時点でステートは破棄されている
- 残されたハンドルからコルーチンが完了したかを知るすべはない
final_suspend()で中断した場合、そのコルーチンを再開してはいけない- この状態のコルーチンを再開することは未定義動作になる
- 呼び出し元はpromise経由で処理結果を受け取るなどしたら、
handle.destroy()で速やかに破棄する必要がある