浏览知识库目录

C++

手写 std::weak_ptr

实现非拥有观察者、强弱计数协作与无竞态提升的 lock,并用 weak_ptr 打破 shared_ptr 引用环。

手写 std::weak_ptr

实现非拥有观察者、强弱计数协作与无竞态提升的 lock,并用 weak_ptr 打破 shared_ptr 引用环。

本系列代码使用 C++20 和 oc::handmade 命名空间,目标是解释实现机制、复杂度和工程边界,不是替代标准库。智能指针管理生命周期,但不会自动解决业务对象的线程安全。


一、学习目标

  • 实现不延长对象生命周期的 weak_ptr
  • 使用 compare_exchange 实现不会从零复活对象的 lock
  • 理解对象销毁与控制块释放为何发生在不同时间

二、前置条件

完成 shared_ptr 篇,并能画出控制块中的强计数、显式弱计数与隐式弱引用。

Linux/macOS:

g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror smart_ptrs_test.cpp -o smart_ptrs_test
./smart_ptrs_test

Windows PowerShell:

cl /std:c++20 /W4 /WX /EHsc smart_ptrs_test.cpp
.\smart_ptrs_test.exe

三、问题与设计选择

每个 weak_ptr 增加显式弱计数,但不增加强计数。lock 只有在强计数非零时通过 CAS 增加强计数并采用该份所有权;当强计数已经为零时必须返回空指针。最后一个强所有者释放隐式弱引用,最后一个弱观察者释放控制块。

教学实现覆盖核心所有权语义、复制或移动、观察接口、异常回滚与主要辅助函数;不复刻标准库的完整转换构造矩阵、allocator 定制、enable_shared_from_this、owner 比较器和 ABI 细节。


四、内存布局与核心不变量

弱引用绝不能让强计数从 0 重新变为 1;对象只由最后一个强所有者销毁;控制块只在对象已销毁且所有显式弱观察者都离开后释放。

判断实现是否正确时,不只数对象指针,还要分别追踪“资源是否活着”“拥有者数量”和“控制块是否活着”。三者的生命周期边界并不相同。


五、核心实现

bool try_add_strong() noexcept {
    std::size_t observed = strong_.load(std::memory_order_acquire);
    while (observed != 0) {
        if (strong_.compare_exchange_weak(
                observed, observed + 1,
                std::memory_order_acquire,
                std::memory_order_relaxed)) {
            return true;
        }
    }
    return false;
}

shared_ptr<T> lock() const noexcept {
    if (control_ && control_->try_add_strong()) {
        return shared_ptr<T>(
            control_, pointer_, detail::adopt_strong);
    }
    return {};
}

这段代码展示最容易写错的状态转换。完整实现放在下一节,并与另外两种智能指针共用同一份 smart_ptrs.hpp


六、完整教学实现

namespace oc::handmade {

template<class T>
class weak_ptr {
public:
    constexpr weak_ptr() noexcept = default;

    weak_ptr(const shared_ptr<T>& owner) noexcept
        : pointer_(owner.pointer_), control_(owner.control_) {
        if (control_) control_->add_weak();
    }

    weak_ptr(const weak_ptr& other) noexcept
        : pointer_(other.pointer_), control_(other.control_) {
        if (control_) control_->add_weak();
    }

    weak_ptr(weak_ptr&& other) noexcept
        : pointer_(std::exchange(other.pointer_, nullptr)),
          control_(std::exchange(other.control_, nullptr)) {}

    ~weak_ptr() {
        if (control_) control_->release_weak();
    }

    weak_ptr& operator=(const weak_ptr& other) noexcept {
        weak_ptr(other).swap(*this);
        return *this;
    }

    weak_ptr& operator=(weak_ptr&& other) noexcept {
        weak_ptr(std::move(other)).swap(*this);
        return *this;
    }

    weak_ptr& operator=(const shared_ptr<T>& owner) noexcept {
        weak_ptr(owner).swap(*this);
        return *this;
    }

    void reset() noexcept { weak_ptr().swap(*this); }

