boost.png (6897 bytes)shared_ptr 類模板

Introduction 簡介
Best Practices 最佳實踐
Synopsis 概要
Members 成員
Free Functions 自由函數
Example 示例
Handle/Body Idiom Handle/Body 慣用法
Thread Safety 線程安全
Frequently Asked Questions 常見問題
Smart Pointer Timings 智能指針測時
Programming Techniques 編程技術

Introduction 簡介

shared_ptr 類模板存儲一個指向動態分配對像(一般是用 C++ new-expression 生成的)的指針。在最後一個 shared_ptr 所指向的對象被銷毀或重置時,要保證它所指向的對象被刪除。參見示例

每一個 shared_ptr 都符合 C++ 標準庫的 CopyConstructibleAssignable 的必要條件,並因此能夠用於標準庫容器。因為提供了比較操作,因此 shared_ptr 可以和標準庫中的關聯式容器一起工作。

通常,一個 shared_ptr 不能正確地持有一個指向動態分配的數組的指針。關於那種用法請參見 shared_array

因為在實現中使用了引用計數,shared_ptr實例的循環引用不會被回收。例如,如果 main() 持有一個指向 Ashared_ptrA 又直接或間接持有一個指回 Ashared_ptrA 的使用計數是 2。最初的 shared_ptr 析構後將導致一個使用計數為 1 的 A 被懸掛。使用 weak_ptr 以“打破循環”。

這個類模板被 T 參數化,T 是被指向的對象的類型。shared_ptr 和它的大多數成員函數對於 T 沒什麼要求,允許它是一個不完整類型,或者為 void。對 T 有附加要求的成員函數 (constructors, reset) 都明確地記錄在下面。

只要 T* 能被隱式地轉換到 U*,則 shared_ptr<T> 就能被隱式地轉換到 shared_ptr<U>。特別是,shared_ptr<T> 隱式轉換到 shared_ptr<T const>,當 UT 的一個可訪問基類的時候,還能轉換到 shared_ptr<U>,以及轉換到 shared_ptr<void>

shared_ptr 現在是 TR1(第一個 C++ 庫技術報告)的一部分。TR1 的最新草案可以在下述位置找到:

http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2005/n1745.pdf (1.36Mb PDF)

這個實現與 TR1 規範一致,僅有的例外是它依然存在於名字空間 boost 中,而不是 std::tr1

Best Practices 最佳實踐

一個簡單的近乎完全消滅內存洩露的方針是:總是使用一個已命名的智能指針變量接收 new 的結果。代碼中每一次出現 new 關鍵字都應該是如下形式:

shared_ptr<T> p(new Y);

當然,也可以用其它智能指針代替上面的 shared_ptrT 是和 Y 相同的類型,或者是可作為參數傳遞給 Y 的構造函數即可。

如果你遵守這個方針,自然導致你不再需要顯式 deletes,try/catch 結構也將非常罕見。

避免使用匿名 shared_ptr 臨時變量去存儲內容,為了看到這樣做是如何的危險,請考慮下面的例子:

void f(shared_ptr<int>, int);
int g();

void ok()
{
shared_ptr<int> p(new int(2));
f(p, g());
}

void bad()
{
f(shared_ptr<int>(new int(2)), g());
}

函數 ok 亦步亦趨地遵循了方針,相反 bad 構造了臨時的 shared_ptr 來代替,這就為內存洩漏留下了可乘之機。因為函數參數的求值順序是不確定的,new int(2) 首先被求值,g() 第二個是有可能的,如果 g 拋出一個異常,我們永遠也不可能到達 shared_ptr 的構造函數。關於這個問題的更多信息請參見 Herb Sutter 的對策(還有 這裡)。

Synopsis 概要

namespace boost {

class bad_weak_ptr: public std::exception;

template<class T> class weak_ptr;

template<class T> class shared_ptr {

public:

typedef T element_type;

shared_ptr(); // never throws
template<class Y> explicit shared_ptr(Y * p);
template<class Y, class D> shared_ptr(Y * p, D d);
template<class Y, class D, class A> shared_ptr(Y * p, D d, A a);
~shared_ptr(); // never throws

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

shared_ptr & operator=(shared_ptr const & r); // never throws
template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r); // never throws
template<class Y> shared_ptr & operator=(std::auto_ptr<Y> & r);

void reset(); // never throws
template<class Y> void reset(Y * p);
template<class Y, class D> void reset(Y * p, D d);
template<class Y, class D, class A> void reset(Y * p, D d, A a);
 template<class Y> void reset(shared_ptr<Y> const & r, T * p); // never throws

T & operator*() const; // never throws
T * operator->() const; // never throws
T * get() const; // never throws

bool unique() const; // never throws
long use_count() const; // never throws

operator unspecified-bool-type() const; // never throws

void swap(shared_ptr & b); // never throws
};

template<class T, class U>
bool operator==(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

template<class T, class U>
bool operator!=(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

template<class T, class U>
bool operator<(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

template<class T> void swap(shared_ptr<T> & a, shared_ptr<T> & b); // never throws

template<class T> T * get_pointer(shared_ptr<T> const & p); // never throws

template<class T, class U>
shared_ptr<T> static_pointer_cast(shared_ptr<U> const & r); // never throws

template<class T, class U>
shared_ptr<T> const_pointer_cast(shared_ptr<U> const & r); // never throws

template<class T, class U>
shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const & r); // never throws

template<class E, class T, class Y>
std::basic_ostream<E, T> & operator<< (std::basic_ostream<E, T> & os, shared_ptr<Y> const & p);

template<class D, class T>
D * get_deleter(shared_ptr<T> const & p);
}

Members 成員

element_type 元素類型

typedef T element_type;

提供模板參數 T 的類型。

constructors 構造函數

shared_ptr(); // never throws

作用:構造一個 emptyshared_ptr

後置條件:use_count() == 0 && get() == 0

拋出:無。

【這裡保證不拋出異常非常重要,因為 reset() 被指定使用缺省構造函數,這就意味著構造函數不必分配內存。】

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

條件:p 必須可以被轉換到 T *Y 必須是一個完整類型。表達式 delete p 必須是正常可用的,不能發生未定義行為,也不能拋出異常。

作用:構造一個擁有指針 pshared_ptr

後置條件:use_count() == 1 && get() == p

拋出:std::bad_alloc,或者一個由實現定義的當內存之外的某個資源無法獲得時的異常。

異常安全:如果拋出一個異常,則調用 delete p

注意:p 必須是一個指向由 C++ new 表達式分配的對象的指針或者是 0。即使 p 是 0,後置條件中的 use count 也是 1,在一個值為 0 的指針上調用 delete 是無害的。

【這個構造函數被變成模板是為了記住被傳遞的實際的指針類型。析構函數可以針對同樣的指針,連同它的原始類型調用 delete,即使 T 沒有虛擬析構函數,或者是 void

可選的侵入式的計數支持暴露了太多的實現細節,而且無法和 weak_ptr 很好地結合。當前實現使用了一個不同的機制,enable_shared_from_this 以解決 "shared_ptr from this" 問題。】

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

條件:p 必須可變換為 T *D 必須是 CopyConstructible(可拷貝構造)的。D 的拷貝構造函數和析構函數不能拋出異常。表達式 d(p) 必須是正常可用的,不能發生未定義行為,也不能拋出異常。A 必須是一個 Allocator(分配器),關於分配器的描述可參見 C++ 標準 20.1.5 (Allocator requirements)。

作用:構造一個擁有指針 p 和刪除器 dshared_ptr。第二個構造函數用 a 的一個拷貝分配內存。

後置條件:use_count() == 1 && get() == p

拋出:std::bad_alloc,或者一個由實現定義的當內存之外的某個資源無法獲得時的異常。

異常安全:如果拋出一個異常,則調用 d(p)

注意:p 指向的對象被刪除時,所存儲的 d 的拷貝被調用,並以所存儲的 p 的拷貝作為參數。

【custom deallocators(定制化釋放器)准許一個返回 shared_ptr 的工廠函數以將用戶隔離在它的內存分配策略之外。因為這個 deallocator(釋放器)不是類型的一部分,改變其分配策略不會破壞源代碼級或二進制級兼容性,也不需要客戶端重新編譯。例如,一個 "no-op" 釋放器返回一個指向靜態分配對象的 shared_ptr 是有用的,而其它變種卻允許用一個 shared_ptr 來包裝其它智能指針,以方便互操作性。

對 custom deallocators(定制化釋放器)的支持不會強加很大的負擔。其它 shared_ptr 特性依然需要 deallocator(釋放器)提供支持。

D 的拷貝構造函數不能拋出異常的需求來自於以值傳遞。如果拷貝構造函數拋出異常,指針就會洩漏。排除這一需求需要使用以 (const) reference((常)引用)傳遞。

以引用傳遞的主要問題在於它和 rvalues(右值)的相互影響。一個 const reference(常引用)可能還是會引起一次拷貝,並需要一個 const operator()。而一個 non-const reference(非常引用)根本不會綁定在一個右值上。解決這個問題的一個好的方案是 N1377/N1385 提案中的 rvalue reference(右值引用)。】

shared_ptr(shared_ptr const & r); // never throws
template<class Y> shared_ptr(shared_ptr<Y> const & r); // never throws

作用:如果 rempty,構造一個 empty shared_ptr,否則,構造一個帶有 rshares ownership(共享所有權)的 shared_ptr

後置條件: get() == r.get() && use_count() == r.use_count()

拋出:無。

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

作用:構造一個帶有 rshares ownership(共享所有權)的 shared_ptr,並存儲 r 中所存儲指針的一個拷貝。

後置條件:use_count() == r.use_count()

拋出:r.use_count() == 0 時,拋出 bad_weak_ptr

異常安全:如果拋出一個異常,構造函數將不起作用。

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

作用:構造一個 shared_ptr,就像存儲了一個 r.release() 的返回值的拷貝。

後置條件:use_count() == 1

拋出:std::bad_alloc,或者一個由實現定義的當內存之外的某個資源無法獲得時的異常。

異常安全:如果拋出一個異常,構造函數將不起作用。

【這個構造函數所得到的源 auto_ptr 是以引用傳遞的,而非以值傳遞,而且不能接收 auto_ptr 臨時變量。這是故意的,作為構造函數提出的強制保證,一個右值引用也可以解決這個問題。】

destructor 析構函數

~shared_ptr(); // never throws

作用:

拋出:無。

assignment 賦值

shared_ptr & operator=(shared_ptr const & r); // never throws
template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r); // never throws
template<class Y> shared_ptr & operator=(std::auto_ptr<Y> & r);

