Namensräume
Varianten
Aktionen

Coroutinen (C++20)

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 
 

Eine Coroutine ist eine Funktion, deren Ausführung angehalten und später wieder aufgenommen werden kann. Coroutinen sind stapellos: Sie unterbrechen die Ausführung, indem sie zum Aufrufer zurückkehren, und die Daten, die zur Wiederaufnahme der Ausführung benötigt werden, werden getrennt vom Stack gespeichert. Dies ermöglicht sequenziellen Code, der asynchron ausgeführt wird (z. B. zur Behandlung von nicht-blockierendem E/A ohne explizite Rückrufe), und unterstützt auch Algorithmen für lazy-berechnete unendliche Sequenzen und andere Verwendungszwecke.

Eine Funktion ist eine Coroutine, wenn ihre Definition eine der folgenden Bedingungen enthält

  • den co_await Ausdruck — um die Ausführung zu unterbrechen, bis sie wieder aufgenommen wird
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));
    }
}
  • den co_yield Ausdruck — um die Ausführung unterbrechen und einen Wert zurückgeben
generator<unsigned int> iota(unsigned int n = 0)
{
    while (true)
        co_yield n++;
}
  • die co_return Anweisung — um die Ausführung beenden und einen Wert zurückgeben
lazy<int> f()
{
    co_return 7;
}

Jeder Coroutine muss ein Rückgabetyp zugeordnet sein, der eine Reihe von unten genannten Anforderungen erfüllt.

Inhalt

[bearbeiten] Einschränkungen

Coroutinen können keine variadischen Argumente, einfache return-Anweisungen oder Platzhalter-Rückgabetypen (auto oder Concept) verwenden.

Consteval-Funktionen, constexpr-Funktionen, Konstruktoren, Destruktoren und die main-Funktion können keine Coroutinen sein.

[bearbeiten] Ausführung

Jede Coroutine ist verbunden mit

  • dem Promise-Objekt, das von innerhalb der Coroutine manipuliert wird. Die Coroutine übermittelt ihr Ergebnis oder ihre Ausnahme über dieses Objekt. Promise-Objekte haben keinerlei Bezug zu std::promise.
  • dem 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-Frame zu zerstören.
  • dem Coroutine-Zustand, der ein interner, dynamisch allozierter Speicherbereich ist (es sei denn, die Allokation wird optimiert), ein Objekt, das enthält
  • das Promise-Objekt
  • die Parameter (alle per Wert kopiert)
  • eine Darstellung des aktuellen Aufhängepunkts, damit eine Wiederaufnahme weiß, wo fortzufahren ist, und eine Zerstörung weiß, welche lokalen Variablen im Geltungsbereich waren
  • lokale Variablen und temporäre Objekte, deren Lebensdauer den aktuellen Aufhängepunkt überspannt.

Wenn eine Coroutine mit der Ausführung beginnt, führt sie Folgendes aus

  • alloziiert das Coroutine-Zustandsobjekt mithilfe von operator new.
  • kopiert alle Funktionsparameter in den Coroutine-Zustand: Parameter per Wert werden verschoben oder kopiert, Parameter per Referenz bleiben Referenzen (sie können daher ungültig werden, wenn die Coroutine nach dem Ende der Lebensdauer des referenzierten Objekts wieder aufgenommen wird — siehe unten für Beispiele).
  • ruft den Konstruktor für das Promise-Objekt auf. Wenn der Promise-Typ einen Konstruktor hat, der alle Coroutine-Parameter annimmt, wird dieser Konstruktor mit den kopierten Coroutine-Argumenten aufgerufen. Andernfalls wird der Standardkonstruktor aufgerufen.
  • ruft promise.get_return_object() auf und speichert das Ergebnis in einer lokalen Variable. Das Ergebnis dieses Aufrufs wird an den Aufrufer zurückgegeben, wenn die Coroutine zum ersten Mal angehalten wird. Alle bis zu diesem Schritt und einschließlich dieses Schritts geworfenen Ausnahmen werden an den Aufrufer propagiert und nicht im Promise platziert.
  • ruft promise.initial_suspend() auf und wartet auf dessen Ergebnis mit co_await. Typische Promise-Typen geben entweder std::suspend_always für lazy gestartete Coroutinen oder std::suspend_never für eager gestartete Coroutinen zurück.
  • wenn co_await promise.initial_suspend() wieder aufgenommen wird, beginnt die Ausführung des Coroutine-Körpers.

