條款 29:為 “異常安全” 而努力是值得的

Effective C++ 中文版 第三版》讀書筆記

** 條款 29:為 “異常安全” 而努力是值得的 **

有個 class 用來表現夾帶背景圖案的 GUI 菜單單,這個 class 用于多線程環境:

class PrettyMenu { 
public: 
    ... 
    void changeBackground(std::istream& imgSrc); 
    ... 

private: 
    Mutex mutex; 
    Image* bgImage; 
    int imageChanges; 
}; 

void PrettyMenu::changeBackground(std::istream& imgSrc) 
{ 
    lock(&mutex); 
    delete bgImage; 
    ++imageChanges; 
    bgImage = new Image(imgSrc); 
    unlock(&mutex); 
}

從異常安全性的角度看,這個函數很糟。“異常安全” 有兩個條件:當異常被拋出時,帶有異常安全性的函數會:

不泄露任何資源。上述代碼沒有做到這一點,因為一旦 “new Image(imgSrc)” 導致異常,對 unlock 就不會執行,于是互斥器就永遠被把持住了。

不允許數據破壞。如果 “new Image(imgSrc)” 拋出異常,bgImage 就指向一個已被刪除的對象,imageChanges 也已被累加,而其實并沒有新的圖像被 成功安裝起來。

解決資源泄漏的問題很容易,

void PrettyMenu::changeBackground(std::istream& imgSrc) 
{ 
    Lock ml(&mutex);//來自條款14; 
    delete bgImage; 
    ++imageChanges; 
    bgImage = new Image(imgSrc); 
}

關于“資源管理類”如 Lock,一個最棒的事情是,它們通常使函數更短。較少的代碼就是較好的代碼,因為出錯的機會比較少。

異常安全函數(Exception-safe function)提供以下三個保證之一:

基本承諾:如果異常被拋出,程序內的任何事物仍然保持在有效狀態下。沒有任何對象或數據結構會因此而敗壞,所有對象都處于一種內部前后一致的狀態(例如所有的 class 約束條件都繼續獲得滿足)。然而程序的現實狀態恐怕不可預料。如上例 changeBackground 使得一旦有異常被拋出時,PrettyMenu 對象可以繼續擁有原背景圖像,或是令它擁有某個缺省背景圖像,但客戶無法預期哪一種情況。如果想知道,它們恐怕必須調用某個成員函數以得知當時的背景圖像是什么。

強烈保證:如果異常被拋出, 程序狀態不改變。如果函數成功,就是完全成功,否則,程序會回復到“調用函數之前”的狀態。

不拋擲(nothrow)保證:承諾絕不拋出異常,因為它們總是能夠完成它們原先承諾的功能。作用于內置類型(如 ints,指針等等)上的所有操作都提供 nothrow 保證。帶著“空白異常明細”的函數必為 nothrow 函數,其實不盡然

int doSomething() throw(); // “空白異常明細”

這并不是說 doSomething 絕不會拋出異常,而是說如果拋出異常,將是嚴重錯誤,會有你意想不到的函數被調用。實際上 doSomething 也許完全沒有提供任何異常保證。函數的聲明式(包括異常明細)并不能告訴你是否它是正確的、可移植的或高效的,也不能告訴你它是否提供任何異常安全性保證。

異常安全碼(Exception-safe code)必須提供上述三種保證之一。否則,它就不具備異常安全性。

一般而言,應該會想提供可實施的最強烈保證。nothrow 函數很棒,但我們很難再 C part of C++ 領域中完全沒有調用任何一個可能拋出異常的函數。所以大部分函數而言,抉擇往往落在基本保證和強烈保證之間

對 changeBackground 而言,首先,從一個類型為 Image* 的內置指針改為一個 “用于資源管理” 的智能指針,第二,重新排列 changeBackground 內的語句次序,使得在更換圖像之后再累加 imageChanges。

