条款12:了解“抛出一个 exception”与“传递一个参数”或者“调用一个虚函数”之间的差异
1️⃣ 函数参数和catch
语句的声明方法
1 | class Widget {...}; //某个class,细节不重要。 |
1 | void f1 (Widget w) ; // 所有这些函数需要的参数 |
2️⃣ 相同点
- 函数参数和
exceptions
的传递方式都有三种:by value
,by reference
,by pointer
。
3️⃣ 不同点
对程序的控制权
- 当调用函数时,控制权最终会回到调用端。
- 当抛出一个
exception
,控制权不会回到调用端。
不论被捕捉的
exception
是以by value
,还是byreference
方式传递,都会发生copy
行为。1
2
3
4
5
6
7
8//此函数从一个stream中读取一个widget。
istream operator>>(istream& s, Widget& w);
void passAndThrowWidget ()
{
Widget localwidget;
cin >> localWidget; //将localwidget 传给operator>>。
throw localWidget; //将localwidget抛出成为一个exception。
}- 当
localWidget
被交到operator>>
函数手中,并没有发生copy
行为,而是operator>>
内的reference w
被绑定于localWidget
身上。此时,对w
做的事情,其实是施加于localWidget
身上的。 - 不论被捕捉的
exception
是以by value
或by reference
方式传递,都会发生localWidget
的复制行为,而交到<font style="background-color:#FCE75A;">catch</font>
子句手上的正是那个副本。一旦控制权离开passAndThrowWidget
,localWidget
便离开了作用域,于是localWidget
destructor
会被调用。
- 当
可以将一个临时对象传递给
exception
。catch 语句总是按照出现顺序做匹配尝试
缺点
当
try
语句块中分别有针对base class
而设计和针对derived class
而设计的catch
子句,一个derived class exception
仍有可能被“针对base class
而设计的catch
子句”处理掉。1
2
3
4
5
6
7
8
9
10
11try {
...
}
catch (logic_error& ex) { //此语句块将捕捉所有的
... // logic_error exceptions,
} //甚至包括其 derivedtypes。
catch (invalid_argument& ex) { //此语句块绝不会执行起来,
... // 因为所有的invalid_argument
} //exceptions都会被上述子句捕捉。
与虚函数的差异
- 虚函数执行的是“best fit”(最佳吻合)策略
- exception执行的是”first fit“(最先吻合)策略
4️⃣ 进一步理解
exception
objects
必定会造成复制行为,这导致其效率不高。当对象被复制当做一个
exception
,复制行为是由对象的copy constructor
执行的。这个copy constructor
相应于该对象的“静态类型”而非“动态类型”。1
2
3
4
5
6
7
8
9
10class Widget { ... };
class Specialwidget : public Widget ( ... };
void passAndThrowWidget ()
{
Specialwidget localSpecialwidget;
...
Widget& rw = localSpecialwidget; // rw 代表一个 Specialwidget。
throw rw; //抛出一个类型为 Widget 的 exception。
}- 这里抛出的是一个
widget exception
——虽然rw
实际代表的是一个Special widget
。 - 这是因为
rw
的静态类型是widget
而非Specialwidget
。 rw
虽然代表一个SpecialWidget
,编译器却不关心这个事实,它们关心的是rw
的静态类型。
- 这里抛出的是一个
5️⃣ 如何在 catch 语句块内再次传播 exception
1 | catch (Widget& w) // 捕捉 widget exceptions。 |
- 这两个
catch
语句块之间唯一的差异就是,前者重新抛出当前的exception
,后者抛出的是当前exception
的副本。这两种做法的区别是什么?- 第一语句块,重新抛出当前的
exception
,不会根据其类型之前的类型是什么。 - 第二语句块,抛出一个新的
exception
,其类型总是widget
,因为那是w
的静态类型。
- 第一语句块,重新抛出当前的
- 那种做法更好?
- 一般而言,你必须使用以下语句:
throw
,才能重新抛出当前的exception。 - 此外,它也比较有效率,因为不需要产生新的
exception object
。
- 一般而言,你必须使用以下语句:
6️⃣ 三种 catch 子句
1 | catch (Widget w) ... // 以 by value 的方式捕捉。 |
- “参数传递”和“exception 传播”的另一个区别
- 一个被抛出的对象,一定是个临时对象。
- 在函数调用中,将一个临时对象传递给一个
non-const reference
参数是不允许的;但是对exception
是合法的。
7️⃣ 回到“复制 exception objects”主题
- 以
<font style="background-color:#FBDE28;">by value</font>
方式捕捉<font style="background-color:#FBDE28;">exception</font>
,便是对被传递的对象做一个副本。
1 | catch (Widget w) ... // 以 by value 方式捕捉 |
- 预期付出“被抛出物”的“两个副本”的构造代价。
* 其中一个构造动作用于“任何`exceptions`都会产生的临时对象”身上。
* 另一个构造动作用于“将临时对象复制到`catch`语句中的参数`w`”。
以
by reference
方式捕捉exception
,便是对被传递的对象做一个引用。1
catch (Widget& w) ... // 以 by reference 方式捕捉
- 预期付出“被抛出物”的“一个副本”的构造代价。
- 这里的副本便是指临时对象。由于以
by reference
方式传递函数参数时并不会发生复制行为,所以“抛出exception
”和“传递函数参数”相比,前者会多构造一个“被抛出物”的副本(并于稍后析构)。
- 这里的副本便是指临时对象。由于以
- 预期付出“被抛出物”的“一个副本”的构造代价。
8️⃣ exception 与 catch 语句的类型吻合
一般不会发生类型转换
1
2
3
4
5
6
7
8
9
10
11
12void f(int value)
{
try {
if (someFunction()) { // 如果 someFunction()返回true,
throw value; //就抛出一个int。
}
}
catch (double d) { // 在这里处理类型为double的exceptions。
...
}
...
}- try 语句块抛出的
int exception
绝不会被“用来捕捉double exception
”的catch
子句捕捉。 - 后者只能捕捉类型确确实实为
double
的exceptions
,其间不会有类型转换的行为发生。 - 所以,如果
int exception
被捕捉,它一定是被某些其他(也许是外围的)catch
子句捕捉的(它们的捕捉类型一定是int或int&,或许再加上const或volatile之类的限定词)。
- try 语句块抛出的
仅有两种转换可以发生
- 继承结构中的类转换(inheritance-based conversions)
- 一个针对
base class exceptions
编写的catch
子句,可以处理类型为derived class
的exceptions
。 - 第二个允许发生的转换是从一个“有型指针”转为“无型指针”,所以一个针对
const void*
指针而设计的catch
子句,可捕捉任何指针类型的exception
:catch(const void*)
。
- 一个针对
- 继承结构中的类转换(inheritance-based conversions)
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 GYu的妙妙屋!