多数场景不该手写RefCountedBase,因其带来内存布局污染、线程安全陷阱和生命周期割裂三类成本;真需侵入式计数时,必须用std::atomic、禁用拷贝、正确实现ref/unref及protected虚析构。

直接用 std::shared_ptr 就行,别自己写侵入式引用计数基类 —— 除非你明确需要零分配、确定性析构、或与 C API/裸指针生态深度互操作。
为什么多数场景不该手写 RefCountedBase
自己实现侵入式引用计数(比如带 ref()/unref() 的基类)看似轻量,实际隐藏三类成本:
- 内存布局污染:每个派生类对象多占 4 或 8 字节(
m_refCount),且无法被编译器优化掉 - 线程安全陷阱:
std::atomic<int></int>的读-改-写开销比std::shared_ptr的控制块原子操作更难对齐,容易漏掉memory_order约束 - 生命周期割裂:
new T+ref()和unref()手动配对,极易出现悬空或泄漏;而std::shared_ptr<t>(new T)</t>或std::make_shared<t>()</t>天然绑定构造与计数
真要写,必须用 std::atomic<int></int> 且禁止拷贝构造
常见错误是把引用计数成员声明为普通 int,或在基类里提供默认拷贝构造函数 —— 这会导致两个指针同时指向同一对象却各自维护独立计数,unref() 后立即释放,另一方变野指针。
正确做法:
立即学习“C++免费学习笔记(深入)”;
-
m_refCount必须是std::atomic<int></int>,初始化为 1(对象诞生即被持有) - 基类禁用拷贝:
RefCountedBase(const RefCountedBase&) = delete;,移动也禁用(侵入式计数不支持转移语义) -
unref()必须用fetch_sub(1, std::memory_order_acq_rel),检查返回值是否为 1 再delete this;不能用--m_refCount == 0 - 析构函数必须是
protected virtual,防止用户直接delete p
示例关键片段:
class RefCountedBase {
protected:
std::atomic<int> m_refCount{1};
virtual ~RefCountedBase() = default;
RefCountedBase(const RefCountedBase&) = delete;
void ref() { m_refCount.fetch_add(1, std::memory_order_relaxed); }
void unref() {
if (m_refCount.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
}
}
};和 std::shared_ptr 混用时,enable_shared_from_this 是唯一安全桥接方式
如果已有侵入式基类,又想临时转成 std::shared_ptr(比如调第三方库),绝不能写 std::shared_ptr<t>(raw_ptr)</t> —— 这会新建控制块,导致双重释放。
正确路径只有两条:
- 让类继承
std::enable_shared_from_this<T>,并在构造时确保对象一定由std::shared_ptr管理(即只通过std::make_shared创建) - 若必须从裸指针出发,且该指针确属某
std::shared_ptr管理,则需原始控制块信息 —— 实际不可行,等价于放弃安全
换句话说:侵入式基类和 std::shared_ptr 属于两种内存管理范式,硬桥接等于主动放弃 RAII 保障。
真正轻量且现代的替代方案:用 std::shared_ptr + 自定义删除器
如果你的目标是“零额外内存分配”或“精确控制释放时机”,与其重写基类,不如定制 std::shared_ptr 的删除行为:
- 用静态池或 arena 分配对象,再传给
std::shared_ptr,删除器调用pool.free()而非delete - 删除器可触发回调、日志、甚至延迟释放(如等待 GPU 完成)
- 所有接口保持标准,兼容 STL 算法、
std::weak_ptr、线程安全,且无基类侵入
例如:
struct ArenaDeleter {
Arena* arena;
void operator()(MyType* p) const { arena->free(p); }
};
auto ptr = std::shared_ptr<MyType>(arena->alloc<MyType>(), ArenaDeleter{arena});侵入式引用计数不是“更轻”,而是“换一种重法”——它把复杂性从堆分配挪到了对象布局、线程同步和生命周期契约上。除非你在写图形驱动、嵌入式中间件或对接 legacy C 库,否则优先让 std::shared_ptr 做它该做的事。


