作用:等價於 shared_ptr(r).swap(*this)

返回:*this

注意:由臨時對象的構造和析構造成的使用計數的更新不被認為是可察覺的副作用,而實現可以自由地經由不同的手段達到其效果(以及隱含的保證),而不創建臨時變量。特別是,在下例中:

shared_ptr<int> p(new int);
shared_ptr<void> q(p);
p = p;
q = p;

兩個賦值可能都是 no-op(什麼都不做)。

reset 重置

void reset(); // never throws

作用:等價於 shared_ptr().swap(*this)

template<class Y> void reset(Y * p);

作用:等價於 shared_ptr(p).swap(*this)

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

作用:等價於 shared_ptr(p, d).swap(*this)

template<class Y, class D, class A> void reset(Y * p, D d, A a);

作用:等價於 shared_ptr(p, d, a).swap(*this)

template<class Y> void reset(shared_ptr<Y> const & r, T * p); // 不拋出

作用:等價於 shared_ptr(r, p).swap(*this).

indirection 間接引用

T & operator*() const; // never throws

條件:所存儲的指針不能為 0。

返回:一個引向所存儲的指針所指向的對象的引用。

拋出:無。

T * operator->() const; // never throws

條件:所存儲的指針不能為 0。

返回:所存儲的指針。

