40 / 80 · C++11 · 约 10 分钟
weak_ptr:打破环并安全取得临时拥有权
weak_ptr 观察已有 shared_ptr 所有权组而不增加强引用数量,适合回指、订阅者和非拥有缓存。访问前直接 lock,成功得到的 shared_ptr 会保护这次使用的生存期;expired 检查本身不能预留对象。
强引用环为什么不会自动回收
若 A 通过 shared_ptr 拥有 B,而 B 也通过 shared_ptr 拥有 A,外部句柄都消失后,两者仍各自拥有一个强拥有者。引用计数并不是追踪式垃圾回收,不会发现这组对象整体已与业务根节点断开,所以不会自动打破这个环。
先确定业务上哪条边拥有对象,哪条边只是回看。示例父节点共享拥有孩子,孩子用 weak_ptr 观察父节点。释放外部父拥有者后,父析构释放其孩子句柄;即使外部仍持有孩子,孩子的弱回指也不会强行留住父节点。
lock 合并判断与取得拥有权
weak_ptr 不能直接解引用。auto p = weak.lock() 原子地尝试取得同一所有权组的强引用;若对象已过期,得到空 shared_ptr。把 p 保存为局部变量,并在非空分支中使用,就把目标的生存期保护覆盖到整个使用区间。
先检查 expired 再使用另存的裸地址,存在检查后对象立即销毁的时间窗口。expired 适合显示状态或清理缓存,不适合证明下一步访问安全。lock 解决的是生存期竞争,不同步对象字段,也不能让对同一个 weak_ptr 变量的任意并发写入变安全。
弱观察者也需要生命周期设计
弱缓存只保存 weak_ptr 时,最后一个真正的拥有者消失后缓存项就过期;下一次请求必须接受重新创建。若缓存的目的就是强制保留对象,则应使用有容量与淘汰策略的拥有型缓存,而不是误把弱缓存当永久存储。
示例在临时锁定父节点时访问对象,随后销毁这份临时拥有权,再释放父节点。assert 展示父已销毁而孩子仍存活。weak_ptr 可以继续存在并报告过期,此时没有可用的父对象,但控制块相关存储可能仍需保留直到弱观察者也消失。
容易答错的地方
- 把回指改为 weak_ptr 只有在所有强引用环被切断时才有效;lambda 按值捕获 shared_ptr 也可能形成隐藏的环。
- 每次 lock 都得到一个新的临时强拥有者;应保存一次结果,不要第一次检查非空后又对第二次 lock 的结果直接解引用。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <memory>
struct Node {
static int alive;
std::shared_ptr<Node> child;
std::weak_ptr<Node> parent;
Node() { ++alive; }
~Node() { --alive; }
};
int Node::alive = 0;
int main() {
auto parent = std::make_shared<Node>();
auto child = std::make_shared<Node>();
parent->child = child;
child->parent = parent;
{
auto locked = child->parent.lock();
assert(locked && locked->child == child);
}
parent.reset();
assert(Node::alive == 1);
assert(child->parent.expired());
assert(!child->parent.lock());
child.reset();
assert(Node::alive == 0);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-weak-ptr.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
一个对象保存回调,回调又按值捕获该对象的 shared_ptr。怎样改写这份所有权关系?
查看参考答案
在建立回调前从 shared_ptr 构造 weak_ptr,让回调捕获 weak_ptr。执行回调时使用 if (auto self = weak.lock()),只在成功分支访问对象。这样保存回调不再增加对象的强拥有者,外部最后一份 shared_ptr 消失后对象可以销毁,迟到的回调则直接跳过。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。