我正在参加「掘金·启航计划」
本文是《More Effective C++:35个改善编程与设计的有效方法》对条款12:了解“抛出一个 exception”与“传递一个参数”或“调用一个虚函数”间差异和条款15:了解异常处理(exception handling)的成本的总结和理解。
条款12:了解“抛出一个 exception”与“传递一个参数”或“调用一个虚函数”间差异
函数参数和exceptions的传递方式有3种:by value, by reference,by pointer。当调用一个函数,控制权最终会回到调用端(除非函数失败以至于无法返回),但是当抛出 exception,控制权不会再回到抛出端。
Widget localWidget;
throw localWidget; // localWidget作为异常被抛出会发生复制,将副本抛出
C++ 特别声明,一个对象被抛出作为 exception时,总是会发生复制(copy)。 导致抛出异常通常比传递参数要慢。不论被捕捉的exception 是以by value 或 by reference方式传递,都会发生localWidget的复制行为,而交到 catch 子句上的是副本。即使此exception 以by reference 方式被捕捉,catch 端还是不可能修改localWidget,只能修改 localWidget的副本。下面的代码对于上述描述可以进行更直观的展示:
class Widget {
public:
Widget() = default;
Widget(const Widget& widget) {
std::cout << "Copy Constructtor" << std::endl;
}
void ShowWidgetInfo() {
std::cout << this << std::endl;
}
};
int main(int argc, char* argv[]) {
try {
Widget localWidget;
localWidget.ShowWidgetInfo();
throw localWidget; // localWidget作为异常被抛出会发生复制
} // 将副本抛出
catch (const Widget& e) { // 捕获localWidget的副本,可以看到两个指针不同
const_cast<Widget&>(e).ShowWidgetInfo();
}
return 0;
}
当对象当作异常被复制,复制行为是由对象的复制构造( copy constructor)执行,copy constructor 相应于该对象的“静态类型”而非“动态类型”。 代码对于该描述的解释如下:
class SpecialWidget: public Widget {
public:
SpecialWidget() = default;
SpecialWidget(const SpecialWidget& widget) {
std::cout << "SpecialWidget : Copy Constructtor" << std::endl;
}
};
try {
SpecialWidget specialWidget;
Widget& rw = specialWidget;
throw rw; // rw的静态类型为Widget,执行Widget的复制构造函数创建副本并抛出Widget类型的异常
} catch (const SpecialWidget& e) {
std::cout << "SpecialWidget Exception" << std::endl;
} catch (const Widget& e) { // 可以捕获Widget和SpecialWidget异常
std::cout << "Widget Exception" << std::endl;
}
由于抛出的异常对象是对象副本,这导致如何 catch 语句块内传播 exceptions存在差异,假设在try语句块中抛出的是SpecialWidget。
try {
SpecialWidget specialWidget;
throw specialWidget;
}
下面两个实现继续传播异常
catch(Widget& e) {
throw; // 抛出当前异常,抛出是SpecialWidget类型的异常
}
catch(Widget& e) {
throw e; // 抛出Widget类型的异常
}
函数调用过程中将一个临时对象传递给一个 non-const reference 参数是不允许的,但对exceptions则属合法。
catch(Widget e) // 以by value的方式捕获异常,会发生两次复制构造(抛出对象产生副本+将临时对象复制到w)
catch(Widget& e) // 以by reference方式捕获异常
catch(const Widget& e) // 以by reference方式捕获异常
throw by pointer 事实上相当于 pass by pointer,两者都传递指针副本。必须特别注意的是,千万不要抛出一个指向局部对象的指针,因为该局部对象会在 exception 传离作用域时被销毁。
“自变量传递”与“exception 传播”两动作有着互异的做法:
- 其中一个不同是对象从“调用端或抛出端”转移到“参数或 catch子句”时的做法。如上述描述
- 第二个不同是“调用者或抛出者”和“被调用者或捕捉者”之间的类型匹配(type match)规则。
exceptions 与 catch 子句匹配的过程,仅有两种转换可以发生
-
一种是“继承架构中的类转换(inheritance-based conversions)”。一个针对 base class exceptions而编写的catch 子句,可以处理类型为 derived class的exceptions。
C++ 标准程序库定义有 exceptions 继承体系:
try { ... } catch (logic_error& ex) { // 此语句块将捕捉所有的logic_error exceptions,甚至包括其 derived types } catch (invalid_argument& ex) { // 此语句块绝不会被执行,因为所有的invalid_argument是logic_error的派生类 // 都会被上面的catch子句捕获 }当你调用一个虚函数,被调用的函数是“调用者(某个对象)的动态类型”中的函数。可以说,虚函数采用所谓的“best fit”(最佳匹配)策略,而exception 处理机制遵循所谓的“first fit”(最先吻合)策略。
try { ... } catch (invalid_argument& ex) { // 处理invalid_argument异常 } catch (logic_error& ex) { // 处理其他的logic_error异常 } -
第二个允许发生的转换是从一个“有型指针”转为“无型指针”,所以一个针对
const void*指针而设计的catch子句,可捕捉任何指针类型的exception。
上述实现完整实现代码
条款15:了解异常处理(exception handling)的成本
编译过程中如果没有加上对 exceptions的支持,程序通常比较小,执行时也比较快。如果编译过程中加上对 exceptions的支持,程序就比较大,执行时也比较慢。如果你的程序没有任何一处使用 try, throw 或catch,而且你连接的程序库没有一个有用到 try, throw 或 catch ,可以在编译过程中放弃支持exception,并因而免除了大小和速度的成本。目前,程序对 exceptions的运用普及度愈来愈高,但是如果你决定不使用 exceptions,并让编译器知道,编译器可以适度完成某种性能优化。
Exception 处理机制带来的第二种成本来自 try 语句块。粗略估计,如果使用 try 语句块,代码大约整体膨胀 5%~10%,执行速度亦大约下降这个数。 面对 exception specifications,编译器产出的代码倾向于类似面对 try 语句块的行为,所以一个exception specification 通常会招致与 try 语句块相同的成本
和正常的函数返回动作比较,由于抛出exception而导致的函数返回,其速度可能比正常情况下慢3个数量级。
为了让 exception的相关成本最小化,只要能够不支持 exceptions,编译器便不支持;请将你对 try 语句块和exception specifications的使用限制于非用不可的地点,并且在真正异常情况下才抛出 exceptions。
参考资料
- 《More Effective C++:35个改善编程与设计的有效方法(中文版)》