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


78

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



1
“如果在构造函数中没有异常就不能使用可恢复的错误处理机制,那么就不要使用构造函数。” 在此处阅读更多信息:foonathan.net/2017/01/exceptions-constructor
加布里埃尔·斯台普斯

Answers:


39

如果对象构造失败,则引发异常。

替代方案是可怕的。如果构造成功,则必须创建一个标志,并在每种方法中进行检查。


5
最好也提出一个“真实的”解决方案,比如传递一个istream&参数:)
Matthieu M.

@Matthie是的,控制权倒置了。我一直忘记它。感谢您的提醒。
2011年


@Nils在任何正常的代码审查中都将被称为可憎的。另外,您必须先检查对象是否处于正常状态,然后再执行操作。
BЈовић

28

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

是。

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

您的选择是:

  • 重新设计应用程序,这样就不需要构造函数了-真的,如果可能的话
  • 添加标志并测试是否成功构建
    • 您可以让每个成员函数在构造函数测试完标志后立即合法调用,理想情况下,如果已设置该标志,则抛出该异常,否则返回错误代码
      • 如果您有一群不稳定的开发人员来处理代码,那么这很丑陋,并且很难保持正确。
      • 您可以通过使对象多态性地遵循以下两种实现之一来进行编译时检查:一个成功构造的对象和一个始终出错的版本,但这会导致堆使用和性能成本。
    • 您可以通过记录在使用对象之前要求他们调用某些“ 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>X x; //永远无法使用;如果(init_x(&x))...
    • 等等...

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

(PS我喜欢传递将由指针修改的变量(worked如上所述),我知道FAQ精简版不鼓励使用它,但不同意推理。除非您没有FAQ所涵盖的内容,否则对讨论不特别感兴趣。)


17

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

最佳选择:

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

  • 功能风格:好消息是,(未命名的)构造函数永远不会是虚拟的。因此,您可以用静态模板成员函数替换它们,除了构造函数参数外,该函数还需要两个(或多个)lambda:如果成功,则为一个,如果失败,则为一个。“实际”构造函数仍然是私有的,不会失败。这听起来似乎有些过分,但是编译器对lambda进行了优化。您甚至if可以通过这种方式保留可选的选项。

不错的选择:

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

  • Builder类:如果构造复杂,请使用一个类进行验证,并可能进行一些预处理,以确保操作不会失败。让它有一种返回状态的方法(是,错误功能)。我个人将其设置为仅堆栈,因此人们不会将其传递出去。然后让它有一个.build()构造另一个类的方法。如果builder是朋友,则constructor可以是私有的。甚至可能只有构建器才能构建某些东西,因此有文档证明此构建器只能由构建器调用。

错误的选择:(但屡次出现)

  • 标志:不要通过具有“无效”状态来使类不变。这正是我们拥有的原因optional<>。认为optional<T>那可能是无效的,T那不可能。仅对有效对象起作用的(成员或全局)函数在上起作用T。一种肯定会在上返回有效作品的作品T。一个可能返回无效对象return的对象optional<T>。一个可能会使对象无效的对象采用non-constoptional<T>&optional<T>*。这样,您将不需要检入对象有效的每个函数(那些函数if可能会变得有些昂贵),但也不必在构造函数中失败。

  • 默认的构造器和设置器:这与Flag基本相同,只是这次您被迫具有可变的模式。忘记设置器,不必要地使您的类不变式复杂化。记住要使您的班级简单而不是构造简单。

  • 默认构造并init()带有ctor参数:这比返回an的函数更好optional<>,但是需要两种构造并弄乱您的不变式。

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

  • 返回指针的工厂:这不太普遍,因为它强制动态分配对象。您要么返回给定类型的托管指针(并因此限制分配/作用域架构),要么返回裸ptr,并冒着客户泄漏的风险。同样,就移动原理图而言,就性能而言,这可能会变得不那么理想(局部变量在堆栈中时非常快速且对缓存友好)。

例:

#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;
    });
}

bool& succeed 实际上不必是笨蛋。它也可能是错误代码,它将为您提供更多信息,然后提供std :: optional。
甘妮亚·丹·安德烈