拋出:無。

get 取得

T * get() const; // never throws

返回:所存儲的指針。

拋出:無。

unique 唯一性

bool unique() const; // never throws

返回:use_count() == 1

拋出:無。

注意:unique() 可能比 use_count() 更快。如果你用 unique() 實現 copy on write(寫時拷貝),當所存儲的指針為 0 是,不要依賴於一個特定的值。

use_count 使用計數

long use_count() const; // never throws

返回:*this 共享所有權的 shared_ptr 對像(包括 *this 在內)的數量,或者當 *this 為空時,一個不特定的非負值。

拋出:無。

注意:use_count() 達不到必要的效率。只用於調試和測試的目的。而不要用於產品代碼。

conversions 轉換

operator unspecified-bool-type () const; // never throws

返回:一個未確定的值,在需要布爾值的上下文中,它等價於 get() != 0

拋出:無。

注意:這一轉換操作符允許將 shared_ptr 對像用於需要布爾值的上下文中,就像 if (p && p->valid()) {}。實際目標類型通常是一個指向成員函數的指針,消除了很多隱式轉換的陷阱。

【到 bool 的轉換不僅僅是語法糖。它允許在使用 dynamic_pointer_castweak_ptr::lock 時,在條件中聲明 shared_ptrs。】

swap 交換

void swap(shared_ptr & b); // never throws

作用:交換兩個智能指針中的內容。

拋出:無。

Free Functions 自由函數

comparison 比較

