【C++】信号槽与事件总线的轻量实现

在现代软件架构中,事件通信(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)

本文提供的两种轻量实现,可在你的项目中直接使用或作为扩展基础。如需支持更高级功能(如:异步处理、优先级队列、自动解绑等),可在此架构之上进一步演化构建更完整的事件系统。

相关推荐
巨可爱熊24 分钟前
高并发内存池(定长内存池基础)
linux·运维·服务器·c++·算法
qq_365911602 小时前
GPT-4、Grok 3与Gemini 2.0 Pro:三大AI模型的语气、风格与能力深度对比
开发语言
码农新猿类3 小时前
服务器本地搭建
linux·网络·c++
Susea&3 小时前
数据结构初阶:队列
c语言·开发语言·数据结构
慕容静漪3 小时前
如何本地安装Python Flask并结合内网穿透实现远程开发
开发语言·后端·golang
ErizJ3 小时前
Golang|锁相关
开发语言·后端·golang
GOTXX3 小时前
【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
开发语言·数据库·c++·qt·图形渲染·图形化界面·qt新手入门
搬砖工程师Cola3 小时前
<C#>在 .NET 开发中,依赖注入, 注册一个接口的多个实现
开发语言·c#·.net
巨龙之路4 小时前
Lua中的元表
java·开发语言·lua
徐行1104 小时前
C++核心机制-this 指针传递与内存布局分析
开发语言·c++