標準C++中只有明文的throw才是唯一發出例外的管道,像存取違規、除以零之類的錯誤並不會丟出任何例外,如果沒有在操作前偵知,則會導致程式直接發生錯誤而關閉。

在Visual C++當中,除了標準的C++語言層級例外處理,還提供了SEH(Structured Exception Handling)。SEH的功能在於,將系統層級發生的錯誤塞回給應用程式,使得程式可以直接catch這些系統層級的錯誤,加以處理。這一類的例外又稱為「非同步例外」。

這篇文章並不打算介紹SEH的功能和用法,這些在網路上已經汗牛充棟了。這裡要說的是,在VC++ 8.0(即VS 2005)之前,用catch捕捉SEH都有可能會造成stack無法正確釋放。

#include <iostream>
#include <cstdlib>

using namespace std;

struct Trace {
    Trace()  {cout << "ctor. \n";}
    ~Trace() {cout << "dtor. \n" }
};

int main() {
    try {
        Trace trace;
        char* ptr = 0;
        *ptr = 0;
    } catch (...) {
        cout << "caught. \n";
    }
    // system("pause");
}


若採用正常的C++例外處理方式,螢幕上只會印出一行「ctor.」,然後程式執行到 *ptr = 0 時會因為存取違規而直接當掉,正常的C++可是完全不處理這類的玩意。這和VC 8使用 /EHsc 編譯選項結果是一致的。

若採用SEH,照理來說會先建構trace物件,由於ptr發生存取違規會被當作exception丟回程式,所以try內的區域變數進行解構,之後進入catch區塊。螢幕上依序會印出「ctor.」、「dtor.」、「caught.」。這與VC 8使用 /Eha 編譯選項結果是一致的。

但在VC6就不是這麼一回事,只要開啟例外處理 /Gx 選項,螢幕上就只會印出「ctor.」、「caught.」。這表示存取違規轉換成的例外確實被捕捉到了,但是區域變數完全沒有被正確解構。

再看以下範例:

#include <iostream>
#include <cstdlib>

using namespace std;

struct Trace {
    Trace()  {cout << "ctor. \n";}
    ~Trace() {cout << "dtor. \n" }
};

// RAII resource holder
class TraceHolder {
public:
    TraceHolder(Trace* p) : ptr_(p) {}
    ~TraceHolder()          {delete ptr_;}
private:
    Trace* ptr_;
};

void Fun() {
    TraceHolder th = new Trace;
    char* ptr = 0;
    *ptr = 0;
}

int main() {
    try {
        Fun();
    } catch (...) {
        cout << "caught. \n";
    }
    // system("pause");
}


在VC6當中使用 /Gx 編譯這段程式碼,雖然存取違規會被catch,但是Trace物件的解構式根本不會被執行到。

我想對C++ 稍微敏感的人都會立即察覺到,這代表就算用了RAII手法照樣會memory leak。如果還沒察覺問題的嚴重性,這裡再說清楚一點:在VC6只要啟動例外處理/Gx,例外就有可能在任何地方發出,不論你有沒有打算throw,而且很可會強迫你遺失一些看似安全的資源。

網路上有一些文章使用__set_se_translator()指定SE的轉換函數,然後重新丟出標準的C++ exception。不過這招在VC6上面一點用也沒有,上述的情形照樣發生。有時候調整程式碼流程會使得stack被正確釋放,不過只要一開啟optimize又失敗了。

(同樣的情形據說也存在VC 7.1,即VS 2003對應的VC版本,但是要/Eha和optimize同時啟用才會重現。我手邊沒有VC 2003,看有沒有網友願意試試。)

結論
例外處理最終的目的還是希望程式能繼續運行,不要因為一些算不上錯誤的因素提早領便當,即使不得不下台也得優雅的結束,因此會resource leak的例外處理稱不上好的例外處理。直接垮掉由OS接手收拾殘局也就罷了,但若是resource leak後程式仍然繼續運作就很糟糕,一些長時間掛著的軟體可能就會因此耗盡系統資源。

若正確性優先的話,在VC2005上最好的處理方式應該是/Eha + __set_se_translator()。這麼做會喪失些許的效能,因為非同步例外必須跑完一次SEH的路徑,然後再跑一次標準C++ exception路徑。但這是值得的,在某些標準C++會直接陣亡的情境下,此法有機會讓程式優雅結束並且回報錯誤發生的原因和位置,使得debug更有效率。

甚至在某些極端的情境下,例如當別人寫的程式庫發生存取違規,在無法更動其原始碼的情況,至少有機會靠SEH使程式免於垮掉而繼續運作。

至於VC6的話,只得完全放棄相容性,改採非標準的__try、__except,不然只能說沒藥可救了。(雖然理論上VC6快被淘汰,但還是很多人用.....)

 

novus 發表在 痞客邦 PIXNET 留言(0) 人氣()