intrusive_ptr

头文件: "boost/intrusive_ptr.hpp"

intrusive_ptrshared_ptr的插入式版本。有时我们必须使用插入式的引用计数智能指针。典型的情况是对于那些已经写好了内部引用计数器的代码,而我们又没有时间去重写它(或者已经不能获得那些代码了)。另一种情况是要求智能指针的大小必须与裸指针大小严格相等,或者shared_ptr的引用计数器分配严重影响了程序的性能(我可以肯定这是非常罕见的情况!)。从功能的观点来看,唯一需要插入式智能指针的情况是,被指类的某个成员函数需要返回this,以便它可以用于另一个智能指针(事实上,也有办法使用非插入式智能指针来解决这个问题,正如我们在本章前面看到的)。intrusive_ptr 不同于其它智能指针,因为它要求你来提供它所要的引用计数器。

intrusive_ptr 递增或递减一个非空指针上的引用计数时,它是通过分别调用函数 intrusive_ptr_add_refintrusive_ptr_release来完成的。这两个函数负责确保引用计数的正确性,并且负责在引用计数降为零时删除指针。因此,你必须为你的类重载这两个函数,正如我们后面将看到的。

以下是intrusive_ptr的部分摘要,只列出了最重要的函数。

namespace boost {

  template<class T> class intrusive_ptr {
  public:
    intrusive_ptr(T* p,bool add_ref=true);

    intrusive_ptr(const intrusive_ptr& r);

    ~intrusive_ptr();

    T& operator*() const;
    T* operator->() const;
    T* get() const; 

    operator unspecified-bool-type() const; 
  };

  template <class T> T* get_pointer(const intrusive_ptr<T>& p); 

  template <class T,class U> intrusive_ptr<T>
    static_pointer_cast(const intrusive_ptr<U>& r); 
}

成员函数

intrusive_ptr(T* p,bool add_ref=true);

这个构造函数将指针p保存到*this中。如果 p 非空,并且 add_reftrue, 构造函数将调用 intrusive_ptr_add_ref(p). 如果 add_reffalse, 构造函数则不调用 intrusive_ptr_add_ref. 如果intrusive_ptr_add_ref会抛出异常,则构造函数也会。

intrusive_ptr(const intrusive_ptr& r);

该复制构造函数保存一份r.get()的拷贝,并且如果指空非空则用它调用 intrusive_ptr_add_ref 。这个构造函数不会抛出异常。

~intrusive_ptr();

如果保存的指针为非空,则 intrusive_ptr 的析构函数会以保存的指针为参数调用函数 intrusive_ptr_releaseintrusive_ptr_release 负责递减引用计数并在计数为零时删除指针。这个函数不会抛出异常。

T& operator*() const;

解引用操作符返回所存指针的解引用。如果所存指针为空则会导致未定义行为。你应该确认intrusive_ptr有一个非空的指针,这可以用函数 get 实现,或者在Boolean上下文中测试 intrusive_ptr 。解引用操作符不会抛出异常。

T* operator->() const;

这个操作符返回保存的指针。在引用的指针为空时调用这个操作符会有未定义行为。这个操作符不会抛出异常。

T* get() const;

这个成员函数返回保存的指针。它可用于你需要一个裸指针的时候,即使保存的指针为空也可以调用。这个函数不会抛出异常。

operator unspecified-bool-type() const;

这个类型转换函数返回一个可用于布尔表达式的类型,而它绝对不是 operator bool, 因为那样会允许一些必须要禁止的操作。这个转换允许intrusive_ptr在一个布尔上下文中被测试,例如,if (p), p 是一个 intrusive_ptr. 这个转换函数当intrusive_ptr引向一个非空指针时返回True ; 否则返回 false. 这个转换函数不会抛出异常。

普通函数

template <class T> T* get_pointer(const intrusive_ptr<T>& p);

这个函数返回 p.get(), 它主要用于支持泛型编程。[10] 它也可以用作替代成员函数 get, 因为它可以重载为可以与裸指针或第三方智能指针类一起工作。有些人宁愿用普通函数而不用成员函数。[11] 这个函数不会抛出异常。

