【问题标题】:How to catch a constructor exception?如何捕获构造函数异常?
【发布时间】:2016-08-02 08:26:43
【问题描述】:

我有一个 C++ 类,它在失败时从构造函数中抛出异常。如何分配此类的本地实例(不使用new)并处理任何可能的异常,同时保持try 块范围尽可能小?

基本上,我正在寻找以下 Java 习语的 C++ 等价物:

boolean foo() {
    Bar x;
    try {
        x = new Bar();
    } catch (Exception e) {
        return false;
    }
    x.doSomething();
    return true;
}

我不想捕获来自x.doSomething() 的异常,只有构造函数

我想我正在寻找一种将x 的声明和初始化分开的方法。

是否可以在不使用堆分配和指针的情况下完成此操作?

【问题讨论】:

  • 是的。将所有内容放在 try 块内的成功路径上。您将留在函数范围内。
  • @StoryTeller 如果我不想从例如捕获异常怎么办? x.doSomething(),只有构造函数的异常?
  • @AndrewSun 使用不同的异常,或将x.doSomething() 放入内部异常块中。
  • C++ 异常非常昂贵(对于 c++ 速度迷而言)。与 Java 不同,你不应该一开始就随意扔掉它们。如果你已经这样做了,那就重新设计你的异常层次结构并从x.doSomething();重新抛出东西
  • 您知道要捕获什么异常吗?内存分配可能会失败并且构造函数代码可能会失败...

标签: c++ constructor exception-handling


【解决方案1】:

你可以在 C++17 中使用std::optional

bool foo() {
    std::optional<Bar> x; // x contains nothing; no Bar constructed
    try {
        x.emplace();      // construct Bar (via default constructor)
    } catch (const Exception& e) {
        return false;
    }
    x->doSomething();     // call Bar::doSomething() via x (also (*x).doSomething() )
    return true;
}

