当构造函数抛出异常时,RAII是如何工作的

How does RAII work when a constructor throws an exception?

本文关键字:何工作 工作 抛出异常 构造函数 RAII      更新时间:2023-10-16

我正在学习C++中的RAII习惯用法,以及如何使用智能指针。

在我的阅读中,我发现了两件事,对我来说,似乎是相互矛盾的。

引用自http://www.hackcraft.net/raii/:

如果已经创建了一个具有RAII语义的成员对象,并且在构造函数完成之前发生了异常,那么它的析构函数将作为堆栈展开的一部分被调用。因此,控制多个资源的对象可以保护它们的清理,即使它不是完全通过使用成员RAII对象构建的。

但引用自http://www.parashift.com/c++-faq-lite/exceptions.html#faq-17.10:

如果构造函数抛出异常,则对象的析构函数不会运行。如果您的对象已经做了一些需要撤消的事情(例如分配一些内存、打开文件或锁定信号量),那么对象内部的数据成员必须记住这些"需要撤消的东西"。

然后,第二个链接源建议使用智能指针来处理构造函数中已经分配的内容的问题。

那么,在这些场景中实际会发生什么呢?

你误解了第一句话。这并不难,因为它令人困惑。

如果已经创建了一个具有RAII语义的成员对象,并且在构造函数完成之前发生了异常,那么它的析构函数将作为堆栈展开的一部分被调用。

它就是这么说的。以下是的含义

如果已经创建了具有RAII语义的成员对象,并且在外部对象的构造函数完成之前在外部对象中发生异常,则成员对象的析构函数将作为堆栈展开的一部分被调用。

看到区别了吗?其思想是成员对象完成了它的构造函数,但拥有的类型没有。它在其构造函数(或在该构造函数之后初始化的另一个成员的构造函数)中的某个位置抛出。这将导致调用其所有成员的析构函数(即所有完成构造的成员),但不是自己的析构因子。

这里有一个例子:

class SomeType
{
  InnerType val;
public:
  SomeType() : val(...)
  {
    throw Exception;
  }
};

创建SomeType实例时,它将调用InnerType::InnerType。只要不抛出,它就会进入SomeType的构造函数。当抛出时,它将导致val被销毁,从而调用InnerType::~InnerType

这里没有矛盾;只是在不同的语境中使用了一些令人困惑的术语。

如果对象的构造函数抛出异常,则会发生以下情况(假设异常被捕获):

  1. 构造函数中的所有局部变量都调用了它们的析构函数,从而释放它们获取的所有资源(如果有的话)
  2. 构造函数抛出异常的对象的所有直接子对象都将调用其析构函数,从而释放它们获取的资源(如果有的话)
  3. 构造函数抛出的对象的所有基类都将调用其析构函数(因为它们是在派生类构造函数运行之前完全构造的)
  4. 将进行来自呼叫者等的进一步清理

因此,由智能指针或其他RAII对象管理的任何资源(这些对象是被销毁对象的数据成员)都确实会被清理,但在对象的销毁器中进行清理的专用代码不会启动。

希望这能有所帮助!

这两个语句并不矛盾,但第一个语句使用了一些不幸的语言。当一些对象的构造抛出时,它的解构器不会被调用,但该对象所拥有的所有对象都会被其各自的解构器解构。

因此,使用RAII和智能指针,对象的任何指针成员的析构函数都将独立于所属对象的析构因子而被调用。原始指针不能释放它们所指向的内存,必须手动删除。如果所属对象的构造函数抛出原始指针,则不会释放。智能指针不可能发生这种情况。