介绍

这是很久之前的一个项目了,最近刚好有些时间,就来总结一下吧!

推荐初步熟悉项目后阅读本文: https://gitee.com/smalldyy/easy-msg-cpp

从何而来

这要从我从事Qt开发的那些日子说起了,项目说大不大,说小也不小,人倒是一茬又一茬,需求也换了又换,后来的事情大家都懂了,项目变成了一坨浓Shit,且不说其中的设计、构架、以及需求问题,单说说我对这个项目的直观感受,在我看来,整个程序仿佛一颗大树,从某点作为根然后一直向上延伸,在没有足够时间重构的情况下,它的层级越来越深,这时候问题来了,如果想让树木的两个不同分支的叶子节点发生关系,事情就马上会变得十分痛苦!

这两个想要联系的对象根本不再一个地方,我可能要将其中一个对象的指针在这颗大树的节点上倒退3层然后再前进2层才能让他们见面,然后暗戳戳的写下一个connect。

这时候我就想,如果有一个专门的通信组件负责传递各种消息,让两个对象中间产生一个媒介作为他们通信的桥梁,获取这件事情就会变得更加轻松了,我不用再费尽心思的将两个对象引用到同一个作用域,甚至还要考虑哪个作用域更加合理。

诚然,如果在前期就对项目的各个组件进行全盘规划,我想这种困境可能不会或者很少出现,但是并非所有事情都会按照美好的方向前进,就如曾经堆在我面前的那坨浓Shit,尽管我也为它的存在出过不少力…………

设计目标

  • 提供C++对象进程内通信功能 可进行消息传递;
  • 将已经存在的结构体定义为消息时,不能破坏已经存在的结构体本身的结构;
  • 处理消息的类无需继承任何基类;
  • 足够简单的订阅方法;
  • RAII形式的取消订阅,但也支持手动取消订阅。

你可能注意到了,我特意强调了不破坏原有结构。目的很简单,就是为了保证项目不会因为引入这个组件而发生太大的变化。众所周知,大部分程序员都是懒癌晚期,如果引入一个组件会导致工作量激增,程序员就会开始衡量shit的臭味和工作量之间的关系了。

总之,核心特征只有两个:易用,改动小。

原理分析

我首先将这个组件设计为一个基于订阅分发方式的通信组件,它有三个主要角色,订阅者,发布者,和消息。

首先考虑最简单的发布者,发布者的功能非常直观——发送消息,也就是说用户只要在需要的位置调用一个sendMsg之类的函数即可,这个函数的功能就是将用户给定的消息发送出去。

然后便是订阅者,我们要求订阅的宿主类型不可以继承任何基类,这个要求决定了我们订阅的方式,我们需要提供一个函数,它接受一个对象的指针(我称之为素质)和它的成员函数,将两者包装成一个std::function,将这个包装好的回调函数与一个定义好的消息关联并记录下来,这就形成了订阅关系。

当发布者发送消息时,我们的组件需要查询订阅关系,找到消息对应的回调函数,将消息作为参数调用它!此时,对象间就完成了一次通信。我们的组件就是信使,这样就无需发信人四处奔波了。

我们还要求不破坏原本的结构体的结构,这也就意味着我们不能改动已经存在的结构体,比如果让它继承一个消息基类然后就能作为消息传递之类的操作——虽然很好,但是我们得对这个设计说拜拜了。但是,上树订阅分发的流程必然要求消息拥有一个统一的基类类型,否则我们无法统一回调函数的函数签名,存储订阅关系也就无从谈起了!因为参数类型不同的函数,是很难存储到一个容器中以供查询的!

为了解决这个闹人的问题,我们不妨反向思考一下,既然我们不能让一个已经存在的消息结构继承我们的基类,那么就创建一个新的类型同时继承两者吧!

 class NewExistMsg : public ExistMsg, public em::EasyMsg

用户可以使用 NewExistMsg 来创建消息体,就像使用 ExistMsg一样,回调函数可以使用EasyMsg*作为参数,来达到类型的统一,并可以安全的进行多态设计。

至此,消息的问题也解决了。

你可能会感兴趣的技术细节

以下是EasyMsg的头文件:

class EASYMSG_API EasyMsg {
public:
EasyMsg();
virtual ~EasyMsg() = default;
virtual std::string id() const = 0; template <typename T> struct is_easymsg {
template <typename U> static char test(typename U::MsgType *x);
template <typename U> static long test(U *x);
static const bool value = sizeof(test<T>(0)) == 1;
}; // c++17 support constexpr if
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L)
template <typename EASY_MSG_ID> bool match() {
is_easymsg<EASY_MSG_ID> test_easymsg;
if constexpr (test_easymsg.value) { // c++17
return id() == EASY_MSG_ID::value;
} else {
std::cerr << "匹配消息ID时发生错误,检查是否使用了未定义的消息? 检查:"
<< typeid(EASY_MSG_ID).name() << std::endl;
return false;
}
}
#else
template <class MSGID>
typename std::enable_if<!is_easymsg<MSGID>::value, bool>::type match() {
std::cerr
<< "匹配消息ID时发生错误,检查是否使用了未定义的消息? typeinfo : "
<< typeid(MSGID).name() << std::endl;
return false;
} template <class MSGID>
typename std::enable_if<is_easymsg<MSGID>::value, bool>::type match() {
return id() == MSGID::value;
} #endif
};

这里边有一些有意思的东西可以学习一下,首先映入眼帘的就像是经典的虚析构函数,这是作为多态基类的必要手续。接下来就是SFINAE的经典用法,我是用这个技巧实现了match函数,这个函数的主要作用就是判断给定的EASY_MSG_ID是否和传入的消息指针是同一种消息类型。

