今天繼續來看resource managing系列的第二個守則,看看開始用物件來管理resource後,還有哪些注意事項。
今天的守則是:
Think carefully about copying behavior in resource-managing classes。
昨天有提到可以利用smart pointer來管理heap-based的資源,不過,如第三章前言有提到的,還有一些資源是heap-based這些memory以外的東西。例如,當我們用到mutex objects:
void lock(Mutex *pm);
void unlock(Mutext *pm);
為了確保不會忘記unlock Mutex
object,我們也可以用一個class來管理這些locks資源。我們一樣遵循RAII原則來管理這個resource,讓這個lock被物件管理,並在destroy的時候自動unlock:
class Lock
{
public:
explicit Lock(Mutex *pm): mutexPtr(pm)
{
lock(mutextPtr);
}
~Lock()
{
unclock(mutextPtr);
}
private:
Mutex *mutexPtr;
}
clients在用的時候就可以這樣用:
Mutex m;
...
{
Lock ml(&m);
...
}
以上看起來沒什麼問題,不過當Lock
這個物件被copy的時候,會發生什麼事呢?
Lock ml1(&m)l
Lock ml2(ml1);
其實所有RAII class都會面對到這個問題,當RAII物件被複製的時候,我們應該怎麼做?
很多情況下RAII物件其實不該能被複製,例如lock
的情形,複製同步處理物件沒有意義;而此時如果有禁止,就可以參考第六條守則─[Day 10] Explicitly disallow the use of compiler-generated functions you do not want,去把不想要有的copying functions ban掉。
Lock(const Lock&)= delete;
Lock& operator=(const Lock&)= delete;
有時候我們對資源的管理,是預期資源直到最後一個有用到它的物件被destroy後才跟著釋放,而在這個情形下,每copy一次等於是多一個用到它的物件,所以參考計數要+1,這也是shared_ptr
採用的概念。
而如果要採用這種reference-counting copying的方式,我們通常可以使用shared_ptr
來做。例如當我們想要讓我們的Lock
採用這種方式,我們可以把mutexPtr
從Mutex*
轉成shared_ptr<Mutex>
。不過shared_ptr
的預設destructor行為是delete,這邊我們期望的release行為則是unlock,需要做一些處理─ shared_ptr
有開放 deleter 的參數,讓你可以傳入想要在ref count歸0時做的行為,可以這樣寫:
class Lock
{
public:
explicit Lock(Mutex *pm): mutexPtr(pm, unlock)
{
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr;
}
值得注意的是,採用此寫法之後,我們沒有再定義Lock的destructor了。why?因為unlock這個行為會在shared_ptr
ref count歸0 的時候自動去call,因此就不需要了。
有時候我們的設計是想要讓resource可以被無限複製,而resource-managing class只是要確保這個resource被用完後又被正確release,這情況下,copy resource managing物件就需要同步copy它管理的resource,那就是做deep copy的概念。
std::string就是其中一個範例。這個物件包含了指向heap memory的pointer,而當string
被複製的時候,這個copy就包含了pointer跟它指向的memory,就是做了deep copy。
還有一個稀有的情況,就是需要確保只有一個RAII物件在管理一個resource,所以當它被copy的時候,也需要同步把ownership轉給copy的物件。這就像是auto_ptr
的行為─只有一個人能指向它,所以copy過去之後原本的會被設為null(在unique_ptr
的情況下,我們可以用move
來轉移ownership)。
如第五個守則所說([Day 9] Know what functions C++ silently writes and calls),copying functions會自動由compiler產生,而在resource manaing object的case中,更需要分外注意default產生的functions是不是我們預期的,若不是要記得按照上面幾種方式去處理。
貼心重點提醒:
- Copying an RAII object entails copying the resource it manages, so the copying behavior of the resource determines the copying behavior of the RAII obejct.
- Common RAII class copying behvaviors are disallowing copying and performing reference counting, but other behaviors are possible.
第一就是切合主題,RAII的copy要注意它所管理的resource是怎麼被copy的;第二則是通常會直接把resource managing object的copy行為ban掉,但仍然有其他處理方式。