std::shared_ptr和std::weak_ptr是响应式观察的基础,因裸指针或值拷贝无法感知生命周期变化;shared_ptr管理被观察对象生命周期,weak_ptr避免循环引用和悬挂回调,确保销毁时自动解绑。

为什么 std::shared_ptr 和 std::weak_ptr 是响应式观察的基础
裸指针或值拷贝无法感知数据生命周期变化,而响应式观察必须保证:观察者不阻止被观察对象销毁,且销毁时能自动解绑回调。用 std::shared_ptr 管理被观察数据的生命周期,用 std::weak_ptr 持有观察者对它的弱引用——这是避免循环引用和悬挂回调的唯一稳妥方式。
常见错误是直接传 std::shared_ptr<t></t> 给观察者,导致观察者强持有数据,数据永远无法析构;或者用原始指针,在数据释放后回调仍被触发,引发 segmentation fault。
- 被观察对象(如
ObservableValue<int></int>)内部用std::shared_ptr<int></int>存值 - 每个观察者注册时,只保存一个
std::weak_ptr<int></int>用于后续比对是否还有效 - 触发回调前,先调用
lock()尝试升级为std::shared_ptr,失败则跳过该观察者
如何让 set() 触发所有有效观察者回调
关键不在“存值”,而在“通知”。每次 set() 必须比较新旧值是否真正变化(避免无意义触发),再遍历观察者列表并安全调用回调。注意:回调执行期间可能有人调用 unobserve(),所以不能用普通 vector::iterator 遍历。
示例中采用索引倒序遍历 + erase-remove 惯用法:
立即学习“C++免费学习笔记(深入)”;
for (int i = observers_.size() - 1; i >= 0; --i) {
auto& obs = observers_[i];
if (auto ptr = obs.value_.lock()) {
if (*ptr != *old_value_) {
obs.callback_(*ptr);
}
} else {
observers_.erase(observers_.begin() + i);
}
}- 必须用倒序遍历,否则
erase()后迭代器失效会导致越界 -
obs.value_.lock()返回空shared_ptr表示被观察对象已销毁,立即清理该观察项 - 值比较应基于语义相等(比如自定义类型需重载
operator==),而非地址比较
注册观察者时为什么要传 std::weak_ptr 而不是 this
很多初学者想在类内部直接用 this 注册观察,但 this 是裸指针,无法参与生命周期管理。一旦该对象提前析构,Observable 中残留的 this 指针就会变成悬垂指针。
正确做法是让观察者自身也由 shared_ptr 管理,并向 Observable 传递 weak_from_this():
struct Observer : std::enable_shared_from_this<Observer> {
void watch(ObservableValue<int>& obs) {
obs.observe(weak_from_this(), [](int v) { /* ... */ });
}
};- 观察者类必须继承
std::enable_shared_from_this - 注册时传
weak_from_this(),Observable内部保存其std::weak_ptr<observer></observer> - 回调触发前,先
lock()升级,失败说明观察者已析构,跳过调用 - 否则裸指针方案在多线程或复杂生命周期下几乎必然崩溃
ObservableValue 的线程安全性怎么取舍
加锁能保证多线程读写安全,但会显著拖慢高频更新场景(如动画帧数据)。大多数响应式需求发生在单线程(如主线程 UI 更新),强行加 std::mutex 反而掩盖了设计缺陷。
更务实的做法是明确约定:所有 set() 和 observe() 必须在同一线程调用。若需跨线程通知,应由用户自行把回调转发到目标线程(例如 Qt 的 QMetaObject::invokeMethod 或 libuv 的 uv_async_send)。
- 默认不加锁,文档里写清线程约束
- 若真需要并发安全,可提供模板特化版本,如
ObservableValueThreadSafe<int></int>,内部封装std::mutex和std::condition_variable - 不要在
set()里做跨线程派发——那属于业务逻辑,不应侵入基础类
最易被忽略的是:即使用了 std::shared_ptr,也不代表线程安全。多个线程同时调用 set() 仍会竞争修改内部值和观察者列表,这点必须由使用者兜底。


















