Creare un generatore di python tramite coroutine c ++ 20
Diciamo che ho questo codice Python:
def double_inputs():
while True:
x = yield
yield x * 2
gen = double_inputs()
next(gen)
print(gen.send(1))
Stampa "2", proprio come previsto. Posso creare un generatore in c ++ 20 in questo modo:
#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() << ' ';
}
}
Questo generatore produce solo una stringa lettera per lettera, ma la stringa è codificata in essa. In python, è possibile non solo produrre qualcosa DAL generatore, ma anche fornire qualcosa a esso. Credo che potrebbe essere fatto tramite co_await in C ++.
Ho bisogno che funzioni in questo modo:
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
}
}
Come posso ottenerlo? Come rendere questo "produttore" usando le coroutine c ++ 20?
Risposte
Hai essenzialmente due problemi da superare se vuoi farlo.
Il primo è che C ++ è un linguaggio tipizzato staticamente. Ciò significa che i tipi di tutto ciò che è coinvolto devono essere conosciuti in fase di compilazione. Questo è il motivo per cui il tuo generatortipo deve essere un modello, in modo che l'utente possa specificare quale tipo deve guidare dalla coroutine al chiamante.
Quindi, se vuoi avere questa interfaccia bidirezionale, allora qualcosa sulla tua hellofunzione deve specificare sia il tipo di output che il tipo di input.
Il modo più semplice per farlo è semplicemente creare un oggetto e passare un non constriferimento a quell'oggetto al generatore. Ogni volta che esegue un co_yield, il chiamante può modificare l'oggetto a cui si fa riferimento e quindi chiedere un nuovo valore. La coroutine può leggere dal riferimento e vedere i dati forniti.
Tuttavia, se insisti nell'usare il tipo futuro per la coroutine sia come output che come input, devi risolvere sia il primo problema (facendo in modo che il tuo generatormodello prenda OutputTypee InputType) sia questo secondo problema.
Vedi, il tuo obiettivo è dare un valore alla coroutine. Il problema è che l'origine di quel valore (la funzione che chiama la tua coroutine) ha un oggetto futuro. Ma la coroutine non può accedere all'oggetto futuro. Né può accedere all'oggetto della promessa a cui fa riferimento il futuro.
O almeno, non può farlo facilmente.
Ci sono due modi per farlo, con diversi casi d'uso. Il primo manipola i macchinari coroutine per backdoor una via nella promessa. Il secondo manipola una proprietà di co_yieldfare sostanzialmente la stessa cosa.
Trasformare
L'oggetto promesso per una coroutine è solitamente nascosto e inaccessibile dalla coroutine. È accessibile all'oggetto futuro, che la promessa crea e che funge da interfaccia per i dati promessi. Ma è accessibile anche durante alcune parti del co_awaitmacchinario.
In particolare, quando esegui un co_awaitsu qualsiasi espressione in una coroutine, la macchina guarda il tuo tipo di promessa per vedere se ha una funzione chiamata await_transform. Se è così, chiamerà quell'oggetto di promessa await_transformsu ogni espressione co_awaitsu cui stai (almeno, in una co_awaitche scrivi direttamente, non attese implicite, come quella creata da co_yield).
In quanto tale, dobbiamo fare due cose: creare un sovraccarico del await_transformtipo di promessa e creare un tipo il cui unico scopo è permetterci di chiamare quella await_transformfunzione.
Quindi sarebbe qualcosa del genere:
struct generator_input {};
...
//Within the promise type:
auto await_transform(generator_input);
Una breve nota. Lo svantaggio di usare await_transformcome questo è che, specificando anche un sovraccarico di questa funzione per la nostra promessa, noi impatto ogni co_await in ogni coroutine che utilizza questo tipo. Per un generatore coroutine, non è molto importante, dal momento che non c'è motivo di co_awaitfarlo a meno che tu non stia facendo un trucco come questo. Ma se stavi creando un meccanismo più generale che potrebbe attendere distintamente su attesa arbitrari come parte della sua generazione, avresti un problema.
OK, quindi abbiamo questa await_transformfunzione; cosa deve fare questa funzione? Deve restituire un oggetto attendibile, poiché co_awaitaspetterà su di esso. Ma lo scopo di questo oggetto di attesa è fornire un riferimento al tipo di input. Fortunatamente, il meccanismo co_awaitutilizzato per convertire l'atteso in un valore è fornito dal await_resumemetodo dell'atteso . Quindi il nostro può semplicemente restituire 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.
}
Ciò dà alla coroutine l'accesso al valore, invocando co_await generator_input{};. Notare che ciò restituisce un riferimento all'oggetto.
Il generatortipo può essere facilmente modificato per consentire la possibilità di modificare un InputTypeoggetto memorizzato nella promessa. Aggiungi semplicemente un paio di sendfunzioni per sovrascrivere il valore di input:
void send(const InputType &input)
{
coro.promise().input_value = input;
}
void send(InputType &&input)
{
coro.promise().input_value = std::move(input);
}
Questo rappresenta un meccanismo di trasporto asimmetrico. La coroutine recupera un valore in un luogo e in un momento di sua scelta. In quanto tale, non ha alcun obbligo reale di rispondere immediatamente a qualsiasi modifica. Questo è positivo per alcuni aspetti, poiché consente a una coroutine di isolarsi da cambiamenti deleteri. Se stai utilizzando un forciclo basato su intervallo su un contenitore, quel contenitore non può essere modificato direttamente (nella maggior parte dei modi) dal mondo esterno, altrimenti il tuo programma mostrerà UB. Quindi, se la coroutine è fragile in questo modo, può copiare i dati dall'utente e quindi impedire all'utente di modificarli.
Tutto sommato, il codice necessario non è così grande. Ecco un esempio eseguibile del tuo codice con queste modifiche:
#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() << ' ';
}
}
Sii più arrendevole
Un'alternativa all'uso di un esplicito co_awaitè sfruttare una proprietà di co_yield. Vale a dire, co_yieldè un'espressione e quindi ha un valore. Nello specifico, è (per lo più) equivalente a co_await p.yield_value(e)dov'è pl'oggetto promesso (ohh!) Ed eè ciò che stiamo cedendo.
Fortunatamente, abbiamo già una yield_valuefunzione; ritorna std::suspend_always. Ma potrebbe anche restituire un oggetto che si sospende sempre, ma anche che co_awaitpuò scompattare in 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};
}
Questo è un meccanismo di trasporto simmetrico; per ogni valore che produci, ricevi un valore (che può essere lo stesso di prima). A differenza del co_awaitmetodo esplicito , non puoi ricevere un valore prima di iniziare a generarli. Questo potrebbe essere utile per alcune interfacce.
E, naturalmente, puoi combinarli come meglio credi.