More Effective C++ 13:通過引用捕獲異常

當你寫一個 catch 子句時,必須確定讓異常通過何種方式傳遞到 catch 子句裏。你可以有三個選擇:
1.指針2.傳值3.引用

通過指針

通過指針方式捕獲異常在理論上這種方法的實現對於這個過程來說是效率最高的。因爲在傳遞異常信息時,只有採用通過指針拋出異常的方法才能夠做到不拷貝對象。比如:

class exception { ... }; // 來自標準 C++庫(STL) 
 // 中的異常類層次 
 // (參見條款 12) 
void someFunction() 
{ 
	static exception ex; // 異常對象 
	... 
	throw &ex; // 拋出一個指針,指向 ex 
	... 
} 
void doSomething() 
{ 
	try 
	{ 
		someFunction(); // 拋出一個 exception* 
	} 
	catch (exception *ex) 
	{ // 捕獲 exception*; 
		... // 沒有對象被拷貝 
	} 
}

爲了能讓程序正常運行,程序員定義異常對象時必須確保當程序控制權離開拋出指針的函數後,對象還能夠繼續生存。全局與靜態對象都能夠做到這一點,但是程序員很容易忘記這個約束。如果真是如此的話,他們會這樣寫代碼:

void someFunction() 
{ 
	 exception ex; // 局部異常對象; 
	 // 當退出函數的生存空間時 
	 // 這個對象將被釋放。 
	 ... 
	 throw &ex; // 拋出一個指針,指向已被釋放的對象
	 ... 
}

這樣做很不好,因爲處理這個異常的 catch 子句接受到的指針,其指向的對象已經不再存在。

另一種拋出指針的方法是建立一個堆對象:

void someFunction() 
{ 
 ... 
 throw new exception; // 拋出一個指針,指向一個在堆中建立的對象 
 ... 
}

這避免了捕獲一個指向已被釋放對象的指針的問題,但是 catch 子句的作者又面臨一個令人頭疼的問題:
他們是否應該刪除他們接受的指針?如果是在堆中建立的異常對象,那他們必須刪除它,否則會造成資源泄漏。如果不是在堆中建立的異常對象,他們絕對不能刪除它,否則程序的行爲將不可預測。該如何做呢
這是不可能知道的,所以你最好避開它。

而且,通過指針捕獲異常也不符合 C++語言本身的規範。四個標準的異常bad_alloc(當operator new不能分配足夠的內存時,被拋出),bad_cast(當dynamic_cast 針對一個引用( reference)操作失敗時,被拋出),bad_typeid(當dynamic_cast 對空指針進行操作時,被拋出)和 bad_exception(用於 unexpected 異常)都不是指向對象的指針,所以你必須通過值或引用來捕獲它們。

通過值

通過值捕獲異常可以解決上述的問題,例如異常對象刪除的問題和使用標準異常類型的問題。但是當它們被拋出時系統將對異常對象拷貝兩次。而且它會產生 slicing problem,即派生類的異常對象被做爲基類異常對象捕獲時,那它的派生類行爲就被切掉了(sliced off)。
比如:

class exception 
{ // 如上,這是 
public: // 一個標準異常類 
	virtual const char * what() noexcept; 
 	// 返回異常的簡短描述.  
}; 

 
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 函 數 , 即 使 被 拋 出 的 異 常 對 象 是 runtime_errorValidation_error 類型並且它們已經重新定義了這個虛擬函數。這種 slicing 行爲絕不是你所期望的。

通過引用

最後剩下方法就是通過引用捕獲異常。通過引用捕獲異常能使你避開上述所有問題。不像通過指針捕獲異常,這種方法不會有對象刪除的問題而且也能捕獲標準異常類型。也不象通過值捕獲異常,這種方法沒有slicing problem,而且異常對象只被拷貝一次。

通過引用改下如下:

void someFunction() // 拋出一個 validation 
{ // 異常 
 ... 
	if (a validation 測試失敗) 
	{ 
		throw Validation_error(); 
	} 
... 
} 
void doSomething() 
{ 
	try 
	{ 
		someFunction(); // 拋出 validation異常 
	} 
	catch (exception& ex) //通過引用捕獲異常
	{ 
		cerr << ex.what(); //現在調用的是 Validation_error::what()

	} 
}

這裏沒有對 throw 進行任何改變,僅僅改變了 catch 子句,給它加了一個&符號。然而這個微小的改變能造成了巨大的變化,因爲 catch 塊中的虛擬函數能夠如我們所願那樣工作了:調用的 Validation_erro 函數是我們重新定義過的函數

總結

通過引用捕獲異常,能避免爲是否刪除異常對象而煩惱,能夠避開slicing異常對象,能夠捕獲標準異常類型;減少異常對象需要被拷貝的數目。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章