如果你想要实现这个,你需要解决两个问题。
第一个问题是C++是一种静态类型语言,这意味着在编译时需要知道所有涉及内容的类型。这就是为什么你的generator
类型需要成为一个模板,以便用户可以指定它从协程到调用者牧羊的类型。
所以如果你想要拥有这样一个双向接口,那么hello
函数中的某些东西必须同时指定输出类型和输入类型。
最简单的方法就是创建一个对象,并将对该对象的非const
引用传递给生成器。每次它执行co_yield
,调用者就可以修改引用对象,然后请求一个新值。协程可以从引用中读取并查看给定的数据。
但是,如果你坚持使用future类型作为协程的输出和输入,则需要解决第一个问题(通过使你的generator
模板获取OutputType
和InputType
)以及第二个问题。
你的目标是将一个值传递给协程。问题是该值的来源(调用你的协程的函数)具有future对象。但是协程无法访问future对象。也无法访问future引用的promise对象。
或者至少不容易做到。
有两种方法可以解决这个问题,具有不同的用例。第一种是操纵协程机制,以便在promise中建立一个后门。第二种是操纵co_yield
的属性,以基本实现相同的功能。
转换
协程的promise对象通常是隐藏的,并且无法从协程中访问。它可以通过被promise创建并充当承诺数据接口的future对象访问。但是,在某些co_await
机制的部分时期,也可以访问它。
特别地,当你在协程中对任何表达式执行co_await
时,机器会查看你的promise类型是否有一个名为await_transform
的函数。如果有,它将调用该promise对象的await_transform
函数,每次你在其中进行co_await
的表达式(至少在你直接编写的co_await
中,而不是由co_yield
创建的隐式等待)。
因此,我们需要做两件事:在promise类型上创建一个await_transform
重载,并创建一个唯一目的是让我们调用那个await_transform
函数的类型。
所以代码看起来会像这样:
struct generator_input {};
...
auto await_transform(generator_input);
需要注意的是,使用 await_transform
的缺点是,如果我们为我们的 promise 指定了一个重载函数,那么它将影响到使用此类型的任何协程中的每个 co_await
。对于生成器协程来说,这并不是很重要,因为除非像这样进行黑客攻击,否则没有太多理由使用 co_await
。但是,如果您正在创建一种更通用的机制,该机制可以作为其生成的一部分明确地等待任意可等待对象,那么您就会遇到问题。
好的,现在我们有了这个 await_transform
函数;这个函数需要做什么呢?它需要返回一个可等待对象,因为 co_await
将等待它。但是,这个可等待对象的目的是传递输入类型的引用。幸运的是,co_await
用于将可等待对象转换为值的机制是由可等待对象的 await_resume
方法提供的。因此,我们的方法只需返回一个 InputType&
:
struct passthru_value
{
InputType &ret_;
bool await_ready() {return true;}
void await_suspend(coro_handle) {}
InputType &await_resume() { return ret_; }
};
auto await_transform(generator_input)
{
return passthru_value{input_value};
}
通过调用 co_await generator_input{};
, coroutine 可以访问该值。请注意,这将返回对该对象的引用。
generator
类型可以轻松修改,以允许修改存储在 promise 中的 InputType
对象的能力。只需添加一对send
函数来覆盖输入值:
void send(const InputType &input)
{
coro.promise().input_value = input;
}
void send(InputType &&input)
{
coro.promise().input_value = std::move(input);
}
这代表了一种不对称的传输机制。协程在自己选择的时间和地点检索值。因此,它没有实质性的义务立即响应任何更改。在某些方面,这是很好的,因为它允许协程与有害变化隔离开来。如果您正在使用基于范围的for
循环遍历容器,则该容器不能直接被外部世界(大多数情况下)修改,否则您的程序将表现出未定义行为。所以,如果协程在这方面脆弱,它可以复制用户的数据,从而防止用户修改它。
总体来说,所需代码并不是那么大。这里有一个可运行的代码示例,包含这些修改:
#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_await p.yield_value(e)
(其中p
是promise对象,e
是我们要yield的内容)在大多数情况下是等价的。
幸运的是,我们已经拥有了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_; }
};
...
auto yield_value(OutputType value) {
current_value = value;
return yield_thru{input_value};
}
这是一种对称的传输机制;对于你产生的每一个值,你都会收到一个值(可能与之前相同)。与显式的 co_await
方法不同,你不能在开始生成值之前就接收到一个值。这对某些接口可能很有用。
当然,你可以自由地将它们组合使用。
hello
不是更合理吗?我的意思是,你可能可以通过使用co_await
来完成它,但是为什么要使用这样的机制,而不是最明显的方法(将其传递给生产者)呢?C++引入协程并不是为了将其变成Python。 - Nicol Bolas