异常与错误处理
运行时错误已发异常,这些异常可以通过陷阱或软件中断的形式被检测到。这些异常可以通过 try-catch 块捕捉到。程序会崩溃并产生一个错误消息,如果异常处理被启用并且没有 try-catch 块。
异常处理的目的是检测很少出现的错误,并以一个优雅的方式从错误中恢复。你可能认为只要错误不发生,异常处理就不需要额外时间,但不幸的是,这不总是成立的。程序必须进行许多簿记以便知道如何从异常事件中恢复。这个簿记的代价很大程度上依赖于编译器。某些编译器有高效的、极小或没有开销的基于表的方法,而其他编译器有低效的基于代码的方法,或者要求运行时类型识别(RTTI),这影响代码的其他部分。进一步解释,参考ISO/IEC TR18015 Technical Report on C++ Performance。
下面的例子解释了为什么需要簿记:
// Example 7.48
class C1 {
public:
...
~C1();
};
void F1() {
C1 x;
...
}
void F0() {
try {
F1();
}
catch (...) {
...
}
}
假定函数F1
在返回时调用对象x
的析构函数。但如果异常出现在F1
里会怎么样?我们不经return
就跳出F1
。F1
的清理被阻止,因为它被直接地打断了。现在,调用x
的析构函数是异常处理的责任。如果F1
保存了要调用析构函数的所有信息或任何其他可能需要的清理,这才可能。如果F1
调用另一个函数,它进而调用另一个函数,以此类推,如果异常发生在最里层函数中,异常处理需要所有关于函数调用链的信息。它需要追踪记录,在函数调用中回溯,检查所有需要进行的必要清理。这称为栈回滚。
所有函数必须为异常处理保存某些信息,即使没有异常发生。这是为什么在某些编译器中,异常处理会是代价高昂的原因。如果异常处理对应用是不必要的,那么应该禁止它,以使代码更小、更高效。可以通过在编译器中关闭异常处理选项,禁止全程序的异常处理。可以通过向函数原型添加throw()
,禁止单个函数的异常处理。
void F1() throw();
这允许编译器假定F1
不会抛出任何异常,因此它无需保存函数F1
的恢复信息。不过,如果F1
调用另一个可能抛出异常的函数F2
,那么F1
必须检查F2
抛出的异常。在F2
确实抛出异常的情形下,调用std::unexpected()
函数。因此,仅当所有被F1
调用的函数也有一个throw()
声明时,才向F1
应用throw()
声明。对库函数,空throw()
说明是有用的。
编译器区分_叶子函数_与_框架函数_。框架函数至少调用其他一个函数。叶子函数不调用任何其他函数。叶子函数比框架函数更简单,因为如果可以排除异常,或者在异常出现时没有清理工作,就可以不考虑栈回滚信息。通过内联所有调用的函数,框架函数可以转换为叶子函数。如果程序关键的最里层循环不包含框架函数的调用,可得到最好的性能。
虽然在某些情形里,空throw()语句有益于优化,但没有理由添加像throw(A, B, C)的语句显式告知函数会抛出哪些类型的异常。实际上,编译器可能实际上会添加额外的代码来检查抛出的异常是否就是指定的类型(参考Sutter:A Pragmatic Look at Exception Specifications, Dr Dobbs Journal, 2002)。
在某些情形里,即使在程序最关键部分中使用异常处理也是最优的。如果替代实现效率较低,且希望能够从错误恢复,就是这样。下面的例子展示了这样一个情形:
异常与向量代码
避免异常处理的代价
开发异常安全代码
欢迎交流