使用 C++23 协程实现第一个 co_yield 同步风格调用接口--Qt计算排列组合

上一篇介绍了 co_await 的例子。与 co_await 类似,在C++23的协程特性里, co_yield 用于从协程执行过程中暂停,并返回值。这个功能乍一听起来很奇怪,网上的例子大多是用一个计数器来演示多次中断协程函数,返回顺序的计数值。这看起来毫无意义。

其实这个功能主要想演示的就是协程 co_yield 具备打断一个函数的执行,并多次返回值的能力。这种能力允许实现一种隐式状态机,每次使用时,返回下一个状态。这对于极为复杂的状态计算来说,是很有用的。它(协程)避免了显式的设置状态记忆句柄,大大简化了实现难度。同时,由于可以任意打断执行,便于在中间获取、展示一些数据状态、甚至单步调试,对构造一些教学程序意义重大。典型的是观察堆排序的中间态,不需要大幅度修改排序算法插入很多的printf,而是在函数外部做。

我们以产生任意P(N,M)、C(N,M)这样的排列、组合数序列为例子,看看传统算法和协程的区别。

1. 回溯法迭代排列组合

传统的回溯法,求取一个排列的算法如下:

void pnm_calc(const int n, const int m)
{std::vector<int> vec_buf,vec_bz;int swim = 0;bool finished = false;for (int i=0;i<m;++i)    vec_buf.push_back(0);for (int i=0;i<n;++i)    vec_bz.push_back(0);do{int ch = 0;if (swim<m){while (vec_bz[ch])++ch;vec_buf[swim] = ch;vec_bz[ch] = 1;++swim;}if (swim==m){//打印for (int i=0;i<m;++i)printf("%d,",vec_buf[i]);printf("\n");bool hit = false;do{if (swim<m && swim >=0) vec_bz[vec_buf[swim]] = 0;--swim;if (swim>=0){int nextv = vec_buf[swim];do{++nextv;if (nextv >=n)break;if (vec_bz[nextv]==0)hit = true;} while (hit == false);if (hit==true){vec_bz[vec_buf[swim]] = 0;vec_buf[swim] = nextv;vec_bz[nextv] = 1;++swim;}}elsefinished = true;} while (finished == false && hit == false);}}while(finished == false);
};
int main(int argc, char *argv[])
{pnm_calc(4,3);return 0;
}

输出:

0,1,2,
0,1,3,
0,2,1,
0,2,3,
...
3,1,2,
3,2,0,
3,2,1,

2 传统状态机封装

上述打印显示结果演示的是回溯法本身。若为了更好地使用组合数,需要对算法进行封装,以便于批量的获取、运用组合数的各组结果。比如考虑到总数可能很大,需要分批次返回结果等功能,显著增加了工作量。

#include <vector>
#include <cstdio>
struct tag_NM_State
{std::vector<unsigned short> vec_buf;std::vector<unsigned short> vec_bz;int swim;bool finished;
};
/*!\brief pnm 快速算法,使用带有记忆效应的 tag_NM_State 记录穷尽进度很好的避免了重新计算的耗时\fn pnm\param n				N,集合数\param m				M, 子集\param vec_output		存储结果的集合,本集合会自动增长\param state			状态存储\param limit			本次最多样本数\return int			本次给出的样本数*/
int pnm(int n, int m, std::vector<std::vector <unsigned short> > & vec_output,tag_NM_State * state, int limit/* = 0*/)
{std::vector<unsigned short> & vec_buf = state->vec_buf,& vec_bz = state->vec_bz;int &swim = state->swim;bool &finished = state->finished;const bool firstRun = vec_output.size()?false:true;if (vec_bz.size()==0){for (int i=0;i<m;++i)    vec_buf.push_back(0);for (int i=0;i<n;++i)    vec_bz.push_back(0);swim = 0;finished = false;}if (finished==true)return 0;int group = 0;do{int ch = 0;if (swim<m){while (vec_bz[ch])++ch;vec_buf[swim] = ch;vec_bz[ch] = 1;++swim;}if (swim==m){if (!firstRun)memcpy(vec_output[group].data(),vec_buf.data(),m*sizeof(unsigned short));elsevec_output.push_back(vec_buf);++group;bool hit = false;do{if (swim<m && swim >=0) vec_bz[vec_buf[swim]] = 0;--swim;if (swim>=0){int nextv = vec_buf[swim];do{++nextv;if (nextv >=n)break;if (vec_bz[nextv]==0)hit = true;} while (hit == false);if (hit==true){vec_bz[vec_buf[swim]] = 0;vec_buf[swim] = nextv;vec_bz[nextv] = 1;++swim;}}elsefinished = true;} while (finished == false && hit == false);if (group>=limit && limit>0)break;}}while(finished == false);return group;
}int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);using std::vector;tag_NM_State state;const int n = 4, m = 3, group = 10;vector<vector<unsigned short> > result;int ret = pnm(n,m,result,&state,group);while (ret>0){printf("\ngroup contains %d results:\n",ret);for (int i=0;i<ret;++i){printf("\n\t");for (int j=0;j<m;++j)printf("%d ",result[i][j]);}ret = pnm(n,m,result,&state,group);}printf("\nFinished\n");return 0;
}