Einige Beispiele für Parameter, die ungültig werden

#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} destroyed
    h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
    h.destroy();
}
 
coroutine bad2()
{
    S s{0};
    return s.f(); // returned coroutine can't be resumed without committing use after free
}
 
void bad3()
{
    coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
    {
        std::cout << i;
        co_return;
    }(); // immediately invoked
    // lambda destroyed
    h.resume(); // uses (anonymous lambda type)::i after free
    h.destroy();
}
 
void good()
{
    coroutine h = [](int i) -> coroutine // make i a coroutine parameter
    {
        std::cout << i;
        co_return;
    }(0);
    // lambda destroyed
    h.resume(); // no problem, i has been copied to the coroutine
                // frame as a by-value parameter
    h.destroy();
}

Wenn eine Coroutine einen Aufhängepunkt erreicht

  • wird das zuvor erhaltene Rückgabeobjekt an den Aufrufer/Wiederaufnehmer 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() für
  • 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 wartet mit co_await auf das Ergebnis.

Das Auslassen des Coroutine-Endes entspricht co_return;, außer dass das Verhalten undefiniert ist, wenn im Geltungsbereich von Promise keine Deklarationen von return_void gefunden werden können. Eine Funktion ohne die definierenden Schlüsselwörter in ihrem Funktionskörper ist keine Coroutine, unabhängig von ihrem Rückgabetyp, und das Auslassen des Endes führt zu undefiniertem Verhalten, wenn der Rückgabetyp nicht (möglicherweise cv-qualifiziert) void ist.

// assuming that task is some coroutine task type
task<void> f()
{
    // not a coroutine, undefined behavior
}
 
task<void> g()
{
    co_return;  // OK
}
 
task<void> h()
{
    co_await g();
    // OK, implicit co_return;
}

Wenn die Coroutine mit einer unbehandelten Ausnahme endet, führt sie Folgendes aus

  • fängt die Ausnahme ab und ruft promise.unhandled_exception() aus dem Catch-Block auf
  • ruft promise.final_suspend() auf und wartet mit co_await auf das Ergebnis (z. B. um eine Fortsetzung fortzusetzen oder ein Ergebnis zu veröffentlichen). Es ist undefiniertes Verhalten, eine Coroutine von diesem Punkt an fortzusetzen.

Wenn der Coroutine-Zustand zerstört wird, entweder weil er über co_return oder eine unbehandelte Ausnahme beendet wurde, oder weil er über sein Handle zerstört wurde, geschieht Folgendes

  • ruft den Destruktor des Promise-Objekts auf.
  • ruft die Destruktoren der Funktionsparameter-Kopien auf.
  • ruft operator delete auf, um den für den Coroutine-Zustand verwendeten Speicher freizugeben.
  • überträgt die Ausführung zurück an den Aufrufer/Wiederaufnehmer.

[bearbeiten] Dynamische Allokation

Der Coroutine-Zustand wird dynamisch über nicht-Array operator new alloziiert.

Wenn der Promise-Typ eine klassenweite Ersetzung definiert, wird diese verwendet, andernfalls wird der globale operator new verwendet.

Wenn der Promise-Typ eine Platzierungsform von operator new definiert, die zusätzliche Parameter annimmt, und diese mit einer Argumentliste übereinstimmen, bei der das erste Argument die angeforderte Größe (vom Typ std::size_t) und die restlichen die Coroutine-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 (auch wenn ein benutzerdefinierter Allokator verwendet wird), wenn

  • die Lebensdauer des Coroutine-Zustands streng in die Lebensdauer des Aufrufers verschachtelt ist, und
  • die Größe des Coroutine-Frames am Aufruf-Site bekannt ist.

In diesem Fall wird der Coroutine-Zustand in den Stack-Frame des Aufrufers (wenn der Aufrufer eine normale Funktion ist) oder in den Coroutine-Zustand (wenn der Aufrufer eine Coroutine ist) eingebettet.

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 bei Allokationsfehlern gibt die Coroutine sofort das von Promise::get_return_object_on_allocation_failure() erhaltene Objekt an den Aufrufer zurück, z. B.

struct Coroutine::promise_type
{
    /* ... */
 
