Wednesday, April 30, 2014

Smart Pointers, Ch7, Modern C++ Design

Smart pointer 對 C++ 來說,是很常用的東西。使用 smart pointer 主要的目的,是 "resource allocation is initialization (RAII)",這樣有能在 resource 不需要用的時候,藉由 smart pointer 自動 delete,省去一些管理 pointer 的困難。

這章提到的 SmartPtr 設計細節很多,主要是使用上的需要支援許多功能,卻又需要考慮效能和維持易用的語法,所以有些地方需要取捨。這些地方,都是設計 library 的時候常常碰到的問題,是很好的範例。

Smart Pointers 101


Smart Pointer 主要的用法,就是除了宣告以外,其他地方要用起來跟 raw pointer 一樣,因此需要實作下面的 member function:

  1. explicit SmartPtr(T* pointee)
  2. SmartPtr& operator=(const SmartPtr& other)
  3. ~SmartPtr()
  4. T& operator*() const
  5. T* operator->() const

除了 operator*() 和 operator->(),還有其他 operator 是 raw pointer 支援的,但是 SmartPtr 考量使用情況不支援的:

  1. T** operator&(): 可以提供更多的 raw pointer 相容性,方便直接以 SmartPtr 替換 raw pointer。但是這會曝露裡面的指標,而且會讓 STL container 等 template 用來取址的時候出錯。
  2. Implicit Conversion Operator (operator T*()): 可以提供更多相容性,但是很容易被誤用,像是 delete spFoo 就可以自動轉型。這對於 raw pointer 直接把宣告換成 SmartPtr 的 code 非常容易出錯。
  3. Array operator[](): 應該盡量使用 std::vector,所以不支援 array。實際上可以用修改 StoragePolicy 的方式,在 destructor 呼叫對應的 delete[] 達成指向 array 的目的。要使用 operator[] 或者指標加減,需要額外取得實際的指標。
  4. 指標的 ordering comparisons,SmartPtr 預設也沒有提供。SmartPtr 指到的位址通常是分別配置的,比較大小沒有意義。Effective C++ 也有提到,避免直接使用指標運算加減,同一個陣列應該用 pArray[index] 的寫法。

另外,operator==, operator!=, operator! 的部份,raw pointer 很常用,必需要支援。直接用 operator bool() 自動轉型有很多問題,像是兩個不同型的指標可以都自動轉成 bool 去比較,或者自動轉成 bool 跟其他整數型別比較。總之,C++ 自動轉型很容易出問題,像是 std::string 也是額外提供 c_str() 而不是自動轉型。因為沒有自動轉型,所以這些 operator 必須一個一個定義出來,包括 SmartPtr 跟 raw pointer 的排列組合。

SmartPtr Member Functions


除了 operator overload 為了讓 SmartPtr 用起來跟一般指標相容,SmartPtr 不提供 member function,因為容易跟 pointee 的 member function 搞渾。像是下面的 release,改成 global 比較明顯。

struct Foo{ void release(); };
SmartPtr<foo> spFoo;
spFoo->release();
spFoo.release(); // 跟上面搞混了
release(spFoo); // 明顯不是呼叫 Foo

Multithreaded Reference Linking


因為使用環狀的 double-linked list,恰好兩個 SmartPtr 物件的情況,prev_ == next_ 對兩個物件都成立。下面這節處理 linked-list 的 code 需要改進,才能處理兩個物件的情況。修改以後,恰好兩個物件的時候,走 else 的分支,也會形成正確的 circular double-linked list.

//7.13.2.2 Multithreaded Reference Linking, p. 164
~SmartPtr()
{
    if ((prev_ == next_) && (prev_ == this))
    {
        delete pointee_;
    }
    else
    {
        prev_->next_ = next_;
        next_->prev_ = prev_;
    }
}

這裡還有另外一個觀察,SmartPtr 維護 book-keeping data 的 thread safe 的重要性。Book-keeping 只有在 construct, assign, destruct 這三個地方發生,相對於 dereference 算是很少的。個人經驗上,通常是某 thread 頻繁使用某變數,而其他任何一個 thread 偶爾用到同一個變數,這種情況比較容易發生 race-condition。如果 SmartPtr 沒有頻繁複製的話,應該不太需要擔心 thread safety。這也可以解釋 reference-linking 不處理 thread 問題,因為要額外處理 mutex lock 太花成本。相比之下,reference-counting 只要 atomic increment 就達成 thread safe,相對來說快很多。


Storage Policy Revisited


讀到 7.14.1 The Storage Policy 的時候,突然想到 C code 常有下面這樣的 API 設計:

file_id fid = file_open("aaa.txt");
file_read(fid);
…
file_close(fid);

像是 file_id 這樣的 handle,作用跟指標類似,也需要資源管理。這樣的 handle 可以用 SmartPtr 來操作嗎?測試以後確定,只要適當的使用 StoragePolicy,定義好 StoredType 就可以用 SmartPtr 來管理這種 handle,但是只能管理 ownership,不能用 operator->() 或者 operator*(),因為無法對應相關的 global function。如此使用上需要 GetImp(ptrHandle),這是先天的限制,因為 handle 或 id 常常就是為了不直接使用 pointer 的刻意設計。

心得:實用案例


假設有某個 Task 需要二個不同 resource 才能運作,

struct Task
{
    SmartPtr<R1> pR1;
    SmartPtr<R2> pR2;
};

在 StartTask() 的時候,需要取得每一個 resource,每個都取得才會成功。

bool StartTask()
{
    Task task;
    if(NULL == (task.pR1 = getResource1()))
    {
        return false;
    }
    if(NULL == (task.pR2 = getResource2()))
    {
        return false; // 自動釋放 task.pR1
    }
    m_runningTask = task; // 成功了,保存這個 Task 的狀態
    return true;
}

如此這段邏輯就很清楚,萬一中間多插了一個 return,或是發生 exception,也可以正確釋放資源。這個例子裡面,SmartPtr 使用 ownership transfer 就夠用了。如果其他地方有用到,reference counting 就比較適合。

No comments: