找回密码
 立即注册
首页 业界区 业界 事件系统设计(C++)

事件系统设计(C++)

讥慰捷 2025-10-1 13:01:28
具体调试代码参考github:https://github.com/hggzhang/CppTest/tree/master
概述

在程序设计中,我们希望关联程度低的对象之间的联系是“松耦合”的,也即减少直接依赖。一般的做法是使用消息机制进行信息的传递和响应,其中事件系统是其一种常规手段之一,下面我们尝试使用C++实现一个事件系统。
观察者模式

假如你想买一份报纸:

  • 不停的去邮局问今天的报纸到了嘛?到了嘛?到了嘛?... (轮询模式)
  • 去邮局登记要买今天的报纸,之后就回家;邮局报纸到了之后,邮局按照登记名单送报纸。(观察者模式)
对比这两种方式,相对你而言是不是方法2更加高效?你不必一直在邮局等着一直问报纸到了没,登记信息回家看电视等着就好了。所谓观察者模式基本就是这种思想,其核心要素有:

  • 订阅消息:对某个感兴趣的消息A进行登记。
  • 发布消息:“相关单位”发布消息A,通知所有登记者该消息内容。
当然,这只是主题思想,在程序上实现观察者模式还需要考虑一些其他因素,下面将会进行介绍。
运行测试
  1. void event_test_main()
  2. {
  3.         auto lmd1 = [](const EventPosition& e) {
  4.                 LOG_VAR(e.X);
  5.                 LOG_VAR(e.Y);
  6.         };
  7.         auto register1 = std::make_shared<EventRegister>();
  8.         int id1 = register1->Sub<EventPosition>(lmd1);
  9.         EventBus::GetInst().Pub(EventPosition(10, 20));
  10.         register1->UnSub(id1);
  11.         EventBus::GetInst().Pub(EventPosition(20, 20));
  12. }
复制代码
1.png

程序总体设计

参考上述邮局的例子,我们来介绍下程序的核心“角色”(类):

  • Event 报纸
  • EventBus 邮局
  • EventListener 买报纸的人
另外还需要其他辅助

  • EventRegister 类似秘书或助手,帮助管理订阅相关事务
总结下各个角色的相关职责划分:

  • Event 事件信息载体,比如鼠标移动事件包含{x, y}的坐标
  • EventBus 事件总线,管理事件的订阅/发布
  • EventListener 事件观察者,包含其关心的事件的类型,事件的响应函数,自身的ID

    • ID:由于一个事件可能有多个观察者,用于比较确定我们自身是哪个

  • EventRegister 管理EventListener,对事件相关行为进行封装,避免让每个使用者都实现一遍。
程序细节设计

这里结合C++语言特性和程序的特点说明下一些需要注意的细节:
Event

https://github.com/hggzhang/CppTest/blob/master/Program/Event.h
使用多态来实现不同的事件。
  1. class EventBase
  2. {
  3. public:
  4.         virtual ~EventBase() = default;
  5. };
  6. class EventPosition : public EventBase
  7. {
  8. public:
  9.         EventPosition(int x, int y) :X(x), Y(y) {}
  10.         int X;
  11.         int Y;
  12. };
复制代码
EventBus

https://github.com/hggzhang/CppTest/blob/master/Program/EventBus.h
  1. class EventBus : public Singleton<EventBus>
  2. {
  3.         friend class Singleton<EventBus>;
  4.         // ...
  5. }
复制代码

  • Singleton 使用全局单例提高效率
  1. std::unordered_map<std::type_index, std::list<std::weak_ptr<EventListenerBase>>> subers;
复制代码

  • 使用type_index作为key来存储监听者列表,相对于字符串(需使用者定义)和typeid更加稳定
  • 监听者列表使用list容器,添加和删除效率更好
  • 使用弱指针weak_ptr来缓存监听者避免循环依赖
  1. #ifdef ENABLE_EVENTBUS_MULTI_THREAD
  2.         mutable std::mutex mutex;
  3. #endif
复制代码

  • 使用编译开关控制线程锁
  1. template<typename EventT>
  2. void Pub(const EventT& event)
  3. {
  4.         // prevent infinite recursion
  5.         static thread_local int depth = 0;
  6.         constexpr int MAX_DEPTH = 10;
  7.         // ...
  8.         std::vector<std::shared_ptr<EventListenerBase>> validListeners;
  9.         // ...
  10.         std::type_index typeIndex = typeid(EventT);
  11.         // ...
  12. }
复制代码

  • 使用模板方法来发布事件,typeid(EventT)计算标识符Key更加高效
  • depth和validListeners副本列表避免循环,如发布事件的回调里油订阅了事件等
EventListener
  1. class EventListenerBase
  2. {
  3. public:
  4.         int ID = 0;
  5.         std::type_index Key = typeid(void);
  6. };
  7. template<typename EventT>
  8. class EventListener : public EventListenerBase
  9. {       
  10. public:
  11.         std::function<void(const EventT&)> callback;
  12.         EventListener( std::function<void(const EventT&)> InCB)
  13.                 :callback(std::move(InCB))
  14.         {
  15.                 Key = typeid(EventT);
  16.         };
  17. };
复制代码

  • 使用type_index作为标识符相比string和typename更高效可靠
  • 由于事件的监听者可能有多个,使用ID用于标识监听者,方便查找
  • std::move 转移内存控制权,减少拷贝
