C++ / a working model

40 / 80   ·   C++11   ·   约 10 分钟

weak_ptr:打破环并安全取得临时拥有权

先记住这句话

weak_ptr 观察已有 shared_ptr 所有权组而不增加强引用数量,适合回指、订阅者和非拥有缓存。访问前直接 lock,成功得到的 shared_ptr 会保护这次使用的生存期;expired 检查本身不能预留对象。

本篇内容
  1. 强引用环为什么不会自动回收
  2. lock 合并判断与取得拥有权
  3. 弱观察者也需要生命周期设计
  4. 运行示例
  5. 动手练习

强引用环为什么不会自动回收

若 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 消失后对象可以销毁,迟到的回调则直接跳过。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录