More Effective C++ 条款12 了解”抛出一个exception"与“传递一个参数”或“调用一个虚函数”之间的差异
1. 函数return值与try块throw exception、函数接收参数与catch字句捕获异常相当类似(不仅声明形式相像,函数参数与exception传递方式都有三种:by value,by reference ,by pointer(本质上也是by value) )。
2. 尽管函数调用与异常抛出相当类似,“从抛出端传递一个exception到catch子句”和“从函数调用端传递一个实参到被调函数参数”仍然大有不同:
1)调用一个函数,控制权会最终回到调用端(除非函数失败以致无法返回),但是抛出一个exception,控制权不会再回到抛出端
可以简单理解函数调用作用域是“外—里—外”的转换,而异常抛出是“里—外—···”的转换(只是便于理解,实际上这个比方并不正确)
2)如果函数调用的参数是按引用传递的,那么实参不会被复制,但无论catch接收的异常是按引用还是按值传递,被抛出的异常对象至少被复制一次,原因在于栈展开过程中局部对象都被销毁,因而需要产生一个临时对象保存被throw的异常,这与函数return时用一个临时对象来暂时保存return的对象是一样的(函数return存在NRV(有的也叫RVO)优化,可以省略调用拷贝构造函数)。也就是说,在第一个catch子句接受异常时,那个异常已经是被复制过一次的临时对象,如果catch子句的参数是按值传递,那么临时对象还需要再被复制一次。因此异常处理通常要付出较高的代价。
3)函数可以返回引用,catch子句不可能重新抛出一个引用,对于以下代码:
1 try{ 2 throw Derived; 3 } 4 catch(Base & tmp){ 5 throw tmp; 6 }
catch子句重新throw的过程中创建临时对象并调用拷贝构造函数,由于构造函数不可能为虚(虽然可以采取其他方式形成虚的"伪构造函数"),这意味着如果经由catch子句抛出的异常已经变为了Base类型(尽管传入的时候是按引用传递的),此时异常是当前exception的副本。如果要重新抛出Derived类型对象可以采用以下代码:
1 try{ 2 throw Derived 3 } 4 catch(Based& tmp){ 5 throw; 6 }
这样抛出的是当前的exception
4)函数调用与异常抛出参数匹配规则不同,如果有多个重载函数,那么选择参数最为匹配的那个,找不到匹配的函数则进行实参的转换尽量匹配上,有多个相当匹配的函数则发生二义性,也就是说,函数匹配采用“最佳吻合”策略;
异常抛出则不同,catch子句依出现顺序做匹配尝试,一旦找到一个“相对兼容的”类型就视为匹配成功,就算后面有完全匹配的类型也会被无视,也就是说,异常抛出的参数匹配采用“最先吻合”策略,也正是由于这种策略,异常抛出的参数所允许的转换比函数实参匹配所允许的转换要严格得多,只允许以下转换:
1)“继承架构中的类转换”:派生类异常可以被基类参数捕获,因此catch子句出现顺序应该是先派生类再基类
2)非const到const的转换
3)数组转为数组类型的指针
4)其他指针转“无型指针”(void*指针)