Posted on July 6th, 2008 at 23:57 by fr3@K
Quote from Google C++ Style Guide:
在極端狀況下, 我能夠想像在 no virtual function 或是 no templates 但仍能使用 STL 的環境寫 C++. 但完全禁止 user-defined classes/functions 以拋出 exception 的方式回報錯誤的環境呢? No, thanks. 即便有免費的食物與漂亮的辦公室, 這還是太折磨人了.
除了
vector 與 basic_string 因為特殊考量 (主要是 performance) 只提供了 basic guarantee (e.g. no leak), 其他的 STL 的 container 與部份特別的 STL algorithm (如 uninitialized_copy), 基於 exception/RAII 建構出 commit or rollback 的 strong guarantee. 而在一個有高可靠需求的系統中, 如 DB, 狀態的一致性是不可或缺的特性. 為何不有效利用 C++ 語言與標準 library 所鼓勵的 commit or rollback 的 coding style?
在一個 non-trivial (具規模) 的軟體 code base, 在深不見底的 code stack 裏, 偵測到錯誤的 code 與知道該怎麼 recover 該錯誤或 pop-up 錯誤訊息給使用者的實質 handler 可能相距十萬八千里. 讓每一層都以 if/else/return 方式向上層回報是連 不支持 exception 的 C 標準函式庫都儘量避免的事情. 況且, 除了向上回報, 絕大部份中間的 code 根本不知道該拿這個錯誤如何是好.
禁止了 exception, RAII 當然也跟著沒了. 而少了 RAII 的 code 看起來可能就會像這樣:
int StringLength(const String& str)
{
if(str.IsInitOk() == false)
return ERROR_BAD_STRING;
return strlen(str.GetInternalData());
}
在不盡理想的情況下, 很可能大部份直接/間接呼叫 StringLength 的 code 都需要 check/translate/report 這個 error condition (更別提某段中間的 code 忘了處理這個狀況). 或許在 StringLength 裏面用 assert 可將這個問題簡單化:
size_t StringLength(const String& str)
{
assert(str.IsInitOk() == true);
return strlen(str.GetInternalData());
}
看起來似乎好些了. 可是 StringLength 的使用者呢?
int foo()
{
String name("fr3@K");
if(name.IsInitOk() == false)
return ERROR_BAD_STRING;
size_t len = StringLength(name);
// do things with `name' and/or `len'...
return 0;
}
Client code 的情況完全沒有改善!
那把 assert 放到 foo 裏面怎麼樣? 或者更進一步, 直接把 assert 放到 String 的 constructor 裏面 (下例中的第二個 assert):
String::String(const char* str)
{
// Check for prerequisite, see here for detail.
assert(str != 0);
data_ = malloc(strlen(str) + 1);
// Check for exceptional condition!
assert(data_ != 0);
strcpy(data_, str);
}
這樣的確避免了前面的層層錯誤回報. 但這個作法跟在 malloc 失敗後拋出 exception 有什麼差別? 在遇到例外狀況時, 拋出 exception VS. 觸發 assertion failure 有什麼不一樣?
String::String(const char* str)
{
// Check for prerequisite, see here for detail.
assert(str != 0);
data_ = malloc(strlen(str) + 1);
// Check for exceptional condition!
if(data_ == 0)
throw bad_alloc();
strcpy(data_, str);
}
對於 class String 的 constructor 來說差異不大. 若上層的 code 能善用 resource handle (如 std::auto_ptr, Boost.Smart-Ptr 與 GION 等, disclaimer: GION 為筆者的 Free Software 作品) 來管理 resource, 除了知道該拿這個例外狀況怎麼辦的最上層, 對其餘中間層的 code 來說只有省掉一種錯誤狀況的檢查, 除此幾乎沒有差別!
如果在這個時候拋出 exception, 由於 stack unwind, catch 到這 exception 的 (遙遠) 上層可能有足夠的 heap memory 顯示錯誤訊息. 又或者上層 reserve 了部份 memory 可以 release (只是要提示使用者 low memory) 進而 re-try. 在 server 等級的應用上, 更可以優雅地 rollback 然後放棄某個 transaction, 而不影響到其他同時間進行的 transaction. 就算只有在 main 有簡單的 try-catch, 而沒有任何其他的錯誤處理, 程式也有很好的機會可以乾淨地 (no leak) 退出. 這些都是 assert 做不到的.
C++ 原生支援三種錯誤回報機制; assertion failure, exception 與 error code . 三者都有各自恰當的使用時機. 沒有一種比其他的方式更具有絕對的優勢.
assert適合拿來 (在NDEBUG沒有被 define 時) 檢驗程式的正確性. 例如檢查 module 內部的狀態完整性 (integrity) 與外部使用者操作的邏輯錯誤 (call sequence 順序與 pointer 參數不可為NULL等)- Exception 則更適合用來回報執行時期的特殊 (exceptional) 錯誤, 如資源請求失敗與物件起始時發生的錯誤 (i.e. in constructor) 等等
- (更多?) 其他時候, 使用傳統的 error code 可能更為適當
即便做出正確的選擇不總是件容易的事情, 完全地迴避任何一種 error reporting 機制都不會是明智的 C++ coding style.
[延伸閱讀]
![]() |
|
| Previous Post « Delicious Bookmarks Upgrade Breaks Firefox Tab Navigation « |
Next Post » C++0x, Near Feature-Complete » |








基本赞同。应该有限制的使用异常。
google的风格必然导致两阶段构造对象法。
但是编写异常安全的代码比想象的要难一些。
像String这种属于基础库的设施,而对于普通的应用开发,如果没有特别的理由,都不应该使用异常。
Comment by eXile — July 10, 2008 @ 22:16