[10] 这种函数被称为 shims. 见参考书目的 [12] 。

[11]这种想法是出于以下原因,使用智能指针的成员函数时,很难分清它是操作智能指针还是操作它所指向的对象。例如, p.get()p-&gt;get() 有完全不同的意思,不认真看还很难区别,而 get_pointer(p)p-&gt;get() 则一看就知道不一样。对于你来说这是不是问题,主要取决于你的感觉和经验。

template <class T,class U>
  intrusive_ptr<T> static_pointer_cast(const intrusive_ptr<U>& r);

这个函数返回 intrusive_ptr&lt;T&gt;(static_cast&lt;T*&gt;(r.get())). 和 shared_ptr不一样,你可以对保存在intrusive_ptr中的对象指针安全地使用static_cast。但是你可能出于对智能指针类型转换的用法一致性而想使用这个函数。static_pointer_cast 不会抛出异常。

用法

使用intrusive_ptr与使用shared_ptr相比,有两个主要的不同之处。第一个是你需要提供引用计数的机制。第二个是把this当成智能指针是合法的[12],正如我们即将看到的,有时候这样很方便。注意,在多数情况下,应该使用非插入式的 shared_ptr.

[12] 你不能用shared_ptr 来做到这一点,如果没有进行特殊处理的话,如 enable_shared_from_this.

要使用 boost::intrusive_ptr, 要包含 "boost/intrusive_ptr.hpp" 并定义两个普通函数 intrusive_ptr_add_refintrusive_ptr_release. 它们都要接受一个参数,即指向你要使用intrusive_ptr的类型的指针。这两个函数的返回值被忽略。通常的做法是,泛化这两个函数,简单地调用被管理类型的成员函数去完成工作(例如,调用 add_refrelease)。如果引用计数降为零,intrusive_ptr_release 应该负责释放资源。以下是你应该如何实现这两个泛型函数的示范:

template <typename T> void intrusive_ptr_add_ref(T* t) {
  t->add_ref();
}

template <typename T> void intrusive_ptr_release(T* t) {
  if (t->release()<=0)
    delete t;
}

注意,这两个函数应该定义在它们的参数类型所在的作用域内。这意味着如果这个函数接受的参数类型来自 于一个名字空间,则函数也必须定义在那里。这样做的原因是,函数的调用是非受限的,即允许采用参数相关查找,而如果有多个版本的函数被提供,那么全部名字 空间肯定不是放置它们的好地方。我们稍后将看到一个关于如何放置它们的例子,但首先,我们需要提供某类的引用计数器。

提供一个引用计数器

现在管理用的函数已经定义了,我们必须要提供一个内部的引用计数器了。在本例中,引用计数是一个初始化为零的私有数据成员,我们将公开 add_refrelease 成员函数来操作它。add_ref 递增引用计数而 release 递减它[13]。 我们可以增加一个返回引用计数当前值的成员函数,但release也可以做到这一点。下面的基类,reference_counter, 提供了一个计数器以及 add_refrelease 成员函数,我们可以简单地用继承来为一个类增加引用计数了。

[13] 注意,在多线程环境下,对保持引用计数的变量的任何操作都必须同步化。

class reference_counter {
  int ref_count_;
  public:
    reference_counter() : ref_count_(0) {}

    virtual ~reference_counter() {}

    void add_ref() { 
      ++ref_count_;
    }

    int release() {
      return --ref_count_;
    }

  protected:
    reference_counter& operator=(const reference_counter&) {
    // 无操作
      return *this;
    }
  private:
    // 禁止复制构造函数
    reference_counter(const reference_counter&); 
};

reference_counter的析构函数声明为虚拟的原因是这个类将被公开继承,有可能会使用一个reference_counter指针来delete派生类。我们希望删除操作能够正确地调用派生类的析构函数。实现非常简单:add_ref 递增引用计数,release 递减引用计数并返回它。要使用这个引用计数,要做的就是公共地继承它。以下是一个类 some_ class ,包含一个内部引用计数,并使用 intrusive_ptr

