如果需要某种智能指针能够像std::shared_ptr
一样方便,但又无需参与管理所指涉到的对象的共享所有权的话。就很好适合用std::weak_ptr
。
但这样的功能同样会带来一个问题。这种指针需要处理一个对std::shared_ptr
而言不是问题的问题:所指涉的对象有可能已经被析构。而std::weak_ptr
的确是可以判断所指向对象是否还存在。
std::weak_ptr
的用途
在看完std::weak_ptr
的API后,你可能会困惑,这东西不能取地址,也不能检查是否为空。这东西到底有什么用呢。其实这个东西需要配合std::shared_ptr
使用,相当于std::shared_ptr
的一种扩充。
用途示例,一般std::weak_ptr
由std::shared_ptr
创建:
auto spw = //完成spw构造后,指涉到Widget的引用计数为1
std::make_shared<Widget>();
std::weak_ptr<Widget> wpw(spw); //wpw和spw指向同一个Widget,引用计数保持为1
...
spw = nullptr; // 引用计数为0,Widget对象被析构,wpw悬空
if (wpw.expired()) ... // 若wpw不再指涉到任何对象,可以用来判断悬空
std::weak_ptr
的用法
通常的一个使用场景是,判断一个std::weak_ptr
是否已经失效,如果没有失效,就访问它所指涉的对象。
这个想法想起来容易,做起来难。由于std::weak_ptr
缺乏取地址接口。写不出这样的代码,即便能写出来,也会导致竞态风险。例如如下危险代码:
if (!wpw.expired())
auto p = *wpw; //没有取地址接口,并且这里会存在竞态风险
//判断的时候没失效,但是执行的时候可能失效了
所以上述场景需要一个原子操作来检验是否悬空和使用。std::weak_ptr
接口定义了这样的用法:
std::shared_ptr<Widget> spw1 = wpw.lock(); //若wpw悬空,则spw1为空
auto spw2 = wpw.lock(); //若wpw悬空,则spw2为空
//或者用下面的方式,与上面等效
std::shared_ptr<Widget> spw3(wpw);
std::weak_ptr
的使用场景1
考虑一个工厂函数,该函数基于唯一ID来创建一些指涉到只读对象的智能指针:
std::unique_ptr<const Widget> loadWidget(WidgetID id);
如果loadWidget成本高昂,并且ID会被频繁使用的话,一个合理的优化是,撰写一个能够完成loadWidget的工作,但又能缓存结果的函数。而缓存所有用过的Widget可能会引起性能问题,因此另一种合理的优化是,在缓存的Widget不再有人用的时候,及时将其删除。
那么对于这个场景看来,返回unique_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的型别是std::shared_ptr
//指涉到缓存的对象,如果不存在,则返回空指针
if (!objPtr) { //如果对象不再缓存中,
objPtr = loadWidget(id); //则加载
cache[id] = objPtr; //并缓存
}
return objPtr;
}
之所以说粗糙,是因为缓存里不用的指针会越来越多。这里如何改进的方式暂时不在本讲讨论范围内。
std::weak_ptr
的使用场景2
考虑这样一个使用场景,A,B,C三个对象的数据结构,A和C共享B的所有权,因此各持有一个指向B的std::shared_ptr
。
为了使用方便,假设有一个指针从B指向A,那么应该如何表示呢?
有三个选择:
-
裸指针: 在此情况下,A被析构,而C仍然指涉到B,B将保存着指涉到A的悬空指针。B却检查不出来,可能会产生未定义行为。
-
std::shared_ptr
:这种设计中,AB相互保存着指向对方的std::shared_ptr
。这种环路实际上已经无法释放,已经内存泄露了。
-
std::weak_ptr
:避免了上述两个问题,可以判空,不会产生循环引用。
但是这里需要指出:虽然std::weak_ptr
在这里可以使用,但是用std::weak_ptr
打破循环引用不是特别常见的做法。类似树结构的艳歌继承谱系中,子节点通常只被其父节点拥有,当父节点被析构后,子节点也应被析构。一般来说,这种严格的接口,可以用std::unique_ptr
实现父节点指向子节点,而子节点的反指向可以用裸指针安全实现,因为子节点的生命周期必定小于父节点。但不是严格的树结构就不能这么用了。
std::weak_ptr
效率分析
从效率上说,std::weak_ptr
和std::shared_ptr
是一致的。两个指向的对象是同一个,并且拥有相同的控制块,其构造,析构,赋值操作都包含了对引用计数的原子操作。
这么说可能令你惊讶,但的确如此。std::weak_ptr
不干涉共享对象所有权,因此不会影响所指涉对象的引用计数。但实际上控制块里还有第二个引用计数(弱引用计数)。更多细节请参看Item 21。
要点速记 |
1. 使用std::weak_ptr 来代替可能悬空的std::shared_ptr 。 |
2. std::weak_ptr 可能的用武之地包括:缓存, 观察者列表,以及避免std::shared_ptr 指针环路。 |