有關C++異常安全的一點個人想法 (轉)

worldblog發表於2007-12-15
有關C++異常安全的一點個人想法 (轉)[@more@]

本人有幸於10月26日下午在清華大學的建築報告館聆聽了C++之父Bjarne Stroustrup博士的講座。精彩的演講,引人入勝的內容,著實令人難忘。同時,我也親身感受到了大師在回答場下聽眾問題時的平易近人和循循善誘。本次講座的主題是Exception Safety,以下是我在聽完講座後,結合自己的一點切身體會,對C++ Exception Handling及相關內容的一點思考,如有考慮不周之處還請大家指正。

在C++語言中,資源管理(Managing Res)始終是一個十分重要的話題,也是員在使用C++語言編寫程式碼時需要十分注意的地方,稍有不慎就可能導致資源洩漏,在我以往的實踐中就經常遇到此類問題。而“resource acquisition in initialization”是一種處理此類問題的較好方法,這是Stroustrup博士在演講中所提到的。關於這一點,在博士所著的D&E以及相關論文中也有所提及。該方法使用一個類來代表對資源的管理邏輯,將指向資源的控制程式碼(指標或引用)透過ctor傳遞給該類,在該類的例項被銷燬時由dtor負責釋放資源。可以在建立該類例項之前申請資源,也可以在構造時由該類的ctor負責申請資源。這種方式的基本思路是,不論exception是否發生,由於C++的語言機制保證了,一定會位於當前pe的的dtor,所以只要在dtor中加入資源回收的程式碼,那麼這些程式碼總是會被的。這種方法的好處在於,由於將資源回收的邏輯透過單獨的類從原有程式碼中剝離出來,使程式設計師總是不會遺漏,思路也變得清晰。

我覺得,“resource acquisition in initialization”技法,在處理有關exception的問題時,其適用範圍還可以擴充套件。不單涉及資源管理,只要當scope裡存在類似於fopen/fclose、new/delete這樣的對稱操作時,就可以酌情考慮採用這種方法。避免資源洩漏固然是頭等大事,應該列於basic guarantee之內。但某些對稱操作,如果會影響程式的正常執行甚至是產生al error的話,那麼也是不可輕視的。而對於一個而言,杜絕fatal error應該也算是一個basic guarantee了。

以下是我在實踐中遇到的一個例子。有意思的是,這個例子是本人在所負責的軟體模組中首次決定使用exception handling所遇到的,可謂出師不利:)經過簡化後的程式碼基本如下:
void f(C *pObj)
{
 pObj->Editable(true);
 // do some work with
 pObj->Editable(false);
}
f的作用是對傳入其scope的pObj所指物件進行某些操作。當最初引入exception handling時,程式碼改變如下:
void f(C *pObj)
{
 pObj->Editable(true);
 try {
 // do some work with object
 // may cause exception
 } catch(...)
 {
 // do some thing and rethrow
 throw;
 }
 pObj->Editable(false);
}
此處rethrow是為了使f的呼叫者能有機會做一些處理,這是在設計時所需要的。類似這樣的做法在一般的exception處理程式中是很常見的,但是我的疏忽卻另自己吃了大虧。雖然,從經過簡化的程式碼中很容易看出破綻來,但是由於當時不足,加之程式邏輯複雜,直到測試時透過最終的GUI才發現了問題。經過幾個小時的艱苦,最後發現問題出在f函式。事實上,函式f的行為隱含了一個assert,即:f保證不對pObj所指物件的不可編輯狀態做出更改,在呼叫f前物件是不可編輯的,呼叫後仍然如此。而在上述程式中,當exception發生時,由於沒有執行pObj->Editable(false)這一語句,所以導致程式最終出錯,而且這一錯誤隱蔽在無數程式碼中,exception情況又並非每次都發生,使我在除錯時定位錯誤花費了不少精力。
在找到了錯誤根源之後,我採用瞭如下的補救措施,這一做法被Stroustrup博士稱為naive use:
void f(C *pObj)
{
 pObj->Editable(true);
 try {
 // do some work with object
 // may cause exception
 } catch(...)
 {
 // do some thing and rethrow
 pObj->Editable(false);
 throw;
 }
 pObj->Editable(false);
}
在寫下這段程式碼的時候,直覺告訴自己,這裡存在Bed Smell,但是由於時間緊迫,所以當時暫且容忍了這種Quick and Dirty的做法。正如Stroustrup博士在D&E中所指出的,這種做法的缺點是囉嗦,冗長乏味,而且可能代價昂貴。仔細分析一下,就可以看出這裡存在的潛在危險:兩處pObj->Editable(false)事實上是重複程式碼,我們需要始終保持兩處程式碼的一致性,如果一段時間後,需要在pObj中增加一種類似Editable的屬性,這種一致性的保持,就需要延續,很難保證不會再次疏忽。
於是,遵照大師的教誨,我增加了一個輔助類,程式碼如下:
class C_Handle {
 C* _pObj;
public:
 C_Handle(C* pObj) {
 _pObj = pObj;
 _pObj->Editable(true);
 // may be other operations
 }
 ~C_Handle() {
 _pObj->Editable(false);
 // also may be operations according to ctor
 }
 operator C* () { return _pObj; }
};
C_Handle的ctor和dtor中,對_pObj所指物件的操作是成對出現的,所以在以後擴充套件時也不容易出錯。此時f函式的程式碼也變得簡潔了許多:
void f(C* pObj)
{
 C_Handle ch(pObj);
 try {
 // do some work with object
 // may cause exception
 } catch(...)
 {
 // do some thing and rethrow
 throw;
 }
}

個人覺得,這種技法應該具有普遍意義。現總結如下:在某個scope內出現針對某個物件的若干對稱操作,而在彼此對稱的兩組操作間可能丟擲exception以破壞這種對稱性,並且這種破壞將導致與該scope相關的某種assert為false時,就可以考慮使用類似於Stroustrup博士在處理資源管理問題時所推薦的這種“resource acquisition in initialization”技法。甚至可以認為,資源管理中發生的例子是這裡所提到的情形的一個特例。在資源管理方面的另一個很典型的例子是Smart Pointer。

此外,對於這種方法可能存在的一個缺點是,或許會出現很多類似C_Handle這樣的規模很小的輔助類。對此我們可以這樣考慮:如果這些類不是很多,那麼它們的存在將會給程式碼的編寫和維護帶來好處(想想前面提到的維護一致性的代價),並且如果程式中多處出現這樣的類似情況時,這些類就可以複用了。而當類的數目多到讓你無法容忍時,就該考慮一下其中某些類存在的必要性了,畢竟並非程式的每處都要使用exception handling,也許你的設計本身存在問題。此外,如果這些輔助類彼此有關聯則可以考慮引入繼承體系,而如果它們之間的行為及其相似,使用template機制進行泛化,也不失為一個策略。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993796/,如需轉載,請註明出處,否則將追究法律責任。

相關文章