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;
};
上述方案中:
- 为DBConn类提供了供用户调用的接口close,赋予用户机会来处理调用可能导致的异常(由用户自己调用close并不会造成用户的负担,而是给用户一个处理错误的机会);
- 属性closed用来标记资源关闭是否成功,供DBConn追踪管理的DBConnection是否已经关闭,如果没有关闭,在析构函数中调用close函数关闭连接;
- 但如果析构函数中调用的close函数抛出异常,又会走向“强迫程序结束”或“吞下异常”的困境。
5. 总结
1.析构函数绝对不要抛出异常。如果析构函数抛出异常,总会带来“过早结束程序”或“发生不明确行为”的风险。为了避免风险,如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉异常,然后吞下异常或结束程序。
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,class就该提供一个普通函数,而不是在析构函数中执行该操作。