介绍

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

推荐初步熟悉项目后阅读本文: 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. Hyperledger Fabric定制联盟链网络工程实践

    总体来看,网络上成体系的可用的 Fabric 教程极少--不是直接在 Fabric 官网复制内容大谈基础理论就是在描述一个几乎无法复现的项目实践,以至于学习 Fabric 的效率极低,印象最深刻的就是 ...

  2. sqlplus文件查看自带oracle命令的执行过程

    问题描述:看到一篇文章 在$ORACLE_HOME/bin/sqlplus中可以查看到数据库命令的查询语句.可以直接编辑sqlplus文件,查到到我们平时标准系统命令的原脚本,但是自己进行编辑查看却是 ...

  3. Fabric2.2中的Raft共识模块源码分析

    引言 Hyperledger Fabric是当前比较流行的一种联盟链系统,它隶属于Linux基金会在2015年创建的超级账本项目且是这个项目最重要的一个子项目.目前,与Hyperledger的另外几个 ...

  4. 如何为我的VUE项目编写高效的单元测试--Jest

    Unit Testing(单元测试)--Jest 一个完整的测试程序通常由几种不同的测试组合而成,比如end to end(E2E)测试,有时还包括整体测试.简要测试和单元测试.这里要介绍的是Vue中 ...

  5. Promise了解

    Promise是一个构造函数,excutor是他的执行函数,同时也是构造函数的参数. new Promise(excutor) excutor有两个参数(resolve,reject) Promise ...

  6. 阿里云centos postgresql9.4源码安装 精简步骤、问题解答

    阿里云centos环境源码安装postgresql9.4 本文的安装步骤主要来源于http://www.cnblogs.com/mchina/archive/2012/06/06/2539003.ht ...

  7. JuiceFS 缓存预热详解

    缓存预热是一个比较常见的概念,相信很多小伙伴都有所了解.对于 JuiceFS 来说,缓存预热就是将需要操作的数据预先从对象存储拉取到本地,从而获得与使用本地存储类似的性能表现. 缓存预热 JuiceF ...

  8. MySQL数据存储

    MySQL体系架构 客户端连接器 提供与MySQL服务器建立的支持.目前几乎支持所有主流的服务端编程技术,例如常见的 Java.C.Python..NET等,它们通过各自API技术与MySQL建立连接 ...

  9. numpy.core._exceptions.UFuncTypeError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('<U1'), dtype('float64')) -> None

    在机器学习实战的Logistic回归梯度上升优化算法中遇到了这个问题 numpy.core._exceptions.UFuncTypeError: ufunc 'subtract' did not c ...

  10. Web Api源码(路由注册)

    这篇文章只是我学习Web API框架的输出,学习方法还是输出倒逼输入比较行得通,所以不管写的好不好,坚持下去,肯定有收获.篇幅比较长,仔细思考阅读下来大约需要几分钟. 做.NET开发有好几年时间了,从 ...