日期:2004/02/28– 2007/01/07
唐亮(千里马肝),四年游戏从业经验,曾任职于大宇软星科技(上海)有限公司任程序技术指导,现在ATI任职Engineer,主要负责ATI CrossFire在XP/Vista上的开发和维护。迄今为止主要个人作品为《阿猫阿狗2》,参与开发《汉朝与罗马》、《阿猫阿狗大作战OLG》和《仙剑奇侠传4》,主要研究方向为C++、图形渲染技术和系统架构。
blog地址:http://oiramario.cnblogs.com
本文是鄙人在2004年有感而发写下的一篇文章,当时受到他人讨论的启发,不由得兴从心头起,code从手中生。文中所介绍的技法,可能以今天的眼光来看,尚有不足之处,不过它提出了一种比较新奇的方法来解决问题,我想其思想本身才是需要注意的重点。C++是一个灵活自如的语言,身为C++饭,吾辈有责任将她发扬光大,我相信还有更多有趣的技巧,有待大家一起挖掘,希望本文能起到抛砖引玉的作用,谢谢。
任何语言,任何程序,都会有“操作失败”的情况发生。在C语言这种结构化编程语言中,处理这种情况的方法就是通过返回值来表现。在我们的游戏中,往往会因为这样那样的不小心,存在着成百上千的bug,有时候真是“修不完理还乱”,甚至有的游戏在不得已的情况下,还将bug遗留到发行后再通过patch的方式来进行弥补。可以见得,在没有一个好的方法来避免bug产生的时候,我们面对bug是多么得无奈。我们应该如何避免它,以及如何通过一个好的方法来捕捉它呢?
这实在是一个全方位的问题,例如前期进行仔细的项目分析,中期保持清晰的逻辑从而编写强壮的代码,后期通过详尽的log信息来回溯事故现场,对大量异常条件的处理,使用大量的Assert对不正确参数的断言。所有的这些方法,都是想在调试期就将问题尽量全部得排除,把bug们扼杀在摇篮里。
但是这毕竟是理想的情况,人非圣贤孰能无过?谁也不能保证自己永远处于逻辑亢奋状态,而纯理论往往讨论的是一种“乌托邦”的理想国度,但是我们不是学院派,期望能出现一种切合实际的解决方案。所以,当意识到人的惰性的必然性,那么就需要产生出一种制度来进行控管和规避。那么接下来,我将会介绍了一种新的方法,它叫作:可爆炸性析构函数。
通常我们会将某函数设计成:在操作成功的情况下返回0;当遇到错误发生时,可能会用1代表内存分配失败,用2代表文件打开失败,用3代表无法找到设备等等。虽然这样看起来很美,但是实际执行下来,通常会出现以下的几个问题:
1. 要求同步维护文档,说明各个返回值所代表的意义。
2. 在某些情况下可能需要修改其返回值所代表的意义,例如将返回值1从内存分配失败改成代表成功,这样一来,函数使用者的代码就需要修改。
3. 最重要的是,使用者完全可以不检查返回值。这样一来,如果接下来的代码依赖于该函数必须正确执行完成(通常我们都这样假设),一旦发生错误,按照顺序执行的流程,下面的代码照样会执行,从而一错再错,变成破罐子破摔。以我多年Debug的经验,通常最不好修的bug都是由此而引起的。
所以,在面向对象的语言中,如DELPHI和C++,都引入了“异常”这种概念来处理错误。当出现错误时,实现者可以选择抛出异常,这意味着如果调用者不对该异常进行处理,该异常将会按照函数调用堆栈一级级地向上抛出,直到找到对应的处理模块,如果一直抛到最外层的main函数都无法找到,则程序会立即中止。
但是以C++为例,为了处理异常,C++需要维护像是函数调用堆栈等一类的东西,这样会对程序的执行效率和空间上带来开销。对比异常所带来的好处,一般的程序大都可以忽略这种开销;但是像是一些对效率和内存空间要求很高的,如嵌入式或驱动级的程序,通常在这类程序的Coding Standard里就直接被声明为不被允许使用。所以,这时又不得不退回来重新使用返回值来处理错误。
那么我们应该怎么办呢?考虑到使用者完全可以忽略返回值的问题,于是就有了接下来的方法。
我们的口号是:强迫使用者必须检查函数的返回值,如果返回值不被检查的话,将会在运行期弹出错误以警告使用者。那么首先概略得设计一下,就是将会有一个bool变量 ,暂且称作为checked,将会在返回值构造时初始为false,只有返回值被使用者检查了,才会被置为true,然后在返回值析构时会判断checked变量是否为true,否则将立即报错。
当然,实现方法是将这种概念用“类”来表现,使用者在使用该类(以下统一称作类型T)作为函数返回值时,假设原本是以int作为返回值,则该类所表现的行为和操作,应该与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被实现为一个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
template <typename ResultType=int>
class InspectResult
{
mutable bool _checked; // 检查标志
ResultType _ret; // 返回值
public:
/*-------------------------------------------------------------
构造函数
-------------------------------------------------------------*/
InspectResult(const ResultType &ret)
: _checked(false), _ret(ret)
{
}
/*-------------------------------------------------------------
拷贝构造函数
-------------------------------------------------------------*/
InspectResult(const InspectResult &rhs)
: _checked(rhs._checked), _ret(rhs._ret)
{
// rhs的"被检查权"传递给this(下同)
rhs._checked = true;
}
/*-------------------------------------------------------------
析构函数
-------------------------------------------------------------*/
~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), 则不建议使用本函数
-------------------------------------------------------------*/
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. 矫枉不必过正,只需要对于那些“必须成功执行”或“必须对执行中产生的错误进行处理”的函数使用上面所介绍的方法。
2. 语言是表达思想的一种工具,利用C++的特点(支持运算符的重载)。我们可以实现一些在基本语言层面上无法表现的东西。
3. 通过template,我们可以用泛型实现对所有类型的支持。
4. 因为检查会有开销(至少会多出一个bool,内存对齐的情况下类型T会膨胀),通过define,我们可以在需要的时候作检查,不需要的时候则消除开销。
5. 为了贯穿思想,实现出来的东西往往不像想的时候那么简单,需要考虑很多方面。总之,思想是最重要的东西。
注:本文及代码,启发自《程序员》2002年9月中的《C++ Exception》专栏讨论,其中myan(孟岩)在与某老外通信中谈到此名词:“可爆炸性析构函数”,希望能给你带来启发或是帮助。
代码下载地址:http://oiramario.cnblogs.com/std56.rar