技術(非IT系)

C++ コルーチンの概要は?C++20での実装と活用方法(co_await:co_yield:co_return:コンパイラサポート:非同期処理の実装など)

当サイトでは記事内に広告を含みます

C++20で正式に導入されたコルーチンは、C++における非同期処理・ジェネレータ・遅延評価の実装を大きく変える機能です。

従来のC++では非同期処理にスレッドやコールバックを使う必要がありましたが、コルーチンによってより直感的で読みやすいコードが書けるようになりました。

本記事では、C++20のコルーチンにおけるco_await・co_yield・co_returnの使い方と、コンパイラサポートの現状・非同期処理の実装方法について詳しく解説します。

C++の最新機能に興味がある方やモダンC++を学びたい方は、ぜひ参考にしてみてください。

C++20コルーチンの概要と3つのキーワード(結論)

それではまず、C++20コルーチンの全体像と重要な3つのキーワードについて解説していきます。

C++20のコルーチンは、関数の実行を途中で一時停止・再開できる仕組みで、co_await・co_yield・co_returnの3つのキーワードで制御します。

これらのキーワードのいずれか1つでも含む関数は、C++コンパイラによって自動的にコルーチンとして扱われます。

キーワード 意味 主な用途
co_await 非同期操作の完了を待機して一時停止 非同期処理・I/O待機
co_yield 値を返しながら一時停止 ジェネレータ・遅延評価
co_return コルーチンを終了して値を返す コルーチンの最終結果返却

C++20のコルーチンは「フレームワーク」であり、コルーチンを動かすための基盤(promise_type・Awaitable等)はユーザーまたはライブラリが実装する必要があります。標準ライブラリのコルーチンサポートはC++23以降で整備が進んでおり、C++20単体では比較的低レベルな実装が必要です。

co_awaitとは何か

co_awaitは非同期操作の完了を待機するキーワードです。

co_await式が実行されると、対象の非同期操作が完了するまでコルーチンが一時停止し、完了後に再開されます。

// co_awaitの基本イメージ