class PrettyMenu{ 
    ... 
    std::tr1::shared_ptr<Image> bgImage; 
    ... 
};

void PrettyMenu::changeBackground(std::istream& imgSrc) 
{ 
    Lock ml(&mutex); 
    bgImage.reset(new Image(imgSrc)); 
    ++imageChanges; 
}

不再需要手動 delete 舊圖像,只有在 reset 在其參數(也就是 “new Image(imgSrc)” 的執行結果)被成功生成之后才會被調用。美中不足的是參數 imgSrc。如果 Image 構造函數拋出異常,有可能輸入流的讀取記號(read marker)已被移走,而這樣的搬移對程序其余部分是一種可見的狀態改變。所以在解決這個之前只提供基本點異常安全保證。

有一個一般化的策略很典型會導致強烈保證,被稱為 “copy and swap”:為打算修改的對象做一個副本,在那個副本上做一切必要修改。若有任何修改動作拋出異常,源對象仍然保持未改變狀態。待所有改變都成功后,再將修改過的副本和原對象在一個不拋出異常的 swap 中置換。

實現上通常是將所有“隸屬對象的數據”從原對象放進另一個對象內,然后賦予源對象一個指針,指向那個所謂的實現對象(implementation object,即副本)。對 PrettyMenu 而言,典型的寫法如下:

struct PMImpl{ 
    std::tr1::shared_ptr<Image> bgImage; 
    int imageChanges; 
}; 

class PrettyMenu{ 
    ... 
private: 
    Mutex mutex; 
    std::tr1::shared_ptr<PMImpl> pImpl; 
}; 

void PrettyMenu::changeBackground(std::istream& imgSrc) 
{ 
    using std::swap; 
    Lock ml(&mutex); 
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); 
    pNew->bgImage.reset(new Image(imgSrc)); // 修改副本 
    ++pNew->imageChanges; 
    swap(pImpl, pNew);// 置換數據 
}

copy and swap 策略雖然做出“全有或全無”改變的一個好辦法,但一般而言并不保證整個函數有強烈的異常安全性。

如 someFunc。使用 copy-and-swap 策略,但函數還包括對另外連個函數 f1 和 f2 的調用:

void someFunc()
{
    …
    f1();
    f2();
    …
}

顯然,如果 f1 或 f2 的異常安全性比 “強烈保證” 低,就很難讓 someFunc 成為 “強烈異常安全”。如果 f1 和 f2 都是 “強烈異常安全”,情況并不因此好轉。畢竟,如果 f1 圓滿結束,程序狀態在任何方面都有可能有所改變,因此如果 f2 隨后拋出異常,程序狀態和 someFunc 被調用前并不相同,甚至當 f2 沒有改變任何東西時也是如此。

問題出現在 “連帶影響”,如果由函數只操作局部狀態,便相對容易的提供強烈保證,但是函數對 “非局部性數據” 有連帶影響時,提供強烈保證就困難的多。例如,如果調用 f1 帶來的影響是某個數據庫被改動了,那就很難讓 someFunc 具備強烈安全性。另一個主題是效率。copy-and-swap 得好用你可能無法(或不愿意)供應的時間和空間。所以,“強烈保證” 并不是在任何時候都顯得實際。

當 “強烈保證” 不切實際時,你就必須提供 “基本保證”。

你應該挑選 “現實可操作” 條件下最強烈等級,只有當你的函數調用了傳統代碼,才別無選擇的將它設為 “無任何保證”。

請記?。?/p>

  1. 異常安全函數即使發生異常也不會泄露資源或允許任何數據結構敗壞。這樣的函數區分為三種可能的保證:基本型、強烈型、不拋異常型。
  2. “強烈保證” 往往能夠以 copy-and-swap 實現出來,但 “強烈保證” 并非對所有函數都可實現或具備現實意義。
  3. 函數提供的 “異常安全保證” 通常最高只等于其所調用之各個函數的 “異常安全保證” 中的最弱者。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容