国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 學(xué)院 > 開(kāi)發(fā)設(shè)計(jì) > 正文

C++箴言:爭(zhēng)取異常安全的代碼

2019-11-17 05:11:59
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

  異常安全(Exception safety)有點(diǎn)像懷孕(PRegnancy)……但是,請(qǐng)把這個(gè)想法先控制一會(huì)兒。我們還不能真正地議論生育(reprodUCtion),直到我們排除萬(wàn)難渡過(guò)求愛(ài)時(shí)期(courtship)。(此段作者使用的 3 個(gè)詞均有雙關(guān)含義,pregnancy 也可理解為富有意義,reproduction 也可理解為再現(xiàn),再生,courtship 也可理解為爭(zhēng)取,謀求。為了與后面的譯文對(duì)應(yīng),故按照現(xiàn)在的譯法。——譯者注)

  假設(shè)我們有一個(gè)類,代表帶有背景圖像的 GUI 菜單。這個(gè)類被設(shè)計(jì)成在多線程環(huán)境中使用,所以它有一個(gè)用于并行控制(concurrency control)的互斥體(mutex):

class PrettyMenu {
public:
 ...
 void changeBackground(std::istream& imgSrc); // change background
 ... // image

private:

 Mutex mutex; // mutex for this object

 Image *bgImage; // current background image
 int imageChanges; // # of times image has been changed
};
  考慮這個(gè) PrettyMenu 的 changeBackground 函數(shù)的可能的實(shí)現(xiàn):

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
 lock(&mutex); // acquire mutex (as in Item 14)

 delete bgImage; // get rid of old background
 ++imageChanges; // update image change count
 bgImage = new Image(imgSrc); // install new background

 unlock(&mutex); // release mutex
}
  從異常安全的觀點(diǎn)看,這個(gè)函數(shù)爛到了極點(diǎn)。異常安全有兩條要求,而這里全都沒(méi)有滿足。

  當(dāng)一個(gè)異常被拋出,異常安全的函數(shù)應(yīng)該:

  ·沒(méi)有資源泄露。上面的代碼沒(méi)有通過(guò)這個(gè)測(cè)試,因?yàn)榧偃?"new Image(imgSrc)" 表達(dá)式產(chǎn)生一個(gè)異常,對(duì) unlock 的調(diào)用就永遠(yuǎn)不會(huì)執(zhí)行,而那個(gè)互斥體也將被永遠(yuǎn)掛起。

  ·不答應(yīng)數(shù)據(jù)結(jié)構(gòu)惡化。假如 "new Image(imgSrc)" 拋出異常,bgImage 被遺留下來(lái)指向一個(gè)被刪除對(duì)象。另外,盡管并沒(méi)有將一張新的圖像設(shè)置到位,imageChanges 也已經(jīng)被增加。(在另一方面,舊的圖像被明確地刪除,所以我料想你會(huì)爭(zhēng)辯說(shuō)圖像已經(jīng)被“改變”了。)

  規(guī)避資源泄露問(wèn)題比較輕易,我們以前解釋了如何使用對(duì)象治理資源,也討論了引進(jìn) Lock 類作為一種時(shí)尚的確保互斥體被釋放的方法:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
 Lock ml(&mutex); // from Item 14: acquire mutex and
 // ensure its later release
 delete bgImage;
 ++imageChanges;
 bgImage = new Image(imgSrc);
}
  關(guān)于像 Lock 這樣的資源治理類的最好的事情之一是它們通常會(huì)使函數(shù)變短。看到對(duì) unlock 的調(diào)用不再需要了嗎?作為一個(gè)一般的規(guī)則,更少的代碼就是更好的代碼。因?yàn)樵诟淖兊臅r(shí)候這樣可以較少誤入歧途并較少產(chǎn)生誤解。

  隨著資源泄露被我們甩在身后,我們可以把我們的注重力集中到數(shù)據(jù)結(jié)構(gòu)惡化。在這里我們有一個(gè)選擇,但是在我們能選擇之前,我們必須先面對(duì)定義我們的選擇的術(shù)語(yǔ)。 異常安全函數(shù)提供下述三種保證之一:

  ·函數(shù)提供基本保證(the basic guarantee),允諾假如一個(gè)異常被拋出,程序中剩下的每一件東西都處于合法狀態(tài)。沒(méi)有對(duì)象或數(shù)據(jù)結(jié)構(gòu)被破壞,而且所有的對(duì)象都處于內(nèi)部調(diào)和狀態(tài)(所有的類不變量都被滿足)。然而,程序的精確狀態(tài)可能是不可預(yù)期的。例如,我們可以重寫(xiě) changeBackground,以致于假如一個(gè)異常被拋出,PrettyMenu 對(duì)象可以繼續(xù)保留原來(lái)的背景圖像,或者它可以持有某些缺省的背景圖像,但是客戶無(wú)法預(yù)知到底是哪一個(gè)。(為了查明這一點(diǎn),他們大概必須調(diào)用某個(gè)可以告訴他們當(dāng)前背景圖像是什么的成員函數(shù)。)

  ·函數(shù)提供強(qiáng)力保證(the strong guarantee),允諾假如一個(gè)異常被拋出,程序的狀態(tài)不會(huì)發(fā)生變化。調(diào)用這樣的函數(shù)在感覺(jué)上是極其微弱的,假如它們成功了,它們就完全成功,假如它們失敗了,程序的狀態(tài)就像它們從沒(méi)有被調(diào)用過(guò)一樣。

  ·與提供強(qiáng)力保證的函數(shù)一起工作比與只提供基本保證的函數(shù)一起工作更加輕易,因?yàn)檎{(diào)用提供強(qiáng)力保證的函數(shù)之后,僅有兩種可能的程序狀態(tài):像預(yù)期一樣成功執(zhí)行了函數(shù),或者繼續(xù)保持函數(shù)被調(diào)用時(shí)當(dāng)時(shí)的狀態(tài)。與之相比,假如調(diào)用只提供基本保證的函數(shù)引發(fā)了異常,程序可能存在于任何合法的狀態(tài)。

  函數(shù)提供不拋出保證(the nothrow guarantee),允諾決不拋出異常,因?yàn)樗鼈冎蛔鏊鼈兇饝?yīng)要做的。所有對(duì)內(nèi)建類型(例如,ints,指針,等等)的操作都是不拋出(nothrow)的(也就是說(shuō),提供不拋出保證)。這是異常安全代碼中必不可少的基礎(chǔ)構(gòu)件。

  假定一個(gè)帶有空的異常規(guī)格(exception specification)的函數(shù)是不拋出的似乎是合理的,但這不一定正確的。例如,考慮這個(gè)函數(shù):


