C++20 のコルーチンに関する私の話は続きます。今日は、コルーチン フレームワークを深く掘り下げて、無限のデータ ストリームを作成します。そのため、準備のために、以前の 2 つの投稿「C++20:コルーチン - 最初の概要」と「C++20:コルーチンの詳細」を読む必要があります。
コルーチンを記述するためのフレームワークは、部分的に実装する必要があり、部分的に上書きできる 20 を超える関数で構成されています。したがって、必要に応じてコルーチンを調整できます。最後に、たとえば、次のような無限データ ストリーム用のジェネレータ Generator
Generator<int> getNext(int start = 0, int step = 1) { auto value = start; for (int i = 0;; ++i) { co_yield value; value += step; } }
今、私たちは自分の仕事の運命を知っています。始めましょう。
フレームワーク
コルーチンは、promise オブジェクト、コルーチン ハンドル、コルーチン フレームの 3 つの部分で構成されます。
- Promise オブジェクト :promise オブジェクトはコルーチン内から操作され、promise オブジェクトを介して結果を配信します。
- コルーチン ハンドル :コルーチン ハンドルは、外部からコルーチン フレームを再開または破棄する非所有ハンドルです。
- コルーチン フレーム :コルーチン フレームは内部の、通常はヒープ割り当て状態です。これは、前述の promise オブジェクト、コピーされたコルーチンのパラメーター、一時停止ポイントの表現、現在の一時停止ポイントの前に有効期間が終了するローカル変数、および現在の一時停止ポイントを超える有効期間を持つローカル変数で構成されます。
簡素化されたワークフロー
関数で co_yield、co_await、または co_return を使用すると、関数はコルーチンになり、コンパイラはその本体を次の行と同等のものに変換します。
{ Promise promise; co_await promise.initial_suspend(); try { <function body> } catch (...) { promise.unhandled_exception(); } FinalSuspend: co_await promise.final_suspend(); }
コルーチンが実行を開始します
- コルーチン フレームを割り当てます
- すべての関数パラメータをコルーチン フレームにコピーします
- promise オブジェクトの promise を作成します
- promise.get_return_object() を呼び出してコルーチン ハンドルを作成し、それをローカル変数に保持します。呼び出しの結果は、コルーチンが最初に中断されたときに呼び出し元に返されます。
- promise.initial_suspend() と co_await の結果を呼び出します。 promise タイプは通常、積極的に開始されたコルーチンに対して std::suspend_never を返し、遅延的に開始されたコルーチンに対して std::suspend_always を返します。
- co_await promise.initial_suspend() が再開すると、コルーチンの本体が実行されます
コルーチンが一時停止ポイントに到達
- コルーチン ハンドル (promise.get_return_object()) が呼び出し元に返され、コルーチンが再開されます
コルーチンが co_return に到達
- co_return または co_return 式の promise.return_void() を呼び出します。式の型は void です
- co_return 式の promise.return_value(expression) を呼び出します。ここで、expression は非型 void です。
- スタックで作成されたすべての変数を破棄します
- promise.final_suspend() を呼び出し、co_await の結果を呼び出します
コルーチンが破棄されます (co_return、キャッチされない例外、またはコルーチン ハンドルを介して終了することにより)
- promise オブジェクトの破棄を呼び出します
- 関数パラメータのデストラクタを呼び出します
- コルーチン フレームで使用されているメモリを解放します
- 制御を呼び出し元に戻します
理論を実践してみましょう。
co_yield を使用した無限データ ストリーム
次のプログラムは、無限のデータ ストリームを生成します。コルーチン getNext は、co_yield を使用して、start で開始し、要求に応じて次の値を段階的にインクリメントするデータ ストリームを作成します。
// infiniteDataStream.cpp #include <coroutine> #include <memory> #include <iostream> template<typename T> struct Generator { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; Generator(handle_type h): coro(h) {} // (3) handle_type coro; ~Generator() { if ( coro ) coro.destroy(); } Generator(const Generator&) = delete; Generator& operator = (const Generator&) = delete; Generator(Generator&& oth) noexcept : coro(oth.coro) { oth.coro = nullptr; } Generator& operator = (Generator&& oth) noexcept { coro = oth.coro; oth.coro = nullptr; return *this; } T getValue() { return coro.promise().current_value; } bool next() { // (5) coro.resume(); return not coro.done(); } struct promise_type { promise_type() = default; // (1) ~promise_type() = default; auto initial_suspend() { // (4) return std::suspend_always{}; } auto final_suspend() { return std::suspend_always{}; } auto get_return_object() { // (2) return Generator{handle_type::from_promise(*this)}; } auto return_void() { return std::suspend_never{}; } auto yield_value(const T value) { // (6) current_value = value; return std::suspend_always{}; } void unhandled_exception() { std::exit(1); } T current_value; }; }; Generator<int> getNext(int start = 0, int step = 1) noexcept { auto value = start; for (int i = 0;; ++i){ co_yield value; value += step; } } int main() { std::cout << std::endl; std::cout << "getNext():"; auto gen = getNext(); for (int i = 0; i <= 10; ++i) { gen.next(); std::cout << " " << gen.getValue(); // (7) } std::cout << "\n\n"; std::cout << "getNext(100, -10):"; auto gen2 = getNext(100, -10); for (int i = 0; i <= 20; ++i) { gen2.next(); std::cout << " " << gen2.getValue(); } std::cout << std::endl; }
メイン関数は 2 つのコルーチンを作成します。最初の gen は 0 から 10 までの値を返し、2 番目の gen2 は 100 から -100 までの値を返します。ワークフローに入る前に、Compiler Explorer と GCC 10 のおかげで、ここにプログラムの出力があります。
プログラム∞DataStream.cpp の数字は、ワークフローの最初の繰り返しのステップを表します。
<オール>追加の反復では、ステップ 5 から 7 のみが実行されます。
コルーチンの基礎となるフレームワークを理解することは非常に困難です。既存のコルーチンをいじって、変更された動作を観察することが、それらを把握する最も簡単な方法かもしれません。無限のデータ ストリームを作成する提示されたコルーチンは、最初の実験の出発点として適しています。コンパイラ エクスプローラで実行可能プログラムへのリンクを使用するだけです。
次は?
今日の投稿では、co_yield を使用して無限のデータ ストリームを作成しました。次回は、co_await によるスレッド同期についてです。