Effective Modern C++ 条款20 把std::weak_ptr当作类似std::shared_ptr的、可空悬的指针使用

把std::weak_ptr当作类似std::shared_ptr的、可空悬的指针使用

让人觉得矛盾的是,我们可以很方便地创造一个行为类似std::shared_ptr的指针,但它不参与指向资源的共享所有权管理。换句话说,这个类似std::shared_ptr的智能指针不会影响对象的引用计数。这种指针不得不与对std::shared_ptr无效的问题做斗争:指向的资源可能已被销毁。这真正的智能指针能够追踪它什么时候空悬(dangle,即指向的对象不再存在),从而解决这种问题。这个指针就是std::weak_ptr

你可能奇怪std::weak_ptr有什么用途,当看std::weak_ptr的API候你觉得更奇怪,它看起来一点都不智能。std::weak_ptr不能被解引用,也不能检测是否为空。这是因为std::weak_ptr不是一个单独使用的智能指针,它要和std::shared_ptr搭配使用。

std::weak_ptrstd::shared_ptr之间的羁绊从它诞生时便存在了,std::weak_ptr通常是由std::shared_ptr中创建而来。它们指向的地方与初始化它们的std::shared_ptr指向的地方相同,但它们不会影响指向对象的引用计数:

auto spw = std::make_shared<Widget>();  // spw是std::shared_ptr<Widget>
                                        // 引用计数为1
...
std::weak_ptr<Widget> wpw(spw); // wpw指向spw指向的Widget,引用计数仍然为1
...
spw = nullptr;    // 引用计数变成0,Widget被销毁,wpw现在变成空悬指针

空悬的std::weak_ptr被称作是已经过期(expired)的,你可以直接检查它,
if (wpw.expired()) ... // 如果wpw指向的不是一个对象

不过一般状况是,你去检查std::weak_ptr是否过期,如果没有过期(即不是空悬),就要取得它指向的对象。这想的比做的简单,因为std::weak_ptr没有解引用操作,所以没有办法写出解引用的代码。就算有这个操作,单独的检查操作和解引用操作会引出一个竞争条件:在调用检查操作和解引用操作之间,另一个线程重赋值或销毁最后一个指向对象的std::shared_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就为空:

