裸用 std::function 实现信号槽不工程化,因存在线程安全、连接生命周期管理、重复订阅/断连、槽函数调用时对象已析构四大问题;必须用 weak_ptr 防悬垂、connection 自动管理、shared_mutex 读写分离、唯一键去重。

直接用 std::function + std::vector 实现信号槽,能跑但不工程化;真要落地,必须解决线程安全、连接生命周期管理、重复订阅/断连、以及槽函数调用时对象已析构这四大问题。
为什么不能裸用 std::function 存槽函数
裸存会导致悬垂引用:如果槽函数绑定了某个对象的成员函数(如 std::bind(&Widget::onData, widget_ptr)),而 widget_ptr 先于信号对象销毁,后续触发就会崩溃。这不是“偶尔出错”,是必然未定义行为。
实操建议:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 强制槽函数携带弱引用语义:用
std::weak_ptr包裹被观察对象,调用前lock()检查有效性 - 不接受裸指针或栈对象绑定;只支持
std::shared_ptr管理的对象 + 成员函数,或静态/lambda 函数(无捕获或仅捕获值) - 禁止在构造函数里直接 connect —— 此时派生类成员尚未初始化,
shared_from_this()可能抛异常
connect() 返回值必须是可销毁的连接句柄
返回 void 或 bool 的接口无法支持“自动断连”和“作用域绑定”。用户需要显式调用 disconnect(),极易遗漏。
实操建议:
- 让
connect()返回一个轻量级connection对象(内部持有一个std::weak_ptr到信号内部的连接节点) -
connection析构时自动从信号的监听列表中移除自身,无需用户干预 - 支持
connection.disconnect()手动提前断开,也支持connection.blocked(true)临时禁用(避免递归触发)
多线程下 emit() 的锁策略选型
粗粒度全局锁(如整个 emit() 加 std::mutex)会严重拖慢高频信号场景;无锁又难保顺序与一致性。
实操建议:
- 对监听列表读取加
std::shared_mutex(C++17):emit 时共享读,connect/disconnect 时独占写 - 槽函数调用本身不持有锁——把锁粒度控制在“遍历+拷贝监听器”阶段,避免槽函数阻塞影响其他 emit
- 若业务明确单线程,提供编译期开关(如
#define SIGNAL_NO_THREAD_SAFETY)跳过所有锁逻辑,减少 15%~20% 调用开销
如何避免重复 connect 导致多次调用
同一对象同一成员函数反复 connect,不是“覆盖”,而是叠加注册——emit 时会被调用 N 次。用户通常意识不到这是自己代码的问题。
实操建议:
- 在连接时生成唯一键:组合
type_id+object address+member function pointer(注意:MSVC 和 GCC 的 member func ptr 布局不兼容,需用std::hash封装后比较) - 默认启用去重(可配置关闭);重复 connect 返回已有
connection,而非新建 - 调试模式下触发
assert或日志警告,提醒开发者检查 connect 位置是否在循环/重入路径中
真正难的不是发信号,是确保“谁还活着、谁该被跳过、谁连错了、谁正在被另一个线程修改”。这些细节不写进析构函数、不压进 connection 生命周期、不暴露给用户做选择,就谈不上工程化。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










