智能指針Boost.smart_ptr學習6--intrusive_ptr

intrusive_ptr

頭文件: "boost/intrusive_ptr.hpp"

intrusive_ptr 是shared_ptr的插入式版本。有時我們必須使用插入式的引用計數智能指針。典型的情況是對於那些已經寫好了內部引用計數器的代碼,而我們又沒有時間去重寫它(或者已經不能獲得那些代碼了)。另一種情況是要求智能指針的大小必須與裸指針大小嚴格相等,或者shared_ptr的引用計數器分配嚴重影響了程序的性能(我可以肯定這是非常罕見的情況!)。從功能的觀點來看,唯一需要插入式智能指針的情況是,被指類的某個成員函數需要返回this,以便它可以用於另一個智能指針(事實上,也有辦法使用非插入式智能指針來解決這個問題,正如我們在本章前面看到的)。intrusive_ptr 不同於其它智能指針,因爲它要求你來提供它所要的引用計數器。

當 intrusive_ptr 遞增或遞減一個非空指針上的引用計數時,它是通過分別調用函數 intrusive_ptr_add_ref 和 intrusive_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_ref 爲 true, 構造函數將調用 intrusive_ptr_add_ref(p). 如果 add_ref 爲 false, 構造函數則不調用 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_release。 intrusive_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(), 它主要用於支持泛型編程。它也可以用作替代成員函數 get, 因爲它可以重載爲可以與裸指針或第三方智能指針類一起工作。有些人寧願用普通函數而不用成員函數。這個函數不會拋出異常。


template <class T,class U>

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

這個函數返回 intrusive_ptr<T>(static_cast<T*>(r.get())). 和 shared_ptr不一樣,你可以對保存在intrusive_ptr中的對象指針安全地使用static_cast。但是你可能出於對智能指針類型轉換的用法一致性而想使用這個函數。static_pointer_cast 不會拋出異常。

用法

使用intrusive_ptr與使用shared_ptr相比,有兩個主要的不同之處。第一個是你需要提供引用計數的機制。第二個是把this當成智能指針是合法的,正如我們即將看到的,有時候這樣很方便。注意,在多數情況下,應該使用非插入式的 shared_ptr. 

要使用 boost::intrusive_ptr, 要包含 "boost/intrusive_ptr.hpp" 並定義兩個普通函數 intrusive_ptr_add_ref 和 intrusive_ptr_release. 它們都要接受一個參數,即指向你要使用intrusive_ptr的類型的指針。這兩個函數的返回值被忽略。通常的做法是,泛化這兩個函數,簡單地調用被管理類型的成員函數去完成工作(例如,調用 add_ref 和 release)。如果引用計數降爲零,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_ref 和 release 成員函數來操作它。add_ref 遞增引用計數而 release 遞減它。 我們可以增加一個返回引用計數當前值的成員函數,但release也可以做到這一點。下面的基類,reference_counter, 提供了一個計數器以及 add_ref 和 release 成員函數,我們可以簡單地用繼承來爲一個類增加引用計數了。

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_ref 和 intrusive_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_release 去 delete 該指針。你可能注意到 reference_counter 的實現不是線程安全的,因此不能用於多線程應用,除非加上同步化。

比起依賴於intrusive_ptr_add_ref 和 intrusive_ptr_release的泛型實現,我們最好有一些直接操作基類(在這裏是 reference_counter)的函數。這樣做的優點在於,即使從reference_counter派生的類定義在其它的名字空間,intrusive_ptr_add_ref 和 intrusive_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<A>, 而不是一個普通的A指針。因此,在 A::call_do_stuff裏,應該如何創建 shared_ptr ?現在,讓我們重寫 A ,讓它兼容於 intrusive_ptr, 通過從 reference_counter派生,然後我們再增加一個 do_stuff的重載版本,接受一個 intrusive_ptr<A>類型的參數。

