实现自定义智能指针的细节与最佳实践

在 C++ 中,智能指针是管理资源的重要工具。虽然 std::unique_ptrstd::shared_ptr 等已经为我们提供了常用的实现,但在某些特殊场景下,我们可能需要一个定制化的智能指针。本文将从设计思路、关键实现细节以及常见误区三个方面,系统阐述如何实现一个既安全又高效的自定义智能指针。

一、设计目标

  1. 所有权语义:支持独占(类似 unique_ptr)与共享(类似 shared_ptr)两种所有权模式,且可以在编译期选择。
  2. 资源回收:使用 RAII 原则,确保对象生命周期结束时自动释放资源。
  3. 线程安全:在共享模式下,计数器的增减必须原子化。
  4. 可扩展性:支持自定义 deleter、可与 STL 容器配合使用。

二、核心实现

2.1 基础模板

template <typename T, bool Shared = false>
class SmartPtr;
  • Shared 控制所有权模式。若为 false,实现独占指针;若为 true,实现共享指针。

2.2 独占指针实现

template <typename T>
class SmartPtr<T, false> {
private:
    T* ptr_;
public:
    explicit SmartPtr(T* p = nullptr) noexcept : ptr_(p) {}
    SmartPtr(const SmartPtr&) = delete;
    SmartPtr& operator=(const SmartPtr&) = delete;

    SmartPtr(SmartPtr&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }
    SmartPtr& operator=(SmartPtr&& other) noexcept {
        reset();
        ptr_ = other.ptr_;
        other.ptr_ = nullptr;
        return *this;
    }

    ~SmartPtr() { reset(); }

    T& operator*() const noexcept { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }
    T* get() const noexcept { return ptr_; }
    explicit operator bool() const noexcept { return ptr_ != nullptr; }

    void reset(T* p = nullptr) noexcept {
        if (ptr_) delete ptr_;
        ptr_ = p;
    }
};

2.3 共享指针实现

共享指针需要一个控制块来维护引用计数与 deleter。实现简化示例:

template <typename T>
class SmartPtr<T, true> {
private:
    struct ControlBlock {
        std::atomic <size_t> count{1};
        T* ptr;
        void (*deleter)(T*) = [](T* p){ delete p; };

        explicit ControlBlock(T* p, void(*d)(T*) = nullptr)
            : ptr(p), deleter(d ? d : [](T* p){ delete p; }) {}
    };

    ControlBlock* cb_;

    void release() noexcept {
        if (!cb_) return;
        if (cb_->count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            cb_->deleter(cb_->ptr);
            delete cb_;
        }
        cb_ = nullptr;
    }

public:
    explicit SmartPtr(T* p = nullptr, void(*d)(T*) = nullptr) noexcept
        : cb_(p ? new ControlBlock(p, d) : nullptr) {}

    SmartPtr(const SmartPtr& other) noexcept
        : cb_(other.cb_) {
        if (cb_) cb_->count.fetch_add(1, std::memory_order_acq_rel);
    }

    SmartPtr& operator=(const SmartPtr& other) noexcept {
        if (this != &other) {
            release();
            cb_ = other.cb_;
            if (cb_) cb_->count.fetch_add(1, std::memory_order_acq_rel);
        }
        return *this;
    }

    SmartPtr(SmartPtr&& other) noexcept : cb_(other.cb_) {
        other.cb_ = nullptr;
    }

    SmartPtr& operator=(SmartPtr&& other) noexcept {
        if (this != &other) {
            release();
            cb_ = other.cb_;
            other.cb_ = nullptr;
        }
        return *this;
    }

    ~SmartPtr() { release(); }

    T& operator*() const noexcept { return *(cb_->ptr); }
    T* operator->() const noexcept { return cb_->ptr; }
    T* get() const noexcept { return cb_ ? cb_->ptr : nullptr; }
    explicit operator bool() const noexcept { return cb_ && cb_->ptr; }

    size_t use_count() const noexcept { return cb_ ? cb_->count.load(std::memory_order_relaxed) : 0; }
};

2.4 自定义 deleter 支持

通过在 SmartPtr 构造函数中接收 deleter,用户可以实现非 delete 的资源释放方式,例如:

SmartPtr<Foo, true> p(new Foo, [](Foo* f){ f->cleanup(); delete f; });

三、常见误区

  1. 拷贝构造时忘记计数
    共享指针的拷贝构造必须显式递增引用计数。若未递增,多个指针指向同一对象会导致重复删除。

  2. 多线程下计数器不原子
    共享指针的计数器若不是原子操作,在并发环境中会产生数据竞争,导致程序崩溃。请使用 std::atomic

  3. 自定义 deleter 与控制块不匹配
    如果 deleter 需要额外信息(如引用计数),应在控制块中存储相关成员,或使用 std::shared_ptr 的自定义 deleter/allocator 机制。

  4. 忽略异常安全
    在构造函数中分配控制块或执行 deleter 时若抛异常,需保证已分配的资源能被正确释放。使用 try-catch 或 RAII 包装器是常见做法。

四、使用示例

struct Resource {
    void release() { std::cout << "Resource released\n"; }
};

int main() {
    // 独占指针
    SmartPtr<Resource, false> up(new Resource);
    // 共享指针
    SmartPtr<Resource, true> sp1(new Resource);
    SmartPtr<Resource, true> sp2 = sp1;
    std::cout << "Use count: " << sp2.use_count() << '\n';

    // 自定义 deleter
    SmartPtr<Resource, true> sp3(new Resource,
                                 [](Resource* r){ r->release(); delete r; });

    return 0;
}

五、总结

自定义智能指针可以在满足特殊需求时提供更大的灵活性。关键在于:

  • 明确所有权语义并保持一致;
  • 正确实现计数器与资源释放逻辑;
  • 在多线程环境下确保原子操作;
  • 提供易用的接口以降低使用成本。

在实际项目中,如果需求不超过标准库所能覆盖的范围,建议优先使用 std::unique_ptr / std::shared_ptr。但在需要特殊 deleter、与旧接口兼容或学习目的时,自定义实现会是一个不错的练手项目。

发表评论