#include <iostream>
#include "boost/intrusive_ptr.hpp"

class some_class : public reference_counter {
public:
  some_class() {
    std::cout << "some_class::some_class()\n";
  }

  some_class(const some_class& other) {
    std::cout << "some_class(const some_class& other)\n";
  }

  ~some_class() {
    std::cout << "some_class::~some_class()\n";
  }
};

int main() {
  std::cout << "Before start of scope\n";
  {
    boost::intrusive_ptr<some_class> p1(new some_class());
    boost::intrusive_ptr<some_class> p2(p1);
  }
  std::cout << "After end of scope \n";
}

为了显示 intrusive_ptr以及函数 intrusive_ptr_add_refintrusive_ptr_release 都正确无误,以下是这个程序的运行输出:

Before start of scope
some_class::some_class()
some_class::~some_class()
After end of scope

intrusive_ptr 为我们打点一切。当第一个 intrusive_ptr p1 创建时,它传送了一个some_class的新实例。intrusive_ptr 构造函数实际上有两个参数,第二个是一个 bool ,表示是否要调用 intrusive_ptr_add_ref 。由于这个参数的缺省值是 True, 所以在构造 p1时,some_class实例的引用计数变为1。然后,第二个 intrusive_ptr, p2初构造。它是从 p1复制构造的,当 p2 看到 p1 是引向一个非空指针时,它调用 intrusive_ptr_add_ref. 引用计数变为2。然后,两个 intrusive_ptr都离开作用域了。首先, p2 被销毁,析构函数调用 intrusive_ptr_release. 它把引用计数减为1。然后,p1 被销毁,析构函数再次调用 intrusive_ptr_release ,导致引用计数降为0;这使得我们的intrusive_ptr_releasedelete 该指针。你可能注意到 reference_counter 的实现不是线程安全的,因此不能用于多线程应用,除非加上同步化。

比起依赖于intrusive_ptr_add_refintrusive_ptr_release的泛型实现,我们最好有一些直接操作基类(在这里是 reference_counter)的函数。这样做的优点在于,即使从reference_counter派生的类定义在其它的名字空间,intrusive_ptr_add_refintrusive_ptr_release 也还可以通过ADL (参数相关查找法)找到它们。修改reference_counter的实现很简单。

class reference_counter {
  int ref_count_;
  public:
    reference_counter() : ref_count_(0) {}

    virtual ~reference_counter() {}

     friend void intrusive_ptr_add_ref(reference_counter* p) { 
       ++p->ref_count_;
     }

     friend void intrusive_ptr_release(reference_counter* p) {
       if (--p->ref_count_==0)
         delete p;
     }

  protected:
    reference_counter& operator=(const reference_counter&) {
    // 无操作
      return *this;
    }
  private:
    // 禁止复制构造函数
    reference_counter(const reference_counter&); 
};

把 this 用作智能指针

总的来说,提出一定要用插入式引用计数智能指针的情形是不容易的。大多数情况下,但不是全部情况下,非插入式智能指针都可以解决问题。但是,有一种情形使用插入式引用计数会更容易:当你需要从一个成员函数返回 this ,并把它存入另一个智能指针。当从一个被非插入式智能指针所拥有的类型返回 this时, 结果是有两个不同的智能指针认为它们拥有同一个对象,这意味着它们会在某个时候一起试图删除同一个指针。这导致了两次删除,结果可能使你的应用程序崩溃。 必须有什么办法可以通知另一个智能指针,这个资源已经被一个智能指针所引用,这正好是内部引用计数器(暗地里)可以做到的。由于 intrusive_ptr 的逻辑不直接对它们所引向的对象的内部引用计数进行操作,这就不会违反所有权或引用计数的完整性。引用计数只是被简单地递增。

