《Effective C++ 中文版 第三版》讀書筆記
** 條款 29:為 “異常安全” 而努力是值得的 **
有個 class 用來表現(xiàn)夾帶背景圖案的 GUI 菜單單,這個 class 用于多線程環(huán)境:
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);
}
從異常安全性的角度看,這個函數(shù)很糟。“異常安全” 有兩個條件:當(dāng)異常被拋出時,帶有異常安全性的函數(shù)會:
不泄露任何資源。上述代碼沒有做到這一點(diǎn),因為一旦 “new Image(imgSrc)” 導(dǎo)致異常,對 unlock 就不會執(zhí)行,于是互斥器就永遠(yuǎn)被把持住了。
不允許數(shù)據(jù)破壞。如果 “new Image(imgSrc)” 拋出異常,bgImage 就指向一個已被刪除的對象,imageChanges 也已被累加,而其實(shí)并沒有新的圖像被 成功安裝起來。
解決資源泄漏的問題很容易,
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);//來自條款14;
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
關(guān)于“資源管理類”如 Lock,一個最棒的事情是,它們通常使函數(shù)更短。較少的代碼就是較好的代碼,因為出錯的機(jī)會比較少。
異常安全函數(shù)(Exception-safe function)提供以下三個保證之一:
基本承諾:如果異常被拋出,程序內(nèi)的任何事物仍然保持在有效狀態(tài)下。沒有任何對象或數(shù)據(jù)結(jié)構(gòu)會因此而敗壞,所有對象都處于一種內(nèi)部前后一致的狀態(tài)(例如所有的 class 約束條件都繼續(xù)獲得滿足)。然而程序的現(xiàn)實(shí)狀態(tài)恐怕不可預(yù)料。如上例 changeBackground 使得一旦有異常被拋出時,PrettyMenu 對象可以繼續(xù)擁有原背景圖像,或是令它擁有某個缺省背景圖像,但客戶無法預(yù)期哪一種情況。如果想知道,它們恐怕必須調(diào)用某個成員函數(shù)以得知當(dāng)時的背景圖像是什么。
強(qiáng)烈保證:如果異常被拋出, 程序狀態(tài)不改變。如果函數(shù)成功,就是完全成功,否則,程序會回復(fù)到“調(diào)用函數(shù)之前”的狀態(tài)。
不拋擲(nothrow)保證:承諾絕不拋出異常,因為它們總是能夠完成它們原先承諾的功能。作用于內(nèi)置類型(如 ints,指針等等)上的所有操作都提供 nothrow 保證。帶著“空白異常明細(xì)”的函數(shù)必為 nothrow 函數(shù),其實(shí)不盡然
int doSomething() throw(); // “空白異常明細(xì)”
這并不是說 doSomething 絕不會拋出異常,而是說如果拋出異常,將是嚴(yán)重錯誤,會有你意想不到的函數(shù)被調(diào)用。實(shí)際上 doSomething 也許完全沒有提供任何異常保證。函數(shù)的聲明式(包括異常明細(xì))并不能告訴你是否它是正確的、可移植的或高效的,也不能告訴你它是否提供任何異常安全性保證。
異常安全碼(Exception-safe code)必須提供上述三種保證之一。否則,它就不具備異常安全性。
一般而言,應(yīng)該會想提供可實(shí)施的最強(qiáng)烈保證。nothrow 函數(shù)很棒,但我們很難再 C part of C++ 領(lǐng)域中完全沒有調(diào)用任何一個可能拋出異常的函數(shù)。所以大部分函數(shù)而言,抉擇往往落在基本保證和強(qiáng)烈保證之間
對 changeBackground 而言,首先,從一個類型為 Image* 的內(nèi)置指針改為一個 “用于資源管理” 的智能指針,第二,重新排列 changeBackground 內(nèi)的語句次序,使得在更換圖像之后再累加 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 在其參數(shù)(也就是 “new Image(imgSrc)” 的執(zhí)行結(jié)果)被成功生成之后才會被調(diào)用。美中不足的是參數(shù) imgSrc。如果 Image 構(gòu)造函數(shù)拋出異常,有可能輸入流的讀取記號(read marker)已被移走,而這樣的搬移對程序其余部分是一種可見的狀態(tài)改變。所以在解決這個之前只提供基本點(diǎn)異常安全保證。
有一個一般化的策略很典型會導(dǎo)致強(qiáng)烈保證,被稱為 “copy and swap”:為打算修改的對象做一個副本,在那個副本上做一切必要修改。若有任何修改動作拋出異常,源對象仍然保持未改變狀態(tài)。待所有改變都成功后,再將修改過的副本和原對象在一個不拋出異常的 swap 中置換。
實(shí)現(xiàn)上通常是將所有“隸屬對象的數(shù)據(jù)”從原對象放進(jìn)另一個對象內(nèi),然后賦予源對象一個指針,指向那個所謂的實(shí)現(xiàn)對象(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);// 置換數(shù)據(jù)
}
copy and swap 策略雖然做出“全有或全無”改變的一個好辦法,但一般而言并不保證整個函數(shù)有強(qiáng)烈的異常安全性。
如 someFunc。使用 copy-and-swap 策略,但函數(shù)還包括對另外連個函數(shù) f1 和 f2 的調(diào)用:
void someFunc()
{
…
f1();
f2();
…
}
顯然,如果 f1 或 f2 的異常安全性比 “強(qiáng)烈保證” 低,就很難讓 someFunc 成為 “強(qiáng)烈異常安全”。如果 f1 和 f2 都是 “強(qiáng)烈異常安全”,情況并不因此好轉(zhuǎn)。畢竟,如果 f1 圓滿結(jié)束,程序狀態(tài)在任何方面都有可能有所改變,因此如果 f2 隨后拋出異常,程序狀態(tài)和 someFunc 被調(diào)用前并不相同,甚至當(dāng) f2 沒有改變?nèi)魏螙|西時也是如此。
問題出現(xiàn)在 “連帶影響”,如果由函數(shù)只操作局部狀態(tài),便相對容易的提供強(qiáng)烈保證,但是函數(shù)對 “非局部性數(shù)據(jù)” 有連帶影響時,提供強(qiáng)烈保證就困難的多。例如,如果調(diào)用 f1 帶來的影響是某個數(shù)據(jù)庫被改動了,那就很難讓 someFunc 具備強(qiáng)烈安全性。另一個主題是效率。copy-and-swap 得好用你可能無法(或不愿意)供應(yīng)的時間和空間。所以,“強(qiáng)烈保證” 并不是在任何時候都顯得實(shí)際。
當(dāng) “強(qiáng)烈保證” 不切實(shí)際時,你就必須提供 “基本保證”。
你應(yīng)該挑選 “現(xiàn)實(shí)可操作” 條件下最強(qiáng)烈等級,只有當(dāng)你的函數(shù)調(diào)用了傳統(tǒng)代碼,才別無選擇的將它設(shè)為 “無任何保證”。
請記住:
- 異常安全函數(shù)即使發(fā)生異常也不會泄露資源或允許任何數(shù)據(jù)結(jié)構(gòu)敗壞。這樣的函數(shù)區(qū)分為三種可能的保證:基本型、強(qiáng)烈型、不拋異常型。
- “強(qiáng)烈保證” 往往能夠以 copy-and-swap 實(shí)現(xiàn)出來,但 “強(qiáng)烈保證” 并非對所有函數(shù)都可實(shí)現(xiàn)或具備現(xiàn)實(shí)意義。
- 函數(shù)提供的 “異常安全保證” 通常最高只等于其所調(diào)用之各個函數(shù)的 “異常安全保證” 中的最弱者。