    // ensure the use of non-throwing operator-new
    static Coroutine get_return_object_on_allocation_failure()
    {
        std::cerr << __func__ << '\n';
        throw std::bad_alloc(); // or, return Coroutine(nullptr);
    }
 
    // custom non-throwing overload of new
    void* operator new(std::size_t n) noexcept
    {
        if (void* mem = std::malloc(n))
            return mem;
        return nullptr; // allocation failure
    }
};

[bearbeiten] Promise

Der Promise-Typ wird vom Compiler aus dem Rückgabetyp der Coroutine mittels std::coroutine_traits ermittelt.

Formal, sei

  • R und Args... der Rückgabetyp und die Parameterliste einer Coroutine bezeichnen,
  • ClassT der Klassentyp ist, zu dem die Coroutine gehört, wenn sie als nicht-statische Memberfunktion definiert ist,
  • cv die cv-Qualifikation bezeichnet, die in der Funktionsdeklaration deklariert ist, wenn sie als nicht-statische Memberfunktion definiert ist,

ihr Promise-Typ wird bestimmt durch

  • std::coroutine_traits<R, Args...>::promise_type, wenn die Coroutine nicht als implizite Objekt-Memberfunktion definiert ist,
  • std::coroutine_traits<R, cv ClassT&, Args...>::promise_type, wenn die Coroutine als implizite Objekt-Memberfunktion definiert ist, die keine rvalue-Referenz-qualifizierte ist,
  • std::coroutine_traits<R, cv ClassT&&, Args...>::promise_type, wenn die Coroutine als implizite Objekt-Memberfunktion definiert ist, die 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

[bearbeiten] 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 Funktionskörpers (einschließlich des Funktionskörpers eines Lambda-Ausdrucks) erscheinen und kann nicht erscheinen

  • in einem Handler,
  • in einer Deklaration, es sei denn, sie erscheint in einem Initialisierer dieser Deklarationsanweisung,
  • in der einfachen Deklaration eines init-statement (siehe if, switch, for und [[../range-for|Bereich-for]]), es sei denn, sie erscheint in einem Initialisierer dieses init-statement ,
  • in einem Standardargument, oder
  • im Initialisierer einer Variable im Block-Geltungsbereich mit statischer oder Thread-Speicherdauer.

Ein co_await-Ausdruck kann kein potenziell ausgewerteter Unterausdruck des Prädikats einer Vertragsbedingung sein.

(seit C++26)

Zuerst wird expr wie folgt in ein Awaitable konvertiert

  • wenn expr von einem initialen Aufhängepunkt, einem finalen Aufhängepunkt oder einem Yield-Ausdruck erzeugt wird, ist das Awaitable expr, so wie es ist.
  • andernfalls, wenn der Promise-Typ der aktuellen Coroutine die Memberfunktion await_transform hat, dann ist das Awaitable promise.await_transform(expr).
  • andernfalls ist das Awaitable expr, so wie es ist.

Dann wird das Awaiter-Objekt wie folgt ermittelt

  • wenn die Überladungsauflösung für operator co_await eine eindeutige beste Überladung ergibt, ist der Awaiter das Ergebnis dieses Aufrufs
  • awaitable.operator co_await() für 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 der Awaitable, so wie er ist.
  • andernfalls, wenn die Überladungsauflösung mehrdeutig ist, ist das Programm fehlerhaft.

Wenn der obige Ausdruck ein prvalue ist, ist das Awaiter-Objekt ein temporäres Objekt, das daraus materialisiert wird. Andernfalls, wenn der obige Ausdruck ein glvalue ist, ist das Awaiter-Objekt das Objekt, auf das er sich bezieht.

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 dessen Ergebnis, kontextbezogen in bool konvertiert, false ist, dann

