📘 本篇围绕 Qt 编程中最经典的设计之一——Signal/Slot(信号-槽)机制进行完整复现与讲解。从原理推导到实战构建,我们将结合 Day 10~11 的回调与观察者经验,逐步实现一个类型安全、生命周期安全、支持弱引用与自动解绑的轻量信号-槽系统,打通事件驱动设计能力。
🔁 Day 11 回顾:观察者模式构建要点
模块 | 内容总结 |
---|---|
发布者-订阅者模型 | 通过模板与 function 实现注册/通知体系 |
weak_ptr 防悬空 | 使用智能指针确保观察者自动失效,不崩溃 |
事件 ID 注册机制 | 通过 Token 管理连接与解绑 |
模板化封装 | 支持任意参数传递、任意观察者结构 |
📌 本篇将在此基础上,引入 Slot(槽函数)+ Signal(信号发出者)模型,实现更自然、简洁的连接调用体验。
🎯 今日目标:构建轻量级 Signal/Slot 通信系统
组件 | 功能说明 |
---|---|
Signal<T…> | 提供 connect/disconnect/emit 接口 |
Slot / 回调 | 支持任意类型可调用对象,包括成员函数与 Lambda |
生命周期安全 | 使用 weak_ptr 避免调用已销毁对象 |
高级支持 | 一次性连接、临时解绑、参数任意、嵌套安全调用 |
✅ 一、设计动机:Signal/Slot 比观察者更优在哪里?
🔍 特点对比:
功能维度 | 观察者模式 | Signal/Slot |
---|---|---|
注册方式 | 通常使用注册函数(如 addObserver) | 使用 connect 连接 signal 与 slot |
调用触发 | 使用 notifyAll() | signal.emit(…) |
参数泛型支持 | 需手动泛型 | 模板内封装参数类型 |
支持成员方法 | 需配合 bind/std::function | 可直接连接成员函数 + this |
生命周期安全 | weak_ptr + lock | 内部管理 weak_ptr,自动判断失效 |
📌 Signal/Slot 更适合构建“模块间解耦事件响应机制”,特别是 UI 与业务层之间。
✅ 二、基本骨架设计:Signal 模板类
#include <functional>
#include <unordered_map>
#include <memory>
#include <iostream>template<typename... Args>
class Signal {
public:using Slot = std::function<void(Args...)>;using Token = size_t;Token connect(Slot slot) {slots[++counter] = slot;return counter;}void disconnect(Token token) {slots.erase(token);}void emit(Args... args) {for (auto& [_, fn] : slots) {fn(args...);}}private:std::unordered_map<Token, Slot> slots;Token counter = 0;
};
📌 接口说明:
connect(fn)
:注册一个槽函数,返回唯一连接标识disconnect(id)
:根据 ID 解绑指定回调emit(...)
:发送信号,自动调用所有注册函数
✅ 三、支持成员函数 + 弱引用绑定
🔸 辅助工具:bindWeakMember
template<typename T, typename... Args>
std::function<void(Args...)> bindWeakMember(std::weak_ptr<T> wp, void (T::*method)(Args...)) {return [wp, method](Args... args) {if (auto sp = wp.lock()) {(sp.get()->*method)(args...);}};
}
📌 用法:防止调用已销毁对象成员,自动跳过
🔸 示例结构:
struct Receiver : public std::enable_shared_from_this<Receiver> {void handle(int val) {std::cout << "📥 接收值:" << val << "\n";}
};Signal<int> sig;{auto r = std::make_shared<Receiver>();sig.connect(bindWeakMember(r, &Receiver::handle));sig.emit(100); // 正常输出
}sig.emit(200); // 已销毁,自动跳过
✅ 四、拓展功能:一次性连接 / 自动解绑
🔸 一次性连接 once
template<typename... Args>
class SignalOnce : public Signal<Args...> {
public:typename Signal<Args...>::Token connectOnce(typename Signal<Args...>::Slot slot) {auto id = this->connect([this, slot, id = this->nextId()] (Args... args) mutable {slot(args...);this->disconnect(id);});return id;}private:typename Signal<Args...>::Token nextId() { return ++this->counter; }
};
📌 适用场景:仅需一次响应的事件(如初始化完毕、一次确认)
✅ 五、完整应用案例:业务模块通信
📦 定义模块
class LoginManager {
public:Signal<std::string> onLoginSuccess;void login() {std::cout << "🔐 登录中...\n";onLoginSuccess.emit("user123");}
};class ChatPanel : public std::enable_shared_from_this<ChatPanel> {
public:void onUserReady(const std::string& uid) {std::cout << "💬 欢迎用户:" << uid << " 进入聊天室\n";}
};
🤝 模块连接:
auto chat = std::make_shared<ChatPanel>();
LoginManager login;
login.onLoginSuccess.connect(bindWeakMember(chat, &ChatPanel::onUserReady));login.login(); // 输出登录 & 聊天面板响应
✅ 六、经验总结
设计目标 | 实现方式 |
---|---|
支持多回调 | 使用 map 储存 slots |
生命周期安全 | 使用 weak_ptr + lock |
支持任意参数函数 | 使用模板参数展开 |
支持解绑/一次性调用 | 提供 token + connectOnce 接口 |
支持成员方法绑定 | 使用 bindWeakMember 工具函数 |
📌 Signal/Slot 是观察者模式的模板化封装 + 生命周期控制的优雅实现方式。
📘 实战反思与扩展建议
- 在 UI、游戏、客户端业务中非常适用
- 可拓展支持事件分组、跨线程调用、异步触发
- 可结合线程池、事件队列构建完整事件调度系统
🔭 Day 13 预告:协程 + 异步模型整合信号响应
下一篇将结合:
std::future / std::promise
+ Signal- 信号触发后自动 resume 协程流程
- 构建“异步事件驱动业务模块”的基本框架
📌 如果你希望结合 GUI、游戏引擎、任务调度等真实项目场景,我可优先提供方向图与代码结构 💡