最終更新:2026-10-12
C++20 コルーチン入門¶
1.Coroutines とは何か¶
C++20 以降、ついに C++ はコルーチンをサポートするようになりました。C++23 では追加のサポートがもたらされるはずで、少なくとも最も一般的なユースケースはカバーされるでしょう。
コルーチン(英語:coroutine)はコンピュータプログラムの一類のコンポーネントで、協調的マルチタスクを一般化したサブルーチンであり、実行の一時停止と再開を許します。サブルーチンと比べると、コルーチンはより一般的で柔軟ですが、実践での使用はサブルーチンほど広くありません。コルーチンは、協調的マルチタスク、例外処理、イベントループ、イテレータ、無限リスト、パイプラインなど、互いに親和性の高いプログラムコンポーネントの実装に適しています。
ウィキペディアからの引用は、コルーチンの基本的な考え方を説明しています。
C++ の観点からは、コルーチンを**co_return、co_yield または co_await を含む任意の関数**と単純に理解できます。
現在の C++20 コルーチンは関数オブジェクト上の構文糖であり、コンパイラはコルーチンを囲むコードフレームワークを生成します。このコードはユーザー定義の**promise types**に依存します。C++23 以降に変更があるかもしれませんが、それまでは C++20 のルールに従って、これらの型を自分で書く必要があります。
前述の co_return、co_yield、co_await については、今後の節で解説します。
2.Step By Step Coroutine¶
次に、簡単なコルーチンの例から始めます。次のコルーチンは何もしないので、HelloTask コルーチンと呼ぶことにします。
#include <coroutine>
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
HelloTask myCoroutine() {
co_return; // make it a coroutine
}
int main() {
HelloTask x = myCoroutine();
}
このコードをコンパイルして実行します:
コンパイラが C++20 機能をサポートしている必要があります。
この HelloWorld プログラムを上から下に見ていき、普段の開発と何が違うのかを確認しましょう。
2.1 ヘッダファイル¶
まずヘッダファイルでcoroutineをインクルードしています。ここでは gcc-10 のソースコードを直接見て、実際に何をしているのか確認しましょう。
https://github.com/gcc-mirror/gcc/blob/releases/gcc-10/libstdc++-v3/include/std/coroutine
内容は以下の通りです:
// 用于发现协程promise_type的特征类型
// 核心代码:using promise_type = typename _Result::promise_type;
1.coroutine_traits
// 协程句柄,用于管理暂停或执行的协程以及Promise
2.coroutine_handle
// coroutine_handle全特化版本 是其他特化的公开基类。它保有handle的底层 地址。
2.1 coroutine_handle<void>
// coroutine_handle泛化版本
2.2 template <typename _Promise>
struct coroutine_handle : coroutine_handle<>
// coroutine_handle全特化版本 处理无操作协程
2.3 template <>
struct coroutine_handle<noop_coroutine_promise> : public coroutine_handle<>
3.平凡多可等待对象 Trivial awaitables
// await表达式应该暂停
3.1 suspend_always
// await表达式不应该暂停
3.2 suspend_never
多くの専門用語が含まれていることがわかります:
- coroutine_handle
- Promise
- awaitable
など。
今後解説するコルーチンの内容は、すべてこれらを中心に展開されます。
2.2 Promise¶
上の HelloTask を続けて見ると、次のような一節があることに気づきます:
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
さて問題です。なぜここで promise_type を書くのでしょうか?
promise_type を書く理由は、上述の2.1節で触れた handle にあります。コンパイラがコルーチンの handle を作成するとき、そのコルーチンオブジェクトの promise_type を指定する必要があるため、すべてのコルーチンオブジェクトは基本的に HelloTask::promise_type 構造体を持ちます。注意深い方ならもうお分かりでしょう。ここで promise_type を渡すと、Handle の中でそれが解析され、現在のユーザー定義コルーチンオブジェクトの動作を判断できるようになります。
さらに promise_type の中身を見てみましょう。ここにある内容はすべて固定です。後の内容が増えるにつれて、関わる関数も増えていきます。例えば、ここではreturn_voidを使っていますが、後で co_return でオブジェクトを返すようになると、別の関数が使われます。
**get_return_object**は現在のユーザー定義オブジェクトを返せます。ここでは何もしていませんが、前述の2.1を受けて、coroutine_handle を使って行うこともできます。
struct promise_type {
using Handle = std::coroutine_handle<promise_type>;
HelloTask get_return_object() {
return HelloTask{Handle::from_promise(*this)};
}
// other member function
}
ここでは coroutine_handle の from_promise メンバを使ってオブジェクトを構築しているのがわかります。
initial_suspend:コルーチンが最初の実行時に一時停止したままにすべきか、つまりコルーチンのインスタンス化時に一時停止したままにするかをコンパイラに伝えます。
final_suspend:initial_suspend と逆で、コルーチン関数本体の完了後、コルーチンの最後に final_suspend があります。これにより追加のクリーンアップを行う機会が得られます。
この2つはどちらも awaitable を返します。ここではヘッダファイルの suspend_always を使っていますが、もちろんユーザーが awaitable をカスタムすることもできます。これについては後の記事で解説します。
suspend_always と suspend_never のソースコードは以下の通りです:
struct suspend_always
{
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
struct suspend_never
{
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
ここにはいくつかのメンバが含まれています:
- await_ready
- 結果がすでに準備できているとわかる場合、同期的に完了するか?
- await_suspend
- 戻り値は bool、void、coroutine handle のいずれかになれます。
- ここでは void を返しており、その場合、直ちに現在のコルーチンの caller/resumer に制御を戻します(このコルーチンは引き続き一時停止されたまま)。await_suspend が bool を返す場合、true は制御権を caller/resumer に渡すことを、false は現在のコルーチンを再開することを意味します。coroutine handle を返す場合については、後の記事で解説します。
- await_resume
- その結果が co_await 式全体の結果になります。したがって戻り値は void とは限らず、カスタムオブジェクトなどでも構いません。
return_void: co_return;を使うとき、return_void が呼ばれます。
unhandled_exception:例外を捕捉するために使われます。
最後に、コルーチンを作成する一歩が残っています。最初の定義によれば、コルーチンは**co_return、co_yield または co_await を含む任意の関数**です。したがって:
2.3 全体の流れ¶
全体の呼び出しフローは何でしょうか?本節では上述のコードの実行ロジックを詳しく説明します。始める前にコードを変更し、print 操作をいくつか追加します:
#include <iostream>
#include <coroutine>
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { std::cout << "get_return_object" << std::endl; return { }; }
std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
void return_void() { std::cout << "return_void" << std::endl; }
void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
};
};
HelloTask myCoroutine() {
std::cout << "create coroutine" << std::endl;
co_return; // make it a coroutine
}
int main() {
std::cout << "start main" << std::endl;
HelloTask x = myCoroutine();
std::cout << "end main" << std::endl;
}
このときの出力:
フローは以下の通りです:
- start main
- ヒープ上にコルーチンオブジェクトを割り当て、promise を構築し、promise.get_return_object を呼び出して現在のオブジェクトを返します(このとき get_return_object が出力されます)。こうする目的は、結果をローカル変数に保存するためです。
- promise.initial_suspend、このとき initial_suspend を出力します。ここで co_await がトリガーされ、create coroutine を出力し、コルーチン本体を実行した後、co_return を呼び出します。
- co_return が promise.return_void を呼び出し、return_void を出力します。
- 最後に、promise.final_suspend が final_suspend を出力します。
- end main
コルーチンに関する内容はまだ非常に多くあります。本節は序章であり、今後も更新を続けます〜
関連トピック¶
- C++20 channel 実践 | C++那些事
- C++20 Concepts コンセプト | C++那些事
- C++20 constexpr とカンマ式 | C++那些事
- C++20 バイトオーダー endian | C++那些事
- C++20 Lambda 式(前編) | C++那些事
- C++20 モジュール Modules | C++那些事
- C++20 Ranges ライブラリ | C++那些事
- C++20 std::span の設計と落とし穴 | C++那些事
- C++20 新機能ガイド | C++那些事
- C++20 [[likely]] と [[unlikely]] | C++那些事