【问题标题】:RAII and members if constructor throws?如果构造函数抛出 RAII 和成员?
【发布时间】:2020-06-13 10:13:06
【问题描述】:

我之前曾在异常已关闭且内存分配失败意味着我们终止程序的环境中工作。现在处理异常我想知道以下内容的精确语义:

class Foo {
  std::unique_ptr<Bar> x;
  std::unique_ptr<Bar> y;
 public:
  Foo(): x{new Bar}, y{new Bar} {}
};

我的问题是在分配y 时抛出new Bar 会发生什么?我假设调用了x 的析构函数,以便清理第一个分配。语言如何保证这一点?任何人都知道解释精确语义的标准引用吗?

【问题讨论】:

  • y 不是“分配的”本身;它的内存空间是Foo 的一部分。它指向的东西是由你的new Bar 表达式分配的。请注意术语。我认为您的意思是“初始化”。

标签: c++ exception constructor language-lawyer object-lifetime


【解决方案1】:

是的,所有完全构造的成员都将被销毁。您的对象不会处于任何“半生不死”状态。不会发生内存泄漏。

[except.ctor]/3: 如果一个对象的初始化或销毁不是通过委托构造函数被异常终止,则为对象的每个直接子对象调用析构函数,对于完整的对象,虚拟基类子对象,其初始化已完成 ([dcl.init]) [..] 子对象按其构造完成的相反顺序销毁。 [..]

我们可以自己演示一下:

#include <memory>
#include <iostream>

struct Bar
{
    Bar(const char* name, bool doThrow = false) : m_name(name)
    {
        if (doThrow)
        {
            std::cout << name << ": Bar() throwing\n";
            throw 0;
        }

        std::cout << name << ": Bar()\n";
    }

    ~Bar() { std::cout << m_name << ": ~Bar()\n"; }

private:
    const char* m_name;
};

class Foo {
  std::unique_ptr<Bar> x;
  std::unique_ptr<Bar> y;
 public:
  Foo(): x{new Bar("A")}, y{new Bar("B", true)} {}
};

int main()
{
    try
    {
        Foo f;
    }
    catch (...) {}
}

// g++ -std=c++17 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
// A: Bar()
// B: Bar() throwing
// A: ~Bar()

(live demo)

事实上,这就是所谓的“智能指针”的主要好处之一:异常安全。如果x 是一个原始指针,你就会泄露它所指向的东西,因为原始指针的破坏不会做任何事情。在异常安全的情况下,您可以拥有 RAII;没有它,祝你好运。

【讨论】:

  • 另外,构造顺序是成员的声明顺序并且严格排序,所以要调用其析构函数的成员都是在相应的初始化程序抛出的那个之前声明的所有成员。
【解决方案2】:

如果您担心两个 new Bar 表达式在句柄被初始化以保存它们的含义之前交错和抛出,标准不允许这样做。

首先在[intro.execution]

12 一个完整的表达式是

  • init-declarator 或 mem-initializer,包括初始化器的组成表达式,

16 每个值计算和副作用都与 全表达式在每个值计算和边之前排序 与要评估的下一个完整表达式相关联的效果。

无需过多介绍细节,x{new Bar} 和 y{new Bar} 都被认为是标准的“完整表达”(即使它们不是语法方面的表达)。我引用的两段表明要么必须首先进行x(包括new Bar)的整个初始化,要么必须首先进行y 的整个初始化。我们从 [class.base.init]

得知

13.3 - 然后,非静态数据成员按照它们在类定义中声明的顺序进行初始化(同样不管顺序如何) mem-initializers)。

所以x被完全初始化,然后y。所以即使new Bar 在初始化y 时抛出,x 已经拥有它应该持有的资源。在这种情况下,当抛出异常时,[except.ctor] 第 3 段中的措辞将适用于完全构造的 x,并将其销毁,从而释放资源。

【讨论】:

    【解决方案3】:

    如果在对象构造过程中抛出异常,则该对象未构造,您不应该对 any 的状态做出任何假设成员。该对象根本不可用。在抛出异常之前初始化的任何成员都将按照其构造的相反顺序被销毁。

    https://eel.is/c++draft/basic.life#def:lifetime 描述生命周期规则。

    【讨论】:

      猜你喜欢
      • 2012-02-25
      • 1970-01-01
      • 2013-10-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-19
      • 2016-04-12
      • 2016-07-21
      • 1970-01-01
      相关资源
      最近更新 更多