用std::list因删除节点时迭代器不失效,避免通知时崩溃;线程安全需用shared_mutex保护读写;防悬空回调应结合weak_ptr或shared_ptr管理生命周期;通知需try/catch隔离异常。

为什么用 std::list 而不是 std::vector 存储订阅者
因为发布过程中可能有监听者主动取消订阅(比如析构或调用 unsubscribe()),而 std::list 的迭代器在删除其他节点时不会失效——这避免了在遍历通知时因 erase() 导致迭代器崩溃。用 std::vector 则必须手动管理索引或改用反向遍历,稍不注意就触发 iterator invalidation 或越界访问。
不过代价是:随机访问慢、内存局部性差;如果你确定订阅者极少变动(比如初始化后只增不删),std::vector 反而更轻量。
subscribe() 和 unsubscribe() 怎么保证线程安全
标准库容器本身不提供线程安全的读写并发保护。如果多个线程同时调用 subscribe() 或 unsubscribe(),必须加锁。常见做法是在 Publisher 内部持有一个 std::shared_mutex(C++17)或 std::mutex:
- 读操作(如遍历通知)用共享锁(
lock_shared()) - 写操作(增/删监听者)用独占锁(
lock())
注意:不能只对 notify() 加锁——它内部要读取列表,但若此时另一个线程正在 unsubscribe(),没锁就会 UB。最简方案是所有成员函数都保护同一把互斥量,除非你明确区分读写频次并愿意承担复杂度。
立即学习“C++免费学习笔记(深入)”;
如何避免监听者被销毁后仍收到通知(悬空回调)
这是 PubSub 最常见的 crash 原因:监听者对象已析构,但其 std::function 还留在 std::list 里。解决方案不是“靠使用者自觉”,而是从接口设计上堵住漏洞:
- 强制监听者继承一个带虚析构的基类(如
Subscriber),并在析构时自动调用unsubscribe() - 或改用
std::weak_ptr<:function>></:function>+ 每次通知前lock()检查有效性(但会增加开销) - 更推荐的是用
std::shared_ptr管理监听者生命周期,并让subscribe()接收std::shared_ptr绑定的 lambda 或成员函数——这样只要监听者还活着,回调就安全;一旦释放,对应节点可由unsubscribe()显式清理
别依赖 std::function 的空检查:std::function 即使绑定了已销毁对象的成员函数,调用时仍会解引用野指针。
通知时能否支持异常传播与中断
默认情况下,一个监听者的异常会直接终止整个通知循环——后续监听者收不到消息。这通常不符合预期。解决方式很简单:在 notify() 的遍历中用 try/catch 包裹单个回调调用:
for (auto& cb : subscribers_) {
try {
cb(data);
} catch (const std::exception& e) {
// 记录日志,但不 re-throw
}
}
如果你需要「某个监听者失败就停止通知」,那就把 catch 移到循环外;但多数场景下静默吞掉单个异常更健壮。另外注意:不要在回调里抛出未声明的异常类型(如从 noexcept 函数抛出),否则程序会调用 std::terminate()。
实际写的时候,最容易被忽略的是监听者生命周期和异常隔离这两点——光把函数存进 std::list 不难,难的是让它在线程切换、对象销毁、回调出错时不崩不漏。


















