Python-Generator über C ++ 20-Coroutinen erstellen

Sep 27 2020

Angenommen, ich habe diesen Python-Code:

def double_inputs():
    while True:
        x = yield
        yield x * 2
gen = double_inputs()
next(gen)
print(gen.send(1))

Es wird wie erwartet "2" gedruckt. Ich kann einen Generator in c ++ 20 so machen:

#include <coroutine>

template <class T>
struct generator {
    struct promise_type;
    using coro_handle = std::coroutine_handle<promise_type>;

    struct promise_type {
        T current_value;
        auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        auto yield_value(T value) {
            current_value = value;
            return std::suspend_always{};
        }
    };

    bool next() { return coro ? (coro.resume(), !coro.done()) : false; }
    T value() { return coro.promise().current_value; }

    generator(generator const & rhs) = delete;
    generator(generator &&rhs)
        :coro(rhs.coro)
    {
        rhs.coro = nullptr;
    }
    ~generator() {
        if (coro)
            coro.destroy();
    }
private:
    generator(coro_handle h) : coro(h) {}
    coro_handle coro;
};

generator<char> hello(){
    //TODO:send string here via co_await, but HOW???
    std::string word = "hello world";
    for(auto &ch:word){
        co_yield ch;
    }
}

int main(int, char**) {
    for (auto i = hello(); i.next(); ) {
        std::cout << i.value() << ' ';
    }
}

Dieser Generator erzeugt nur eine Zeichenfolge Buchstabe für Buchstabe, aber die Zeichenfolge ist darin fest codiert. In Python ist es möglich, nicht nur etwas vom Generator zu liefern, sondern auch etwas dazu. Ich glaube, dass dies über co_await in C ++ erfolgen könnte.

Ich brauche es, um so zu arbeiten:

generator<char> hello(){
    std::string word = co_await producer; // Wait string from producer somehow 
    for(auto &ch:word){
        co_yield ch;
    }
}

int main(int, char**) {
    auto gen = hello(); //make consumer
    producer("hello world"); //produce string
    for (; gen.next(); ) {
        std::cout << gen.value() << ' '; //consume string letter by letter
    }
}

Wie kann ich das erreichen? Wie macht man diesen "Produzenten" mit c ++ 20 Coroutinen?

Antworten

12 NicolBolas Sep 27 2020 at 08:33

Sie müssen im Wesentlichen zwei Probleme überwinden, wenn Sie dies tun möchten.

Das erste ist, dass C ++ eine statisch typisierte Sprache ist. Dies bedeutet, dass die Typen aller beteiligten Elemente bei der Kompilierung bekannt sein müssen. Aus diesem Grund muss Ihr generatorTyp eine Vorlage sein, damit der Benutzer angeben kann, welchen Typ er von der Coroutine zum Anrufer führt.

Wenn Sie also diese bidirektionale Schnittstelle haben möchten, muss etwas in Ihrer helloFunktion sowohl den Ausgabetyp als auch den Eingabetyp angeben.

Der einfachste Weg, dies zu tun, besteht darin, einfach ein Objekt zu erstellen und eine Nichtreferenz constauf dieses Objekt an den Generator zu übergeben. Jedes Mal, wenn a ausgeführt wird co_yield, kann der Aufrufer das referenzierte Objekt ändern und dann nach einem neuen Wert fragen. Die Coroutine kann aus der Referenz lesen und die angegebenen Daten anzeigen.

Wenn Sie jedoch darauf bestehen, den zukünftigen Typ für die Coroutine sowohl als Ausgabe als auch als Eingabe zu verwenden, müssen Sie sowohl das erste Problem (indem Sie Ihre generatorVorlage übernehmen OutputTypeund InputType) als auch dieses zweite Problem lösen.

Sehen Sie, Ihr Ziel ist es, einen Wert für die Coroutine zu erhalten. Das Problem ist, dass die Quelle dieses Werts (die Funktion, die Ihre Coroutine aufruft) ein zukünftiges Objekt hat. Die Coroutine kann jedoch nicht auf das zukünftige Objekt zugreifen . Es kann auch nicht auf das Versprechen zugreifen, auf das sich die Zukunft bezieht.

