EMCPP条款20:对于类似 std::shared_ptr 但有可能空悬的指针使用std::weak_ptr

YiQi 管理员

std::weak_ptr 并不是一种独立的智能指针,而是 std::shared_ptr 的一种扩充。这种关系在指针生成的时刻就已存在。std::weak_ptr 一般者是通过 std::shared_ptr 来创建的。当使用 std::shared_ptr 完成初始化 std::weak_ptr 的时刻,两者就指涉到了相同位置,但 std::weak_ptr 并不影响所指涉到的对象的引用计数:

1
2
3
4
5
6
7
// after spw is constructed, the pointed-to Widget's
// ref count (RC) is 1. (See Item 21 for info on std::make_shared.)
auto spw = std::make_shared<Widget>();
// wpw points to same Widget as spw. RC remains 1
std::weak_ptr<Widget> wpw(spw);
// RC goes to 0, and the Widget is destroyed. wpw now dangles
spw = nullptr;

std::weak_ptr 的空悬,也被称作失效(expired)。你需要的是一个原子操作来完成 std::weak_ptr 是否失效的校验,以及在未失效的条件下提供对所指涉到的对象的访问。这个操作可以通过由 std::weak_ptr 创建 std::shared_ptr 来实现。该操作有两种形式,选择哪一种取决于由 std::weak_ptr 来创建 std::shared_ptr 时若该 std::weak_ptr 失效,期望得到什么结果。

  • 一种形式是 std::weak_ptr::lock, 它返回一个 std::shared_ptr。如果 std::weak_ptr 已经失效,则 std::shared_ptr 为空:
    1
    2
    3
    4
    std::shared_ptr<Widget> spw1 = wpw.lock(); 
    // if wpw's expired, spw1 is null
    auto spw2 = wpw.lock();
    // same as above, but uses auto
  • 另一种形式是用 std::weak_ptr 作为实参来构造 std::shared_ptr。这样,如果 std::weak_ptr 失效的话,抛出异常:
    1
    2
    std::shared_ptr<Widget> spw3(wpw); 
    // if wpw's expired, throw std::bad_weak_ptr

考虑一个工厂函数,该函数基于唯一ID来创建一些指涉到只读对象的智能指针。构造函数成本高昂,需要缓存。缓存管理器的指针需要能够校验它们何时会空悬,因为工厂函数的用户用完由该工厂函数返回的对象后,该对象就将被析构,此时相应的缓存条目将会空悬。这里是带缓存的 loadWidget 的一个快速而粗糙的实现版本:

1
2
3
4
5
6
7
8
9
10
11
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 is std::shared_ptr to cached object
// (or null if object's not in cache)
if (!objPtr) { // if not in cache,
objPtr = loadWidget(id); // load it
cache[id] = objPtr; // cache it
}
return objPtr;
}

观察者设计模式 (Observer design pattern) 也会用到 std::weak_ptr,参见原文

要点速记

  • 使用 std::weak_ptr 来代替可能空悬的 std::shared_ptr
  • std::weak_ptr 可能的用武之地包括缓存,观察者列表,以及避免 std::shared_ptr 指针环路。
此页目录
EMCPP条款20:对于类似 std::shared_ptr 但有可能空悬的指针使用std::weak_ptr