前言

之前說過想花點整理 boost 和 C++11 當中 Smart Ptr 使用上的小細節,本系列假設讀者對 Smart Ptr 有一定程度的認識,只做一些重點探討。

目前 smart pointer 的現況:

  • C++ 11:unique_ptr、shared_ptr、weak_ptr

    • 廢除 auto_ptr。
  • Boost:scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr、intrusive_ptr。

    • Howard Hinnant 提供了非 Boost 官方的 unique_ptr C++03 backport,由於底層語言機制的限制,行為可能無法完全與 C++11 相容。
    • 在 Boost.interprocess 中提供上述所有指標的 interprocess 版本,其中 unique_ptr 是由 Hinnant 版衍生而來。

由於受個人使用經驗的限制,本系列內容可能會非常偏向 boost,有需要的話會另外補充 C++11 內容,

intrusive_ptr

我想先從 intrusive_ptr 開始,因為我發現網路上很多教學文章講到 intrusive_ptr 都草草了事。一般情況下建議優先考慮 shared_ptr,甚至 C++11 也沒有收錄 intrusive_ptr,然而 intrusive_ptr 在實用上還是相當具有吸引力:

  • intrusive_ptr 比 shared_ptr 來得有效率,主要原因之一是 shared_ptr 有考慮到部分的 thread-safty 問題(但沒有完全解決),這是一般 intrusive_ptr 使用者懶得做的事。另一個原因是,shared_ptr 必須要動態配置額外的空間來存放引用計數,這也會造成微小的 overhead,幸運的是這點可用 make_shared() 解決。
  • intrusive_ptr 可以充分利用物件既有的計數功能,甚至能用計數以外的方式設計引用管理功能。
  • 可以從同一個原生指標建立多個 intrusive_ptr ,這是 shared_ptr 無法辦到的事。

intrusive_ptr 主要缺點為無法使用 weak_ptr,因此循環引用會是個麻煩。

使用 intrusive_ptr 的最低需求,就是必須在編譯器可見的範圍內為目標類別提供 intrusive_ptr_add_ref 和 intrusive_ptr_release 函數。在網路上很容易找到一些恰恰滿足最低需求但有瑕疵的範例,類似這樣子:

class MyObject
{
public:
    friend void intrusive_ptr_add_ref(const MyObject* p)
    {
        ++p->refCount_;
    }

    friend void intrusive_ptr_release(const MyObject* p)
    {
        if (--p->refCount_ == 0)
            delete p;
    }

    MyObject() : refCount_(0) { cout << "hello. \n" ; }
    ~MyObject() { cout << "bye. \n" ; }

private:
    long refCount_;
};

雖然滿足語法上的最低需求,但在使用上很容易出問題。在使用 intrusive_ptr 時 ,絕大多數時候會將引用計數實作成物件的一部分。但這個概念並不是很精確,當我們在考慮一個物件的內容時,其實並沒有把引用計數算在內。上面這個版本連最簡單的例子都編不過:

intrusive_ptr<MyObject> obj1;       // ok
intrusive_ptr<const MyObject> obj2; // fail

上面這段程式碼告訴我們,儘管我們希望obj2所指的物件不能被修改,可是計數器還是照樣要跑啊。所以refCount_必須宣告為 mutable。事情還沒完,考慮下面的的範例:

intrusive_ptr<MyObject> proto(new MyObject);
...
intrusive_ptr<MyObject> p( new MyObject(*proto) );

程式目的是依據 proto 所指的物件內容建立新物件,由於 MyObject 沒有實作複製建構式,所以編譯器會免費贈送一個。可是預設的複製建構子會順便把引用計數也給複製過去,新生成的物件打從一開始就沒有正確計數,因此最後並不會被自動收拾掉。指派運算也是同樣的道理:

// p1 and p2 are both instances of intrusive_ptr<MyObject>
*p1 = *p2;

雖然一般情形不會故意把程式寫成這樣,不過當程式複雜起來的時候,複製動作可能會以更間接、隱晦的方式出現。要使上面的範例達到最基本的可用性,至少要作到:(1)令refCount_為 mutable;(2)明確自訂複製建構子將引用計數歸零;(3)在 operator= 讓引用計數保持不變。

class MyObject
{
public:
    ...

    MyObject(const MyObject& other) : refCount_(0) { ... }

    MyObject& operator=(const MyObject& other)
    {
        // Copy other members, but make this->refCount_ unchanged.
    }

private:
    mutable long refCount_;
};

透過繼承獲得引用計數

如果沒有特別需求,intrusive_ptr_add_ref() 和 intrusive_ptr_release() 函數的實作幾乎是一成不變,透過繼承以重用這兩個函數很有吸引力。最常見的情形應該是在建立某種類別家族,所有衍生物件都會透過共通的基底類別指標操作,這種情況很適合在基底類別實作引用計數;另一種可能的情形是透過 CRTP 式的繼承。

像上面的 MyObject 寫法,intrusive_ptr_add_ref 和 intrusive_ptr_release 函數即可透過繼承讓衍生類別使用。當然,MyClass 還必須依照用途稍微修改。例如作為類別家族的基底,解構式須為 virtual;如果是 CRTP 繼承,則建構/解構式可為 protected non-virtual。

麻煩的地方在於,多重繼承有可能會造成同一個物件有多組計數器。典型的鑽石型繼承雖然能透過 virtual 繼承解決問題,不過若能忍受 virtual 引入了額外的 overhead,或許直接用 shared_ptr 還比較省事。回過頭來說,鑽石型繼承通常是麻煩製造者,或許也該考慮設計是否合理。

當同一個物件存在多組計數器時,空間浪費就不用說了,最大的麻煩在於當不同類型 intrusive_ptr 指向同一個物件時將導致計數器不一致,物件可能被提早被刪除而形成 dangling pointer。這個問題無法在程式語言的層面解決,必須在設計的時候就規劃好物件持有權的政策。

novus 發表在 痞客邦 PIXNET 留言(0) 人氣()