1. 程式人生 > >C++“準”標準庫Boost學習指南(1):智慧指標Boost.smart_ptr

C++“準”標準庫Boost學習指南(1):智慧指標Boost.smart_ptr

我們學習C++都知道智慧指標,例如STL中的std::auto_ptr,但是為什麼要使用智慧指標,使用它能帶給我們什麼好處呢?

最簡單的使用智慧指標可以不會因為忘記delete指標而造成記憶體洩露。還有如果我們開發或者使用第三方的lib中的某些函式需要返回指標,這樣的返回的指標被client使用的時候,lib就會失去對返回的指標的控制,這樣delete的指標的任務一般就會交給呼叫方client,但是如果 client忘記呼叫delete或是呼叫的時機不正確,都有可能導致問題,在這種情況下就最好使用智慧指標。還有使用智慧指標可以保證異常安全,保證程式在有異常丟擲時仍然無記憶體洩露。
   
std::auto_ptr很多的時候並不能滿足我們的要求,比如她不能用在STL的container中。boost的smart_ptr中提供了4種智慧指標和2種智慧指標陣列來作為std::auto_ptr的補充。   
  • shared_ptr<boost/shared_ptr.hpp>:使用shared_ptr進行物件的生存期自動管理,使得分享資源所有權變得有效且安全.
  • scoped_ptr<boost/scoped_ptr.hpp>: 用於確保能夠正確地刪除動態分配的物件。scoped_ptr 有著與std::auto_ptr類似的特性,而最大的區別在於它不能轉讓所有權而auto_ptr可以。事實上,scoped_ptr永遠不能被複制或被賦值!scoped_ptr 擁有它所指向的資源的所有權,並永遠不會放棄這個所有權。
  • weak_ptr<boost/weak_ptr.hpp>:weak_ptr 是 shared_ptr 的觀察員。它不會干擾shared_ptr所共享的所有權。當一個被weak_ptr所觀察的 shared_ptr 要釋放它的資源時,它會把相關的 weak_ptr的指標設為空。使用此輔助指標一般是防止懸空指標。
  • intrusive_ptr<boost/intrusive_ptr.hpp>:shared_ptr的插入是版本,一般不使用,因為需要對使用此指標的型別中增加ref計數功能。但是可以保證不增加指標的大小。
  • scoped_array<boost/scoped_array.hpp>: scoped_array 為陣列做了scoped_ptr為單個物件的指標所做的事情:它負責釋放記憶體。shared_array<boost/shared_array.hpp>: shared_array 用於共享陣列所有權的智慧指標。一般指向std::vector的shared_ptr提供了比shared_array更多的靈活性,所以一般使用 std::vector<shared_ptr>。



Smart_ptr庫如何改進你的程式?
  • 使用shared_ptr進行物件的生存期自動管理,使得分享資源所有權變得有效且安全。
  • 使用weak_ptr可以安全地觀測共享資源,避免了懸掛的指標。
  • 使用scoped_ptr 和 scoped_array限制資源的使用範圍,使得程式碼更易於編寫和維護,並有助於寫出異常安全的程式碼。

智慧指標解決了資源生存期管理的問題(尤其是動態分配的物件). 智慧指標有各種不同的風格。多數都有一種共同的關鍵特性:自動資源管理。這種特性可能以不同的方式出現:如動態分配物件的生存期控制,和獲取及釋放資源 (檔案, 網路連線)。Boost的智慧指標主要針對第一種情況,它們儲存指向動態分配物件的指標,並在正確的時候刪除這些物件。你可能覺得奇怪為什麼這些智慧指標不多做一點工作。它們不可以很容易就覆蓋所有資源管理的不同情況嗎?是的,它們可以(在一定範圍內它們可以),但不是沒有代價的。通用的解決方案意味著更高的複雜性,而對於Boost的智慧指標,可用性比靈活性具有更高的優先順序。但是,通過對可定製刪除器的支援,Boost的最智慧的智慧指標(boost::shared_ptr)可以支援那些不是使用delete進行析構的資源。Boost.Smart_ptr的五個智慧指標型別是專門特製的,適用於每天的程式設計中最常見的需求。

何時我們需要智慧指標?

有三種典型的情況適合使用智慧指標:
  • 資源所有權的共享
  • 要編寫異常安全的程式碼時
  • 避免常見的錯誤,如資源洩漏

共享所有權是指兩個或多個物件需要同時使用第三個物件的情況。這第三個物件應該如何(或者說何時)被釋放?為了確保釋放的時機是正確的,每個使用這個共享資源的物件必須互相知道對方,才能準確掌握資源的釋放時間。從設計或維護的觀點來看,這種耦合是不可行的。更好的方法是讓這些資源所有者將資源的生存期管理責任委派給一個智慧指標。當沒有共享者存在時,智慧指標就可以安全地釋放這個資源了。

異常安全,簡單地說就是在異常丟擲時沒有資源洩漏並保證程式狀態的一致性。如果一個物件是動態分配的,當異常丟擲時它不會自動被刪除。由於棧展開以及指標離開作用域,資源可以會洩漏直至程式結束(即使是程式結束時的資源回收也不是由語言所保證的)。不僅可能程式會由於記憶體洩漏而耗盡資源,程式的狀態也可能變得混亂。智慧指標可以自動地為你釋放這些資源,即使是在異常發生的情況下。

避免常見的錯誤。忘記呼叫 delete 是書本中最古老的錯誤(至少在這本書中)。一個智慧指標不關心程式中的控制路徑;它只關心在它所指向的物件的生存期結束時刪除它。使用智慧指標,你不再需要知道何時刪除物件。並且,智慧指標隱藏了釋放資源的細節,因此使用者不需要知道是否要呼叫 delete, 有些特殊的清除函式並不總是刪除資源的。

