[轉]http://m.shnenglu.com/tiandejian/archive/2007/05/13/cp_15.html
第15條: 要為資源管理類提供對原始資源的訪問權
資源管理類的特征是振奮人心的。它構筑起可靠的屏障,有效地防止你的程序發生資源泄漏。對于一個系統的設計方案是否優異,能否預防這樣的泄漏可作為一個基本評判標準。在完美的世界里,你可以依靠資源管理類來完成所有的與資源交互的工作,你永遠也不能直接訪問原始資源。然而世界并不是完美的。許多 API 會直接引用資源,所以除非你發誓不使用這樣的 API (這樣做顯得太不實際了),否則,你必須繞過資源管理類,然后在需要的時候及時手工處理原始資源。
舉例說,第 13 條中引入了下面的做法 :使用諸如 auto_ptr 或者 tr1::shared_ptr 這樣的智能指針來保存諸如 createInvestment 這樣的工廠函數的返回值:
std::tr1::shared_ptr<Investment> pInv(createInvestment());
// 來自第 13 條
假設,當你使用一個 Investment 對象時,你需要一個這樣的函數:
int daysHeld(const Investment *pi); // 返回投資持續的天數
你可能希望這樣來調用它:
int days = daysHeld(pInv); // 錯!
但是這段代碼無法通過編譯:因為 daysHeld 需要一個原始的 Investment* 指針,但是你傳遞給它的對象的類型卻是 tr1::shared_ptr<Investment> 。
你需要一個渠道來 將一個 RAII 類的對 象(在上面的示例中是 tr1::shared_ptr )轉變為它所包含的原始資源(比如說,原始的 Investment* )。這里實現這一轉變有兩個一般的方法:顯式轉換和隱式轉換。
tr1::shared_ptr 和 auto_ptr 都提供了一個 get 成員函數來進行顯式轉換,也就是說,返回一個智能指針對象中的裸指針(的副本):
int days = daysHeld(pInv.get()); // 工作正常,將 pInv 中的
// 裸指針傳遞給 daysHeld
似乎所有的智能指針類,包括 tr1::shared_ptr 和 auto_ptr 等等,都會重載指針解析運算符( operator-> 和 operator* ),這便使得對原始裸指針進行隱式轉換成為現實:
class Investment { // 投資類型的層次結構中
// 最為根基的部分
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); // 工廠函數
std::tr1::shared_ptr<Investment> pi1(createInvestment());
// 使用 tr1::shared_ptr
// 管理資源
bool taxable1 = !(pi1->isTaxFree());
// 通過 operator-> 訪問資源
...
std::auto_ptr<Investment> pi2(createInvestment());
// 使用 auto_ptr 管理資源
bool taxable2 = !((*pi2).isTaxFree());
// 通過 operator* 訪問資源
...
由于 某些時刻你需要獲取一個 RAII 對象中的原始資源,所以一些 RAII 類的設計者使用了一個小手段來使系統正常運行,那就是:提供一個隱式轉換函數。舉例說,以下是一個 C 語言 API 中提供的處理字體的一個 RAII 類:
FontHandle getFont(); // 來自一個 C 語言 API
// 省略參數表以簡化代碼
void releaseFont(FontHandle fh); // 來自同一個 C 語言 API
class Font { // RAII 類
public:
explicit Font(FontHandle fh) // 通過傳值獲取資源
: f(fh) // 因為這一 C 語言 API 這樣做
{}
~Font() { releaseFont(f); } // 釋放資源
private:
FontHandle f; // 原始的字體資源
};
假設這里有一個大型的相關的 C 語言 API 僅僅通過 FontHandle 解決字體問題,那么將存在十分頻繁的把 Font 對象轉換為 FontHandle 的操作。 Font 類可以提供一個顯式轉換函數,比如 get :
class Font {
public:
...
FontHandle get() const { return f; }
// 進行顯式轉換的函數
...
};
遺憾的是,這樣做使得客戶端程序員在每次 與這一 API 通 信時都要調用一次 get :
void changeFontSize(FontHandle f, int newSize);
// 來自一個 C 語言 API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize);
// 顯式轉換:從 Font 到 FontHandle
由于需要顯式請求這樣的轉換,這樣做顯得得不償失,一些程序員也許會拒絕使用這個類。然而這又增加了字體資源泄漏的可能性,這與 Font 類的設計初衷是完全相悖的。
有一個替代方案,讓 Font 提供一個將其隱式轉換為 Fonthandle 的函數:
class Font {
public:
...
operator FontHandle() const { return f; }
// 進行隱式轉換的函數
...
};
這使得調用這一 C 語言 API 的工作變得簡潔而且自然:
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // 隱式轉換:從 Font 到 FontHandle
隱式轉換會帶來一定的負面效應:它會增加出錯的可能。比如說,一個客戶端程序員在一個需要 Font 的地方意外地創建了一個 FontHandle :
Font f1(getFont());
...
FontHandle f2 = f1; // 啊哦!本想復制一個 Font 對象,
// 但是卻卻將 f1 隱式轉換為其原始的
// FontHandle ,然后復制它
現在程序中有一個 FontHandle 資源正在由 Font 對象 f1 來管理,但是仍然可以通過 f2 直接訪問 FontHandle 資源。這是很糟糕的。比如說,當 f1 被銷毀時,字體就會被釋放, f2 也一樣。
是 為 RAII 類提供顯式轉換為其原始資源的方法,還是允許隱式轉換,上面兩個問題的答案取決于 RAII 類設計用于完成的具體任務,及其被使用的具體環境。最好的設計方案應該遵循第 18 條 的建議,讓接口更容易被正確使用,而不易被誤用。通常情況下,定義一個類似于 get 的顯式轉換函數是一個較好的途徑,應為它可以使非故意類型轉換的可能性降至最低。然而,使用隱式類型轉換顯得更加自然,人們更趨向于使用它。
你可能已經發現,讓一個函數返 回一個 RAII 類內部的原始資源是違背封裝性原則的。的確是這樣,但是它實際上并不像看上去那樣糟糕。 RAII 類并不是用來封裝什么的。它們是用來進行一些特別的操作的,那就是資源釋放。如果需要,資源封裝工作可以放在這一主要功能的最頂端,但是這并不是必需的。另外,一 些 RAII 類 結合了實現封裝的嚴格性和原始資源封裝的寬松性。比如 tr1::shared_ptr 對其引用計數機制進行了整體封裝,但是它仍然為其所包含的裸指針提供了方便的訪問方法。就像其它設計優秀的類一樣,它隱藏了客戶端程序員不需要關心的內容,但是它使得客戶端程序員的確需要使用的部分對其可見。
牢記在心
l API 通常需要訪問原始資源,因此每個 RAII 類都應該提供一個途徑來獲取它所管理的資源。
l 訪問可以通過顯式轉換或隱式轉換來實現。一般情況下,顯式轉換更安全,但是隱式轉換對于客戶端程序員來說使用更方便。