Oder zumindest nicht so einfach.

Es gibt zwei Möglichkeiten, dies mit unterschiedlichen Anwendungsfällen zu tun. Der erste manipuliert die Coroutine-Maschinerie, um einen Weg in das Versprechen zu finden. Der zweite manipuliert eine Eigenschaft co_yield, im Grunde das Gleiche zu tun.

Verwandeln

Das Versprechungsobjekt für eine Coroutine ist normalerweise verborgen und für die Coroutine nicht zugänglich. Es ist für das zukünftige Objekt zugänglich, das durch das Versprechen erstellt wird und das als Schnittstelle zu den versprochenen Daten fungiert. Es ist aber auch an bestimmten Stellen der co_awaitMaschine zugänglich .

Insbesondere wenn Sie einen co_awaitAusdruck in einer Coroutine ausführen, überprüft die Maschinerie Ihren Versprechenstyp, um festzustellen, ob eine Funktion aufgerufen wurde await_transform. In diesem Fall wird dieses Versprechungsobjekt await_transformfür jeden Ausdruck aufgerufen, auf dem Sie sich befinden co_await(zumindest in einem co_await, den Sie direkt schreiben, und nicht implizit erwartet, wie dem von co_yield).

Als solches müssen wir zwei Dinge tun: eine Überladung des await_transformVersprechungstyps erstellen und einen Typ erstellen, dessen einziger Zweck darin besteht, uns zu ermöglichen, diese await_transformFunktion aufzurufen .

Das würde also ungefähr so ​​aussehen:

struct generator_input {};

...

//Within the promise type:
auto await_transform(generator_input);

Eine kurze Anmerkung. Der Nachteil der Verwendung await_transformwie dieser ist , dass, selbst durch eine Überlastung dieser Funktion für unser Versprechen spezifizieren, wir beeinflussen jeden co_await in jedem Koroutine , die diese Art verwendet. Für eine Generator-Coroutine ist das nicht sehr wichtig, da es nicht viel Grund dafür gibt, es co_awaitsei denn, Sie machen einen solchen Hack. Wenn Sie jedoch einen allgemeineren Mechanismus erstellen würden, der als Teil seiner Generierung eindeutig auf beliebige Erwartungswerte warten könnte, hätten Sie ein Problem.

OK, also haben wir diese await_transformFunktion; Was muss diese Funktion tun? Es muss ein erwartetes Objekt zurückgeben, da co_awaites darauf warten wird. Der Zweck dieses erwarteten Objekts besteht jedoch darin, einen Verweis auf den Eingabetyp zu liefern. Glücklicherweise wird der Mechanismus co_awaitzum Konvertieren des Erwarteten in einen Wert durch die await_resumeMethode des Wartens bereitgestellt . Unsere können also einfach Folgendes zurückgeben InputType&:

//Within the `generator<OutputType, InputType>`:
    struct passthru_value
    {
        InputType &ret_;

        bool await_ready() {return true;}
        void await_suspend(coro_handle) {}
        InputType &await_resume() { return ret_; }
    };


//Within the promise type:
auto await_transform(generator_input)
{
    return passthru_value{input_value}; //Where `input_value` is the `InputType` object stored by the promise.
}

Dadurch erhält die Coroutine durch Aufrufen Zugriff auf den Wert co_await generator_input{};. Beachten Sie, dass dies einen Verweis auf das Objekt zurückgibt.

Der generatorTyp kann leicht geändert werden, um ein InputTypeim Versprechen gespeichertes Objekt ändern zu können. Fügen Sie einfach ein sendFunktionspaar zum Überschreiben des Eingabewerts hinzu:

void send(const InputType &input)
{
    coro.promise().input_value = input;
} 

void send(InputType &&input)
{
    coro.promise().input_value = std::move(input);
} 