安全和高效的智慧指標是程式設計師的軍火庫中重要的武器。雖然C++標準庫中提供了 std::auto_ptr, 但是它不能完全滿足我們對智慧指標的需求。例如,auto_ptr不能用作STL容器的元素。Boost的智慧指標類填充了標準所留下來的缺口。

本章主要關注 scoped_ptr, shared_ptr, intrusive_ptr, 和 weak_ptr. 雖然剩下的 scoped_array 和 shared_array 有時候也很有用,但它們用的不是很多,而且它們與已討論的非常相近,這裡就不重複討論它們了。

Smart_ptr如何適應標準庫?

Smart_ptr庫已被提議包含進標準庫中,主要有以下三個原因:
  • 標準庫現在只提供了一個auto_ptr, 它僅是一類智慧指標,僅僅覆蓋了智慧指標族譜中的一個部分。shared_ptr 提供了不同的,也是更重要的功能。
  • Boost的智慧指標專門為了與標準庫良好合作而設計,並可作為標準庫的自然擴充。例如,在 shared_ptr之前,還沒有一個標準的智慧指標可用作容器的元素。
  • 長久以來,現實世界中的程式設計師已經在他們的程式中大量使用這些智慧指標類,它們已經得到了充分的驗證。

以上原因使得Smart_ptr庫成為了C++標準庫的一個非常有用的擴充。Boost.Smart_ptr的 shared_ptr (以及隨同的助手 enable_shared_from_this) 和 weak_ptr 已被收入即將釋出的Library Technical Report。

scoped_ptr

標頭檔案: "boost/scoped_ptr.hpp"

boost::scoped_ptr 用於確保動態分配的物件能夠被正確地刪除。scoped_ptr 有著與std::auto_ptr類似的特性,而最大的區別在於它不能轉讓所有權而auto_ptr可以。事實上,scoped_ptr永遠不能被複制或被賦值!scoped_ptr 擁有它所指向的資源的所有權,並永遠不會放棄這個所有權。scoped_ptr的這種特性提升了我們的程式碼的表現,我們可以根據需要選擇最合適的智慧指標(scoped_ptr 或 auto_ptr)。

要決定使用std::auto_ptr還是boost::scoped_ptr, 就要考慮轉移所有權是不是你想要的智慧指標的一個特性。如果不是,就用scoped_ptr. 它是一種輕量級的智慧指標;使用它不會使你的程式變大或變慢。它只會讓你的程式碼更安全,更好維護。

下面是scoped_ptr的摘要,以及其成員的簡要描述:

namespace boost {

  template<typename T> class scoped_ptr : noncopyable {
  public:
    explicit scoped_ptr(T* p = 0); 
    ~scoped_ptr(); 

    void reset(T* p = 0); 

    T& operator*() const; 
    T* operator->() const; 
    T* get() const; 
   
    void swap(scoped_ptr& b); 
  };

  template<typename T> 
    void swap(scoped_ptr<T> & a, scoped_ptr<T> & b); 
}


成員函式
  1. explicit scoped_ptr(T* p=0)
建構函式,儲存p的一份拷貝。注意,p 必須是用operator new分配的,或者是null. 在構造的時候,不要求T必須是一個完整的型別。當指標p是呼叫某個分配函式的結果而不是直接呼叫new得到的時候很有用:因為這個型別不必是完整的,只需要型別T的一個前向宣告就可以了。這個建構函式不會丟擲異常。
  1. ~scoped_ptr()
刪除被指物。型別T在被銷燬時必須是一個完整的型別。如果scoped_ptr在它被析構時並沒有儲存資源,它就什麼都不做。這個解構函式不會丟擲異常。
  1. void reset(T* p=0);
重置一個 scoped_ptr 就是刪除它已儲存的指標,如果它有的話,並重新儲存 p. 通常,資源的生存期管理應該完全由scoped_ptr自己處理,但是在極少數時候,資源需要在scoped_ptr的析構之前釋放,或者scoped_ptr要處理它原有資源之外的另外一個資源。這時,就可以用reset,但一定要儘量少用它。(過多地使用它通常表示有設計方面的問題) 這個函式不會丟擲異常。
  1. T& operator*() const;
返回一個到被儲存指標指向的物件的引用。由於不允許空的引用,所以解引用一個擁有空指標的scoped_ptr將導致未定義行為。如果不能肯定所含指標是否有效,就用函式get替代解引用。這個函式不會丟擲異常。
  1. T* operator->() const;
返回儲存的指標。如果儲存的指標為空,則呼叫這個函式會導致未定義行為。如果不能肯定指標是否空的,最好使用函式get。這個函式不會丟擲異常。
  1. T* get() const;
返回儲存的指標。應該小心地使用get,因為它可以直接操作裸指標。但是,get使得你可以測試儲存的指標是否為空。這個函式不會丟擲異常。get通常在呼叫那些需要裸指標的函式時使用。
  1. operator unspecified_bool_type() const
返回scoped_ptr是否為非空。返回值的型別是未指明的,但這個型別可被用於Boolean的上下文中。在if語句中最好使用這個型別轉換函式,而不要用get去測試scoped_ptr的有效性
  1. void swap(scoped_ptr& b)
交換兩個scoped_ptr的內容。這個函式不會丟擲異常。

普通函式
  1. template<typename T> void swap(scoped_ptr<T>& a,scoped_ptr<T>& b)
