条款08:别让异常逃离析构函数
1.析构函数绝对不要吐出异常。如果一个析构函数内调用的函数可能抛出异常,析构函数应该捕捉任何出现的异常,然后选择吞下它们或结束程序,别让异常逃离析构函数。
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中),让客户自己去调用该操作函数,给客户一个处理错误的机会。(见例2)
例1:
class Widget {
public:
//......
~Widget() { // 假定这个析构函数可能会吐出异常
//......
}
//......
};
void doSomething()
{
//......
std::vector<Widget> v;
//......
}
当 doSomething 函数执行完毕的时候,会开始析构容器 v 中存放的各个Widget对象。
然而,由于我们假设Widget析构函数可能会吐出异常。在这种情况下,假如v内含10个Widget的对象,当v被销毁时,它有责任销毁内含的所有Widget对象。而在执行第一个Widget对象的析构函数时有个异常被抛出。其它九个Widget对象还是应该被销毁,但执行第二个Widget对象的析构函数时又有个异常被抛出。这对c++而言太多了,在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确的行为。
例2:
如果在对象结束的时候,必须在析构函数中执行一个可能抛出异常的动作,那该怎么办呢?
首先,定义两个类分别负责数据库连接以及该类资源的管理:
// 该类负责数据库的连接
class DBConnection {
public:
//......
static DBConnection create(); // 静态成员函数,建立一个DBConnection对象
//......
void close();
//......
};
// 该类负责类DBConnection的资源管理
class DBConn {
public:
//......
~DBConn() { // 析构函数确保数据库连接总是会被关闭
db.close();
}
//......
private:
//......
DBConnection db;
//......
};
这样,客户可以写出如下代码以管理数据库连接(代码很好看懂):
{
//......
DBConn dbc(DBConnection::create());
//......
}
该段代码结束时,会自动调用~DBConn()
,如果close调用成功,那最好,如果close调用失败,那么DBConn析构函数就会传播该异常,即允许异常离开这个析构函数,会造成问题。
两个办法可以避免这个问题:1.如果close抛出异常就结束程序,通常通过调用abort完成。2.吞下close抛出的异常。
solution1.
DBConn::~DBConn() {
try {
db.close();
}
catch (...) {
//记下对close的调用失败
std::abort();
}
}
solution2.
DBConn::~DBConn() {
try {
db.close();
}
catch (...) {
//记下对close的调用失败
}
}
更好的方法是将 close的执行移交给用户,由客户自己调用close,然后再在析构函数中try and catch,代码如下:
// 修改后的DBConn类实现
class DBConn {
public:
//......
void close() { // 要求用户自己关闭数据库对象
db.close();
closed = true;
}
//......
~DBConn() {
if (!closed) { // 如果用户忘记了这么做,就采用 try catch 机制吞下异常。
try {
db.close();
}
catch (...) {
// 记录此次 close 失败
//......
}
}
}
//......
private:
//......
DBConnection db;
bool closed; // 增设此变量用以判断用户是否已经自行调用 close(),用户也可根据此变量判断 close() 是否顺利执行并作出相应的异常处理。
//......
};
这里DBConn析构函数则起到一个双保险的作用。