C++ 智慧指針淺析
C++ 智慧指針淺析
為了解決 C++ 中記憶體管理這個老大難問題,C++ 11 中提供了三種可用的智慧指針。(早期標準庫中還存在一種 auto_ptr,但由於設計上的缺陷,已經被 unique_ptr 取代了)
智慧指針不僅能用來管理動態記憶體,還能用來管理其他類型的資源,比如互斥鎖、資料庫連接等,這種用資源管理對象來管理資源的思想被稱為 RAII。
原始指針的缺陷
- 看不出來指向的是對象還是數組,也就不知道該用 delete 還是
delete[]; - 不知道用完後是否應該銷毀,也就是不包含所有權的資訊;
- 不知道該用 delete 還是其他方式釋放該對象;
- double free;
- dangling pointers(懸空指針),即對象已經被釋放了,但指針還指向它。
shared_ptr
shared_ptr 體現的是共享的所有權(shared ownership),通俗地講,就是這個被管理的對象可以被多個用戶使用,只有所有用戶都不再需要該資源時,它才可以被釋放。
為了實現共享的所有權,shared_ptr 會維護一個引用計數,表示有多少 shared_ptr 指向這個被管理的對象。
簡單地來說,當 shared_ptr 被拷貝時(不管是我們主動調用,還是將它作為傳值參數或返回值),引用計數就會增加;當 shared_ptr 被析構時(比如離開它的作用域),引用計數就會減少。當引用計數歸零時,這個被管理的對象就會被釋放。
shared_ptr 中引用計數的操作是原子的,這點和 shared_ptr 的執行緒安全性有關。
shared_ptr 使用規範
要正確地使用 shared_ptr,需要注意一些問題:
- 不使用相同的內置指針初始化或 reset 多個 shared_ptr,原因請看後文的 shared_ptr 實現,這樣操作會產生多個控制塊對象,從而造成 double free;
get()返回的原始指針只應該有一種用途,用來作為參數傳遞給遺留介面;- 不應該 delete
get()返回的指針; - 不使用
get()的返回值去初始化或 reset 另一個智慧指針; - 當最後一個對應的智慧指針被銷毀時,
get()返回的指針就無效了;
- 不應該 delete
- 如果使用智慧指針管理資源而不是記憶體,應當定義自定義刪除器;
- 如果將 shared_ptr 存放到容器中,之後不再需要其中的部分元素,應當調用 erase 將它們刪除,不然這些元素的生命周期會被意外地延長。
在單條語句里將 new 來的指針放到智慧指針里
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
processWidget(std::shared_ptr<Widget>(new Widget), priority());
該調用事實上由三部分組成:
- new Widget
- 調用 shared_ptr 構造函數
- 調用
priority()
1 應當發生在 2 之前,但標準並沒有規定 3 要在何時發生;如果編譯器以 1、3、2 這樣的順序來執行,那麼如果調用 priority() 時拋出異常,那麼就會出現記憶體泄漏。
如果將程式改為:
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
可以強制上面的過程以 1、2、3 的順序發生,消除了記憶體泄漏的隱患。
此外,將這裡改成 std::move() 效率更高,因為移動不涉及引用計數的增減。
std::shared_ptr<Widget> pw(new Widget);
processWidget(std::move(pw), priority());
避免該記憶體泄露問題的另一種方法是改用 make_shared():
processWidget(std::make_shared<Widget>(), priority());
make_shared
使用 make_shared() 的優勢:
- 避免上面提到的記憶體泄露問題;
- 效率更高:先 new 出記憶體再傳遞給 shared_ptr,涉及到兩次記憶體分配,一次分配被管理的對象,一次分配控制塊對象;但如果改用
make_shared(),只需要分配一次記憶體就足夠容納這兩個對象。
使用 make_shared() 的缺陷:
- 不允許指定自定義刪除器。
- 沒法用
{}初始化,make_shared()將參數完美轉發給構造函數時,默認使用()。 - 如果類重載了
operator new或operator delete,它的記憶體分配行為可能表現出特殊的行為;- 比如 Widget 類的
operator new和operator delete可能只處理大小為sizeof(Widget)的記憶體分配; - 但
make_shared()不只是分配對象的動態記憶體,還會同時分配控制塊的記憶體,這種情況下,就會造成麻煩。
- 比如 Widget 類的
- 由於控制塊和對象放在同一塊記憶體里,引用計數為 0 的時候,對象並不會被釋放,只有等到控制塊也不再需要的時候,這塊記憶體才會被釋放;
- 只要還有 weak_ptr 指向這個對象,控制塊就不會被釋放;
- 對於特別大的對象,這可能是問題。
unique_ptr
unique_ptr 體現的是獨佔/排他的所有權(exclusive ownership),通俗地講,就是這個被管理的對象為單個用戶所有,當該用戶不再需要這個資源時,它就會被釋放掉;同時,該用戶還可以將資源的所有權移交給其他用戶。
unique_ptr 的拷貝構造和賦值是刪除的,提供了移動構造和賦值,因此要想將持有的獨佔資源交給其他 unique_ptr,往往需要顯式地調用 std::move() 來觸發移動操作。
UML 中的組合(或稱複合,Composition)關係,就可以使用 unique_ptr 來建模,當整體被析構時,部分也會跟著析構。
unique_ptr 可以轉換為 shared_ptr,因此 unique_ptr 很適合作為工廠函數的返回值,因為你不知道使用者需要的所有權語義究竟是獨佔的還是共享的;相反,一旦所有權交給了 shared_ptr,就沒法再轉換為獨佔的所有權了。
C++11 中沒有類似於 make_shared() 的標準庫函數,應當將 new 返回的指針傳遞給 unique_ptr 的構造函數;C++14 中加入了 make_unique()。
unique_ptr 的大小
- 使用默認刪除器的情況下,unique_ptr 內部只維護了一個指針,和原始指針大小相同
由於 unique_ptr 直接將自定義刪除器存儲在對象內部
- 使用函數指針作為刪除器,大小為變為兩倍的原始指針
- 使用函數對象作為刪除器
- 無狀態的函數對象,比如不捕獲變數的 lambda 表達式,不影響 uniqur_ptr 的大小
- 否則,隨著函數對象的大小增加而增加
為什麼不捕獲變數的 lambda 表達式在 unique_ptr 里不佔空間?
實現上,lambda 表達式其實是個匿名類對象,如果它不捕獲任何變數,就是個空的類,C++ 規定任何對象和對象成員的大小都至少是 1,即使該對象的類型是一個空的類,這就能保證相同類型的不同對象的地址總是不同的。
class Empty {};
class HoldsAnInt {
private:
int x;
Empty e;
};
由於上述規定的存在,導致 sizeof(HoldsAnInt) > sizeof(int)
但該規定存在例外,那就是這個空的類作為基類的時候,是不需要佔據空間的。這種優化被稱為空基類優化(empty base optimization,EBO)。
class HoldsAnInt: private Empty {
private:
int x;
};
這種情況下,sizeof(HoldsAnInt) == sizeof(int)
我們查看 unique_ptr 的源碼,會發現它是這樣存儲指針和刪除器的:
tuple<pointer, _Dp> _M_t;
gcc 的 tuple 的實現上應用 EBO 做空間壓縮,所以不捕獲變數的 lambda 表達式作為刪除器時,unique_ptr 的空間不會變大。
對於程式設計師來說,用不捕獲變數的 lambda 表達式做刪除器是非常常見的情況,所以這是一個很有價值的優化。
auto_ptr 的缺陷
auto_ptr 是早期標準庫中實現的一種智慧指針,具有 unique_ptr 的部分特性,它用拷貝來模擬資源所有權的移交,拷貝 auto_ptr 會導致源 auto_ptr 被設為 null,這種行為是比較匪夷所思的。
std::auto_ptr<Invesetment> pInv2(pInv1); // pInv1 被設為 null
pInv1 = pInv2; // pInv2 被設為 null
假設將 auto_ptr 存儲到容器里,並使用泛型演算法對容器做操作,由於泛型演算法可能將元素拷貝到局部臨時對象中,會導致操作後的容器里存在大量空的 auto_ptr,因此 auto_ptr 不能和容器一起使用。
和 unique_ptr 作比較,unique_ptr 僅允許移動操作,而禁止了拷貝操作,語義更加清晰且安全。
自定義刪除器
shared_ptr 和 unique_ptr 都允許自定義刪除器,區別在於 unique_ptr 的自定義刪除器是模板參數的一部分,而 shared_ptr 僅僅是作為構造函數的一個參數。
auto loggingDel = [](Widget *pw) // 自定義刪除器
{
makeLogEntry(pw);
delete pw;
};
// 刪除器類型是指針類型的一部分
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
// 刪除器類型不是指針類型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel);
不包含刪除器類型模板參數給了 shared_ptr 更大的靈活性,比如 std::shared_ptr<Widget> 的容器中可以放刪除器不同的各種 Widget 對象。
shared_ptr 的大小不會隨著自定義刪除器的大小而增加,因為這部分是放在它的控制塊對象里;但 uniqur_ptr 是直接將刪除器存到指針對象內了,因此它的大小是會隨著自定義刪除器的大小變化的。
智慧指針和動態數組
shared_ptr 和動態數組
shared_ptr 不支援動態數組的管理,將動態數組傳遞給它後,它仍會使用 delete,而不是 delete[] 去釋放這塊記憶體,因此必須提供自己的刪除器。
可以直接使用 default_delete 類型作為自定義刪除器,這個類提供了針對數組類型的偏特化,會調用 delete[] 來釋放記憶體。
std::shared_ptr<int> sp3(new int[10](), std::default_delete<int[]>());
同時,由於 shared_ptr 沒有提供下標運算符,要訪問數組中的元素,必須通過 get() 獲取內置指針,用它來訪問數組元素。
作為替代,可以使用 boost 提供的 boost::scoped_array 和 boost::shared_array 來管理動態數組。
C++17 的改進
支援直接用數組作為模板參數,它會正確地調用 delete[],且增加了下標運算符:
std::shared_ptr<int[]> sp3(new int[10]());
int i = sp3[2];
使用 unique_ptr 管理動態數組
在類型名後面加 [],它會自動調用 delete[] 來釋放記憶體,且可以使用下標運算符訪問其中的元素。
unique_ptr<int[]> int_array(new int[10]{ 1, 2, 3 });
cout << int_array[1] << endl;
但比起使用 unique_ptr,更應該優先考慮 std::array、std::vector、std::string 等數據容器。
RAII
RAII 簡介
Resource Acquisition Is Initialization (RAII),即獲取到資源後立刻移交給資源管理對象。資源管理對象使用它們的析構函數確保資源被釋放。這裡說的資源可以是記憶體、資料庫連接、互斥鎖等各種形式的資源。
假設有一個 C 風格 API 的 Mutex 類:
void lock(Mutex *pm);
void unlock(Mutex *pm);
Mutex 的資源管理類(C++11 中類似的類叫 std::lock_guard)可以定義為:
class LockGuard
{
public:
explicit LockGuard(Mutex* pm) :mutexPtr(pm) { lock(mutexPtr); }
~LockGuard() { unlock(mutexPtr); }
private:
Mutex* mutexPtr;
};
對想要寫出異常安全程式碼的程式設計師來說,RAII 非常重要,因為如果在程式碼中顯式地釋放資源,就可能出現因為異常拋出而資源沒能正確釋放地情況,比如下面這段程式碼,如果 new 拋異常,mutex 就不會被解鎖。
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
但如果改為用 RAII 的方式來管理資源,如果有異常被拋出,棧展開的過程中,資源管理對象就會被析構掉,從而釋放掉被管理的資源。
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
用智慧指針實現 RAII
C++11 後,可以通過智慧指針來實現 RAII,對於共享資源,將它交給 shared_ptr,而對於獨佔資源,將它交給 unique_ptr,同時通過使用自定義刪除器,來決定資源該怎樣被釋放。
shared_ptr 的實現
shared_ptr 實現上包含兩個指針:一個指向被管理的對象,一個指向動態分配的控制塊對象。控制塊對象里包含:
- 引用計數(use_count),用來判斷是否可以釋放被管理的對象;
- 弱引用計數(weak_count),和 weak_ptr 的實現有關;
- 其他數據,比如自定義刪除器。
模擬隱式轉換行為
智慧指針可以模擬指針的隱式轉換行為,自動將派生類指針轉換為基類指針,為了做到這點,需要為智慧指針增加一個泛化拷貝構造函數:
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other): heldPtr(other.get())
{
//...
}
T* get() const { return heldPtr; }
//...
private:
T *heldPtr;
};
這個泛化拷貝構造函數不是 explicit 的,以支援隱式轉換;同時它的初始化列表告訴我們只有能隱式轉換為 T* 類型的 U* 才能通過編譯。
源碼分析
下面包含一些 gcc 的源碼分析,不想看程式碼的讀者可以直接跳過本章。
gcc 中控制塊對象的類叫 _Sp_counted_base,包含兩個引用計數,它們都是原子類型:
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
// ...
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
// ...
};
shared_ptr 中會保存指向 _Sp_counted_base 的指針。
_Sp_counted_base<_Lp>* _M_pi;
之前我們說過 shared_ptr 的模板參數中是不包含自定義刪除器類型的,這就導致 deleter 沒法保存在 shared_ptr 內部。
template< class T > class shared_ptr; // shared_ptr 的聲明
template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d ); // 帶自定義刪除器的構造函數
很顯然,deleter 只能存放在控制塊對象中。為此,_Sp_counted_base 有一個帶刪除器模板參數的派生類 _Sp_counted_deleter:
template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
這些類之間的關係如下圖所示:
當創建 shared_ptr 時,會動態分配 _Sp_counted_deleter 對象,並將自定義刪除器存到它裡面。
template<typename _Ptr, typename _Deleter, typename _Alloc,
typename = typename __not_alloc_shared_tag<_Deleter>::type>
__shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
{
typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
__try
{
typename _Sp_cd_type::__allocator_type __a2(__a);
auto __guard = std::__allocate_guarded(__a2);
_Sp_cd_type* __mem = __guard.get();
::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
_M_pi = __mem;
__guard = nullptr;
}
__catch(...)
{
__d(__p); // Call _Deleter on __p.
__throw_exception_again;
}
}
shared_ptr 析構的時候,_M_pi->_M_release() 會被調用,(原子地)減少引用計數:
void _M_release() // nothrow
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) {
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
if (_Mutex_base<_Lp>::_S_need_barriers) {
__atomic_thread_fence(__ATOMIC_ACQ_REL);
}
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) {
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
_M_destroy();
}
}
}
如果 use_count 減少到 0,調用虛函數 _M_dispose(),其實調用到的是_Sp_counted_deleter 的實現,由此,自定義刪除器被用來銷毀被管理的對象。
如果 weak_count 減少到 0,調用 _M_destroy(),銷毀控制塊對象。
virtual void _M_dispose() noexcept
{
_M_impl._M_del()(_M_impl._M_ptr);
}
virtual void _M_destroy() noexcept
{
__allocator_type __a(_M_impl._M_alloc());
__allocated_ptr<__allocator_type> __guard_ptr{ __a, this };
this->~_Sp_counted_deleter();
}
在不定義自定義刪除器的情況下,如果被管理對象是是非數組類型,_M_pi 指向_Sp_counted_deleter 的默認實現 _Sp_counted_ptr,它直接調 delete 析構被管理對象。
virtual void _M_dispose() noexcept
{
delete _M_ptr;
}
virtual void _M_destroy() noexcept
{
delete this;
}
但如果被管理對象是是數組類型,自定義刪除器將被設置為 __sp_array_delete,它調用 delete[] 來析構對象。
struct __sp_array_delete
{
template<typename _Yp> void operator() (_Yp* __p) const { delete[] __p; }
};
shared_ptr 的性能分析
- 大小是原始指針的兩倍,因為它要同時持有指向被管理對象的指針和控制塊對象的指針;
- 引用計數的記憶體必須是動態分配的,可以用 make_shared 來對這點做優化;
- 引用計數操作是原子的,原子操作比非原子操作要慢;
- 控制塊對象包含繼承關係,為了正確的銷毀對象使用了虛函數,因此對象銷毀的時候要承擔虛函數帶來的開銷。
移動和引用計數
用一個 shared_ptr 移動構造另一個 shared_ptr 不會導致引用計數的變化,新的 shared_ptr 會直接接管老的 shared_ptr 的控制塊對象,因此 shared_ptr 的移動操作要比拷貝操作高效。
weak_ptr
weak_ptr 不負責對象的生命周期管理,它指向一個由 shared_ptr 管理的對象,但不改變它的引用計數(use_count)。通過 weak_ptr 可以判它所指向的對象是否已經被銷毀。有兩種方法可以做這種判斷:
- 嘗試用
lock()提升它為 shared_ptr,如果過期,返回的結果為空; - 用它構造一個新的 shared_ptr,如果過期,會拋
std::bad_weak_ptr異常。
auto spw2 = wpw.lock();
std::shared_ptr<Widget> spw3(wpw);
weak_ptr 的實現
weak_ptr 的實現和 shared_ptr 的控制塊對象相關。
由於 weak_ptr 想要知道 shared_ptr 管理的對象是否已經被析構了,那麼最直接的手段就是讓它訪問 shared_ptr 的控制塊對象,查看 use_count 是否已經是 0。
控制塊對象的生命周期必須延續到所有 shared_ptr 和 weak_ptr 都已經析構掉,它才可以析構。所以需要一個控制塊對象的引用計數來管理它的生命周期,即 weak_count。
weak_ptr 的使用場景
shared_ptr 可能懸空的情況下,都可以使用 weak_ptr 來做 shared_ptr 是否懸空的判斷。比如 UML 中的聚合(aggregation)關係,一般由整體持有部分的 shared_ptr,部分持有整體的 weak_ptr,並使用升級操作,來判斷整體是否還存活。
帶快取的工廠函數
假設說有一個開銷很大的工廠函數,它針對不同的 ID 產生不同的產品,但該工廠函數的開銷很大,我們希望能快取下來它的產品:
std::shared_ptr<const Widget> loadWidget(WidgetID id);
我們希望當工廠函數客戶端不再需要該快取對象時,該對象可以被析構,因此我們用哈希表存儲對象的 weak_ptr 來做快取,獲取對象時,先嘗試升級快取中的 weak_ptr,如果能成功,說明對象仍未被釋放,可以節省調用工廠函數的開銷:
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock();
if (!objPtr) {
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr;
}
觀察者模式
觀察者模式允許我們實現一種「發布-訂閱」機制,當對象的狀態發生變化時,通知正在「觀察」該對象的其他對象。
觀察者模式中有兩種角色:subject 或 publisher 是狀態會發生變化的對象;observer 或 subscriber 是在 subject 狀態發生變化時要(通過調用 observer 的 update 介面)通知的對象。
如果使用裸指針來保存 observer,有可能發生的是 observer 已經被析構,但 subject 還不知道,又調用了 observer 的 update 方法,導致未定義行為。
解決方法是用 weak_ptr 保存 observer,在調用 update 前先升級為 shared_ptr,如果 observer 仍存活就調用 update,否則將它從 observer 列表中刪除。
解決循環引用
在上圖中,如果 B 也有指向 A 的引用,這時就不能選擇 shared_ptr,因為如果有多個 shared_ptr 的引用關係形成環狀,會導致這些指針的引用計數永遠都不會歸零,結果就是這些對象永遠都不會被釋放,即使程式中的其他部分已經不持有指向它們的智慧指針。
如果我們使用裸指針,就可能出現 A 已經被析構了,B 還嘗試調用它的方法,導致未定義行為。
解決方法:將引用鏈中的一個指針改為使用 weak_ptr 從而打破環狀關係。
shared_ptr 的執行緒安全性
智慧指針本身的執行緒安全
如果多個執行緒在不同的 shared_ptr 對象(即使這些對象是拷貝出來的,共享同一個對象的所有權)上不加同步地調用任何成員函數(包括拷貝和賦值),都是執行緒安全的。
如果多個執行緒不加同步地訪問同一個 shared_ptr 對象,並且有執行緒使用了非 const 的成員函數(拷貝和賦值也是非 const 的),那麼會發生數據競爭。雖然智慧指針針對引用計數的操作是原子的,但是非 const 的成員函數往往涉及多個操作,這些操作組合在一起不是原子的。
被管理對象的執行緒安全
使用多個不同的 shared_ptr 對象訪問同一個被管理對象,如果對該對象的操作本身不是執行緒安全的,那麼即使通過智慧指針訪問,也會發生數據競爭,智慧指針並不對被管理對象的執行緒安全性負責。
enable_shared_from_this
之前我們說過,當你持有一個 shared_ptr ,絕不應該再從它的原始指針(不管是 new 出來的還是通過 get() 得到的)中創建出一個新的 shared_ptr ,因為它們的引用計數是不共享的,會帶來 double free 的問題。對於這點,即使是 this 指針也不例外。
struct S
{
shared_ptr<S> dangerous()
{
return shared_ptr<S>(this); // don't do this!
}
};
int main()
{
shared_ptr<S> sp1(new S);
shared_ptr<S> sp2 = sp1->dangerous();
return 0;
}
在這個例子中,智慧指針 sp1 和 sp2 指向的是同一個底層對象,但 sp1 和 sp2 對此一無所知,它們各自管理自己的引用計數,因此這塊記憶體會被釋放兩次。
要避免此問題,可以使用類模板 enable_shared_from_this,以它的派生類作為模板實參,這種繼承的模式被稱為 curiously recurring template pattern(CRTP)。
struct S : enable_shared_from_this<S>
{
shared_ptr<S> not_dangerous()
{
return shared_from_this();
}
};
int main()
{
shared_ptr<S> sp1(new S);
shared_ptr<S> sp2 = sp1->not_dangerous();
return 0;
}
使用場景
有時候對象可能需要獲得一個指向自己的 shared_ptr,以傳遞給 lambda 表達式或消息隊列的時候,就需要調用 shared_from_this()。
boost::asio::async_write(socket_, boost::asio::buffer(message_),
boost::bind(&tcp_connection::handle_write, shared_from_this()));
上面這個例子來自 asio 的教程,asio 伺服器會為每個客戶端連接動態創建一個 tcp_connection 對象(包含連接的上下文資訊),並在上面執行非同步操作,所以必須保證 tcp_connection 對象存活到回調函數被調用的時候。
但由於非同步操作的特點,你不知道回調函數 handle_write 什麼時候會被調用,所以很難直接去管理 tcp_connection 的生命周期,通常的做法是用 shared_ptr 來管理它,並且讓非同步操作捕獲一份它的 shared_ptr(不管是通過 std::bind 還是 lambda 表達式的捕獲列表),這樣只要非同步操作還未執行,tcp_connection 對象就不會被析構。
如果在 tcp_connection 的成員函數中要執行非同步操作,就必須捕獲一個指向自己的 shared_ptr,這時候就需要調用 shared_from_this(),因此 tcp_connection 必須繼承 enable_shared_from_this。
使用限制
必須通過 shared_ptr 來調用 shared_from_this(),下面的程式碼是錯誤的:
int main()
{
S *p = new S;
shared_ptr<S> sp2 = p->not_dangerous(); // don't do this
}
此外,shared_from_this() 不能在構造函數里被調用。
參考材料
- [1] C++ Primer
- [2] Effective Modern C++
- [3] Effective C++
- [4] Linux多執行緒服務端編程 使用muduo C++網路庫

