Item 14: 謹慎考慮 resource-managing classes(資源管理類)中的拷貝行為
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
Item 13 介紹了作為 resource-managing classes(資源管理類)支柱的 Resource Acquisition Is Initialization (RAII) 原則,並描述了 auto_ptr 和 tr1::shared_ptr 在 heap-based(基於堆)的資源上運用這一原則的表現。然而,並非所有的資源都是 heap-based(基於堆)的,而對於這樣的資源,像 auto_ptr 和 tr1::shared_ptr 這樣的 smart pointers(智能指標)通常就不像 resource handlers(資源控制代碼)那樣合適。在這種情況下,有時,你很可能要根據你自己的需要去建立你自己的 resource-managing classes(資源管理類)。
例如,假設你使用一個 C API 提供的 lock 和 unlock 函數去操縱 Mutex 類型的 mutex objects(互斥體對象):
void lock(Mutex *pm); // lock mutex pointed to by pm
void unlock(Mutex *pm); // unlock the mutex
為了確保你從不會忘記解鎖一個被你加了鎖的 Mutex,你案打算建立一個 class 來管理鎖。這樣一個 class 的基本結構被 RAII 原則規定,通過 construction(建構函式)擷取資源並通過 destruction(解構函式)釋放它:
class Lock {
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{ lock(mutexPtr); } // acquire resource
~Lock() { unlock(mutexPtr); } // release resource
private:
Mutex *mutexPtr;
};
客戶按照慣常的 RAII 風格來使用 Lock:
Mutex m; // define the mutex you need to use
...
{ // create block to define critical section
Lock ml(&m); // lock the mutex
... // perform critical section operations
} // automatically unlock mutex at end
// of block
這沒什麼問題,但是如果一個 Lock object 被拷貝應該發生什嗎?
Lock ml1(&m); // lock m
Lock ml2(ml1); // copy ml1 to ml2—what should
// happen here?
這是一個更一般的問題的特定執行個體,每一個 RAII class 的作者都必須面對它:當一個 RAII object 被拷貝的時候應該發生什嗎?大多數情況下,你需要從下面各種可能性中挑選一個:
- prohibit copying(禁止拷貝)。在很多情況下,允許 RAII objects 被拷貝是沒有意義的。這對於像 Lock 這樣 class 很可能是正確的,因為同步原本的“拷貝”很少有什麼意義。當拷貝對一個 RAII class 沒有什麼意義的時候,你應該禁止它。Item 6 解釋了如何做到這一點:聲明拷貝操作為私人。對於 Lock,看起來可能就像這樣:
class Lock: private Uncopyable { // prohibit copying — see
public: // Item 6
... // as before
};
- reference-count the underlying resource(對底層的資源引用計數)。有時人們需要的是保持一個資源直到最後一個使用它的 object 被銷毀。在這種情況下,拷貝一個 RAII object 應該增加引用這一資源的 objects 的數目。這也就是被 tr1::shared_ptr 使用的 "copy"(“拷貝”)的含意。
通常,RAII classes 只需要包含一個 tr1::shared_ptr data member(資料成員)就能夠實現 reference-counting(引用計數)的拷貝行為。例如,如果 Lock 要使用 reference counting(引用計數),他可能要將 mutexPtr 的類型從 Mutex* 變為 tr1::shared_ptr<Mutex>。不幸的是,tr1::shared_ptr 的預設行為是當它所指向的東西的 reference count(引用計數)變為零的時候將它刪除,但這不是我們想要的。當我們使用完一個 Mutex 後,我們想要將它解鎖,而不是將它刪除。
幸運的是,tr1::shared_ptr 允許有一個對 "deleter"(當 reference count(引用計數)變為零時調用的一個 function(函數)或者 function object(函數對象))的特殊說明。(這一功能是 auto_ptr 所沒有的,auto_ptr 總是刪除它的 pointer(指標)。)deleter 是 tr1::shared_ptr 的 constructor(建構函式)的可選的第二個參數,所以,代碼看起來就像這樣:
class Lock {
public:
explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
: mutexPtr(pm, unlock) // to point to and the unlock func
{ // as the deleter
lock(mutexPtr.get()); // see Item 15 for info on "get"
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
}; // instead of raw pointer
在這個例子中,注意 Lock class 是如何不再聲明一個 destructor(解構函式)的。那是因為它不需要。Item 5 解釋了一個 class 的 destructor(解構函式)(無論它是 compiler-generated(編譯器產生)的還是 user-defined(使用者定義)的)會自動調用這個 class 的 non-static data members(非待用資料成員)的 destructors(解構函式)。在本例中,就是 mutexPtr。但是,當 mutex(互斥體)的 reference count(引用計數)變為零時,mutexPtr 的 destructor(解構函式)會自動調用 tr1::shared_ptr 的 deleter ——在此就是 unlock。(看過這個 class 的原始碼的人多半意識到需要增加一條注釋表明你並非忘記了 destruction(析構),而只是依賴 compiler-generated(編譯器產生)的預設行為。)
- copy the underlying resource(拷貝底層的資源)。有時就像你所希望的你可以擁有一個資源的多個拷貝,你需要一個 resource-managing class(資源管理類)的唯一理由就是確保當你使用完每一個拷貝之後,它都會被釋放。在這種情況下,拷貝一個 resource-managing object(資源管理對象)也應該同時拷貝它所包覆的資源。也就是說,拷貝一個 resource-managing object(資源管理對象)需要實行一次 "deep copy"(“深層拷貝”)。
某些標準 string 類型的實現是由 pointers to heap memory(指向堆記憶體的指標)構成,那裡儲存著組成那個 string(字串)的字元。這樣的 strings 的 objects 包含一個 pointers to heap memory(指向堆記憶體的指標)。當一個 string object 被拷貝,一個拷貝應該由那個指標和它所指向的記憶體兩者組成。這樣的 strings 表現為 deep copying(深層拷貝)。
- transfer ownership of the underlying resource(傳遞底層資源的所有權)。在非常特殊的場合,你可能希望確保只有一個 RAII object 引用一個 raw resource(裸資源),而當這個 RAII object 被拷貝的時候,資源的所有權從 copied object(被拷貝對象)傳遞到 copying object(拷貝對象)。就像 Item 13 所說明的,這就是被 auto_ptr 使用的 "copy"(“拷貝”)的含意。
copying functions(拷貝函數)(copy constructor(拷貝建構函式)和 copy assignment operator(拷貝賦值運算子))可能由編譯器產生,所以除非 compiler-generated(編譯器產生)的版本所做的事正是你想要的(Item 5 說明了這些預設行為),否則你應該自己編寫它們。在某些情況下,你還要支援這些函數的泛型化版本。這樣的版本在 Item 45 中描述。
Things to Remember
- 拷貝一個 RAII object 必須拷貝它所管理的資源,所以資源的拷貝行為決定了 RAII object 的拷貝行為。
- 通常的 RAII class 的拷貝行為是 disallowing copying(不允許拷貝)和 performing reference counting(實行引用計數),但是其它行為也是有可能的。