让我们先看一下一个依赖于boost::shared_ptr来共享资源所有权的实现中潜在的问题。它基于本章前面讨论enable_shared_from_this时的例子。

#include "boost/shared_ptr.hpp"

class A;

void do_stuff(boost::shared_ptr<A> p) {
  // ...
}

class A {
public:
  call_do_stuff() {
   shared_ptr<A> p(???);
    do_stuff(p);
  }
};

int main() {
  boost::shared_ptr<A> p(new A());
  p->call_do_stuff();
}

A 要调用函数 do_stuff, 但问题是 do_stuff 要一个 shared_ptr&lt;A&gt;, 而不是一个普通的A指针。因此,在 A::call_do_stuff里,应该如何创建 shared_ptr ?现在,让我们重写 A ,让它兼容于 intrusive_ptr, 通过从 reference_counter派生,然后我们再增加一个 do_stuff的重载版本,接受一个 intrusive_ptr&lt;A&gt;类型的参数。

#include "boost/intrusive_ptr.hpp"

class A;

void do_stuff(boost::intrusive_ptr<A> p) {
  // ...
}

void do_stuff(boost::shared_ptr<A> p) {
  // ...
}

class A : public reference_counter {
public:
  void call_do_stuff() {
    do_stuff(this);
  }
};

int main() {
  boost::intrusive_ptr<A> p(new A());
  p->call_do_stuff();
}

如你所见,在这个版本的 A::call_do_stuff里,我们可以直接把 this 传给需要一个 intrusive_ptr&lt;A&gt;的函数,这是由于 intrusive_ptr的类型转换构造函数。

最后,这里有一个特别的地方:现在 A 可以支持 intrusive_ptr了,我们也可以创建一个包装intrusive_ptrshared_ptr,这们我们就可以调用原来版本的 do_stuff, 它需要一个 shared_ptr&lt;A&gt; 作为参数。假如你不能控制 do_stuff的源码,这可能是你要解决的一个非常真实的问题。这次,还是用定制删除器的方法来解决,它需要调用 intrusive_ptr_release. 下面是一个新版本的 A::call_do_stuff.

void call_do_stuff() {
  intrusive_ptr_add_ref(this);
  boost::shared_ptr<A> p(this,&intrusive_ptr_release<A>);
  do_stuff(p);
}

真是一个漂亮的方法。当没有 shared_ptr剩下时,定制的删除器被调用,它调用 intrusive_ptr_release, 递减A的内部引用计数。注意,如果 intrusive_ptr_add_refintrusive_ptr_release 被实现为直接操作 reference_counter, 你就要这样来创建 shared_ptr

boost::shared_ptr<A> p(this,&intrusive_ptr_release);

支持不同的引用计数器

我们前面提过可以为不同的类型支持不同的引用计数。这在集成已有的采用不同引用计数机制的类时是有必要的(例如,第三方的类使用它们自己版本的引用计数器)。又或者对于资源的释放有不同的需求,如调用delete以外的另一个函数。如前所述,对 intrusive_ptr_add_refintrusive_ptr_release 的调用是非受限的。这意味着在名字查找时要考虑参数(指针的类型)的作用域,从而这些函数应该与它们操作的类型定义在同一个作用域。如果你在全局名字空间里实现 intrusive_ptr_add_refintrusive_ptr_release 的泛型版本,你就不能在其它名字空间中再创建泛型版本了。例如,如果一个名字空间需要为它的所有类型定义一个特殊的版本,特化版本或重载版本必须提供给每 一个类型。否则,全局名字空间中的函数就会引起歧义。因此在全局名字空间中提供泛型版本不是一个好主意,而在其它名字空间中提供则可以。

既然我们已经用基类reference_counter实现了引用计数器,那么在全局名字空间中提供一个接受reference_counter*类型的参数的普通函数应该是一个好主意。这还可以让我们在其它名字空间中提供泛型重载版本而不会引起歧义。例如,考虑my_namespace名字空间中的两个类 another_classderived_class

