當你寫一個catch子句時,必須確定讓異常通過何種方式傳遞到catch子句裡。你可以有三個選擇:與你給函數傳遞參數一樣,通過指針(by pointer),通過傳值(by value)或通過引用(by reference)。
我們首先討論通過指針方式捕獲異常(catch by pointer)。從throw處傳遞一個異常到catch子句是一個緩慢的過程,在理論上這種方法的實現對於這個過程來說是效率最高的。因為在傳遞異常信息時,只有采用通過指針拋出異常的方法才能夠做到不拷貝對象,例如:
class exception { ... }; // 來自標准C++庫(STL)
// 中的異常類層次
void someFunction()
{
static exception ex; // 異常對象
...
throw &ex; // 拋出一個指針,指向ex
...
}
void doSomething()
{
try {
someFunction(); // 拋出一個 exception*
}
catch (exception *ex) { // 捕獲 exception*;
... // 沒有對象被拷貝
}
}
這看上去很不錯,但是實際情況卻不是這樣。為了能讓程序正常運行,程序員定義異常對象時必須確保當程序控制權離開拋出指針的函數後,對象還能夠繼續生存。全局與靜態對象都能夠做到這一點,但是程序員很容易忘記這個約束。如果真是如此的話,他們會這樣寫代碼:
void someFunction()
{
exception ex; // 局部異常對象;
// 當退出函數的生存空間時
// 這個對象將被釋放。
...
throw &ex; // 拋出一個指針,指向
... // 已被釋放的對象
}
這簡直糟糕透了,因為處理這個異常的catch子句接受到的指針,其指向的對象已經不再存在。
另一種拋出指針的方法是在建立一個堆對象(new heap object):
void someFunction()
{
...
throw new exception; // 拋出一個指針,指向一個在堆中
... // 建立的對象(希望
}
// 自己不要再拋出一個
// 異常!)
這避免了捕獲一個指向已被釋放對象的指針的問題,但是catch子句的作者又面臨一個令人頭疼的問題:他們是否應該刪除他們接受的指針?如果是在堆中建立的異常對象,那他們必須刪除它,否則會造成資源洩漏。如果不是在堆中建立的異常對象,他們絕對不能刪除它,否則程序的行為將不可預測。該如何做呢?
這是不可能知道的。一些clients可能會傳遞全局或靜態對象的地址,另一些可能轉遞堆中建立的異常對象的地址。通過指針捕獲異常,將遇到一個哈姆雷特式的難題:是刪除還是不刪除?這是一個難以回答的問題。所以你最好避開它。
而且,通過指針捕獲異常也不符合C++語言本身的規范。四個標准的異常――bad_alloc(當operator new(參見條款8)不能分配足夠的內存時,被拋出),bad_cast(當dynamic_cast針對一個引用(reference)操作失敗時,被拋出),bad_typeid(當dynamic_cast對空指針進行操作時,被拋出)和bad_exception(用於unexpected異常;參見條款14)――都不是指向對象的指針,所以你必須通過值或引用來捕獲它們。
通過值捕獲異常(catch-by-value)可以解決上述的問題,例如異常對象刪除的問題和使用標准異常類型的問題。但是當它們被拋出時系統將對異常對象拷貝兩次(參見條款12)。而且它會產生slicing problem,即派生類的異常對象被做為基類異常對象捕獲時,那它的派生類行為就被切掉了(sliced off)。這樣的sliced對象實際上是一個基類對象:它們沒有派生類的數據成員,而且當調用它們的虛擬函數時,系統解析後調用的是基類對象的函數。(當一個對象通過傳值方式傳遞給函數,也會發生一樣的情況――參見Effective C++ 條款22)。例如下面這個程序采用了擴展自標准異常類的異常類層次體系:
class exception { // 如上,這是
public: // 一個標准異常類
virtual const char * what() throw();
// 返回異常的簡短描述.
... // (在函數聲明的結尾處
// 的"throw()",
}; //有關它的信息
class runtime_error: //也來自標准C++異常類
public exception { ... };
class Validation_error: // 客戶自己加入個類
public runtime_error {
public:
virtual const char * what() throw();
// 重新定義在異常類中
... //虛擬函數
}; //
void someFunction() // 拋出一個 validation
{ // 異常
...
if (a validation 測試失敗) {
throw Validation_error();
}
...
}
void doSomething()
{
try {
someFunction(); // 拋出 validation
} //異常
catch (exception ex) { //捕獲所有標准異常類
// 或它的派生類
cerr << ex.what(); // 調用 exception::what(),
... // 而不是Validation_error::what()
}
}
調用的是基類的what函數,即使被拋出的異常對象是Validation_error和 Validation_error類型,它們已經重新定義的虛擬函數。這種slicing行為絕不是你所期望的。
最後剩下方法就是通過引用捕獲異常(catch-by-reference)。通過引用捕獲異常能使你避開上述所有問題。不象通過指針捕獲異常,這種方法不會有對象刪除的問題而且也能捕獲標准異常類型。也不象通過值捕獲異常,這種方法沒有slicing problem,而且異常對象只被拷貝一次。
我們采用通過引用捕獲異常的方法重寫最後那個例子,如下所示:
void someFunction() //這個函數沒有改變
{
...
if (a validation 測試失敗) {
throw Validation_error();
}
...
}
void doSomething()
{
try {
someFunction(); // 沒有改變
}
catch (exception& ex) { // 這裡,我們通過引用捕獲異常
// 以替代原來的通過值捕獲
cerr << ex.what(); // 現在調用的是
// Validation_error::what(),
... // 而不是 exception::what()
}
}
這裡沒有對throw進行任何改變,僅僅改變了catch子句,給它加了一個&符號。然而這個微小的改變能造成了巨大的變化,因為catch塊中的虛擬函數能夠如我們所願那樣工作了:調用的Validation_erro函數是我們重新定義過的函數。
如果你通過引用捕獲異常(catch by reference),你就能避開上述所有問題,不會為是否刪除異常對象而煩惱;能夠避開slicing異常對象;能夠捕獲標准異常類型;減少異常對象需要被拷貝的數目。所以你還在等什麼?通過引用捕獲異常吧(Catch exceptions by reference)!