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 主要有兩項缺點:
- 效率不佳。這邊的效率不僅僅是「compiler 為了這個功能而幫你產生額外的程式碼」,還包含「你為了達成 exception safety 而犧牲的效能」。在 Exceptional C++ 一書中提到,若你的 class 包含其它物件,而你想寫一個具有強烈保證1的 exception safe copy assignment,多半必須依賴「產生暫時物件」加上「nothrow swapping」的手法來達成。然而「產生暫時物件」顯然會犧牲效率,nothrow swapping 則要求所包含的其它物件配置在 heap,同樣也是犧牲效率。另外,丟出 exception 讓上層函式接受,通常會比直接回傳 error code 慢許多,因為 compiler 多半假設 exception 並不常發生,自然也不太會在這方面尋求最佳化。
- Exception 需要在軟體設計之初就被納入設計考量內,我想這也是 Google 不採用 exception 的原因。已寫好、未使用 exception 的程式碼,若想改用 exception 來處理錯誤,往往會打破原本的設計架構。而這樣的程式碼勢必也無法搭配其它有使用 exception 的程式碼互相合作。
參考資料
這篇 Exception Handling 新思維 對於程式設計師如何面對 exception 有非常棒的禪述:
不願面對 exception 的 programmer 或許應該避免使用 C++,轉而擁抱其他的 programming language。
面對 exception, programmer 的 mind set 要改變. 把心態從:
- 只有某些操作會 throw
進化到:
- 只有某些操作不會 throw
Post preview:
Close preview