int doSomething() throw(); // note empty exception spec.
  這并不是說(shuō) doSomething 永遠(yuǎn)不會(huì)拋出異常;而是說(shuō)假如 doSomething 拋出一個(gè)異常,它就是一個(gè)嚴(yán)重的錯(cuò)誤,應(yīng)該調(diào)用 uneXPected 函數(shù) [1]。實(shí)際上,doSomething 可能根本不提供任何異常保證。一個(gè)函數(shù)的聲明(假如有的話,也包括它的異常規(guī)格(exception specification))不能告訴你一個(gè)函數(shù)是否正確,是否可移植,或是否高效,而且,即便有,它也不能告訴你它會(huì)提供哪一種異常安全保證。所有這些特性都由函數(shù)的實(shí)現(xiàn)決定,而不是它的聲明能決定的。

  [1] 關(guān)于 unexpected 函數(shù)的資料,可以求助于你中意的搜索引擎或包羅萬(wàn)象的 C++ 課本。(你或許有幸搜到 set_unexpected,這個(gè)函數(shù)用于指定 unexpected 函數(shù)。)

  異常安全函數(shù)必須提供上述三種保證中的一種。假如它沒(méi)有提供,它就不是異常安全的。于是,選擇就在于決定你寫(xiě)的每一個(gè)函數(shù)究竟要提供哪種保證。除非要處理遺留下來(lái)的非異常安全的代碼(稍后我們要討論這個(gè)問(wèn)題),只有當(dāng)你的最高明的需求分析團(tuán)隊(duì)為你的應(yīng)用程序識(shí)別出的一項(xiàng)需求就是泄漏資源以及運(yùn)行于被破壞的數(shù)據(jù)結(jié)構(gòu)之上時(shí),不提供異常安全保證才能成為一個(gè)選項(xiàng)。

  作為一個(gè)一般性的規(guī)則,你應(yīng)該提供實(shí)際可達(dá)到的最強(qiáng)力的保證。從異常安全的觀點(diǎn)看,不拋出的函數(shù)(nothrow functions)是極好的,但是在 C++ 的 C 部分之外部不調(diào)用可能拋出異常的函數(shù)簡(jiǎn)直就是寸步難行。使用動(dòng)態(tài)分配內(nèi)存的任何東西(例如,所有的 STL 容器)假如不能找到足夠的內(nèi)存來(lái)滿足一個(gè)請(qǐng)求,在典型情況下,它就會(huì)拋出一個(gè) bad_alloc 異常。只要你能做到就提供不拋出保證,但是對(duì)于大多數(shù)函數(shù),選擇是在基本的保證和強(qiáng)力的保證之間的。

  在 changeBackground 的情況下,提供差不多的強(qiáng)力保證并不困難。首先,我們將 PrettyMenu 的 bgImage 數(shù)據(jù)成員的類型從一個(gè)內(nèi)建的 Image* 指針改變?yōu)?Item 13 中描述的智能資源治理指針中的一種。坦白地講,在預(yù)防資源泄漏的基本原則上,這完全是一個(gè)好主意。它幫助我們提供強(qiáng)大的異常安全保證的事實(shí)進(jìn)一步加強(qiáng)了這樣的觀點(diǎn)——使用對(duì)象(諸如智能指針)治理資源是良好設(shè)計(jì)的基礎(chǔ)。在下面的代碼中,我展示了 tr1::shared_ptr 的使用,因?yàn)楫?dāng)進(jìn)行通常的拷貝時(shí)它的更符合直覺(jué)的行為使得它比 auto_ptr 更可取。

  第二,我們重新排列 changeBackground 中的語(yǔ)句,以致于直到圖像發(fā)生變化,才增加 imageChanges。這是一個(gè)很好的策略——直到某件事情真正發(fā)生了,再改變一個(gè)對(duì)象的狀態(tài)來(lái)表示某事已經(jīng)發(fā)生。

  這就是修改之后的代碼:

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

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
 Lock ml(&mutex);

 bgImage.reset(new Image(imgSrc)); // replace bgImage’s internal
 // pointer with the result of the
 // "new Image" expression
 ++imageChanges;
}
  注重這里不再需要手動(dòng)刪除舊的圖像,因?yàn)樵谥悄苤羔槂?nèi)部已經(jīng)被處理了。此外,只有當(dāng)新的圖像被成功創(chuàng)建了刪除行為才會(huì)發(fā)生。更準(zhǔn)確地說(shuō),只有當(dāng) tr1::shared_ptr::reset 函數(shù)的參數(shù)("new Image(imgSrc)" 的結(jié)果)被成功創(chuàng)建了,這個(gè)函數(shù)才會(huì)被調(diào)用。只有在對(duì) reset 的調(diào)用的內(nèi)部才會(huì)使用 delete,所以假如這個(gè)函數(shù)從來(lái)不曾進(jìn)入,delete 就從來(lái)不曾使用。同樣請(qǐng)注重一個(gè)治理資源(動(dòng)態(tài)分配的 Image)的對(duì)象(tr1::shared_ptr)的使用再次縮短了 changeBackground 的長(zhǎng)度。

  正如我所說(shuō)的,這兩處改動(dòng)差不多有能力使 changeBackground 提供強(qiáng)力異常安全保證。美中不足的是什么呢?參數(shù) imgSrc。假如 Image 的構(gòu)造函數(shù)拋出一個(gè)異常,輸入流(input stream)的讀標(biāo)記(read marker)可能已經(jīng)被移動(dòng),而這樣的移動(dòng)就成為對(duì)程序的其它部分來(lái)說(shuō)可見(jiàn)的一個(gè)狀態(tài)的變化。直到 changeBackground 著手解決這個(gè)問(wèn)題之前,它只能提供基本異常安全保證。

  無(wú)論如何,讓我們把它放在一邊,并且依然假裝 changeBackground 可以提供強(qiáng)力保證。(我相信你至少能用一種方法做到這一點(diǎn),或許可以通過(guò)將它的參數(shù)從一個(gè) istream 改變到包含圖像數(shù)據(jù)的文件的文件名。)有一種通常的設(shè)計(jì)策略可以有代表性地產(chǎn)生強(qiáng)力保證,而且熟悉它是非常必要的。這個(gè)策略被稱為 "copy and swap"。它的原理很簡(jiǎn)單。先做出一個(gè)你要改變的對(duì)象的拷貝,然后在這個(gè)拷貝上做出全部所需的改變。假如改變過(guò)程中的某些操作拋出了異常,最初的對(duì)象保持不變。在所有的改變完全成功之后,將被改變的對(duì)象和最初的對(duì)象在一個(gè)不會(huì)拋出異常的操作中進(jìn)行交換。 這通常通過(guò)下面的方法實(shí)現(xiàn):將每一個(gè)對(duì)象中的全部數(shù)據(jù)從“真正的”對(duì)象中放入到一個(gè)單獨(dú)的實(shí)現(xiàn)對(duì)象中,然后將一個(gè)指向?qū)崿F(xiàn)對(duì)象的指針交給真正對(duì)象。這通常被稱為 "pimpl idiom",Item 31 描述了它的一些細(xì)節(jié)。對(duì)于 PrettyMenu 來(lái)說(shuō),它一般就像這樣:

