29. 智能指针在Boost库中的应用:boost::intrusive_ptr
讲完了标准库里的智能指针,咱们来看看Boost库中的一员猛将——boost::intrusive_ptr。说实话,我在项目里第一次遇到它时,还有点懵。因为它的设计思路跟shared_ptr完全相反——它不自己管理引用计数,而是要求你管理的对象自己带着计数。
嗯,听起来有点反直觉对吧?但正是这种设计,让它在某些场景下大放异彩。
什么是intrusive_ptr?
说白了,intrusive_ptr是一种「侵入式」的智能指针。所谓侵入,就是要求你的类内部必须包含一个引用计数成员。指针本身不分配额外的计数空间,而是直接操作对象内部的计数。
我刚开始用的时候觉得这设计有点多余——既然shared_ptr那么好用,干嘛还要折腾这个?直到我在一个高性能网络库中遇到了内存池和自定义分配器的场景,才明白它的价值。
核心区别一句话:
shared_ptr:引用计数在堆上单独分配(控制块)intrusive_ptr:引用计数在对象内部(侵入式)
什么时候该用它?
我个人习惯在以下几种场景优先考虑intrusive_ptr:
- 对象已经自带引用计数:比如COM对象、某些框架的基础类
- 内存极度受限:嵌入式系统、游戏引擎,能省一个控制块是一个
- 需要从裸指针构造智能指针:
intrusive_ptr可以从任意裸指针构造,不会像shared_ptr那样搞出多个控制块 - 与第三方库交互:有些C库返回的指针,你没法让它们用
shared_ptr的管理机制
基本用法
先看一个最简单的例子。你需要定义一个类,里面放一个引用计数,然后实现两个全局函数:intrusive_ptr_add_ref和intrusive_ptr_release。
#include <boost/intrusive_ptr.hpp>
#include <atomic>
class MyObject {
public:
MyObject() : ref_count_(0) {
// 注意:初始化为0,intrusive_ptr构造时会+1
}
// 友元函数需要访问ref_count_
friend void intrusive_ptr_add_ref(const MyObject* p) {
p->ref_count_.fetch_add(1, std::memory_order_relaxed);
}
friend void intrusive_ptr_release(const MyObject* p) {
if (p->ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete p;
}
}
private:
mutable std::atomic<int> ref_count_;
};
int main() {
boost::intrusive_ptr<MyObject> ptr1(new MyObject());
boost::intrusive_ptr<MyObject> ptr2 = ptr1; // 引用计数变成2
// ptr1和ptr2离开作用域时,计数归零,自动delete
return 0;
}
注意看,ref_count_初始化为0。为什么?因为intrusive_ptr的构造函数会调用intrusive_ptr_add_ref,把计数变成1。这个细节我曾经踩过坑——如果初始化为1,那计数就会变成2,导致内存泄漏。
与shared_ptr的对比
| 特性 | shared_ptr | intrusive_ptr |
|---|---|---|
| 引用计数位置 | 堆上控制块 | 对象内部 |
| 内存开销 | 额外分配控制块 | 无额外分配 |
| 从裸指针构造 | 容易产生多个控制块 | 安全 |
| 与第三方库兼容 | 困难 | 容易(只要对方提供增减计数的接口) |
| weak_ptr支持 | 原生支持 | 需要自己实现 |
| 类型要求 | 任意类型 | 必须实现侵入式计数接口 |
你看,没有银弹。每个工具都有它的适用场景。
避坑指南:我曾经犯过的错
坑一:忘记实现友元函数
我曾经在项目里定义了一个带ref_count_的类,但忘了写intrusive_ptr_add_ref和intrusive_ptr_release。编译能通过,但链接时报错。嗯,Boost库的报错信息你懂的,那叫一个酸爽。排查了半天才发现是这两个函数没实现。
坑二:线程安全问题
如果你在多线程环境下使用intrusive_ptr,引用计数操作必须是原子的。我早期图省事用了普通的int,结果在高并发下偶尔出现double free。换成std::atomic<int>后问题解决。记住:memory_order_relaxed用于增加计数没问题,但减少计数时要用memory_order_acq_rel,确保释放操作的可见性。
坑三:从this构造
如果你在成员函数里需要把this包装成intrusive_ptr传出去,要小心。不能直接boost::intrusive_ptr<T>(this),因为这样会额外增加一次引用计数,而调用方可能已经持有了一个指针。正确的做法是继承boost::intrusive_ref_counter,或者自己实现一个enable_intrusive_from_this。
小技巧:如果你的类不需要多线程,可以用普通的int代替std::atomic<int>,性能会更好。但一定要在文档里注明「非线程安全」。
知识体系结构图
下面这张图帮你理清intrusive_ptr在整个智能指针家族中的位置,以及它的核心设计思路:
进阶:继承intrusive_ref_counter
每次都要手写那两个友元函数,挺烦的。Boost提供了一个便捷基类boost::intrusive_ref_counter,你只需要继承它就行:
#include <boost/intrusive_ptr.hpp>
#include <boost/intrusive_ref_counter.hpp>
class MyObject : public boost::intrusive_ref_counter<MyObject> {
public:
MyObject() = default;
// 不需要自己管理引用计数了!
};
int main() {
boost::intrusive_ptr<MyObject> ptr(new MyObject());
// 一切自动处理
return 0;
}
这个基类默认使用std::atomic<int>作为计数类型,线程安全。如果你不需要线程安全,可以指定boost::thread_unsafe_counter作为模板参数:
class MyObject : public boost::intrusive_ref_counter<MyObject, boost::thread_unsafe_counter> {
// 单线程下性能更好
};
警告:使用thread_unsafe_counter时,千万不要在多线程环境下传递intrusive_ptr。否则你会遇到难以排查的数据竞争问题。我建议除非你100%确定是单线程,否则就用默认的原子计数。
实际项目中的经验
我在一个内存池分配器的项目中用过intrusive_ptr。当时所有对象都从固定大小的内存池中分配,shared_ptr的控制块会破坏内存池的布局——因为控制块是在堆上另外分配的。改用intrusive_ptr后,引用计数直接放在对象内部,整个对象(包括计数)都可以从内存池中分配,性能提升很明显。
另外,如果你在写一个框架,希望所有对象都支持智能指针管理,但又不想强制用户使用shared_ptr,那么intrusive_ptr配合intrusive_ref_counter是个很好的选择。用户只需要继承你的基类,就能获得智能指针的全部能力。
嗯,关于intrusive_ptr的核心内容就这些。它不像shared_ptr那样通用,但在特定场景下,它是最优雅的解决方案。