ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

C++对象间通信组件,让C++对象“无障碍交流”

2022-06-19 23:01:14  阅读:135  来源: 互联网

标签:std 订阅 函数 对象 C++ 间通信 消息 组件 value


介绍

这是很久之前的一个项目了,最近刚好有些时间,就来总结一下吧!
推荐初步熟悉项目后阅读本文: 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++对象间通信组件,或许可以帮助你解决一些头疼的通信问题,并展示了一些你可能感兴趣的技术细节,如果能引发更多的思考那就更好不过了!

标签:std,订阅,函数,对象,C++,间通信,消息,组件,value
来源: https://www.cnblogs.com/xdblog/p/16391730.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有