1 std::weak_ptr
- std::weak_ptr是对 std::shared_ptr的增强,不能被解引用,不能测试是否为空,不会影响所指向对象的引用计数:
cpp
auto spw = std::make_shared<Widget>(); // 引用计数=1
...
std::weak_ptr<Widget> wpw(spw); // 引用计数=1
...
spw = nullptr; // 引用计数=0,spw变为空悬
if (wpw.expired()) ... // 如果 wpw 没有指向任何对象
- 但通常你希望检查 std::weak_ptr是否有效,如果它不是悬挂的,则访问它指向的对象。
1)因为 std::weak_ptr缺乏解引用操作,所以无法编写这样的代码。
2)需要原子操作,检查 std::weak_ptr是否有效, 有效则从 std::weak_ptr创建一个 std::shared_ptr来进行访问。
cpp
std::shared_ptr<Widget> spw1 = wpw.lock(); // 如果 wpw 无效, spw1 将会是空
auto spw2 = wpw.lock(); // 同上
- std::shared_ptr可以使用std::weak_ptr作为参数进行构造,如果std::weak_ptr已经无效,就会抛出一个异常:
cpp
std::shared_ptr<Widget> spw3(wpw); // 如果 wpw 无效, 则抛出 std::bad_weak_ptr
- 对于根据 ID 生成只读对象的工厂函数,其调用可能很昂贵,需要缓存,并在不再使用时销毁缓存的 Widget:
cpp
std::unique_ptr<const Widget> loadWidget(WidgetID id);
1)调用者当然应该获得对象的智能指针,并且确定这些对象的生命周期,但缓存也需要指向这些对象的指针。
2)缓存的指针需要能够检测到它们悬空。因此,缓存的指针应该是std::weak_ptrs。
3)这意味着工厂的返回类型应该是std::shared_ptr。
cpp
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id){
static std::unordered_map< WidgetID,std::weak_ptr<const Widget> > cache;
auto objPtr = cache[id].lock(); // objPtr是缓存对象的std::shared_ptr(或为空指针)
if (!objPtr) { // 如果不在缓存中,
objPtr = loadWidget(id); // 加载它
cache[id] = objPtr; // 缓存它
}
return objPtr;
}
2 std::weak_ptr使用
- "观察者设计模式"是std::weak_ptrs的另一个用例。
1)该模式的主要组成部分是主题(状态可能发生变化的对象)和观察者。主题只关注观察者是否已经被销毁。
2)每个主题持有一个元素为观察者的std::weak_ptr的容器,从而使主题能够在使用指针之前确定它是否悬空。 - 最后一个std::weak_ptr的用例:
考虑一个包含对象 A、B 和 C 的数据结构,其中 A 和 C 共享 B 的所有权,因此持有对 B 的std::shared_ptr,假设从 B 到 A 也有一个指针,该如何设计?

1)raw pointer:如果 A 被销毁,C 仍然指向 B,B 将包含一个指向 A 的悬空指针。
2)std::shared_ptr:A 和 B 都包含对彼此的 std::shared_ptr。产生的 std::shared_ptr 循环(A 指向 B,B 指向 A)将阻止 A 和 B 被销毁。
3)std::weak_ptr:这避免了上述两个问题。如果 A 被销毁,B 指向它的指针将悬空,但 B 能够检测到这一点。此外,尽管 A 和 B 会相互指向,但 B 的指针不会影响 A 的引用计数。 - std::weak_ptr 对象的大小与 std::shared_ptr 对象相同,它们都使用相同的控制块。std::weak_ptr 不参与对象的共享所有权,因此不会影响所指向对象的引用计数。但是,std::weak_ptr 确实会操纵控制块中的第二个引用计数,用于检测所指向的对象是否已经被销毁或过期。
3 要点速记
- 对于类似std::shared_ptr的可能会悬挂的指针,请使用std::weak_ptr。
- std::weak_ptr的潜在用例包括缓存、观察者列表以及防止std::shared_ptr循环。