コルーチンを使用した単純な Future の実装

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_suspendfinal_suspend プログラム eagerFuture.cpp で 定義済みの awaitables std::suspend_never を返します .その名前が示すように、この awaitable は決して中断しないため、コルーチン オブジェクトは決して中断せず、通常の関数のように動作します。 awaitable は、あなたが待つことができるものです。オペレーター co_await には awaitable が必要です。 awaitable と 2 番目の awaiter ワークフローについては、今後の投稿で書きます。

この簡略化されたプロミス ワークフローから、どのメンバーがプロミス (promise_type) を機能するかを推測できます。 ) 少なくとも以下が必要です:

  • デフォルトのコンストラクタ
  • initial_suspend
  • final_suspend
  • unhandled_exception

確かに、これは完全な説明ではありませんでしたが、少なくともコルーチンのワークフローについて最初の直感を得るには十分でした.

次は?

あなたはすでにそれを推測しているかもしれません。次回の投稿では、この単純なコルーチンをさらなる実験の出発点として使用します。まず、プログラムにコメントを追加してワークフローを明示的にします。次に、コルーチンを遅延させ、別のスレッドで再開します。