std::shared_ptr<Widget> spw1 = wpw.lock();  // 如果wpw过期了,spw1为空
                                                  `
auto spw2 = wpw.lock();    // 效果和上面相同, 不过使用了auto

另一种形式是使用接受std::weak_ptr为参数的std::shared_ptr构造函数,这种情况呢,如果std::weak_ptr过期了,会抛出异常:
std::shared_ptr<Widget> spw3(wpw); // 如果wpw过期,抛出std::bad_weak_ptr


不过,你可能还是不知道std::weak_ptr有什么用途。考虑一个基于唯一ID,生产指向只读对象的智能指针的工厂函数,根据条款18关于工厂函数返回类型的建议,这函数返回std::unique_ptr
std::unique_ptr<const Widget> loadWidget(WidgetId id);

如果loadWidget是一个昂贵的函数调用(例如,需要与文件或数据库进行交互),然后使用重复的ID的常见的,那么一种合理的优化是:写一个函数做loadWidget做的事情,不过把结果缓存起来。把请求的每种Widget缓存起来到导致性能问题,不过,另一种优化是把缓存中不再使用的Widget销毁。

对于这种带缓存的工厂函数,返回std::unique_ptr不好,调用者收到的应该是指向缓存对象的智能指针,并且由调用者来决定这些缓存对象的生命期,不过缓存也需要一个指向这些对象的指针。缓存里的指针空悬时,它要有能力发现,因为工厂的用户不再使用工厂返回的对象时,对象会被销毁,然后相应的缓存记录将会空悬。因此,缓存的指针应该是std::weak_ptr——有能力发现是否空悬。这意味着工厂的返回类型是std::shared_ptr,因为std::weak_ptr能够发现指向某对象的指针空悬,只能是——由std::shared_ptr管理生命期的对象。

这是带缓存版本的loadWidget的快速而粗糙的实现:

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是指向缓存对象的shared_ptr(否则为空)

    if (!object) {                    // 如果不在缓存中
        objPtr = loadWidget(id);      // 加载它
        cache[id] = objPtr;           // 缓存它
    }
    return object;
}

这个实现使用了C++11的哈希表容器(std::unordered_map),尽管它没有展示WidgetID的哈希和解决冲突的函数。

fastloadWidget的实现忽略了缓存(即哈希表)中可能会积累不再使用的Widget(因此被销毁)的std::weak_ptr指针。我们可以重新定义fastloadWidget,但是与其花费时间在无关std::weak_ptr的问题上,不如考虑std::weak_ptr的第二个用途:Observer设计模式。这个设计模式的主要组成是subject(主题,即状态可能改变的对象)和observer(观察者,即出现状态改变时被通知的对象)。在大多数状态中,每个主题包含指向观察者的指针作为成员变量,这让主题在状态改变时很容易通知观察者。主题没有兴趣控制观察者的生命期(即它们什么时候被销毁),但是有极大的兴趣在——确保如果一个观察者被销毁,随后主题不会试图使用它。一个合理的设计是让每个主题持有一个元素为std::weak_ptr的容器,std::weak_ptr指向主题的每个观察者,因此主题在使用观察者之前可以查看指针是否空悬。


作为std::weak_ptr的最后一个例子,考虑一个包含对象A、B、C的数据结构,并且A和C共享B的所有权,因此A和C都持有指向B的std::shared_ptr

这里写图片描述

假如B对象含有个指向A的指针也是有用的,那么我们应该使用哪种指针呢:

这里写图片描述

这里有三个选择:

  • 原生指针。用这个方式的话,如果A被销毁,但C仍会指向B,B中的指向A的指针会空悬,而B没有能力发现,因此B可能会不经意地解引用空悬指针,从而产生未定义行为。
  • std::shared_ptr。这样设计的话,A和B都含有指向彼此的std::shared_ptrstd::shared_ptr循环(A指向B,B指向A)的结果会导致A和B都不会被销毁。即使其他数据结构不能取得A和B(例如,因为C不再指向B),A和B还是有引用计数,其值为1。如果真的是这样,从实用的目的上看,A和B都泄漏了:程序不可能使用它们,它们的资源又无法回收。
  • std::weak_ptr。这能避免上面所说的问题。如果A被销毁了,B的指针能知道它已经过期了。而且,尽管A和B在互指,B的指针也不会影响A对象的引用计数,因此当没有std::shared_ptr指向A时,A就会被销毁。

使用std::weak_ptr明显是最好的选择。不过,值得注意的是需要用std::weak_ptr打破可能的std::shared_ptr循环是不常见的。在严格分层的数据结构,例如树,子结点通常只属于父结点,当父结点被销毁,它的子结点也应该被销毁。因此通常情况下,父结点指向子结点的指针最好是std::unique_ptr,而子结点指向父结点的指针可以用原生指针安全实现,因为子结点的生命期决不会比父结点长,所以不必担心子结点解引用空悬的父指针。

当然啦,不是所有基于指针的数据结构都严格分层,在那些例子中,就像缓存和观察者链表的情况一样,知道std::weak_ptr——随时可以投入使用——也是很好的。

从效率的角度,std::weak_ptr的开销和std::shared_ptr差不多,**std::weak_ptr对象的大小与std::shared_ptr对象相同,它们使用者与shared_ptr的一样的控制块(看条款19),也带有涉及引用计数操作的构造函数、析构函数、赋值操作。这可能让你很惊讶,因为一开始我就说std::weak_ptr不参与引用计数。其实我不是这样写的。我写的是std::weak_ptr不参与指向对象的共享所有权管理,因此不影响指向对象的引用计数。实际上有第二个引用计数在控制块中(即控制块不止一个引用计数),而std::weak_ptr管理的就是这个引用计数。详细细节看条款21。

总结

需要记住的2点:

  • std::weak_ptr当作类似std::shared_ptr的、可空悬的指针使用。
  • std::weak_ptr的潜在用途包含缓存,观察者链表,防止std::shared_ptr循环。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值