這章提到的 SmartPtr 設計細節很多,主要是使用上的需要支援許多功能,卻又需要考慮效能和維持易用的語法,所以有些地方需要取捨。這些地方,都是設計 library 的時候常常碰到的問題,是很好的範例。
Smart Pointers 101
Smart Pointer 主要的用法,就是除了宣告以外,其他地方要用起來跟 raw pointer 一樣,因此需要實作下面的 member function:
- explicit SmartPtr(T* pointee)
- SmartPtr& operator=(const SmartPtr& other)
- ~SmartPtr()
- T& operator*() const
- T* operator->() const
除了 operator*() 和 operator->(),還有其他 operator 是 raw pointer 支援的,但是 SmartPtr 考量使用情況不支援的:
- T** operator&(): 可以提供更多的 raw pointer 相容性,方便直接以 SmartPtr 替換 raw pointer。但是這會曝露裡面的指標,而且會讓 STL container 等 template 用來取址的時候出錯。
- Implicit Conversion Operator (operator T*()): 可以提供更多相容性,但是很容易被誤用,像是 delete spFoo 就可以自動轉型。這對於 raw pointer 直接把宣告換成 SmartPtr 的 code 非常容易出錯。
- Array operator[](): 應該盡量使用 std::vector,所以不支援 array。實際上可以用修改 StoragePolicy 的方式,在 destructor 呼叫對應的 delete[] 達成指向 array 的目的。要使用 operator[] 或者指標加減,需要額外取得實際的指標。
- 指標的 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:
Post a Comment