如何处理 C++ 中构造函数的失败?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4989807/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 17:07:29  来源:igfitidea点击:

How to handle failure in constructor in C++?

c++

提问by Thomson

I want to open a file in a class constructor. It is possible that the opening could fail, then the object construction could not be completed. How to handle this failure? Throw exception out? If this is possible, how to handle it in a non-throw constructor?

我想在类构造函数中打开一个文件。有可能打开失败,然后对象构建无法完成。如何处理这个故障?抛出异常?如果这是可能的,如何在非抛出构造函数中处理它?

回答by B?ови?

If an object construction fails, throw an exception.

如果对象构造失败,则抛出异常。

The alternative is awful. You would have to create a flag if the construction succeeded, and check it in every method.

替代方案很糟糕。如果构造成功,您必须创建一个标志,并在每个方法中检查它。

回答by Tony Delroy

I want to open a file in a class constructor. It is possible that the opening could fail, then the object construction could not be completed. How to handle this failure? Throw exception out?

我想在类构造函数中打开一个文件。有可能打开失败,然后对象构建无法完成。如何处理这个故障?抛出异常?

Yes.

是的。

If this is possible, how to handle it in a non-throw constructor?

如果这是可能的,如何在非抛出构造函数中处理它?

Your options are:

您的选择是:

  • redesign the app so it doesn't need constructors to be non-throwing- really, do it if possible
  • add a flag and test for successful construction
    • you could have each member function that might legitimately be called immediately after the constructor test the flag, ideally throwing if it's set, but otherwise returning an error code
      • This is ugly, and difficult to keep right if you have a volatile group of developers working on the code.
      • You can get some compile-time checking of this by having the object polymorphically defer to either of two implementations: a successfully constructed one and an always-error version, but that introduces heap usage and performance costs.
    • You can move the burden of checking the flag from the called code to the callee by documenting a requirement that they call some "is_valid()" or similar function before using the object: again error prone and ugly, but even more distributed, unenforcable and out of control.
      • You can make this a little easier and more localised for the caller if you support something like: if (X x) ...(i.e. the object can be evaluated in a boolean context, normally by providing operator bool() constor similar integral conversion), but then you don't have xin scope to query for details of the error. This may be familiar from e.g. if (std::ifstream f(filename)) { ... } else ...;
  • have the caller provide a stream they're responsible for having opened... (known as Dependency Injection or DI)... in some cases, this doesn't work that well:
    • you can still have errors when you go to use the stream inside your constructor, what then?
    • the file itself might be an implementation detail that should be private to your class rather than exposed to the caller: what if you want to remove that requirement later? For example: you might have been reading a lookup table of precalculated results from a file, but have made your calculations so fast there's no need to precalculate - it's painful (sometimes even impractical in an enterprise environment) to remove the file at every point of client usage, and forces a lot more recompilation rather than potentially simply relinking.
  • force the caller to provide a buffer to a success/failure/error-condition variable which the constructor sets: e.g. bool worked; X x(&worked); if (worked) ...
    • this burden and verbosity draws attention and hopefullymakes the caller much more conscious of the need to consult the variable after constructing the object
  • force the caller to construct the object via some other function that can use return codes and/or exceptions:
    • if (X* p = x_factory()) ...
    • Smart_Ptr_Throws_On_Null_Deref p_x = x_factory();</li> <li>X x; // never usable; if (init_x(&x)) ...`
    • etc...
  • 重新设计应用程序,使其不需要构造函数不抛出- 真的,如果可能的话
  • 添加标志并测试成功构建
    • 您可以让每个成员函数在构造函数测试标志后立即合法地调用,理想情况下如果设置了则抛出,否则返回错误代码
      • 如果您有一群不稳定的开发人员在处理代码,这很丑陋,并且很难保持正确。
      • 您可以通过让对象多态地遵循以下两种实现中的任何一种来对此进行一些编译时检查:成功构造的版本和始终错误的版本,但这会引入堆使用和性能成本。
    • 您可以通过记录在使用对象之前调用某些“is_valid()”或类似函数的要求,将检查标志的负担从被调用代码转移到被调用者:同样容易出错和丑陋,但更加分散、不可执行和失控。
      • 如果您支持以下内容,您可以使调用者更容易和更本地化:(if (X x) ...即可以在布尔上下文中评估对象,通常通过提供operator bool() const或类似的积分转换),但是您没有x范围查询错误的详细信息。这可能很熟悉,例如if (std::ifstream f(filename)) { ... } else ...;
  • 让调用者提供一个他们负责打开的流......(称为依赖注入或 DI)......在某些情况下,这不能很好地工作:
    • 当你在构造函数中使用流时,你仍然会出错,然后呢?
    • 文件本身可能是一个实现细节,应该是您的类私有的,而不是公开给调用者的:如果您以后想删除该要求怎么办?例如:您可能一直在从文件中读取预先计算结果的查找表,但计算速度如此之快,因此无需预先计算 - 在每个点删除文件是痛苦的(有时在企业环境中甚至不切实际)客户端使用,并强制进行更多的重新编译,而不是可能简单地重新链接。
  • 强制调用者为构造函数设置的成功/失败/错误条件变量提供缓冲区:例如 bool worked; X x(&worked); if (worked) ...
    • 这种负担和冗长引起了人们的注意,并希望使调用者更加意识到在构造对象后需要查阅变量
  • 强制调用者通过其他一些可以使用返回码和/或异常的函数来构造对象:
    • if (X* p = x_factory()) ...
    • Smart_Ptr_Throws_On_Null_Deref p_x = x_factory(); </li> <li>××;// 永远不可用;如果 (init_x(&x)) ...`
    • 等等...

