1. 程式人生 > >Guru of the Week 條款21:程式碼的複雜性(第二部分)

Guru of the Week 條款21:程式碼的複雜性(第二部分)

GotW #21 Code Complexity – Part II<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

著者:Herb Sutter

翻譯:K ][ N G of @rk™

[宣告]:本文內容取自www.gotw.ca網站上的Guru of the Week欄目,其著作權歸原著者本人所有。譯者kingofark在未經原著者本人同意的情況下翻譯本文。本翻譯內容僅供自學和參考用,請所有閱讀過本文的人不要擅自轉載、傳播本翻譯內容;下載本翻譯內容的人請在閱讀瀏覽後,立即刪除其備份。譯者

kingofark對違反上述兩條原則的人不負任何責任。特此宣告。

Revision 1.0

Guru of the Week條款21:程式碼的複雜性(第二部分)

難度:7 / 10

(大挑戰:修改GotW#20中那個只有三行程式碼的函式,使其成為強異常安全的(strongly exception-safe)。這個練習給我們上了關於異常安全性(exception-safety)的重要一課。)

[問題]

讓我們來考慮GotW#20裡的那個函式。這個函式是異常安全的(exception-safe)(再出現異常時仍能正常工作)還是異常中立的(exception-neutral)(能將所有異常都轉給呼叫者)?

請對你的回答做出解釋。如果它是異常安全的,那它是支援basic guarantee還是支援strong guarantee[譯註:basic guarantee,基本保證;strong guarantee,強力保證] 如果它不是異常安全的,那該如何對其進行修改以使其支援basic guaranteestrong guarantee

 

這裡我們假設所有被呼叫的函式都是異常安全的(即可能丟擲異常,但在丟擲異常時沒有任何副作用),並且假設所使用的任何物件(包括臨時物件在內)也都是異常安全的(即當這些物件被銷燬時,其佔用的資源也能被清理)。

 

[背景知識:Basic GuaranteeStrong Guarantee

]

 

關於basic guaranteestrong guarantee的詳細論述,請看我在C++ Report Sep/Nov/Dec 1997中的文章。簡單來說,basic guarantee保證可銷燬性(destructibility)且沒有洩漏;而strong guarantee除此之外還保證完全的commit-or-rollback(譯註:即“要麼執行,要麼不執行”的原子規則)語義。

[解答]

 

讓我們來考慮GotW#20裡的那個函式。這個函式是異常安全的(exception-safe)(再出現異常時仍能正常工作)還是異常中立的(exception-neutral)(能將所有異常都轉給呼叫者)?

[關於假設的一點說明]

[A Word About Assumptions]

 

如題所述,我們假設所有被呼叫的函式——包括流函式(stream function)在內——都是異常安全的(即可能丟擲異常,但在丟擲異常時無副作用),並且假設所使用到的所有物件——包括臨時物件在內——也都是異常安全的(即當這些物件被銷燬時,其佔用的資源也都能被清理)。

然而流(stream)卻偏偏要對此使個拌兒——這緣於其可能產生的“un-rollbackable(不可回覆)”副作用。例如,運算子<<可能會在輸出(emitting)了string的一部分之後丟擲一個異常,而此時已經被輸出的那部分是無法被“反輸出(un-emitted)”的;同樣,流(stream)的錯誤狀態也會在此時被設定(譯註:即產生了錯誤狀態的改變)。在大部分情況下,我們都忽略這些情形;本次討論的重點是考查「當函式具有兩個互不相同的副作用時,如何使函式成為異常安全的」。

[Basic Guarantee vs. Strong Guarantee]

 

由題可知,該函式滿足basic guarantee:當出現異常的時候,函式不會產生資源洩漏。

 

該函式不滿足strong guaranteestrong guarantee意即:如果函式由異常而造成失敗,程式的狀態必須仍保持不變。然而這裡的函式有兩個互不相同的副作用(正如函式的名稱所暗示的那樣):

 

1.一個“…overpaid…”訊息被送到cout

2.一個名稱字串被返回。

若考慮到第2點,那函式就可以滿足strong guarantee了,因為當異常產生時,值將不會被返回。若考慮到第1點,函式則仍然不是異常安全的,原因有兩個:

 

1.如果在欲輸出訊息的第一部分被送到cout之後、整個訊息被完全送出到cout之前的時候有異常被丟擲(比如,程式碼中的第4<<丟擲異常),那麼此時已經有一部分訊息被輸出了。[1]

2.如果訊息被成功的輸出,但在成功輸出之後函式產生異常(),那麼這個訊息也的確已經(無法挽回的)被送到cout了,儘管該函式因為一個異常而宣告失敗。

要滿足strong guarantee,函式的行為應該是:要麼兩件事(譯註:即輸出到cout和傳值返回)都圓滿完成,要麼就是遇到該函式丟擲異常,兩件事都不做。

 

我們可以達成這樣的要求嗎?下面是一種我們可能會嘗試的方式(不妨稱其為第一次嘗試):

這段程式碼還算不壞。應當注意到,為了讓整個string只使用一個<<呼叫,我們用換行符代替了endl(雖然兩者並不完全等同)。(當然,這樣做並不能保證「底層的流系統本身不會在對訊息施以寫操作的時候失敗,從而造成不完整的輸出」——但我們在這樣的高層次已經做了力所能及的努力。)

[一個稍微有點揪心的問題]

[A Little Bothersome Issue]

 

到現在,我們仍然有一個微小的瑕疵,它如下面的使用者程式碼(client code)所示:

由於函式的結果採用了return by value(傳值返回)方式返回,因此String的拷貝建構函式(copy constructor)被喚起;拷貝賦值運算子(copy assignment operator)也被喚起,用來將結果拷貝到theName。如果這兩個拷貝操作中有任一個失敗了,那麼函式的副作用就已發生效應(因為訊息已被完全輸出,返回值也已被完全構造好了),而其結果也就無法挽回的丟失了(噢歐!)。

我麼能否做得更好一些?可以通過「避免拷貝操作」來避免這個問題嗎?這即是說,我們讓函式接受一個non-constString之引用引數,並將返回值放在這個引數中:

然而不幸的是,對r的賦值仍然可能失敗,這將造成其中一個副作用被完成而另一個沒被完成。最關鍵的問題在於,這第二次嘗試並沒有給我們帶來多大好處。

於是我們可能會嘗試著使用auto_ptr來返回結果(不妨把這一次稱為第三次嘗試):

這正是解題的訣竅之所在——我們有效的隱藏了產生第二個副作用(返回值)的操作,同時也保證了「在第一個副作用(列印訊息)被完成後,只使用不丟擲異常的(nonthrowing)操作把結果安全的返回給函式呼叫者」。那麼這樣做的代價呢?正如在實現強異常安全性(strong exception safety)時經常發生的那樣,這種強安全性以效率為代價——我們使用了額外的動態記憶體分配。

[異常安全性和多重副作用]

[Exception Safety and Multiple Side Effects]

 

從本次討論可以看出,在第三次嘗試中有可能以基本的commit-or-rollback語義來完成那兩個副作用(與流有關的那個除外)。究其原因,是因為這兩個副作用看起來應該可以通過某種技術而被自動完成——這即是說,為兩個副作用所做的全部“真正的”工作能夠以這樣一種方式被完成:即可見的副作用能夠只通過不丟擲異常的(nonthrowing)操作來完成。

儘管這一次我們還算比較幸運,但情況並不總是那麼簡單:要編寫強異常安全的函式,且讓該函式包含兩個或多個能被自動完成的、互不相關的副作用(例如,當兩個副作用中一個向cout送訊息,另一個向cerr送訊息,那會怎麼樣呢?)——這是不可能的,因為strong guarantee要求在出現異常時“程式的狀態保持不變”;換句話說,意即只要有異常出現就不能有副作用產生。通常當你遇到兩個副作用無法被自動完成的情況時,要實現強異常安全性的唯一方法就是把函式分成兩個能自動完成副作用的函式。

本期GotW意在描述3個重點:

1.要對強異常安全性提供保證,經常(但並不總是)需要你以放棄一部分效能為程式碼。

2.如果一個函式含有多重的副作用,那麼其總是無法成為強異常安全的。此時,唯一的方法就是將函式分為幾個函式,以使得每一個分出來的函式之副作用能被自動完成。

3.並不是所有函式都需要具有強異常安全性。本條款中的原始程式碼和第一次嘗試的程式碼已經能夠滿足basic guarantee了。在許多情況下,第一次嘗試的程式碼已經足夠好用,能夠將副作用在異常情況下發生的可能性減到最小,而並不需要像第三次嘗試那樣非要損失一定的效能。

[又及:流和副作用]

[Postscript: Streams and Side Effects]

 

正如本條款所示,我們對「被呼叫的函式沒有副作用」之假設並不完全是真實的情況。特別的,我們根本無法保證「流在輸出一部分結果之後不會突然失敗」。這意味著,我們無法在執行流輸出的函式中實現真正的commit-or-rollback語義——至少在這些標準流中是不可能的。另外還有一點是,如果流輸出失敗了,流的狀態也將會改變。目前我們不去檢查這種情況,也不嘗試對其予以恢復——但我們仍可以對函式進行修改,以使其能夠捕獲由於流而引起的異常,並在重新向呼叫者丟擲異常之前重置couterror flags

[1]:如果你覺得「擔心一條訊息是否能夠被完全施以cout操作」這樣的事情多少有點過分學究的味道,那麼可以說你的想法並不錯。在這裡,可能沒有人會關心這個。然而任何試圖完成兩個副作用的函式都是遵循著同樣的原理——這也就是為什麼我們後續的討論還有意義的原因。

(完)