截止到上一篇文章我们全部介绍完了C++ coroutine相关的概念,这一篇文章作为整个系列的第3篇将实现一个通用的 Generator 类型方便以及展示如何通过 Generator 向coroutine内传递数据的方法。这一章里不会有新的关于Coroutine的概念,所以我会比较详细的解释类型设计的方法和容易出错的地方,不会细致讲解每一行代码。
Generic Generator
这一段代码开始我们将大量使用模版(毕竟是C++的精髓),如果大家对模板不是特别熟悉可以找一找相关的教程。接下来我们首先看一个简单的通用Generator模版类的实现:
#include
#include
#include
#include
template
class Generator {
public:
class promise_type {
public:
Generator get_return_object() { return Generator(std::coroutine_handle::from_promise(*this)); }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
void return_void() {}
template From> // C++20 concept
std::suspend_always yield_value(From&& value) {
value_ = std::forward(value);
return {};
}
T value_;
std::exception_ptr exception_;
};
Generator(const std::coroutine_handle& handle) : handle_(handle) {}
~Generator() { handle_.destroy(); }
explicit operator bool() {
Next();
return !handle_.done();
}
const T& operator()() {
Next();
consumed_ = true;
return handle_.promise().value_;
}
private:
void Next() {
if (consumed_) {
handle_();
if (handle_.promise().exception_) {
std::rethrow_exception(handle_.promise().exception_);
}
consumed_ = false;
}
}
bool consumed_ = true;
std::coroutine_handle handle_;
};
/*
The real logic implementation, looks great even we've wrote a lot of clunky code above...
*/
Generator counter(size_t num) {
for (size_t i = 0; i < num; ++i) {
co_yield i;
}
}
int main() {
auto gen = counter(3);
while (gen) {
std::cout << "main:" << gen() << std::endl;
}
return 0;
}
/*
Outputs:
main:0
main:1
main:2
*/
这段代码6 - 55行是 Generator 的实现,63 - 75行是使用方法,我们可以看到最终实际使用时的代码非常简单。这里要稍微讲解一下的是:
- Generator的 operator bool() 以及 operator()() 都会尝试继续coroutine的执行以便于确定是否还有新的值。
- Generator的 operator bool() 以及 operator()() 都有可能抛出异常,我个人感觉这样的设计是不好的。因为一般来说,一个“判断Generator是否有值”这样的操作不太应该发生错误,异常只应该在读取这个值的时候发生。不过这个实现其实主要抄袭的 cppreference 上给出的例子,所以我也没改。
这段代码很简单,没啥要解释的,有疑问的朋友可以在评论里留下问题。
接下来我们看本篇文章中主要想介绍的“升级强化版Generator”,主要是新实现两个功能:
- counter可以接收main发送给它的信息(类似python generator.send)
- main可以通过迭代器接口来读取generator的数据
接下来我们直接看代码,然后讲解关键点:
#include
#include
#include
#include
#include
template
class Generator {
public:
//
// Promise
//
class promise_type {
public:
Generator get_return_object() { return Generator(std::coroutine_handle::from_promise(*this)); }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
void return_void() {}
template From> // C++20 concept
auto yield_value(From&& value) {
return_value_ = std::forward(value);
struct send_awaiter {
std::coroutine_handle handle_;
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {}
const SendType& await_resume() const noexcept {
// Return the send value
return handle_.promise().send_value_;
}
};
return send_awaiter{std::coroutine_handle::from_promise(*this)};
}
SendType send_value_;
ReturnType return_value_;
std::exception_ptr exception_;
};
//
// Iterator
//
class sentinel {};
class iterator {
public:
using iterator_category = std::input_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = ReturnType;
using reference = ReturnType&;
using pointer = ReturnType*;
template
iterator(Generator& gen, T&& send_value) noexcept : gen_(gen), send_value_(std::forward(send_value)) {
operator++(); // Initial read
}
iterator(const iterator&) = default;
~iterator() = default;
friend bool operator==(const iterator& it, sentinel) noexcept {
// The iterator stopped when generator is done
return it.gen_.Done();
}
iterator& operator++() {
gen_.Next(send_value_);
return *this;
}
void operator++(int) { operator++(); }
reference operator*() const { return gen_.Get(); }
private:
Generator& gen_;
SendType send_value_;
};
//
// Generator
//
Generator(const std::coroutine_handle& handle) : handle_(handle) {}
~Generator() { handle_.destroy(); }
explicit operator bool() const noexcept { return !handle_.done() && !consumed_; }
ReturnType& Get() {
consumed_ = true;
if (exception_) {
std::rethrow_exception(exception_);
}
return *value_;
}
bool Next() { return Next(DEFAULT_SEND_VALUE); }
template
bool Next(T&& send_value) {
if (!handle_.done()) {
handle_.promise().send_value_ = std::forward(send_value);
handle_();
if (exception_ = handle_.promise().exception_; exception_) {
// Has exception, and should return true as if there's new value (To let the exception rethrow by Get)
consumed_ = false;
value_ = {};
return true;
} else if (handle_.done()) {
// The final suspend, no more to read
consumed_ = true;
value_ = {};
return false;
} else {
// Has new value
consumed_ = false;
value_ = std::move(handle_.promise().return_value_); // Move the value out of promise
return true;
}
} else {
// Done, no more to read
consumed_ = true;
value_ = {};
return false;
}
}
bool Done() const { return handle_.done(); }
iterator begin() { return begin(DEFAULT_SEND_VALUE); }
iterator begin(SendType&& send_value) {
// Create iterator by custom send value
return iterator(*this, std::forward(send_value));
}
sentinel end() noexcept { return {}; }
private:
std::coroutine_handle handle_;
bool consumed_ = true;
std::optional value_;
std::exception_ptr exception_;
};
template
Generator counter(size_t max) {
for (size_t i = 0; i < max;) {
auto step = co_yield i;
i += step;
}
}
int main() {
//
// Usage 1
//
std::cout << "[+] Usage1" << std::endl;
auto gen1 = counter(5);
while (gen1.Next(2)) {
std::cout << "main:" << gen1.Get() << std::endl;
}
//
// Usage 2
//
std::cout << "[+] Usage2" << std::endl;
auto gen2 = counter(9);
for (auto it = gen2.begin(3); it != gen2.end(); ++it) {
std::cout << "main:" << *it << std::endl;
}
//
// Usage 3
//
std::cout << "[+] Usage3" << std::endl;
auto gen3 = counter(3);
for (const auto value : gen3) {
std::cout << "main:" << value << std::endl;
}
return 0;
}
/*
Outputs:
[+] Usage1
main:0
main:2
main:4
[+] Usage2
main:0
main:3
main:6
[+] Usage3
main:0
main:1
main:2
*/
我们首先看 counter 函数,主要逻辑是不变的但是变量i的自增逻辑发生了变化,for循环每次不再默认i+1,而是通过yield返回的step来进行自增。在main函数中我们展示了3种用法:
- 使用while循环配合 Next / Get 方法获得 Generator 返回的值,通过Next方法的参数把step值传递给counter函数
- 使用for循环配合迭代器获得 Generator 返回的值,通过begin方法的参数设置迭代过程中默认传递的值
- 使用for range循环,采用默认参数的begin方法
上面的代码逻辑上很简单,我们首先看main函数是如何通过 Next 方法把值传递给 counter 方法的。首先回忆一下 co_yield 关键字:
// co_yield a;等价于
co_await promise.yield_value(a);
// 那么 auto r = co_yield a;等价于
auto r = co_await promise.yield_value(a);
我们看最后一行就非常容易理解 co_yield 关键字的返回值了,实际上就是 yield_value 所返回的awaiter对象的 await_resume 方法的返回值了。所以我们来看21 - 37行 yield_value 实现代码,其返回了一个匿名的awaiter类型,这个类型只干了两件事:
- 让coroutine暂停在 co_yield 的位置
- co_yield 返回 promise.send_value_ 的值
接下来我们看 send_value_ 是在哪里被写入的,我们来看107行 Next 函数的实现,在恢复counter执行之前修改 promise.send_value_。
接下来我们看迭代器的实现,这一部分就非常简单没有什么奇淫巧技(如果有对iterator不太熟悉的朋友,可以找一找这方面的教程),这里面有几个要强调的是:
- 在Next被调用之前, Generator 是没有任何有效返回值的( promise.initial_suspend 暂停住了),因此 iterator 的构造函数中需要执行一次迭代操作。
- iterator 的终止条件就是 Generator 已经执行完毕了。
这段代码也没什么要解释的了,有疑问的朋友可以在评论里留下问题。
本文到此就结束了,因为这一篇文章没有引入任何C++ coroutine的新概念因此解释的非常快,只对 Generator 实现的关键点做了一些讲解。大家有看不懂、不理解的地方可以在评论里留下问题,我会逐个回复。下一篇也就是本系列的最后一篇,我们将实现一个简单的“异步运行时框架”,欢迎大家继续关注~
喜欢就点个赞、收个藏、关个注吧!