Effective C++ 条款08_不止于此

别让异常逃离析构函数

C++ 并不禁止析构函数吐出异常,但它也不鼓励这样做。
考虑下面代码:

class Widget {
    
    
public: 
	...
	~Widget( ) {
    
    ...}  // 假设这里吐出一个异常
};
void soSomething()
{
    
    
	std::vector<Widget> vec;
	...
}			// vec 在这里被自动销毁

当 vector vec 被销毁,它有责任销毁其内含的所有 Widgets。假设 vec 内含10个Widgets,而在析构第一个元素期间,有一个异常被抛出。其他九个 Widgets 还是应该被销毁(否则它们保存的任何资源都会发生泄漏),因此 vec 应该调用它们各个析构函数。但假设在那些调用期间,第二个 Widget 析构函数又抛出异常。现在有两个同时作用的异常,这对 C++ 而言太多了。在两个异常同时存在的情况下,程序若不是结束就是导致不明确行为。本例会导致不明确行为。
这很容易理解,但如果你的析构函数必须执行一个动作,而该动作可能会在失败是抛出异常,该咋办呢?举个例子,假设你使用一个 class 来负责数据库连接:

class DBConnection {
    
    
public:
	...
	static DBConnection create();  //返回 DBConnection 对象
	void close(); 		// 关闭联机;失败则抛出异常
};
  • 为了确保用户不忘记在 DBConnection 对象身上调用 close() ,我们可以这样做,创建一个用来管理 DBConnection 资源的 class,并在其析构函数中调用 close。
    代码如下:
class DBConn {
    
    
	DBConnection db;
public:
	...
	~DBConn(){
    
    
	db.close();
}
};

这样便允许用户写出这样的代码:

{
    
    				// 开启一个区块(block)。
DBConn dbc(DBConnection::create()); // 建立 DBConnection 对象并交给 DBConn 对象以便管理。
					// 通过 DBConn 的接口使用 DBConnection 对象。	
					// 在区块结束点,DBConn 对象被销毁,因而自动为 DBConnection 对象调用 close
...
}

只要调用 close 成功,一切都美好,但如果该调用导致异常,DBConn 析构函数会传播该异常,也就是允许它离开这个析构函数。那样就会造成问题,因为那是抛出了难以驾驭的麻烦。
下面给出两种办法解决这个问题

  • 如果 close 抛出异常就结束程序。通常通过调用 abort 完成:
DBConn::~DBConn( ){
    
    
	try {
    
     db.close(); }
	catch ( ... ){
    
    
		// 制作运转记录,记下对 close 的调用失败
		std::abort();  // 使用 abort 可以抢先制 “ 不明确 ” 行为于死地,也就是强迫结束程序
		}
}
  • 吞下因调用 close 而发生的异常:
DBConn::~DBConn( ){
    
    
	try {
    
     db.close(); }
	catch ( ... ){
    
    
		// 制作运转记录,记下对 close 的调用失败
		}
}

一般而言,这样做是一个坏主意,因为它压制了 “ 某些动作失败 ” 的重要信息!然而有时候吞下异常也比负担 “ 草率结束程序 ” 或 “ 不明确行为带来的风险 ” 好。
显然这些方法都不具有吸引力。问题在于两者都无法对 “ 导致 close抛出异常 ” 的情况做出反应。
一种较佳策略是重新设计 DBConn 接口,使用户有机会对可能出现的问题做出反应:

class DBConn {
    
    
public:
	void close (){
    
     			// 供用户使用的新函数
	db.close();
	closed = true;
}
~DBConn(){
    
    
	if(!closed) {
    
    
	try {
    
    			// 关闭连接(如果用户不那么做(调用close())的话)
	db.close();
 	}
 	catch(...){
    
    
	// 制作运转记录,记下对 close 的调用失败
	...
	}
	}
}
private:
	DBConnection db;
	bool closed;
};

把调用 close 的责任从 DBConn 析构函数手上移到 DBConn 用户手上,让用户有机会第一手处理问题。

最后请记住:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果用户需要对某个操作函数运行期间抛出的异常作出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

猜你喜欢

转载自blog.csdn.net/weixin_48033173/article/details/108986030
今日推荐