【more effective c++读书笔记】【第3章】异常(2)

条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异
1、函数参数和exceptions的传递方式有3种:by value,by reference,by pointer。
当调用一个函数,控制权最终会回到调用端,但当抛出一个exception,控制权不会再回来抛出端。

2、例子:

istream operator>>(istream& s, Widget& w);
void passAndThrowWidget(){
	Widget localWidget;
	cin >> localWidget;
	throw localWidget;
}
当localWidget被交到operator>>函数中时,并没有发生复制行为,而是operator>>的reference w被绑定于localWidget身上。不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都会发生localWidget的复制行为,交到catch子句中的正是那个副本。C++特别声明,一个对象被抛出作为exception时,总是会发生复制。这也造成了“传递参数”和“抛出exception”之间的另一个不同:后者常常比前置慢。
3、当对象被复制当做一个exception时,复制行为由对象的拷贝构造函数完成。该拷贝构造函数相应于对象的静态类型而非动态类型。
4、例子:

catch (Widget& w)                 // 捕获Widget异常
{
	...                           // 处理异常
	throw;                        // 重新抛出异常,使它继续传递
}                            
catch (Widget& w)                 // 捕获Widget异常
{
	...                           // 处理异常
	throw w;                     // 传递被捕获异常的一个副本
}
这两个catch块的差异在于第一个catch块中重新抛出当前的异常,而第二个catch块中重新抛出的是当前异常的副本。除了额外的复制行为所带来的性能成本因素,这两种方法还有以下差异:第一个块中重新抛出的是当前异常,无论其类型为何。如果这个异常最初抛出的异常类型是SpecialWidget,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型是Widget。因为重新抛出异常时没有进行拷贝操作。第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型是Widget。一般来说,你应该用
throw;来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。
5、例子:
catch (Widget w) ...          // 通过传值捕获异常
catch (Widget& w) ...         // 通过传递引用捕获异常
catch (const Widget& w) ...  //通过传递指向const的引用捕获异常
通过传值捕获会付出被抛出对象的两个副本的构造代价,一个是所有异常都必须建立的临时对象,第二个是把临时对象复制到w中。通过传递引用或传递指向const的引用捕获异常会付出被抛出对象的单一副本的构造代价。而以传引用方式传递函数参数时不会发生复制行为。
6、自变量传递与exception传播的第二个不同是在“函数调用者或抛出者”和“被调用者或捕捉者”之间的类型匹配规则不同。“exceptions与catch子句相匹配”的过程中,仅可发生两种类型转换。第一种是继承类与基类间的转换,一个针对基类异常而编写的catch子句也可以处理派生类类型的异常。第二种是允许从一个有型化指针转变成无型指针,所以一个针对const void* 指针而设计的catch子句能捕获任何指针类型的exception。
7、传递参数和传递异常间最后一个不同时,catch子句依出现顺序做匹配尝试。因此一个派生类异常可能被处理其基类异常的catch子句捕获。而调用一个虚函数,被调用的函数时调用者的动态类型中的函数。虚函数采用的是所谓的最佳吻合策略,而exception处理机制遵循所谓的最先吻合机制。
总结:“传递对象到函数去,或是以对象调用虚拟函数”和“将对象抛出成为一个exception”之间有三个主要差异。第一、异常对象总是会被复制,通过传值方式捕捉,异常对象被复制了两次。传递给函数参数的对象不一定得复制。第二、“被抛出成为exceptions”的对象,其类型转换动作比“被传递到函数去”的对象少。第三,catch子句以其出现于源代码的顺序被编译器检查比对,其中第一个匹配成功便执行;而以某对象调用一个虚拟函数,被选中执行的是那个鱼对象类型匹配吻合的函数,不论它是不是源代码所列的第一个。


条款13:以by reference 方式捕捉exceptions
1、有三种选择可以让异常对象传递到catch子句里,分别是通过指针,传值和引用。
2、catch by pointer
程序员必须要让异常对象在控制权离开那个抛出指针的函数之后依然存在,global对象和static对象都没问题,但local对象会出现问题。
例子:
void doSomething(){
	try{
		someFunction(); //可能抛出一个exception *
	}
	catch (exception *ex){ //捕获异常exception*,没有对象被复制
		...
	}
}
void someFunction(){
	exception ex;//局部对象,函数结束时被销毁
	...
	throw &ex;   //抛出一个指针,指向即将被销毁的对象
}
上述例子会出现问题,因为catch子句所收到的指针,指向不复存在的对象。
另一个做法是抛出一个指针,指向一个新的heap object
void someFunction(){
	...
	throw new exception; //抛出一个指针,指向一个新的heap-based object
	...
}
上述做法避免了捕获一个指向已被释放的对象的指针问题,但是出现了是否应该在catch子句中应该删除获得的指针的问题。如果异常对象被分配于heap,必须删除,否则会泄漏资源;如果异常对象不被分配于heap,不必删除,否则会使程序未定义。
3catch by value
可以消除“exception是否需要删除”及与标准的exception不一致等问题,但异常对象被抛出就会复制两次,并且可能会引起切割问题。
例子:
class exception {                   // 这是标准异常类
public:                             
	virtual const char * what() throw();// 返回异常的简要描述
	...                          
};                               
class runtime_error :                //也来自标准C++异常类
	public exception { ... };
class Validation_error :             // 客户自己加入个类
	public runtime_error {
public:
	virtual const char * what() throw();// 重新定义先前异常类中的虚函数
	...                              
};             

