2009年5月11日 星期一

淺談C++例外處理 (前篇)

例外處理是一種程式語言的機制,他是用來偵測和處理一些執行時期的錯誤或異常狀況(Runtime error/exception) ,這也意味著如果程式語言想內建這個機制的話,就必須要有執行環境的支援,直覺上也代表著那段程式碼將會跑得比較慢。然而,在某大部份的狀況,執行時期的異常安全(exception safety)是非常重要的,付出那些代價相對來說是非常值得的。對一般的使用者而言,出現Segmentation fault或跳出一個記憶體無法存取的視窗都不太能被接受了,更不用說那些需要高度安全的程式,就好比汽車的剎車系統或核電廠的控制程式,出現執行時期的異常是需要付出慘痛的代價的。

例外處理機制"內建"在程式語言當中最早是從IBM的PL/I( I是羅馬數字的1 , 1964 Appeared, 1976 ANSI, 1979 ISO) 開始的,後來也有一些物件導向的語言開始跟進,在某些作業系統中也有API來讓某些語言(比如說慣C最喜歡的C)能有例外處理的支援,也有某些語言使用了一些語法甜頭來加強他的功能性(如C#的using block),甚至在新興的腳本語言中都可以看到他們的蹤跡。然而,程式語言內建的例外處理機制其實還蠻多人討厭的,慣C的人也覺得這個機制不太直覺。猶有甚者,認為例外處理機制根本就是無形的goto語法,不僅讓工程師們永遠不知道他們是從哪裡開始變換程式流程的,而且他們覺得使用例外處理機制並不會讓問題變得更簡單,更不用說會破壞程式的結構性了。

程式語言的例外處理機制之所以會引發很多的爭議是在於某些例外處理的情況是可以靠check return code來早期發現的,慣C的人(應該)也已經很習慣使用這種例外處理的機制了(不過有時候會夾帶很多macro),而程式語言所提供的例外處理機制要一般化且具有通透性,在黑箱作業的結果就是很多狀況是會破壞程式的控制流程,畢竟程式如何從例外發生點跑到catch block的細節已由編譯器代為處理了。加上為了能在很多特殊的狀況中都能使用,所以他們的實現會比較困難,而且效能也不是相當好。不過程式語言的例外處理是靠執行環境來支援的,這意味著只有他們可以成為最後一道防線,這是任何編譯時期的機制無法辦到的。

例外處理的語法在程式語言當中算是複雜的,也是比較少被使用的,乃至於少數工程師不是很有辦法完全駕馭,即使是有語法甜頭的支援,有時候也會有一些迷失。那讓我們回到主題來吧,C++的例外處理機制說複雜其實也蠻複雜的,其中一個複雜點在C++可以把物件放在自動變數內。自動變數的一個特性就是可以自動進行建構和解構。由於C++標準明確地定義了程式的control flow從throw(異常發生點) 到 exception handler 被執行前是必須解構區塊內所有已建構成功的自動變數。標準講的很簡單,但這其中卻隱藏了許多的陷阱,我們就來看個例子吧。

class testthrow {
public:
    testthrow() {
       test tx;
        cout<<"testthrow constructor"<<endl;
        throw;
    }
    ~testthrow(){cout<<"testthrow deconstructor"<<endl;}
};

int main() {
    try { testthrow tt; } catch(…) {}
    return 0;
}

這段程式碼大部分的人會覺得tx必須被解構(因為他已經被建構了),很可惜是不會的,即便main中使用try catch的區塊並攔截所有例外。原因出現在 rethrow 的語法當中,C++標準規定了throw保留字如沒有參數,那所表達的語意就是進行 rethrow 。然而,C++標準又規定了在沒有發生 exception 的狀況使用rethrow的話,程式是會被終結的。接下來我們來看第二個例子:

int main() {
    someclass c;
    throw ( std::runtime_error("error!!") );
}

在以上的程式碼中,假設c會建構成功的話,那c會被解構嗎?答案是不一定,基本上這段程式碼在C++的標準當中是沒有定義的。C++標準提到了自動變數要解構時機是在stack unwinding 的時候。然而,C++標準又提到,如果在程式裡沒有定義 exception handler 那無論有沒有做stack unwinding 程式都會被終結,依現行的C++編譯器 VC++ 和g++ 的 libstdc++v3 的實作,c是不會被解構的。

沒有留言:

張貼留言