return
の代わりに 、コルーチンは co_return
を使用します その結果を返します。この投稿では、co_return を使用して単純なコルーチンを実装したいと考えています。
コルーチンの背後にある理論を紹介しましたが、コルーチンについてもう一度書きたいと思います。私の答えは簡単で、私の経験に基づいています。 C++20 は具体的なコルーチンを提供しません。代わりに、C++20 はコルーチンを実装するためのフレームワークを提供します。このフレームワークは 20 を超える関数で構成されており、その一部は実装する必要があり、一部はオーバーライドできます。これらの関数に基づいて、コンパイラはコルーチンの動作を定義する 2 つのワークフローを生成します。短くするために。 C++20 のコルーチンは諸刃の剣です。一方で、彼らはあなたに巨大な力を与えますが、他方では、理解するのが非常に困難です.私は著書「C++20:Get the Details」で 80 ページ以上をコルーチンに捧げましたが、まだすべてを説明していません。
私の経験からすると、単純なコルーチンを使用して変更することが、それらを理解するための最も簡単な (おそらく唯一の) 方法です。そして、これはまさに私が次の投稿で追求しているアプローチです。単純なコルーチンを提示し、それらを変更します。ワークフローを明確にするために、多くのコメントを内部に入れ、コルーチンの内部を理解するために必要な理論のみを追加します。私の説明は決して完全ではなく、コルーチンに関する知識を深めるための出発点としてのみ役立つはずです.
短いリマインダー
関数しか呼び出せませんが、 そこから戻ると、コルーチンを呼び出すことができます 、中断して再開し、中断されたコルーチンを破棄します。
新しいキーワード co_await
で と co_yield
、C++20 は 2 つの新しい概念で C++ 関数の実行を拡張します。
co_await expression
に感謝 式の実行を一時停止および再開することができます。 co_await expression
を使用する場合 関数内 func
、呼び出し auto getResult = func()
関数呼び出しの結果 func()
の場合はブロックしません 利用できません。リソースを消費するブロッキングの代わりに、リソースにやさしい待機を行います。
co_yield
式はジェネレータ関数をサポートしています。ジェネレーター関数は、呼び出すたびに新しい値を返します。ジェネレーター関数は、値を選択できる一種のデータ ストリームです。データ ストリームは無限にすることができます。したがって、私たちは C++ による遅延評価の中心にいます。
さらに、コルーチンは return
しません。 その結果、コルーチンは co_return
を行います その結果。
// ...
MyFuture<int> createFuture() { co_return 2021; } int main() { auto fut = createFuture(); std::cout << "fut.get(): " << fut.get() << '\n'; }
この簡単な例では createFuture
3 つの新しいキーワード co_return, co_yield,
のいずれかを使用するため、コルーチンです。 または co_await
コルーチン MyFuture<int>
を返します .何?これは私をしばしば困惑させました。コルーチンという名前は、2 つのエンティティに使用されます。新しい用語を 2 つ紹介します。 createFuture
コルーチン ファクトリです コルーチン オブジェクトを返す fut, which
結果を尋ねるために使用できます:fut.get()
.
この理論で十分なはずです。 co_return
について話しましょう .
co_return
確かに、次のプログラムのコルーチン eagerFuture.cpp
は最も単純なコルーチンですが、それでも意味のあることを行うと想像できます。呼び出しの結果を自動的に保存します。
// eagerFuture.cpp #include <coroutine> #include <iostream> #include <memory> template<typename T> struct MyFuture { std::shared_ptr<T> value; // (3) MyFuture(std::shared_ptr<T> p): value(p) {} ~MyFuture() { } T get() { // (10) return *value; } struct promise_type { std::shared_ptr<T> ptr = std::make_shared<T>(); // (4) ~promise_type() { } MyFuture<T> get_return_object() { // (7) return ptr; } void return_value(T v) { *ptr = v; } std::suspend_never initial_suspend() { // (5) return {}; } std::suspend_never final_suspend() noexcept { // (6) return {}; } void unhandled_exception() { std::exit(1); } }; }; MyFuture<int> createFuture() { // (1) co_return 2021; // (9) } int main() { std::cout << '\n'; auto fut = createFuture(); std::cout << "fut.get(): " << fut.get() << '\n'; // (2) std::cout << '\n'; }
MyFuture
すぐに実行される Future として動作します (「非同期関数呼び出し」を参照してください)。コルーチン createFuture
の呼び出し (1 行目) 未来を返し、 fut.get
を呼び出す (2 行目) 関連する promise の結果を取得します。
future との微妙な違いが 1 つあります。コルーチン createFuture
の戻り値です。 呼び出し後に使用できます。コルーチンの寿命の問題により、コルーチンは std::shared_ptr
によって管理されます (3 行目と 4 行目)。コルーチンは常に std::suspend_never
を使用します (5 行目と 6 行目) したがって、実行前も実行後もサスペンドしません。これは、関数 createFuture
が呼び出されたときにコルーチンがすぐに実行されることを意味します。 が呼び出されます。メンバー関数 get_return_object
(7 行目) ハンドルをコルーチンに返し、それをローカル変数に格納します。 return_value
(8 行目) co_return 2021
によって提供されたコルーチンの結果を格納します。 (9 行目)。クライアントは fut.get
を呼び出します (2 行目) で、promise のハンドルとして future を使用します。メンバー関数 get
最後に結果をクライアントに返します (10 行目)。
関数のように振る舞うコルーチンを実装するのは無駄だと思うかもしれません。あなたが正しいです!ただし、この単純なコルーチンは、さまざまな Future の実装を作成するための理想的な出発点です。
この時点で、少し理論を追加する必要があります。
約束のワークフロー
co_yield
を使用する場合 、 co_await
、または co_return
関数では、関数はコルーチンになり、コンパイラはその関数本体を次の行と同等のものに変換します。
{ Promise prom; // (1) co_await prom.initial_suspend(); // (2) try { <function body> // (3) } catch (...) { prom.unhandled_exception(); } FinalSuspend: co_await prom.final_suspend(); // (4) }
これらの関数名に聞き覚えはありますか?右!これらは内部クラス promise_type
のメンバー関数です .コルーチン ファクトリ createFuture
の戻り値としてコルーチン オブジェクトを作成するときに、コンパイラが実行する手順は次のとおりです。 .最初に promise オブジェクトを作成し (1 行目)、その initial_suspend
を呼び出します。 メンバー関数を呼び出し (2 行目)、コルーチン ファクトリの本体を実行し (3 行目)、最後にメンバー関数を呼び出します final_suspend
(4 行目)。両方のメンバー関数 initial_suspend
と final_suspend
プログラム eagerFuture.cpp
で 定義済みの awaitables std::suspend_never
を返します .その名前が示すように、この awaitable は決して中断しないため、コルーチン オブジェクトは決して中断せず、通常の関数のように動作します。 awaitable は、あなたが待つことができるものです。オペレーター co_await には awaitable が必要です。 awaitable と 2 番目の awaiter ワークフローについては、今後の投稿で書きます。
この簡略化されたプロミス ワークフローから、どのメンバーがプロミス (promise_type
) を機能するかを推測できます。 ) 少なくとも以下が必要です:
- デフォルトのコンストラクタ
initial_suspend
final_suspend
unhandled_exception
確かに、これは完全な説明ではありませんでしたが、少なくともコルーチンのワークフローについて最初の直感を得るには十分でした.
次は?
あなたはすでにそれを推測しているかもしれません。次回の投稿では、この単純なコルーチンをさらなる実験の出発点として使用します。まず、プログラムにコメントを追加してワークフローを明示的にします。次に、コルーチンを遅延させ、別のスレッドで再開します。