如何在C++20中实现自定义弱引用(Weak Pointer)?

在 C++ 标准库中,std::shared_ptrstd::weak_ptr 一起提供了对共享对象的自动内存管理。std::weak_ptr 通过弱引用避免了循环引用导致的内存泄漏,并在访问对象前可以检查对象是否已被销毁。本文将演示如何在 C++20 环境下手动实现一个最简洁的弱引用类 WeakPtr<T>,并说明其工作原理、使用场景以及如何与自定义引用计数结合。


1. 设计思路

核心目标是实现:

  1. 弱引用计数:记录还有多少 WeakPtr 指向同一对象。
  2. 共享计数:记录有多少 SharedPtr(或我们自定义的 SharedPtr)仍在持有对象。
  3. 自动销毁:当共享计数为 0 时,销毁对象;当弱计数为 0 时,释放内部结构。

实现方式:

  • 使用一个独立的 控制块(Control Block) 存放计数和对象指针。
  • `SharedPtr `(示例实现)持有指向控制块的指针并递增共享计数。
  • `WeakPtr ` 同样持有指向控制块的指针,但递增弱计数。
  • `WeakPtr ::lock()` 能在共享计数>0 时返回对应的 `SharedPtr`,否则返回 `nullptr`。

2. 代码实现

以下代码使用 C++20 特性(如 std::unique_ptrstd::addressof)实现了一个极简的弱指针。为保持重点清晰,示例中省略了异常安全和线程安全等细节。

#pragma once
#include <cstddef>
#include <memory>
#include <utility>
#include <atomic>
#include <iostream>

template<typename T>
struct ControlBlock
{
    std::atomic<std::size_t> strong{0};   // SharedPtr计数
    std::atomic<std::size_t> weak{0};     // WeakPtr计数
    T* ptr;                               // 被管理的对象

    explicit ControlBlock(T* p) : ptr(p) {}
};

template<typename T>
class SharedPtr;

template<typename T>
class WeakPtr
{
public:
    WeakPtr() noexcept : cb(nullptr) {}
    WeakPtr(const WeakPtr& other) noexcept : cb(other.cb)
    {
        if (cb) ++cb->weak;
    }
    WeakPtr(WeakPtr&& other) noexcept : cb(std::exchange(other.cb, nullptr)) {}

    explicit WeakPtr(SharedPtr <T> const& sp);

    ~WeakPtr()
    {
        if (cb && !--cb->weak && cb->strong == 0) {
            delete cb;
        }
    }

    WeakPtr& operator=(const WeakPtr& rhs) noexcept
    {
        if (this != &rhs) {
            WeakPtr tmp(rhs);
            std::swap(cb, tmp.cb);
        }
        return *this;
    }

    WeakPtr& operator=(WeakPtr&& rhs) noexcept
    {
        if (this != &rhs) {
            WeakPtr tmp(std::move(rhs));
            std::swap(cb, tmp.cb);
        }
        return *this;
    }

    SharedPtr <T> lock() const noexcept
    {
        if (cb && cb->strong > 0) {
            return SharedPtr <T>(*this);
        }
        return SharedPtr <T>();
    }

    bool expired() const noexcept
    {
        return !cb || cb->strong == 0;
    }

private:
    ControlBlock <T>* cb;
    friend class SharedPtr <T>;
};

template<typename T>
class SharedPtr
{
public:
    SharedPtr() noexcept : cb(nullptr), ptr(nullptr) {}

    explicit SharedPtr(T* p) : cb(nullptr), ptr(p)
    {
        if (p) {
            cb = new ControlBlock <T>(p);
            cb->strong = 1;
        }
    }

    SharedPtr(const SharedPtr& other) noexcept : cb(other.cb), ptr(other.ptr)
    {
        if (cb) ++cb->strong;
    }

    SharedPtr(SharedPtr&& other) noexcept
        : cb(std::exchange(other.cb, nullptr)), ptr(std::exchange(other.ptr, nullptr)) {}

