はじめに
C++の通常の関数は、一度呼び出されたら、returnするまで処理を中断できません。しかし、非同期処理や、データを少しずつ生成するジェネレータのような機能では、「処理の途中で一旦中断し、呼び出し元に制御を戻し、後でまた続きから再開する」という仕組みが必要になります。
これを実現するのが、C++20で導入されたコルーチンです。コルーチンは、co_await, co_yield, co_return という3つの新しいキーワードを使って、関数の実行を自在に中断・再開できます。
この記事では、コルーチンの基本的な仕組みと、簡単なジェネレータ(数値を順番に生成する関数)を例にした実装方法を解説します。
【前提】C++20とは?
C++20は、2020年に正式化されたC++言語のメジャーアップデート版です。コルーチンはこのC++20で導入された言語機能のため、利用するにはC++20に対応した最新のコンパイラと、適切なコンパイラ設定が必要になります。
コルーチンの仕組みの概要
コルーチンを実装するには、コンパイラとの間で「お約束」を定義する必要があります。その中心となるのがプロミスオブジェクト (promise_type) です。 コルーチンが返す型(戻り値型)は、自身の内部に promise_type という名前のクラス(または構造体)を定義しなければなりません。この promise_type が、コルーチンの状態管理や、値の受け渡し方法などを定義します。
コルーチンを使ったサンプルコード
このコードは、1から3までの数値を一つずつ生成する、ジェネレータとして機能するコルーチンcounterを実装します。
完成コード
#include <iostream>
#include <coroutine> // C++20 コルーチンライブラリ
using namespace std;
// 1. コルーチンの戻り値となる型を定義
struct Generator {
// 内部クラスとして promise_type を定義
struct promise_type {
int current_value; // 現在の値を保持
Generator get_return_object() { return Generator{coroutine_handle<promise_type>::from_promise(*this)}; }
suspend_always initial_suspend() noexcept { return {}; }
suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
// co_yieldで値を受け取る
suspend_always yield_value(int value) {
current_value = value;
return {};
}
// co_return; (値なし) のための関数
void return_void() {}
};
coroutine_handle<promise_type> coro_handle;
// 外部からコルーチンを操作するためのメンバ関数
bool resume() {
if (!coro_handle || coro_handle.done()) {
return false;
}
coro_handle.resume();
return !coro_handle.done();
}
int getValue() {
return coro_handle.promise().current_value;
}
~Generator() {
if (coro_handle) {
coro_handle.destroy();
}
}
};
// 2. ジェネレータとして機能するコルーチン
Generator counter() {
cout << "コルーチン: 1を生成します。" << endl;
co_yield 1;
cout << "コルーチン: 2を生成します。" << endl;
co_yield 2;
cout << "コルーチン: 3を生成します。" << endl;
co_yield 3;
cout << "コルーチン: 終了します。" << endl;
}
int main() {
// 3. コルーチンを呼び出し、オブジェクトを取得
auto gen = counter();
// 4. ループで値を一つずつ取り出す
while (gen.resume()) {
cout << "main: 値 " << gen.getValue() << " を取得しました。" << endl;
}
return 0;
}
コードの解説
1. 戻り値型 (Generator) と promise_type
Generatorは、コルーチンの状態を管理するためのハンドルcoroutine_handleを保持します。その内部クラスpromise_typeが、コルーチンの実際の動作を定義します。
initial_suspend: コルーチンが開始した直後に、まず中断するかどうかを決めます。suspend_alwaysは「常に中断する」という意味です。final_suspend: コルーチンが終了した後に、中断するかどうかを決めます。yield_value(int value):co_yieldが使われたときに呼び出され、生成された値 (value) を受け取ります。
2. コルーチン本体 (counter) と co_yield
counter関数は、戻り値の型がpromise_typeを持つGeneratorであるため、コルーチンとして扱われます。
co_yield 1;: このキーワードがコルーチンの核心です。- 値
1を、promise_typeのyield_valueメソッドに渡します。 - 関数の実行をその場で中断し、呼び出し元(
main関数)に制御を返します。
- 値
3. コルーチンの呼び出し
auto gen = counter(); main関数でコルーチンcounterを呼び出すと、コルーチンの本体はすぐには実行されません。initial_suspendの定義により、即座に中断され、コルーチンを管理するためのGeneratorオブジェクトが返されます。
4. 処理の再開と値の取得
while (gen.resume()) { ... }
gen.resume(): 中断されているコルーチンの実行を、中断された次の行から再開させます。- コルーチンは次の
co_yieldに到達すると、再び中断します。 gen.getValue():promise_typeに保持されている、co_yieldで生成された現在の値を取得します。gen.resume()は、コルーチンが完全に終了(final_suspendに到達)するとfalseを返すため、whileループが終了します。
まとめ
今回は、C++20のコルーチンの非常に基本的な仕組みを解説しました。
- コルーチンは、
co_await,co_yield,co_returnを使って、処理を中断・再開できる特殊な関数。 - コルーチンの動作は、戻り値の型が持つ
promise_typeによって定義される。 co_yieldは、値を生成(yield)して、処理を中断する。- 呼び出し元は、
coroutine_handleを通じて、コルーチンの処理を再開させることができる。
コルーチンは非同期I/Oやジェネレータなど、複雑な制御フローを、同期処理のようなフラットで読みやすいコードで記述するための、強力な現代C++の機能です。
