Effective C++ Item 08:别让异常逃离析构函数

1.C++不禁止析构函数抛出异常,但不鼓励在析构函数中抛出异常

通过以下代码分析:

class Widget {
public:
	...
	~Widget(){...}    //假设这个析构函数会抛出异常
};
void dosomething() {
	std::vector<Widget>v;      //v在函数结束后被自动销毁
	...
}

上述代码中,函数dosomething中声明的变量v,在函数运行结束后,自动销毁。变量vector销毁内含的所有Widgets。

假设v内含有10个Widget,在析构第一个元素时,有个异常被抛出,而其他9个Widgets还是应该被销毁(否则他们保存的任何资源都会发生资源泄漏),因此,v还会调用其他9个的析构函数。但假设在其他9个的析构函数调用时,第2个Widget的析构函数又抛出异常,此时有两个同时作用的异常,C++程序这时不是结束执行就是导致不明确的行为。

对于标准库中的其他容器或TR1的任何容器,在上述场景下也会出现相同的情况。因此,C++不鼓励在析构函数中抛出异常。

2.析构函数中执行的动作可能抛出异常

根据上部分的分析,析构函数中不应该抛出异常,但有的场景下,析构函数却不可避免地要抛出异常:

class DBConnection {
public:
	...
	static DBConnection create();  //函数返回一个DBConnection对象
	void close();                  //关闭联机,失败则抛出异常
};

在上述代码中,使用类DBConnection负责数据库连接,为了确保客户不忘记在DBConnection对象身上调用close函数,一个合理的想法是:“以对象管理资源”,即创建一个用来管理DBConnection资源的class,并在其析构函数中调用close函数。因此,在下面代码中创建类DBConn管理DBConnection的资源:

class DBConn {
public:
	...
	~DBConn() {
		db.close();            //在析构函数中调用close函数,利用对象析构时
		                       //必执行析构函数的特性管理类DBConnection的资源
	}
private:
	DBConnection db;
};

于是,客户端便有下面的代码:

{
	DBConn dbc(DBConnection::create());
}

在上述代码区块中,利用类DBConnection的create函数创建DBConnection对象,将创建的对象作为参数传递给DBConnection对象dbc,以便管理资源。

在区块结束点,DBConn对象被销毁,因而自动为DBConnection对象调用close函数,释放资源。

当析构函数中调用的close函数成功时,没有任何问题。但是当该效用抛出异常时,DBConn析构函数会传播该异常,就会造成类似上述的程序崩溃的“不明确行为”的问题。

3. 避免析构函数中异常的传播

为了避免析构函数中抛出的异常传播出去,导致程序“不明确的行为”问题,有两种解决方案:

1)如果close函数抛出异常就结束程序,通过调用abort函数完成:

DBConn::~DBConn() {
	try {
		db.close();
	}
	catch (...) {
		//制作运转记录,记下对close的调用失败
		std::abort();
	}
}

使用abort函数阻止异常从析构函数中传播出去,就可以避免不明确行为的发生。

2)吞下因调用close而发生的异常:

DBConn::~DBConn() {
	try {
		db.close();
	}
	catch (...) {
		//制作运转记录,记下对close的调用失败
	}
}

将异常吞掉不是好的做法,这样压制了“某些动作失败”的重要信息。但有时吞下异常比“草率结束程序”或“不明确行为带来的风险”好。

4. 让客户有机会对可能的异常做出反应

上面的两种方案都无法对出现的异常做出反应。该怎么做才能使客户有机会处理发生的异常呢?

为了让客户有机会处理发生的异常,需要重新设计DBConn接口:

class DBConn {
public:
	...
		void close() {
		db.close();
		closed = true;
	}
	~DBConn() {
		if (!closed) {         //如果客户没有关闭连接,在析构函数中调用close关闭连接
			try {
				db.close();           
			}
			catch (...) {
				             //制作运转记录,记下对close的调用失败

			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

上述方案中:

  1. 为DBConn类提供了供用户调用的接口close,赋予用户机会来处理调用可能导致的异常(由用户自己调用close并不会造成用户的负担,而是给用户一个处理错误的机会);
  2. 属性closed用来标记资源关闭是否成功,供DBConn追踪管理的DBConnection是否已经关闭,如果没有关闭,在析构函数中调用close函数关闭连接;
  3. 但如果析构函数中调用的close函数抛出异常,又会走向“强迫程序结束”或“吞下异常”的困境。

5. 总结

1.析构函数绝对不要抛出异常。如果析构函数抛出异常,总会带来“过早结束程序”或“发生不明确行为”的风险。为了避免风险,如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉异常,然后吞下异常或结束程序。

2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,class就该提供一个普通函数,而不是在析构函数中执行该操作。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值