這個函式提供了交換兩個scoped pointer的內容的更好的方法。之所以說它更好,是因為 swap(scoped1,scoped2) 可以更廣泛地用於很多指標型別,包括裸指標和第三方的智慧指標。scoped1.swap(scoped2) 則只能用於它的定義所在的智慧指標,而不能用於裸指標。

用法

scoped_ptr的用法與普通的指標沒什麼區別;最大的差別在於你不必再記得在指標上呼叫delete,還有複製是不允許的。典型的指標操作(operator* 和 operator->)都被過載了,並提供了和裸指標一樣的語法。用scoped_ptr和用裸指標一樣快,也沒有大小上的增加,因此它們可以廣泛使用。使用boost::scoped_ptr時,包含標頭檔案"boost/scoped_ptr.hpp". 在宣告一個scoped_ptr時,用被指物的型別來指定類模板的引數。例如,以下是一個包含std::string指標的scoped_ptr:
  1. boost::scoped_ptr<std::string> p(new std::string("Hello"));
當scoped_ptr被銷燬時,它對它所擁有的指標呼叫delete 。

不需要手工刪除

讓我們看一個程式,它使用scoped_ptr來管理std::string指標。注意這裡沒有對delete的呼叫,因為scoped_ptr是一個自動變數,它會在離開作用域時被銷燬。
#include "boost/scoped_ptr.hpp"
#include <string>
#include <iostream>

int main() {
  {
  boost::scoped_ptr<std::string> 
  p(new std::string("Use scoped_ptr often."));

  // 列印字串的值
  if (p)
    std::cout << *p << '\n';
    
  // 獲取字串的大小
  size_t i=p->size();

  // 給字串賦新值
  *p="Acts just like a pointer";
  
  } // 這裡p被銷燬,並刪除std::string 
}


這段程式碼中有幾個地方值得註明一下。首先,scoped_ptr可以測試其有效性,就象一個普通指標那樣,因為它提供了隱式轉換到一個可用於布林表示式的型別的方法。其次,可以象使用裸指標那樣呼叫被指物的成員函式,因為過載了operator->. 第三,也可以和裸指標一樣解引用scoped_ptr,這歸功於operator*的過載。這些特性正是scoped_ptr和其它智慧指標的用處所在,因為它們和裸指標的不同之處在於對生存期管理的語義上,而不在於語法上。

和auto_ptr幾乎一樣

scoped_ptr 與 auto_ptr間的區別主要在於對擁有權的處理。auto_ptr在複製時會從源auto_ptr自動交出擁有權,而scoped_ptr則不允許被複制。看看下面這段程式,它把scoped_ptr 和 auto_ptr放在一起,你可以清楚地看到它們有什麼不同。
void scoped_vs_auto() {

  using boost::scoped_ptr;
  using std::auto_ptr;

  scoped_ptr<std::string> p_scoped(new std::string("Hello"));
  auto_ptr<std::string> p_auto(new std::string("Hello"));

  p_scoped->size();
  p_auto->size();

  scoped_ptr<std::string> p_another_scoped=p_scoped;
  auto_ptr<std::string> p_another_auto=p_auto;

  p_another_auto->size();
  (*p_auto).size();
}


這個例子不能通過編譯,因為scoped_ptr不能被複制構造或被賦值。auto_ptr既可以複製構造也可以賦值,但這們同時也意味著它把所有權從p_auto 轉移給了 p_another_auto, 在賦值後p_auto將只剩下一個空指標。這可能會導致令人不快的驚訝,就象你試圖把auto_ptr放入容器內時所發生的那樣。如果我們刪掉對p_another_scoped的賦值,程式就可以編譯了,但它的執行結果是不可預測的,因為它解引用了p_auto裡的空指標(*p_auto).

由於scoped_ptr::get會返回一個裸指標,所以就有可能對scoped_ptr做一些有害的事情,其中有兩件是你尤其要避免的。第一,不要刪除這個裸指標。因為它會在scoped_ptr被銷燬時再一次被刪除。第二,不要把這個裸指標儲存到另一個scoped_ptr (或其它任何的智慧指標)裡。因為這樣也會兩次刪除這個指標,每個scoped_ptr一次。簡單地說,儘量少用get, 除非你要使用那些要求你傳送裸指標的遺留程式碼!

scoped_ptr 和Pimpl用法

scoped_ptr可以很好地用於許多以前使用裸指標或auto_ptr的地方,如在實現pimpl用法時。pimpl 用法背後的思想是把客戶與所有關於類的私有部分的知識分隔開。由於客戶是依賴於類的標頭檔案的,標頭檔案中的任何變化都會影響客戶,即使僅是對私有節或保護節的修改。pimpl用法隱藏了這些細節,方法是將私有資料和函式放入一個單獨的類中,並儲存在一個實現檔案中,然後在標頭檔案中對這個類進行前向宣告並儲存一個指向該實現類的指標。類的建構函式分配這個pimpl類,而解構函式則釋放它。這樣可以消除標頭檔案與實現細節的相關性。我們來構造一個實現pimpl 用法的類,然後用智慧指標讓它更為安全。
// pimpl_sample.hpp

#if !defined (PIMPL_SAMPLE)
#define PIMPL_SAMPLE

class pimpl_sample {
  struct impl;  // 譯者注:原文中這句在class之外,與下文的實現程式碼有矛盾
  impl* pimpl_;
public:
  pimpl_sample();
  ~pimpl_sample();
  void do_something();
};

#endif


這是pimpl_sample類的介面。struct impl 是一個前向宣告,它把所有私有成員和函式放在另一個實現檔案中。這樣做的效果是使客戶與pimpl_sample類的內部細節完全隔離開來。
// pimpl_sample.cpp 

#include "pimpl_sample.hpp"
#include <string>
#include <iostream>