EventRegister
  1. class EventRegister
  2. {
  3. private:
  4. #ifdef ENABLE_EVENTBUS_MULTI_THREAD
  5.         std::atomic<int> id = 0;
  6. #else
  7.         int id = 0;
  8. #endif
  9. public:
  10.         std::vector<std::shared_ptr<EventListenerBase>> listeners;
  11.         // ...
  12. }
复制代码

  • 封装管理EventListener
  • 启用多线程时,需要原子化变量
代码清单

参考github地址:https://github.com/hggzhang/CppTest/tree/master
部分代码在此贴出
Event.h
  1. class EventBase
  2. {
  3. public:
  4.         virtual ~EventBase() = default;
  5. };
  6. class EventPosition : public EventBase
  7. {
  8. public:
  9.         EventPosition(int x, int y) :X(x), Y(y) {}
  10.         int X;
  11.         int Y;
  12. };class EventKeyPress : public EventBase{public:        EventKeyPress(char key) :Key(key) {}        char Key;};
复制代码
EventBus.h
  1. #pragma once#include #include #include #include #include #include #include #include #include "Event.h"#include "TSingleton.h"#define ENABLE_EVENTBUS_MULTI_THREAD#ifdef ENABLE_EVENTBUS_MULTI_THREAD#include #include #endifclass EventBus;class EventListenerBase
  2. {
  3. public:
  4.         int ID = 0;
  5.         std::type_index Key = typeid(void);
  6. };
  7. template<typename EventT>
  8. class EventListener : public EventListenerBase
  9. {       
  10. public:
  11.         std::function<void(const EventT&)> callback;
  12.         EventListener( std::function<void(const EventT&)> InCB)
  13.                 :callback(std::move(InCB))
  14.         {
  15.                 Key = typeid(EventT);
  16.         };
  17. };class EventBus : public Singleton{        friend class Singleton;private:#ifdef ENABLE_EVENTBUS_MULTI_THREAD
  18.         mutable std::mutex mutex;
  19. #endif        // we use type_index as the key to store different event types, they more safe than using typeid(T) as string,         // and use weak_ptr to avoid circular reference        std::unordered_map<std::type_index, std::list<std::weak_ptr<EventListenerBase>>> subers;public:        void Sub(std::shared_ptr lsner)        {#ifdef ENABLE_EVENTBUS_MULTI_THREAD                std::lock_guard lock(mutex);#endif                std::type_index typeIndex = lsner->Key;                if (subers.find(typeIndex) == subers.end())                {                        subers[typeIndex] = std::list();                }                subers[typeIndex].push_back(lsner);        }        void UnSub(std::shared_ptr lsner)        {#ifdef ENABLE_EVENTBUS_MULTI_THREAD                std::lock_guard lock(mutex);#endif                std::type_index typeIndex = lsner->Key;                auto it = subers.find(typeIndex);                if (it != subers.end())                {                        auto& vec = subers[typeIndex];                        // we need to compare the raw pointer, because the shared_ptr in vec is different from lsner                        vec.erase(std::remove_if(vec.begin(), vec.end(),                                 [&](const std::weak_ptr& wp) {                                        auto sp = wp.lock();                                        return !sp || sp.get() == lsner.get();                                }),                                vec.end());                        if (vec.empty()) {                                subers.erase(it);                        }                }        }                template        void Pub(const EventT& event)        {                // prevent infinite recursion                static thread_local int depth = 0;                constexpr int MAX_DEPTH = 10;                if (depth >= MAX_DEPTH) {                        // log error                        return;                }                depth++;                std::vector validListeners;                {                        #ifdef ENABLE_EVENTBUS_MULTI_THREAD                        std::lock_guard lock(mutex);                        #endif                        // get valid ones and prevent callbacks from modifying the subers map during iteration                        // we cant't put validListeners outside, because Pub might be called recursively                        std::type_index typeIndex = typeid(EventT);                        if (subers.find(typeIndex) != subers.end())                        {                                auto& list = subers[typeIndex];                                for (auto it = list.begin(); it != list.end(); )                                {                                        if (auto lsner = it->lock())                                        {                                                validListeners.push_back(lsner);                                                ++it;                                        }                                        else                                        {                                                it = list.erase(it);                                        }                                }                        }                }                for (auto& lsner : validListeners) {                        try                        {                                auto derivedLsner = std::static_pointer_cast(lsner);                                if (derivedLsner)                                {                                        derivedLsner->callback(event);                                }                                else                                {                                        // log error                                        continue;                                }                        }                        catch (...)                        {                                // log error                                continue;                        }                }                depth--;        }};class EventRegister{private:#ifdef ENABLE_EVENTBUS_MULTI_THREAD        std::atomic id = 0;#else        int id = 0;#endifpublic:        std::vector listeners;        template        int Sub(std::function callback)        {                auto& bus = EventBus::GetInst();                auto listener = std::make_shared(callback);                listener->ID = ++id;                bus.Sub(listener);                listeners.push_back(std::move(listener));                return id;        }        void UnSub(int ID)        {                auto& bus = EventBus::GetInst();                auto it = std::remove_if(listeners.begin(), listeners.end(),                        [&](const std::shared_ptr& lsner) {                                if (lsner->ID == ID)                                {                                        bus.UnSub(lsner);                                        return true;                                }                                return false;                        });                listeners.erase(it, listeners.end());        }        void Clear()        {                auto& bus = EventBus::GetInst();                for (auto& lsner : listeners)                {                        bus.UnSub(lsner);                }                listeners.clear();        }};
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
10 小时前

举报

这个好,看起来很实用
您需要登录后才可以回帖 登录 | 立即注册