不能直接用 std::function + std::vector 存槽函数,因裸指针无法感知被观察对象生命周期,导致析构后调用引发未定义行为;安全方案是用 std::weak_ptr + std::shared_ptr 实现弱引用绑定,并封装 Connection 自动管理连接生命周期。

为什么不能直接用 std::function + std::vector 存槽函数?
可以存,但会立刻遇到生命周期管理问题:如果 Slot 是某个对象的成员函数(比如 &obj.onDataReady),而 obj 被提前析构,后续信号触发时调用已释放对象的成员函数 → 未定义行为(常见表现是段错误或静默崩溃)。裸指针或原始 std::function 无法感知被观察对象是否还活着。
如何安全绑定成员函数并自动解绑?
核心是引入弱引用语义。C++11 起,std::weak_ptr 配合 std::shared_ptr 是标准解法:观察者对象需继承自 std::enable_shared_from_this,信号类存储 std::weak_ptr + 成员函数指针,每次触发前用 lock() 检查对象是否有效。
- 信号类中不直接存
std::function,而是存std::pair<:weak_ptr>, void (T::*)()></:weak_ptr>这类组合(实际需类型擦除) - 更实用的做法是封装一个
Connection对象,内部持有一个std::weak_ptr,并在析构时从信号的槽列表中移除自己 - 避免手动管理连接生命周期:让
connect()返回一个可移动、不可复制的Connection,用户持有它即保持绑定;一旦Connection离开作用域,自动断开
一个最小可行的 Signal 类模板实现
以下代码省略异常处理和线程安全(多线程需加 std::mutex),聚焦核心逻辑:
template<typename... Args>
class Signal {
private:
struct SlotBase {
virtual ~SlotBase() = default;
virtual void invoke(Args... args) = 0;
virtual bool is_alive() const = 0;
};
template<typename T, typename Func>
struct Slot final : SlotBase {
std::weak_ptr<T> obj_;
Func func_;
Slot(std::shared_ptr<T> obj, Func&& f)
: obj_(std::move(obj)), func_(std::forward<Func>(f)) {}
void invoke(Args... args) override {
if (auto ptr = obj_.lock()) {
std::invoke(func_, *ptr, std::forward<Args>(args)...);
}
}
bool is_alive() const override {
return obj_.lock() != nullptr;
}
};
std::vector<std::unique_ptr<SlotBase>> slots_;
public:
template<typename T, typename Func>
void connect(std::shared_ptr<T> obj, Func&& f) {
slots_.push_back(std::make_unique<Slot<T, std::decay_t<Func>>>(
std::move(obj), std::forward<Func>(f)));
}
void emit(Args... args) {
// 注意:此处不删除失效 slot,仅跳过;真实项目建议定期清理
for (auto& s : slots_) {
if (s->is_alive()) {
s->invoke(std::forward<Args>(args)...);
}
}
}
};
使用时,被观察对象必须是 std::shared_ptr 管理的(例如继承 std::enable_shared_from_this),否则无法构造 std::weak_ptr。
立即学习“C++免费学习笔记(深入)”;
容易忽略的关键细节
信号触发期间若槽函数内部又调用 connect() 或 disconnect()(比如在槽里删自己),会导致迭代器失效或内存访问越界。解决方式有二:
- 延迟清理:把待移除的 slot 标记为 “dead”,
emit()结束后统一擦除 - 用索引而非迭代器遍历,或改用
std::list配合erase()安全删除 - 更彻底的方案是禁止槽函数修改信号本身 —— 这需要文档约束,而非强制语法限制
另外,std::invoke 是 C++17 引入的,若用 C++11/14,需手写函数对象调用逻辑(如用 std::bind 包装再调用,但性能稍差)。











