7.2 異常處理(Exception Handing)
Exception Handing快速檢閱
C++的exception handing由三個主要語彙元件構成:
- 一個throw子句。它在程式某處發出一個exception,被丟擲去exception可能是內建型別,也可是自定義型別。
- 一個或多個catch。每個catch子句都是一個exception handler,它用來表示這個子句準備處理某種型別的exception,並且在封閉的大括號內提供實際的處理程式。
- 一個try語句。它被圍繞以一系列的敘述句(statements),這些敘述句可能引發catch子句起作用。
當一個exception被丟擲,控制權會從函式呼叫中被釋放出來,並尋找一個吻合的catch子句。如果沒有吻合者,那麼預設的處理例程terminate()會被呼叫。當控制權被放棄後,堆疊中的每一個函式呼叫也被推離(popped up)。這個程式稱為unwinding the stack。每一個函式被推離堆疊之前,函式的local class objects的destructor會被呼叫。
Exception handing中比較不那麼直覺的就是它對於那些看似沒什麼事做的函式所帶來的衝擊:
Point* mumble()
{
Point*pt1,pt2;
pt1 = foo();
if(!pt1)
return 0;
Point p;
pt2 = foo();
if(!pt2)
return pt1;
...
}
如果有一個exception第一次呼叫foo()時被丟擲,那麼這個mumble()函式會被推出程式堆疊。由於呼叫foo()的操作並不在一個try語句內,也就不需要和一個catch子句吻合。這裡沒有任何的local class object需要析構。當第二次呼叫foo()時候丟擲異常,exception handing機制就必須在推出程式堆疊前,先呼叫p的destructor。
在exception handing之下,上述兩次呼叫foo()函式被視為兩塊語意不同的區域。因為當exception被丟擲來的時候,這兩個區域有不同的執行語意。欲支援exception handing,需要額外的一些“簿記”操作與資料。編譯器的做法有兩種:一種是把兩塊區域以個別的“將被摧毀之local objects”連結串列聯合起來;另一種做法就是讓兩塊區域共享一個連結串列,該連結串列會在執行期擴大或縮小。
在程式設計師層面,exception handing也改變了函式在資源管理的語意。例如下嗎的函式對一塊共享記憶體的locking和unlocking:
void mumble(void* arena) { Point* p = new Point; smLock(arena); //如果異常觸發 //... smUnLock(arena); delete p; }
如果上述語句異常被觸發,函式沒有unlock記憶體和delete p。
最直接的做法就是加上異常機制,或者使用RAII技術(將資源封裝成class)。
使用RAII技術,會使那些擁有member class subobject或base class subobject(並且它們也都有constructors)的classes的constructor 更加複雜。一個class如果被部分析構,其destructor必須施行於那些已經被構造的subobjects和member objectssh身上。
例如,一個class X有member object A‘,B,C;都各有一對constructor和destructor。如果A的constructor丟擲exception,不論A,B,C都不需要呼叫destructor。如果B的constructor丟擲異常,A的destructor必須被呼叫,但C不用。處理所有意外事故,是編譯器的責任。
同樣道理:
//class Point3d: public Point2d{...};
Point3d* cvs = new Point3d[512];
如上程式碼,會發生兩件事:
- 從heap中分配512個Point3d object所用的記憶體。
- 如果成功,先是Point2d constructor,然後是Point3d constructor,會被施行於每一個元素身上。
如果第27個元素的Point3dconstructor丟擲異常,對於第27個元素,只有Point2d destructor需要呼叫。對於前26個元素,Point3d destructor和Point2d destructor都需要被呼叫。然後記憶體被釋放回去。
對Exception Handing支援
對於一個exception發生時,編譯器系統必須完成以下的事情:
- 檢驗發生throw操作的函式。
- 決定throw操作是否發生在try區段中。
- 若是,編譯器必須把exception type拿來和每一個catch子句進行比較。
- 如果比較吻合,流程控制交到catch子句手中。
- 如果throw傳送並不在try區段中,或沒有一個子句吻合,那麼系統必須摧毀所有的active local objects;並從堆疊中將目前的函式“unwind”掉;再進入到程式堆疊的下一個函式中區,然後重複2~5。
決定throw是否發生在一個try區段中
一個函式可以被想象成好幾個區域:
- try區段以外的區域,而且沒有active local objects。
- try區段以外的區域,但有一個及以上的active local objects需要析構。
- rty以內的區域。
編譯器必須標識出以上各區域,並使它們對執行期的exception handling系統有所作用。
將exception的型別和每一個catch子句的型別做比較
對於每一個被丟擲來的exception,編譯器必須產生一個型別描述器,對exception的型別進行編碼。如果那是一個derived type,編碼內容必須包括其所有base class的型別資訊。只編進public base class的型別是不夠的,因為這個exception可能被一個member function不再,而在一個member function的範圍之中,derived class和nonpublic base class之間轉換。
型別描述器(type descriptor)是必要的,因為真正的exception是在執行期被處理的,其object必須有自己的型別。
編譯器還必須為每一個catch子句產生一個型別描述器,執行期的exception handler會將“被丟擲的object型別描述器”和“每一個catch子句的描述器”進行比較,直到找到吻合的一個,或者直到堆疊被“unwound”而terminate()呼叫。
每一個函式會產生一個exception表格,它描述與函式相關的各區域、任何必要的善後處理程式碼以及catch子句的位置。
當一個實際物件在程式執行時被丟擲,會發生什麼?
當一個exception被丟擲時,exception object會被產生出來並通常放置在相同形式的exception資料堆疊中。從throw端傳遞給catch子句的,是exception object的地址、型別描述器(或是一個函式指標,該函式會傳回於該exception type有關的型別描述器物件)以及可能會有的exception object描述器(如果有人定義它的話)。
考慮如下的程式碼:
catch(exPoint p)
{
//do something
throw;
}
以及一個exception object,型別是exVertex,派生自exPoint。這兩種型別都吻合,於是catch子句會起作用,那麼p會發生什麼事:
- p將以exception object作為初值,就像一個函式引數一樣。如果有定義(或編譯器合成)一個copy constructor和一個destructor的話,它將會施行於local copy身上。
- 由於p是一個object而不是一個reference,其內容被拷貝的時候,exception object的non-exPoint部分切割呼叫。此外,如果為了exception的繼承而提供virtual function,那麼p的vptr會被設定成exPoint的vtbl;exception object的vptr不會被拷貝。
當這個exception 再次丟擲,p是拷貝的一個臨時物件,並意味著喪失了原來的exception的exVertex部分。
如果exception object是引用,這是真正的型別。任何對此object的改變都是會被繁殖到下一個catch子句。
對於全域性性的exception object,任何在throw中丟擲的是一個被複製品,意味著一個catch子句對於exception object的任何改變都是區域性性的,不會影響全域性性的object。只有在一個catch子句評估完畢並且知道它不會再丟擲exception之後,真正的exception object才會被摧毀。
於其他語言比起來,C++編譯器支援EH機制所付出的代價最大。某種程度上是由於其執行期的天性以及對底層硬體的依賴,以及UNIX和PC兩種平臺對於執行速度和程式大小有著不同的取捨優先狀態之故。