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_refintrusive_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_refintrusive_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_ptr 知识体系 boost::intrusive_ptr 设计特点 引用计数在对象内部 无额外控制块分配 从裸指针构造安全 适用场景 对象自带引用计数 内存受限环境 与第三方C库交互 核心要求:实现 intrusive_ptr_add_ref / intrusive_ptr_release

进阶:继承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那样通用,但在特定场景下,它是最优雅的解决方案。


公众号:蓝海资料掘金营,微信deep3321