More Effective C++ 12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异

从语法上看,在函数里声明参数与在 catch 子句中声明参数几乎没有什么差别:

class Widget { ... }; //一个类

void f1(Widget w); // 一些函数,其参数分别为 
void f2(Widget& w); // Widget, Widget&,或 
void f3(const Widget& w); // Widget* 类型 
void f4(Widget *pw); 
void f5(const Widget *pw); 
catch (Widget w) ... //一些 catch 子句,用来 
catch (Widget& w) ... //捕获异常,异常的类型为 
catch (const Widget& w) ... // Widget, Widget&, 或 
catch (Widget *pw) ... // Widget* 
catch (const Widget *pw)

你因此可能会认为用 throw抛出一个异常到 catch子句中与通过函数调用传递一个参数两者基本相同。这里面确有一些相同点,但是他们也存在着巨大的差异。

相同点

传递函数参数与异常的途径可以是传值、传递引用或传递指针。

不同点

但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的


产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方


考虑下面代码:

istream operator>>(istream& s, Widget& w); 
void passAndThrowWidget() 
{ 
	 Widget localWidget; 
	 cin >> localWidget; //传递 localWidget 到 operator>> 
	 throw localWidget; // 抛出 localWidget 异常 
}

当传递 localWidget 到函数 operator>>里,不用进行拷贝操作,而是把 operator>>内的引用类型变量 w 指向localWidget,任何对 w 的操作实际上都施加到localWidget 上。这与抛出 localWidget 异常有很大不同

不论通过传值捕获异常还是通过引用捕获都将进行 lcalWidget 的拷贝操作,也就说传递到catch子句中的是 localWidget 的拷贝。

原因:因为当 localWidget 离开了生存空间后,其析构函数将被调用。如果把 localWidget 本身(而不是它的拷贝)传递给 catch 子句,这个子句接收到的只是一个被析构了的Widget,一个 Widget 的“尸体”。这是无法使用的。因此C++规范要求被做为异常抛出的对象必须被复制。


即使被抛出的对象不会被释放,也会进行拷贝操作。例如如果 passAndThrowWidget 函数声明 localWidget 为静态变量(static):

void passAndThrowWidget() 
{ 
	static Widget localWidget; // 现在是静态变量(static); 
	 //一直存在至程序结束 
	cin >> localWidget; // 像以前那样运行 
	throw localWidget; // 仍将对 localWidget进行拷贝操作
}

对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢。


当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamictype)对应类的拷贝构造函数。比如以下这经过少许修改的 passAndThrowWidget

class Widget { ... }; 
class SpecialWidget: public Widget { ... }; 
void passAndThrowWidget() 
{ 
	SpecialWidget localSpecialWidget; 
	... 
	Widget& rw = localSpecialWidget; // rw 引用 SpecialWidget  throw rw; //它抛出一个类型为 Widget 的异常 
}

这里抛出的异常对象是 Widget,即使 rw 引用的是一个 SpecialWidget。因为 rw 的静态类型(static type)是Widget,而不是 SpecialWidget。你的编译器根本没有注意到 rw 引用的是一个SpecialWidget编译器所注意的是 rw 的静态类型(static type)。这种行为可能与你所期待的不一样,但是这与在其他情况下 C++中拷贝构造函数的行为是一致的



异常是其它对象的拷贝,这个事实影响到你如何在 catch 块中再抛出一个异常。比如下面这两个 catch 块,乍一看好像一样:

catch (Widget& w) // 捕获 Widget 异常 
{ 
	 ... // 处理异常 
	 throw; // 重新抛出异常,让它继续传递 
} 
catch (Widget& w) // 捕获 Widget 异常 
{ 
	 ... // 处理异常 
	 throw w; // 传递被捕获异常的拷贝
}  

这两个catch块的差别在于第一个 catch 块中重新抛出的是当前捕获的异常,而第二个catch 块中重新抛出的是当前捕获异常的一个新的拷贝。如果忽略生成额外拷贝的系统开销,这两种方法还有差异么?

