通过 C++20 协程制作 python 生成器
Making python generator via c++20 coroutines
假设我有这个 python 代码:
def double_inputs():
while True:
x = yield
yield x * 2
gen = double_inputs()
next(gen)
print(gen.send(1))
正如预期的那样打印“2”。
我可以像这样在 c++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 中,不仅可以从生成器中产生一些东西,而且也可以从生成器中产生一些东西。我相信它可以通过 C++ 中的 co_await 来完成。
我需要它像这样工作:
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
类型需要是一个模板,以便用户可以指定它从协程到调用者的牧羊人类型。
所以如果你想要这个bi-directional接口,那么something你的hello
函数必须指定输出类型和输入类型。
最简单的方法是创建一个对象并将对该对象的非const
引用传递给生成器。每次它执行 co_yield
时,调用者都可以修改引用的对象,然后请求一个新值。协程可以从引用中读取并查看给定的数据。
但是,如果您坚持使用协程的未来类型作为输出和输入,那么您需要同时解决第一个问题(通过使您的 generator
模板采用 OutputType
和InputType
) 以及第二个问题。
看,您的目标是为协程获取一个值。问题在于该值的来源(调用协程的函数)有一个未来的对象。但是协程 无法访问 未来的对象。它也不能访问 future 引用的 promise 对象。
或者至少,它不能轻易做到。
有两种方法可以解决这个问题,适用于不同的用例。第一个操纵协程机制以通过后门方式进入 promise。第二个操纵 属性 of co_yield
来做基本相同的事情。
转换
协程的 promise 对象通常是隐藏的,协程无法访问。未来对象可以访问它,承诺创建该对象并充当承诺数据的接口。但它也可以在 co_await
机器的某些部分访问。
具体来说,当您对协程中的任何表达式执行 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
的缺点是,通过为我们的承诺指定此函数的一个重载,我们会影响任何使用此函数的协程中的 every co_await
类型。对于生成器协程,这不是很重要,因为没有太多理由 co_await
除非你正在做这样的黑客攻击。但是,如果您正在创建一个更通用的机制,可以明确地等待任意可等待对象作为其生成的一部分,那么您就会遇到问题。
好的,所以我们有这个 await_transform
函数;这个功能需要做什么?它需要 return 一个可等待的对象,因为 co_await
将等待它。但是这个可等待对象的目的是传递对输入类型的引用。幸运的是,co_await
用于将可等待对象转换为值的机制由可等待对象的 await_resume
方法提供。所以我们可以 return 一个 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{};
访问该值。请注意,此 return 是对该对象的引用。
可以很容易地修改 generator
类型以允许修改存储在承诺中的 InputType
对象。只需添加一对 send
函数来覆盖输入值:
void send(const InputType &input)
{
coro.promise().input_value = input;
}
void send(InputType &&input)
{
coro.promise().input_value = std::move(input);
}
这代表了一种非对称传输机制。协程在它自己选择的地点和时间检索一个值。因此,它没有真正的义务立即响应任何变化。这在某些方面是好的,因为它允许协程将自己与有害更改隔离开来。如果您在容器上使用 range-based for
循环,则外部世界不能(在大多数情况下)直接修改该容器,否则您的程序将显示 UB。所以如果协程那样脆弱,它可以从用户那里复制数据,从而防止用户修改它。
总而言之,所需的代码并不多。下面是经过这些修改的 run-able example of your code:
#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
功能;它 returns std::suspend_always
。但它也可以 return 一个总是挂起的对象,但是也 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
方法不同,您无法在开始生成它们之前 收到值 。这可能对某些界面有用。
当然,您可以根据需要组合它们。
假设我有这个 python 代码:
def double_inputs():
while True:
x = yield
yield x * 2
gen = double_inputs()
next(gen)
print(gen.send(1))
正如预期的那样打印“2”。 我可以像这样在 c++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 中,不仅可以从生成器中产生一些东西,而且也可以从生成器中产生一些东西。我相信它可以通过 C++ 中的 co_await 来完成。
我需要它像这样工作:
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
类型需要是一个模板,以便用户可以指定它从协程到调用者的牧羊人类型。
所以如果你想要这个bi-directional接口,那么something你的hello
函数必须指定输出类型和输入类型。
最简单的方法是创建一个对象并将对该对象的非const
引用传递给生成器。每次它执行 co_yield
时,调用者都可以修改引用的对象,然后请求一个新值。协程可以从引用中读取并查看给定的数据。
但是,如果您坚持使用协程的未来类型作为输出和输入,那么您需要同时解决第一个问题(通过使您的 generator
模板采用 OutputType
和InputType
) 以及第二个问题。
看,您的目标是为协程获取一个值。问题在于该值的来源(调用协程的函数)有一个未来的对象。但是协程 无法访问 未来的对象。它也不能访问 future 引用的 promise 对象。
或者至少,它不能轻易做到。
有两种方法可以解决这个问题,适用于不同的用例。第一个操纵协程机制以通过后门方式进入 promise。第二个操纵 属性 of co_yield
来做基本相同的事情。
转换
协程的 promise 对象通常是隐藏的,协程无法访问。未来对象可以访问它,承诺创建该对象并充当承诺数据的接口。但它也可以在 co_await
机器的某些部分访问。
具体来说,当您对协程中的任何表达式执行 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
的缺点是,通过为我们的承诺指定此函数的一个重载,我们会影响任何使用此函数的协程中的 every co_await
类型。对于生成器协程,这不是很重要,因为没有太多理由 co_await
除非你正在做这样的黑客攻击。但是,如果您正在创建一个更通用的机制,可以明确地等待任意可等待对象作为其生成的一部分,那么您就会遇到问题。
好的,所以我们有这个 await_transform
函数;这个功能需要做什么?它需要 return 一个可等待的对象,因为 co_await
将等待它。但是这个可等待对象的目的是传递对输入类型的引用。幸运的是,co_await
用于将可等待对象转换为值的机制由可等待对象的 await_resume
方法提供。所以我们可以 return 一个 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{};
访问该值。请注意,此 return 是对该对象的引用。
可以很容易地修改 generator
类型以允许修改存储在承诺中的 InputType
对象。只需添加一对 send
函数来覆盖输入值:
void send(const InputType &input)
{
coro.promise().input_value = input;
}
void send(InputType &&input)
{
coro.promise().input_value = std::move(input);
}
这代表了一种非对称传输机制。协程在它自己选择的地点和时间检索一个值。因此,它没有真正的义务立即响应任何变化。这在某些方面是好的,因为它允许协程将自己与有害更改隔离开来。如果您在容器上使用 range-based for
循环,则外部世界不能(在大多数情况下)直接修改该容器,否则您的程序将显示 UB。所以如果协程那样脆弱,它可以从用户那里复制数据,从而防止用户修改它。
总而言之,所需的代码并不多。下面是经过这些修改的 run-able example of your code:
#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
功能;它 returns std::suspend_always
。但它也可以 return 一个总是挂起的对象,但是也 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
方法不同,您无法在开始生成它们之前 收到值 。这可能对某些界面有用。
当然,您可以根据需要组合它们。