はじめに
コルーチンの中断のカスタマイズポイントとして
operator co_await
await_transform
の二つがあります。
コルーチンライブラリを作る際には都合のいいほうを選んで使えばよいと思いますが、これら2つを改めて比べてみました。
ただの雑談:
MSVCがバグってて再開時に値を返すようなAwaiterをawait_transformで実装したら何故か致命的なコンパイルエラーでコンパイルできないケースがあったが試しにoperator co_awaitで実装しなおしたらコンパイルが通ったこともあった
コンパイラのバグは引きたくないですね
operator co_await
任意の型をAwaiterへ変換する
基本形
auto operator co_await(const Type&)
{
return awaiter{};
}
メンバ関数でもOK
struct Type
{
auto operator co_await() const
{
return awaiter{};
}
};
await_transform
Promise側から指定する形で任意の型をAwaiterに変換する
基本形
struct promise_type
{
// 他のメソッド省略…
auto await_transform(const Type&) const
{
return awaiter{};
}
};
なお、
operator co_await
とawait_transform
両方ある場合は、
await_transform
が優先される
どちらを使うのが良さそうか
-
AwaiterがPromiseに全く依存しない場合
→ operator co_await
(await_transformで同じ実装を何度もする必要がなくなるので) -
Promiseごとに変換するAwaiterが型レベルで異なる
→ await_transform
(operator co_awaitの定義をPromiseごとに複数定義はできないので当然) -
それ以外
→ 都合のいいほうを使う
個人的には、Promiseに依存するようなAwaiterは、
なんの問題もなければawait_transformを使っておくのがベターそうに思う
実装の切り替え
operator co_await から await_transform
Awaiterが依存するPromiseごとにawait_transformを実装するだけ
(この時await_suspendの仮引数がstd::coroutine_handle<>
で済むようなケースはそもそもPromiseに依存していないのでawait_transformを使うべきではなさそう)
auto operator co_await(const Type&)
{
// something
struct awaiter {
bool await_ready() const noexcept { return true;}
void await_suspend(std::coroutine_handle<promise_type>)
{
// something with promise
}
void await_suspend(std::coroutine_handle<promise_type2>)
{
// something with promise2
}
void await_resume() {}
};
return awaiter{};
}
変換後
struct awaiter
{
bool await_ready() const noexcept { return true;}
void await_suspend(std::coroutine_handle<promise> h)
{
// something with promise
}
void await_suspend(std::coroutine_handle<promise2> h)
{
// something with promise2
}
void await_resume() {}
};
struct promise_type
{
// 他のメソッド省略…
auto await_transform(const Type&) const
{
// something
return awaiter{};
}
};
struct promise_type2
{
// 他のメソッド省略…
auto await_transform(const Type&) const
{
// something
return awaiter{};
}
};
await_transform から operator co_await
こちらは処理次第ではそのまま移植できないこともあるので注意がいる
たとえば、await_transformの動作やAwaiterの状態が、Promiseに依存してしまう以下のようなケース
struct promise_type
{
auto await_transform(const Type& v)
{
// Promiseの状態に依存して、動作が変わる
bool ready = this->something(v);
struct awaiter {
bool ready{};
bool await_ready() const noexcept
{
return ready;
}
void await_suspend(std::coroutine_handle<>) {}
void await_resume() {}
};
// Awaiterの状態がPromiseに依存
return awaiter{ready};
}
};
operator co_awaitでは任意のPromiseの状態にアクセスできないので、同じように実装しようにも上手くいかない。
この場合はもろもろの動作タイミングをawait_suspend
時に遅延されるなどの工夫がいる
auto operator co_await(const Type& v)
{
struct awaiter {
Type v; // もしPromiseの状態と複合して絡むならフィールドにもつしかない
bool await_ready() const noexcept
{
// await_suspendに流れるように中断させる(false)
return false;
}
bool await_suspend(std::coroutine_handle<promise_type> h)
{
// もろもろの動作をawait_suspendに遅延させる
bool ready = h.promise().something(v);
return not ready;
}
void await_resume() {}
};
return awaiter{v};
}
await_suspend
はboolの返り値をもつこともできる
この際に、trueを返せば中断、falseを返せばこのコルーチンを再開させることになります。
上記の例のようにPromiseに依存してawait_readyの結果が変わるような場合は代わりにawait_suspend時点で判断する必要があります。
この時に、await_readyで返す値と反転することに注意がいります。
(await_readyはfalseを返すと中断)
return not ready;
まとめ
- コルーチン中断のカスタマイズポイントは2つある
-
operator co_await
- Promiseに依存することなく任意の型をAwaiterに変換
- Promiseに依存しないケースならこちらを使うのが良さそう
-
await_transform
- Promiseごとに指定する形で任意の型をAwaiterに変換
- Promiseに依存するケースならこちらを使うのが良さそう
-
- どちらも実装されている場合は
await_transform
が優先 - 相互変換は基本可能そうだが
- operator co_awaitからawait_transformは、そのまま移植可能
- await_transformからoperator co_awaitは、そのまま移植できないケースがありうるので工夫が必要