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() 返回的指針就無效了;
  • 如果使用智慧指針管理資源而不是記憶體,應當定義自定義刪除器;
  • 如果將 shared_ptr 存放到容器中,之後不再需要其中的部分元素,應當調用 erase 將它們刪除,不然這些元素的生命周期會被意外地延長。

在單條語句里將 new 來的指針放到智慧指針里

int priority();

void processWidget(std::shared_ptr<Widget> pw, int priority);

processWidget(std::shared_ptr<Widget>(new Widget), priority());

該調用事實上由三部分組成:

  1. new Widget
  2. 調用 shared_ptr 構造函數
  3. 調用 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() 不只是分配對象的動態記憶體,還會同時分配控制塊的記憶體,這種情況下,就會造成麻煩。
  • 由於控制塊和對象放在同一塊記憶體里,引用計數為 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++網路庫
Tags: