boost>shared_ptr

由於前輩們在開發事廣泛使用了boost的shared_ptr因此我打算自己瞭解一些這個shared_ptr。

 

shared_ptr應該就是一種智能指針,這一概念在C++primer當中有提到過。或者稱“引用計數指針”

 

boost shared_ptr的教程內容網上有不少,其中比較詳細的要算《boost庫導論》了。

 
 

shared_ptr

頭文件: "boost/shared_ptr.hpp"

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

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

成員函數

template <class Y> explicit shared_ptr(Y* p);

這個構造函數獲得給定指針p的所有權。參數 p 必須是指向 Y 的有效指針。構造後引用計數設爲1。唯一從這個構造函數拋出的異常是std::bad_alloc (僅在一種很罕見的情況下發生,即不能獲得引用計數器所需的自由空間)。

template <class Y,class D> shared_ptr(Y* p,D d);

這個構造函數帶有兩個參數。第一個是shared_ptr將要獲得所有權的那個資源,第二個是shared_ptr被銷燬時負責釋放資源的一個對象,被保存的資源將以d(p)的形式傳給那個對象。因此p的值是否有效取決於d。如果引用計數器不能分配成功,shared_ptr拋出一個類型爲std::bad_alloc的異常。

shared_ptr(const shared_ptr& r);

r中保存的資源被新構造的shared_ptr所共享,引用計數加一。這個構造函數不會拋出異常。

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的異常。

template <typename Y> shared_ptr(std::auto_ptr<Y>& r);

這個構造函數從一個auto_ptr獲取r中保存的指針的所有權,方法是保存指針的一份拷貝並對auto_ptr調用release。構造後的引用計數爲1。而r當然就變爲空的。如果引用計數器不能分配成功,則拋出 std::bad_alloc

~shared_ptr();

shared_ptr析構函數對引用計數減一。如果計數爲零,則保存的指針被刪除。刪除指針的方法是調用operator delete 或者,如果給定了一個執行刪除操作的客戶化刪除器對象,就把保存的指針作爲唯一參數調用這個對象。析構函數不會拋出異常。

shared_ptr& operator=(const shared_ptr& r);  

賦值操作共享r中的資源,並停止對原有資源的共享。賦值操作不會拋出異常。

void reset();

reset函數用於停止對保存指針的所有權的共享。共享資源的引用計數減一。

T& operator*() const;

這個操作符返回對已存指針所指向的對象的一個引用。如果指針爲空,調用operator* 會導致未定義行爲。這個操作符不會拋出異常。

T* operator->() const;

這個操作符返回保存的指針。這個操作符與operator*一起使得智能指針看起來象普通指針。這個操作符不會拋出異常。

T* get() const;

get函數是當保存的指針有可能爲空時(這時 operator*operator-> 都會導致未定義行爲)獲取它的最好辦法。注意,你也可以使用隱式布爾類型轉換來測試 shared_ptr 是否包含有效指針。這個函數不會拋出異常。

bool unique() const;

這個函數在shared_ptr是它所保存指針的唯一擁有者時返回 true ;否則返回 falseunique 不會拋出異常。

long use_count() const;
      

use_count 函數返回指針的引用計數。它在調試的時候特別有用,因爲它可以在程序執行的關鍵點獲得引用計數的快照。小心地使用它,因爲在某些可能的shared_ptr實現中,計算引用計數可能是昂貴的,甚至是不行的。這個函數不會拋出異常。

operator unspecified-bool-type() const;

這是個到unspecified-bool-type類型的隱式轉換函數,它可以在Boolean上下文中測試一個智能指針。如果shared_ptr保存着一個有效的指針,返回值爲True;否則爲false。注意,轉換函數返回的類型是不確定的。把返回類型當成bool用會導致一些荒謬的操作,所以典型的實現採用了safe bool idiom,[8] 它很好地確保了只有可適用的Boolean測試可以使用。這個函數不會拋出異常。

[8] 由Peter Dimov發明的。

void swap(shared_ptr<T>& b);

這可以很方便地交換兩個shared_ptrswap 函數交換保存的指針(以及它們的引用計數)。這個函數不會拋出異常。

普通函數

template <typename T,typename U>
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解決的主要問題是知道刪除一個被多個客戶共享的資源的正確時機。下面是一個簡單易懂的例子,有兩個類 AB, 它們共享一個int實例。使用 boost::shared_ptr, 你需要必須包含 "boost/shared_ptr.hpp".

#include "boost/shared_ptr.hpp"
#include <cassert>

class A {
boost::shared_ptr<int> no_;
public:
A(boost::shared_ptr<int> no) : no_(no) {}
void value(int i) {
*no_=i;
}
};

class B {
boost::shared_ptr<int> no_;
public:
B(boost::shared_ptr<int> no) : no_(no) {}
int value() const {
return *no_;
}
};

int main() {
boost::shared_ptr<int> temp(new int(14));
A a(temp);
B b(temp);
a.value(28);
assert(b.value()==28);
}

AB都保存了一個 shared_ptr<int>. 在創建 AB的實例時,shared_ptr temp 被傳送到它們的構造函數。這意味着共有三個 shared_ptra, b, 和 temp,它們都引向同一個int實例。如果我們用指針來實現對一個的共享,AB 必須能夠在某個時間指出這個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來解決。

下面是如何把共享指針存入標準庫容器的例子。

#include "boost/shared_ptr.hpp"
#include <vector>
#include <iostream>

class A {
public:
virtual void sing()=0;
protected:
virtual ~A() {};
};

class B : public A {
public:
virtual void sing() {
std::cout << "Do re mi fa so la";
}
};

