大量のデータを順番に処理したいとき、皆さんはどう実装しますか?std::vector
に全てのデータを格納してからループ処理する方法が一般的ですが、「もしデータが無限に続くとしたら?」「全てのデータをメモリに保持したくない」といったケースでは、この方法は使えません。
C++23では、このような課題をエレガントに解決する**std::generator
が導入されました。これはC++20で導入されたコルーチン**のパワーを、誰でも簡単に利用できるようにした画期的な機能です。この記事では、std::generator
の使い方とその魅力を解説します。
これまでの課題:複雑なコルーチン実装
std::generator
を理解するために、まずはその背景にあるコルーチンについて触れておきましょう。
コルーチンは、処理を途中で一時停止したり、再開したりできる特殊な関数です。これにより、値を一つ生成しては呼び出し元に返し、次の値が必要になったら処理を再開する…といった「遅延評価」が可能になります。
しかし、C++20の標準機能だけでコルーチンを実装するには、promise_type
といったお決まりの作法を多数記述する必要があり、その複雑さから利用のハードルが高いという課題がありました。
救世主std::generator
の登場
その課題を解決するのが、C++23で導入されたstd::generator
です。
std::generator
は、値を一つずつ生成(yield)するという目的に特化した、コルーチンのラッパーです。プログラマは面倒な内部実装を一切気にすることなく、ただ値を生成するロジックを記述するだけで、コルーチンベースのジェネレータ(生成器)を簡単に作ることができます。
実践!std::generator
で無限シーケンスを作る
言葉で説明するよりも、実際のコードを見るのが一番です。ここでは、指定した数値から始まる偶数の列を(理論上)無限に生成するジェネレータを作成してみましょう。
#include <iostream>
#include <generator> // C++23で追加されたヘッダ
#include <ranges> // views::take を使うために必要
// 指定した開始番号以降の偶数を生成し続けるジェネレータ
std::generator<int> even_numbers_from(int start) {
// 開始番号が奇数なら、次の偶数に調整
int current = (start % 2 == 0) ? start : start + 1;
while (true) {
// co_yieldで値を呼び出し元に返し、この場所で処理を一時停止する
co_yield current;
// 次の値が必要になったら、ここから処理が再開される
current += 2;
}
}
int main() {
std::cout << "50から始まる最初の5つの偶数:" << std::endl;
// even_numbers_from(50) でジェネレータを作成し、
// rangesのviews::take(5)で最初の5要素だけを取り出す
for (int value : even_numbers_from(50) | std::views::take(5)) {
std::cout << value << " ";
}
// 出力: 50 52 54 56 58
std::cout << std::endl;
return 0;
}
コードのポイント解説
#include <generator>
C++23で新しく追加されたヘッダです。これを利用するには、対応した新しいコンパイラとコンパイルオプション(例:-std=c++23
)が必要です。std::generator<int>
関数の戻り値の型としてstd::generator<生成したい値の型>
を指定します。これにより、この関数がジェネレータとして振る舞うことを示します。co_yield current;
std::generator
の心臓部です。return
の代わりにco_yield
を使うことで、current
の値を関数の呼び出し元(この例ではfor
ループ)に返します。重要なのは、値を返した後、関数は終了せずにこの場所で実行を一時停止することです。| std::views::take(5)
even_numbers_from
は無限に偶数を生成し続けますが、C++20 Rangesのtake
アダプタを組み合わせることで、「そのうち最初の5つだけが欲しい」という要求を簡潔に表現できます。for
ループが5回まわると、ジェネレータはそれ以上呼び出されなくなり、処理は終了します。
std::generator
のメリット
- 遅延評価(Lazy Evaluation) 値は実際に必要になるまで生成されません。これにより、メモリ消費量を大幅に節約でき、無限シーケンスのような概念も扱えるようになります。
- シンプルなコード ループの状態を管理するための複雑なクラスを自前で実装する必要がありません。関数のローカル変数がそのまま状態として保持されるため、ロジックが非常に直感的になります。
- Rangesライブラリとの高い親和性
std::generator
はRangesのview
のように振る舞うため、take
やfilter
といったアダプタとシームレスに連携し、柔軟なデータ処理パイプラインを構築できます。
まとめ
std::generator
は、C++におけるコルーチンの利用を劇的に簡単にする、非常に強力な新機能です。これまで複雑なイテレータクラスを書いていたような処理を、シンプルで可読性の高い「生成関数」として記述できるようになります。
メモリ効率が良く、表現力豊かなstd::generator
を使いこなして、よりモダンで効率的なC++プログラミングを実践してみてはいかがでしょうか。