1. 程式人生 > >C++:編寫異常安全程式碼

C++:編寫異常安全程式碼

在C++的使用當中,最令人頭疼的地方莫非是記憶體管理或者異常的使用。

想寫出一個真正異常安全的程式碼是非常難得,需要考慮的因素有非常多。

在現代C++當中也有很多人提倡不使用異常,但是要完全杜絕使用C++異常

也是很難的,除非打算不使用任何一個標準庫,重寫所有需要用的資料結構演算法等等。

在一般情況下,適當的使用標準庫也能提高程式的開發效率,和健壯性。

畢竟標準庫已經被廣泛使用,程式碼質量高,但是使用異常會導致程式碼的膨脹。

為此,我們只能保證最少的使用異常,但是編寫異常安全的函式是必不可少的。

void MyClass::foo() {
    lock(&mutex);
    
delete buffer; ++counter; buffer = new CBuffer(); unlock(&mutex); }

從“異常安全性”的觀點來看,這個函式很糟。“異常安全”有兩個條件,而這個函式沒有滿足其中的任何一個條件

當異常被丟擲時,帶有異常安全性的函式會:

  • 不洩露任何資源。上述程式碼沒有做到,因為一旦new CBuffer()導致異常,unlock就永遠不會呼叫,於是就產生了死鎖。
  • 不允許資料破壞。如果new CBuffer()導致異常,buffer就指向了一個已經被刪除的物件,counter也已經被累加。

使用RAII保證在出現異常時正確的釋放資源

void MyClass::foo() {
    Lock lock(&mutex);
    delete buffer;
    ++counter;
    buffer = new CBuffer();
}

目前死鎖的問題被解決了,但是資料破壞的問題還沒有被解決。

此刻我們需要做一些抉擇,在抉擇之前我們必須先面對一些用來定義選項的術語。

異常安全函式提供一下三個保證之一:

  • 基本承諾:如果異常被丟擲,程式內的任何事物仍然保證在有效狀態下。
  • 強烈保證:如果異常被丟擲,程式狀態不改變,擁有原子性,要麼全部成功,要麼回到呼叫函式前的狀態。
  • 不拋擲(nothrow)保證:承諾絕對不丟擲異常。

我們可以採用一種策略。這個策略被稱為:copy and swap。

原則很簡單,打算為所有要改變的物件做出一份副本,然後在副本上面進行修改,

待所有副本修改完畢,再將副本與原物件在一個不丟擲異常的操作中置換出來(swap)。

struct PMImpl {
    std::shared_ptr<CBuffer> buffer;
    int    counter;
};

void MyClass::foo() {
    Lock lock(&mutex);
    // 使用基於RAII的指標管理方式
    std::shared_ptr<PMImpl> new_impl(new PMImpl );
    new_impl->buffer.reset(new CBuffer);
    new_impl->counter = pimpl->counter + 1;
    // 使用不丟擲異常的swap
    swap(pimpl, new_impl);
}

“copy and swap”策略是對物件狀態做出”全有或者全無“改變的一個很好的辦法。

一個軟體系統要麼就具備異常安全性,要不就全然否定,沒有所謂的”區域性異常安全系統“。

如果系統內有一個函式不具備異常安全性,整個系統就不具備異常安全性。

 

四十年前,滿載goto的程式碼被視為一種美好實踐,而今我們卻致力寫出結構化控制流。二十年前,全域性資料被視為一種美好實踐,而今我們卻致力於資料的封裝。十年前,寫“未將異常考慮在內”的函式被視為一種美好實踐,而今我們致力寫出“異常安全碼”。時間不斷前進,我們與時俱進。