本文翻譯自
相比錯誤**,異常為錯誤處理提供了很多便利。這些好處包括:
儘管有這些好處,然而大部分人仍然介懷異常的額外開銷而不願意使用。基於異常的實現機制,額外開銷來自兩方面:時間開銷(增加執行時間)和空間開銷(增加可執行檔案和記憶體消耗)。在這二者之中,時間開銷更被關注。然而,對於乙個良好的c++異常實現,除非真的有異常被丟擲,在正常執行時並不會引入執行時間開銷[2]。c++異常帶來的真正問題並不是執行效能,而是如何正確的使用異常。下面是一些對正確使用c++異常非常有用的事實。
考慮下面**的情況
try
catch(std::exception & ex)
如果myclass1:: dosomework()
方法丟擲了異常,在**執行離開try **塊之前,因為obj1
是乙個正常構造了的物件,obj1
的析構函式需要被呼叫。那麼請試想一下,如果在myclass1的析構函式裡面也丟擲了異常會發生什麼?這個異常丟擲時,有乙個異常正處於active狀態。如果異常在丟擲時,有另外乙個異常處於active狀態,c++的行為是呼叫terminate()
方法,這個方法的作用是終止當前應用程式。因此要想避免兩個異常同時處於active狀態,析構函式一定不能丟擲異常。
當乙個異常被丟擲時,鑑於原始的異常物件在堆疊回滾(stack unwinding)時會被析構掉,這個異常總是需要被重新拷貝乙份。因此這個物件的拷貝建構函式一定會被呼叫。如果我們程式設計時沒有寫拷貝建構函式,那麼c++會為我們提供乙個預設的拷貝建構函式。但是會出現一些情況,預設的拷貝建構函式無法正常工作;尤其時當類成員時指標的時候。使用這樣的物件作為異常物件時,一定要確保我們提供了正確的拷貝建構函式。現在,重點來了,c++裡面物件拷貝所使用的拷貝建構函式是基於靜態型別,而不是動態型別。考慮下面的**:
class widget ;
class specialwidget: public widget ;
void passandthrowwidget()
在上面的**中,throw
語句丟擲了乙個widget
型別的物件,rw
的靜態型別是widget
。這可能並不是我們想要的執行行為。
catch語句可以有三種方式來捕獲異常:
以值(by value)捕獲
以指標捕獲
以引用捕獲
以值捕獲成本高昂,且會遭遇到分片(slicing)問題。成本高昂是因為這種方式每次都需要建立兩個異常物件。因為堆疊回滾時,這個異常的原始物件可能因為超出作用域而被析構,因此當乙個異常丟擲時,無論這個異常是否**獲,都需要建立這個異常的乙個拷貝。如果這個異常是按值(以值)捕獲,就需要建立另外乙個拷貝以便傳給catch語句。因此,如果異常時按值捕獲,會有兩個異常物件被建立,導致異常處理過程變慢。
分片問題來自於這樣的場景,當乙個子類物件被throw丟擲,但catch語句的宣告卻是父類型別。在這種情況下,catch語句只會收到父類的拷貝,顯然這樣丟失了原始異常物件的屬性。因此實際使用中,一定要避免按值捕獲異常。
如果是按指標來捕獲異常,**會像下面這樣:
void dosomething()
catch (exception* ex)
}
為了以指標方式捕獲異常,丟擲時就應該以指標丟擲,而且丟擲異常的地方必須保證異常物件在堆疊回滾後仍然可用。儘管仍然會建立異常物件的副本,但這時建立的副本是指標。因而必須有其他手段來保證異常物件在丟擲後的可用。這點是可以做到的,可以把指標指向全域性或者靜態物件,或者把異常物件建立在堆裡。
然而,異常的捕獲者對於異常物件是如何建立的缺毫無主意,因此他也無法確定是否應該delete掉收到的異常物件指標。所以按指標捕獲異常是欠妥的做法。此外,所有從標準函式丟擲的異常都是物件,而不是指標。
按引用捕獲異常不會有上面』按指標』或者『按值』捕獲帶來的任何問題。使用者不需要擔心如何delete捕獲的異常物件。而且因為傳遞的是原始異常物件的引用,也不會有額外的異常物件被複製。
除此之外,按引用傳遞不會出現分片問題(slicing problem)。因此正確且高效的異常捕獲方法是按引用。
考慮下面的**
void somefunction()
在這個方法中,new操作建立了乙個******object
物件,然後******object::dosomework()
做了其他的工作,最終銷毀這個物件。但是如果object::dosomework()
丟擲了異常,會發生什麼呢?在這個場景,我們沒有機會去deletepobj
。這會導致記憶體洩漏。這只是乙個簡單的示例來展示異常可能導致的資源洩露,當然這個例子可以通過使用try catch語句來消除資源洩露。但是在實際條件下這種情況還是會在**的各個點發生而且很難被一眼發現。這種情況的乙個補救措施是使用標準庫的自動指標(std::auto_ptr
)[1]。
考慮下面的示例**[4]:
template class stack
;template void stack::push(t element)
v[top] = element;
}
如果"out of memory"異常被丟擲,stack::push()
方法會把stack
物件留在乙個不一致狀態,因為stack的top已經被增加了,但是卻沒有push進去任何元素。當然,這個**可以修改避免著各種情況發生。在丟擲異常依然要特別留意,保證處於正確狀態的物件在丟擲異常後仍然是正確狀態。
進一步地,這種情況經常伴隨互斥量和鎖發生。在下面這個幼稚的threadsafequeue::pushback()
方法實現中,如果dopushback()
方法丟擲異常,_mutex
將會保持被鎖的狀態,讓threadsafequeue
物件處於不一致狀態。要克服這種場景,可以使用lock_guards
,就像用自動指標避免記憶體洩漏一樣原理。需要注意的是lock_guard
只在c++11後的標準庫里才有。然而你可以很容易實現乙個lock_guard
類。
template void threadsafequeue::pushback(t element)
如果乙個方法丟擲了乙個其異常規約沒有列出的異常,這個錯誤會在執行時被檢測到,乙個特殊函式unexcepted()
會被呼叫。這個函式的預設行為是呼叫terminate()
,而terminate()
的預設行為是呼叫abort()
。所以乙個程式違反異常規約的預設行為後果就是終止執行。考慮下面的**:
void f1(); // might throw anything
void f2() throw(int); //throws only int
void f2() throw(int)
當需要把不支援異常規約的舊**和新**一起整合時,這種場景下上面的**是合法的。但是如果f1()
丟擲一些除int
型別外的異常時程式就會終止執行,因為f2()
不允許丟擲int
外的型別。在這種情況想要恢復著實會有點難度,但仍然是有方法可以做到。參考1:第14條詳細介紹了這個問題的補救措施。
有兩種方法可以把乙個捕獲的異常傳遞給呼叫方,考慮下面的兩個**塊:
catch (widget& w) // catch widget exceptions
catch (widget& w) // catch widget exceptions
這兩個**塊的唯一區別就是第乙個丟擲當前異常,而第二個**塊丟擲了當前異常的乙個拷貝。第二種情況有2個問題。乙個是拷貝操作帶來的效能成本,另外就是分片(slicing)問題。如果異常物件是widget
的子類,那麼異常物件只有widget
部分被rethrow,這是因為拷貝操作是基於編譯時靜態型別進行的。
當異常被丟擲時,catch語句按照他們出現在**中的次序被匹配。在catch語句裡,乙個異常物件的型別也會匹配到他的父型別,因為子型別是父型別的子集。所以,當乙個子類物件被丟擲時,如果父類catch語句先出現在**裡,這個語句就會被執行。而不再管後面還有針對子型別的catch語句。
[1] more effective c++, by scott meyers, 1996.
[2] when and how to use exceptions, by herb sutter, 2004 (
[3] technical report on c++ performance, 2005 (
[4] exception handling: a false sense of security, by tom cargill (
C 異常處理須知
帖子內容 第一部分 1.異常發生時,異常物件會沿函式呼叫棧的反方向丟擲,這個過程常稱為棧展開 堆疊解退 2.在棧展開過程中,如果異常物件始終都沒遇到可行的 catch 處理塊,系統將呼叫 terminate 函式強制終止程式。當然如果連 try 塊都沒有,系統將直接呼叫 terminate 函式。3...
C 異常處理須知
第一部分 1.異常發生時,異常物件會沿函式呼叫棧的反方向丟擲,這個過程常稱為棧展開。2.在棧展開過程中,如果異常物件始終都沒遇到可行的catch處理塊,系統將呼叫terminate函式強制終止程式。當然如果連try塊都沒有,系統將直接呼叫terminate函式。3.在棧展開過程中,編譯器保證適當的撤...
C 異常處理須知
帖子內容 第一部分 1.異常發生時,異常物件會沿函式呼叫棧的反方向丟擲,這個過程常稱為棧展開 堆疊解退 2.在棧展開過程中,如果異常物件始終都沒遇到可行的 catch 處理塊,系統將呼叫 terminate 函式強制終止程式。當然如果連 try 塊都沒有,系統將直接呼叫 terminate 函式。3...