#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<A>的函數,這是由於 intrusive_ptr的類型轉換構造函數。
最後,這裏有一個特別的地方:現在 A 可以支持 intrusive_ptr了,我們也可以創建一個包裝intrusive_ptr的shared_ptr,這們我們就可以調用原來版本的 do_stuff, 它需要一個 shared_ptr<A> 作爲參數。假如你不能控制 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_ref 和 intrusive_ptr_release 被實現爲直接操作 reference_counter, 你就要這樣來創建 shared_ptr :

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

支持不同的引用計數器

我們前面提過可以爲不同的類型支持不同的引用計數。這在集成已有的採用不同引用計數機制的類時是有必要的(例如,第三方的類使用它們自己版本的引用計數器)。又或者對於資源的釋放有不同的需求,如調用delete以外的另一個函數。如前所述,對 intrusive_ptr_add_ref 和 intrusive_ptr_release 的調用是非受限的。這意味着在名字查找時要考慮參數(指針的類型)的作用域,從而這些函數應該與它們操作的類型定義在同一個作用域。如果你在全局名字空間裏實現 intrusive_ptr_add_ref 和 intrusive_ptr_release 的泛型版本,你就不能在其它名字空間中再創建泛型版本了。例如,如果一個名字空間需要爲它的所有類型定義一個特殊的版本,特化版本或重載版本必須提供給每一個類型。否則,全局名字空間中的函數就會引起歧義。因此在全局名字空間中提供泛型版本不是一個好主意,而在其它名字空間中提供則可以。
既然我們已經用基類reference_counter實現了引用計數器,那麼在全局名字空間中提供一個接受reference_counter*類型的參數的普通函數應該是一個好主意。這還可以讓我們在其它名字空間中提供泛型重載版本而不會引起歧義。例如,考慮my_namespace名字空間中的兩個類 another_class 和 derived_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_ref 和 intrusive_ptr_release的泛型版本。因此我們必須刪掉在全局名字空間中的泛型版本,把它們替換爲以一個reference_counter指針爲參數的非模板版本。或者,我們乾脆從全局名字空間中刪掉這些函數,也可以避免引起混亂。對於這兩個類 my_namespace::another_class 和 my_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_ref 和 intrusive_ptr_release時,它們應該總是定義在它們操作的類型所在的名字空間裏。從設計的角度來看,這也是完美的,把相關的東西放在一起,這有助於確保總是調用正確的版本,而不用擔心是否有多個不同的實現可供選擇。

總結

在多數情況下,你不應該使用 boost::intrusive_ptr, 因爲共享所有權的功能已在 boost::shared_ptr中提供,而且非插入式智能指針比插入式智能指針更靈活。但是,有時候也會需要插入式的引用計數,可能是由於舊的代碼,或者是爲了與第三方的類進行集成。當有這種需要時,可以用 intrusive_ptr ,它具有與其它Boost智能指針相同的語義。如果你使用過其它的Boost智能指針,你就會發現不論是否插入式的,所有智能指針都有一致的接口。使用intrusive_ptr的類必須可以提供引用計數。ntrusive_ptr 通過調用兩個函數,intrusive_ptr_add_ref 和 intrusive_ptr_release來管理引用計數;這兩個函數必須正確地操作插入式的引用計數,以保證 intrusive_ptr正確工作。在使用intrusive_ptr的類中已經內置有引用計數的情況下,實現對intrusive_ptr的支持就是實現這兩個函數。有些情況下,可以創建這兩個函數的參數化版本,然後對所有帶插入式引用計數的類型使用相同的實現。多數時候,聲明這兩個函數的最好的地方就是它們所支持的類型所在的名字空間。
在以下情況時使用 intrusive_ptr :
  • 你需要把 this 當作智能指針來使用。
  • 已有代碼使用或提供了插入式的引用計數。
  • 智能指針的大小必須與裸指針的大小相等。


發佈了13 篇原創文章 · 獲贊 6 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章