More effective C++ 條款12 (轉)

worldblog發表於2007-12-09
More effective C++ 條款12 (轉)[@more@]

條款12:理解“丟擲一個異常”與“傳遞一個引數”或“一個虛”間的差異:namespace prefix = o ns = "urn:schemas--com::office" />

 

從語法上看,在函式里宣告引數與在catch子句中宣告引數幾乎沒有什麼差別:

class Widget { ... };  //一個類,具體是什麼類


  // 在這裡並不重要


void f1(Widget w);  // 一些函式,其引數分別為


void f2(Widget& w);   // Widget, Widget&,或


void f3(const Widget& w);  // Widget* 型別


void f4(Widget *pw);


void f5(const Widget *pw);


catch (Widget w) ...  //一些catch 子句,用來


catch (Widget& w)  ...  //捕獲異常,異常的型別為


catch (const Widget& w) ...  // Widget, Widget&, 或


catch (Widget *pw) ...  // Widget*


catch (const Widget *pw) ...


 

你因此可能會認為用throw丟擲一個異常到catch子句中與透過函式呼叫傳遞一個引數兩者基本相同。這裡面確有一些相同點,但是他們也存在著巨大的差異。

讓我們先從相同點談起。你傳遞函式引數與異常的途徑可以是傳值、傳遞引用或傳遞指標,這是相同的。但是當你傳遞引數和異常時,所要完成的操作過程則是完全不同的。產生這個差異的原因是:你呼叫函式時,的控制權最終還會返回到函式的呼叫處,但是當你丟擲一個異常時,控制權永遠不會回到丟擲異常的地方。

有這樣一個函式,引數型別是Widget,並丟擲一個Widget型別的異常:

// 一個函式,從流中讀值到Widget中


istream operator>>(istream& s, Widget& w);


void passAndThrowWidget()


{


  Widget localWidget;


  cin >> localWidget;  //傳遞localWidget到 operator>>


  throw localWidget;  // 丟擲localWidget異常


}


當傳遞localWidget到函式operator>>裡,不用進行複製操作,而是把operator>>內的引用型別變數w指向localWidget,任何對w的操作實際上都施加到localWidget上。這與丟擲localWidget異常有很大不同。不論透過傳值捕獲異常還是透過引用捕獲(不能透過指標捕獲這個異常,因為型別不匹配)都將進行lcalWidget的複製操作,也就說傳遞到catch子句中的是localWidget的複製。必須這麼做,因為當localWidget離開了生存空間後,其解構函式將被呼叫。如果把localWidget本身(而不是它的複製)傳遞給catch子句,這個子句接收到的只是一個被析構了的Widget,一個Widget的“屍體”。這是無法使用的。因此C++規範要求被做為異常丟擲的必須被複制。

即使被丟擲的物件不會被釋放,也會進行複製操作。例如如果passAndThrowWidget函式宣告localWidget為靜態變數(static),

void passAndThrowWidget()


{


  static Widget localWidget;  // 現在是靜態變數(static);


  //一直存在至程式結束


 


  cin >> localWidget;  // 象以前那樣執行


  throw localWidget;  // 仍將對localWidget


}  //進行複製操作


當丟擲異常時仍將複製出localWidget的一個複製。這表示即使透過引用來捕獲異常,也不能在catch塊中修改localWidget;僅僅能修改localWidget的複製。對異常物件進行強制複製複製,這個限制有助於我們理解引數傳遞與丟擲異常的第二個差異:丟擲異常執行速度比引數傳遞要慢。

當異常物件被複製時,複製操作是由物件的複製建構函式完成的。該複製建構函式是物件的靜態型別(static type)所對應類的複製建構函式,而不是物件的動態型別(dynamic type)對應類的複製建構函式。比如以下這經過少許修改的passAndThrowWidget

class Widget { ... };


class SpecialWidget: public Widget { ... };


void passAndThrowWidget()


{


  SpecialWidget localSpecialWidget;


  ...


  Widget& rw = localSpecialWidget;  // rw 引用SpecialWidget


  throw rw;  //它丟擲一個型別為Widget


  // 的異常


}


這裡丟擲的異常物件是Widget,即使rw引用的是一個SpecialWidget。因為rw的靜態型別(static type)是Widget,而不是SpecialWidget。你的根本沒有主要到rw引用的是一個SpecialWidget。編譯器所注意的是rw的靜態型別(static type)。這種行為可能與你所期待的不一樣,但是這與在其他情況下C++中複製建構函式的行為是一致的。(不過有一種技術可以讓你根據物件的動態型別dynamic type進行複製,參見條款25)

