条款20.对于shared_ptr但有可能空悬的指针使用weak_ptr

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

如果有一个像std::shared_ptr的指针但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似std::shared_ptr但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个std::shared_ptr不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空指针就是指针指向的对象不再存在。这就是对std::weak_ptr最精确的描述。

std::weak_ptr不能解引用,也不能测试是否为空,因为std::weak_ptr不是一个独立的智能指针,而是std::shared_ptr的一种扩充

std::weak_ptr通常从std::shared_ptr上创建,当从std::shared_ptr上创建std::weak_ptr时两者指向相同的对象,但是std::weak_ptr不会影响所指对象的引用计数

auto spw = std::make_shared<Widget>();	 //spw构造完成后,指向的Widget的引用计数为1

...
std::weak_ptr<Widget> wpw(spw);		//wpw和spw指向同一个Widget

spw = nullptr;				//引用计数为0
							//Widget对象被析构
							//wpw空悬

std::weak_ptr的空悬,也被称作失效,可以直接测试

if(wpw.expired())	...	//若wpw不再指到任何对象

但是通常你期望的是检查std::weak_ptr是否已经失效,如果没有失效则访问其指向的对象。这做起来比较容易,因为缺少解引用操作,没有办法写这样的代码。即使有,将检查和解引用分开会引入竞态条件:在调用expired和解引用操作之间,另一个线程可能对指向的对象重新赋值或者析构,并由此造成对象已析构。这种情况下,你的解引用将会产生未定义行为。

你需要的是一个原子操作实现检查是否过期,如果没有过期就访问所指的对象。这可以通过从std::weak_ptr创建std::shared_ptr来实现,具体有两种形式可以从std::weak_ptr上创建std::shared_ptr,具体用哪种取决于std::weak_ptr过期时你希望std::shared_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类型的异常

考虑一个工厂函数,该函数基于唯一ID来创建一个指向只读对象的智能指针。针对条款18中针对工厂函数返回类型的建议,它返回一个std::unique_ptr

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

如果调用loadWidget是个昂贵的操作(例如,因为其执行了文件或数据库的IO操作),并且ID被频繁的重复使用的话,一个合理优化是撰写一个能够完成loadWidget的工作,但又能缓存结果的函数。然而用缓存所有用过的Widget造成缓存拥塞,可能本身就会引起性能问题,因此另一个合理的优化就是在缓存的Widget不再有用时将其删除。

对于这个带缓存的工厂函数而言,返回std::unique_ptr类型不合适。调用者应该获取指向缓存对象的智能指针,调用者也当然应该决定这些对象的生存期,然后缓存管理器也需要一个指向到这些对象的指针。缓存管理器的指针需要能够校验它们何时会空悬,因为工厂函数的用户用完由工厂函数返回的对象后,该对象就将被析构,此时相应的缓存条目将会空悬。因此,应该缓存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();		//objPtr的类型是shared_ptr
    									//指向缓存的对象
    									//如果不在缓存中,则返回空指针
    
    if(!objPtr)							//如果对象不再缓存中
    {									
        objPtr = loadWidget(id);		//则加载并缓存
        cache[id] = objPtr;
    }
    return objPtr;
}

第二种用例:观察者设计模式。该模式的主要组件是主题(可以改变状态的对象)和观察者(对象状态发生改变后通知的对象)。在多数实现中,每个主题包含了一个数据成员,该成员持有指向其观察者的指针,这使得主题能够很容易地在其发生状态改变时发出通知。主题不会控制其观察者的生存期,但需要确认的话当一个观察者被析构以后,主题不会去访问它。一种合理的设计就是让每个主题持有一个容器来防止指向其观察者的std::weak_ptr,以便注意在使用某个指针之前,能够先确定它是否空悬。

冠以std::weak_ptr的使用场景,再举最后一个例子。考虑一个包含A,B,C三个对象的数据结构,A和C共享B的所有权,因此各自持有一个指向B的std::shared_ptr

为了方便使用,假设有一个指针从B指向A,那么该指针应该使用何种类型?

选择有三:

  • 裸指针:在此情况下,若A被析构,而C仍然指向B,B将保存着指向A的空悬指针。B却检测不出来,所以B有可能无意中去解引用这个空悬指针,就会产生未定义行为。
  • std::shared_ptr。这种设计中,A和B相互保存着指向对方的shared_ptr,这种std::shared_ptr环路(A指向B且B指向A)阻止了A和B被析构,即使程序的其他数据结构已经不能再访问A和B(例如,C不再指向B),两者也会保持着彼此的引用计数为一,这样,实际上A和B已经发生了内存泄漏,因为程序已无法访问A和B,但是它们的资源得不到回收。
  • std::weak_ptr。这避免了上述的两个问题。假如A被析构,那么B的回指指针将会空悬,但是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_ptrstd::shared_ptr从本质上来说是一致的std::weak_ptr的对象和std::shared_ptr对象的尺寸相同,它们和std::shared_ptr使用同样的控制块,其构造,析构,复制操作都包含了对引用计数的原子操作。这种说法可能令你惊讶,因为在本条款开头我曾提到,std::weak_ptr不参与引用计数操作。我说的是std::weak_ptr不干涉对象共享所有权,因此不会影响所指向的对象的引用计数。实际上控制块里面还有第二个引用计数,std::weak_ptr操作的就是第二个引用计数。

要点速记

  • 使用std::weak_ptr来代替可能空悬的std::shared_ptr
  • std::weak_ptr可能的用武之地包括缓存,观察者列表,以及避免std::shared_ptr指针环路
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值