struct pimpl_sample::impl {
  void do_something_() {
    std::cout << s_ << "\n";
  }

  std::string s_;
};

pimpl_sample::pimpl_sample()
  : pimpl_(new impl) {
  pimpl_->s_ = "This is the pimpl idiom";
}

pimpl_sample::~pimpl_sample() {
  delete pimpl_;
}

void pimpl_sample::do_something() {
  pimpl_->do_something_();
}


看起來很完美,但並不是的。這個實現不是異常安全的!原因是pimpl_sample的建構函式有可能在pimpl被構造後丟擲一個異常。在建構函式中丟擲異常意味著已構造的物件並不存在,因此在棧展開時將不會呼叫它的解構函式。這樣就意味著分配給pimpl_指標的記憶體將洩漏。然而,有一樣簡單的解決方法:用scoped_ptr來解救!
class pimpl_sample {
  struct impl;
  boost::scoped_ptr<impl> pimpl_;
  ...
};


讓scoped_ptr來處理隱藏類impl的生存期管理,並從解構函式中去掉對impl的刪除(它不再需要,這要感謝scoped_ptr),這樣就做完了。但是,你必須記住要手工定義解構函式;原因是在編譯器生成隱式解構函式時,類impl還是不完整的,所以它的解構函式不能被呼叫。如果你用auto_ptr來儲存impl, 你可以編譯,但也還是有這個問題,但如果用scoped_ptr, 你將收到一個錯誤提示。

要注意的是,如果你使用scoped_ptr作為一個類的成員,你就必須手工定義這個類的複製建構函式和賦值操作符。原因是scoped_ptr是不能複製的,因此聚集了它的類也變得不能複製了。

最後一點值得注意的是,如果pimpl例項可以安全地被多個封裝類(在這裡是pimpl_sample)的例項所共享,那麼用boost::shared_ptr來管理pimpl的生存期才是正確的選擇。用shared_ptr比用scoped_ptr的優勢在於,不需要手工去定義複製建構函式和賦值操作符,而且可以定義空的解構函式,shared_ptr被設計為可以正確地用於未完成的類。

scoped_ptr 不同於 const auto_ptr

留心的讀者可能已經注意到auto_ptr可以幾乎象scoped_ptr一樣地工作,只要把auto_ptr宣告為const:
  1. const auto_ptr<A> no_transfer_of_ownership(new A);
它們很接近,但不是一樣。最大的區別在於scoped_ptr可以被reset, 在需要時可以刪除並替換被指物。而對於const auto_ptr這是不可能的。另一個小一點的區別是,它們的名字不同:儘管const auto_ptr意思上和scoped_ptr一樣,但它更冗長,也更不明顯。當你的詞典裡有了scoped_ptr,你就應該使用它,因為它可以更清楚地表明你的意圖。如果你想說一個資源是要被限制在作用域裡的,並且不應該有辦法可以放棄它的所有權,你就應該用 boost::scoped_ptr.

總結

使用裸指標來寫異常安全和無錯誤的程式碼是很複雜的。使用智慧指標來自動地把動態分配物件的生存期限制在一個明確的範圍之內,是解決這種問題的一個有效方法,並且提高了程式碼的可讀性、可維護性和質量。scoped_ptr 明確地表示被指物不能被共享和轉移。正如你所看到的,std::auto_ptr可以從另一個auto_ptr那裡竊取被指物,那怕是無意的,這被認為是auto_ptr的最大缺點。正是這個缺點使得scoped_ptr成為auto_ptr最好的補充。當一個動態分配的物件被傳送給scoped_ptr, 它就成為了這個物件的唯一的擁有者。因為scoped_ptr幾乎總是以自動變數或資料成員來分配的,因此它可以在離開作用域時正確地銷燬物件,從而在執行流由於返回語句或異常丟擲而離開作用域時,也總能釋放它所管理的記憶體。

在以下情況時使用 scoped_ptr :
  • 在可能有異常丟擲的作用域裡使用指標
  • 函式裡有幾條控制路徑
  • 動態分配物件的生存期應被限制於特定的作用域內
  • 異常安全非常重要時(總應如此!)

scoped_array

標頭檔案: "boost/scoped_array.hpp"

需要動態分配陣列時,通常最好用std::vector來實現,但是有兩種情形看起來用陣列更適合: 一種是為了優化,用vector多少有一些額外的記憶體和速度開銷;另一種是為了某種原因,要求陣列的大小必須是固定的。動態分配的陣列會遇到與普通指標一樣的危險,並且還多了一個(也是最常見的一個),那就是錯誤呼叫delete操作符而不是delete[]操作符來釋放陣列。我曾經在你想象不到的地方見到過這個錯誤,那也是它常被用到的地方,就是在你自己實現的容器類裡!scoped_array 為陣列做了scoped_ptr為單個物件指標所做的事情:它負責釋放記憶體。區別只在於scoped_array 是用delete[] 操作符來做這件事的。

scoped_array是一個單獨的類而不是scoped_ptr的一個特化,其原因是,因為不可能用超程式設計技術來區分指向單個物件的指標和指向陣列的指標。不管如何努力,也沒有人能發現一種可靠的方法,因為陣列太容易退化為指標了,這使得沒有型別資訊可以表示它們是指向陣列的。結果,只能由你來負責,使用scoped_array而不是scoped_ptr,就如你必須用delete[]操作符而不是用delete操作符一樣。這樣的好處是scoped_array 負責為你處理釋放記憶體的事情,而你則告訴scoped_array 我們要處理的是陣列,而不是裸指標。

scoped_array與scoped_ptr非常相似,不同的是它提供了operator[] 來模仿一個裸陣列。

