Fazendo gerador de python via co-rotinas c ++ 20

Sep 27 2020

Digamos que eu tenha este código Python:

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

Ele imprime "2", exatamente como esperado. Posso fazer um gerador em c ++ 20 assim:

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

Este gerador apenas produz uma string letra por letra, mas a string está codificada nele. Em python, é possível não apenas produzir algo do gerador, mas também produzir algo PARA ele. Eu acredito que isso poderia ser feito via co_await em C ++.

Eu preciso que funcione assim:

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

Como posso conseguir isso? Como fazer este "produtor" usando co-rotinas c ++ 20?

Respostas

12 NicolBolas Sep 27 2020 at 08:33

Você tem essencialmente dois problemas a superar se quiser fazer isso.

A primeira é que C ++ é uma linguagem tipada estaticamente. Isso significa que os tipos de tudo envolvido precisam ser conhecidos em tempo de compilação. É por isso que o seu generatortipo precisa ser um modelo, para que o usuário possa especificar que tipo ele conduz da co-rotina ao chamador.

Portanto, se você deseja ter essa interface bidirecional, algo em sua hellofunção deve especificar o tipo de saída e o tipo de entrada.

A maneira mais simples de fazer isso é apenas criar um objeto e passar uma não constreferência a esse objeto para o gerador. Cada vez que faz um co_yield, o chamador pode modificar o objeto referenciado e, em seguida, solicitar um novo valor. A co-rotina pode ler a referência e ver os dados fornecidos.

No entanto, se você insiste em usar o tipo futuro para a co-rotina como saída e entrada, você precisa resolver o primeiro problema (fazendo seu generatormodelo tomar OutputTypee InputType) e também este segundo problema.

Veja, seu objetivo é obter um valor para a co-rotina. O problema é que a fonte desse valor (a função que chama sua co-rotina) tem um objeto futuro. Mas a co-rotina não pode acessar o objeto futuro. Nem pode acessar o objeto de promessa que o futuro faz referência.

Ou, pelo menos, não pode ser feito tão facilmente.

Existem duas maneiras de fazer isso, com diferentes casos de uso. O primeiro manipula a maquinaria da co-rotina para abrir caminho para a promessa. O segundo manipula uma propriedade de co_yieldpara fazer basicamente a mesma coisa.

Transformar

O objeto de promessa para uma co-rotina geralmente está oculto e inacessível da co-rotina. É acessível ao objeto futuro, que a promessa cria e que atua como uma interface para os dados prometidos. Mas também é acessível durante certas partes da co_awaitmáquina.

Especificamente, quando você executa um co_awaitem qualquer expressão em uma co-rotina, o mecanismo examina seu tipo de promessa para ver se ele tem uma função chamada await_transform. Em caso afirmativo, ele chamará esse objeto de promessa await_transformem cada expressão co_awaitem que você (pelo menos, em uma co_awaitque você escreve diretamente, não espera implícita, como a criada por co_yield).

Como tal, precisamos fazer duas coisas: criar uma sobrecarga de await_transformno tipo de promessa e criar um tipo cujo único propósito é permitir que chamemos essa await_transformfunção.

Isso seria algo assim:

struct generator_input {};

...

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

Uma nota rápida. A desvantagem de usar await_transformassim é que, ao especificar até mesmo uma sobrecarga dessa função para nossa promessa, impactamos cada co_await em qualquer corrotina que usa esse tipo. Para uma co-rotina de gerador, isso não é muito importante, já que não há muitos motivos para isso, a co_awaitmenos que você esteja fazendo um hack como este. Mas se você estivesse criando um mecanismo mais geral que pudesse aguardar distintamente em aguardáveis ​​arbitrários como parte de sua geração, você teria um problema.

OK, então temos esta await_transformfunção; o que essa função precisa fazer? Ele precisa retornar um objeto aguardável, já que co_awaitvai aguardar nele. Mas o propósito desse objeto aguardável é fornecer uma referência ao tipo de entrada. Felizmente, o mecanismo co_awaitusado para converter o aguardável em um valor é fornecido pelo await_resumemétodo do aguardável . Portanto, o nosso pode apenas retornar um 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.
}

Isso dá à co-rotina acesso ao valor, invocando co_await generator_input{};. Observe que isso retorna uma referência ao objeto.

O generatortipo pode ser facilmente modificado para permitir a modificação de um InputTypeobjeto armazenado na promessa. Basta adicionar um par de sendfunções para substituir o valor de entrada:

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

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

Isso representa um mecanismo de transporte assimétrico. A co-rotina recupera um valor em um lugar e hora de sua própria escolha. Como tal, não tem nenhuma obrigação real de responder instantaneamente a quaisquer alterações. Isso é bom em alguns aspectos, pois permite que uma co-rotina se isole de mudanças deletérias. Se você estiver usando um forloop baseado em intervalo em um contêiner, esse contêiner não pode ser modificado diretamente (na maioria das maneiras) pelo mundo externo ou então seu programa exibirá UB. Portanto, se a co-rotina for frágil dessa forma, ela pode copiar os dados do usuário e, assim, evitar que o usuário os modifique.

Em suma, o código necessário não é tão grande. Aqui está um exemplo executável de seu código com estas modificações:

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

Seja mais flexível

Uma alternativa ao uso de um explícito co_awaité explorar uma propriedade de co_yield. Ou seja, co_yieldé uma expressão e, portanto, tem um valor. Especificamente, é (principalmente) equivalente a co_await p.yield_value(e), onde pestá o objeto de promessa (ohh!) E eé o que estamos produzindo.

Felizmente, já temos uma yield_valuefunção; ele retorna std::suspend_always. Mas também pode retornar um objeto que sempre suspende, mas também que co_awaitpode descompactar em um 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};
}

Este é um mecanismo de transporte simétrico; para cada valor obtido, você recebe um valor (que pode ser o mesmo de antes). Ao contrário do co_awaitmétodo explícito , você não pode receber um valor antes de começar a gerá-lo. Isso pode ser útil para certas interfaces.

E, claro, você pode combiná-los como achar melhor.