namespace my_namespace {
  class another_class : public reference_counter {
  public:
    void call_before_destruction() const {
      std::cout << 
        "Yes, I'm ready before destruction\n";
    }
  };

  class derived_class : public another_class {};

   template <typename T> void intrusive_ptr_add_ref(T* t) {
     t->add_ref();
   }

  template <typename T> void intrusive_ptr_release(T* t) {
    if (t->release()<=0) {
      t->call_before_destruction();
      delete t;
    }
  }
}

这里,我们实现了intrusive_ptr_add_refintrusive_ptr_release的泛型版本。因此我们必须删掉在全局名字空间中的泛型版本,把它们替换为以一个reference_counter指针为参数的非模板版本。或者,我们干脆从全局名字空间中删掉这些函数,也可以避免引起混乱。对于这两个类 my_namespace::another_classmy_namespace::derived_class, 将调用这个特殊版本(那个调用了它的参数的成员函数 call_before_destruction 的版本)。其它类型或者在它们定义所在的名字空间中有相应的函数,或者使用全局名字空间中的版本,如果有的话。下面程序示范了这如何工作:

int main() {
  boost::intrusive_ptr<my_namespace::another_class> 
    p1(new my_namespace::another_class());
  boost::intrusive_ptr<A> 
    p2(new good_class());
  boost::intrusive_ptr<my_namespace::derived_class> 
    p3(new my_namespace::derived_class());
}

首先,intrusive_ptr p1 被传入一个新的 my_namespace::another_class实例。在解析对 intrusive_ptr_add_ref的调用时,编译器会找到 my_namespace里的版本,即 my_namespace::another_class* 参数所在名字空间。因而,为那个名字空间里的类型所提供的泛型函数会被正确地调用。在查找 intrusive_ptr_release时也是同样。然后,intrusive_ptr p2 被创建并被传入一个类型A (我们早前创建的那个类型)的指针。那个类型是在全局名字空间里的,所以当编译器试图去找到函数 intrusive_ptr_add_ref的最佳匹配时,它只会找到一个版本,即接受reference_counter指针类型的那个版本(你应该记得我们已经从全局名字空间中删掉了泛型版本)。因为 A 公共继承自 reference_counter, 通过隐式类型转换就可以进行正确的调用。最后,my_namespace 里的泛型版本被用于类 my_namespace::derived_class; 这与 another_class例子中的查找是一样的。

这里最重要的教训是,在实现函数 intrusive_ptr_add_refintrusive_ptr_release时,它们应该总是定义在它们操作的类型所在的名字空间里。从设计的角度来看,这也是完美的,把相关的东西放在一起,这有助于确保总是调用正确的版本,而不用担心是否有多个不同的实现可供选择。

总结

在多数情况下,你不应该使用 boost::intrusive_ptr, 因为共享所有权的功能已在 boost::shared_ptr中提供,而且非插入式智能指针比插入式智能指针更灵活。但是,有时候也会需要插入式的引用计数,可能是由于旧的代码,或者是为了与第三方的类进行集成。当有这种需要时,可以用 intrusive_ptr ,它具有与其它Boost智能指针相同的语义。如果你使用过其它的Boost智能指针,你就会发现不论是否插入式的,所有智能指针都有一致的接口。使用intrusive_ptr的类必须可以提供引用计数。ntrusive_ptr 通过调用两个函数,intrusive_ptr_add_refintrusive_ptr_release来管理引用计数;这两个函数必须正确地操作插入式的引用计数,以保证 intrusive_ptr正确工作。在使用intrusive_ptr的类中已经内置有引用计数的情况下,实现对intrusive_ptr的支持就是实现这两个函数。有些情况下,可以创建这两个函数的参数化版本,然后对所有带插入式引用计数的类型使用相同的实现。多数时候,声明这两个函数的最好的地方就是它们所支持的类型所在的名字空间。

在以下情况时使用 intrusive_ptr

  • 你需要把 this 当作智能指针来使用。

  • 已有代码使用或提供了插入式的引用计数。

  • 智能指针的大小必须与裸指针的大小相等。