如何在C++中实现自定义智能指针的弱引用功能

在现代C++编程中,智能指针是管理动态资源的核心工具。标准库提供了std::shared_ptrstd::unique_ptrstd::weak_ptr三种主要类型,每种都有其适用场景。若想在项目中自定义智能指针,尤其是需要支持弱引用的场景,必须仔细设计计数机制和线程安全。以下是一种实现思路,涵盖了核心概念、关键代码示例以及常见陷阱。

1. 基本思路

  • 引用计数:使用一个单独的计数器对象(类似于std::shared_ptr的控制块)来记录强引用(strong_count)和弱引用(weak_count)。
  • 控制块:在控制块中存放被管理对象的指针、strong_countweak_count以及可选的自定义删除器。
  • 构造与析构MySharedPtr在构造时会增大strong_count,在析构时减小并在计数为0时销毁资源;MyWeakPtr仅操作weak_count,在释放时检查是否需要销毁控制块。
  • 升级MyWeakPtr::lock()尝试将弱引用升级为强引用,若资源已被释放则返回空指针。

2. 核心数据结构

template <typename T>
struct ControlBlock {
    T* ptr;                     // 被管理对象
    std::atomic <size_t> strong{1};  // 强引用计数
    std::atomic <size_t> weak{0};    // 弱引用计数
    std::function<void(T*)> deleter; // 可选自定义删除器

    ControlBlock(T* p, std::function<void(T*)> del = nullptr)
        : ptr(p), deleter(del) {}
};

使用std::atomic保证多线程环境下计数操作的原子性。若你确定在单线程中使用,可以直接用size_t

3. MySharedPtr实现

template <typename T>
class MySharedPtr {
public:
    explicit MySharedPtr(T* p = nullptr)
        : ctrl(p ? new ControlBlock <T>(p) : nullptr) {}

    // 复制构造
    MySharedPtr(const MySharedPtr& other) noexcept
        : ctrl(other.ctrl) {
        if (ctrl) ctrl->strong.fetch_add(1, std::memory_order_relaxed);
    }

    // 移动构造
    MySharedPtr(MySharedPtr&& other) noexcept
        : ctrl(other.ctrl) {
        other.ctrl = nullptr;
    }

    ~MySharedPtr() {
        release();
    }

    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            release();
            ctrl = other.ctrl;
            if (ctrl) ctrl->strong.fetch_add(1, std::memory_order_relaxed);
        }
        return *this;
    }

    T* operator->() const { return ctrl->ptr; }
    T& operator*() const { return *(ctrl->ptr); }
    explicit operator bool() const { return ctrl && ctrl->ptr; }

    size_t use_count() const {
        return ctrl ? ctrl->strong.load(std::memory_order_relaxed) : 0;
    }

private:
    ControlBlock <T>* ctrl;

    void release() {
        if (ctrl && ctrl->strong.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            // 资源释放
            if (ctrl->deleter)
                ctrl->deleter(ctrl->ptr);
            else
                delete ctrl->ptr;
            // 计数器已零,检查弱引用是否为零
            if (ctrl->weak.load(std::memory_order_acquire) == 0)
                delete ctrl;
        }
    }
};

4. MyWeakPtr实现

template <typename T>
class MyWeakPtr {
public:
    explicit MyWeakPtr(const MySharedPtr <T>& shared)
        : ctrl(shared.ctrl) {
        if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
    }

    MyWeakPtr(const MyWeakPtr& other) noexcept
        : ctrl(other.ctrl) {
        if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
    }

    MyWeakPtr(MyWeakPtr&& other) noexcept
        : ctrl(other.ctrl) {
        other.ctrl = nullptr;
    }

    ~MyWeakPtr() {
        release();
    }

    MyWeakPtr& operator=(const MyWeakPtr& other) {
        if (this != &other) {
            release();
            ctrl = other.ctrl;
            if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
        }
        return *this;
    }

    MySharedPtr <T> lock() const {
        if (ctrl && ctrl->strong.load(std::memory_order_acquire) > 0) {
            // 尝试提升计数
            ctrl->strong.fetch_add(1, std::memory_order_relaxed);
            return MySharedPtr <T>(ctrl);
        }
        return MySharedPtr <T>();
    }

    bool expired() const {
        return !ctrl || ctrl->strong.load(std::memory_order_acquire) == 0;
    }

private:
    ControlBlock <T>* ctrl;

    void release() {
        if (ctrl && ctrl->weak.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            if (ctrl->strong.load(std::memory_order_acquire) == 0)
                delete ctrl;
        }
    }
};

注意MySharedPtr内部没有直接提供构造函数接受控制块的版本。可以添加私有构造函数供lock()使用,或使用友元实现。

5. 常见陷阱与最佳实践

  1. 循环引用MySharedPtr相互持有导致计数永不归零。使用MyWeakPtr打破循环。
  2. 线程安全:计数器必须原子化;如果你还需要对ptr做读写同步,需进一步加锁或使用std::shared_mutex
  3. 自定义删除器:在构造MySharedPtr时传入std::function<void(T*)>,支持数组删除、资源回收池等。
  4. 异常安全:所有操作在异常抛出前已确保计数正确更新。使用std::atomicmemory_order_acq_rel能保证原子操作的完整性。
  5. 性能考虑std::shared_ptr的实现已高度优化。自定义实现可根据需求裁剪,例如不需要弱引用就省略相关字段。
  6. 内存泄漏:如果忘记删除控制块,可能导致内存泄漏。确保在计数为零时释放控制块。

6. 简单示例

int main() {
    MySharedPtr <int> sp1(new int(42));
    MyWeakPtr <int> wp(sp1);

    std::cout << "use_count: " << sp1.use_count() << '\n'; // 1

    if (auto sp2 = wp.lock()) {
        std::cout << "locked value: " << *sp2 << '\n';   // 42
        std::cout << "use_count after lock: " << sp2.use_count() << '\n'; // 2
    }

    sp1.~MySharedPtr(); // 手动析构
    std::cout << "expired? " << std::boolalpha << wp.expired() << '\n'; // true
}

通过上述实现,你可以获得一个与标准库功能相似但可自由扩展的自定义智能指针。根据项目需求,你可以进一步添加:

  • 对齐/内存池支持
  • 对象生命周期回调
  • 兼容std::allocator的内存管理

以上即为在C++中实现自定义智能指针弱引用功能的完整思路与关键代码。祝你编码愉快!

发表评论