struct PMImpl { // PMImpl = "PrettyMenu
 std::tr1::shared_ptr<Image> bgImage; // Impl."; see below for
 int imageChanges; // why it’s a struct
};

class PrettyMenu {
 ...

private:
 Mutex mutex;
 std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
 using std::swap; // see Item 25

 Lock ml(&mutex); // acquire the mutex

 std::tr1::shared_ptr<PMImpl> // copy obj. data
 pNew(new PMImpl(*pImpl));

 pNew->bgImage.reset(new Image(imgSrc)); // modify the copy
 ++pNew->imageChanges;

 swap(pImpl, pNew); // swap the new
 // data into place

} // release the mutex
  在這個(gè)例子中,我選擇將 PMImpl 做成一個(gè)結(jié)構(gòu)體,而不是類,因?yàn)橥ㄟ^(guò)讓 pImpl 是 private 就可以確保 PrettyMenu 數(shù)據(jù)的封裝。將 PMImpl 做成一個(gè)類雖然有些不那么方便,卻沒(méi)有增加什么好處。(這也會(huì)使有面向?qū)ο鬂嶑闭咦咄稛o(wú)路。)假如你愿意,PMImpl 可以嵌套在 PrettyMenu 內(nèi)部,像這樣的打包問(wèn)題與我們這里所關(guān)心的寫(xiě)異常安全的代碼的問(wèn)題沒(méi)有什么關(guān)系。

  copy-and-swap 策略是一種全面改變或絲毫不變一個(gè)對(duì)象的狀態(tài)的極好的方法,但是,在通常情況下,它不能保證全部函數(shù)都是強(qiáng)力異常安全的。為了弄清原因,考慮一個(gè) changeBackground 的抽象化身—— someFunc,它使用了 copy-and-swap,但是它包含了對(duì)另外兩個(gè)函數(shù)(f1 和 f2)的調(diào)用:


