news 2026/7/6 4:13:01

使用C++20 的协程创建通用的生成器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用C++20 的协程创建通用的生成器

在上一篇中,我们通过拆解一个简单的例子描述了C++20 处理协程的过程。在此先简单的回顾一下各个部分的作用

回顾

  1. 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 协程函数发生异常时调用
  1. 等待体是协程函数中调用 co_await/co_yield 时创建, 主要用来告诉编译器遇到这两个操作符时应该继续执行还是等待, 关于等待体 awaiter 它有如下几个接口:
  • bool await_ready(): 根据返回值决定是继续还是等待
  • await_suspend(coroutine_handle<>): 协程被挂起时调用
  • await_resume(): 协程被恢复时调用

关于等待体,标准库有两个简单的实现:suspend_alwayssuspend_never

改进生成器

c++ 23 中存在一个标准的生成器std::generator,我们利用这个生成器可以将整个程序修改为:

#include<coroutine>#include<iostream>#include<generator>#include<ranges>std::generator<int>fibonacci(){inta=0;intb=1;for(;;){co_yielda;intnext=a+b;a=b;b=next;}}intmain(){for(autox:fibonacci()|std::views::take(10)){std::cout<<x<<std::endl;}return0;}

本文的目标是最终完成一个简单的、可用的、贴近标准库的 std::generator,进一步理解协程的原理

我们还是按照上一篇的组织方式,先放出完整的源代码,然后依次说明重要的点:

#include<coroutine>#include<iostream>#include<generator>#include<ranges>template<typenameT>structGenerator{structpromise_type{std::suspend_alwaysinitial_suspend(){returnstd::suspend_always{};}std::suspend_alwaysfinal_suspend()noexcept{returnstd::suspend_always{};}voidunhandled_exception()noexcept{_exp=std::current_exception();}Generatorget_return_object(){returnGenerator(std::coroutine_handle<promise_type>::from_promise(*this));}voidreturn_void(){}std::suspend_alwaysyield_value(T value){_value=std::move(value);returnstd::suspend_always{};}voidrethrow_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(constGenerator&)=delete;Generator&operator=(constGenerator&)=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;}boolis_done()noexcept{return!_handle||_handle.done();}Tnext(){if(is_done())throwstd::runtime_error("Generator exhausted");_handle.resume();_handle.promise().rethrow_if_exception();return_handle.promise()._value;}std::coroutine_handle<promise_type>_handle;// 迭代器structIterator{Generator&_gen;bool_is_end;Iterator(Generator&gen,boolis_end=false):_gen(gen),_is_end(is_end){}voidmove_next(){if(!_gen.is_done()){_gen._handle.resume();_gen._handle.promise().rethrow_if_exception();}else{_is_end=true;}}Toperator*()const{return_gen._handle.promise()._value;}Iterator&operator++(){move_next();return*this;}booloperator!=(constIterator&other)const{returnother._is_end!=_is_end;}};Iteratorbegin(){Iterator it{*this};it.move_next();returnit;}Iteratorend(){returnIterator{*this,true};}};Generator<int>fibonacci(){inta=0;intb=1;for(;;){co_yielda;intnext=a+b;a=b;b=next;}}intmain(){Generator<int>generator=fibonacci();for(autox:fibonacci()){std::cout<<x<<std::endl;}// std::generator 示例//for (auto x : fibonacci() | std::views::take(10))//{// std::cout << x << std::endl;//}return0;}

泛型与移动语义的优化

之前我们在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相同的代码。这里主要出于两点考虑:

  1. Generator 支持通过next来获取下一个数据,但是该函数返回T类型的下一个数据。外界实际上没有途经判断生成器是否已经无法生成多余的数据。所以我通过抛出异常,外界可以通过异常来判断是否还能生成数据,当然这里也可以使用std::optional来判断数据是否合理。既然会抛出异常,那么迭代器中的move_next就无法直接调用,谁也不希望范围for最后是靠异常退出的。

  2. 迭代器需要一个end函数,end一般是返回一个越界的迭代器,在协程中我们实际上不需要构建一个额外的Generator 来判断是否越界。所以我在里面额外维护了一个_is_end。利用该变量来判断迭代器是否越界了。

基于以上理由,我没有复用Generator::next的代码

另外在Generator::begin函数中,我们首先调用了一次move_next,然后才返回迭代器。这是因为我们的initial_suspend函数返回suspend_always,在生成器对象被创建的时候协程还没有开始运行,此时生成器中的值是无效值。begin函数是用来获取它的起始值,此时值应该是有效的,所以提前调用一次move_next让它获取第一个值。

小结

当然标准库的std::generator实现比上述代码要复杂的多,特别是它支持ranges组件来设置范围,而我们的简单的生成器的退出完全依赖协程函数自身的退出条件,无法做到自主退出。

希望通过本节各位读者能对协程的原理有一个更深入的理解。也希望这个简陋的实现可以做到抛砖引玉的作用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 4:12:06

从事编程工作这么多年,经常会有人问我什么样的程序是好程序

有的甚至把程序给我看&#xff0c;让我给程序提提意见。而我从编程开始就踏上寻觅好程序之路。“路漫漫其修远兮 吾将上下而求索。”这好象正是我心历的写照。可以说追求编写好程序是我不变的目标。好的程序&#xff1f;好的程序&#xff1f;什么样程序是好的&#xff1f;为什么…

作者头像 李华
网站建设 2026/7/6 4:09:32

2026年5月28日更新:GPT-5.5 Instant 更新与旧模型退场

&#x1f525;个人主页&#xff1a;杨利杰YJlio❄️个人专栏&#xff1a;《Windows 疑难杂症与工单复盘案例库》 《Sysinternals实战教程》《WINDOWS教程》 《Windows PowerShell 实战》 《人工智能实战合集》《超简单&#xff1a;用Python让Excel飞起来》&#x1f31f; …

作者头像 李华
网站建设 2026/7/6 4:07:17

101与金根回顾敏捷个人:(17)技术研究之道

作为技术人员&#xff0c;经常遇到没有接触过的技术&#xff0c;有时是点滴的小技能&#xff0c;有时可能是大的一个研究课题。在《个人管理 &#xff0d; 从小工到专家》中有一个复用级别的图&#xff0c;其中每一级别都是需要研究的&#xff0c;复用粒度越大的需要投入研究的…

作者头像 李华
网站建设 2026/7/6 4:05:35

Nginx进行配置文件拆分(以windows解压版为例)

Nginx压缩包解压后&#xff0c;只有一个默认的nginx.conf配置文件&#xff0c;可以通过下面的配置方法简易拆分成多个配置&#xff0c;提高可读性和可维护性。 原理简介 了解过 Nginx 的应该知道&#xff0c;配置文件的http块里面是由一个个server块组成的。 每个 server 块都含…

作者头像 李华
网站建设 2026/7/6 4:05:26

网上的若干算法都太复杂了,现提出包氏算法如下:

先for循环把arr1中的元素入栈&#xff0c;并在每次遍历时&#xff0c;检索arr2中可以pop的元素。如果循环结束&#xff0c;而stack中还有元素&#xff0c;就说明arr2序列不是pop序列。static bool JudgeSequenceIsPossible(int[] arr1, int[] arr2){Stack stack new Stack();f…

作者头像 李华