2014年3月5日

[C++] Exception Safety & Guarantees

Exception Handling 是個大學程式設計課通常會教,但不會著墨太深,卻又很重要的東西之一。一般課程關於 exception handling 的部分通常在講解完該語言如何使用 exception 之後就差不多結束了,但是 exception 的重要性卻完全沒有被涵蓋其中,一般作業上也很難出到可以讓同學理解它的重要性何在,何況在僅講究有寫出來就好的作業/project 等情況下,即便是個適合使用 exception 的場合也多半不會使用,是以大多數沒有自行鑽研的人是不太了解的。這篇是來簡單淺談 C++ 的 Exception Handling 中的 exception safety 與 exception guarantees,內容主要參考自 Effective C++More Effective C++ 以及 Exceptional C++。書中會更深入探討這部分,有興趣的人可以自行參閱。




談 exception guarantees 之前先來看看 exception safety,大致了解這東西後再來看 exception gurantee 就容易理解為什麼 exception gurantee 會有那些項目了。在文章中我們可以看到作者對於 exception-safe 的意思是 "exhibits reasonable behavior when an exception is thrown during its execution",翻成中文就是你的程式在發生 exception 時會有合理的行為。那何謂合理的行為? 比方說該釋放的資源有沒有釋放? 能不能處理掉這個 exception 讓程式回歸正常? 或者是把 exception 丟給可以處理的人來處理? 等等。而 exception 最困難的地方就在於發生異常時,程式的執行流程會因此而改變,不論 exception 是來自於自己,或是別人,找出可能發生 exception 的地方並為此撰寫適當的程式碼處理就是 exception handling 要做的事。

Exception 可能來自於許多地方,不管是自己或別人,一旦發生 exception 程式的狀態會因此而改變,因此如果會使用到別人的程式,我們會希望得知可能會有哪些 exception 產生、程式的狀態會變得如何;如果是別人來使用自己的程式,我們也需要告訴使用者同樣的事情。所謂的 Exception Guarantees 就是在告訴使用者你所寫的程式在發生 exception 時,提供什麼程度的保證,例如:絕對不會丟出 exception (No-Throw Guarantee),或者是保證發生 excpetion 時資料會維持原樣不動 (Strong Guarantee: commit or roll-back),或者是只保證發生 exception 時不會有 resource leak,其他無法保證 (Basic Guarantee)。

在 Effective C++ 的條款 29 有舉一個因為 exception 所可能導致的問題:

class PrettyMenu
{
public:
    ...
    void changeBackGround(std::istream& imgSrc); // 改變背景圖片
    ...

private:
    Mutex mutex;
    Image* bgImage;
};

void PrettyMenu::changeBackGround(std::istream& imgSrc)
{
    lock(&mutex);                // 取得 mutex lock 以進入 critical section
    delete bgImage;              // 刪除舊背景
    bgImage = new Image(imgSrc); // 改變背景
    unlock(&mutex);              // 釋放 mutex lock, 離開 critical section
}

這段看來簡單的程式在 exception 發生時會有許多問題,不過我們先來看哪邊會發生 exception:

bgImage = new Image(imgSrc);

可能發生的原因很好舉例:記憶體空間不足導致記憶體配置失敗就會發生 bad_alloc 這個 exception 了。然而當 exception 發生時,最後一行的 unlock 就不會被執行到,因此 mutex 永遠不會被釋放,此時就會有 resource leakage 的問題了,這點就是為什麼 exception safety 會有基本保證的原因。

下一個問題則來自於 delete。當記憶體配智失敗而丟出 exception 時,此時因為已經執行了 delete,換言之原有的背景就不見啦,還找不回來呢!此時可以想像使用者在換背景換到一半發現出了問題,結果竟然連原本的背景都不見了!這當然不是個好現象,對吧?因此強烈寶正的理由就在於使用者可以得知如果你的程式發生 exception 時會不會導致程式狀態也跟著被改變呢? 如果滿足強烈保證,那使用者可以知道 "換背景要嘛成功,不然就是維持原樣",如此一來也容易掌握現在程式的狀態為何。當然有時後程式很難提供強烈保證,此時就只會有基本保證而已 (可以參閱 More Effective C++)。

至於最後 no-throw 的保證比較難解釋一點,可以參閱 Effective C++ 的條款 8:"別讓異常逃離解構式" 與 More Effective C++ 的條款 11:"禁止 exception 留出 destructor 之外"。其原因其實也很簡單:如果今天在 destructor 的執行過程中發生 exception 很容易導致 resource leakage。而有時後我們如果想要在 dtor 多做點除了釋放空間外的事情 (例如產生 log file 之類的) 就特別需要注意 exception 的問題,此時 dtor 內所呼叫的 function 如果提供 no-throw 的保證可以降低許多額外處理的負擔。

為了提供相對應的 exception safety,程式在撰寫上必須要考慮所有可能產生 exception 的路徑以及問題,因此若在程式撰寫初期未加以考慮,而是在功能都寫好後才以附加的方式來處理,此時很容易產生怎麼寫都無法滿足 exception safety,因此在程式撰寫初期、還在設計整體架構時就將 exception safety 納入考量是必要的!在 Exceptional C++ 的條款 18 有舉一個我覺得不錯的例子,在短短的 8 行程式碼可以有多到你無法想像的執行路徑:

String EvaluateSalaryAndReturnName (Employee e)
{
    if( e.Title() == "CEO" || e.Salary() > 100000 ) 
    {
        cout << e.First() << "" << e.Last() << " is overpaid" << endl;
    }
    return e.First() + " " + e.Last();
}

這段簡短的程式碼共有 23 條執行路徑,你找的出幾條?


以下為解答:
1. 如果不產生任何異常,此時會有 3 種可能的執行路徑
    (從 if 的條件是否成立導致)
2.如果考慮 exception,會有額外的 20 條可能的路徑要考慮
   a. String 跟 Employee 是以 by value 的方式傳遞,其 ctor 有可能丟出 exception
   b. 所有 member function 都有可能會有 exception
   c. "e.Title() == "CEO"" 這一行可能會因為需要將 "CEO" 轉成其他型別,此時也可能會有 exception
  d. operator== 如果是使用者自定義的 function 也有可能丟出 exception
  e. 最後一行的 return 是個 string 的 concatenation,每個地方都有可能產生 exception

當然,如果這些 function 有提供 exception safety 的保證,可能產生的執行路徑就會變少,但不能否認的是 exception 會對程式架構影響甚巨,不得不慎!

Reference:
[1] Is "Effective C++" still worthy of reading?

沒有留言:

張貼留言