Créer un générateur python via c ++ 20 coroutines

Sep 27 2020

Disons que j'ai ce code python:

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

Il imprime "2", comme prévu. Je peux faire un générateur en c ++ 20 comme ça:

#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() << ' ';
    }
}

Ce générateur produit juste une chaîne lettre par lettre, mais la chaîne y est codée en dur. En python, il est possible non seulement de produire quelque chose DU générateur mais aussi de lui donner quelque chose. Je pense que cela pourrait être fait via co_await en C ++.

J'en ai besoin pour fonctionner comme ceci:

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
    }
}

Comment puis-je y parvenir? Comment faire ce "producteur" en utilisant des coroutines c ++ 20?

Réponses

12 NicolBolas Sep 27 2020 at 08:33

Vous avez essentiellement deux problèmes à surmonter si vous voulez faire cela.

Le premier est que C ++ est un langage typé statiquement. Cela signifie que les types de tout ce qui est impliqué doivent être connus au moment de la compilation. C'est pourquoi votre generatortype doit être un modèle, afin que l'utilisateur puisse spécifier le type qu'il dirige de la coroutine à l'appelant.

Donc, si vous voulez avoir cette interface bidirectionnelle, quelque chose sur votre hellofonction doit spécifier à la fois le type de sortie et le type d'entrée.

La manière la plus simple de procéder est de simplement créer un objet et de transmettre une non- constréférence à cet objet au générateur. A chaque fois qu'il effectue une co_yield, l'appelant peut modifier l'objet référencé puis demander une nouvelle valeur. La coroutine peut lire à partir de la référence et voir les données données.

Cependant, si vous insistez pour utiliser le futur type pour la coroutine à la fois en sortie et en entrée, vous devez à la fois résoudre le premier problème (en faisant prendre votre generatormodèle OutputTypeet InputType) ainsi que ce deuxième problème.

Voyez, votre objectif est d'obtenir une valeur pour la coroutine. Le problème est que la source de cette valeur (la fonction appelant votre coroutine) a un futur objet. Mais la coroutine ne peut pas accéder au futur objet. Il ne peut pas non plus accéder à l'objet de promesse auquel les futures références font référence.

Ou du moins, cela ne peut pas le faire facilement.

Il existe deux façons de procéder, avec des cas d'utilisation différents. Le premier manipule la machinerie coroutine pour faire une porte dérobée vers la promesse. Le second manipule une propriété de co_yieldpour faire fondamentalement la même chose.

Transformer

L'objet de promesse d'une coroutine est généralement caché et inaccessible depuis la coroutine. Il est accessible à l'objet futur, que crée la promesse et qui sert d'interface aux données promises. Mais il est également accessible pendant certaines parties de la co_awaitmachine.

Plus précisément, lorsque vous exécutez une co_awaitsur une expression dans une coroutine, la machinerie regarde votre type de promesse pour voir si une fonction est appelée await_transform. Si tel est le cas, il appellera cet objet de promesse await_transformsur chaque expression co_awaitsur laquelle vous vous trouvez (au moins, dans un co_awaitque vous écrivez directement, pas d'attente implicite, comme celui créé par co_yield).

En tant que tel, nous devons faire deux choses: créer une surcharge de await_transformsur le type de promesse et créer un type dont le seul but est de nous permettre d'appeler cette await_transformfonction.

Cela ressemblerait donc à ceci:

struct generator_input {};

...

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

Une petite note. L'inconvénient d'une await_transformtelle utilisation est que, en spécifiant ne serait-ce qu'une seule surcharge de cette fonction pour notre promesse, nous avons un impact sur chaque co_await coroutine qui utilise ce type. Pour une coroutine de générateur, ce n'est pas très important, car il n'y a pas beaucoup de raisons de co_awaitne pas faire un hack comme celui-ci. Mais si vous créiez un mécanisme plus général qui pourrait clairement attendre sur des éléments attendus arbitraires dans le cadre de sa génération, vous auriez un problème.

OK, nous avons donc cette await_transformfonction; que doit faire cette fonction? Il doit renvoyer un objet attendu, car il co_awaitva l'attendre. Mais le but de cet objet attendu est de fournir une référence au type d'entrée. Heureusement, le mécanisme co_awaitutilisé pour convertir l'attendable en une valeur est fourni par la await_resumeméthode de l'attendable . Ainsi, le nôtre peut simplement renvoyer un 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.
}

Cela donne à la coroutine accès à la valeur, en appelant co_await generator_input{};. Notez que cela renvoie une référence à l'objet.

Le generatortype peut facilement être modifié pour permettre la possibilité de modifier un InputTypeobjet stocké dans la promesse. Ajoutez simplement une paire de sendfonctions pour écraser la valeur d'entrée:

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

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

Cela représente un mécanisme de transport asymétrique. La coroutine récupère une valeur à un endroit et à une heure de son choix. En tant que tel, il n'a aucune obligation réelle de répondre instantanément à tout changement. Ceci est bon à certains égards, car cela permet à une coroutine de s'isoler des changements délétères. Si vous utilisez une forboucle basée sur une plage sur un conteneur, ce conteneur ne peut pas être directement modifié (dans la plupart des cas) par le monde extérieur ou bien votre programme affichera UB. Ainsi si la coroutine est fragile de cette manière, elle peut copier les données de l'utilisateur et ainsi empêcher l'utilisateur de les modifier.

Dans l'ensemble, le code nécessaire n'est pas si volumineux. Voici un exemple exécutable de votre code avec ces modifications:

#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() << ' ';
    }
}

Soyez plus productif

Une alternative à l'utilisation d'un explicite co_awaitconsiste à exploiter une propriété de co_yield. À savoir, co_yieldest une expression et donc elle a une valeur. Plus précisément, c'est (principalement) équivalent à co_await p.yield_value(e), où pest l'objet de la promesse (ohh!) Et ec'est ce que nous cédons .

Heureusement, nous avons déjà une yield_valuefonction; il revient std::suspend_always. Mais il pourrait aussi renvoyer un objet qui suspend toujours, mais aussi qui co_awaitpeut décompresser dans un 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};
}

Il s'agit d'un mécanisme de transport symétrique; pour chaque valeur que vous donnez, vous recevez une valeur (qui peut être la même qu'avant). Contrairement à la co_awaitméthode explicite , vous ne pouvez pas recevoir de valeur avant de commencer à les générer. Cela pourrait être utile pour certaines interfaces.

Et bien sûr, vous pouvez les combiner comme bon vous semble.