異常是其它物件的複製,這個事實影響到你如何在catch塊中再丟擲一個異常。比如下面這兩個catch塊,乍一看好像一樣:

 


catch (Widget& w)  // 捕獲Widget異常


{


  ...  // 處理異常


  throw;  // 重新丟擲異常,讓它


}  // 繼續傳遞


catch (Widget& w)  // 捕獲Widget異常


{


  ...  // 處理異常


  throw w;  // 傳遞被捕獲異常的


}   // 複製


這兩個catch塊的差別在於第一個catch塊中重新丟擲的是當前捕獲的異常,而第二個catch塊中重新丟擲的是當前捕獲異常的一個新的複製。如果忽略生成額外複製的系統開銷,這兩種方法還有差異麼?

當然有。第一個塊中重新丟擲的是當前異常(current exception),無論它是什麼型別。特別是如果這個異常開始就是做為SpecialWidget型別丟擲的,那麼第一個塊中傳遞出去的還是SpecialWidget異常,即使w的靜態型別(static type)是Widget。這是因為重新丟擲異常時沒有進行複製操作。第二個catch塊重新丟擲的是新異常,型別總是Widget,因為w的靜態型別(static type)是Widget。一般來說,你應該用

throw

來重新丟擲當前的異常,因為這樣不會改變被傳遞出去的異常型別,而且更有,因為不用生成一個新複製。

(順便說一句,異常生成的複製是一個臨時物件。正如條款19解釋的,臨時物件能讓編譯器它的生存期(optimize it out of existence),不過我想你的編譯器很難這麼做,因為程式中很少發生異常,所以編譯器廠商不會在這方面花大量的精力。)

讓我們測試一下下面這三種用來捕獲Widget異常的catch子句,異常是做為passAndThrowWidgetp丟擲的:

catch (Widget w) ...  // 透過傳值捕獲異常


catch (Widget& w) ...  // 透過傳遞引用捕獲


  // 異常


catch (const Widget& w) ...  //透過傳遞指向const的引用


  //捕獲異常


我們立刻注意到了傳遞引數與傳遞異常的另一個差異。一個被異常丟擲的物件(剛才解釋過,總是一個臨時物件)可以透過普通的引用捕獲;它不需要透過指向const物件的引用(reference-to-const)捕獲。在函式呼叫中不允許轉遞一個臨時物件到一個非const引用型別的引數裡(參見條款19),但是在異常中卻被允許。

讓我們先不管這個差異,回到異常物件複製的測試上來。我們知道當用傳值的方式傳遞函式的引數,我們製造了被傳遞物件的一個複製(參見Effective C++ 條款22),並把這個複製到函式的引數裡。同樣我們透過傳值的方式傳遞一個異常時,也是這麼做的。當我們這樣宣告一個catch子句時:

catch (Widget w) ...  // 透過傳值捕獲


會建立兩個被丟擲物件的複製,一個是所有異常都必須建立的臨時物件,第二個是把臨時物件複製進w中。同樣,當我們透過引用捕獲異常時,

catch (Widget& w) ...  // 透過引用捕獲


 


catch (const Widget& w) ...  //也透過引用捕獲


這仍舊會建立一個被丟擲物件的複製:複製是一個臨時物件。相反當我們透過引用傳遞函式引數時,沒有進行物件複製。當丟擲一個異常時,系統構造的(以後會析構掉)被丟擲物件的複製數比以相同物件做為引數傳遞給函式時構造的複製數要多一個。

我們還沒有討論透過指標丟擲異常的情況,不過透過指標丟擲異常與透過指標傳遞引數是相同的。不論哪種方法都是一個指標的複製被傳遞。你不能認為丟擲的指標是一個指向區域性物件的指標,因為當異常離開區域性變數的生存空間時,該區域性變數已經被釋放。Catch子句將獲得一個指向已經不存在的物件的指標。這種行為在設計時應該予以避免。

物件從函式的呼叫處傳遞到函式引數裡與從異常丟擲點傳遞到catch子句裡所採用的方法不同,這只是引數傳遞與異常傳遞的區別的一個方面,第二個差異是在函式呼叫者或丟擲異常者與被呼叫者或異常捕獲者之間的型別匹配的過程不同。比如在標準數學庫(the standard math library)中sqrt函式:

double sqrt(double);  // from or


