【C++20】コルーチン入門 | 処理を中断・再開できる関数の作り方

目次

はじめに

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. 1 を、promise_typeyield_valueメソッドに渡します。
    2. 関数の実行をその場で中断し、呼び出し元(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++の機能です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次