可爆炸性析構函數(shù)
日期:2004/02/28– 2007/01/07
本文首次刊登于《游戲創(chuàng)造》,現(xiàn)開放與大家共享,轉載請注明出處。
下載地址
作者介紹
唐亮(千里馬肝),四年游戲從業(yè)經驗,曾任職于大宇軟星科技(上海)有限公司任程序技術指導,現(xiàn)在ATI任職Engineer,主要負責ATI CrossFire在XP/Vista上的開發(fā)和維護。迄今為止主要個人作品為《阿貓阿狗2》,參與開發(fā)《漢朝與羅馬》、《阿貓阿狗大作戰(zhàn)OLG》和《仙劍奇?zhèn)b傳4》,主要研究方向為C++、圖形渲染技術和系統(tǒng)架構。
blog地址:http://oiramario.cnblogs.com
開場白
本文是鄙人在2004年有感而發(fā)寫下的一篇文章,當時受到他人討論的啟發(fā),不由得興從心頭起,code從手中生。文中所介紹的技法,可能以今天的眼光來看,尚有不足之處,不過它提出了一種比較新奇的方法來解決問題,我想其思想本身才是需要注意的重點。C++是一個靈活自如的語言,身為C++飯,吾輩有責任將她發(fā)揚光大,我相信還有更多有趣的技巧,有待大家一起挖掘,希望本文能起到拋磚引玉的作用,謝謝。
前言
任何語言,任何程序,都會有“操作失敗”的情況發(fā)生。在C語言這種結構化編程語言中,處理這種情況的方法就是通過返回值來表現(xiàn)。在我們的游戲中,往往會因為這樣那樣的不小心,存在著成百上千的bug,有時候真是“修不完理還亂”,甚至有的游戲在不得已的情況下,還將bug遺留到發(fā)行后再通過patch的方式來進行彌補??梢砸姷?,在沒有一個好的方法來避免bug產生的時候,我們面對bug是多么得無奈。我們應該如何避免它,以及如何通過一個好的方法來捕捉它呢?
這實在是一個全方位的問題,例如前期進行仔細的項目分析,中期保持清晰的邏輯從而編寫強壯的代碼,后期通過詳盡的log信息來回溯事故現(xiàn)場,對大量異常條件的處理,使用大量的Assert對不正確參數(shù)的斷言。所有的這些方法,都是想在調試期就將問題盡量全部得排除,把bug們扼殺在搖籃里。
但是這畢竟是理想的情況,人非圣賢孰能無過?誰也不能保證自己永遠處于邏輯亢奮狀態(tài),而純理論往往討論的是一種“烏托邦”的理想國度,但是我們不是學院派,期望能出現(xiàn)一種切合實際的解決方案。所以,當意識到人的惰性的必然性,那么就需要產生出一種制度來進行控管和規(guī)避。那么接下來,我將會介紹了一種新的方法,它叫作:可爆炸性析構函數(shù)。
現(xiàn)場
通常我們會將某函數(shù)設計成:在操作成功的情況下返回0;當遇到錯誤發(fā)生時,可能會用1代表內存分配失敗,用2代表文件打開失敗,用3代表無法找到設備等等。雖然這樣看起來很美,但是實際執(zhí)行下來,通常會出現(xiàn)以下的幾個問題:
1. 要求同步維護文檔,說明各個返回值所代表的意義。
2. 在某些情況下可能需要修改其返回值所代表的意義,例如將返回值1從內存分配失敗改成代表成功,這樣一來,函數(shù)使用者的代碼就需要修改。
3. 最重要的是,使用者完全可以不檢查返回值。這樣一來,如果接下來的代碼依賴于該函數(shù)必須正確執(zhí)行完成(通常我們都這樣假設),一旦發(fā)生錯誤,按照順序執(zhí)行的流程,下面的代碼照樣會執(zhí)行,從而一錯再錯,變成破罐子破摔。以我多年Debug的經驗,通常最不好修的bug都是由此而引起的。
所以,在面向對象的語言中,如DELPHI和C++,都引入了“異?!边@種概念來處理錯誤。當出現(xiàn)錯誤時,實現(xiàn)者可以選擇拋出異常,這意味著如果調用者不對該異常進行處理,該異常將會按照函數(shù)調用堆棧一級級地向上拋出,直到找到對應的處理模塊,如果一直拋到最外層的main函數(shù)都無法找到,則程序會立即中止。
但是以C++為例,為了處理異常,C++需要維護像是函數(shù)調用堆棧等一類的東西,這樣會對程序的執(zhí)行效率和空間上帶來開銷。對比異常所帶來的好處,一般的程序大都可以忽略這種開銷;但是像是一些對效率和內存空間要求很高的,如嵌入式或驅動級的程序,通常在這類程序的Coding Standard里就直接被聲明為不被允許使用。所以,這時又不得不退回來重新使用返回值來處理錯誤。
那么我們應該怎么辦呢?考慮到使用者完全可以忽略返回值的問題,于是就有了接下來的方法。
分析
我們的口號是:強迫使用者必須檢查函數(shù)的返回值,如果返回值不被檢查的話,將會在運行期彈出錯誤以警告使用者。那么首先概略得設計一下,就是將會有一個bool變量 ,暫且稱作為checked,將會在返回值構造時初始為false,只有返回值被使用者檢查了,才會被置為true,然后在返回值析構時會判斷checked變量是否為true,否則將立即報錯。
當然,實現(xiàn)方法是將這種概念用“類”來表現(xiàn),使用者在使用該類(以下統(tǒng)一稱作類型T)作為函數(shù)返回值時,假設原本是以int作為返回值,則該類所表現(xiàn)的行為和操作,應該與int“完全一致”。
而“檢查”的概念,我認為在語言表達中,即是:
T::operator == ()
T::operator != ()
T::operator int ()
為了保證“傳遞性”,當T的實例x作為返回值返回時,有以下二條應該被遵守:
1. 如果x不被檢查,則x會在析構時報錯
2. 當y=x時,x會被認為已經將“責任”傳遞給了y,x解除責任,而y則有義務同上
整理
1. 因為T將用來代替int,則T應有operator int()
2. 為了與int的行為保持一致性,T應該重載operator ==和operator !=
3. 為了支持所有返回值的類型,所以T被實現(xiàn)為一個template
4. 為了只在DEBUG期進行,避免RELEASE期的開銷,則有
enum ErrType
{
Success,
Fail
};
#ifdef CHECK_RESULT
typedef InspectResult<> ResultInt;
typedef InspectResult<ErrType> ResultEnum;
#else
typedef int ResultInt;
typedef ErrType ResultEnum;
#endif
ResultInt Func1()
{
ResultInt ret = 1;
return ret;
}
ResultEnum Func2()
{
ResultEnum ret = Success;
return ret;
}
5. 如果返回值是一個“大的類型”如string,為避免臨時變量產生導致開銷,以及不同的調用方式和習慣的支持,則有const string &result()const
實現(xiàn)
template <typename ResultType=int>
class InspectResult
{
mutable bool _checked; // 檢查標志
ResultType _ret; // 返回值
public:
/*-------------------------------------------------------------
構造函數(shù)
-------------------------------------------------------------*/
InspectResult(const ResultType &ret)
: _checked(false), _ret(ret)
{
}
/*-------------------------------------------------------------
拷貝構造函數(shù)
-------------------------------------------------------------*/
InspectResult(const InspectResult &rhs)
: _checked(rhs._checked), _ret(rhs._ret)
{
// rhs的"被檢查權"傳遞給this(下同)
rhs._checked = true;
}
/*-------------------------------------------------------------
析構函數(shù)
-------------------------------------------------------------*/
~InspectResult()
{
// 如果沒有檢查過返回值則報錯
assert(_checked);
}
/*-------------------------------------------------------------
operator =
-------------------------------------------------------------*/
InspectResult & operator = (const InspectResult &rhs)
{
_checked = rhs._checked;
_ret = rhs._ret;
rhs._checked = true;
return *this;
}
/*-------------------------------------------------------------
重載operator = (const ResultType &ret)
以支持InspectResult與ResultType之間的直接操作
因為ctor是non-explicit, 以避免臨時變量的產生
-------------------------------------------------------------*/
InspectResult & operator = (const ResultType &ret)
{
_checked = false;
_ret = ret;
return *this;
}
/*-------------------------------------------------------------
所謂"返回值必須檢查", 在此我視為operator ==動作
注意這里因為值已被檢查, 所以this和rhs都將視為已檢查
-------------------------------------------------------------*/
bool operator == (const InspectResult &rhs)const
{
_checked = rhs._checked = true;
return _ret == rhs._ret;
}
/*-------------------------------------------------------------
所謂"返回值必須檢查", 在此我視為operator ==動作
注意這里因為值已被檢查, 所以_checked = True
-------------------------------------------------------------*/
bool operator == (const ResultType &ret)const
{
_checked = true;
return _ret == ret;
}
/*-------------------------------------------------------------
operator != (const InspectResult &rhs)const
-------------------------------------------------------------*/
bool operator != (const InspectResult &rhs)const
{
return !(*this == rhs);
}
/*-------------------------------------------------------------
operator != (const ResultType &ret)const
-------------------------------------------------------------*/
bool operator != (const ResultType &ret)const
{
return !(*this == ret);
}
/*-------------------------------------------------------------
operator ResultType
-------------------------------------------------------------*/
operator ResultType ()const
{
_checked = true;
return _ret;
}
/*-------------------------------------------------------------
如果ResultType是一個大的class(如string)
這時使用operator ResultType會有臨時變量的開銷
但若ResultType是內建的類型(如char), 則不建議使用本函數(shù)
-------------------------------------------------------------*/
const ResultType &result()const
{
_checked = true;
return _ret;
}
};
enum ErrType
{
Success,
Fail
};
#define CHECK_RESULT
#ifdef CHECK_RESULT
typedef InspectResult<> ResultInt;
typedef InspectResult<ErrType> ResultEnum;
#else
typedef int ResultInt;
typedef ErrType ResultEnum;
#endif
結論
1. 矯枉不必過正,只需要對于那些“必須成功執(zhí)行”或“必須對執(zhí)行中產生的錯誤進行處理”的函數(shù)使用上面所介紹的方法。
2. 語言是表達思想的一種工具,利用C++的特點(支持運算符的重載)。我們可以實現(xiàn)一些在基本語言層面上無法表現(xiàn)的東西。
3. 通過template,我們可以用泛型實現(xiàn)對所有類型的支持。
4. 因為檢查會有開銷(至少會多出一個bool,內存對齊的情況下類型T會膨脹),通過define,我們可以在需要的時候作檢查,不需要的時候則消除開銷。
5. 為了貫穿思想,實現(xiàn)出來的東西往往不像想的時候那么簡單,需要考慮很多方面。總之,思想是最重要的東西。
注:本文及代碼,啟發(fā)自《程序員》2002年9月中的《C++ Exception》專欄討論,其中myan(孟巖)在與某老外通信中談到此名詞:“可爆炸性析構函數(shù)”,希望能給你帶來啟發(fā)或是幫助。
代碼下載地址:http://oiramario.cnblogs.com/std56.rar