首先先抛出结论:
- 析构函数绝对不要抛出异常。如果在一个析构函数中某个函数可能调用失败,抛出异常,这个析构函数应该捕捉这个异常,然后“吞下”他们,或者结束程序。
- 如果客户端需要对某个操作函数运行期间抛出的异常做出反应,那么这个 class 应该提供一个普通函数接口,而不是在析构函数中自己处理异常。
假如有以下的代码片段:
class Widget {
public:
...
~Widget() { } //假设抛出异常
};
void DoSomething()
{
std::vector<Widget> v;
...
}
当 vector 容器被销毁的时候,它是有责任销毁它内含的所有的 Widget 对象的。但是假如,v 在析构第一个 Widget 对象的时候,Widget 对象的析构函数抛出异常,但是其他的 Widget 对象仍然需要正常被销毁。如果多个 Widget 对象都抛出异常,这个时候就会造成程序不是结束执行就是会导致不明确行为。
那如果在析构函数中必须要执行某个动作,但是这个动作又可能会在失败的时候抛出异常呢?比如说下面这一种情况:
有一个 DBConnection 类负责与数据库的连接:
class DBConnection {
public:
static DBConnection create();
void close();
//...
};
然后又一个 DBConnectionManager 类负责管理 DBConnection 对象的连接状态,为了保证在用户在忘记调用 DBConnection 对象的 close() 函数的时候,数据库的连接也能够正确的关闭,一个合理的想法就是在 DBConnectionManager 类的析构函数中调用 DBConnection 的 close() 函数,大概是这个样子:
class DBConnectionManager {
public:
DBConnectionManager(DBConnection db)
{
dbConnection = db;
}
~DBConnectionManager()
{
dbConnection.close();
}
private:
DBConnection dbConnection;
};
这个时候就会存在一个问题:如果 DBConnection 的 close 函数抛出异常,DBConnectionManager 的析构函数就会传播这个异常,也就是会允许程序离开这个析构函数,这个时候就会造成潜在的问题。
有两个办法可以避免这个问题:
第一个就是让这个析构函数把异常给“吞掉”,但是有一个前提条件就是在这个析构函数把异常“吞掉”之后也要保证程序能够继续可靠的执行。
~DBConnectionManager()
{
try {
dbConnection.close();
}
catch (std::exception)
{
//记录调用失败
}
}
第二个解决办法就是 在抛出异常的时候就强行结束程序,阻止异常从析构函数中传播出去,产生不明确行为。
~DBConnectionManager()
{
try {
dbConnection.close();
}
catch (std::exception)
{
//记录调用失败
std::abort();
}
}
前两者的解决方案都会导致客户端无法对“导致 close 抛出异常” 的情况做出反应,所以一个更好的解决方案是重新设计 DBConnectionManager 的接口,使得客户端能够对 close 抛出的异常做出反应,也就是提供客户端手动调用 close 函数的接口,但是为防止客户端忘记手动关闭,又会在其析构函数中调用 close 函数。这样的话,即使真的有错误发生,也是客户端“活该”。(都给你接口了让你手动调用关闭,谁让你忘记的,就不要怪我自己处理的时候出错咯)
class DBConnectionManager {
public:
DBConnectionManager(DBConnection db)
{
dbConnection = db;
}
void closeConnection()
{
dbConnection.close();
hasClosed = true;
}
~DBConnectionManager()
{
if (!hasClosed)
{
try {
dbConnection.close();
}
catch (std::exception)
{
//记录调用失败
}
}
}
private:
DBConnection dbConnection;
bool hasClosed;
};