在上一篇文章中介紹了作為資源管理類支柱的 Resource Acquisition Is Initialization (RAII) 原則,並描述了 auto_ptr 和 tr1::shared_ptr 在基於堆的資源上運用這一原則的表現。並非所有的資源都是基於堆的,然而,對於這樣的資源,像 auto_ptr 和 tr1::shared_ptr 這樣的智能指針通常就不像 resource handlers(資源管理者)那樣合適。在這種情況下,有時,你可能要根據你自己的需要去創建你自己的資源管理類。
例如,假設你使用 C API 提供的 lock 和 unlock 函數去操縱 Mutex 類型的互斥體對象:
void lock(Mutex *pm); // lock mutex pointed to by pm
void unlock(Mutex *pm); // unlock the mutex
為了確保你從不會忘記解鎖一個被你加了鎖的 Mutex,你希望創建一個類來管理鎖。RAII 原則規定了這樣一個類的基本結構,通過構造函數獲取資源並通過析構函數釋放它:
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 對象被拷貝應該發生什麼?
Lock ml1(&m); // lock m
Lock ml2(ml1); // copy ml1 to ml2-what should
// happen here?
這是一個更一般問題的特定實例,每一個 RAII 類的作者都要面臨這樣的問題:當一個 RAII 對象被拷貝的時候應該發生什麼?大多數情況下,你可以從下面各種可能性中挑選一個:
禁止拷貝。在很多情況下,允許 RAII 被拷貝是沒有意義的。這對於像 Lock 這樣類很可能是正確的,因為同步的基本要素的“副本”很少有什麼意義。當拷貝對一個 RAII 類沒有什麼意義的時候,你應該禁止它。Item 6 解釋了如何做到這一點。聲明拷貝操作為私有。對於 Lock,看起來也許像這樣:
class Lock: private Uncopyable { // prohibit copying - see
public: // Item 6
... // as before
};
對底層的資源引用計數。有時人們需要的是保持一個資源直到最後一個使用它的對象被銷毀。在這種情況下,拷貝一個 RAII 對象應該增加引用這一資源的對象的數目。這也就是使用 tr1::shared_ptr 時“拷貝”的含意。
通常,RAII 類只需要包含一個 tr1::shared_ptr 數據成員就能夠實現引用計數的拷貝行為。例如,如果 Lock 要使用引用計數,他可能要將 mutexPtr 的類型從 Mutex* 改變為 tr1::shared_ptr<Mutex>。不幸的是,tr1::shared_ptr 的缺省行為是當它所指向的東西的引用計數變為 0 的時候將它刪除,但這不是我們要的。當我們使用 Mutex 完畢後,我們想要將它解鎖,而不是將它刪除。
幸運的是,tr1::shared_ptr 允許一個 "deleter" 規范——當引用計數變為 0 時調用的一個函數或者函數對象。(這一功能是 auto_ptr 所沒有的,auto_ptr 總是刪除它的指針。)deleter 是 tr1::shared_ptr 的構造函數的可選的第二個參數,所以,代碼看起來就像這樣:
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 類是如何不再聲明一個析構函數的。那是因為它不再需要。Item 5 解釋了一個類的析構函數(無論它是編譯器生成還是用戶定義)會自動調用這個類的非靜態(non-static)數據成員的析構函數。在本例中,就是 mutexPtr。但是,當互斥體的引用計數變為 0 時,mutexPtr 的析構函數會自動調用的是 tr1::shared_ptr 的 deleter ——在此就是 unlock。(看過這個類的源代碼的人多半意識到需要增加一條注釋表明你並非忘記了析構,而只是依賴編譯器生成的缺省行為。)
拷貝底層的資源。有時就像你所希望的你可以擁有一個資源的多個副本,唯一的前提是你需要一個資源管理類確保當你使用完它之後,每一副本都會被釋放。在這種情況下,拷貝一個資源管理對象也要同時拷貝被它隱藏的資源。也就是說,拷貝一個資源管理類需要完成一次“深層拷貝”。
某些標准 string 類型的實現是由堆內存的指針組成,堆內存中存儲著組成那個 string 的字符。這樣的字符串對象包含指向堆內存的指針。當一個 string 對象被拷貝,這個副本應該由那個指針和它所指向的內存組成。這樣的 strings 表現為深層拷貝。
傳遞底層資源的所有權。在某些特殊場合,你可能希望確保只有一個 RAII 對象引用一個未加工的資源,而當這個 RAII 對象被拷貝的時候,資源的所有權從被拷貝的對象傳遞到拷貝對象。就像上一篇文章所說明的,這就是使用 auto_ptr 時“拷貝”的含意。
拷貝函數(copying functions)(拷貝構造函數和拷貝賦值運算符)可能是由編譯器生成的,所以除非編譯器生成的版本所做的事正是你所要的,你應該自己編寫它們。在某些情況下,你也要支持這些函數的泛型化版本。
Things to Remember
·拷貝一個 RAII 對象必須拷貝它所管理的資源,所以資源的拷貝行為決定了 RAII 對象的拷貝行為。
·普通的 RAII 類的拷貝行為不接受拷貝和進行引用計數,但是其它行為是有可能的。