scoped_array 是比普通的動態分配陣列更好用。它處理了動態分配陣列的生存期管理問題,就如scoped_ptr管理物件指標的生存期一樣。但是記住,多數情況下應該使用std::vector,它更靈活、更強大。只有當你需要確保陣列的大小是固定的時候,才使用scoped_array 來替代 std::vector.

shared_ptr

標頭檔案: "boost/shared_ptr.hpp"

幾乎所有稍微複雜點的程式都需要某種形式的引用計數智慧指標。這些智慧指標讓我們不再需要為了管理被兩個或多個物件共享的物件的生存期而編寫複雜的邏輯。當引用計數降為零,沒有物件再需要這個共享的物件時,這個物件就自動被銷燬了。引用計數智慧指標可以分為插入式(intrusive)和非插入式(non-intrusive)兩類。前者要求它所管理的類提供明確的函式或資料成員用於管理引用計數。這意味著在類的設計時就必須預見到它將與一個插入式的引用計數智慧指標一起工作,或者重新設計它。非插入式的引用計數智慧指標對它所管理的類沒有任何要求。引用計數智慧指標擁有與它所存指標有關的記憶體的所有權。沒有智慧指標的幫助,物件的共享會存在問題,必須有人負負責刪除共享的記憶體。誰負責?什麼時候刪除?沒有智慧指標,你必須在管理的記憶體之外增加生存期的管理,這意味著在各個擁有者之間存在更強的依賴關係。換言之,沒有了重用性並增加了複雜性。

被管理的類可能擁有一些特性使得它更應該與引用計數智慧指標一起使用。例如,它的複製操作很昂貴,或者它所代表的有些東西必須被多個例項共享,這些特性都值得去共享所有權。還有一種情形是共享的資源沒有一個明確的擁有者。使用引用計數智慧指標可以在需要訪問共享資源的物件之間共享資源的所有權。引用計數智慧指標還讓你可以把物件指標存入標準庫的容器中而不會有洩漏的風險,特別是在面對異常或要從容器中刪除元素的時候。如果你把指標放入容器,你就可以獲得多型的好處,可以提高效能(如果複製的代價很高的話),還可以通過把相同的物件放入多個輔助容器來進行特定的查詢。

在你決定使用引用計數智慧指標後,你應該選擇插入式的還是非插入式的?非插入式智慧指標幾乎總是更好的選擇,由於它們的通用性、不需要修改已有程式碼,以及靈活性。你可以對你不能或不想修改的類使用非插入式的引用計數智慧指標。而把一個類修改為使用插入式引用計數智慧指標的常見方法是從一個引用計數基類派生。這種修改可能比你想象的更昂貴。至少,它增加了相關性並降低了重用性。它還增加了物件的大小,這在一些特定環境中可能會限制其可用性。

shared_ptr 可以從一個裸指標、另一個shared_ptr、一個std::auto_ptr、或者一個boost::weak_ptr構造。還可以傳遞第二個引數給shared_ptr的建構函式,它被稱為刪除器(deleter)。刪除器稍後會被呼叫,來處理共享資源的釋放。這對於管理那些不是用new分配也不是用delete釋放的資源時非常有用(稍後將看到建立客戶化刪除器的例子)。shared_ptr被建立後,它就可象普通指標一樣使用了,除了一點,它不能被顯式地刪除。

以下是shared_ptr的部分摘要;最重要的成員和相關普通函式被列出,隨後是簡單的討論。

namespace boost {

