Создание генератора Python через сопрограммы c ++ 20
Скажем, у меня есть этот код на Python:
def double_inputs():
while True:
x = yield
yield x * 2
gen = double_inputs()
next(gen)
print(gen.send(1))
Он печатает «2», как и ожидалось. Я могу сделать такой генератор на С ++ 20:
#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() << ' ';
}
}
Этот генератор просто создает строку по буквам, но строка в ней жестко запрограммирована. В python можно не только отдать что-то ОТ генератора, но и что-то передать ему. Я считаю, что это можно сделать с помощью co_await на C ++.
Мне нужно, чтобы он работал так:
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
}
}
Как я могу этого добиться? Как сделать этого «производителя» с помощью сопрограмм c ++ 20?
Ответы
Если вы хотите это сделать, вам необходимо решить две проблемы.
Во-первых, C ++ - это статически типизированный язык. Это означает, что типы всего задействованного должны быть известны во время компиляции. Вот почему ваш generatorтип должен быть шаблоном, чтобы пользователь мог указать, какой тип он передает от сопрограммы к вызывающей стороне.
Итак, если вы хотите иметь этот двунаправленный интерфейс, то что-то в вашей helloфункции должно указывать как тип вывода, так и тип ввода.
Самый простой способ сделать это - просто создать объект и передать constгенератору не ссылку на этот объект. Каждый раз, когда он выполняет a co_yield, вызывающий может изменить объект, на который указывает ссылка, а затем запросить новое значение. Сопрограмма может читать по ссылке и видеть заданные данные.
Однако, если вы настаиваете на использовании будущего типа для сопрограммы как в качестве вывода, так и в качестве ввода, вам необходимо решить как первую проблему (заставив ваш generatorшаблон принимать OutputTypeи InputType), так и вторую проблему.
Видите ли, ваша цель - получить ценность для сопрограммы. Проблема в том, что у источника этого значения (функции, вызывающей вашу сопрограмму) есть будущий объект. Но сопрограмма не может получить доступ к будущему объекту. Он также не может получить доступ к объекту обещания, на который ссылается будущее.
По крайней мере, это не может быть так просто.
Есть два способа сделать это с разными вариантами использования. Первый манипулирует механизмом сопрограмм, чтобы проникнуть в обещание. Второй манипулирует свойством co_yieldделать в основном то же самое.
Преобразовать
Объект обещания для сопрограммы обычно скрыт и недоступен для сопрограммы. Он доступен для будущего объекта, который создается обещанием и который действует как интерфейс для обещанных данных. Но он также доступен во время некоторых частей co_awaitоборудования.
В частности, когда вы выполняете a co_awaitдля любого выражения в сопрограмме, механизм смотрит на ваш тип обещания, чтобы увидеть, есть ли у него вызванная функция await_transform. Если да, то он будет называть это обещание объекта , await_transformна каждом выражении , то вы co_awaitна (по крайней мере, в co_awaitтом , что вы непосредственно писать, не неявные Ждет, как, например, созданный путем co_yield).
Таким образом, нам нужно сделать две вещи: создать перегрузку для await_transformтипа обещания и создать тип, единственная цель которого - позволить нам вызывать эту await_transformфункцию.
Это будет выглядеть примерно так:
struct generator_input {};
...
//Within the promise type:
auto await_transform(generator_input);
Одно небольшое примечание. Обратной стороной такого использования await_transformявляется то, что, указав хотя бы одну перегрузку этой функции для нашего обещания, мы воздействуем на каждую co_await сопрограмму, которая использует этот тип. Для сопрограммы-генератора это не очень важно, поскольку нет особых причин для этого, co_awaitесли только вы не выполняете подобный взлом. Но если бы вы создавали более общий механизм, который мог бы явно ожидать произвольных ожидаемых объектов как часть его генерации, у вас возникла бы проблема.
Хорошо, у нас есть эта await_transformфункция; что эта функция должна делать? Ему нужно вернуть ожидаемый объект, поскольку co_awaitон будет ожидать его. Но цель этого ожидаемого объекта - предоставить ссылку на тип ввода. К счастью, механизм, co_awaitиспользуемый для преобразования ожидаемого в значение, предоставляется методом awaitable await_resume. Так что наш может просто вернуть 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.
}
Это дает сопрограмме доступ к значению путем вызова co_await generator_input{};. Обратите внимание, что это возвращает ссылку на объект.
generatorТипа может быть легко изменена , чтобы возможность модифицировать InputTypeобъект , хранящийся в обещании. Просто добавьте пару sendфункций для перезаписи входного значения:
void send(const InputType &input)
{
coro.promise().input_value = input;
}
void send(InputType &&input)
{
coro.promise().input_value = std::move(input);
}
Это представляет собой асимметричный транспортный механизм. Сопрограмма извлекает значение в месте и в любое время по своему выбору. Таким образом, он не обязан немедленно реагировать на любые изменения. В некоторых отношениях это хорошо, так как позволяет сопрограмме изолировать себя от вредоносных изменений. Если вы используете forцикл на основе диапазона над контейнером, этот контейнер не может быть напрямую изменен (в большинстве случаев) внешним миром, иначе ваша программа будет отображать UB. Таким образом, если сопрограмма в этом отношении хрупкая, она может копировать данные пользователя и, таким образом, не позволять пользователю изменять их.
В общем, необходимый код не такой уж и большой. Вот работоспособный пример вашего кода с этими изменениями:
#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() << ' ';
}
}
Будьте более уступчивыми
Альтернативой явному использованию co_awaitявляется использование свойства co_yield. А именно, co_yieldявляется выражением и, следовательно, имеет значение. В частности, он (в основном) эквивалентен co_await p.yield_value(e), где pнаходится объект обещания (ооо!), И eэто то, что мы получаем.
К счастью, у нас уже есть yield_valueфункция; он возвращается std::suspend_always. Но это также может возвращать объект , который всегда приостанавливаемый, но и который co_awaitможет распаковать в 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};
}
Это симметричный транспортный механизм; за каждое полученное значение вы получаете значение (которое может быть таким же, как и раньше). В отличие от явного co_awaitметода, вы не можете получить значение до того, как начнете их генерировать. Это может быть полезно для определенных интерфейсов.
И, конечно же, вы можете комбинировать их по своему усмотрению.