- 有时我们需要一种行为类似
std::shared_ptr
,但不影响目标对象引用计数的智能指针。这种指针需要处理一种对std::shared_ptr
来说不存在的问题:其指向的对象可能已被销毁。即本节主题std::weak_ptr
。 std::weak_ptr
通常由std::shared_ptr
创建,指向位置与其相同,不影响引用计数(实际上会影响控制块中的另一项弱引用计数,下节内容会涉及)。若只想检查指向对象有效性,调用weak_ptr::expired()
函数。若还想获得指向的对象,不能通过operator *
直接解引用,因为检查—解引用的两步操作可能导致竞争:某线程在检查有效后其它线程销毁了原shared_ptr
,最终该线程对悬空指针解引用。我们需要的是一个二合一的原子操作。标准C++的设计提供两种途径,它们的区别在于当weak_ptr
失效时的表现不同:(1)调用weak_ptr::lock()
函数,返回指向对象的shared_ptr
;如果weak_ptr
无效,返回为 null。(2)以weak_ptr
为参数调用shared_ptr
的构造函数;如果weak_ptr
无效,抛出std::bad_weak_ptr
异常。
auto spw = std::make_shared<Widget>(); // 关于 make_shared,见Item 21
std::weak_ptr<Widget> wpw(spw);
//spw.reset(); // 如果添加此行,以下构建会使spw1成为nullptr, spw2抛出异常
auto spw1 = wpw.lock();
if (spw1 == nullptr) {
...
}
else {
...
}
auto spw2 = std::shared_ptr<Widget>(wpw);
-
以下介绍
weak_ptr
的三个使用场景: -
场景1:缓存。假设要设计一个工厂函数,根据入参 id 返回一个只读对象。如果加载操作很费时(如涉及文件或数据库I/O),很自然会想到在函数中做一个 id 到对象的哈希表缓存。如果对象体积很大,一直保持所有对象的缓存也是不合理的。一种合理的设计是:工厂函数对外返回
shared_ptr
(允许外部调用者在使用完毕后能自动将其销毁);内部保存一份缓存,缓存需要能够检测是否已经失效,有效则直接返回,若失效则再次加载。一个简单的设计如下:
std::shared_ptr<const Widget> fastLoadWidget(int id)
{
static std::unordered_map<int, std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // 如果缓存有效,objPtr是shared_ptr,否则为null
if (!objPtr) {
objPtr = loadWidget(id); // 缓存失效,加载
cache[id] = objPtr;
}
return objPtr;
}
-
场景2:观察者设计模式。此设计模式中,被观察对象(Subjects)的状态随时可能改变,观察者(Observer)可以注册或取消注册对某被观察对象的关注。当被观察者的状态改变时,需要通知所有注册的观察者。被观察者通常需要以容器保存一组对当前关注的观察者的引用。它们不应该控制观察者的生命周期,但需要检测保存的观察者是否还有效,如果已经失效则后续不再试图通知它。此时
weak_ptr
是最合适的选择。 -
场景3。想象以下数据结构:A 和 C 对象共享 B 对象的所有权(持有
shared_ptr
),假设还需要一个从 B 到 A 的指针,应该选择那种?
三个选择:- 裸指针。如果用这种方法,当 A 被销毁而 C 继续指向 B 时,B 会包含一个指向 A 的悬空指针,而且 B 无法检测到这一点。
shared_ptr
。这种情况下 A 和 B 互相持有一个指向对方的shared_ptr
,导致循环引用问题,二者的引用计数永不为0(即使程序的其它部分不再使用它们),资源永远无法被释放。weak_ptr
。避免了以上问题。如果 A 被销毁,B 向 A 的指针会悬空,但可以检测到。B 向 A 的指针不会影响其引用计数,因此不会干扰 A 的释放。
总结
- 使用
std::weak_ptr
管理可能悬空的类似 std::shared_ptr 的指针 std::weak_ptr
的潜在使用场景包括缓存、观察者列表和避免std::shared_ptr
的循环引用。