C++ 中自定义智能指针的实现细节与实践

在现代 C++ 开发中,智能指针(如 std::shared_ptr、std::unique_ptr)已经成为管理资源的核心工具。然而,在一些特殊场景下,标准库提供的智能指针可能不满足需求,或者需要更细粒度的控制。这时,自定义智能指针显得尤为重要。本文将从实现思路、关键技术点以及典型应用场景三个维度,详细剖析如何在 C++ 中实现一个自定义智能指针。

1. 为什么要自定义智能指针?

  1. 自定义引用计数策略
    标准的 std::shared_ptr 使用原子引用计数,线程安全,但在单线程或轻量级场景下会带来不必要的开销。可以实现一个非原子计数器,或使用读写锁优化。

  2. 延迟销毁或回收
    某些对象需要延迟销毁,例如需要在特定时间点或条件下回收。自定义智能指针可以包装一个回调或生命周期管理器,满足此需求。

  3. 多重资源管理
    例如一个对象同时拥有文件句柄、网络连接和内存,想要一次性管理。可以在智能指针中统一处理所有资源的释放。

  4. 安全性与可测性
    标准智能指针不允许自定义拷贝构造时的行为。自定义指针可以提供更细粒度的控制,例如在拷贝时执行特定日志或监控。

2. 关键技术实现

下面给出一个简化版的 MySharedPtr,实现基本的引用计数和自定义销毁策略。

#include <atomic>
#include <memory>
#include <utility>
#include <iostream>

template <typename T>
class MySharedPtr {
public:
    // 默认构造
    MySharedPtr() noexcept : ptr_(nullptr), ref_count_(nullptr) {}

    // 从裸指针构造
    explicit MySharedPtr(T* ptr) : ptr_(ptr) {
        ref_count_ = new std::atomic <size_t>(1);
    }

    // 拷贝构造
    MySharedPtr(const MySharedPtr& other) noexcept
        : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        if (ref_count_) {
            ref_count_->fetch_add(1, std::memory_order_relaxed);
        }
    }

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

    // 拷贝赋值
    MySharedPtr& operator=(const MySharedPtr& other) noexcept {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            if (ref_count_) {
                ref_count_->fetch_add(1, std::memory_order_relaxed);
            }
        }
        return *this;
    }

    // 移动赋值
    MySharedPtr& operator=(MySharedPtr&& other) noexcept {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            other.ptr_ = nullptr;
            other.ref_count_ = nullptr;
        }
        return *this;
    }

    // 析构
    ~MySharedPtr() {
        release();
    }

    // 访问对象
    T* get() const noexcept { return ptr_; }
    T& operator*() const noexcept { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }

    // 用自定义销毁器
    template<typename Deleter>
    void set_deleter(Deleter deleter) noexcept {
        // 这里实现自定义 deleter 逻辑
        // 例如存储 deleter 并在 release 时调用
        // 省略实现细节
    }

private:
    void release() noexcept {
        if (ref_count_ && ref_count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
            delete ptr_;
            delete ref_count_;
        }
    }

    T* ptr_;
    std::atomic <size_t>* ref_count_;
};

2.1 线程安全性考虑

  • 原子计数:使用 `std::atomic `,保证在多线程下拷贝、析构的安全。
  • 内存序fetch_add 使用 memory_order_relaxed,因为这里只需要计数的原子性。fetch_sub 使用 memory_order_acq_rel,保证在最后一次释放时同步。

2.2 自定义销毁器

如果想让 MySharedPtr 支持自定义销毁器,可以将 ref_count_ 换成一个结构体,包含计数和 deleter。实现方式类似于 std::shared_ptr 的控制块。

struct ControlBlock {
    std::atomic <size_t> ref_count;
    std::function<void(T*)> deleter;
    ControlBlock(T* p, std::function<void(T*)> d)
        : ref_count(1), deleter(std::move(d)) {}
};

然后在 release 时调用 deleter(ptr_)

2.3 与 RAII 的结合

自定义智能指针天然符合 RAII 原则,资源在离开作用域时自动释放。可以结合 std::optionalstd::variant 等高级类型进一步扩展。

3. 典型使用场景

3.1 资源池化

在高性能游戏或服务器中,频繁的 new/delete 会导致碎片。可以用自定义智能指针包装对象池,例如:

class ObjectPool {
public:
    T* acquire();
    void release(T* ptr);
};

template<typename T>
class PoolPtr {
    T* ptr_;
    ObjectPool* pool_;
public:
    PoolPtr(T* p, ObjectPool* pool) : ptr_(p), pool_(pool) {}
    ~PoolPtr() { if (ptr_) pool_->release(ptr_); }
    // ... 访问语法
};

3.2 线程局部缓存

有时需要在每个线程中存放一个共享资源的副本。可以在 thread_local 变量中持有自定义智能指针,保证线程安全。

3.3 延迟释放的场景

例如图形渲染管线,某些纹理资源需要在帧结束后统一释放。可以用回调函数或事件队列在 release 时推迟到帧结束。

4. 性能比较

指标 std::shared_ptr MySharedPtr
引用计数存储 原子变量 原子变量
内存占用 控制块 + 指针 控制块 + 指针
构造/析构 2 次内存分配 1 次内存分配(如果自定义)
线程安全 原子操作 原子操作
可定制化 限制 高度可定制

在大多数业务场景下,使用标准库已足够;但当需要定制化资源管理或性能优化时,自定义智能指针能提供更好的灵活性。

5. 结语

自定义智能指针是一把双刃剑:它可以提供更细粒度的资源控制和性能优化,但也会增加代码复杂度与维护成本。建议在以下两种情况慎重使用:

  1. 标准库已满足需求时,优先使用 std::unique_ptr/std::shared_ptr
  2. 当业务场景确实需要自定义销毁策略、资源池化或特殊线程安全策略时,再考虑自定义实现。

通过对引用计数、线程安全、销毁器等核心细节的深入理解,你可以根据项目需求灵活选择合适的智能指针实现方式,从而让 C++ 代码既安全又高效。

发表评论