Task fetchData() {

  auto result = co_await asyncFetch(“https://example.com”);

  // asyncFetch完了後にここから再開

  std::cout << result << std::endl;

}

co_awaitの対象となるオブジェクトは「Awaitable」と呼ばれ、await_ready・await_suspend・await_resumeの3つのメンバ関数を持つ必要があります。

co_yieldとは何か

co_yieldは値を呼び出し元に返しながらコルーチンを一時停止するキーワードです。

Pythonのyieldに相当し、ジェネレータパターンの実装に使います。

// co_yieldを使ったジェネレータの例

Generator<int> range(int start, int end) {

  for (int i = start; i < end; ++i) {

    co_yield i; // iを返して一時停止

  }

}

// 使用例

for (int x : range(0, 5)) {

  std::cout << x << ” “; // 0 1 2 3 4

}

co_returnとは何か

co_returnはコルーチンを終了して値を返すキーワードです。

通常の関数のreturnと似ていますが、コルーチン内では必ずco_returnを使う必要があります。

値を返さない場合はco_return;(セミコロンのみ)を使います。

Task<int> compute() {

  int result = 42;

  co_return result; // コルーチンを終了して42を返す

}

C++20コルーチンの仕組みとpromise_type

続いては、C++20コルーチンの内部的な仕組みとpromise_typeについて確認していきます。

C++20のコルーチンを使いこなすには、promise_typeという概念の理解が欠かせません。

コルーチンフレームとは

コルーチンが呼び出されると、コンパイラはヒープ上に「コルーチンフレーム(coroutine frame)」を作成します。

コルーチンフレームには、ローカル変数・promise_typeオブジェクト・現在の実行位置などが保存されます。

これにより、コルーチンが一時停止してもすべての状態が保持され、再開時に正確にそこから続きを実行できます。

コルーチンフレームのヒープアロケーションはコンパイラ最適化(HALO)によって省略されることがありますが、一般には動的メモリ確保が発生することを意識しておく必要があります。

promise_typeの役割

promise_typeはコルーチンの「制御ポリシー」を定義する型です。

コルーチンの戻り値型(例:Task<T>やGenerator<T>)の内部にpromise_typeを定義することで、コルーチンの動作をカスタマイズできます。

promise_typeに必要な主なメンバ関数

・get_return_object():コルーチンの戻り値オブジェクトを生成

・initial_suspend():コルーチン開始時に即実行するか停止するかを指定

・final_suspend():コルーチン終了時の動作を指定

・return_value(v) または return_void():co_returnの値を処理

・yield_value(v):co_yieldの値を処理

・unhandled_exception():例外発生時の処理

このようにC++のコルーチンは非常に低レベルでカスタマイズ可能な仕組みになっており、柔軟性が高い反面、実装の複雑さもあります。

SuspendAlwaysとSuspendNever

C++20標準には2つのAwaitable型が用意されています。

std::suspend_alwaysはco_awaitした際に必ず一時停止するAwaitable、std::suspend_neverはco_awaitしても一時停止しないAwaitable(即実行)です。

promise_typeのinitial_suspend()やfinal_suspend()にこれらを返すことで、コルーチンの開始・終了時の動作を制御できます。

コンパイラサポートと主要ライブラリ

続いては、C++20コルーチンのコンパイラサポートの現状と、活用を助ける主要ライブラリについて確認していきます。

主要コンパイラのサポート状況

C++20コルーチンは主要コンパイラで広くサポートされています。

コンパイラ サポートバージョン 有効化オプション
GCC GCC 10以降(部分的)、GCC 11以降(本格的) -std=c++20
Clang Clang 12以降 -std=c++20
MSVC Visual Studio 2019 16.8以降 /std:c++20

実務では最新のコンパイラを使うことでC++20コルーチンの完全なサポートが得られます。

ただし、コンパイラによって実装の細部に差がある場合があるため、クロスプラットフォーム開発では注意が必要です。

cppcoro ライブラリ

cppcoroはC++20コルーチンを使いやすくするためのユーティリティライブラリです。

Task・Generator・AsyncGenerator・Schedular・SyncWaitなど、実用的なコルーチンプリミティブが提供されています。

C++20コルーチンの低レベルな実装の複雑さをcppcoroが吸収してくれるため、非同期処理やジェネレータの実装が格段に簡単になります。

C++23以降の標準化の動向

C++20ではコルーチンの基本的な言語機能のみが導入されましたが、C++23以降で標準ライブラリレベルのサポートが強化されています。

std::generatorはC++23で標準化されたジェネレータ型で、co_yieldを使ったシーケンス生成を簡単に実装できます。

将来のC++標準ではExecutor・Schedulerなどの非同期実行基盤の標準化も進んでおり、コルーチンはC++の非同期処理の中核として位置づけられています。

C++20コルーチンの実践的な活用方法

続いては、C++20コルーチンの実践的な活用方法と具体的なパターンについて確認していきます。

ジェネレータパターンの実装

co_yieldを使ったジェネレータはC++20コルーチンの最もシンプルな応用例です。

フィボナッチ数列を無限に生成するジェネレータを例に示します。

// 簡略化したGeneratorの使用イメージ(cppcoroを使用)

#include <cppcoro/generator.hpp>

cppcoro::generator<long long> fibonacci() {

  long long a = 0, b = 1;

  while (true) {

    co_yield a;

    auto tmp = a + b;

    a = b;

    b = tmp;

  }

}

// 先頭10件だけ取得

for (auto val : fibonacci() | std::views::take(10)) {

  std::cout << val << ” “;

}

無限シーケンスを遅延評価で生成できるため、全要素を事前にメモリに展開する必要がありません。

非同期処理の実装パターン

co_awaitを使った非同期処理の実装は、従来のコールバック・Futureベースの実装と比べてコードの可読性が大幅に向上します。

// 従来のコールバックスタイル(読みにくい)

readFile(“a.txt”, [](auto data1) {

  processData(data1, [](auto result1) {

    writeFile(“b.txt”, result1, [](auto ok) {

      // コールバック地獄…

    });

  });

});

// コルーチンスタイル(読みやすい)

Task processFiles() {

  auto data1 = co_await readFile(“a.txt”);

  auto result1 = co_await processData(data1);

  co_await writeFile(“b.txt”, result1);

}

コルーチンを使うことでコールバック地獄を解消し、同期的なコードと同じような直線的な書き方で非同期処理が記述できます。

コルーチンとスレッドプールの組み合わせ

C++20コルーチンはスレッドプールと組み合わせることで、効率的な並行処理が実現できます。

co_awaitでスレッドプール上のタスクを待機することで、CPUバウンドな処理を複数スレッドで並行実行しながら、コルーチンの読みやすいコードスタイルを維持できます。

Ioフレームワークやネットワークライブラリにコルーチンを組み込むことで、高性能なサーバーアプリケーションの実装が可能です。

まとめ

C++20のコルーチンはco_await・co_yield・co_returnの3つのキーワードを中心に構成されており、非同期処理・ジェネレータ・遅延評価の実装を大きく改善します。

promise_typeを通じて非常に柔軟にカスタマイズできる反面、低レベルな実装が必要なケースもあるため、cppcoroなどのライブラリ活用が実務では有効です。

GCC・Clang・MSVCの主要コンパイラで広くサポートされており、C++23以降でさらなる標準化が進んでいます。

コルーチンはモダンC++の非同期プログラミングの中核的な機能として今後ますます重要になるでしょう。