關於 C++ Exception

1244456477|%Y-%m-%d|agohover

這是為了回應 Ptt C_and_CPP 討論板上一篇關於是否應使用 exception 的問題而發表的文章,算是就目前自己了解 exception 的程度做一個整理。原問題如下:

作者 os653 (allstar)
標題 [問題] 例外處理
時間 Sat May 30 18:33:43 2009


想請問,一般而言,比較大的程式都用什麼方法處理錯誤呢?
之前曾用 winsock 寫過下載網頁的程式
當時處理的方法是,所有錯誤一概印出相關資料後 exit
結果就是,如果沒有連接上網路,程式就完全不能執行
想想好像不太對的樣子,雖然自己用是沒啥差啦 …
其他常見的方法好像就 return error code 跟 throw exception
可是這兩種方法的問題也實在蠻多的
所以想請有經驗的人解說一下,一般大程式的處理方式是什麼
我個人是傾向 error code 多一些啦,畢竟 exception 很難寫
附帶請教一下,有沒有網站可以查詢 STL 可能丟出的例外?
我找到 http://www.dinkumware.com/manuals/default.aspx
不過他只有寫出哪些 function 保證不丟出例外
還是說,實際上必須要將所有 functino 都當成會丟出例外來處理?


只要注意到現代化的 OOP 語言(C++、Java、C#、Python、Ruby 等),幾乎都提供 exception 的支援,大概就可以了解 exception 相對於 error code 具有相當大的優勢。Exception 最重要的優點在於你可以分離「正常程序」與「錯誤處理程序」,讓你的程式碼變得清楚易懂。

這是一段實際存在的程式碼,來源是一個稱為 Minibase 的教學用資料庫系統。他們使用 error code 來作為錯誤處理的方式:

while( ((SortedPage*)cur_page)->get_type() != LEAF ){
    ((BTIndexPage*)cur_page)->get_page_no(key, header.keytype, next_id);
    st = MINIBASE_BM->unpinPage(cur_id); // 正常程序
    if(st != OK)
        return MINIBASE_CHAIN_ERROR(BTREE, st); // 錯誤處理
 
    st = MINIBASE_BM->pinPage(next_id, cur_page); // 正常程序
    if(st != OK)
        return MINIBASE_CHAIN_ERROR(BTREE, st); // 錯誤處理
 
    cur_id = next_id;
}

你會發現,只要你呼叫了任何會回傳 error code 的函式,你都必須在其後馬上檢查 error code。最糟糕的是,大部份的情況下,你就算發現有 error 發生,但底層函式並不知道該如何處理,只能把錯誤往上層丟,使得你必須浪費大量的時間去寫重覆的 if(st != OK) return … 。只要稍微翻一下整個 Minibase 的原始碼,就可以體會到一行正常程序拌隨兩行錯誤處理程序的寫法,嚴重影響整體的可讀性。有時甚至可以看到程式設計師偷懶的地方:

if ((st = MINIBASE_BM->pinPage(curPageNo, curPage)) != OK)
    assert(0); // 這行很慘,因為 release mode 不會有 assertion error!

這種大量而重覆工作,就應該交給 compiler 來完成。Exception 的好處就在此:當你的函式不知道怎麼處理錯誤,就不需要寫錯誤處理的程式碼,exception 發生時,會自動把控制權一層層往上傳遞,直到它被抓住 (catch) 為止。

使用 RAII 避免資源遺失

有板友提到因為 C++ 沒有 GC,因此使用 exception 容易產生 memory leak 的問題,其實 exception 可能造成的危害不只如此,因為會遺失的「資源」並不只有記憶體。比如說已建立的網路連線、mutex、檔案寫入鎖定等等,都可能因為 exception 的發生,而導致這些資源持續被占用。GC 只管記憶體,因此無法釋放這類資源。大量使用 exception 作為錯誤處理的 Java 雖然有 GC,也同樣會發生這類資源遺失的問題。

事實上,不管你如何處理錯誤,只要你要求「錯誤發生時,應該馬上中斷並回傳到上一層」,那麼就必需面對資源遺失的問題,即使你使用 error code 亦同。唯一的不同點在於使用 error code 時,你明確地使用 return 來離開函式,因此你比較容易確定發生錯誤時的流程,至於 exception 就沒那麼簡單了。

為了確保你的資源不會遺失,一般來說會使用 RAII 的手法:產生一個物件,在建構式中取得資源,並在解構式中釋放資源。比如說你原本的程式如下:

void foo(FILE* f)
{
    flock(fileno(f), LOCK_EX); // lock the file
    do_something(f);
    flock(fileno(f), LOCK_UN); // unlock the file
}

這段程式碼並非 exception safe,因為 do_something() 產生 exception 時,其後的 flock() 操作並不會被執行,因此該檔案會保持在被鎖定的狀態。RAII 的方法如下:

class FileLocker {
    public:
        explict FileLocker(FILE* f) : fd(fileno(f))
        {
            flock(fd, LOCK_EX);
        }
        ~FileLocker()
        {
            flock(fd, LOCK_UN);
        }
 
    private:
        FileLocker(const FileLocker&);      // prevent copy constructor
        FileLocker& operator=(const FileLocker&); // and copy assignment
        int fd;
};
 
void foo(FILE* f)
{
    FileLocker lock(f); // lock the file
    do_something(f);
    // The dtor of FileLocker will unlock the file automatically
}

不管 do_something() 有沒有產生 exception,FileLocker 物件在脫離 foo() 函式時都會進行解構,進而確保檔案會解鎖。而這也是我很鼓勵大家多用 vector,少用 new 來達成動態陣列的原因:

void foo(int n)
{
    int* a = new int[n];
    do_something(a);   // FIXME: do_something(a) may throw exceptions!
    delete [] a;
}
 
void bar(int n)
{
    vector<int> a(n);
    do_something(a);   // a will be released even if exceptions are thrown
}

Exception 的缺點

說了這麼多,再來說說 exception 的缺點吧。我個人覺得 exception 主要有兩項缺點:

  1. 效率不佳。這邊的效率不僅僅是「compiler 為了這個功能而幫你產生額外的程式碼」,還包含「你為了達成 exception safety 而犧牲的效能」。在 Exceptional C++ 一書中提到,若你的 class 包含其它物件,而你想寫一個具有強烈保證1的 exception safe copy assignment,多半必須依賴「產生暫時物件」加上「nothrow swapping」的手法來達成。然而「產生暫時物件」顯然會犧牲效率,nothrow swapping 則要求所包含的其它物件配置在 heap,同樣也是犧牲效率。另外,丟出 exception 讓上層函式接受,通常會比直接回傳 error code 慢許多,因為 compiler 多半假設 exception 並不常發生,自然也不太會在這方面尋求最佳化。
  2. Exception 需要在軟體設計之初就被納入設計考量內,我想這也是 Google 不採用 exception 的原因。已寫好、未使用 exception 的程式碼,若想改用 exception 來處理錯誤,往往會打破原本的設計架構。而這樣的程式碼勢必也無法搭配其它有使用 exception 的程式碼互相合作。

參考資料

這篇 Exception Handling 新思維 對於程式設計師如何面對 exception 有非常棒的禪述:

不願面對 exception 的 programmer 或許應該避免使用 C++,轉而擁抱其他的 programming language。

面對 exception, programmer 的 mind set 要改變. 把心態從:

  • 只有某些操作會 throw

進化到:

  • 只有某些操作不會 throw

Comments

Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +
c++
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License