如何在 C++20 中安全地实现自定义智能指针?

在 C++20 之前,实现自定义智能指针往往需要手动管理引用计数、线程安全以及异常安全。随着标准的演进,std::shared_ptrstd::unique_ptr 已经提供了非常完整的功能,通常不再需要自己实现。但在某些极端场景下,例如需要与旧库交互、对生命周期做特殊约束,或实现特殊资源管理策略,手写智能指针仍然有意义。下面给出一个基于 std::atomic 的线程安全引用计数实现,并说明关键点。

1. 基本结构

template <typename T>
class SharedPtr {
public:
    explicit SharedPtr(T* ptr = nullptr);
    SharedPtr(const SharedPtr& other);
    SharedPtr(SharedPtr&& other) noexcept;
    ~SharedPtr();

    SharedPtr& operator=(const SharedPtr& other);
    SharedPtr& operator=(SharedPtr&& other) noexcept;

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

    std::size_t use_count() const noexcept { return count_->load(); }

private:
    void release();

    T* ptr_;
    std::atomic<std::size_t>* count_;
};
  • ptr_ 存放实际指针。
  • count_ 指向一个原子计数器。计数器的生命周期与 SharedPtr 对象共享。

2. 构造与析构

template <typename T>
SharedPtr <T>::SharedPtr(T* ptr) : ptr_(ptr) {
    if (ptr) {
        count_ = new std::atomic<std::size_t>(1);
    } else {
        count_ = nullptr;
    }
}

template <typename T>
SharedPtr <T>::~SharedPtr() {
    release();
}
  • 直接裸指针构造时,计数器初始化为 1。
  • nullptr 时,计数器为 nullptr,表示空指针。

3. 拷贝构造

template <typename T>
SharedPtr <T>::SharedPtr(const SharedPtr& other)
    : ptr_(other.ptr_), count_(other.count_) {
    if (count_) {
        count_->fetch_add(1, std::memory_order_relaxed);
    }
}
  • 使用 fetch_add 递增计数。memory_order_relaxed 适用于计数器,因为引用计数本身不需要同步其他内存操作。

4. 移动构造

template <typename T>
SharedPtr <T>::SharedPtr(SharedPtr&& other) noexcept
    : ptr_(other.ptr_), count_(other.count_) {
    other.ptr_ = nullptr;
    other.count_ = nullptr;
}
  • 只转移指针和计数器,后者置空。

5. 赋值运算符

template <typename T>
SharedPtr <T>& SharedPtr<T>::operator=(const SharedPtr& other) {
    if (this != &other) {
        release();                     // 先释放自身
        ptr_ = other.ptr_;
        count_ = other.count_;
        if (count_) count_->fetch_add(1, std::memory_order_relaxed);
    }
    return *this;
}

template <typename T>
SharedPtr <T>& SharedPtr<T>::operator=(SharedPtr&& other) noexcept {
    if (this != &other) {
        release();
        ptr_ = other.ptr_;
        count_ = other.count_;
        other.ptr_ = nullptr;
        other.count_ = nullptr;
    }
    return *this;
}
  • 赋值前先释放旧资源,防止泄露。

6. 资源释放

template <typename T>
void SharedPtr <T>::release() {
    if (count_ && count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
        delete ptr_;
        delete count_;
    }
}
  • fetch_sub 返回递减前的值。若递减后为 0,则当前实例是最后一个持有者,需销毁对象和计数器。
  • 使用 memory_order_acq_rel 以确保析构顺序的可见性。

7. 访问运算符

template <typename T>
T& SharedPtr <T>::operator*() const noexcept {
    return *ptr_;
}

template <typename T>
T* SharedPtr <T>::operator->() const noexcept {
    return ptr_;
}
  • 简单地转发给内部指针。

8. 线程安全与异常安全

  • 引用计数本身是原子操作,线程安全。
  • 构造时若分配计数器失败(new 抛异常),对象已处于空状态,析构时不做任何操作,保证不泄露。
  • 赋值时先释放后获取,避免异常导致资源泄露。

9. 使用示例

int main() {
    SharedPtr <int> p1(new int(42));
    SharedPtr <int> p2 = p1;          // 计数 2
    {
        SharedPtr <int> p3(std::move(p1)); // 计数 2, p1 为空
        std::cout << *p3 << " 计数: " << p3.use_count() << '\n';
    } // p3 销毁,计数 1
    std::cout << *p2 << " 计数: " << p2.use_count() << '\n';
    return 0;
}

输出:

42 计数: 2
42 计数: 1

10. 进一步改进

  • 自定义删除器:在构造函数中加入模板参数 Deleter,类似 std::unique_ptr 的实现。
  • 弱引用:实现 `WeakPtr ` 与 `SharedPtr` 配合使用,解决循环引用。
  • 内存池:对计数器做对象池化,减少 new/delete 频繁开销。

结语

通过上述实现,已完成一个最小化、线程安全、异常安全的自定义 SharedPtr。虽然标准库已提供了成熟的 std::shared_ptr,但在需要自定义生命周期管理或与旧 API 集成时,手写智能指针仍然是可行且有用的方案。希望本文能帮助你在特定场景中快速搭建自己的智能指针。

发表评论