  template<typename T> class shared_ptr {
  public:
    template <class Y> explicit shared_ptr(Y* p);
    template <class Y,class D> shared_ptr(Y* p,D d);

    ~shared_ptr();

    shared_ptr(const shared_ptr & r);
    template <class Y> explicit 
      shared_ptr(const weak_ptr<Y>& r);
    template <class Y> explicit shared_ptr(std::auto_ptr<Y>& r);

    shared_ptr& operator=(const shared_ptr& r);

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

    bool unique() const;
    long use_count() const;

    operator unspecified_bool_type() const;  //譯註:原文是unspecified-bool-type(),有誤

    void swap(shared_ptr<T>& b);
  };

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


成員函式
  1. template <class Y> explicit shared_ptr(Y* p);
這個建構函式獲得給定指標p的所有權。引數 p 必須是指向 Y 的有效指標。構造後引用計數設為1。唯一從這個建構函式丟擲的異常是std::bad_alloc (僅在一種很罕見的情況下發生,即不能獲得引用計數器所需的自由空間)。
  1. template <class Y,class D> shared_ptr(Y* p,D d);
這個建構函式帶有兩個引數。第一個是shared_ptr將要獲得所有權的那個資源,第二個是shared_ptr被銷燬時負責釋放資源的一個物件,被儲存的資源將以d(p)的形式傳給那個物件。因此p的值是否有效取決於d。如果引用計數器不能分配成功,shared_ptr丟擲一個型別為std::bad_alloc的異常。
  1. shared_ptr(const shared_ptr& r);
r中儲存的資源被新構造的shared_ptr所共享,引用計數加一。這個建構函式不會丟擲異常。
  1. template <class Y> explicit shared_ptr(const weak_ptr<Y>& r);
從一個weak_ptr (本章稍後會介紹)構造shared_ptr。這使得weak_ptr的使用具有執行緒安全性,因為指向weak_ptr引數的共享資源的引用計數將會自增(weak_ptr不影響共享資源的引用計數)。如果weak_ptr為空 (r.use_count()==0), shared_ptr 丟擲一個型別為bad_weak_ptr的異常。
  1. template <typename Y> shared_ptr(std::auto_ptr<Y>& r);
複製程式碼 這個建構函式從一個auto_ptr獲取r中儲存的指標的所有權,方法是儲存指標的一份拷貝並對auto_ptr呼叫release。構造後的引用計數為1。而r當然就變為空的。如果引用計數器不能分配成功,則丟擲 std::bad_alloc 。
  1. ~shared_ptr();
複製程式碼 shared_ptr解構函式對引用計數減一。如果計數為零,則儲存的指標被刪除。刪除指標的方法是呼叫operator delete 或者,如果給定了一個執行刪除操作的客戶化刪除器物件,就把儲存的指標作為唯一引數呼叫這個物件。解構函式不會丟擲異常。
  1. shared_ptr& operator=(const shared_ptr& r);  
複製程式碼 賦值操作共享r中的資源,並停止對原有資源的共享。賦值操作不會丟擲異常。
  1. void reset();
複製程式碼 reset函式用於停止對儲存指標的所有權的共享。共享資源的引用計數減一。
  1. T& operator*() const;
複製程式碼 這個操作符返回對已存指標所指向的物件的一個引用。如果指標為空,呼叫operator* 會導致未定義行為。這個操作符不會丟擲異常。
  1. T* operator->() const;
複製程式碼 這個操作符返回儲存的指標。這個操作符與operator*一起使得智慧指標看起來象普通指標。這個操作符不會丟擲異常。
  1. T* get() const;
複製程式碼 get函式是當儲存的指標有可能為空時(這時 operator* 和 operator-> 都會導致未定義行為)獲取它的最好辦法。注意,你也可以使用隱式布林型別轉換來測試 shared_ptr 是否包含有效指標。這個函式不會丟擲異常。
  1. bool unique() const;
複製程式碼 這個函式在shared_ptr是它所儲存指標的唯一擁有者時返回 true ;否則返回 false。 unique 不會丟擲異常。
  1. long use_count() const;
複製程式碼 use_count 函式返回指標的引用計數。它在除錯的時候特別有用,因為它可以在程式執行的關鍵點獲得引用計數的快照。小心地使用它,因為在某些可能的shared_ptr實現中,計算引用計數可能是昂貴的,甚至是不行的。這個函式不會丟擲異常。
  1. operator unspecified-bool-type() const;
複製程式碼 這是個到unspecified-bool-type型別的隱式轉換函式,它可以在Boolean上下文中測試一個智慧指標。如果shared_ptr儲存著一個有效的指標,返回值為True;否則為false。注意,轉換函式返回的型別是不確定的。把返回型別當成bool用會導致一些荒謬的操作,所以典型的實現採用了safe bool idiom,它很好地確保了只有可適用的Boolean測試可以使用。這個函式不會丟擲異常。
  1. void swap(shared_ptr<T>& b);
複製程式碼 這可以很方便地交換兩個shared_ptr。swap 函式交換儲存的指標(以及它們的引用計數)。這個函式不會丟擲異常。

普通函式
  1. template <typename T,typename U>
  2.   shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r);
複製程式碼 要對儲存在shared_ptr裡的指標執行static_cast,我們可以取出指標然後強制轉換它,但我們不能把它存到另一個shared_ptr裡;新的 shared_ptr 會認為它是第一個管理這些資源的。解決的方法是用 static_pointer_cast. 使用這個函式可以確保被指物的引用計數保持正確。static_pointer_cast 不會丟擲異常。

用法

使用shared_ptr解決的主要問題是知道刪除一個被多個客戶共享的資源的正確時機。下面是一個簡單易懂的例子,有兩個類 A 和 B, 它們共享一個int例項。使用 boost::shared_ptr, 你需要必須包含 "boost/shared_ptr.hpp".
  1. #include "boost/shared_ptr.hpp"
  2. #include <cassert>
  3. class A {
  4.   boost::shared_ptr<int> no_;
  5. public:
  6.   A(boost::shared_ptr<int> no) : no_(no) {}
  7.   void value(int i) {
  8.     *no_=i;
  9.   }
  10. };
  11. class B {
  12.   boost::shared_ptr<int> no_;
  13. public:
  14.   B(boost::shared_ptr<int> no) : no_(no) {}
  15.   int value() const {
  16.     return *no_;
  17.   }
  18. };
  19. int main() {
  20.     boost::shared_ptr<int> temp(new int(14));
  21.     A a(temp);
  22.     B b(temp);
  23.     a.value(28);
  24.     assert(b.value()==28);
  25. }
複製程式碼 類 A 和 B都儲存了一個 shared_ptr<int>. 在建立 A 和 B的例項時,shared_ptr temp 被傳送到它們的建構函式。這意味著共有三個 shared_ptr:a, b, 和 temp,它們都引向同一個int例項。如果我們用指標來實現對一個的共享,A 和 B 必須能夠在某個時間指出這個int要被刪除。在這個例子中,直到main的結束,引用計數為3,當所有 shared_ptr離開了作用域,計數將達到0,而最後一個智慧指標將負責刪除共享的 int.

回顧Pimpl用法

前一節展示了使用scoped_ptr的pimpl 用法,如果使用這種用法的類是不允許複製的,那麼scoped_ptr在儲存pimpl的動態分配例項時它工作得很好。但是這並不適合於所有想從pimpl用法中獲益的型別(注意,你還可以用 scoped_ptr,但必須手工實現複製建構函式和賦值操作符)。對於那些可以處理共享的實現細節的類,應該用 shared_ptr。當pimpl的所有權被傳遞給一個 shared_ptr, 複製和賦值操作都是免費的。你可以回憶起,當使用 scoped_ptr 去處理pimpl類的生存期時,對封裝類的複製是不允許的,因為 scoped_ptr是不可複製的。這意味著要使這些類支援複製和賦值,你必須手工定義複製建構函式和賦值操作符。當使用 shared_ptr 去處理pimpl類的生存期時,就不再需要使用者自己定義複製構造函數了。注意,這時pimpl例項是被該類的多個物件所共享,因此如果規則是每個pimpl例項只能被類的一個例項使用,你還是要手工編寫複製建構函式。解決的方法和我們在scoped_ptr那看到的很相似,只是把scoped_ptr換成了shared_ptr。

