資源管理類的特征是振奮人心的。它構筑起一道可靠的屏障,可有效地防止資源泄漏。能否預防資源泄漏是“系統的設計方案是否優異”的一個基本評判標準。在完美的世界里,你應該依靠資源管理類來完成所有的與資源交互的工作,而永遠不要直接訪問原始資源。然而世界并不是完美的。由于許多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 訪問可以通過顯式轉換或隱式轉換來實現。一般情況下,顯式轉換更安全,而隱式轉換對于客戶來說更方便。