    void swap(weak_ptr& other) noexcept {
        using std::swap;
        swap(pointer_, other.pointer_);
        swap(control_, other.control_);
    }

    [[nodiscard]] std::size_t use_count() const noexcept {
        return control_ ? control_->use_count() : 0;
    }
    [[nodiscard]] bool expired() const noexcept { return use_count() == 0; }

    [[nodiscard]] shared_ptr<T> lock() const noexcept {
        if (control_ && control_->try_add_strong()) {
            return shared_ptr<T>(control_, pointer_, detail::adopt_strong);
        }
        return {};
    }

private:
    template<class U>
    friend class shared_ptr;

    T* pointer_{};
    detail::control_block* control_{};
};

template<class T>
shared_ptr<T>::shared_ptr(const weak_ptr<T>& weak) {
    if (!weak.control_ || !weak.control_->try_add_strong()) {
        throw std::bad_weak_ptr();
    }
    pointer_ = weak.pointer_;
    control_ = weak.control_;
}

}  // namespace oc::handmade

统一测试覆盖移动、复制、数组、别名所有权、对象析构、过期观察者和失败提升;实现没有向 std 命名空间注入任何类型。


七、使用示例与输出

预期输出或状态:

对象存活时 lock 返回非空且强计数增加;最后一个强所有者销毁后 expired 为 true,lock 返回空,显式构造 shared_ptr 抛 bad_weak_ptr。

示例使用断言验证状态,因此成功时不打印额外文本。析构计数必须回到 0,才能说明资源和控制块路径已经闭合。


八、复杂度与失效规则

操作 复杂度 说明
从 shared_ptr 构造 O(1) 原子增加弱计数
复制/移动 weak_ptr O(1) 复制增加弱计数
use_count/expired O(1) 只读取强计数快照
lock O(1) 期望 CAS 竞争时可能重试
reset/析构 O(1) 最后弱引用释放控制块

智能指针本身没有容器迭代器;这里的“失效”指资源销毁后,裸指针、引用或观察者能否继续安全使用。任何从 get()operator*operator-> 取得的非拥有访问,都不能延长对象生命周期。


九、异常安全与资源管理

  • lock 把“检查未过期”和“增加强计数”合并为一个 CAS 循环,避免检查后对象立即销毁的竞态。
  • CAS 失败会把最新强计数写回 observed;只有 observed 非零才继续尝试。
  • 成功 CAS 已经取得一份强所有权,构造返回值时必须使用 adopt 标记,不能再次增加计数。
  • weak_ptr 保存的对象指针在过期后不可解引用,只能用于构造空结果或释放控制块引用。

析构和引用计数释放路径必须为 noexcept。用户 deleter 若从析构路径抛出,程序无法可靠恢复,因此教学实现要求 deleter 的调用和移动不抛异常。


十、常见错误

1. 先 expired 再解引用旧裸指针

检查与使用不是原子事务;正确做法是保存 lock() 返回的 shared_ptr。

2. 用普通 load 加 fetch_add 实现 lock

对象可能在两步之间被销毁,随后把已经为零的计数错误复活。

3. 双向关系全部使用 shared_ptr

强引用环让每个计数都无法归零;至少一条非拥有边应改为 weak_ptr。


十一、面试追问

  1. 为什么控制块的弱计数需要一个隐式弱引用?
  2. weak_ptr::lock 如何保证不会复活已经销毁的对象?
  3. expired() 返回 false 后为什么仍不能直接使用旧裸指针?
  4. 循环引用和内存泄漏之间的因果链是什么?

回答时先说明所有权,再画出对象与控制块,最后给出原子计数、异常和并发边界。


十二、练习与自测

  1. 实现 parent 使用 weak_ptr、children 使用 shared_ptr 的树节点
  2. 用两个线程分别 reset 与 lock,重复运行并开启 TSan
  3. 增加 weak_ptr 的派生类到基类转换构造

自测标准:能够不看代码画出每次复制、移动、reset、lock 和析构后的强弱计数,并解释最后由谁销毁对象、由谁释放控制块。


十三、官方资料与延伸阅读


上一篇:手写 std::shared_ptr | 下一篇:手写内存池