分批输出:


group contains 10 results:0 1 20 1 30 2 10 2 30 3 10 3 21 0 21 0 31 2 01 2 3
group contains 10 results:1 3 01 3 22 0 12 0 32 1 02 1 32 3 02 3 13 0 13 0 2
group contains 4 results:3 1 03 1 23 2 03 2 1
Finished

详细算法参考 https://goldenhawking.blog.csdn.net/article/details/80037669

3. 协程封装

使用C++23 协程后,使用变得非常简洁:

int main(int argc, char *argv[])
{const int n = 4 , m = 3;nmCalc pnm = pnm_calc(n,m);while (pnm.next()){const int * res = pnm.currResult();printf("\n\t");for (int j=0;j<m;++j)printf("%d ",res[j]);}
}

每次调用 pnm.next() 就返回下一组结果且无需记忆状态。

但这也是有代价的!为了达到上述的效果,协程封装如下:

#ifndef NMCALC_H
#define NMCALC_H
#include<coroutine>
#include<vector>
class nmCalc
{
public:struct promise_type {//记录本次排列组合的结果const int * m_currResult;auto get_return_object() { return nmCalc{ handle::from_promise(*this) }; }auto initial_suspend() { return std::suspend_always{}; }auto final_suspend() noexcept { return std::suspend_always{}; }void unhandled_exception() { return ;}void return_void(){}auto yield_value(const int *  result ) {this->m_currResult=result; return std::suspend_always{}; }};using handle = std::coroutine_handle<promise_type>;
private:handle hCoroutine;nmCalc(handle handle) :hCoroutine(handle) {}
public:nmCalc(nmCalc&& other)noexcept :hCoroutine(other.hCoroutine) { other.hCoroutine = nullptr; }~nmCalc() { if (hCoroutine) hCoroutine.destroy(); }//请求下一组结果,调用后 co_yield继续。bool next() const { return hCoroutine && (hCoroutine.resume(), !hCoroutine.done()); }const int *  currResult() const { return hCoroutine.promise().m_currResult; }
};nmCalc pnm_calc(const int n, const int m)
{std::vector<int> vec_buf,vec_bz;int swim = 0;bool finished = false;for (int i=0;i<m;++i)    vec_buf.push_back(0);for (int i=0;i<n;++i)    vec_bz.push_back(0);do{int ch = 0;if (swim<m){while (vec_bz[ch])++ch;vec_buf[swim] = ch;vec_bz[ch] = 1;++swim;}if (swim==m){//返回一组结果!!!!!co_yield vec_buf.data();bool hit = false;do{if (swim<m && swim >=0) vec_bz[vec_buf[swim]] = 0;--swim;if (swim>=0){int nextv = vec_buf[swim];do{++nextv;if (nextv >=n)break;if (vec_bz[nextv]==0)hit = true;} while (hit == false);if (hit==true){vec_bz[vec_buf[swim]] = 0;vec_buf[swim] = nextv;vec_bz[nextv] = 1;++swim;}}elsefinished = true;} while (finished == false && hit == false);}}while(finished == false);
};

4. 体会与思考

这种封装方式,显著提高了算法流程的紧凑程度。无需考虑如何巧妙的保留状态,而是直接借助协程随时打断并返回。

这在算法极其复杂的情况下,尤其有效。同时,对于单步演示,比如按一下按钮出一次,也很方便,主要代码参考:

https://gitcode.net/coloreaglestdio/qtcpp_demo/-/tree/master/qt_coro_test

运行效果:

co_yield

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/707819.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】DCL

DCL英文全称是Data Control Language(数据控制语言)&#xff0c;用来管理数据库用户、控制数据库的访问权限。 1. 管理用户 在MySQL数据库中&#xff0c;DCL&#xff08;数据控制语言&#xff09;是用来管理用户和权限的语句集合。通过DCL语句&#xff0c;可以创建、修改、删…

机器遗忘同等重要,AI如何忘记不良数据,保护用户隐私?

引言&#xff1a;大语言模型中的机器遗忘问题 在人工智能领域&#xff0c;大语言模型&#xff08;LLMs&#xff09;因其在文本生成、摘要、问答等任务中展现出的卓越能力而备受关注。然而&#xff0c;这些模型在训练过程中可能会记住大量数据&#xff0c;包括敏感或不当的信息…

数据分析(二):学生成绩预测分析报告

目录 摘要 一、引言 二、 数据源介绍 三、 数据清洗和预处理 3.1 缺失值处理 3.2 异常值处理 3.3 数据编码 四、 探索性数据分析 4.1 可视化相关统计量 4.2 目标数据的分布情况 4.3 Pearson 相关性分析 五、 特征工程 5.1 特征构造 5.1.1 总饮酒量 5.1.2 整体关…

使用空闲电脑免费搭建一个私人的网盘

如果你也有一台空闲电脑&#xff0c;可以使用它来搭建一个私人的网盘。 这里使用的是飞梦云网盘&#xff1b; 服务端&#xff1a;下载 服务器文件使用hash校验进行储存&#xff0c;实现重复上传的文件秒传功能。 Fuse4Ui&#xff08;虚拟分区工具&#xff09;&#xff1a;下…

GaN建模:强大但富有挑战性

来源&#xff1a;Modeling GaN: Powerful but Challenging&#xff08;10年&#xff09; 文章的研究内容 这篇文章主要研究了氮化镓&#xff08;GaN&#xff09;高电子迁移率晶体管&#xff08;HEMTs&#xff09;的建模问题。GaN HEMTs是微波频段高功率发射器设计中的关键技术…

java面试题基础篇,kafka与rabbitmq面试题

1. Java 堆空间 **发生频率&#xff1a;**5颗星 造成原因 无法在 Java 堆中分配对象 吞吐量增加 应用程序无意中保存了对象引用&#xff0c;对象无法被 GC 回收 应用程序过度使用 finalizer。finalizer 对象不能被 GC 立刻回收。finalizer 由结束队列服务的守护线程调用&a…

BOOT电路

本质&#xff1a;BOOT电路本质上是单片机的引脚 作用&#xff1a;BOOT电路的作用是用于确定单片机的启动模式 使用方法&#xff1a;在单片机上电或者复位时给BOOT管脚设置为指定电平即可将单片机设置为指定启动模式。 原理&#xff1a;单片机上电或复位后会先启动内部晶振&a…

【C++进阶】哈希 + unordered系列容器

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

华为 OD 一面算法原题

2.2 亿彩票公布调查结果 昨天&#xff0c;闹得沸沸扬扬的《10 万中 2.2 亿》的彩票事件&#xff0c;迎来了官方公告。 简单来说&#xff0c;调查结果就是&#xff1a;一切正常&#xff0c;合规合法。 关于福利彩票事件&#xff0c;之前的推文我们已经分析过。 甚至在后面出现《…

鸿运(通天星CMSV6车载)主动安全监控云平台敏感信息泄露漏洞

文章目录 前言声明一、系统简介二、漏洞描述三、影响版本四、漏洞复现五、修复建议 前言 鸿运主动安全监控云平台实现对计算资源、存储资源、网络资源、云应用服务进行7*24小时全时区、多地域、全方位、立体式、智能化的IT运维监控&#xff0c;保障IT系统安全、稳定、可靠运行…

unity初学问题:如何修改图片的坐标

如图&#xff0c;我们想要修改图片的轴心点坐标&#xff08;Pivot&#xff09; 选择图片组 打开编辑器在里面修改即可&#xff08;最下面的Custom Pivot&#xff09;

golang使用gorm操作mysql1

1.mysql连接配置 package daoimport ("fmt""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger" )var DB *gorm.DB// 连接数据库&#xff0c;启动服务的时候&#xff0c;init方法就会执行 func init() {username : "roo…

浅谈 Linux 网络编程 - 网络字节序

文章目录 前言核心知识关于 小端法关于 大端法网络字节序的转换 函数 前言 在进行 socket 网络编程时&#xff0c;会用到字节流的转换函数、例如 inet_pton、htons 等&#xff0c;那么为什么要用到这些函数呢&#xff0c;本篇主要就是对这部分进行介绍。 核心知识 重点需要记…

数仓项目6.0(二)数仓

中间的几步意义就在于&#xff0c;缓存中间处理数据样式&#xff0c;避免重复计算浪费算力 分层 ODS&#xff08;Operate Data Store&#xff09; Spark计算过程中&#xff0c;存在shuffle的操作&#xff0c;而shuffle会将计算过程一分为二&#xff0c;前一阶段不执行完&…

链表之“带头双向循环链表”

目录 ​编辑 1.链表的分类 2.带头双向循环链表的实现 1.创建结构体 2.创建返回链表的头节点 3.双向链表销毁 4.双向链表打印 5.双向链表尾插 6.双向链表尾删 7.双向链表头插 8.双向链表头删 9.双向链表查找 10.双向链表在pos的前面进行插入 11.双向链表删除pos位…

ECLIP

denote the representation of the positive prompt produced by the momentum model as h ξ i h_{\xi}^{i} hξi​ 辅助信息 作者未提供代码

蓝桥杯前端Web赛道-课程列表

蓝桥杯前端Web赛道-课程列表 题目链接&#xff1a;0课程列表 - 蓝桥云课 (lanqiao.cn) 题目要求如下&#xff1a; 分析题目我们发现其实就是需要我们手写一个分页的功能&#xff0c;根据题目的要求&#xff0c;分析如下 需要通过axios获取数据每页显示5条数据&#xff0c;默…

11.vue学习笔记(组件生命周期+生命周期应用+动态组件+组件保持存活)

文章目录 1.组件生命周期2.生命周期应用2.1通过ref获取元素DOM结构2.2.模拟网络请求渲染数据 3.动态组件3.1.A&#xff0c;B两个组件 4.组件保持存活&#xff08;销毁期&#xff09; 1.组件生命周期 每个Vue组件实例在创建时都需要经历一系列的初始化步骤&#xff0c;比如设置…

Rocky Linux安装部署Elasticsearch(ELK日志服务器)

一、Elasticsearch的简介 Elasticsearch是一个强大的开源搜索和分析引擎&#xff0c;可用于实时处理和查询大量数据。它具有高性能、可扩展性和分布式特性&#xff0c;支持全文搜索、聚合分析、地理空间搜索等功能&#xff0c;是构建实时应用和大规模数据分析平台的首选工具。 …

Linux学习之system V

目录 一&#xff0c;system V共享内存 快速认识接口 shmget(shared memory get) shmat(shared memory attach) shmdt(shared memory delete) shmctl (shared memory control) 编写代码 综上那么共享内存与管道通信有什么区别&#xff1f; system v消息队列 system v信号…