template<class T, class U>
bool operator==(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

返回:a.get() == b.get()

拋出:無。

template<class T, class U>
bool operator!=(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

返回:a.get() != b.get()

拋出:無。

template<class T, class U>
bool operator<(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

返回:一個未確定值,以致於

拋出:無。

注意:允許 shared_ptr 對像在關聯式容器中作為鍵值使用。

【因為兼容性和合法性的原因,Operator<std::less 的特化版本更優先被選用,因為 std::less 需要返回一個 operator< 的結果,而當沒有提供謂詞時,許多標準算法在比較中使用 operator< 來代替 std::less。組合對象,比如 std::pair,也根據它們所包含的子對象的 operator< 來實現它們的 operator<

其餘的比較操作符被故意省略。】

swap 交換

template<class T>
void swap(shared_ptr<T> & a, shared_ptr<T> & b); // never throws

作用:等價於 a.swap(b)

拋出:無。

注意:std::swap 的接口匹配。為泛型編程提供幫助。

swap 被定義在和 shared_ptr 同樣的名字空間中,這是當前提供一個讓標準庫有機會使用的 swap 函數的僅有的合法方法。】

get_pointer 取得指針

template<class T>
T * get_pointer(shared_ptr<T> const & p); // never throws

返回:p.get()

拋出:無。

注意:為泛型編程提供幫助,用於 mem_fn

static_pointer_cast

template<class T, class U>
shared_ptr<T> static_pointer_cast(shared_ptr<U> const & r); // never throws

條件:表達式 static_cast<T*>(r.get()) 必須正常可用。

返回:如果 rempty,返回一個 empty shared_ptr<T>;否則,返回一個存儲 static_cast<T*>(r.get()) 的拷貝並和 r 共享所有權的 shared_ptr<T> 對象。

拋出:無。

注意:表面上看似乎等價的表達式

shared_ptr<T>(static_cast<T*>(r.get()))

因為試圖刪除同樣的對象兩次,而最終陷入未定義行為。

const_pointer_cast

template<class T, class U>
shared_ptr<T> const_pointer_cast(shared_ptr<U> const & r); // never throws

條件:表達式 const_cast<T*>(r.get()) 必須正常可用。

返回:如果 rempty,返回一個 empty shared_ptr<T>;否則,返回一個存儲 const_cast<T*>(r.get()) 的拷貝並和 r 共享所有權的 shared_ptr<T> 對象。

拋出:無。

注意:表面上看似乎等價的表達式

shared_ptr<T>(const_cast<T*>(r.get()))

因為試圖刪除同樣的對象兩次,而最終陷入未定義行為。

dynamic_pointer_cast

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

條件:表達式 dynamic_cast<T*>(r.get()) 必須正常可用,而且它的行為已被定義。

返回:

拋出:無。

注意:表面上看似乎等價的表達式

shared_ptr<T>(dynamic_cast<T*>(r.get()))

因為試圖刪除同樣的對象兩次,而最終陷入未定義行為。

operator<<

template<class E, class T, class Y>
std::basic_ostream<E, T> & operator<< (std::basic_ostream<E, T> & os, shared_ptr<Y> const & p);

作用:os << p.get();

返回:os

get_deleter

template<class D, class T>
D * get_deleter(shared_ptr<T> const & p);

返回:如果 *this 擁有一個類型為 D 的刪除器 d,則返回 &d;否則返回 0。

Example 示例

來看一個完整的示例程序 shared_ptr_example.cpp。這個程序創建了一個 shared_ptr 對象的 std::vectorstd::set

注意,在容器被填充之後,一些 shared_ptr 對象的使用計數為 1,而不是 2,這是因為 set 是一個 std::set 而不是 std::multiset,而這樣就不能包含重複的條目。此外,當 push_backinsert 容器操作執行的次數不同,使用計數可能會更高。還有更複雜的,容器操作在多種情況下都可能拋出異常。在這個示例程序中的內存管理和異常處理都很正常,而沒有讓智能指針成為一場噩夢。

Handle/Body Idiom Handle/Body 慣用法

shared_ptr 的一個慣常用法是實現 handle/body(也稱為 pimpl)慣用法,以避免在頭文件中暴露身體(實現)。

shared_ptr_example2_test.cpp 示例程序包含一個頭文件 shared_ptr_example2.hpp,其中使用了一個指向一個完整類型的 shared_ptr<> 來隱藏實現。成員函數的實例化要求在 shared_ptr_example2.cpp 實現文件中存在完整類型。注意這裡不需要顯示的析構函數。不像 ~scoped_ptr,~shared_ptr 不需要 T 是一個完整類型。

Thread Safety 線程安全

shared_ptr 對像提供與內建類型一樣的線程安全級別。一個 shared_ptr 實例可以同時被多個線程“讀”(僅使用不變操作進行訪問)。不同的 shared_ptr 實例可以同時被多個線程“寫入”(使用類似 operator=reset 這樣的可變操作進行訪問)(即使這些實例是拷貝,而且共享下層的引用計數)。

任何其它的同時訪問的結果會導致未定義行為。

示例:

shared_ptr<int> p(new int(42));

//--- Example 1 ---

// thread A
shared_ptr<int> p2(p); // reads p

// thread B
shared_ptr<int> p3(p); // OK, multiple reads are safe

//--- Example 2 ---

// thread A
p.reset(new int(1912)); // writes p

// thread B
p2.reset(); // OK, writes p2

//--- Example 3 ---

// thread A
p = p3; // reads p3, writes p

// thread B
p3.reset(); // writes p3; undefined, simultaneous read/write

//--- Example 4 ---

// thread A
p3 = p2; // reads p2, writes p3

// thread B
// p2 goes out of scope: undefined, the destructor is considered a "write access"

//--- Example 5 ---

// thread A
p3.reset(new int(1));

// thread B
p3.reset(new int(2)); // undefined, multiple writes

 

從 Boost 版本 1.33.0 開始,shared_ptr 在以下平台上使用了 lock-free 實現:

如果你的程序是單線程的,而且在它的缺省配置中沒有連接任何可能使用了 shared_ptr 的庫,你可以在項目基準中 #defineBOOST_SP_DISABLE_THREADS 以轉換到普通的非原子的引用計數更新。

(在部分(而非全部)編譯單元中定義 BOOST_SP_DISABLE_THREADS 在技術上是對單一定義規則和未定義行為的觸犯。然而,實現會盡其所能滿足那些提出使用非原子更新的編譯單元的要求。但是,沒有任何保證。)

你可以定義宏 BOOST_SP_USE_PTHREADS 以避開特定平台的 lock-free 實現,並退回到普通的基於 pthread_mutex_t 的代碼。

Frequently Asked Questions 常見問題

問:有幾種共享指針的變化,反映不同的權衡,為什麼智能指針庫僅提供一種單一的實現?能夠用每一種類型做試驗以發現最適合手邊工作的那一種不是非常有好處的嗎?

答:shared_ptr 的一個重要目標是提供一個標準的共享所有權指針。對於穩定的庫接口來說,單一指針類型是很重要的,因為不同的共享指針一般無法互操作,例如,一個 reference counted pointer(引用計數指針)(用於庫 A)不能和一個 linked pointer(連接指針)(用於庫 B)共享所有權。

問:shared_ptr 為什麼沒有用來提供特性或規則的模板參數以允許廣泛的用戶定制性?

答:參數化會阻礙用戶。shared_ptr 模板小心謹慎地精工細作以滿足通用需求,而沒有大量的參數化。有朝一日,可能會有人發明可配置性很高的智能指針,而且很易於使用並難於犯錯。到那時,shared_ptr 會成為為各種各樣的應用而選擇的智能指針。(那些對基於策略的智能指針感興趣的人可以閱讀 Andrei Alexandrescu 寫的 Modern C++ Design。)

問:我不確定。缺省參數可以用於那些需要隱藏複雜度的地方。再問一次,為什麼不規則化?

答:模板參數影響類型。參見上面第一個問題的回答。

問:shared_ptr 為什麼不用鏈表實現?

答:鏈表實現的好處不足以抵消一個額外的指針所增加的開銷。參見 timings 頁面。另外,讓一個鏈表實現線程安全的代價太大了。

問:shared_ptr(或其它任何一個 Boost 智能指針)不提供一個到 T* 的自動轉換?

答:自動轉換被認為更易於導致錯誤。

問:shared_ptr 為什麼提供 use_count()?

答:為了幫助寫測試用例和調試顯示。一個祖先類有 use_count(),它對於在一個複雜的項目中追蹤 bugs 並切斷循環依賴是非常有用的。

問:為什麼 shared_ptr 不指定複雜度要求?

答:因為複雜度要求限制了實現,並且使規範更複雜,對於 shared_ptr 的用戶卻沒有明顯的好處。例如,如果不得不符合嚴厲的複雜度要求,錯誤檢查的實現可能會變得不一致。

問:為什麼 shared_ptr 不提供一個 release() 函數?

答:shared_ptr 不能放棄所有權除非它是 unique(),因為其它拷貝仍然可以銷毀這個對象。

考慮:

shared_ptr<int> a(new int);
shared_ptr<int> b(a); // a.use_count() == b.use_count() == 2

int * p = a.release();

// Who owns p now? b will still call delete on it in its destructor.

此外,當源 shared_ptr 可能帶有一個定制化的刪除器時,release() 返回的指針很難可靠地被釋放。

問:為什麼 operator->() 是 const 的,而它的返回值卻是指向元素類型的 non-const 指針?

答:淺拷貝指針,包括裸指針,一般不會傳遞常量性。當你能夠總是從一個 const 指針得到一個 non-const 指針,並繼而通過它改變那個對象的時候,還是有一點兒意義的。shared_ptr 是“盡可能接近而不重合裸指針”。


$Date: 2007-11-25 13:38:02 -0500 (Sun, 25 Nov 2007) $

Copyright 1999 Greg Colvin and Beman Dawes. Copyright 2002 Darin Adler. Copyright 2002-2005 Peter Dimov. Distributed under the Boost Software License, Version 1.0. See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt.