Coroutines (C++20)
Eine Coroutine ist eine Funktion, deren Ausführung unterbrochen und später fortgesetzt werden kann. Coroutines sind stacklos: Sie unterbrechen die Ausführung durch Rückkehr zum Aufrufer, und die Daten, die zur Wiederaufnahme der Ausführung benötigt werden, werden separat vom Stack gespeichert. Dies ermöglicht sequentiellen Code, der asynchron ausgeführt wird (z.B. zur Behandlung von nicht-blockierendem I/O ohne explizite Callbacks), und unterstützt auch Algorithmen für lazy-berechnete unendliche Sequenzen und andere Anwendungen.
Eine Funktion ist eine Coroutine, wenn ihre Definition eines der folgenden Elemente enthält:
- der co_await Ausdruck — um die Ausführung bis zur Wiederaufnahme anzuhalten
task<> tcp_echo_server() { char data[1024]; while (true) { std::size_t n = co_await socket.async_read_some(buffer(data)); co_await async_write(socket, buffer(data, n)); } }
- der co_yield Ausdruck — um die Ausführung zu unterbrechen und einen Wert zurückzugeben
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- die co_return -Anweisung — zur Beendigung der Ausführung mit Rückgabe eines Wertes
lazy<int> f() { co_return 7; }
Jede Coroutine muss einen Rückgabetyp haben, der eine Reihe von Anforderungen erfüllt, wie nachstehend aufgeführt.
Inhaltsverzeichnis |
Einschränkungen
Coroutinen können keine
variadischen Argumente
verwenden, keine einfachen
return
-Anweisungen oder
Platzhalter-Rückgabetypen
(
auto
oder
Concept
).
Consteval-Funktionen , constexpr-Funktionen , Konstruktoren , Destruktoren , und die main-Funktion können keine Coroutinen sein.
Ausführung
Jede Coroutine ist assoziiert mit
- das Promise-Objekt , das von innerhalb der Coroutine manipuliert wird. Die Coroutine übergibt ihr Ergebnis oder ihre Ausnahme durch dieses Objekt. Promise-Objekte stehen in keiner Weise in Beziehung zu std::promise .
- das Coroutine-Handle , das von außerhalb der Coroutine manipuliert wird. Dies ist ein nicht-besitzendes Handle, das verwendet wird, um die Ausführung der Coroutine fortzusetzen oder den Coroutine-Rahmen zu zerstören.
- der Coroutine-Zustand , welcher ein interner, dynamisch allokierter Speicher (sofern die Allokation nicht optimiert wurde) ist, ein Objekt, das enthält
-
- das Promise-Objekt
- die Parameter (alle als Kopie übergeben)
- eine Darstellung des aktuellen Unterbrechungspunkts, damit ein Resume weiß, wo fortgesetzt werden soll, und ein Destroy weiß, welche lokalen Variablen im Geltungsbereich waren
- lokale Variablen und temporäre Objekte, deren Lebensdauer über den aktuellen Unterbrechungspunkt hinausreicht.
Wenn eine Coroutine die Ausführung beginnt, führt sie Folgendes aus:
- reserviert das Coroutinen-Zustandsobjekt mittels operator new .
- kopiert alle Funktionsparameter in den Coroutinen-Zustand: By-Value-Parameter werden verschoben oder kopiert, By-Reference-Parameter bleiben Referenzen (können somit hängende Referenzen werden, wenn die Coroutine nach Ende der Lebensdauer des referenzierten Objekts fortgesetzt wird — siehe Beispiele unten).
- ruft den Konstruktor für das Promise-Objekt auf. Wenn der Promise-Typ einen Konstruktor besitzt, der alle Coroutinen-Parameter entgegennimmt, wird dieser Konstruktor mit den nach-Kopieren-Coroutinen-Argumenten aufgerufen. Andernfalls wird der Standardkonstruktor aufgerufen.
- ruft promise. get_return_object ( ) auf und bewahrt das Ergebnis in einer lokalen Variable. Das Ergebnis dieses Aufrufs wird an den Aufrufer zurückgegeben, wenn die Coroutine erstmals suspendiert. Bis zu diesem Schritt ausgelöste Ausnahmen werden an den Aufrufer weitergegeben, nicht im Promise-Objekt abgelegt.
-
ruft
promise.
initial_suspend
(
)
auf und
co_awaitet das Ergebnis. TypischePromise-Typen liefern entweder std::suspend_always für verzögert gestartete Coroutinen oder std::suspend_never für sofort gestartete Coroutinen zurück. - wenn co_await promise. initial_suspend ( ) fortgesetzt wird, beginnt die Ausführung des Coroutinen-Rumpfes.
Einige Beispiele für einen Parameter, der zu einem Dangling-Pointer wird:
#include <coroutine> #include <iostream> struct promise; struct coroutine : std::coroutine_handle<promise> { using promise_type = ::promise; }; struct promise { coroutine get_return_object() { return {coroutine::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; struct S { int i; coroutine f() { std::cout << i; co_return; } }; void bad1() { coroutine h = S{0}.f(); // S{0} zerstört h.resume(); // fortgesetzte Coroutine führt std::cout << i aus, verwendet S::i nach Freigabe h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // zurückgegebene Coroutine kann nicht ohne Use-after-free fortgesetzt werden } void bad3() { coroutine h = [i = 0]() -> coroutine // eine Lambda-Funktion, die auch eine Coroutine ist { std::cout << i; co_return; }(); // sofort aufgerufen // Lambda zerstört h.resume(); // verwendet (anonymen Lambda-Typ)::i nach Freigabe h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // mache i zu einem Coroutine-Parameter { std::cout << i; co_return; }(0); // Lambda zerstört h.resume(); // kein Problem, i wurde als Wertparameter in den // Coroutine-Frame kopiert h.destroy(); }
Wenn eine Coroutine einen Unterbrechungspunkt erreicht
- das zuvor erhaltene Rückgabeobjekt wird an den Aufrufer/Fortsetzer zurückgegeben, nach impliziter Konvertierung in den Rückgabetyp der Coroutine, falls erforderlich.
Wenn eine Coroutine die co_return -Anweisung erreicht, führt sie Folgendes aus:
- ruft promise. return_void ( ) auf
-
- co_return ;
- co_return expr ; wobei expr den Typ void hat
- oder ruft promise. return_value ( expr ) für co_return expr ; auf, wobei expr einen Nicht-void-Typ hat
- zerstört alle Variablen mit automatischer Speicherdauer in umgekehrter Reihenfolge ihrer Erstellung.
- ruft promise. final_suspend ( ) auf und co_await -t das Ergebnis.
Das Verlassen des Coroutine-Endes entspricht
co_return
;
, außer dass das Verhalten undefiniert ist, wenn keine Deklarationen von
return_void
im Gültigkeitsbereich von
Promise
gefunden werden können. Eine Funktion ohne eines der definierenden Schlüsselwörter in ihrem Funktionsrumpf ist keine Coroutine, unabhängig von ihrem Rückgabetyp, und das Verlassen des Endes führt zu undefiniertem Verhalten, wenn der Rückgabetyp nicht (möglicherweise cv-qualifiziert)
void
ist.
// Annahme: task ist ein Coroutine-Task-Typ task<void> f() { // keine Coroutine, undefiniertes Verhalten } task<void> g() { co_return; // OK } task<void> h() { co_await g(); // OK, implizites co_return; }
Wenn die Coroutine mit einer nicht abgefangenen Ausnahme endet, führt sie Folgendes aus:
- fängt die Ausnahme und ruft promise. unhandled_exception ( ) innerhalb des Catch-Blocks auf
- ruft promise. final_suspend ( ) auf und co_await et das Ergebnis (z.B. um eine Fortsetzung wiederaufzunehmen oder ein Ergebnis zu veröffentlichen). Es ist undefiniertes Verhalten, eine Coroutine von diesem Punkt aus wiederaufzunehmen.
Wenn der Coroutine-Zustand zerstört wird, entweder weil sie über co_return beendet wurde oder durch eine nicht abgefangene Ausnahme, oder weil sie über ihren Handle zerstört wurde, führt sie folgende Schritte aus:
- ruft den Destruktor des Promise-Objekts auf.
- ruft die Destruktoren der Funktionsparameterkopien auf.
- ruft operator delete auf, um den vom Coroutinen-Zustand belegten Speicher freizugeben.
- gibt die Ausführung an den Aufrufer/Weiterführenden zurück.
Dynamische Allokation
Der Coroutine-Zustand wird dynamisch über den nicht-Array- operator new alloziert.
Wenn der
Promise
-Typ einen klassenbasierten Ersatz definiert, wird dieser verwendet, andernfalls wird der globale
operator new
verwendet.
Falls der
Promise
-Typ eine Platzierungsform von
operator new
definiert, die zusätzliche Parameter akzeptiert, und diese mit einer Argumentliste übereinstimmen, bei der das erste Argument die angeforderte Größe (vom Typ
std::size_t
) ist und der Rest die Coroutinen-Funktionsargumente sind, werden diese Argumente an
operator new
übergeben (dies ermöglicht die Verwendung der
Leading-Allocator-Konvention
für Coroutinen).
Der Aufruf von operator new kann optimiert werden (selbst wenn ein benutzerdefinierter Allokator verwendet wird), wenn
- Die Lebensdauer des Coroutine-Zustands ist streng innerhalb der Lebensdauer des Aufrufers verschachtelt, und
- die Größe des Coroutine-Frames ist an der Aufrufstelle bekannt.
In diesem Fall ist der Coroutine-Zustand in den Stack-Frame des Aufrufers eingebettet (wenn der Aufrufer eine gewöhnliche Funktion ist) oder im Coroutine-Zustand (wenn der Aufrufer eine Coroutine ist).
Wenn die Allokation fehlschlägt, wirft die Coroutine
std::bad_alloc
, es sei denn, der
Promise
-Typ definiert die Memberfunktion
Promise
::
get_return_object_on_allocation_failure
(
)
. Wenn diese Memberfunktion definiert ist, verwendet die Allokation die nothrow-Form von
operator new
und gibt bei Allokationsfehler die Coroutine sofort das Objekt zurück, das von
Promise
::
get_return_object_on_allocation_failure
(
)
erhalten wurde, an den Aufrufer zurück, z.B.:
struct Coroutine::promise_type { /* ... */ // sicherstellen, dass der nicht-werfende operator-new verwendet wird static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // oder, return Coroutine(nullptr); } // benutzerdefinierte nicht-werfende Überladung von new void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // Zuweisungsfehler } };
Promise
Der
Promise
-Typ wird vom Compiler anhand des Rückgabetyps der Coroutine unter Verwendung von
std::coroutine_traits
bestimmt.
Formal sei
-
RundArgs...bezeichnen jeweils den Rückgabetyp und die Parametertypliste einer Coroutine, -
ClassTbezeichnet den Klassentyp, zu dem die Coroutine gehört, wenn sie als nicht-statische Memberfunktion definiert ist, - cv bezeichnet die in der Funktionsdeklaration angegebene CV-Qualifikation, wenn sie als nicht-statische Memberfunktion definiert ist,
sein
Promise
Typ wird bestimmt durch:
- std:: coroutine_traits < R, Args... > :: promise_type , falls die Coroutine nicht als implizite Objekt-Memberfunktion definiert ist,
-
std::
coroutine_traits
<
R,
cvClassT & , Args... > :: promise_type , falls die Coroutine als implizite Objekt-Memberfunktion definiert ist, die nicht als Rvalue-Referenz qualifiziert ist, -
std::
coroutine_traits
<
R,
cvClassT && , Args... > :: promise_type , falls die Coroutine als implizite Objekt-Memberfunktion definiert ist, die als Rvalue-Referenz qualifiziert ist.
Zum Beispiel:
| Wenn die Coroutine definiert ist als ... |
dann ist ihr
Promise
-Typ ...
|
|---|---|
| task < void > foo ( int x ) ; | std:: coroutine_traits < task < void > , int > :: promise_type |
| task < void > Bar :: foo ( int x ) const ; | std:: coroutine_traits < task < void > , const Bar & , int > :: promise_type |
| task < void > Bar :: foo ( int x ) && ; | std:: coroutine_traits < task < void > , Bar && , int > :: promise_type |
co_await
Der unäre Operator co_await unterbricht eine Coroutine und gibt die Kontrolle an den Aufrufer zurück.
co_await
expr
|
|||||||||
Ein co_await -Ausdruck kann nur in einem potenziell ausgewerteten Ausdruck innerhalb eines regulären Funktionsrumpfs (einschließlich des Funktionsrumpfs eines Lambda-Ausdrucks ) erscheinen und darf nicht vorkommen
- in einem Handler ,
- in einer Deklarationsanweisung , es sei denn, sie erscheint in einem Initialisierer dieser Deklarationsanweisung,
-
in der
einfachen Deklaration
eines
Init-Statements
(siehe
if,switch,forund [[../range- for |range- for ]]), es sei denn, sie erscheint in einem Initialisierer dieses Init-Statements , - in einem Default-Argument , oder
- im Initialisierer einer Blockbereich-Variablen mit statischer oder Thread- Speicherdauer .
|
Ein co_await -Ausdruck darf kein potenziell ausgewerteter Teilausdruck des Prädikats einer Vertragsassertion sein. |
(seit C++26) |
Zuerst wird expr wie folgt in ein Awaitable umgewandelt:
- falls expr durch einen initialen Suspend-Punkt, einen finalen Suspend-Punkt oder einen Yield-Ausdruck erzeugt wird, ist das Awaitable expr unverändert.
-
andernfalls, falls der
Promise-Typ der aktuellen Coroutine die Memberfunktionawait_transformbesitzt, dann ist das Awaitable promise. await_transform ( expr ) . - andernfalls ist das Awaitable expr unverändert.
Dann wird das Awaiter-Objekt wie folgt erhalten:
- Wenn die Überladungsauflösung für operator co_await eine einzige beste Überladung ergibt, ist der Awaiter das Ergebnis dieses Aufrufs:
-
- awaitable. operator co_await ( ) für die Member-Überladung,
- operator co_await ( static_cast < Awaitable && > ( awaitable ) ) für die Nicht-Member-Überladung.
- andernfalls, wenn die Überladungsauflösung keinen Operator co_await findet, ist der Awaiter unverändert awaitable.
- andernfalls, wenn die Überladungsauflösung mehrdeutig ist, ist das Programm fehlerhaft.
Wenn der obige Ausdruck ein prvalue ist, wird das Awaiter-Objekt temporär daraus materialisiert . Andernfalls, wenn der obige Ausdruck ein glvalue ist, ist das Awaiter-Objekt das Objekt, auf das es referenziert.
Dann wird awaiter. await_ready ( ) aufgerufen (dies ist eine Abkürzung, um die Kosten der Unterbrechung zu vermeiden, wenn bekannt ist, dass das Ergebnis bereit ist oder synchron abgeschlossen werden kann). Wenn das Ergebnis, kontextuell konvertiert zu bool , false ist, dann
- Die Coroutine wird angehalten (ihr Coroutine-Zustand wird mit lokalen Variablen und dem aktuellen Unterbrechungspunkt gefüllt).
-
awaiter.
await_suspend
(
handle
)
wird aufgerufen, wobei handle das Coroutine-Handle repräsentiert, das die aktuelle Coroutine darstellt. Innerhalb dieser Funktion ist der angehaltene Coroutine-Zustand über dieses Handle beobachtbar, und es ist die Verantwortung dieser Funktion, ihn zur Wiederaufnahme auf einem Executor zu planen oder zu zerstören (die Rückgabe von false zählt als Planung).
-
falls
await_suspendvoid zurückgibt, wird die Steuerung sofort an den Aufrufer/die wiederaufnehmende Instanz der aktuellen Coroutine zurückgegeben (diese Coroutine bleibt angehalten), andernfalls -
falls
await_suspendbool zurückgibt,
-
- gibt der Wert true die Steuerung an den Aufrufer/die wiederaufnehmende Instanz der aktuellen Coroutine zurück
- setzt der Wert false die aktuelle Coroutine fort.
-
falls
await_suspendein Coroutine-Handle für eine andere Coroutine zurückgibt, wird dieses Handle fortgesetzt (durch einen Aufruf von handle. resume ( ) ) (beachten Sie, dass dies letztendlich zur Fortsetzung der aktuellen Coroutine führen kann). -
falls
await_suspendeine Exception wirft, wird die Exception abgefangen, die Coroutine fortgesetzt und die Exception sofort erneut geworfen.
-
falls
Schließlich wird awaiter. await_resume ( ) aufgerufen (ob die Coroutine unterbrochen wurde oder nicht), und ihr Ergebnis ist das Ergebnis des gesamten co_await expr -Ausdrucks.
Wenn die Coroutine im co_await -Ausdruck angehalten wurde und später wieder aufgenommen wird, befindet sich der Wiederaufnahmepunkt unmittelbar vor dem Aufruf von awaiter. await_resume ( ) .
Beachten Sie, dass die Coroutine vollständig angehalten wird, bevor sie awaiter. await_suspend ( ) betritt. Ihr Handle kann mit einem anderen Thread geteilt und fortgesetzt werden, bevor die await_suspend ( ) -Funktion zurückkehrt. (Beachten Sie, dass die standardmäßigen Speichersicherheitsregeln weiterhin gelten. Wenn also ein Coroutine-Handle ohne Sperre über Threads hinweg geteilt wird, sollte der Awaiter mindestens Release-Semantik verwenden und der Fortsetzer mindestens Acquire-Semantik .) Beispielsweise kann das Coroutine-Handle in einen Callback eingefügt und zur Ausführung in einem Threadpool eingeplant werden, wenn ein asynchroner E/A-Vorgang abgeschlossen ist. In diesem Fall, da die aktuelle Coroutine möglicherweise bereits fortgesetzt wurde und somit den Destruktor des Awaiter-Objekts ausgeführt hat, alles gleichzeitig während await_suspend ( ) seine Ausführung im aktuellen Thread fortsetzt, sollte await_suspend ( ) * this als zerstört betrachten und nicht darauf zugreifen, nachdem das Handle für andere Threads freigegeben wurde.
Beispiel
#include <coroutine> #include <iostream> #include <stdexcept> #include <thread> auto switch_to_new_thread(std::jthread& out) { struct awaitable { std::jthread* p_out; bool await_ready() { return false; } void await_suspend(std::coroutine_handle<> h) { std::jthread& out = *p_out; if (out.joinable()) throw std::runtime_error("Output jthread parameter not empty"); out = std::jthread([h] { h.resume(); }); // Potentielles undefiniertes Verhalten: Zugriff auf möglicherweise zerstörtes *this // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // dies ist OK } void await_resume() {} }; return awaitable{&out}; } struct task { struct promise_type { task get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; task resuming_on_new_thread(std::jthread& out) { std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // awaiter wird hier zerstört std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; } int main() { std::jthread out; resuming_on_new_thread(out); }
Mögliche Ausgabe:
Coroutine started on thread: 139972277602112 New thread ID: 139972267284224 Coroutine resumed on thread: 139972267284224
Hinweis: Das Awaiter-Objekt ist Teil des Coroutine-Zustands (als temporäres Objekt, dessen Lebensdauer einen Unterbrechungspunkt überspannt) und wird zerstört, bevor der co_await -Ausdruck beendet wird. Es kann verwendet werden, um zustandsbezogene Informationen pro Operation gemäß den Anforderungen einiger asynchroner I/O-APIs zu verwalten, ohne auf zusätzliche dynamische Allokationen zurückgreifen zu müssen.
Die Standardbibliothek definiert zwei triviale Awaitables: std::suspend_always und std::suspend_never .
|
Dieser Abschnitt ist unvollständig
Grund: Beispiele |
| Demo von promise_type :: await_transform und einem programmseitig bereitgestellten Awaiter |
|---|
Beispiel
Diesen Code ausführen
#include <cassert> #include <coroutine> #include <iostream> struct tunable_coro { // Ein Awaiter, dessen "Bereitschaft" über den Parameter des Konstruktors bestimmt wird. class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // Drei standardmäßige Awaiter-Schnittstellenfunktionen: bool await_ready() const noexcept { return ready_; } static void await_suspend(std::coroutine_handle<>) noexcept {} static void await_resume() noexcept {} }; struct promise_type { using coro_handle = std::coroutine_handle<promise_type>; auto get_return_object() { return coro_handle::from_promise(*this); } static auto initial_suspend() { return std::suspend_always(); } static auto final_suspend() noexcept { return std::suspend_always(); } static void return_void() {} static void unhandled_exception() { std::terminate(); } // Eine benutzerdefinierte Transformationsfunktion, die den benutzerdefinierten Awaiter zurückgibt: auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); } void disable_suspension() { ready_ = false; } private: bool ready_{true}; }; tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); } // For simplicity, declare these 4 special functions as deleted: tunable_coro(tunable_coro const&) = delete; tunable_coro(tunable_coro&&) = delete; tunable_coro& operator=(tunable_coro const&) = delete; tunable_coro& operator=(tunable_coro&&) = delete; ~tunable_coro() { if (handle_) handle_.destroy(); } void disable_suspension() const { if (handle_.done()) return; handle_.promise().disable_suspension(); handle_(); } bool operator()() { if (!handle_.done()) handle_(); return !handle_.done(); } private: promise_type::coro_handle handle_; }; tunable_coro generate(int n) { for (int i{}; i != n; ++i) { std::cout << i << ' '; // Der an co_await übergebene Awaiter geht zu promise_type::await_transform, welcher // Probleme mit tunable_awaiter, die anfänglich eine Unterbrechung verursachen (Rückkehr zu // main bei jeder Iteration), aber nach einem Aufruf von disable_suspension keine Unterbrechung // passiert und die Schleife läuft bis zu ihrem Ende, ohne zu main() zurückzukehren. co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // emittiert nur das erste Element == 0 for (int k{}; k < 4; ++k) { coro(); // gibt 1 2 3 4 aus, jeweils eine Zahl pro Iteration std::cout << ": "; } coro.disable_suspension(); coro(); // gibt die Endzahlen 5 6 7 alle auf einmal aus } Ausgabe: 0 1 : 2 : 3 : 4 : 5 6 7 |
co_yield
co_yield
expression gibt einen Wert an den Aufrufer zurück und unterbricht die aktuelle Coroutine: Es ist das grundlegende Bauelement von wiederaufnehmbaren Generatorfunktionen.
co_yield
expr
|
|||||||||
co_yield
braced-init-list
|
|||||||||
Es entspricht
co_await promise.yield_value(expr)
Eine typische Implementierung von
yield_value
würde ihr Argument (durch Kopieren/Verschieben oder durch Speichern der Adresse, da die Lebensdauer des Arguments den Suspendierungspunkt innerhalb des
co_await
überdauert) im Generator-Objekt speichern und
std::suspend_always
zurückgeben, wodurch die Steuerung an den Aufrufer/Weiterführenden übergeben wird.
#include <coroutine> #include <cstdint> #include <exception> #include <iostream> template<typename T> struct Generator { // Der Klassenname 'Generator' ist unsere Wahl und ist nicht für Coroutinen erforderlich // Magie. Der Compiler erkennt eine Coroutine anhand des Schlüsselworts 'co_yield'. // Sie können den Namen 'MyGenerator' (oder einen anderen Namen) verwenden, solange Sie einschließen // Verschachtelte struct promise_type mit 'MyGenerator get_return_object()' Methode. // (Hinweis: Es ist notwendig, die Deklarationen von Konstruktoren und Destruktoren anzupassen // beim Umbenennen.) struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type // required { T value_; std::exception_ptr exception_; Generator get_return_object() { return Generator(handle_type::from_promise(*this)); } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { exception_ = std::current_exception(); } // Speichern // Ausnahme template<std::convertible_to<T> From> // C++20 concept std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // Zwischenspeichern des Ergebnisses im Promise return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // Die einzige Möglichkeit, zuverlässig herauszufinden, ob wir die Coroutine beendet haben oder nicht, // ob es einen nächsten generierten Wert geben wird (co_yield) // in Koroutine via C++ Getter (operator () unten) ist zu ausführen/fortsetzen // Coroutine bis zum nächsten co_yield-Punkt (oder bis sie das Ende erreicht). // Dann speichern/zwischenspeichern wir das Ergebnis im promise, um den Getter (operator() unten) zu ermöglichen // um es zu erfassen ohne die Coroutine auszuführen). return !h_.done(); } T operator()() { fill(); full_ = false; // wir werden unseren zuvor gecachten Inhalt verschieben // Ergebnis, um das Promise wieder leer zu machen return std::move(h_.promise().value_); } private: bool full_ = false; void fill() { if (!full_) { h_(); if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_); // Ausnahme der Coroutine im aufgerufenen Kontext propagieren full_ = true; } } }; Generator<std::uint64_t> fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw std::runtime_error("Zu große Fibonacci-Folge. Elemente würden überlaufen."); co_yield 0; if (n == 1) co_return; co_yield 1; if (n == 2) co_return; std::uint64_t a = 0; std::uint64_t b = 1; for (unsigned i = 2; i < n; ++i) { std::uint64_t s = a + b; co_yield s; a = b; b = s; } } int main() { try { auto gen = fibonacci_sequence(10); // max 94 bevor uint64_t überläuft for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "Exception: " << ex.was() << '\n'; } catch (...) { std::cerr << "Unbekannte Ausnahme.\n"; } }
Ausgabe:
fib(0)=0 fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34
Hinweise
| Feature-Test Makro | Wert | Std | Feature |
|---|---|---|---|
__cpp_impl_coroutine
|
201902L
|
(C++20) | Coroutines (Compiler-Unterstützung) |
__cpp_lib_coroutine
|
201902L
|
(C++20) | Coroutines (Bibliotheksunterstützung) |
__cpp_lib_generator
|
202207L
|
(C++23) | std::generator : synchroner Coroutine-Generator für Ranges |
Schlüsselwörter
co_await , co_return , co_yield
Bibliotheksunterstützung
Coroutine-Unterstützungsbibliothek definiert mehrere Typen, die Compile- und Laufzeitunterstützung für Coroutines bereitstellen.
Fehlerberichte
Die folgenden verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | Angewendet auf | Verhalten wie veröffentlicht | Korrektes Verhalten |
|---|---|---|---|
| CWG 2556 | C++20 |
ungültiges
return_void
machte das Verhalten beim
Verlassen des Coroutine-Endes undefiniert |
das Programm ist in diesem Fall
fehlerhaft |
| CWG 2668 | C++20 | co_await konnte nicht in Lambda-Ausdrücken erscheinen | erlaubt |
| CWG 2754 | C++23 |
*
this
wurde beim Konstruieren des Promise-Objekts
für explizite Objekt-Memberfunktionen verwendet |
*
this
wird in diesem Fall
nicht verwendet |
Siehe auch
|
(C++23)
|
Eine
view
, die einen synchronen
Coroutine
-Generator darstellt
(Klassentemplate) |
Externe Links
| 1. | Lewis Baker, 2017-2022 - Asymmetric Transfer. |
| 2. | David Mazières, 2021 - Tutorial on C++20 Coroutinen. |
| 3. | Chuanqi Xu & Yu Qi & Yao Han, 2021 - C++20 Prinzipien und Anwendungen von Coroutinen. (Chinesisch) |
| 4. | Simon Tatham, 2023 - Entwicklung benutzerdefinierter C++20 Coroutinen-Systeme. |