首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
264 阅读
2
nvim番外之将配置的插件管理器更新为lazy
141 阅读
3
从零开始配置 vim(15)——状态栏配置
138 阅读
4
2018总结与2019规划
137 阅读
5
PDF标准详解(五)——图形状态
112 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
emacs
VimScript
linux
elisp
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
382
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
1
篇与
的结果
2026-06-04
使用C++20 的协程创建通用的生成器
在上一篇中,我们通过拆解一个简单的例子描述了C++20 处理协程的过程。在此先简单的回顾一下各个部分的作用回顾promise_type主要提供一些与协程相关的接口:initial_suspend: 协程实例创建时执行,通过返回值来决定协程创建的同时执行还是等待final_suspend: 协程实例执行完之后是暂停还是继续,继续意味着会销毁这个实例yield_value/await_transform: 协程函数中执行 co_yield/co_await 时负责将后面的操作数转化为等待体awaiter 对象get_return_object: 返回协程对象return_void / return_value: 真正获取协程函数 co_return 的返回值unhandled_exception 协程函数发生异常时调用等待体是协程函数中调用 co_await/co_yield 时创建, 主要用来告诉编译器遇到这两个操作符时应该继续执行还是等待, 关于等待体 awaiter 它有如下几个接口:bool await_ready() : 根据返回值决定是继续还是等待await_suspend(coroutine_handle<>) : 协程被挂起时调用await_resume() : 协程被恢复时调用关于等待体,标准库有两个简单的实现: suspend_always 和 suspend_never改进生成器c++ 23 中存在一个标准的生成器 std::generator,我们利用这个生成器可以将整个程序修改为:#include <coroutine> #include <iostream> #include <generator> #include <ranges> std::generator<int> fibonacci() { int a = 0; int b = 1; for (;;) { co_yield a; int next = a + b; a = b; b = next; } } int main() { for (auto x : fibonacci() | std::views::take(10)) { std::cout << x << std::endl; } return 0; }本文的目标是最终完成一个简单的、可用的、贴近标准库的 std::generator,进一步理解协程的原理 我们还是按照上一篇的组织方式,先放出完整的源代码,然后依次说明重要的点:#include <coroutine> #include <iostream> #include <generator> #include <ranges> template<typename T> struct Generator { struct promise_type { std::suspend_always initial_suspend() { return std::suspend_always{}; } std::suspend_always final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() noexcept { _exp = std::current_exception(); } Generator get_return_object() { return Generator(std::coroutine_handle<promise_type>::from_promise(*this)); } void return_void() {} std::suspend_always yield_value(T value) { _value = std::move(value); return std::suspend_always{}; } void rethrow_if_exception() { if (_exp) throw _exp; } T _value = {}; std::exception_ptr _exp = nullptr; }; Generator(std::coroutine_handle<promise_type> h) : _handle(h) {} ~Generator() { if (_handle) _handle.destroy(); } Generator(const Generator&) = delete; Generator& operator=(const Generator&) = delete; Generator(Generator&& other) noexcept : _handle(std::exchange(other._handle, nullptr)) { } Generator& operator=(Generator&& other) { if (this != &other) { if (_handle) _handle.destroy(); //删除当前的协程,管理新协程 _handle = std::exchange(other._handle, nullptr); } return *this; } bool is_done() noexcept { return !_handle || _handle.done(); } T next() { if (is_done()) throw std::runtime_error("Generator exhausted"); _handle.resume(); _handle.promise().rethrow_if_exception(); return _handle.promise()._value; } std::coroutine_handle<promise_type> _handle; // 迭代器 struct Iterator { Generator& _gen; bool _is_end; Iterator(Generator& gen, bool is_end = false) : _gen(gen), _is_end(is_end) {} void move_next() { if (!_gen.is_done()) { _gen._handle.resume(); _gen._handle.promise().rethrow_if_exception(); } else { _is_end = true; } } T operator*() const { return _gen._handle.promise()._value; } Iterator& operator++() { move_next(); return *this; } bool operator!=(const Iterator& other) const { return other._is_end != _is_end; } }; Iterator begin() { Iterator it{ *this }; it.move_next(); return it; } Iterator end() { return Iterator{ *this, true }; } }; Generator<int> fibonacci() { int a = 0; int b = 1; for (;;) { co_yield a; int next = a + b; a = b; b = next; } } int main() { Generator<int> generator = fibonacci(); for (auto x : fibonacci()) { std::cout << x << std::endl; } // std::generator 示例 //for (auto x : fibonacci() | std::views::take(10)) //{ // std::cout << x << std::endl; //} return 0; }泛型与移动语义的优化之前我们在 yield_value 函数中传入的参数是int类型,所以在函数中直接采用赋值运算符没有任何问题,但是考虑到泛化之后它可以生成任意类型的数据,在处理大结构的数据时,采用赋值运算将会产生多余的拷贝。因为函数参数中已经进行了拷贝,同时值传递不影响外部真实的数据,我们直接对参数执行移动操作可以节省一次拷贝。异常处理的支持上一篇,我们简单的将将 unhandled_exception 设置为空函数,这次我们在 promise_type 中保存了一个 std::exception_ptr 异常类型的指针用来捕获协程函数中的异常。这个函数的实现也比较简单,我们说在协程函数发生异常时会调用 promise_type 结构中的 unhandled_exception 接口函数。这个函数中通过 _exp = std::current_exception(); 获取最新的异常信息并保存。在获取下一次的数据的时候,我们直接判断当前是否保存了异常信息,如果有则直接抛出。外界在调用next等函数获取值的时候可以直接捕获Generator 禁止拷贝、仅支持移动操作协程句柄 coroutine_handle 独占一段堆上分配的协程栈与 promise 对象,资源唯一不可共享。所以理论上只能有一个对象管理整个协程。如果允许拷贝,则会出现多个结构同时指向一个协程对象。这些结构在析构时多次调用destroy 造成重复销毁的问题。或者某个对象没有及时更新造成访问无效内存。而移动语义是所有权的转移,在同一时间有且只有一个对象真正的控制协程、拥有 coroutine_handle 句柄。配合最后的析构释放操作,能防止内存泄漏move_next 实现这里我们在迭代器中单独又写了一些与 Generator::next 相同的代码。这里主要出于两点考虑:Generator 支持通过next 来获取下一个数据,但是该函数返回T类型的下一个数据。外界实际上没有途经判断生成器是否已经无法生成多余的数据。所以我通过抛出异常,外界可以通过异常来判断是否还能生成数据,当然这里也可以使用 std::optional 来判断数据是否合理。既然会抛出异常,那么迭代器中的 move_next 就无法直接调用,谁也不希望范围for最后是靠异常退出的。迭代器需要一个end函数,end一般是返回一个越界的迭代器,在协程中我们实际上不需要构建一个额外的Generator 来判断是否越界。所以我在里面额外维护了一个 _is_end。利用该变量来判断迭代器是否越界了。基于以上理由,我没有复用 Generator::next 的代码另外在 Generator::begin 函数中,我们首先调用了一次 move_next ,然后才返回迭代器。这是因为我们的 initial_suspend 函数返回 suspend_always,在生成器对象被创建的时候协程还没有开始运行,此时生成器中的值是无效值。begin函数是用来获取它的起始值,此时值应该是有效的,所以提前调用一次 move_next 让它获取第一个值。小结当然标准库的 std::generator 实现比上述代码要复杂的多,特别是它支持 ranges 组件来设置范围,而我们的简单的生成器的退出完全依赖协程函数自身的退出条件,无法做到自主退出。希望通过本节各位读者能对协程的原理有一个更深入的理解。也希望这个简陋的实现可以做到抛砖引玉的作用。
2026年06月04日
2 阅读
0 评论
0 点赞