shared_ptr 與標準庫容器

把物件直接存入容器中有時會有些麻煩。以值的方式儲存物件意味著使用者將獲得容器中的元素的拷貝,對於那些複製是一種昂貴的操作的型別來說可能會有效能的問題。此外,有些容器,特別是 std::vector, 當你加入元素時可能會複製所有元素,這更加重了效能的問題。最後,傳值的語義意味著沒有多型的行為。如果你需要在容器中存放多型的物件而且你不想切割它們,你必須用指標。如果你用裸指標,維護元素的完整性會非常複雜。從容器中刪除元素時,你必須知道容器的使用者是否還在引用那些要刪除的元素,不用擔心多個使用者使用同一個元素。這些問題都可以用shared_ptr來解決。

下面是如何把共享指標存入標準庫容器的例子。
  1. #include "boost/shared_ptr.hpp"
  2. #include <vector>
  3. #include <iostream>
  4. class A {
  5. public:
  6.   virtual void sing()=0;
  7. protected:
  8.   virtual ~A() {};
  9. };
  10. class B : public A {
  11. public:
  12.   virtual void sing() {
  13.     std::cout << "Do re mi fa so la";
  14.   }
  15. };
  16. boost::shared_ptr<A> createA() {
  17.   boost::shared_ptr<A> p(new B());
  18.   return p;
  19. }
  20. int main() {
  21.   typedef std::vector<boost::shared_ptr<A> > container_type;
  22.   typedef container_type::iterator iterator;
  23.   container_type container;
  24.   for (int i=0;i<10;++i) {
  25.     container.push_back(createA());
  26.   }
  27.   std::cout << "The choir is gathered: \n";
  28.   iterator end=container.end();
  29.   for (iterator it=container.begin();it!=end;++it) {
  30.     (*it)->sing();
  31.   }
  32. }
複製程式碼 這裡有兩個類, A 和 B, 各有一個虛擬成員函式 sing. B 從 A公有繼承而來,並且如你所見,工廠函式 createA 返回一個動態分配的B的例項,包裝在shared_ptr<A>裡。在 main裡, 一個包含shared_ptr<A>的 std::vector 被放入10個元素,最後對每個元素呼叫sing。如果我們用裸指標作為元素,那些物件需要被手工刪除。而在這個例子裡,刪除是自動的,因為在vector的生存期中,每個shared_ptr的引用計數都保持為1;當 vector 被銷燬,所有引用計數器都將變為零,所有物件都被刪除。有趣的是,即使 A 的解構函式沒有宣告為 virtual, shared_ptr 也會正確呼叫 B的解構函式!

上面的例子示範了一個強有力的技術,它涉及A裡面的protected解構函式。因為函式 createA 返回的是 shared_ptr<A>, 因此不可能對shared_ptr::get返回的指標呼叫 delete 。這意味著如果為了向某個需要裸指標的函式傳送裸指標而從shared_ptr中取出裸指標的話,它不會由於意外地被刪除而導致災難。那麼,又是如何允許 shared_ptr 刪除它的物件的呢? 這是因為指標指向的真正型別是 B; 而B的解構函式不是protected的。這是非常有用的方法,用於給shared_ptr中的物件增加額外的安全性。

shared_ptr 與其它資源

有時你會發現你要把shared_ptr用於某個特別的型別,它需要其它清除操作而不是簡單的 delete. shared_ptr可以通過客戶化刪除器來支援這種需要。那些處理象 FILE*這樣的作業系統控制代碼的資源通常要使用象fclose這樣的操作來釋放。要在shared_ptr裡使用 FILE* ,我們要定義一個類來負責釋放相應的資源。
  1. class FileCloser {
  2. public:
  3.    void operator()(FILE* file) {
  4.     std::cout << "The FileCloser has been called with a FILE*, "
  5.       "which will now be closed.\n";
  6.     if (file!=0)
  7.       fclose(file);
  8.   }
  9. };
複製程式碼 這是一個函式物件,我們用它來確保在資源要釋放時呼叫 fclose 。下面是使用FileCloser類的示例程式。
  1. int main() {
  2.   std::cout <<
  3.     "shared_ptr example with a custom deallocator.\n";
  4.   {
  5.     FILE* f=fopen("test.txt","r");
  6.     if (f==0) {
  7.       std::cout << "Unable to open file\n";
  8.       throw "Unable to open file";
  9.     }
  10.     boost::shared_ptr<FILE>
  11.       my_shared_file(f, FileCloser());
  12.     // 定位檔案指標
  13.     fseek(my_shared_file.get(),42,SEEK_SET);
  14.   }
  15.   std::cout << "By now, the FILE has been closed!\n";
  16. }
複製程式碼 注意,在訪問資源時,我們需要對shared_ptr使用 &* 用法, get, 或 get_pointer。(請注意最好使用 &*. 另兩個選擇不太清晰) 這個例子還可以更簡單,如果我們在釋放資源時只需要呼叫一個單引數函式的話,就根本不需要建立一個客戶化刪除器型別。上面的例子可以重寫如下:
  1. {
  2.   FILE* f=fopen("test.txt","r");
  3.   if (f==0) {
  4.     std::cout << "Unable to open file\n";
  5.     throw file_exception();
  6.   }
  7.   boost::shared_ptr<FILE> my_shared_file(f,&fclose);
  8.   // 定位檔案指標
  9.   fseek(&*my_shared_file,42,SEEK_SET);
  10. }
  11. std::cout << "By now, the FILE* has been closed!\n";