void someFunc()
{
 ... // make copy of local state
 f1();
 f2();
 ... // swap modified state into place
}
  很明顯,假如 f1 或 f2 低于強(qiáng)力異常安全,someFunc 就很難成為強(qiáng)力異常安全的。例如,假設(shè) f1 僅提供基本保證。為了讓 someFunc 提供強(qiáng)力保證,它必須寫(xiě)代碼在調(diào)用 f1 之前測(cè)定整個(gè)程序的狀態(tài),并捕捉來(lái)自 f1 的所有異常,然后恢復(fù)到最初的狀態(tài)。

  即使 f1 和 f2 都是強(qiáng)力異常安全的,事情也好不到哪去。假如 f1 運(yùn)行完成,程序的狀態(tài)已經(jīng)發(fā)生了毫無(wú)疑問(wèn)的變化,所以假如隨后 f2 拋出一個(gè)異常,即使 f2 沒(méi)有改變?nèi)魏螙|西,程序的狀態(tài)也已經(jīng)和調(diào)用 someFunc 時(shí)不同。

  問(wèn)題在于副作用。只要函數(shù)僅對(duì)局部狀態(tài)起作用(例如,someFunc 僅僅影響調(diào)用它的那個(gè)對(duì)象的狀態(tài)),它提供強(qiáng)力保證就相對(duì)輕易。當(dāng)函數(shù)的副作用影響了非局部數(shù)據(jù),它就會(huì)困難得多。例如,假如調(diào)用 f1 的副作用是改變數(shù)據(jù)庫(kù),讓 someFunc 成為強(qiáng)力異常安全就非常困難。一般情況下,沒(méi)有辦法撤銷(xiāo)已經(jīng)提交的數(shù)據(jù)庫(kù)變化,其他數(shù)據(jù)庫(kù)客戶可能已經(jīng)看見(jiàn)了數(shù)據(jù)庫(kù)的新?tīng)顟B(tài)。

  類似這樣的問(wèn)題會(huì)阻止你為函數(shù)提供強(qiáng)力保證,即使你希望去做。另一個(gè)問(wèn)題是效率。copy-and-swap 的要點(diǎn)是這樣一個(gè)想法:改變一個(gè)對(duì)象的數(shù)據(jù)的拷貝,然后在一個(gè)不會(huì)拋出異常的操作中將被改變的數(shù)據(jù)和原始數(shù)據(jù)進(jìn)行交換。這就需要做出每一個(gè)要改變的對(duì)象的拷貝,這可能會(huì)用到你不能或不情愿動(dòng)用的時(shí)間和空間。強(qiáng)力保證是非常值得的,當(dāng)它可用時(shí)你應(yīng)該提供它,除非在它不能 100% 可用的時(shí)候。

  當(dāng)它不可用時(shí),你就必須提供基本保證。在實(shí)踐中,你可能會(huì)發(fā)現(xiàn)你能為某些函數(shù)提供強(qiáng)力保證,但是效率和復(fù)雜度的成本使得它難以支持大量的其它函數(shù)。無(wú)論何時(shí),只要你作出過(guò)一個(gè)提供強(qiáng)力保證的合理的成果,就沒(méi)有人會(huì)因?yàn)槟銉H僅提供了基本保證而站在批評(píng)你的立場(chǎng)上。對(duì)于很多函數(shù)來(lái)說(shuō),基本保證是一個(gè)完全合理的選擇。

  假如你寫(xiě)了一個(gè)根本沒(méi)有提供異常安全保證的函數(shù),事情就不同了,因?yàn)樵谶@一點(diǎn)上有罪推定是合情合理的,直到你證實(shí)自己是清白的。你應(yīng)該寫(xiě)出異常安全的代碼。除非你能做出有說(shuō)服力的答辯。請(qǐng)?jiān)俅慰紤] someFunc 的實(shí)現(xiàn),它調(diào)用了函數(shù) f1 和 f2。假設(shè) f2 根本沒(méi)有提供異常安全保證,甚至沒(méi)有基本保證。這就意味著假如 f2 發(fā)生一個(gè)異常,程序可能會(huì)在 f2 內(nèi)部泄漏資源。這也意味著 f2 可能會(huì)惡化數(shù)據(jù)結(jié)構(gòu),例如,已排序數(shù)組可能不再排序,一個(gè)正在從一個(gè)數(shù)據(jù)結(jié)構(gòu)傳送到另一個(gè)數(shù)據(jù)結(jié)構(gòu)去的對(duì)象可能丟失,等等。沒(méi)有任何辦法可以讓 someFunc 能彌補(bǔ)這些問(wèn)題。假如 someFunc 調(diào)用的函數(shù)不提供異常安全保證,someFunc 本身就不能提供任何保證。

  請(qǐng)答應(yīng)我回到懷孕的話題。一個(gè)女性或者懷孕或者沒(méi)有。局部懷孕是絕不可能的。與此相似,一個(gè)軟件或者是異常安全的或者不是。沒(méi)有像一個(gè)局部異常安全的系統(tǒng)這樣的東西。一個(gè)系統(tǒng)即使只有一個(gè)函數(shù)不是異常安全的,那么系統(tǒng)作為一個(gè)整體就不是異常安全的,因?yàn)檎{(diào)用那個(gè)函數(shù)可能發(fā)生泄漏資源和惡化數(shù)據(jù)結(jié)構(gòu)。不幸的是,很多 C++ 的遺留代碼在寫(xiě)的時(shí)候沒(méi)有留意異常安全,所以現(xiàn)在的很多系統(tǒng)都不是異常安全的。它們混合了用非異常安全(exception-unsafe)的方式書(shū)寫(xiě)的代碼。

  沒(méi)有理由讓事情的這種狀態(tài)永遠(yuǎn)持續(xù)下去。當(dāng)書(shū)寫(xiě)新的代碼或改變現(xiàn)存代碼時(shí),要仔細(xì)考慮如何使它異常安全。以使用對(duì)象治理資源開(kāi)始。這樣可以防止資源泄漏。接下來(lái),決定三種異常安全保證中的哪一種是你實(shí)際上能夠?yàn)槟銓?xiě)的每一個(gè)函數(shù)提供的最強(qiáng)的保證,只有當(dāng)你不調(diào)用遺留代碼就別無(wú)選擇的時(shí)候,才能滿足于沒(méi)有保證。既是為你的函數(shù)的客戶也是為了將來(lái)的維護(hù)人員,文檔化你的決定。一個(gè)函數(shù)的異常安全保證是它的接口的可見(jiàn)部分,所以你應(yīng)該特意選擇它,就像你特意選擇一個(gè)函數(shù)接口的其它方面。

  四十年前,到處都是 goto 的代碼被尊為最佳實(shí)踐。現(xiàn)在我們?yōu)闀?shū)寫(xiě)結(jié)構(gòu)化控制流程而奮斗。二十年前,全局可訪問(wèn)數(shù)據(jù)被尊為最佳實(shí)踐。現(xiàn)在我們?yōu)榉庋b數(shù)據(jù)而奮斗,十年以前,寫(xiě)函數(shù)時(shí)不必考慮異常的影響被尊為最佳實(shí)踐。現(xiàn)在我們?yōu)閷?xiě)異常安全的代碼而奮斗。

  時(shí)光在流逝。我們生活著。我們學(xué)習(xí)著。

  Things to Remember

  ·即使當(dāng)異常被拋出時(shí),異常安全的函數(shù)不會(huì)泄露資源,也不答應(yīng)數(shù)據(jù)結(jié)構(gòu)被惡化。這樣的函數(shù)提供基本的,強(qiáng)力的,或者不拋出保證。

  ·強(qiáng)力保證經(jīng)常可以通過(guò) copy-and-swap 被實(shí)現(xiàn),但是強(qiáng)力保證并非對(duì)所有函數(shù)都可用。

  ·一個(gè)函數(shù)通常能提供的保證不會(huì)強(qiáng)于他所調(diào)用的函數(shù)中最弱的保證。 更多文章 更多內(nèi)容請(qǐng)看C/C++技術(shù)專題  FreeBSD系統(tǒng)安全治理  linux服務(wù)器的安全性能專題,或

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 岐山县| 承德市| 延边| 多伦县| 双牌县| 合山市| 大邑县| 浦城县| 长乐市| 嵊州市| 定边县| 博爱县| 包头市| 夏邑县| 蕉岭县| 都兰县| 股票| 柏乡县| 云和县| 嘉善县| 许昌市| 岱山县| 曲松县| 军事| 青州市| 防城港市| 抚顺县| 西昌市| 临泉县| 翁牛特旗| 岑溪市| 通渭县| 双桥区| 马关县| 内丘县| 宝兴县| 扬州市| 仁布县| 沐川县| 四平市| 青岛市|