@GaneaDanAndrei:如果您的类型包含值或错误信息,则将其返回(而不是将其作为arg)。如果它仅包含错误信息,则您可能会返回std::variant<ValueType, ErrorInfo>-也不要求可变输入。去boost::variant,如果你没有在你的编译器呢。
lorro

15

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


3
如果文件内容为空怎么办?还是包含无效数据?
CashCow

5
张贴者只说他想在构造函数中打开文件。显然,如果他做得更多,那么它可能会以其他方式失败,并且需要适当地处理。
jcoder

4

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

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

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

是的,那就是这样。

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

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


6
为什么在构造函数中打开文件或任何其他资源是个坏主意?
约尔根Sigvardsson

3
@JörgenSigvardsson:因为最好用任何给定istreamostream对象编写类。这样,您可以进行测试以用stringstream替换流。
Billy ONeal

1
@Jorgen Sigvardsson:这与依赖注入哲学背道而驰。最好反过来阅读您的问题:为什么将您的手向后绑?通过open在类中显式使用a ,可以防止例如内存映射文件的任何重用?另一方面,通过使用基类(类似流),您可以传入实现其接口的任何内容。这使得测试和重用变得更加容易。
Matthieu M.

1
@Crazy:所以标准库也出错吗?std::fstream在其构造函数中打开文件。
jalf

1
@Matthieu:我不知道构造函数的工作量与DI有什么关系...?同样,“努力工作”构造函数的存在并不排除提供开放方法的可能性。
约尔根·西格瓦尔德森(JörgenSigvardsson)2011年

4

一种方法是引发异常。另一个是拥有一个'bool is_open()'或'bool is_valid()'函数,如果构造函数出错,则该函数返回false。

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

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

它不会引发异常,但是具有is_open函数:

bool is_open ( );

1
好吧,ifstream是一个RAII对象,它管理对文件的引用。与大多数“在其构造函数中打开文件的类”(无论如何我已经看到)相比,情况截然不同。好的C ++代码也没有显式的delete语句,但是没有它就无法实现智能指针。
Billy ONeal

只需提及一下:许多人不喜欢is_open()或is_valid()方法,并认为它很糟糕。这是因为该类的用户很容易忘记调用此方法,并且最终会得到部分构造的类,并且需要在许多成员函数中包含is_open()测试。在某些情况下,这可能是一个选择。
sstn 2011年

4

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

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

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

  • 这可以扩展到以后的“首次”惰性评估,在这种评估中,第一次需要加载某些内容时,例如在boost :: once构造中,永远不要抛出call_once函数。

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

  • 确定如果构造函数抛出异常,则不会调用析构函数。因此,在此之前在构造函数中初始化的所有内容都必须包含在RAII对象中。

  • 顺便说一句,如果刷新写入缓冲区,则更危险的是关闭析构函数中的文件。根本无法正确处理此时可能发生的任何错误。

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


0

使用工厂。

工厂可以是Factory<T>用于构建T对象的整个工厂类(不一定是模板),也可以是静态的public方法T。然后,使构造函数受到保护,并使析构函数公开。这样可以确保新类仍可以从“T ”但是除它们之外的任何外部代码都不能直接调用构造函数。

使用工厂方法(C ++ 17)

class Foo {
protected:            
   Foo() noexcept;                 // Default ctor that can't fail 
   virtual bool Initialize(..); // Parts of ctor that CAN fail 
public: 
   static std::optional<Foo>   Create(...) // 'Stack' or value-semantics version (no 'new')
   {
      Foo out();
      if(foo.Initialize(..)) return {out};
      return {};
   }
   static Foo* /*OR smart ptr*/ Create(...) // Heap version.
   {
      Foo* out = new Foo();
      if(foo->Initialize(...) return out;
      delete out;
      return nullptr;
   }
   virtual ~Foo() noexcept; // Keep public to allow normal inheritance
 };

与设置“有效”位或其他黑客不同,这是相对干净且可扩展的。如果做得正确,它可以确保没有无效的对象逃脱到野外,并且编写派生的“ Foo”仍然很简单。而且由于工厂函数是常规函数,因此您可以使用构造函数无法执行的其他操作。

以我的愚见,您永远不应将任何实际上可能会失败的代码放入构造函数中。这几乎意味着可以进行I / O或其他“实际工作”的任何东西。构造函数是该语言的一种特殊情况,它们基本上缺乏执行错误处理的能力。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.