C++的错误和异常处理分析

2010-08-28 10:49:27来源:西部e网作者:

    何时使用异常?

  一个简单的回答是:“当异常的语义和性能要求都恰当的时候。”

  一个经常被提到的方法是这样问自己:“这是一个例外(或者意外的)情形吗?”这个方法貌似挺吸引人,但是通常只会导致错误答案。对一个人来说是“异常”的情形对另一个人却“正常”:当你真正仔细考虑这句话时,就发现无法作出区分,这句话根本帮不了你。毕竟,如果你检查了某个错误条件,就意味着你认为它会发生,否则你的检查不过是垃圾代码。

  一个更合适的问法是:“这里需要栈展开吗?”由于异常处理实际上几乎都意味着比正常流程代码要慢,还应该问自己:“这里负担得起栈展开的代价吗?”比如,正在做的一个要花很长时间的计算,并且周期性地检测用户是否按下了取消键。抛出异常可以优雅地取消操作。另一方面,在这个计算的内部循环中抛出并捕获处理异常可能就不恰当,这么做可能导致严重的性能下降。前述内容包含这样一个原则:对于时间关键的代码,抛出异常才是一种“异常”的做法,而不是常规.

  如何设计异常类?

  1. 从std::exception派生异常类。除了一些非常罕见的情况,例如负担不了需函数的开销。把std::exception作为异常基类是合理的,当它被广泛使用后,将允许程序员捕获任何异常而不必使用catch(...).更多关于catch(...)的内容,请看后文。

  2. 使用虚拟继承。这个深刻的洞察力来自Andrew Koenig. 当抛出的一个异常是从多个基类派生,并且这些基类有共同的部分,catch点就会遇到歧义问题,从异常基类虚拟继承可以防止这种歧义问题:

#include <iostream>
struct my_exc1 : std::exception { char const* what() const throw(); };
struct my_exc2 : std::exception { char const* what() const throw(); };
struct your_exc3 : my_exc1, my_exc2 {};

int main()
{
try { throw your_exc3(); }
catch(std::exception const& e) {}
catch(...) { std::cout << "whoops!" << std::endl; }
}

  上面的程序将打印出“whoops” ,因为C++运行时刻无法决定用那个exception实例去匹配第一个catch.(秃子:我的建议是这里最好别使用多重继承)

  3. 不要内嵌std::string对象或者其他拷贝构造可能抛出异常的数据成员、基类。在上述点抛出异常将导致直接调用std::terminate().让基类或数据成员的默认构造函数可能抛出异常也是同样糟糕的主意,你本来是打算通过一个包含对象构造的throw表达式报告异常, 程序却无谓地中止了:

throw some_exception();

  当发生异常拷贝时,有几种方法避免复制字符串对象,例如在异常对象中嵌入一个定长存储区,或者通过引用计数来管理字符串。不过,在采用这些方法前,先考虑考虑下一条。

  4. 只在确实需要的时候才格式化what()返回的信息。格式化是一个典型的内存相关的操作,有可能抛出异常。最好把格式化推迟到栈展开之后,因为栈展开可能释放某些资源。对what()函数用catch(...)块加以保护是一个好主意,这样你就可以在格式化抛出异常时有了一个退路。

  5. 不要太在意what()的信息。在异常抛出点,对程序员来说,这是给出错误信息的好机会,但是你未必能够把相关信息组合成用户可以理解的形式。国际化就一个典型的情况。Peter Dimov给出了良好建议:建一个错误信息格式化的表格,把what()的字符串作为这个表的键。当标准库抛出异常时,如果我们只能获得其标准的what()字符串……

  6. 在异常类的public接口中暴露导致错误的有关信息。返回固定信息的what()意味着你忽视了暴露信息,而用户可能需要提供相关信息。例如,你的异常想报告数字范围错,报错的代码应该能够透过异常的公共接口让异常包含导致问题的那个变量值。如果你只是在what()中以文本方式表现这些数字,那些需要根据信息做更多(或更少)处理的程序员日子将很难过。

  7. 如果可能,让你的异常类对两次析构免疫。几款流行的编译器偶尔会使异常对象被销毁两次。如果你能采取措施防御危害(比如,把释放的指针置零)就可以使代码更健壮。

  如何处理程序员犯错?

  作为开发者,如果我违反了所使用库的某个前条件,我不希望栈展开。我希望的是core dump或者等价物—一个能精确地在问题发生点检查程序状态的方法。这通常意味着assert()或者其他类似的东西。

  有时候为用户提供可以应付任意误用的强健的API是有必要的,但这样通常要付出不菲的代价。比如,一个常见需求是跟踪客户使用的每一个对象,从而可以验证合法性。如果你需要这种保护,通常是在一个简单API上再封装一层来实现。尽管你做得小心翼翼,有强健承诺的API也只能防御某些而不是所有会导致灾难的误用。客户也开始依赖那些保护并且所依赖的保护也将增长到接口保护不到的部分。

  windows开发者请注意:当你使用assert()时,大部分Windows编译器实际上都是抛出异常,并且被本地截获,这很不幸。事实上,截获的错误经常是段访问失败或者除零错。当你使用JIT(Just In Time)调试时这是个问题,这意味着在在唤醒调试器之前已经异常栈展开了,因为catch(…)将捕获这个异常,其实这个并非C++异常。幸运的是,有一个鲜为人知的简单办法可以处理:

extern "C" void straight_to_debugger(unsigned int, EXCEPTION_POINTERS*)
{
throw;
}
extern "C" void (*old_translator)(unsigned, EXCEPTION_POINTERS*)= _set_se_translator(straight_to_debugger);

  这个方法无法应付在catch块中(或者catch块调用的函数中)抛出结构化异常的情况,但它确实可以解决绝大多数JIT导致的问题。

  该如何处理异常?

  压根就不处理异常一般是处理异常的最好办法。如果你让异常穿越你的代码,并且在析构函数中做清理工作,代码会更干净。

  尽可能避免catch(…)

  很不幸,其他非Windows操作系统一样会把非C++异常(例如线程中止)卷入到C++异常机制中去,而且,有时候也没有类似上面提到的_set_se_translator这样的hack手法加以解决。我们通常在析构函数或者catch块中做合理操作来维持系统的不变式,这通常是安全的。然而catch(...)也会捕获非预期的系统通知,这时是不可能像对待普通C++异常一样来处理的,惯用的手法不再安全了。

  经过新闻组上长期的辩论之后,尽管不情愿,我还是得承认Hillel Y. Sims观点:除非所有操作系统修正前面的问题,否则,所有异常应该继承自std::exception,当所有人适应catch(std::exception&)而不是catch(...)时,世界将会更加美好。

  即使不考虑和操作系统间糟糕的交互情况,有时候,catch(...)仍然是最合适的选择。如果你根本不知道会有什么异常抛出,并且必须停止栈展开,这可能是你唯一出路。一个典型的情况就是跨语言的时候。
关键词:C++

赞助商链接: