《Effective Modern C++》学习笔记 - Item 20: 使用 std::weak_ptr 管理可能悬空的类似 std::shared_ptr 的指针

  • 有时我们需要一种行为类似 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 的释放。

总结

  1. 使用 std::weak_ptr 管理可能悬空的类似 std::shared_ptr 的指针
  2. std::weak_ptr 的潜在使用场景包括缓存、观察者列表和避免 std::shared_ptr 的循环引用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值