複製程式碼 定製刪除器在處理需要特殊釋放程式的資源時非常有用。由於刪除器不是 shared_ptr 型別的一部分,所以使用者不需要知道關於智慧指標所擁有的資源的任何資訊(當然除了如何使用它!)。例如,你可以使用物件池,定製刪除器只需簡單地把物件返還到池中。或者,一個 singleton 物件應該使用一個什麼都不做的刪除器。

使用定製刪除器的安全性

我們已經看到對基類使用 protected 解構函式有助於增加使用shared_ptr的類的安全性。另一個達到同樣安全級別的方法是,宣告解構函式為 protected (或 private) 並使用一個定製刪除器來負責銷燬物件。這個定製刪除器必須是它要刪除的類的友元,這樣它才可以工作。封裝這個刪除器的好方法是把它實現為私有的巢狀類,如下例所示:
  1. #include "boost/shared_ptr.hpp"
  2. #include <iostream>
  3. class A {
  4.   class deleter {
  5.     public:
  6.       void operator()(A* p) {
  7.         delete p;
  8.       }
  9.   };
  10.   friend class deleter;
  11. public:
  12.   virtual void sing() {
  13.     std::cout << "Lalalalalalalalalalala";
  14.   }
  15.   static boost::shared_ptr<A> createA() {
  16.     boost::shared_ptr<A> p(new A(),A::deleter());
  17.     return p;
  18.   }
  19. protected:
  20.   virtual ~A() {};
  21. };
  22. int main() {
  23.   boost::shared_ptr<A> p=A::createA();
  24. }
複製程式碼 注意,我們在這裡不能使用普通函式來作為 shared_ptr<A> 的工廠函式,因為巢狀的刪除器是A私有的。使用這個方法,使用者不可能在棧上建立 A的物件,也不可能對A的指標呼叫 delete 。

從this建立shared_ptr

有時候,需要從this獲得 shared_ptr ,即是說,你希望你的類被shared_ptr所管理,你需要把"自身"轉換為shared_ptr的方法。看起來不可能?好的,解決方案來自於我們即將討論的另一個智慧指標boost::weak_ptr. weak_ptr 是 shared_ptr的一個觀察者;它只是安靜地坐著並看著它們,但不會影響引用計數。通過儲存一個指向this的 weak_ptr 作為類的成員,就可以在需要的時候獲得一個指向this的 shared_ptr。為了你可以不必編寫程式碼來儲存一個指向this的 weak_ptr,接著又從weak_ptr獲shared_ptr得,Boost.Smart_ptr 為這個任務提供了一個助手類,稱為 enable_shared_from_this. 只要簡單地讓你的類公有地派生自 enable_shared_from_this,然後在需要訪問管理this的shared_ptr時,使用函式 shared_from_this 就行了。下面的例子示範瞭如何使用 enable_shared_from_this :
  1. #include "boost/shared_ptr.hpp"
  2. #include "boost/enable_shared_from_this.hpp"
  3. class A;
  4. void do_stuff(boost::shared_ptr<A> p) {
  5.   ...
  6. }
  7. class A : public boost::enable_shared_from_this<A> {
  8. public:
  9.   void call_do_stuff() {
  10.     do_stuff(shared_from_this());
  11.   }
  12. };
  13. int main() {
  14.   boost::shared_ptr<A> p(new A());
  15.   p->call_do_stuff();
  16. }
複製程式碼 這個例子還示範了你要用shared_ptr管理this的情形。類 A 有一個成員函式 call_do_stuff 需要呼叫一個普通函式 do_stuff, 這個普通函式需要一個型別為 boost:: shared_ptr<A>的引數。現在,在 A::call_do_stuff裡, this 不過是一個 A指標, 但由於 A 派生自 enable_shared_from_this, 呼叫 shared_from_this 將返回我們所要的 shared_ptr 。在enable_shared_from_this的成員函式 shared_from_this裡,內部儲存的 weak_ptr 被轉換為 shared_ptr, 從而增加了相應的引用計數,以確保相應的物件不會被刪除。

總結

引用計數智慧指標是非常重要的工具。Boost的 shared_ptr 提供了堅固而靈活的解決方案,它已被廣泛用於多種環境下。需要在使用者之間共享物件是常見的,而且通常沒有辦法通知使用者何時刪除物件是安全的。shared_ptr 讓使用者無需知道也在使用共享物件的其它物件,並讓它們無需擔心在沒有物件引用時的資源釋放。這對於Boost的智慧指標類而言是最重要的。你會看到Boost.Smart_ptr中還有其它的智慧指標,但這一個肯定是你最想要的。通過使用定製刪除器,幾乎所有資源型別都可以存入 shared_ptr。這使得shared_ptr 成為處理資源管理的通用類,而不僅僅是處理動態分配物件。與裸指標相比,shared_ptr會有一點點額外的空間代價。我還沒有發現由於這些代價太大而需要另外尋找一個解決方案的情形。不要去建立你自己的引用計數智慧指標類。沒有比使用 shared_ptr智慧指標更好的了。
在以下情況時使用 shared_ptr :
  • 當有多個使用者使用同一個物件,而沒有一個明顯的擁有者時
  • 當要把指標存入標準庫容器時
  • 當要傳送物件到庫或從庫獲取物件,而沒有明確的所有權時
  • 當管理一些需要特殊清除方式的資源時