C++20 のコルーチンを含む汎用データ ストリーム

実用的な観点からのコルーチンに関するこのミニ シリーズの最後の投稿で、「C++20 のコルーチンを使用した無限データ ストリーム」のワークフローを紹介しました。この投稿では、データ ストリームの一般的な可能性を使用します。

この投稿は、以前の投稿「C++20 のコルーチンを使用した無限データ ストリーム」を知っていることを前提としています。この記事では、新しいキーワード co_yield に基づいて、無限ジェネレーターのワークフローを非常に詳細に説明しています。 ここまで、新しいキーワード co_returnについて書いてきました。 、および co_yield, 関数からコルーチンを作成します。次の投稿では、最も挑戦的な新しいキーワード co_await を詳しく見ていきます。 .

co_return :

  • コルーチンを使用したシンプルな Future の実装
  • C++20 のコルーチンを使用した Lazy Future
  • コルーチンを使用して別のスレッドで Future を実行する

co_yield:

  • C++20 のコルーチンによる無限データ ストリーム

最後に、何か新しいものに。

ジェネレーターの一般化

前回の投稿で、Generator の一般的な可能性をすべて使用しなかったのはなぜかと思うかもしれません。その実装を調整して、標準テンプレート ライブラリの任意のコンテナーの連続する要素を生成させてください。

// coroutineGetElements.cpp

#include <coroutine>
#include <memory>
#include <iostream>
#include <string>
#include <vector>

template<typename T>
struct Generator {
 
 struct promise_type;
 using handle_type = std::coroutine_handle<promise_type>;
 
 Generator(handle_type h): coro(h) {} 

 handle_type coro;
 
 ~Generator() { 
 if ( coro ) coro.destroy();
 }
 Generator(const Generator&) = delete;
 Generator& operator = (const Generator&) = delete;
 Generator(Generator&& oth): coro(oth.coro) {
 oth.coro = nullptr;
 }
 Generator& operator = (Generator&& oth) {
 coro = oth.coro;
 oth.coro = nullptr;
 return *this;
 }
 T getNextValue() {
 coro.resume();
 return coro.promise().current_value;
 }
 struct promise_type {
 promise_type() {} 
 
 ~promise_type() {}
 
 std::suspend_always initial_suspend() { 
 return {};
 }
 std::suspend_always final_suspend() noexcept {
 return {};
 }
 auto get_return_object() { 
 return Generator{handle_type::from_promise(*this)};
 }
 
 std::suspend_always yield_value(const T value) { 
 current_value = value;
 return {};
 }
 void return_void() {}
 void unhandled_exception() {
 std::exit(1);
 }

 T current_value;
 };

};

template <typename Cont>
Generator<typename Cont::value_type> getNext(Cont cont) {
 for (auto c: cont) co_yield c;
}

int main() {

 std::cout << '\n';
 
 std::string helloWorld = "Hello world";
 auto gen = getNext(helloWorld); // (1)
 for (int i = 0; i < helloWorld.size(); ++i) {
 std::cout << gen.getNextValue() << " "; // (4)
 }

 std::cout << "\n\n";

 auto gen2 = getNext(helloWorld); // (2)
 for (int i = 0; i < 5 ; ++i) { // (5)
 std::cout << gen2.getNextValue() << " ";
 }

 std::cout << "\n\n";

 std::vector myVec{1, 2, 3, 4 ,5};
 auto gen3 = getNext(myVec); // (3)
 for (int i = 0; i < myVec.size() ; ++i) { // (6)
 std::cout << gen3.getNextValue() << " ";
 }
 
 std::cout << '\n';

}

この例では、ジェネレーターがインスタンス化され、3 回使用されます。最初の 2 つのケースでは、 gen (1 行目) と gen2 (2 行目) std::string helloWorld で初期化されます 、 gen3 の間 std::vector<int> を使用 (3 行目)。プログラムの出力は驚くべきものではありません。 4 行目は、文字列 helloWorld のすべての文字を返します。 続いて、5 行目は最初の 5 文字のみ、6 行目は std::vector<int> の要素 .

Compiler Explorer でプログラムを試すことができます。 短くします。 Generator<T> の実装 C++20 のコルーチンを使用した無限データ ストリームの前の記事とほとんど同じです。前のプログラムとの決定的な違いはコルーチン getNext です .
template <typename Cont>
Generator<typename Cont::value_type> getNext(Cont cont) {
 for (auto c: cont) co_yield c;
}

getNext コンテナーを引数として取り、コンテナーのすべての要素を範囲ベースの for ループで反復処理する関数テンプレートです。各反復の後、関数テンプレートは一時停止します。戻り型 Generator<typename Cont::value_type> あなたには驚くかもしれません。 Cont::value_type は、パーサーがヒントを必要とする依存テンプレート パラメーターです。デフォルトでは、型または非型として解釈できる場合、コンパイラは非型を想定します。このため、 typename を入れる必要があります Cont::value_type. の前

ワークフロー

コンパイラはコルーチンを変換し、2 つのワークフローを実行します:外側の promise ワークフロー 内部の awaiter ワークフロー .

約束のワークフロー

ここまでは、 promise_type のメンバー関数に基づく外側のワークフローについてのみ説明してきました。 .

{
 Promise prom;
 co_await prom.initial_suspend();
 try {
 <function body having co_return, co_yield, or co_wait>
 }
 catch (...) {
 prom.unhandled_exception();
 }
FinalSuspend:
 co_await prom.final_suspend();
}

前回の投稿に従えば、このワークフローは見覚えがあるはずです。 prom.initial_suspend() など、このワークフローのコンポーネントはすでに知っています。 、関数本体、および prom.final_suspend().

Awaiter ワークフロー

外側のワークフローは、Awaiters を返す Awaitables に基づいています。意図的にこの説明を単純化しました。すでに 2 つの定義済みの Awaitable を知っています:

  • std::suspend_always
struct suspend_always {
 constexpr bool await_ready() const noexcept { return false; }
 constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
 constexpr void await_resume() const noexcept {}
};

  • std::suspend_never
struct suspend_never {
 constexpr bool await_ready() const noexcept { return true; }
 constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
 constexpr void await_resume() const noexcept {}
};

いいえ、待機ワークフローがどの部分に基づいているかは既に推測できますか?右!メンバ関数 await_ready() について 、 await_suspend() 、および await_resume()

awaitable.await_ready() returns false:
 
 suspend coroutine
 
 awaitable.await_suspend(coroutineHandle) returns: 
 
 void:
 awaitable.await_suspend(coroutineHandle);
 coroutine keeps suspended
 return to caller

 bool:
 bool result = awaitable.await_suspend(coroutineHandle);
 if result: 
 coroutine keep suspended
 return to caller
 else: 
 go to resumptionPoint

 another coroutine handle: 
 auto anotherCoroutineHandle = awaitable.await_suspend(coroutineHandle);
 anotherCoroutineHandle.resume();
 return to caller
 
resumptionPoint:

return awaitable.await_resume();

awaiter のワークフローを疑似言語で示しました。 awaiter ワークフローを理解することは、コルーチンの動作とそれらをどのように適応させるかについて直感を持つための最後のパズルのピースです。

次は?

次回の投稿では、Awaitable に基づく awaiter ワークフローをさらに掘り下げます。両刃の剣に備えてください。ユーザー定義の Awaitable は大きな力を発揮しますが、理解するのは困難です。