0CCh Blog

关于std::weak_ptr使用的理解

周末时候和朋友聊了一下C++的智能指针,突然发现虽然智能指针进入C++11标准库已经有十多年了,但是我们对部分细节的理解还是比较局限,以std::weak_ptr为例,很多人的理解只是停留在避免std::shared_ptr出现相互引用,导致对象无法析构,内存无法释放的问题。
当然,并不是说这种用法有什么不对,恰恰相反,它是一个非常经典的使用场景。但是std::weak_ptr的使用场景或者说它诞生的理念却不仅仅是这些,如果没有更加透彻理解std::weak_ptr,也很难合理的使用std::shared_ptr

std::weak_ptr从概念上,它是一个智能指针,相对于std::shared_ptr,它对于引用的对象是“弱引用”的关系。简单来说,它并不“拥有”对象本身。
如果我们去类比生活中的场景,那么它可以是一个房地产中介。房地产中介并不拥有房子,但是我们有办法找到注册过的房产资源。在客户想要买房子的时候,它起初并不知道房子是否已经卖出了,它需要找到房主询问后再答复客户。
std::weak_ptr做的事情几乎和房产中介是一模一样的。std::weak_ptr并不拥有对象,在另外一个std::shared_ptr想要拥有对象的时候,它并不能做决定,需要转化到一个std::shared_ptr后才能使用对象。所以std::weak_ptr只是一个“引路人”而已。

说了这么多,那么std::weak_ptr除了解决相互引用的问题,还能做什么?答案是:一切应该不具有对象所有权,又想安全访问对象的情况。
还是以互相引用的情况为例,通常的场景是:一个公司类可以拥有员工,那么这些员工就使用std::shared_ptr维护。另外有时候我们希望员工也能找到他的公司,所以也是用std::shared_ptr维护,这个时候问题就出来了。但是实际情况是,员工并不拥有公司,所以应该用std::weak_ptr来维护对公司的指针。
再举一个例子:我们要使用异步方式执行一系列的Task,并且Task执行完毕后获取最后的结果。所以发起Task的一方和异步执行Task的一方都需要拥有Task。但是有时候,我们还想去了解一个Task的执行状态,比如每10秒看看进度如何,这种时候也许我们会将Task放到一个链表中做监控。这里需要注意的是,这个监控链表并不应该拥有Task本身,放到链表中的Task的生命周期不应该被一个观察者修改。所以这个时候就需要用到std::weak_ptr来安全的访问Task对象了。

最后再来聊一个新手使用std::weak_ptr容易被坑的地方:对象资源竞争。以下代码在多线程程序中是存在很大风险的,因为wp.expired()wp.lock()运行的期间对象可能被释放:

// std::weak_ptr<SomeClass> wp{ sp };

if (!wp.expired()) {
wp.lock()->DoSomething();
}

正确的做法是:

auto sp = wp.lock();
if (sp) {
sp->DoSomething();
}

std::weak_ptrlock函数是一个原子操作。有趣的是,最开始的C++11标准是没有提到原子操作的,C++14标准才对这一点进行了补充,详细过程可以参考提案文档:LWG2316