Beyond the C++ Standard Library: An Introduction to Boost — Library 1.4 shared_ptr

來源:互聯網
上載者:User
shared_ptr

標頭檔: "boost/shared_ptr.hpp"

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

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

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

[6] 考慮一下對同一個類型使用兩個以上引用計數智能指標的需要。如果兩個都是插入式的,兩個不同的基類可能會不相容,而且也很浪費。如果其中一個是插入式的,那麼使用非插入式的智能指標可以使基類的額外負擔為零。

[7] 另一方面,非插入式智能指標要求額外的儲存用於智能指標本身。

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);
}

成員函數
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 ;否則返回 false。 unique 不會拋出異常。

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_ptr。swap 函數交換儲存的指標(以及它們的引用計數)。這個函數不會拋出異常。

普通函數
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解決的主要問題是知道刪除一個被多個客戶共用的資源的正確時機。下面是一個簡單易懂的例子,有兩個類 A 和 B, 它們共用一個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);
}

類 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來解決。

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

#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();
}
}

這裡有兩個類, 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* ,我們要定義一個類來負責釋放相應的資源。

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_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 :

#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] 通過定製刪除器的協助。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.