match根据c++标准分成了两个实现,C++17版本借助了 constexpr if特性。以前的版本则用了经典的std::enable_if。

SFINAE不甚了解的人应该很难理解这些代码,SFINAE中文含义为“匹配失败不是错误”,这对模板变成来说非常重要,不过这已经超出了本文范围,我仅仅是抛砖引玉,之后我可能会更新文章对此段代码进行详解,从而让大家了解这些惯用法。

其他的便没有什么技术细节了,都是些常规的东西,无非是用map记录下订阅关系,然后send时执行回调之类的东西,不值细说。

结论

本文向大家介绍了一个侵入性较低的C++对象间通信组件,或许可以帮助你解决一些头疼的通信问题,并展示了一些你可能感兴趣的技术细节,如果能引发更多的思考那就更好不过了!

C++对象间通信组件,让C++对象“无障碍交流”的更多相关文章

  1. C++ 对象间通信框架 V2.0 ××××××× 之(五)

    类定义: ======================================================================= // MemberFuncPointer.h: ...

  2. C++ 对象间通信框架 V2.0 ××××××× 之(三)

    类定义:CSignalSlot ======================================================================= // SignalSlo ...

  3. C++ 对象间通信框架 V2.0 ××××××× 之(二)

    公共头文件:ss_type_def.h ================================================================================ ...

  4. C++ 对象间通信框架 V2.0 ××××××× 之(四)

    类定义:CMemberFuncPointer ======================================================================= // Me ...

  5. C++ 对象间通信框架 V2.0 ××××××× 之一

    V2.0 主要是信号槽连接的索引性能做了改进,新设计了程序构架实现了多级分层索引,索引时间性能基本不受连接表的大小影响. 类定义:CSignalSlot C_MemberFuncPointer C_s ...

  6. [转] React 中组件间通信的几种方式

    在使用 React 的过程中,不可避免的需要组件间进行消息传递(通信),组件间通信大体有下面几种情况: 父组件向子组件通信 子组件向父组件通信 跨级组件之间通信 非嵌套组件间通信 下面依次说下这几种通 ...

  7. 如何才能学到Qt的精髓——信号槽之间的无关性,提供了绝佳的对象间通讯方式,QT的GUI全是自己的一套,并且完全开源,提供了一个绝好机会窥视gui具体实现

    姚冬,中老年程序员 叶韵.KY Xu.赵奋强 等人赞同 被邀请了很久了,一直在思考,今天终于下决心开始写回答. 这个问题的确是够大的,Qt的代码规模在整个开源世界里也是名列前茅的,这么大的项目其中的精 ...

  8. Linux下多任务间通信和同步-信号

    Linux下多任务间通信和同步-信号 嵌入式开发交流群280352802,欢迎加入! 1.概述 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式.信号可以直接进行用户空间进程和内核进程之间的 ...

  9. Linux下多任务间通信和同步-概述

    Linux下多任务间通信和同步-概述 嵌入式开发交流群280352802,欢迎加入! 在前面,我们学习了两种多任务的实现手段:进程和线程.由于进程是工作在独立的内存空间中,不同的进程间不能直接访问到对 ...

随机推荐

  1. c语言实现双链表的基本操作—增删改查

    //初始化 Node*InitList() { Node*head=(Node*)malloc(sizeof(Node)); if(NULL==head) { printf("内存分配失败! ...

  2. spring原始注解(value)-03

    本博客依据是是spring原始注解-02的代码 注入普通数据类型:@Value注解的使用 1.添加driver属性,使用value注解 @Service("userService" ...

  3. Spring-Bean的依赖注入分析-01

    ###我们先通过一个例子弄明白为什么要使用依赖注入### 1.创建业务层UserService接口及UserServiceImpl实现类(接口代码省略) public class UserServic ...

  4. 服务器的cpu 核心、线程

    此版本有大范围改动,因为cpu作为一个大脑,所以更细致的进行了,相关的分析和阐述. 1.版本1. 2022.1.242.版本2: 2022.3.2 采集数据: ht2机器为物理机,cpu是4颗cpu, ...

  5. 无法访问 CentOS7服务器上应用监听的端口

    无法访问 CentOS7服务器上应用监听的端口 参考资料 云主机上Centos7配置Iptables规则开启80.3306等端口https://blog.csdn.net/qq_37960007/ar ...

  6. vue动态绑定属性--基本用法及动态绑定class

    动态绑定属性v-bind:,语法糖形式:省略v-bind,仅写一个冒号. 一.动态绑定基本属性 1 <body> 2 <!-- v-bind 动态绑定属性-基本用法 --> 3 ...

  7. javascript中的Ajax基础(一)

    一.手写一个ajax 1 const xhr = new xmlHttpRequest() 2 3 xhr.open(请求方式:post get, 请求地址, 同步或者异步) 4 5 xhr.onre ...

  8. Go汇编语法和MatrixOne使用介绍

    目录 MatrixOne数据库是什么? Go汇编介绍 为什么使用Go汇编? 为什么不用CGO? Go汇编语法特点 操作数顺序 寄存器宽度标识 函数调用约定 对写Go汇编代码有帮助的工具 avo tex ...

  9. 【ACM程序设计】最小生成树 Prim算法

    最小生成树 ● 最小生成树的定义是给定一个无向图,如果它任意两个顶点都联通并且是一棵树,那么我们就称之为生成树(Spanning Tree).如果是带权值的无向图,那么权值之和最小的生成树,我们就称之 ...

  10. ceph日常运维管理

    点击关注上方"开源Linux", 后台回复"读书",有我为您特别筛选书籍资料~ 相关阅读: ceph分布式存储简介 常见问题 nearfull osd(s) o ...