boost::shared_ptr<A> createA() {
boost::shared_ptr<A> p(new B());
return p;
}

int main() {
typedef std::vector<boost::shared_ptr<A> > container_type;
typedef container_type::iterator iterator;

container_type container;
for (int i=0;i<10;++i) {
container.push_back(createA());
}

std::cout << "The choir is gathered: /n";
iterator end=container.end();
for (iterator it=container.begin();it!=end;++it) {
(*it)->sing();
}
}

這裏有兩個類, AB, 各有一個虛擬成員函數 sing. BA公有繼承而來,並且如你所見,工廠函數 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用於某個特別的類型,它需要其它清除操作而不是簡單的 deleteshared_ptr可以通過客戶化刪除器來支持這種需要。那些處理象 FILE*這樣的操作系統句柄的資源通常要使用象fclose這樣的操作來釋放。要在shared_ptr裏使用 FILE* ,我們要定義一個類來負責釋放相應的資源。

class FileCloser {
public:
void operator()(FILE* file) {
std::cout << "The FileCloser has been called with a FILE*, "
"which will now be closed./n";
if (file!=0)
fclose(file);
}
};

這是一個函數對象,我們用它來確保在資源要釋放時調用 fclose 。下面是使用FileCloser類的示例程序。

int main() {
std::cout <<
"shared_ptr example with a custom deallocator./n";
{
FILE* f=fopen("test.txt","r");
if (f==0) {
std::cout << "Unable to open file/n";
throw "Unable to open file";
}

boost::shared_ptr<FILE>
my_shared_file(f, FileCloser());

// 定位文件指針
fseek(my_shared_file.get(),42,SEEK_SET);
}
std::cout << "By now, the FILE has been closed!/n";
}

注意,在訪問資源時,我們需要對shared_ptr使用 &* 用法, get, 或 get_pointer。(請注意最好使用 &*. 另兩個選擇不太清晰) 這個例子還可以更簡單,如果我們在釋放資源時只需要調用一個單參數函數的話,就根本不需要創建一個客戶化刪除器類型。上面的例子可以重寫如下:

{
FILE* f=fopen("test.txt","r");
if (f==0) {
std::cout << "Unable to open file/n";
throw file_exception();
}

boost::shared_ptr<FILE> my_shared_file(f,&fclose);

// 定位文件指針
fseek(&*my_shared_file,42,SEEK_SET);
}
std::cout << "By now, the FILE* has been closed!/n";

定製刪除器在處理需要特殊釋放程序的資源時非常有用。由於刪除器不是 shared_ptr 類型的一部分,所以使用者不需要知道關於智能指針所擁有的資源的任何信息(當然除了如何使用它!)。例如,你可以使用對象池,定製刪除器只需簡單地把對象返還到池中。或者,一個 singleton 對象應該使用一個什麼都不做的刪除器。

使用定製刪除器的安全性

我們已經看到對基類使用 protected 析構函數有助於增加使用shared_ptr的類的安全性。另一個達到同樣安全級別的方法是,聲明析構函數爲 protected (或 private) 並使用一個定製刪除器來負責銷燬對象。這個定製刪除器必須是它要刪除的類的友元,這樣它纔可以工作。封裝這個刪除器的好方法是把它實現爲私有的嵌套類,如下例所示:

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

class A {
class deleter {
public:
void operator()(A* p) {
delete p;
}
};
friend class deleter;
public:

virtual void sing() {
std::cout << "Lalalalalalalalalalala";
}

static boost::shared_ptr<A> createA() {
boost::shared_ptr<A> p(new A(),A::deleter());
return p;
}

protected:
virtual ~A() {};
};

int main() {
boost::shared_ptr<A> p=A::createA();
}

注意,我們在這裏不能使用普通函數來作爲 shared_ptr<A> 的工廠函數,因爲嵌套的刪除器是A私有的。使用這個方法,用戶不可能在棧上創建 A的對象,也不可能對A的指針調用 delete

從this創建shared_ptr  

有時候,需要從this獲得 shared_ptr ,即是說,你希望你的類被shared_ptr所管理,你需要把"自身"轉換爲shared_ptr的方法。看起來不可能?好的,解決方案來自於我們即將討論的另一個智能指針boost::weak_ptrweak_ptrshared_ptr的一個觀察者;它只是安靜地坐着並看着它們,但不會影響引用計數。通過存儲一個指向thisweak_ptr 作爲類的成員,就可以在需要的時候獲得一個指向thisshared_ptr。爲了你可以不必編寫代碼來保存一個指向thisweak_ptr,接着又從weak_ptrshared_ptr得,Boost.Smart_ptr 爲這個任務提供了一個助手類,稱爲 enable_shared_from_this. 只要簡單地讓你的類公有地派生自 enable_shared_from_this,然後在需要訪問管理thisshared_ptr時,使用函數 shared_from_this 就行了。下面的例子示範瞭如何使用 enable_shared_from_this

#include "boost/shared_ptr.hpp"
#include "boost/enable_shared_from_this.hpp"

class A;

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

class A : public boost::enable_shared_from_this<A> {
public:
void call_do_stuff() {
do_stuff(shared_from_this());
}
};

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

這個例子還示範了你要用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

  • 當有多個使用者使用同一個對象,而沒有一個明顯的擁有者時

  • 當要把指針存入標準庫容器時

  • 當要傳送對象到庫或從庫獲取對象,而沒有明確的所有權時

  • 當管理一些需要特殊清除方式的資源時[9]

    [9] 通過定製刪除器的幫助。

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