Dies stellt einen asymmetrischen Transportmechanismus dar. Die Coroutine ruft einen Wert an einem Ort und zu einer Zeit ihrer Wahl ab. Daher besteht keine wirkliche Verpflichtung, sofort auf Änderungen zu reagieren. Dies ist in mancher Hinsicht gut, da sich eine Coroutine vor schädlichen Veränderungen schützen kann. Wenn Sie eine bereichsbasierte forSchleife über einem Container verwenden, kann dieser Container (in den meisten Fällen) nicht direkt von der Außenwelt geändert werden, da Ihr Programm sonst UB aufweist. Wenn die Coroutine auf diese Weise zerbrechlich ist, kann sie die Daten vom Benutzer kopieren und so verhindern, dass der Benutzer sie ändert.

Alles in allem ist der benötigte Code nicht so groß. Hier ist ein ausführbares Beispiel für Ihren Code mit den folgenden Änderungen:

#include <coroutine>
#include <exception>
#include <string>
#include <iostream>

struct generator_input {};


template <typename OutputType, typename InputType>
struct generator {
    struct promise_type;
    using coro_handle = std::coroutine_handle<promise_type>;

    struct passthru_value
    {
        InputType &ret_;

        bool await_ready() {return true;}
        void await_suspend(coro_handle) {}
        InputType &await_resume() { return ret_; }
    };

    struct promise_type {
        OutputType current_value;
        InputType input_value;


        auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        auto yield_value(OutputType value) {
            current_value = value;
            return std::suspend_always{};
        }

        void return_void() {}

        auto await_transform(generator_input)
        {
            return passthru_value{input_value};
        }
    };

    bool next() { return coro ? (coro.resume(), !coro.done()) : false; }
    OutputType value() { return coro.promise().current_value; }

    void send(const InputType &input)
    {
        coro.promise().input_value = input;
    } 

    void send(InputType &&input)
    {
        coro.promise().input_value = std::move(input);
    } 

    generator(generator const & rhs) = delete;
    generator(generator &&rhs)
        :coro(rhs.coro)
    {
        rhs.coro = nullptr;
    }
    ~generator() {
        if (coro)
            coro.destroy();
    }
private:
    generator(coro_handle h) : coro(h) {}
    coro_handle coro;
};

generator<char, std::string> hello(){
    auto word = co_await generator_input{};

    for(auto &ch: word){
        co_yield ch;
    }
}

int main(int, char**)
{
    auto test = hello();
    test.send("hello world");

    while(test.next())
    {
        std::cout << test.value() << ' ';
    }
}

Sei nachgiebiger

Eine Alternative zur Verwendung eines expliziten co_awaitist die Ausnutzung einer Eigenschaft von co_yield. Ist nämlich co_yieldein Ausdruck und hat daher einen Wert. Insbesondere ist es (meistens) äquivalent zu co_await p.yield_value(e), wo psich das Versprechen befindet (ohh!) Und ewas wir nachgeben.

Zum Glück haben wir bereits eine yield_valueFunktion; es kehrt zurück std::suspend_always. Aber es könnte auch ein Objekt zurückgeben , das immer unterbricht, sondern auch die co_awaitin ein entpacken InputType&:

struct yield_thru
{
    InputType &ret_;

    bool await_ready() {return false;}
    void await_suspend(coro_handle) {}
    InputType &await_resume() { return ret_; }
};

...

//in the promise
auto yield_value(OutputType value) {
    current_value = value;
    return yield_thru{input_value};
}

Dies ist ein symmetrischer Transportmechanismus; Für jeden Wert, den Sie erzielen, erhalten Sie einen Wert (der möglicherweise derselbe wie zuvor ist). Im Gegensatz zur expliziten co_awaitMethode können Sie keinen Wert erhalten, bevor Sie mit der Generierung beginnen. Dies kann für bestimmte Schnittstellen nützlich sein.

Und natürlich können Sie sie nach Belieben kombinieren.