wird die Coroutine unterbrochen (ihr Coroutine-Zustand wird mit lokalen Variablen und dem aktuellen Aufhängepunkt gefüllt).
wird awaiter.await_suspend(handle) aufgerufen, wobei handle das Coroutine-Handle ist, das die aktuelle Coroutine repräsentiert. Innerhalb dieser Funktion ist der unterbrochene 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 (false zurückzugeben zählt als Planung)
  • wenn await_suspend void zurückgibt, wird die Kontrolle sofort an den Aufrufer/Wiederaufnehmer der aktuellen Coroutine zurückgegeben (diese Coroutine bleibt unterbrochen), andernfalls
  • wenn await_suspend bool zurückgibt,
  • gibt der Wert true die Kontrolle an den Aufrufer/Wiederaufnehmer der aktuellen Coroutine zurück
  • setzt der Wert false die aktuelle Coroutine fort.
  • wenn await_suspend ein Coroutine-Handle für eine andere Coroutine zurückgibt, wird dieses Handle wieder aufgenommen (durch einen Aufruf von handle.resume()) (beachten Sie, dass dies verkettet werden kann, um letztendlich die Wiederaufnahme der aktuellen Coroutine zu bewirken).
  • wenn await_suspend eine Ausnahme auslöst, wird die Ausnahme abgefangen, die Coroutine wird fortgesetzt und die Ausnahme sofort erneut ausgelöst.

Schließlich wird awaiter.await_resume() aufgerufen (unabhängig davon, ob die Coroutine unterbrochen wurde oder nicht), und dessen Ergebnis ist das Ergebnis des gesamten co_await expr-Ausdrucks.

Wenn die Coroutine im co_await-Ausdruck unterbrochen wurde und später wieder aufgenommen wird, ist der Wiederaufnahmepunkt unmittelbar vor dem Aufruf von awaiter.await_resume().

Beachten Sie, dass die Coroutine vollständig unterbrochen wird, bevor awaiter.await_suspend() betreten wird. Ihr Handle kann mit einem anderen Thread geteilt und wieder aufgenommen werden, bevor die Funktion await_suspend() zurückkehrt. (Beachten Sie, dass die Standardregeln für Speichersicherheit immer noch gelten. Wenn ein Coroutine-Handle ohne Sperre zwischen Threads geteilt wird, sollte der Awaiter mindestens Release-Semantik verwenden und der Wiederaufnehmer mindestens Acquire-Semantik verwenden.) Zum Beispiel kann das Coroutine-Handle in einem Callback platziert und zur Ausführung auf einem Thread-Pool geplant werden, wenn eine asynchrone E/A-Operation abgeschlossen ist. In diesem Fall, da die aktuelle Coroutine möglicherweise wieder aufgenommen und somit den Destruktor des Awaiter-Objekts ausgeführt hat, während await_suspend() auf dem aktuellen Thread weiter ausgeführt wird, sollte await_suspend() *this als zerstört behandeln und nicht darauf zugreifen, nachdem das Handle für andere Threads veröffentlicht wurde.

[bearbeiten] 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(); });
            // Potential undefined behavior: accessing potentially destroyed *this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // this is 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 destroyed here
    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 Aufhängepunkt überspannt) und wird zerstört, bevor der co_await-Ausdruck beendet wird. Es kann verwendet werden, um pro Operation Zustände zu verwalten, wie sie von einigen asynchronen E/A-APIs benötigt werden, ohne auf zusätzliche dynamische Allokationen zurückgreifen zu müssen.

Die Standardbibliothek definiert zwei triviale Awaitables: std::suspend_always und std::suspend_never.

Demo von promise_type::await_transform und einem bereitgestellten Awaiter

[bearbeiten] Beispiel

#include <cassert>
#include <coroutine>
#include <iostream>
 
struct tunable_coro
{
    // An awaiter whose "readiness" is determined via constructor's parameter.
    class tunable_awaiter
    {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
        // Three standard awaiter interface functions:
        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(); }
        // A user provided transforming function which returns the custom awaiter:
        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 << ' ';
        // The awaiter passed to co_await goes to promise_type::await_transform which
        // issues tunable_awaiter that initially causes suspension (returning back to
        // main at each iteration), but after a call to disable_suspension no suspension
        // happens and the loop runs to its end without returning to main().
        co_await std::suspend_always{};
    }
}
 
int main()
{
    auto coro = generate(8);
    coro(); // emits only one first element == 0
    for (int k{}; k < 4; ++k)
    {
        coro(); // emits 1 2 3 4, one per each iteration
        std::cout << ": ";
    }
    coro.disable_suspension();
    coro(); // emits the tail numbers 5 6 7 all at ones
}

Ausgabe

0 1 : 2 : 3 : 4 : 5 6 7

[bearbeiten] co_yield

Der co_yield-Ausdruck gibt einen Wert an den Aufrufer zurück und unterbricht die aktuelle Coroutine: er ist der übliche Baustein für wiederaufnehmbare Generatorfunktionen.

