Skip to content

Latest commit

 

History

History
63 lines (45 loc) · 3.49 KB

File metadata and controls

63 lines (45 loc) · 3.49 KB

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

std::weak_ptr 是对资源的弱引用,不影响 std::shared_ptr 的引用计数。

std::weak_ptr 一般通过 std::shared_ptr 来创建:

auto spw = std::make_shared<Widget>();
std::weak_ptr<Widget> wpw(spw);
spw = nullptr;  // spw引用计数为0
                // Widget对象被析构
                // wpw空悬

std::weak_ptr 的空悬,被称为“失效”,可以直接测试:

if (wpw.expired()) ...

通常想要的效果是:校验一个 std::weak_ptr 是否已经失效,如果尚未失效,就访问它指向的对象。由于 std::weak_ptr 不提供解引用操作,所以需要使用 std::weak_ptr::lock ,它返回一个 std::shared_ptr ,如果 std::weak_ptr 有效,则返回一个 std::shared_ptr, 否则返回一个空的 std::shared_ptr

std::shared_ptr<Widget> spw1 = wpw.lock();
auto spw2 = wow.lock();     // 同上

另一种形式是使用 std::weak_ptr 作为实参来构造 std::shared_ptr ,如果 std::weak_ptr 失效,则抛出异常:

std::shared_ptr<Widget> spw3(wpw);

考虑一个工厂函数,该函数基于唯一ID来创建一些指向只读对象的智能指针:

std::unique_ptr<const Widget> loadWidget(WidgetID id);

如果 loadWidget 成本高昂,并且ID被频繁重复使用的话,一个合理地优化是使用缓存。然而缓存所有用过的 Widget 造成缓存拥塞,因此另一个合理的优化是再缓存的 Widget 不再有用时将其删除。

工厂函数的用户用完该函数返回的对象后,该对象就该被析构,此时相应的缓存条目将会空悬。因此,应该缓存 std::weak_ptr ,一种可以检测空悬的指针。该工厂函数的返回值应为 std::shared_ptr ,因为只有当对象的生存期托管给 std::shared_ptr 时,std::weak_ptr 才能检测空悬。

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
    static std::unordered_map<WIdgetID,
                              std::weak_ptr<const Widget>> cache;

    auto objPtr = cache[id].lock();
    if (!objPtr) {
        objPtr = loadWidget(id);
        cache[id] = objPtr;
    }
    return objPtr;
}

上面的实现忽略了一个事实,由于相应的 Widget 不再使用,缓存中失效的 std::weak_ptr 可能会不断积累。

我们可以考虑观察者设计模式。该模式的主要组件是主题(subject,可以改变状态的对象)和观察者(observer,对象状态发生改变后通知的对象)。每个主题包含了一个数据成员,该成员持有指向其观察者的指针,使得主题能够很容易地在其发生状态改变时发出通知。主题不会控制其观察者的生存期,但需要确认当一个观察者被析构后,主题不会去访问它。一种合理的设计是让每个主题都有一个容器来放置其指向的观察者的 std::weak_ptr ,以便主题再使用某个指针前,能够确定它是否空悬。

使用 std::weak_ptr 来打破 std::shared_ptr 引起的循环引用不是特别常见的做法。在类似树这种严格继承谱系式的数据结构中,子节点通常只被其父节点拥有,当父节点被析构后,子节点应也被析构。因此,从父节点到子节点的链接可以用 std::unique_ptr 来表示,而由子节点到父节点的反向链接可以用裸指针安全实现。因为子节点的生存期不会比父节点的更长。