【问题标题】:C++ Ramifications of ignoring exception from constructorC++ 忽略构造函数异常的后果
【发布时间】:2011-01-12 21:07:00
【问题描述】:

我已经搜索过这个问题的答案,但还没有找到。

当一个对象在构造函数的末尾抛出异常时,该对象是有效的还是其中之一“取决于构造技术”?

例子:

    struct Fraction
    {
      int m_numerator;
      int m_denominator;
      Fraction (double value,
                int denominator);
    };
    Fraction::Fraction(double value, int denominator)
    :  m_numerator(0), m_denominator(denominator)
    {
      if (denominator == 0)
      {
/* E1 */        throw std::logic_error("Denominator is zero.");
      }
      m_numerator = static_cast<int>(value * static_cast<double>(denominator));
      double actual_value = 0.0;
      actual_value = static_cast<double>(m_numerator) / static_cast<double>(m_denominator);
      double error = fabs(actual_value - value);
      if (error > 5.0E-5)
      {
/* E2 */  throw std::logic_error("Can't represent value in exact fraction with given denominator");
      }
    }

程序:

int main(void)
{
    try
    {
        Fraction f1(3.14159264, 4); // Throws exception, E2 above.
    }
    catch (...)
    {
        cerr << "Fraction f1 not exactly representable as fraction with denom. of 4.\n";
    }

    // At this point, can I still use f1, knowing that it is an approximate fraction?

    return EXIT_SUCCESS;
}

在这个例子中,知道f1是一个近似值,捕获到异常后是否可以使用f1?

数据成员已被构造和初始化。

我没有看到任何违反上述规定的 C++ 语言规则。

编辑:将错误增量值从 5.0E05 更改为 5.0E-5。

【问题讨论】:

  • 你真的可以访问 f1 的作用域吗?
  • 那么接下来的挑战就是如何在对象构造失败之后使用它。也许这对于 SO wiki 来说是一个很好的主题。
  • 绝对不可能。 当抛出异常时,它会向外传播。这意味着它离开当前范围,检查是否可以被捕获,如果不能,则重复。当构造函数抛出异常时,very 离开的第一个作用域是每次都在其中创建对象的作用域。要么是因为它直接在 try/catch 块中,要么是因为它需要离开作用域来寻找一个。

标签: c++ exception-handling constructor exception


【解决方案1】:

如果在构造函数的任何阶段抛出异常(并且未在构造函数中捕获),则该对象将不存在。所有已经构造成功的成员变量都会按照构造的相反顺序进行解构。如果异常是从成员变量构造函数或初始化列表中抛出的,则构造失败的成员变量不会调用其析构函数,也不会调用其后的任何析构函数。

在任何情况下,假设您在任何地方都使用 RAII,则所有资源都被正确释放,并且不会有对象可以访问。在ptr = new Foo(); 的情况下,变量 ptr 保留其旧值。同样smartptr.reset(new Foo());根本不会调用reset函数。

请注意在构造其他对象的表达式中使用 operator new 的谬误:somefunc(Foo(), new Bar());。如果 Foo 构造函数失败,则可能存在内存泄漏(取决于编译器处理参数的顺序)。