co_yield expr
co_yield braced-init-list

Es ist äquivalent zu

co_await promise.yield_value(expr)

Ein typisches Generator-yield_value würde sein Argument im Generatorobjekt speichern (kopieren/verschieben oder nur die Adresse davon speichern, da die Lebensdauer des Arguments den Aufhängepunkt innerhalb von co_await überspannt) und std::suspend_always zurückgeben, wodurch die Kontrolle an den Aufrufer/Wiederaufnehmer übertragen wird.

#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>
 
template<typename T>
struct Generator
{
    // The class name 'Generator' is our choice and it is not required for coroutine
    // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword.
    // You can use name 'MyGenerator' (or any other name) instead as long as you include
    // nested struct promise_type with 'MyGenerator get_return_object()' method.
    // (Note: It is necessary to adjust the declarations of constructors and destructors
    //  when renaming.)
 
    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(); } // saving
                                                                              // exception
 
        template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From&& from)
        {
            value_ = std::forward<From>(from); // caching the result in promise
            return {};
        }
        void return_void() {}
    };
 
    handle_type h_;
 
    Generator(handle_type h) : h_(h) {}
    ~Generator() { h_.destroy(); }
    explicit operator bool()
    {
        fill(); // The only way to reliably find out whether or not we finished coroutine,
                // whether or not there is going to be a next value generated (co_yield)
                // in coroutine via C++ getter (operator () below) is to execute/resume
                // coroutine until the next co_yield point (or let it fall off end).
                // Then we store/cache result in promise to allow getter (operator() below
                // to grab it without executing coroutine).
        return !h_.done();
    }
    T operator()()
    {
        fill();
        full_ = false; // we are going to move out previously cached
                       // result to make promise empty again
        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_);
            // propagate coroutine exception in called context
 
            full_ = true;
        }
    }
};
 
Generator<std::uint64_t>
fibonacci_sequence(unsigned n)
{
    if (n == 0)
        co_return;
 
    if (n > 94)
        throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
 
    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 before uint64_t overflows
 
        for (int j = 0; gen; ++j)
            std::cout << "fib(" << j << ")=" << gen() << '\n';
    }
    catch (const std::exception& ex)
    {
        std::cerr << "Exception: " << ex.what() << '\n';
    }
    catch (...)
    {
        std::cerr << "Unknown exception.\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

[bearbeiten] Anmerkungen

Feature-Test-Makro Wert Std Feature
__cpp_impl_coroutine 201902L (C++20) Coroutinen (Compilerunterstützung)
__cpp_lib_coroutine 201902L (C++20) Coroutinen (Bibliotheksunterstützung)
__cpp_lib_generator 202207L (C++23) std::generator: synchroner Coroutine-Generator für Bereiche

[bearbeiten] Schlüsselwörter

co_await, co_return, co_yield

[bearbeiten] Bibliotheksunterstützung

Die Coroutine-Unterstützungsbibliothek definiert mehrere Typen, die Compile- und Laufzeitunterstützung für Coroutinen bieten.

[bearbeiten] Fehlerberichte

Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.

DR angewendet auf Verhalten wie veröffentlicht Korrigiertes Verhalten
CWG 2556 C++20 ungültiges return_void machte das Verhalten von
dem Auslassen des Coroutine-Endes undefiniert
das Programm ist schlecht ge-
formt in diesem Fall
CWG 2668 C++20 co_await konnte nicht in Lambda-Ausdrücken erscheinen erlaubt
CWG 2754 C++23 *this wurde beim Erstellen des Promise genommen
Objekt für explizite Objekt-Memberfunktionen
*this ist nicht
wird in diesem Fall genommen

[bearbeiten] Siehe auch

(C++23)
Eine view, die einen synchronen Coroutine-Generator darstellt
(Klassentemplate) [bearbeiten]

[bearbeiten] Externe Links

1.  Lewis Baker, 2017-2022 - Asymmetric Transfer.
2.  David Mazières, 2021 - Tutorial zu C++20 Coroutinen.
3.  Chuanqi Xu & Yu Qi & Yao Han, 2021 - C++20 Prinzipien und Anwendungen von Coroutinen. (Chinesisch)
4.  Simon Tatham, 2023 - Schreiben benutzerdefinierter C++20 Coroutine-Systeme.