在现代软件架构中,事件通信(Event Communication) 已成为实现模块解耦与灵活扩展的关键机制。无论是 GUI 编程、游戏开发、插件系统,还是通用消息分发场景,我们都常常需要一种“非侵入式”的方式,在对象或模块之间传递消息。
由于 C++ 并不像 C#、JavaScript 等语言自带成熟的事件系统,我们通常需要自行设计或引入相应机制。本文聚焦两种经典模式:
-
信号槽(Signal-Slot)
-
事件总线(EventBus/DataBus)
我们将依次介绍它们的设计理念与适用场景,随后给出精简高效的实现代码,并配以示例,帮助你在实际项目中快速落地使用。
常见事件通信范式对比
模式 | 同步/异步 | 典型用途 | 特征关键词 |
---|---|---|---|
Callback / Listener | ✅ 同步 | 库回调、小型模块 | 简单、强绑定、函数指针 |
Signal-Slot | ✅ 同步 | UI 框架、组件事件 | 一对多、模板信号、自动连接 |
EventBus / DataBus | ✅ 同步 | 插件系统、模块通信 | 类型驱动、广播机制、全局轻量 |
Observer Pattern | ✅ 同步 | MVC、状态同步 | 被动通知、对象绑定、依附主体 |
Pub/Sub (消息队列) | ✅ 异步 | 服务通信、日志系统 | 异步、主题订阅、消息中间件 |
Command Pattern | ✅ 同步 | 撤销重做、命令封装 | 行为抽象、可组合 |
Event Queue / Reactor | ✅ 异步 | UI 循环、异步 I/O | 事件队列、非阻塞调度 |
Actor Model | ✅ 异步 | 并发系统、微服务 | 并发隔离、消息驱动 |
信号槽(Signal-Slot)实现
下面是一个轻量级、线程安全的信号槽实现,支持任意函数签名,可动态连接与断开。
#pragma once
#include <functional>
#include <vector>
#include <mutex>template <typename... Args>
class Signal {
private:std::mutex _mtx;std::vector<std::function<void(Args...)>> _slots;public:// 连接槽函数template <typename Slot>void connect(Slot&& fn) {std::lock_guard<std::mutex> lock(_mtx);_slots.emplace_back(std::forward<Slot>(fn));}// 发射信号,调用所有槽void emit(Args... args) {std::vector<std::function<void(Args...)>> copy;{std::lock_guard<std::mutex> lock(_mtx);copy = _slots;}for (const auto& slot : copy) {slot(args...);}}// 断开所有槽函数void disconnectAll() {std::lock_guard<std::mutex> lock(_mtx);_slots.clear();}// 获取当前连接数量size_t count() const {std::lock_guard<std::mutex> lock(_mtx);return _slots.size();}
};
事件总线(EventBus)实现
以下是一个典型的类型驱动事件总线实现,支持任意事件类型,全局静态管理,线程安全。
#pragma once
#include <functional>
#include <vector>
#include <mutex>
#include <type_traits>class EventBus {
private:template<typename T>static std::mutex& _mutex() {static std::mutex mtx;return mtx;}template<typename T>static std::vector<std::function<void(const T&)>>& _slots() {static std::vector<std::function<void(const T&)>> slots;return slots;}public:// 订阅事件类型 Ttemplate <typename T>static void on(std::function<void(const T&)> fn) {using Type = std::decay_t<T>;std::lock_guard<std::mutex> lock(_mutex<Type>());_slots<Type>().emplace_back(std::move(fn));}// 发布事件template <typename T>static void emit(T&& e) {using Type = std::decay_t<T>;std::vector<std::function<void(const Type&)>> copy;{std::lock_guard<std::mutex> lock(_mutex<Type>());copy = _slots<Type>();}for (const auto& fn : copy) {fn(e);}}// 清除所有该类型的事件监听template <typename T>static void clear() {using Type = std::decay_t<T>;std::lock_guard<std::mutex> lock(_mutex<Type>());_slots<Type>().clear();}
};
使用示例
信号槽示例
**信号槽(Signal-Slot)**是一种观察者范式的变体,强调“一发多收”的调用链。发送方通过 emit()
发射信号,所有已连接的槽函数将被同步调用,常用于 UI 响应、模块内部通信等场景。
#include <iostream>
#include "Signal.hpp"void globalHandler(int value) {std::cout << "全局槽函数:value = " << value << std::endl;
}int main() {Signal<int> signal;signal.connect(globalHandler); // 连接全局函数signal.connect([](int v) {std::cout << "Lambda 槽函数:v = " << v * 2 << std::endl;});signal.emit(42); // 发射信号std::cout << "当前连接数: " << signal.count() << std::endl;signal.disconnectAll();return 0;
}
输出:
全局槽函数:value = 42
Lambda 槽函数:v = 84
当前连接数: 2
事件总线示例
**事件总线(EventBus)**则采用“发布-订阅”的设计理念,基于类型进行事件分发。任何模块都可以发布某类事件,所有对此事件类型感兴趣的订阅者将被自动调用,无需双方直接依赖,极适用于插件系统、游戏逻辑等全局通信场景。
#include <iostream>
#include <string>
#include "EventBus.hpp"struct UserCreatedEvent {std::string username;
};struct LogEvent {std::string message;
};int main() {EventBus::on<UserCreatedEvent>([](const UserCreatedEvent& e) {std::cout << "用户创建事件:用户名 = " << e.username << std::endl;});EventBus::on<LogEvent>([](const LogEvent& e) {std::cout << "日志事件:消息 = " << e.message << std::endl;});EventBus::emit(UserCreatedEvent{"alice"});EventBus::emit(LogEvent{"用户已创建"});return 0;
}
输出:
用户创建事件:用户名 = alice
日志事件:消息 = 用户已创建
信号槽 vs 事件总线:对比总结
特性 | 信号槽(Signal) | 事件总线(EventBus) |
---|---|---|
通信范式 | 观察者模式(响应式) | 发布-订阅(类型驱动) |
接收方注册方式 | 明确调用 connect() | 全局注册 on<T>() |
发射方式 | emit(args...) | emit(Event{}) |
解耦程度 | 模块级解耦 | 全局解耦 |
线程安全 | ✅ 是 | ✅ 是 |
推荐场景 | UI、组件通信 | 插件、后端模块、游戏逻辑 |
结语
信号槽与事件总线作为 C++ 中两种高效的事件通信范式,各自具备清晰的设计哲学与适用边界:
-
若你希望对象间直接响应绑定,选择 信号槽(Signal) 更加直观;
-
若你更倾向于模块间彻底解耦的通信机制,推荐使用 事件总线(EventBus)。
本文提供的两种轻量实现,可在你的项目中直接使用或作为扩展基础。如需支持更高级功能(如:异步处理、优先级队列、自动解绑等),可在此架构之上进一步演化构建更完整的事件系统。