    explicit SharedPtr(WeakPtr <T> const& wp) noexcept : cb(wp.cb), ptr(wp.cb ? wp.cb->ptr : nullptr)
    {
        if (cb && cb->strong > 0) {
            ++cb->strong;
        } else {
            cb = nullptr;
            ptr = nullptr;
        }
    }

    ~SharedPtr()
    {
        if (cb && !--cb->strong) {
            delete ptr;
            if (cb->weak == 0) delete cb;
        }
    }

    SharedPtr& operator=(const SharedPtr& rhs) noexcept
    {
        SharedPtr tmp(rhs);
        std::swap(cb, tmp.cb);
        std::swap(ptr, tmp.ptr);
        return *this;
    }

    SharedPtr& operator=(SharedPtr&& rhs) noexcept
    {
        SharedPtr tmp(std::move(rhs));
        std::swap(cb, tmp.cb);
        std::swap(ptr, tmp.ptr);
        return *this;
    }

    T* operator->() const noexcept { return ptr; }
    T& operator*()  const noexcept { return *ptr; }

    T* get() const noexcept { return ptr; }
    std::size_t use_count() const noexcept { return cb ? cb->strong : 0; }

private:
    ControlBlock <T>* cb;
    T* ptr;
};

template<typename T>
WeakPtr <T>::WeakPtr(SharedPtr<T> const& sp) noexcept : cb(sp.cb)
{
    if (cb) ++cb->weak;
}

关键点说明

  • 控制块:存放共享计数、弱计数和指向对象的裸指针。使用 std::atomic 保证多线程环境下计数的原子性。
  • SharedPtr
    • 构造时若传入裸指针,创建新的控制块并把共享计数设为1。
    • 拷贝构造/赋值时递增共享计数。
    • 销毁时递减共享计数;若变为0,删除对象;若弱计数也为0,删除控制块。
  • WeakPtr
    • 拷贝构造/赋值时递增弱计数。
    • 销毁时递减弱计数;若共享计数已为0且弱计数变为0,删除控制块。
    • lock() 用来尝试获取对应的 SharedPtr,只有当共享计数>0时才成功。

3. 使用示例

#include <iostream>

struct Foo {
    Foo() { std::cout << "Foo ctor\n"; }
    ~Foo() { std::cout << "Foo dtor\n"; }
    void greet() const { std::cout << "Hello from Foo!\n"; }
};

int main()
{
    SharedPtr <Foo> sp1(new Foo);          // sp1 用来管理对象
    {
        WeakPtr <Foo> wp = sp1;            // wp 形成弱引用
        if (!wp.expired()) {
            auto sp2 = wp.lock();        // 尝试获得共享指针
            sp2->greet();                // 输出问候
        }
    } // wp 作用域结束,WeakPtr 被销毁
    std::cout << "sp1 use_count: " << sp1.use_count() << '\n';
    return 0;
}

运行结果示例:

Foo ctor
Hello from Foo!
sp1 use_count: 1
Foo dtor

4. 常见问题与最佳实践

  1. 线程安全:上述实现已使用 std::atomic 计数,但未实现完整的线程安全,例如 WeakPtr::lock() 的检查-递增操作不是原子。实际项目中可使用 std::shared_ptr 的实现或加入互斥锁。

  2. 异常安全:构造控制块时若 new 抛出异常,未持有任何计数。若在构造过程中出现异常,需确保已析构。

  3. 自定义删除器:标准 std::shared_ptr 支持自定义 deleter。若需要同样功能,可在控制块中存储 std::function<void(T*)> 并在销毁时调用。

  4. 多继承与多层包装:若 T 是多重继承或包装类型,注意正确管理裸指针和控制块生命周期。


5. 结语

通过以上代码,你可以在 C++20 中手动实现一个基本的弱引用机制。它演示了控制块、计数管理以及弱指针与共享指针之间的协作方式。实际开发中,建议直接使用标准库 std::shared_ptrstd::weak_ptr,因为它们已被充分测试、优化并具备完整的异常安全与线程安全保证。若你对内存管理细节有兴趣,或者想构建自定义的引用计数系统,这份实现是一个很好的起点。

发表评论