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