1. 程式人生 > >9 More Effective C++—條款12(異常的原理細節)

9 More Effective C++—條款12(異常的原理細節)

1 異常與函式呼叫

丟擲異常與傳遞一個引數、呼叫一個虛擬函式有許多類似點:

1,某個類物件被接受
2,被接受的類物件可以選擇不同的接收端,從而實現多型。
3,可以通過by-value, by-reference, by-pointer三種方式來傳遞類物件。

但是,實際上呼叫函式傳遞引數,與try中丟擲異常,並被catch捕捉異常時完全不同的。

2 被丟擲的物件總是一個副本

每當丟擲exception時候,exception總會被複制。如下面的程式碼

// 示例1:丟擲的異常為區域性變數
void passThrowWidget() {
	Widget widget;
	doSomething(widget);
	
	// 丟擲的物件是widget的一個副本
	// 當前作用域的widget在離開本函式時已經被銷燬
	throw widget; 
}

// 示例2:丟擲的異常為”靜態區域性變數“
void passThrowWidget() {
	static Widget widget;
	doSomething(widget);
	// 儘管本函式內的widget不會被銷燬,但是丟擲的widget依然是一個副本
	throw widget;
}

如上面的兩個例子,無論原物件以什麼形式定義,丟擲的物件總是一個副本。這樣做保證了,catch捕獲的物件總能存在,否則可能導致捕獲的異常物件已經被銷燬。

3 傳遞丟擲

被丟擲的異常物件會呼叫其”複製建構函式”,複製建構函式以“靜態型別”為模板建立。若基類引用指向派生類物件,則異常將會呼叫基類複製建構函式被建立。如下面程式碼所示。

class Widget;
class ChildWidget : public Widget {
}
void passThrowWidget() {
	ChildWidget child;
	Widget &widget = child;
	throw widget;  // 呼叫Widget的複製建構函式進行復制,而不是ChildWidget
}

基於上面原因,下面兩種方式的異常丟擲會帶來不同的結果。第一種方式不會複製異常w,而是直接繼續丟擲;而第二種方式,會將w複製一遍,然後將新複製的異常丟擲。第二種方式會帶來兩個問題:

1,效率降低
2,傳遞丟擲的物件可能是基類物件,不符合原有目的。

// 方式1
catch (Widget w) {
	throw;
}
// 方式2
catch (Widget w) {
	throw w;
}
// 重新丟擲的可能是基類物件,不符合原有目的。
catch (Widget w) {
	WidgetBase &base = w;
	throw base;
}

4 catch效率

被丟擲的異常物件是一個臨時變數,我們不能對傳入函式中的臨時變數進行修改,因此,接受臨時變數的函式引數只能有下面第1、3種形式。

但是傳入catch中的異常可以有如下三種形式,即異常捕獲允許修改傳入的臨時變數。

1,catch (Widget w)
2,catch (Widget &w)
3,catch (const Widget &w)

如果採用第1中方式catch異常,則丟擲異常將會被複制兩次。第一次是在丟擲時,第二次catch時。因此,高效做法是採用引用的方式(第2、3種方式)catch異常。

5 異常的指標

指標也可以當作異常被接受,與上面複製類道理相同,丟擲異常時,指標將會被複制。由於離開作用域後,區域性變數會被銷燬,因此不能丟擲一個區域性變數的指標。如下面程式碼。

void func() {
	Widget error;
	static Widget right;
	throw &error; // 錯誤,離開作用域後就會被銷燬
	throw &right;  // 正確,離開作用域後不會被銷燬,複製指向right的指標
}
catch (Widget *e) { // 接受複製過來的指標
	...
}

6 型別匹配

對於虛擬函式的多型,或者函式的過載,如果有多個函式可供選擇,函式會選擇最適合的一個。如果實在找不到適合的,也會進行型別轉換。具體如下面所述

1,虛擬函式採用繼承體系的型別匹配。當前類物件擁有被呼叫函式,則使用。如果沒有,則使用基類的被呼叫函式。
2,使用函式過載,找到引數型別最匹配的函式。若找不到,則進行型別轉換,如int轉換成double型別。

但是,catch與函式呼叫不同。主要有:

1,catch不會進行基本型別轉換,如int轉換成double型別。其型別轉換僅侷限於繼承體系之間的轉換。如子類可以轉換成基類。
2,所有有型指標都是五型指標的子類。即任何指標都可以轉換成void*。
3,catch傳入引數型別遵循優先原則,而非最佳原則。當基類的catch比子類的catch出現早時,則基類catch捕獲異常。

上面條款對應如下

class BaseClass {
}
class DerivedClass : public BaseClass {
}
void func() {
	static DerivedClass derived;
	throw &derived;
}
catch (void *e) { //優先捕獲,因此void*是所有型別的基類指標
}
catch (BaseClass *e) {
} 
catch (DerivedClass *e) {
}