我們能這樣計算一個整數的平方根,如下所示:

int i;


 


double sqrtOfi = sqrt(i);


毫無疑問,C++允許進行從int到double的隱式型別轉換,所以在sqrt的呼叫中,i 被悄悄地轉變為double型別,並且其返回值也是double。(有關隱式型別轉換的詳細討論參見條款5)一般來說,catch子句匹配異常型別時不會進行這樣的轉換。見下面的程式碼:

void f(int value)


{


  try {


  if (someFunction()) {  // 如果 someFunction()返回


  throw value;  //真,丟擲一個整形值


  ...


  }


  }


  catch (double d) {  // 只處理double型別的異常


  ... 


 }


 


  ...


 


}


在try塊中丟擲的int異常不會被處理double異常的catch子句捕獲。該子句只能捕獲真真正正為double型別的異常;不進行型別轉換。因此如果要想捕獲int異常,必須使用帶有int或int&引數的catch子句。

不過在catch子句中進行異常匹配時可以進行兩種型別轉換。第一種是繼承類與基類間的轉換。一個用來捕獲基類的catch子句也可以處理派生類型別的異常。例如在標準C++庫(STL)定義的異常類層次中的診斷部分(diagnostics portion )(參見Effective C++ 條款49)。

ectratio="t" v:ext="edit">

捕獲runtime_errors異常的Catch子句可以捕獲range_error型別和overflow_error型別的異常,可以接收根類exception異常的catch子句能捕獲其任意派生類異常。

這種派生類與基類(inheritance_based)間的異常型別轉換可以作用於數值、引用以及指標上:

catch (runtime_error) ...  // can catch errors of type


catch (runtime_error&) ...  // runtime_error,


catch (const runtime_error&) ...  // range_error, or


  // overflow_error


catch (runtime_error*) ...  // can catch errors of type


catch (const runtime_error*) ...  // runtime_error*,


  // range_error*, or


  // overflow_error*


 

第二種是允許從一個型別化指標(typed pointer)轉變成無型別指標(untyped pointer),所以帶有const void* 指標的catch子句能捕獲任何型別的指標型別異常:

catch (const void*) ...  //捕獲任何指標型別異常


 

傳遞引數和傳遞異常間最後一點差別是catch子句匹配順序總是取決於它們在程式中出現的順序。因此一個派生類異常可能被處理其基類異常的catch子句捕獲,即使同時存在有能處理該派生類異常的catch子句,與相同的try塊相對應。例如:

try {


  ...


}


catch (logic_error& ex) {  // 這個catch塊 將捕獲


  ...  // 所有的logic_error


}  // 異常, 包括它的派生類


 


catch (invalid_argument& ex) {  // 這個塊永遠不會被


  ...   //因為所有的


}  // invalid_argument


  // 異常 都被上面的


  // catch子句捕獲。


與上面這種行為相反,當你呼叫一個虛擬函式時,被呼叫的函式位於與發出函式呼叫的物件的動態型別(dynamic type)最相近的類裡。你可以這樣說虛擬函式採用最優適合法,而異常處理採用的是最先適合法。如果一個處理派生類異常的catch子句位於處理基類異常的catch子句前面,編譯器會發出警告。(因為這樣的程式碼在C++裡通常是不合法的。)不過你最好做好預先防範:不要把處理基類異常的catch子句放在處理派生類異常的catch子句的前面。象上面那個例子,應該這樣去寫:

try {


  ...


}


catch (invalid_argument& ex) {  // 處理 invalid_argument


  ...  //異常


}


catch (logic_error& ex) {  // 處理所有其它的


  ...  // logic_errors異常


}


綜上所述,把一個物件傳遞給函式或一個物件呼叫虛擬函式與把一個物件做為異常丟擲,這之間有三個主要區別。第一、異常物件在傳遞時總被進行複製;當透過傳值方式捕獲時,異常物件被複製了兩次。物件做為引數傳遞給函式時不需要被複製。第二、物件做為異常被丟擲與做為引數傳遞給函式相比,前者型別轉換比後者要少(前者只有兩種轉換形式)。最後一點,catch子句進行異常型別匹配的順序是它們在中出現的順序,第一個型別匹配成功的catch將被用來執行。當一個物件呼叫一個虛擬函式時,被選擇的函式位於與物件型別匹配最佳的類裡,即使該類不是在原始碼的最前頭。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990475/,如需轉載,請註明出處,否則將追究法律責任。

相關文章