In short, C++ is designed to provide elegant solutions to these sorts of issues: in this case exceptions. If you artificially restrict yourself from using them, then don't expect there to be something else that does half as good a job.

简而言之,C++ 旨在为这些类型的问题提供优雅的解决方案:在这种情况下是异常。如果你人为地限制自己使用它们,那么不要指望有其他东西能起到一半的作用。

(P.S. I like passing variables that will be modified by pointer - as per workedabove - I know the FAQ lite discourages it but disagree with the reasoning. Not particularly interested in discussion thereon unless you've something not covered by the FAQ.)

(PS 我喜欢传递将被指针修改的变量 -worked如上所述 - 我知道 FAQ lite 不鼓励它,但不同意推理。对讨论不是特别感兴趣,除非你有一些没有被 FAQ 涵盖的东西。)

回答by lorro

New C++ standard redefines this in so many ways that it's time to revisit this question.

新的 C++ 标准以多种方式重新定义了这一点,是时候重新审视这个问题了。

Best choices:

最佳选择:

  • Named optional: Have a minimal private constructor and a named constructor: static std::experimental::optional<T> construct(...). The latter tries to set up member fields, ensures invariant and only calls the private constructor if it'll surely succeed. Private constructor only populates member fields. It's easy to test the optional and it's inexpensive (even the copy can be spared in a good implementation).

  • Functional style: The good news is, (non-named) constructors are never virtual. Therefore, you can replace them with a static template member function that, apart from the constructor parameters, takes two (or more) lambdas: one if it was successful, one if it failed. The 'real' constructor is still private and cannot fail. This might sound an overkill, but lambdas are optimized wonderfully by compilers. You might even spare the ifof the optional this way.

  • 命名可选:有一个最小的私有构造函数和一个命名构造函数:static std::experimental::optional<T> construct(...)。后者尝试设置成员字段,确保不变,并且只有在肯定会成功时才调用私有构造函数。私有构造函数只填充成员字段。测试可选的很容易,而且价格便宜(即使在良好的实现中也可以省去副本)。

  • 函数式风格:好消息是,(未命名的)构造函数永远不会是虚拟的。因此,您可以将它们替换为静态模板成员函数,除了构造函数参数之外,该函数采用两个(或多个)lambda:一个是成功的,一个是失败的。“真正的”构造函数仍然是私有的,不会失败。这听起来可能有点矫枉过正,但 lambda 表达式被编译器完美地优化了。您甚至if可以通过这种方式节省可选的。

Good choices:

不错的选择:

  • Exception: If all else fails, use an exception - but note that you can't catch an exception during static initialization. A possible workaround is to have a function's return value initialize the object in this case.

  • Builder class: If construction is complicated, have a class that does validation and possibly some preprocessing to the point that the operation cannot fail. Let it have a way to return status (yep, error function). I'd personally make it stack-only, so people won't pass it around; then let it have a .build()method that constructs the other class. If builder is friend, constructor can be private. It might even take something only builder can construct so that it's documented that this constructor is only to be called by builder.

  • 异常:如果所有其他方法都失败,请使用异常 - 但请注意,您无法在静态初始化期间捕获异常。在这种情况下,一种可能的解决方法是让函数的返回值初始化对象。

  • Builder 类:如果构造很复杂,则使用一个类来进行验证和可能的一些预处理,以确保操作不会失败。让它有办法返回状态(是的,错误函数)。我个人将其设置为仅堆栈,因此人们不会传递它;然后让它有一个.build()方法来构造另一个类。如果builder是朋友,constructor可以是私有的。它甚至可能需要一些只有 builder 才能构造的东西,以便记录此构造函数只能由 builder 调用。

Bad choices: (but seen many times)

糟糕的选择:(但见过很多次)

  • Flag: Don't mess up your class invariant by having an 'invalid' state. This is exactly why we have optional<>. Think of optional<T>that can be invalid, Tthat can't. A (member or global) function that works only on valid objects works on T. One that surely returns valid works on T. One that might return an invalid object return optional<T>. One that might invalidate an object take non-const optional<T>&or optional<T>*. This way, you won't need to check in each and every function that your object is valid (and those ifs might become a bit expensive), but then don't fail at the constructor, either.

  • Default construct and setters: This is basically the same as Flag, only that this time you're forced to have a mutable pattern. Forget setters, they unnecessarily complicate your class invariant. Remember to keep your class simple, not construction simple.

  • Default construct and init()that takes a ctor parameters: This is nothing better than a function that returns an optional<>, but requires two constructions and messes up your invariant.

  • Take bool& succeed: This was what we were doing before optional<>. The reason optional<>is superior, you cannot mistakenly (or carelessly!) ignore the succeedflag and continue using the partially constructed object.

  • Factory that returns a pointer: This is less general as it forces the object to be dynamically allocated. Either you return a given type of managed pointer (and therefore restrict allocation/scoping schema) or return naked ptr and risk clients leaking. Also, with move schematics performance-wise this might become less desirable (locals, when kept on stack, are very fast and cache-friendly).

  • Flag:不要因为“无效”状态而弄乱你的类不变性。这正是我们拥有optional<>. 想想optional<T>那可以是无效的,T那不能。仅适用于有效对象的(成员或全局)函数适用于T。一个肯定会返回有效作品的T. 可能返回无效对象的一个​​ return optional<T>。一个可能使对象无效的对象采用非常量optional<T>&optional<T>*. 这样,您就不需要检查对象有效的每个函数(这些ifs 可能会变得有点昂贵),但也不会在构造函数中失败。

  • 默认构造和设置器:这与 Flag 基本相同,只是这次您被迫使用可变模式。忘记 setter,它们不必要地使您的类不变量复杂化。记住让你的类保持简单,而不是构造简单。

  • 默认构造并init()采用 ctor 参数:这无非是一个返回 的函数optional<>,但需要两个构造并弄乱您的不变量。

  • Takebool& succeed:这就是我们之前所做的optional<>。原因optional<>是优越的,您不能错误地(或粗心地!)忽略succeed标志并继续使用部分构造的对象。

  • 返回指针的工厂:这不太通用,因为它强制动态分配对象。要么返回给定类型的托管指针(并因此限制分配/范围模式),要么返回裸 ptr 并冒客户端泄漏的风险。此外,在移动原理图的性能方面,这可能变得不太理想(本地人,当保持在堆栈上时,速度非常快且对缓存友好)。

Example:

例子:

#include <iostream>
#include <experimental/optional>
#include <cmath>

class C
{
public:
    friend std::ostream& operator<<(std::ostream& os, const C& c)
    {
        return os << c.m_d << " " << c.m_sqrtd;
    }

    static std::experimental::optional<C> construct(const double d)
    {
        if (d>=0)
            return C(d, sqrt(d));

        return std::experimental::nullopt;
    }

    template<typename Success, typename Failed>
    static auto if_construct(const double d, Success success, Failed failed = []{})
    {
        return d>=0? success( C(d, sqrt(d)) ): failed();
    }

    /*C(const double d)
    : m_d(d), m_sqrtd(d>=0? sqrt(d): throw std::logic_error("C: Negative d"))
    {
    }*/
private:
    C(const double d, const double sqrtd)
    : m_d(d), m_sqrtd(sqrtd)
    {
    }

    double m_d;
    double m_sqrtd;
};

int main()
{
    const double d = 2.0; // -1.0

    // method 1. Named optional
    if (auto&& COpt = C::construct(d))
    {
        C& c = *COpt;
        std::cout << c << std::endl;
    }
    else
    {
        std::cout << "Error in 1." << std::endl;
    }

    // method 2. Functional style
    C::if_construct(d, [&](C c)
    {
        std::cout << c << std::endl;
    },
    []
    {
        std::cout << "Error in 2." << std::endl;
    });
}

回答by jcoder

My suggestion for this specific situation is that if you don't want a constuctor to fail because if can't open a file, then avoid that situation. Pass in an already open file to the constructor if that's what you want, then it can't fail...

我对这种特定情况的建议是,如果您不希望构造函数因为无法打开文件而失败,请避免这种情况。如果这是您想要的,则将已经打开的文件传递给构造函数,然后它就不会失败......

回答by CashCow

A constructor may well open a file (not necessarily a bad idea) and may throw if the file-open fails, or if the input file does not contain compatible data.

构造函数很可能打开一个文件(不一定是坏主意),如果文件打开失败,或者输入文件不包含兼容数据,则可能会抛出异常。

It is reasonable behaviour for a constructor to throw an exception, however you will then be limited as to its use.

构造函数抛出异常是合理的行为,但是您将受到其使用的限制。

  • You will not be able to create static (compilation unit file-level) instances of this class that are constructed before "main()", as a constructor should only ever be thrown in the regular flow.

  • This can extend to later "first-time" lazy evaluation, where something is loaded the first time it is required, for example in a boost::once construct the call_once function should never throw.

  • You may use it in an IOC (Inversion of Control / Dependency Injection) environment. This is why IOC environments are advantageous.

  • Be certain that if your constructor throws then your destructor will not be called. So anything you initialised in the constructor prior to this point must be contained in an RAII object.

  • More dangerous by the way can be closing the file in the destructor if this flushes the write buffer. No way at all to handle any error that may occur at this point properly.

  • 您将无法创建在“main()”之前构造的此类的静态(编译单元文件级)实例,因为构造函数只应在常规流程中抛出。

  • 这可以扩展到以后的“第一次”延迟评估,在第一次需要时加载某些东西,例如在 boost::once 构造中,call_once 函数永远不应该抛出。

  • 您可以在 IOC(控制反转/依赖注入)环境中使用它。这就是 IOC 环境具有优势的原因。

  • 确保如果您的构造函数抛出异常,那么您的析构函数将不会被调用。因此,您在此之前在构造函数中初始化的任何内容都必须包含在 RAII 对象中。

  • 更危险的是,如果这会刷新写入缓冲区,则可能会在析构函数中关闭文件。完全没有办法正确处理此时可能发生的任何错误。

You can handle it without an exception by leaving the object in a "failed" state. This is the way you must do it in cases where throwing is not permitted, but of course your code must check for the error.

您可以通过让对象处于“失败”状态来毫无例外地处理它。在不允许抛出的情况下,您必须这样做,但当然您的代码必须检查错误。

回答by Edward Strange

I want to open a file in a class constructor.

我想在类构造函数中打开一个文件。

Almost certainly a bad idea. Very few cases when opening a file during construction is appropriate.

几乎肯定是个坏主意。在构建期间打开文件的情况很少是合适的。

It is possible that the opening could fail, then the object construction could not be completed. How to handle this failure? Throw exception out?

有可能打开失败,然后对象构建无法完成。如何处理这个故障?抛出异常?

Yep, that'd be the way.

是的,就是这样。

If this is possible, how to handle it in a non-throw constructor?

如果这是可能的,如何在非抛出构造函数中处理它?

Make it possible that a fully constructed object of your class can be invalid. This means providing validation routines, using them, etc...ick

使您的类的完全构造的对象可能无效。这意味着提供验证例程,使用它们等...ick

回答by sashoalm

One way is to throw an exception. Another is to have a 'bool is_open()' or 'bool is_valid()' functuon that returns false if something went wrong in the constructor.

一种方法是抛出异常。另一个是有一个 'bool is_open()' 或 'bool is_valid()' 函数,如果构造函数出现问题,它会返回 false。

Some comments here say it's wrong to open a file in the constructor. I'll point out that ifstream is part of the C++ standard it has the following constructor:

这里的一些评论说在构造函数中打开文件是错误的。我会指出 ifstream 是 C++ 标准的一部分,它具有以下构造函数:

explicit ifstream ( const char * filename, ios_base::openmode mode = ios_base::in );

It doesn't throw an exception, but it has an is_open function:

它不会抛出异常,但它有一个 is_open 函数:

bool is_open ( );