C++ 析构函数不要抛出异常

2022-05-09 11:13:10 浏览数 (2)

从语法上来说,析构函数可以抛出异常,但从逻辑上和风险控制上,析构函数中不要抛出异常,因为栈展开容易导致资源泄露和程序崩溃,所以别让异常逃离析构函数。

1.析构函数抛出异常的问题

析构函数从语法上是可以抛出异常的,但是这样做很危险,请尽量不要这要做。原因在《More Effective C 》中提到两个: (1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。 (2)通常异常发生时,c 的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding),因发生异常而逐步退出复合语句和函数定义的过程,被称为栈展开。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。

2.解决办法

如果析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?举个例子,假设你使用一个class负责数据库连接:

代码语言:javascript复制
class DBConnection
{ 
public:
   ...
   static DBConnection create(); //返回DBConnection对象;为求简化暂略参数
   void close(); //关闭联机;失败则抛出异常。
};

为确保客户不忘记在DBConnection对象身上调用close(),一个合理的想法是创建一个用来管理DBConection资源的class,并在其析构函数中调用close。这就是著名的以对象管理资源。

代码语言:javascript复制
//这个class用来管理DBConnection对象
class DBConn
{ 
public:
   ...
  DBConn(const DBConnection& db)
  {
       this->db=db;
   }
  ~DBConn() //确保数据库连接总是会被关闭
  {
      db.close();
  }
  
private:
   DBConnection db;
};

如果调用close成功,没有任何问题。但如果该调用导致异常,DBConn析构函数会传播该异常,如果离开析构函数,那会造成问题,解决办法如下:

2.1 结束程序

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

代码语言:javascript复制
DBConn::~DBconn()
{
    try
    {
	    db.close(); 
    }
    catch(...)
    {
        abort();
    }
}

如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强制结束程序”是个合理选项,毕竟它可以阻止异常从析构函数传播出去导致不明确行为。

2.2 吞下因调用 close 而发生的异常

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

一般而言,将异常吞掉是个坏主意,因为面对动作失败选择无所作为,然而有时候吞下异常比“草率结束程序”或“不明确行为带来的风险”好。能够这么做的一个前提就是程序必须能够继续可靠的执行。

2.3 重新设计 DBConn 接口,使其客户有机会对可能出现的异常作出反应

我们可以给DBConn添加一个close函数,赋予客户一个机会可以处理“因该操作而发生的异常”。把调用close的责任从DBConn析构函数手上移到DBConn客户手中,你也许会认为它违反了“让接口容易被正确使用”的忠告。实际上这污名并不成立。如果某个操作可能在失败的时候抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。

代码语言:javascript复制
class DBConn
{
public:
    ...
    void close() //供客户使用的新函数
    {
        db.close();
        closed = true;
    }
    ~DBConn()
    {
        if(!closed)
        {
            try        //关闭连接(如果客户不调用DBConn::close)
            {       
                  db.close();
            }
            catch(...) //如果关闭动作失败,记录下来并结束程序或吞下异常
            { 
                制作运转记录,记下对close的调用失败;
                ...
           }
        }
    }
private:
    DBConnection db;
    bool closed;
};

本例要说的是,由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会。如果他们不认为这个机会有用(或许他们坚信不会有错误发生),可能忽略它,依赖DBConn析构函数去调用close。

在析构函数中面对异常时,请记住: (1)假如析构函数中抛出了异常,那么你的系统将变得非常危险,也许很长时间什么错误也不会发生;但也许你的系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有,不利于系统的错误排查; (2)析构函数禁止抛出异常。如果析构函数发生异常,不要让异常逃离析构函数,析构函数应该捕捉任何异常,不传播或结束程序; (3)如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

0 人点赞