在现代软件架构中,事件通信(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)实现
下面是一个轻量级、线程安全的信号槽实现,支持任意函数签名,可动态连接与断开。
cpp
#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)实现
以下是一个典型的类型驱动事件总线实现,支持任意事件类型,全局静态管理,线程安全。
cpp
#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:
// 订阅事件类型 T
template <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 响应、模块内部通信等场景。
cpp
#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)**则采用"发布-订阅"的设计理念,基于类型进行事件分发。任何模块都可以发布某类事件,所有对此事件类型感兴趣的订阅者将被自动调用,无需双方直接依赖,极适用于插件系统、游戏逻辑等全局通信场景。
cpp
#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)。
本文提供的两种轻量实现,可在你的项目中直接使用或作为扩展基础。如需支持更高级功能(如:异步处理、优先级队列、自动解绑等),可在此架构之上进一步演化构建更完整的事件系统。