void someFunction(){                 // 可能会抛出一个有效的exception
	...

	if (a validation test fail) {
		throw Validation_error();
	}
	...
}
void doSomething(){
	try {
		someFunction();           // 可能会抛出一个有效的exception
	}
	catch (exception ex) {       //捕获所有标准异常类或其派生类
		cerr << ex.what();       // 调用 exception::what(),
		...                     // 而不是Validation_error::what()
	}
}
上述例子doSomething函数中ex.what()调用 exception::what(),而不是Validation_error::what()
4catch by reference
它不会发生对象删除问题,也不会发生切割问题,而且异常对象只会被复制一次。所以要以by reference方式捕捉exceptions
例子:
void doSomething(){
	try {
		someFunction();           // 可能会抛出一个有效的exception
	}
	catch (exception& ex) {       //捕获所有标准异常类或其派生类
		cerr << ex.what();       // 调用 exception::what(),
		...                     // 而不是Validation_error::what()
	}
}

条款14:明智运用exception specifictions
1exception specifictions让代码更容易被理解,因为它明确指出一个函数可以抛出什么样的exceptions
2、如果函数抛出了一个并未列于其exception specifiction的异常,这个错误在运行时期被检验出来,于是特殊函数unexpected会被自动调用。unexpected的默认行为是调用terminate,而terminate的默认行为是调用abort,所以如果程序违反exception specifiction,默认结果是程序被中止。
3、避免unexpected函数被调用的技术有以下几种:
a、避免将exception specifiction放在需要类型自变量templates身上。没有任何方法可以知道一个template的类型参数可能抛出什么exceptions,所以千万不要为template提供意味深长的exception spcifiction,因为template几乎必然会以某种方式使用其类型参数。
b、如果A函数内调用了B函数,而B函数无exception specifications,那么A函数本身也不要设定exception spcifictions
c、处理系统可能抛出的exceptions。可以将非预期的exceptions转换为一个已知类型,有两种做法:

1C++允许以不同类型的exceptions取代非预期的exceptions。如所有非预期的exceptions都以UnexpectedException对象取而代之:

class UnexpectedException {};// 所有非预期的异常对象被替换为这种类型对象

void convertUnexpected(){         // 如果一个unexpected异常被                        
	throw UnexpectedException(); // 抛出,便调用此函数
}

并以convertUnexpected取代默认的unexpected函数:
set_unexpected(convertUnexpected);
这么做以后,任何非预期的异常便会导致convertUnexpected被调用,于是非预期的异常被一个新的、类型为UnexpectedException的异常取而代之。只要被违反的exception specifiction内含有UnexpectedException,异常的传播便会继续下去,好像exception specifiction获得满足。
2)如果非预期函数的替代者重新抛出当前的异常,该异常会被标准类型bad_exception取而代之。
void convertUnexpected() // 如果非预期的异常被
{                       //抛出,此函数被调用
	throw;              //它只是重新抛出当前异常
}                        
// 安装convertUnexpected,做为unexpected的替代品
set_unexpected(convertUnexpected);
如果这么做,并且每一个exception specifiction都含有bad_exception(或其基类,也就是标准类exception),就不必担心程序会在遇上非预期的异常时终止执行。任何非预期的异常都会被一个bad_exception取代,而该异常代替原来的异常继续传播下去。
4exception specifictions的另一个缺点是他们会造成“当一个较高层次的调用者已经准备好要处理发生的异常时,unexpected函数却被调用的现象。
例子:
class Session { //用来模拟在线活动                
public:
	~Session();
	...
private:
	static void logDestruction(Session *objAddr) throw();
};

Session::~Session(){
	try {
		logDestruction(this);
	}
	catch (...) {}
}
Session的析构函数调用logDestruction记录“有个Session对象正被销毁的事实,并以catch (...)确指出它要捕捉任何可能被logDestruction抛出的异常。logDestructionexception specifiction保证不抛出任何异常。现在假设被logDestruction调用的函数抛出了一个异常,而logDestruction没有捕获。当这个异常传播到logDestructionunexpected将被调用,默认情况下将导致程序的终止。这是一个正确的行为,但这是Session析构函数的作者所希望的行为么?作者想处理所有可能的异常,所以好像不应该不给session析构函数里的catch块执行的机会就终止程序。

条款15:了解异常处理的成本
异常处理机制带来三大成本:
1、为了能够在运行时期处理异常,程序必须做大量簿记工作。在每一个执行点,它们必须能够确认“如果发生异常”,哪些对象需要析构,它们必须在每一个try语句块的进入点和离开点做记号,针对每个try语句它们必须记录对应的catch子句及能够处理的异常类型。这种簿记工作必须付出代价。即使你没有使用trythrowcatch,也必须付出至少某些成本
2、异常处理机制带来的第二种成本来自于try语句块。一旦你决定捕获异常,你就得为此付出成本。不同的编译器实现try块的方法不同,所以编译器与编译器间的开销也不一样。为了减少开销,你应该避免使用无用的try块。
3exception specifictions通常会招致与try语句块相同的成本。
所以为了让异常的相关成本最小化,只要能够不支持异常,编译器便不支持;请对try语句块和exception specifictions的使用限制于非用不可的地方,并且在真正异常的情况下才抛出异常。

版权声明:本文为博主原创文章,未经博主允许不得转载。

posted on 2015-08-29 14:14  ruan875417  阅读(181)  评论(0编辑  收藏  举报

导航