【讨论】:

  • @Jarod42 我犹豫如何为Bar 的默认 ctor 编写它。 x.emplace()? x.emplace({})?
  • 默认构造函数是x.emplace()。后者不会编译。
  • 很好的答案,虽然我从一个 catch 块返回不是一个好主意。您可以改为使 catch 仅抑制异常,并通过测试 x 来决定何时返回。 (即if (!x) return false;
  • @nate:C++ 是一种多出口语言。所有代码都需要设计为处理多个出口。从catch 块返回没有错。相反,不这样做将是不必要的麻烦。
【解决方案2】:

这个 Java 习语不能很好地转换为 C++,因为 Bar x; 将需要 默认构造函数,即使您的真正构造函数需要传递参数。

我建议将语言打到这种程度 - 扩大 try 块就足够了 - 但如果你真的想要缩小范围,那么你可以使用一个函数并依靠返回值优化来避免值复制:

Bar foobar()
{
    try {
        return Bar();
    } catch (Exception& e){
        /* Do something here like throwing a specific construction exception
           which you intercept at the call site.*/
    }
}

但实际上,您可以在构造时抛出特定异常,因此完全避免使用这种函数方法。

【讨论】:

  • RVO 和移动语义在我看来是最好的解决方案。
  • 在 c++17 之前,OP 也必须处理复制/移动构造函数异常。
  • @Jarod42,c++17 有什么变化? (只是为了我自己的好奇心而问)。
  • @StoryTeller:在 c++17 之前,允许 (N)RVO。在 C++17 中,它是必需的(在某些情况下)。
  • “在这里做一些事情,比如抛出一个特定的构造异常”——coconstructor 已经这样做了,不需要将它包装在一个函数中。如果您确实认为这是有益的,请展示如何处理您的函数抛出的异常。
【解决方案3】:

是的,如果您将所有代码放在try 子句中,例如使用function try block(以避免不必要的嵌套和范围),这是可能的:

bool foo() try
{
    Bar x;
    x.doSomething();
    return true;
}
catch (std::exception const& e)
{
    return false;
}

或者在try 子句中调用另一个真正起作用的函数:

void real_foo()
{
    Bar x;
    x.doSomething();
}

bool foo() try
{
    real_foo();
    return true;
}
catch (std::exception const& e)
{
    return false;
}

请注意,在构造函数中抛出异常通常不是一个好主意,因为这会停止对象的构造,并且不会调用其析构函数。


正如 Holt 所说,这也将捕获来自 doSomething 调用的异常。有两种解决方法:

  1. 简单而标准的方式:使用指针

  2. 使用两阶段构造:有一个不能抛出异常的默认构造函数,然后调用一个可以抛出异常的特殊“构造”函数。

第二种方式在 C++ 标准化之前很常见,并在 Symbian system 的代码中广泛使用。它不再常见,因为为此使用指针更加容易和简单,尤其是在今天有很好的智能指针可用的情况下。我真的不推荐现代 C++ 中的第二种方式。

最简单的方法当然是确保构造函数根本不会抛出异常,或者如果抛出任何异常,那么它们的性质就是程序无论如何都无法继续并终止程序。正如您问题的 cmets 中所述,C++ 中的异常很昂贵,然后我们也有废弃的构造问题,并且在 C++ 中使用异常的所有情况都应该只在异常情况下进行。 C++ 不是 Java,即使两种语言都有类似的结构,也不应该这样对待它。

如果您仍然想从构造函数中抛出异常,实际上还有第三种方法可以只捕获这些异常:使用上面的代码示例之一,只抛出 doSomething 永远无法抛出的特定异常,然后捕获这些异常仅限特定的构造函数。

【讨论】:

  • 功能尝试块不需要。普通的try catch 就够了。
  • @Jarod42,那么Bar 必须在另一个块中,而不是在功能级块中。
  • 这不会模仿给定 java 代码的行为,因为您将捕获来自 x.doSomething() 的异常。
  • @Holt 在用 C++ 编写时“不会模仿给定 Java 代码的行为”,通常想用惯用的 C++ 编写而不是模仿其他语言。
  • @n.m.用惯用的 C++ 编写不应妨碍您维护程序的 observable 行为(我认为有 c++ 方法可以获得相同的行为,而不必依赖类似 java 的代码)。
【解决方案4】:

通常,如果您想避免堆分配,您不能将局部变量的声明与其定义分开。因此,如果要将所有内容组合在一个函数中,则必须将 x 的整个范围用 try/catch 块包围:

boolean foo() {
    try {
        Bar x;
        x.doSomething();
    } catch (Exception e) {
        return false;
    }
    return true;
}

【讨论】:

    【解决方案5】:

    没有。从您的 java 示例中,您将不得不在这两种可能性之间进行选择:

    没有指针:

    bool foo() {
        try {
            Bar x;
            x.doSomething();
        } catch (Exception e) {
            return false;
        }
        return true;
    }
    

    用指针:

    bool foo() {
        Bar* x = nullptr;
        try {
            x = new Bar();
        } catch (Exception e) {
            return false;
        }
        x->doSomething();
    
        delete x; // don't forget to free memory
    
        return true;
    }
    

    或者使用托管指针:

    #include <memory>
    
    bool foo() {
        std::unique_ptr<Bar> x;
        try {
            x = new Bar();               // until C++14
            x = std::make_unique<Bar>(); // since C++14
        } catch (Exception e) {
            return false;
        }
        x->doSomething();
        return true;
    }
    

    【讨论】:

    • 您可能想在某处添加delete x ;)
    • 是的。你现在拥有的第二个例子的缺点是,你真的想在x-&gt;doSomething 中捕获任何异常以确保delete 被执行,但是你需要一个单独的带有重新抛出的try-catch 块 - 潜在unique_ptr 很好地解决了令人头疼的问题。很好,你添加了它。
    【解决方案6】:

    你必须选择变体

    bool foo() {
        std::unique_ptr<Bar> x;
        try {
            x = std::make_unique<Bar>();
        } catch (const BarConstructorException& e) {
            return false;
        }
        x->doSomething();
        return true;
    }
    

    bool foo() {
        try {
            Bar x;
            x.doSomething();
        } catch (const BarConstructorException& e) {
            return false;
        }
        return true;
    }
    

    【讨论】:

      【解决方案7】:

      在修订后的问题中,OP 增加了要求

      我不想捕捉来自x.doSomething()的异常,只想捕捉[局部变量]的构造函数。

      翻译 Java 代码的简单方法

      boolean foo() {
          Bar x;
          try {
              x = new Bar();
          } catch (Exception e) {
              return false;
          }
          x.doSomething();
          return true;
      }
      

      ...对于 C++,然后是使用 Optional_ 类(如 Barton-Nackmann Fallibleboost::optional 或 C++17 std::optional

      auto foo()
          -> bool
      {
          Optional_<Bar> xo;
          try
          {
              xo.emplace();
          }
          catch( ... )
          {
              return false;
          }
      
          Bar& x = *xo;
          // Possibly other code here, then:
          x.doSomething();
          return true;
      }
      

      一个不错的选择是重构该代码,如下所示:

      struct Something_failure {};
      
      void do_something( Bar& o )
      {
          // Possibly other code here, then:
          o.doSomething();
      }
      
      auto foo()
          -> bool
      {
          try
          {
              Bar x;
      
              do_something( x );
              return true;
          }
          catch( Something_failure const& )
          {
              throw;
          }
          catch( ... )
          {}
          return false;
      }
      

      如果您不喜欢上述方法,那么您可以随时选择动态分配的 Bar 实例,例如使用std::unique_ptr 保证清理,但它具有动态分配的一般开销。在 Java 中,大多数对象都是动态分配的,因此这似乎不是一个严重的缺点。但是在 C++ 中,大多数对象都是超快的堆栈分配,因此与普通操作相比,动态分配是一个非常慢的操作,因此必须权衡动态分配的可能概念上的简单性。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-09-07
        • 1970-01-01
        • 2013-01-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多