差异
第一个块中重新抛出的是当前异常,无论它是什么类型。特别是如果这个异常开始就是做为 SpecialWidget 类型抛出的,那么第一个块中传递出去的还是 SpecialWidget 异常,即使 w 的静态类型(static type)是 Widget。这是因为重新抛出异常时没有进行拷贝操作
第二个 catch 块重新抛出的是新异常,类型总是 Widget,因为 w 的静态类型(static type)是 Widget

一般来说,你应该用 throw 来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝

catch (Widget w) ... // 通过传值捕获异常 
catch (Widget& w) ... // 通过传递引用捕获  异常  
catch (const Widget& w) ... //通过传递指向 const 的引用捕获异常

可以注意到传递参数与传递异常的另一个差异:一个被异常抛出的对象可以通过普通的引用捕获;它不需要通过指向 const 对象的引用捕获。在函数调用中不允许转递一个临时对象到一个非 const 引用类型的参数里,但是在异常中却被允许。

当我们这样声明一个 catch 子句时:

catch (Widget w) ... // 通过传值捕获

会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进 w 中。
而下面这样声明:

catch (Widget& w) ... // 通过引用捕获 
catch (const Widget& w) ... //也通过引用捕获

这仍旧会建立一个被抛出对象的拷贝:拷贝同样是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。

这只是参数传递与异常传递的区别的一个方面;第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同

比如在标准数学库中的sqrt 函数:

double sqrt(double); // from <cmath> or <math.h>

我们能这样计算一个整数的平方根,如下所示:

int i;
double sqrtOfi = sqrt(i);
C++允许进行从 intdouble 的隐式类型转换,所以在 sqrt 的调用中,i 被悄悄地转变为 double 类型,并且其返回值也是 double

一般来说,catch 子句匹配异常类型时不会进行这样的转换

void f(int value) 
{ 
	try 
	{ 
		if (someFunction()) // 如果 someFunction()返回真 
		{ 
			throw value; //抛出一个整形值 
			...} 
	catch (double d) // 只处理 double 类型的异常 
	{ 
		... 
	} 
	 ...

在 try 块中抛出的 int 异常不会被处理 double 异常的 catch 子句捕获。该子句只能捕获类型真真正正为 double 的异常,不进行类型转换。因此如果要想捕获 int 异常,必须使用带有 intint&参数的 catch 子句


不过在 catch 子句中进行异常匹配时可以进行两种类型转换:
第一种是继承类与基类间的转换。一个用来捕获基类的 catch 子句也可以处理派生类类型的异常,比如:捕获 runtime_errors 异常的 Catch 子句可以捕获 range_error 类型和 overflow_error类型的异常;可以接收根类 exception 异常的 catch 子句能捕获其任意派生类异常

第二种是允许从一个类型化指针转变成无类型指针),所以带有 const void* 指针的 catch 子句能捕获任何类型的指针类型异常:

catch (const void*) ... //捕获任何指针类型异常

传递参数和传递异常间最后一点差别是 catch 子句匹配顺序总是取决于它们在程序中出现的顺序。
因此一个派生类异常可能被处理其基类异常的 catch 子句捕获,即使同时存在有能直接处理该派生类异常的 catch 子句,与相同的 try 块相对应。例如:

try 
{ 
	 ... 
} 
catch (logic_error& ex) // 这个 catch 块 将捕获所有的 
//logic_error异常, 包括它的派生类 
{ 
	...
} 
 
catch (invalid_argument& ex) { // 这个块永远不会被执行 
 ... //因为所有的 
} // invalid_argument 
 // 异常 都被上面的 
 // catch 子句捕获。

与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法
如果一个处理派生类异常的 catch 子句位于处理基类异常
catch 子句后面,编译器会发出警告.
像上面那个例子,应该这样写:

try 
{ 
	 ... 
} 
catch (invalid_argument& ex) 
{ // 处理 invalid_argument 
	 ... //异常 
} 
catch (logic_error& ex) 
{ // 处理所有其它的 
	 ... // logic_errors 异常 
}

总结

把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别:
1.异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝

2.对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)

3.catch 子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的 catch 将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。

猜你喜欢

转载自blog.csdn.net/qq_44800780/article/details/106315116
今日推荐