常言道,要学打架,先学挨打。想打人,就要有被人打的觉悟。写程序,自然也要有程序运行出错的觉悟。游戏编程,就从错误处理开始。
什么是错误处理?我想应该就是发现错误、解决错误的过程。什么是错误?比如我调用某个函数,那总会有一个期望的结果。但在实际运行的时候,有可能与期望的结果相符合,也有可能与期望的结果不符合。后面一种情况,就算作是“错误”。一个函数要么有报告错误的责任,要么确保自己不会出现错误(没有绝对的不出错,但有些函数比如std::swap等,除了堆栈溢出、硬件故障之外几乎没有出错的可能了,因此认为它不会出错。但凡是涉及到new操作的,由于内存分配可能失败,所以都是有可能出错的)。如果两者都没有,那我们的程序就危险了。
听别人的总结,报告错误有三种方法。一是函数直接返回错误代码;二是把错误代码保存起来,让外界用GetLastError之类的函数去获取;三是抛出一个异常。我发现其实还有第四种办法:传入一个回调函数,在错误时调用它。
举例:COM风格的代码喜欢用HRESULT来表示各种错误。Windows API中的多数函数,以及OpenGL都喜欢GetLastError这样的方式。C++标准的new则又用异常来表示内存分配失败。C++还有一个函数set_unexcepted,在产生未声明的异常时调用回调函数。可见,四种方法在C/C++的经典代码中都有出现,最后搞得不知道该用哪种好了。
从性能上讲,抛出异常的开销似乎要大一点,但我这里说“似乎”大一点,这个问题我不确定。异常处理有代价,错误代码的判断同样有代价,两种代价孰轻孰重,我想或许还要看看调用堆栈的深度。每次调用函数都去判断错误代码的话,效率也不见得高(调用层次很深的话,层层判断是很麻烦的。并且大量判断会对CPU指令流水线造成压力)。
错误代码和GetLastError两种方式比较相似,都是需要在函数返回之后进行判断。有人说GetLastError会存在线程安全问题,不过其实这可以用TLS(线程局部存储)去解决。但是如果不小心,函数的返回值被忽略了,则错误可能不会被及时处理,并且编译器不会发出任何有用的警告。
回调函数的性能看起来是最高的,但是它也不像想象的那么好——它的控制权太弱了。其它三种方式都是函数返回之后再进行处理,程序控制要灵活得多。但是回调函数的方式,发生错误后进入回调函数时,产生错误的那个函数并没有返回。此时要再想跳转到别的位置去执行代码,总会觉得力不从心。
或许应该更倾向于用异常来报告错误。原因很简单:代码简短(判断语句少),这个理由就足够了。据说使用异常的代码也比使用返回值的代码更容易进行白盒测试。另外,异常不像返回值那样容易被忽略,也不像回调函数那样控制力不足。如果从JAVA来看的话,采用返回值来报告错误的设计貌似已经不多见了。看来在不考虑性能的情况下,使用异常来报告错误应该是目前的最佳选择。(前面也说了,即使考虑性能,估计仍然是最佳选择)
由于C++不是自动垃圾回收的,在异常机制面前,资源的分配释放显得尤为重要。异常安全是值得考虑的问题(搜索一下“异常安全”)。尽可能的使用RAII的方法会有帮助。
此外还有一个问题,就是链接。假设两个模块采用不同的编译器(或者相同的编译器,但是设置不一样),则一个模块所抛出的异常可能无法被另一个模块所识别。为此,需要确保程序的所有模块都采用相同编译器、相同设置去编译。这在一定程度上限制了使用范围。
虽然推荐使用异常来报告错误,但也要设法在一定程度上减少异常的数量,实际上就是减少错误的出现机会。举例来说,走路的时候,如果前方可能有个坑,应该怎么办呢?方案1:走一步试试,有坑就抛出异常,没坑就啥事儿没有。方案2:先仔细查看是否真的有坑,然后决定是否要踏进去。选择哪种方案取决于“坑存在的可能性”。如果坑存在的可能性较大,则方案1就是不明智的(如果真的踩中了坑,则代价惨重)。如果坑存在的可能性小,则方案2就是不明智的(瞻前顾后,畏首畏尾)。需要根据情况选择合适的处理方式。
小结:1. 使用异常来报告错误。2. 注意异常安全,尽可能的使用RAII。3. 如果某个错误发生的机率较高,则尽可能进行主动的检查,而不要让异常频繁的被抛出。“异常仅仅在异常情况下才出现”。
说明一下,此文拷贝自http://hi.baidu.com/zhlz01/item/eab08bcce2699d1cb77a2427
原文的作者就是我本人,因此算作是原创,不是转载。这是我在2010年时候写的一篇老文。