【讨论】:

    【解决方案2】:

    当一个对象在构造函数结束时抛出异常,是 对象有效还是其中之一“取决于构造技术”?

    是的,确实取决于。我的意思是,这取决于你的意思对象是有效的。 Valid 可能有多种含义。

    众所周知,构造被打断的对象是部分构造的对象。现在,如果您将部分构造视为无效状态,那么是的,这样的对象将是无效的。

    但根据 C++/15.2 中指定的此方案,保证销毁:

    一个部分的对象 建造或部分毁坏 将为所有人执行析构函数 其完全构造的子对象, 也就是说,对于 构造函数已完成执行 而析构函数还没有开始 执行。

    这意味着,只有部分构造对象的子对象会被正确销毁,而部分构造对象本身的析构函数将被调用。

    #include <iostream>
    using namespace std;
    struct A
    {
        ~A() { cout<<"~A()\n"; }
    };
    struct B
    {
        A a;
        B() { throw 1; }
        ~B() { cout<<"~B()\n"; } // never called
    };
    int main()
    {
        try
        {
            B a;
        }
        catch (...)
        {
            cout << "caught\n";
        }
    }
    

    【讨论】:

      【解决方案3】:

      throw in constructor = 构造失败 --> 对象不可用

      如前所述,如果抛出异常,则对象超出范围。但是,您可能对分配对象时的情况感兴趣:

      f = new Fraction(3.14159264, 4);
      

      在这种情况下, f 也是不可用的,因为构造函数没有完成工作,并且指针没有被分配。没有调用析构函数,并且内存被释放,因此无法使用该对象。

      因此,请正常构造您的对象,如果您打算使用该类,请不要使用异常。使用 is_exact() 成员函数来确定构造后是否准确。

      【讨论】:

      • 如果对象是在栈上创建的,也不会调用析构函数。
      • @villintehaspam,没错,我关注的是未发现的变体。
      • 重要的事实是,如果 Fraction 的构造函数抛出,那么对f 指针的赋值永远不会完成,因此new 返回的内存地址实际上是不可访问的,无论发生什么情况分配的内存。
      【解决方案4】:

      不,一旦 f1 定义的范围退出,您就不能再使用该对象。所以,在你的代码中:

      try
      {
          Fraction f1(3.14159264, 4); // Throws exception, E2 above.
      
          // f1 can be used until here
      }
      catch (...)
      {
      }
      
      // The scope that f1 was defined in is over, so the compiler will not let
      // you reference f1
      

      也就是说,也许您应该重新考虑在无法表示 实际价值。因为这可能仅适用于某些用途,您可以要求调用者请求它:

      enum FractionOption { disallowInexact, allowInexact };
      
      Fraction::Fraction(double value, int denominator,
                         FractionOption option = disallowInexact)
      {
          ...
          if ((option == disallowInexact) && (error > 5.0E-5))
          {
              throw std::logic_error("Can't represent value ...");
          }
      }
      
      Fraction f1(3.14159264, 4, allowInexact);
      

      【讨论】:

      • +1,我喜欢为构造函数添加的策略。我没有想到。 {我认为策略仅用于模板,例如 Andrei Alexandrescu 的 Modern C++ Design。}
      【解决方案5】:

      跟随 JMD

      THen 是在 catch 子句中可用的 f1。答案也是否定的。所以你看到范围规则甚至阻止你在代码中提出这个问题。

      如果它的析构函数运行了,唯一会泄露对象存在的事情 - 但如果构造函数没有完成,它就不会运行

      【讨论】:

        【解决方案6】:

        如果对象在构造过程中抛出异常,从技术上讲,它不会使对象无效。在您的示例中, f1 超出范围,因此在引发异常时被释放。

        如果 f1 是在 try 块内分配和构造的指针,并且构造函数(而不是分配器)抛出异常,则您的指针将指向有效的已分配内存。该内存中的对象是否包含有效数据将取决于您的构造函数;基本上,如果数据在抛出之前有效,那么它在抛出之后也是有效的。

        另外,听起来您正在尝试做的并不是对异常的适当使用,我会在这里质疑您的设计。在构造函数调用中抛出异常通常表示对象构造不正确,不应使用。

        【讨论】:

        • "您的指针将指向有效的已分配内存" -- 这是不正确的。
        • 分配大致从:foo* f = 0; f = new foo() 转换为:foo * f = 0; void* __memory = 0; foo* __dummy = 0; try{ __memory = ::operator new(sizeof(f)); __dummy = new (__memory) foo(); }catch(...){::operator delete(__memory); throw;} f = __dummy; 是的,我很清楚这看起来很糟糕。 :P 但是如果new 抛出,指针保持不变;并且内存是空闲的。
        【解决方案7】:

        我同意 fbrereto。

        如果你在构造函数中抛出一个错误,相当于说“构造这个对象不起作用”或“无法创建对象”,那么你需要处理这个事实 - 我只会这样做这适用于无法以其他方式使用对象的致命错误,例如无法打开我们希望能够在 MySettingsReader 类中打开的文件。

        【讨论】:

        • +1 虽然这不是我选择的解决方案,但我给它 +1 以说明何时使用异常。
        【解决方案8】:

        乔纳森的回答是正确的。此外,虽然分数可能处于有效状态,但我不建议将异常用于流控制,尤其是用于关于对象状态的通信。相反,请考虑将某种 is_exactly_representable 添加到返回 bool 的 Fraction 对象 API。

        【讨论】:

        • 关于is_exactly_representable 的好建议。该对象是有效的,但并不完全准确。类似于 1/3 不能用浮点数精确表示。
        【解决方案9】:

        捕获异常后,f1 超出范围。它不再存在,所以你不能使用它。

        【讨论】:

        • 我是否更正了语言语法在构造函数由于范围问题而失败后阻止使用对象?如果没有 try / catch 块和 try 块中的构造,我看不到如何捕获异常。
        • @Thomas:正确,没有 catch 块就无法捕获异常。如果你没有,它会离开main,而操作系统会抓住它。 (从而为您提供那些漂亮的崩溃对话框。)
        • @Thomas Matthews 该语言确实阻止您使用在 try 范围内声明的任何变量,这些变量在 try 之外 - 从那时起它们就会超出范围。因此,您甚至不能尝试在 try 块之外使用这样的变量。解决它的唯一方法是使用指针,然后由于对象处于无效状态,因此应该将其删除,而不是使用。
        • 实际上,经过反思,我不确定您是否可以通过指针解决此问题。由于构造函数失败,内存可能永远不会分配给指针,而是可能会被清理掉。所以,真的,我不认为有任何方法可以使用无效构造的对象。但是,如果您在其中动态分配内存或其他资源,则确实需要确保在构造函数失败时清理内容。编辑:啊,Kornel Kisielewicz 在下面谈到它。
        • @Johnatan - 它被自动释放,但没有调用析构函数,请参阅我的答案。
        猜你喜欢
        • 2020-11-07
        • 2023-04-07
        • 2017-07-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-03-08
        • 1970-01-01
        • 2020-10-18
        相关资源
        最近更新 更多