資源管理(Managing Resources)始終是C++語言一個十分重要的話題,也是程序員在使用C++編寫代碼時需要十分注意的地方,稍有不慎就可能導致資源泄漏(resource leak),在筆者以往的編程實踐中就經常遇到此類問題。而“resource acquisition in initialization”是一種處理此類問題的較好方法,這是Stroustrup博士在演講中所提到的。關于這一點,在D&E [1] 以及相關論文 [2] 中也有所提及。該方法使用一個類來代表對資源的管理邏輯,將指向資源的句柄(指針或引用)通過構造函數傳遞給該類,在該類的實例被銷毀時由析構函數負責釋放資源。可以在創建該類的實例之前申請資源,也可以在構造時由該類負責申請資源。這種方式的基本思路是,不論異常是否發生,由于C++的語言機制保證了,一定會調用位于當前范圍(scope)的對象的析構函數,所以只要在析構函數中加入資源回收的代碼,那么這些代碼總是會被執行的。這種方法的好處在于,由于將資源回收的邏輯通過單獨的類從原有代碼中剝離出來,使程序員總是不會遺漏,思路也變得清晰。
以筆者之見,“resource acquisition in initialization”技法,在處理有關異常的問題時,其適用范圍還可以擴展。不單涉及資源管理,只要當scope里存在類似于fopen/fclose、new/delete這樣的對稱操作時,就可以酌情考慮采用這種方法。避免資源泄漏固然是頭等大事,應該列于基本保證(basic guarantee)之內。但某些對稱操作,如果會影響程序的正常執行甚至是產生致命錯誤(fatal error)的話,那么也是不可輕視的。而對于一個軟件而言,杜絕fatal error應該也算是一個basic guarantee www.yzyedu.com
以下是筆者在實踐中遇到的一個例子。有意思的是,這個例子是本人在所負責的軟件模塊中首次決定使用異常處理機制所遇到的,可謂出師不利:)經過簡化后的代碼基本如下:
void f(C *pObj)
{
pObj->Editable(true);
// do some work with object
pObj->Editable(false);
}
函數f的作用是對傳入其scope的pObj所指對象進行某些操作。當最初引入異常處理機制時,代碼改變如下:
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);
}
此處再度throw是為了使f的調用者能有機會做一些處理,這是在設計時所需要的。類似這樣的做法在一般的異常處理程序中是很常見的,但是筆者的疏忽卻另自己吃了大虧。雖然,從經過簡化的代碼中很容易看出破綻來,但是由于當時經驗不足,加之程序邏輯復雜,直到測試時通過最終的用戶界面才發現了問題。經過幾個小時的艱苦調試,最后發現問題出在f函數。事實上,函數f的行為隱含了一個斷言(assert),即:f保證不對pObj所指對象的不可編輯狀態做出更改,在調用f前對象是不可編輯的,調用后仍然如此。而在上述程序中,當異常發生時,由于沒有執行pObj->Editable(false)這一語句,所以導致程序最終出錯,而且這一錯誤隱蔽在無數代碼中,異常情況又并非每次都發生,使筆者在調試時定位錯誤花費了不少精力。
在找到了錯誤根源之后,筆者采用了如下的補救措施,這一做法被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; www.yzjxsp.com
}
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); www.jokedu.com
// also may be operations according to ctor
}
operator C* () {return _pObj;}
};
C_Handle的構造函數和析構函數中,對_pObj所指對象的操作是成對出現的,所以在以后擴展時也不容易出錯。此時f函數的代碼也變得簡潔了許多 www.liuhebao.com
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內出現針對某個對象的若干對稱操作,而在彼此對稱的兩組操作間可能拋出異常以破壞這種對稱性,并且這種破壞將導致與該scope相關的某種斷言為假時,就可以考慮使用類似于Stroustrup博士在處理資源管理問題時所推薦的這種“resource acquisition in initialization”技法。甚至可以認為,資源管理中發生的例子是這里所提到的情形的一個特例。在資源管理方面的另一個很典型的例子是智能指針(Smart Pointer)[3]。 www.szfuao.com
此外,對于這種方法可能存在的一個缺點是,或許會出現很多類似C_Handle這樣的規模很小的輔助類。對此我們可以這樣考慮:如果這些類不是很多,那么它們的存在將會給代碼的編寫和維護帶來好處(想想前面提到的維護一致性的代價),并且如果程序中多處出現這樣的類似情況時,這些類就可以復用了。而當類的數目多到讓你無法容忍時,就該考慮一下其中某些類存在的必要性了,畢竟并非程序的每處都要使用異常,也許你的設計本身存在問題。此外,如果這些輔助類彼此有關聯則可以考慮引入繼承體系,而如果它們之間的行為及其相似,使用模板機制(template)進行泛化,也不失為一個優化策略。