1. 自己實現一個資源管理類 Item 13中介紹了 “資源獲取之時也是初始化之時(RAII)”的概念,這個概念被當作資源管理類的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如何用堆資源來表現這個概念的。然而並不是所有資源都是在堆上創建的,對於這種資源,像auto_ptr和t ...
1. 自己實現一個資源管理類
Item 13中介紹了 “資源獲取之時也是初始化之時(RAII)”的概念,這個概念被當作資源管理類的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如何用堆資源來表現這個概念的。然而並不是所有資源都是在堆上創建的,對於這種資源,像auto_ptr和tr1::shared_ptr這樣的智能指針就不適合當作資源句柄(handle)來使用了。你會發現你時不時的就會需要創建自己的資源管理類。
舉個例子,假設你正在使用C API來操縱Mutex類型的互斥信號量對象,來為函數提供lock和unlock:
1 void lock(Mutex *pm); // lock mutex pointed to by pm 2 3 void unlock(Mutex *pm); // unlock the mutex
為了確保你不會忘記unlock一個已經加過鎖的Mutex,你需要創建一個類來管理鎖。這樣一個類的基本結構已經由RAII準則表述過了,也就是資源會在執行構造的時候獲取到,在執行析構的時候釋放掉:
1 class Lock { 2 3 public: 4 5 explicit Lock(Mutex *pm) 6 7 : mutexPtr(pm) 8 9 { lock(mutexPtr); } // acquire resource 10 11 ~Lock() { unlock(mutexPtr); } // release resource 12 13 private: 14 15 Mutex *mutexPtr; 16 17 };
客戶端以傳統的RAII方式來使用鎖:
1 Mutex m; // define the mutex you need to use 2 3 ... 4 5 { // create block to define critical section 6 7 Lock ml(&m); // lock the mutex 8 9 ... // perform critical section operations 10 11 } // automatically unlock mutex at end 12 13 // of block
2. 對資源管理類進行拷貝會發生什麼?
這很好,但如果一個鎖對象被拷貝會發生什麼呢?
1 Lock ml1(&m); // lock m 2 3 Lock ml2(ml1); // copy ml1 to ml2 — what should 4 5 // happen here?
上面是一個更加普通的問題,也是每個RAII類的作者必須面對的:當一個RAII對象被拷貝的時候應該發生什麼呢?大多數情況下,你將會從下麵的4種可能中選擇一個:
2.1 禁止拷貝
- 禁止拷貝。在許多情況下,允許RAII對象被拷貝是沒有意義的。對於一個像Lock的類來說這可能是真的,因為一份同步原語(synchronization primitives)的拷貝很少情況下是有意義的。當一個RAII類的拷貝沒有意義時,你應該禁止它。Item 6解釋瞭如何可以做到:將拷貝操作聲明稱private。對於Lock來說,可以是下麵這個樣子:
1 class Lock: private Uncopyable { // prohibit copying — see 2 3 public: // Item 6 4 5 ... // as before 6 7 };
2.2 一份資源,多次引用——使用tr1::shared_ptr
- 對底層資源進行引用計數。有時候需要保留一個資源直到引用這個資源的最後一個對象被銷毀。在這種情況下,拷貝一個RAII對象應該增加對象引用資源的引用計數。這就是用tr1::shared_ptr進行“拷貝”的含義。
通常情況下,RAII類可以通過包含一個tr1::shared_ptr數據成員來實現引用計數的拷貝行為。舉個例子,如果Lock想使用引用計數,它可以將mutexPtr的類型從Mutex*改為tr1::shared_ptr<Mutex>。不幸的是,tr1::shared_ptr的預設行為是當引用技術為0的時候會刪除它所指向的資源,這不是我們想要的。當我們實現一個Mutex類時,我們只是想unlock,並不想刪除它們。幸運的是,tr1::shared_ptr允許指定自己的刪除器(”deleter”)---一個函數或者函數對象,引用計數為0的時候會自動調用這個對像。(auto_ptr中不存在這個功能,它總是會刪除指針。)這個刪除器是tr1::shared_ptr構造函數的第二個可選參數,所以代碼會是下麵這個樣子:
1 class Lock { 2 3 public: 4 5 explicit Lock(Mutex *pm) // init shared_ptr with the Mutex 6 7 : mutexPtr(pm, unlock) // to point to and the unlock func 8 9 { // as the deleter† 10 11 lock(mutexPtr.get()); // see Item 15 for info on “get” 12 13 } 14 15 private: 16 17 std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr 18 19 }; // instead of raw pointer
註意在這個例子中,Lock類不再聲明析構函數。因為沒有必要了。Item 5 解釋到一個類的析構函數(無論是編譯器生成的還是用戶定義的)會自動調用類中的非靜態數據成員的析構函數。在這個例子中,非靜態數據成員為mutexPtr。但是在mutex的引用計數為0的時候其的析構函數會自動調用tr1::shared_ptr的刪除器—也即是unlock。(人們在看到類的源碼的時候如果有一行註釋來說明你沒有忘記析構,你只是使用了編譯器預設生成的析構函數,他們會很感激的。)
2.3 一份資源,多次拷貝——深拷貝
- 拷貝底層的資源。有時你可以擁有一個資源儘可能多的拷貝,你需要一個資源管理類的唯一原因是能夠確保資源被使用完畢後能夠被釋放掉。這種情況下,拷貝一個資源管理對象應該同時拷貝他所包裹(wraps)的資源。也就是拷貝一個資源管理類對象需要執行“深拷貝”。
有一些標準string類型的實現中包含了指向堆記憶體的指針,組成string的字元會保存在這塊記憶體中。當一個string對象被拷貝的時候,會同時拷貝指針和指針指向的記憶體。這樣的string展示出來的是深拷貝。
2.4 一份資源,一次引用,轉移所有權——使用auto_ptr
- 轉移底層資源的所有權。在很少的場合,你可能需要確保只有一個RAII對象指向一個原生(raw)資源,所以當RAII對象被拷貝的時候,資源的擁有權從被拷貝對象轉移到了拷貝到的對象。正如Item 13所解釋的,這是使用auto_ptr進行拷貝的含義。
拷貝函數可能由編譯器生成,所以除非編譯器生成版本能夠做到你想要的(Item 5解釋了預設版本的行為),否則你需要自己實現它們。一些情況下你可能想支持這些函數的一般版本。這些版本在Item 45進行描述。
3. 總結
- 拷貝一個RAII對象需要拷貝他所管理的資源,因此資源的拷貝行為決定了RAII對象的拷貝行為。
- 普通RAII類的拷貝行為是禁止拷貝,執行引用計數,但其他拷貝行為也是可以實現的。