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

gxwragnw  于 2023-06-25  发布在  其他
关注(0)|答案(8)|浏览(157)

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

lsmepo6l

lsmepo6l1#

如果对象构造失败,则引发异常。
另一种选择是可怕的。如果构造成功,您必须创建一个标志,并在每个方法中检查它。

cu6pst1q

cu6pst1q2#

我想在类构造函数中打开一个文件。可能的是,开口可能失败,然后对象构造不能完成。该如何处理这种失败?抛出异常?
是的
如果可能,如何在非抛出构造函数中处理它?
您的选项包括:

*重新设计应用程序,使其不需要构造函数是非抛出的-真的,如果可能的话,就这么做

  • 添加标志并测试成功构建
  • 您可以让每个成员函数在构造函数测试标志后立即被合法调用,理想情况下,如果它被设置,则抛出,否则返回错误代码
    • 如果你有一群不稳定的开发人员在编写代码,这很难保持正确。*
  • 你可以通过让对象多形地服从两个实现中的任何一个来获得一些编译时检查:一个成功构建的版本和一个总是出错的版本,但这会引入堆使用和性能成本。
  • 您可以将检查标志的负担从被调用的代码转移到被调用者,方法是在使用对象之前记录一个要求,即他们调用一些“is_valid()”或类似的函数:同样,容易出错,丑陋,但更分散,无法执行和失控。
  • 如果您支持以下内容,您可以使呼叫者更容易和更本地化:if (X x) ...(即对象可以在布尔上下文中求值,通常通过提供operator bool() const或类似的整数转换),但是你没有x的作用域来查询错误的详细信息。这可能是熟悉的,例如。if (std::ifstream f(filename)) { ... } else ...;
  • 让呼叫者提供一个他们负责打开的流。(称为依赖注入或DI)...在某些情况下,这并不奏效:
  • 当你在构造函数中使用流的时候,仍然会有错误,然后呢?
  • 文件本身可能是一个实现细节,应该是你的类私有的,而不是暴露给调用者:如果您以后想删除该要求,该怎么办?例如:您可能已经从一个文件中阅读了预先计算结果的查找表,但计算速度如此之快,以至于不需要预先计算--在客户端使用的每个点上删除文件是痛苦的(有时甚至在企业环境中不切实际),并且强制进行更多的重新编译,而不是潜在的简单的重新链接。
  • 强制调用者为构造函数设置的success/failure/error-condition变量提供缓冲区:例如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;//永远不可用; if(init_x(&x))...`
  • 等等

简而言之,C++旨在为这些问题提供优雅的解决方案:在这种情况下例外。如果你人为地限制自己使用它们,那么不要指望有其他东西能做到一半好。
(P.S.我喜欢传递变量,将被指针修改-根据worked上面-我知道FAQ精简版不鼓励它,但不同意推理。我对这方面的讨论并不特别感兴趣,除非你有一些FAQ没有涉及的问题。)

4urapxun

4urapxun3#

新的C++标准在很多方面重新定义了这个问题,是时候重新审视这个问题了。
最佳选择:

    • 命名可选 *:有一个最小的私有构造函数和一个命名构造函数:static std::experimental::optional<T> construct(...)。后者尝试设置成员字段,确保不变,并且只有在确定成功的情况下才调用私有构造函数。私有构造函数仅填充成员字段。测试可选代码很容易,而且价格便宜(在一个好的实现中,甚至可以省去副本)。
    • 功能型 *:好消息是,(非命名的)构造函数从来都不是虚的。因此,您可以用静态模板成员函数替换它们,除了构造函数参数之外,该函数还包含两个(或更多)lambda:一个是成功一个是失败“真实的”构造函数仍然是私有的,不能失败。这听起来可能有点过分,但是lambdas可以被编译器很好地优化。您甚至可以通过这种方式省去可选的if

好的选择:

    • 例外情况 *:如果所有其他方法都失败了,那么使用异常--但是请注意,在静态初始化过程中,您无法捕获异常。在这种情况下,一个可能的解决方法是让函数的返回值初始化对象。
    • 生成器类 *:如果构造很复杂,那么就有一个类来进行验证,并可能进行一些预处理,直到操作不会失败。让它有一种返回状态的方法(是的,错误函数)。我个人会让它只放在堆叠上,这样人们就不会传来传去;然后让它有一个.build()方法来构造另一个类。如果builder是friend,constructor可以是private。它甚至可能需要一些只有builder才能构造的东西,这样就可以证明这个构造函数只能由builder调用。

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

  • Flag:不要因为有一个“无效”状态而搞砸你的类不变式。这就是为什么我们有optional<>。想想optional<T>可以是无效的,T不能。仅对有效对象起作用的(成员或全局)函数对T起作用。一个肯定返回有效的方法适用于T。一个可能返回无效对象的方法返回optional<T>。一个可能使对象无效的方法取nonconst optional<T>&optional<T>*。这样,你就不需要检查每个函数来证明你的对象是有效的(而那些if可能会变得有点昂贵),但是在构造函数上也不会失败。
    • 默认构造和setters*:这基本上与Flag相同,只是这次您被迫使用可变模式。忘记setter吧,它们不必要地复杂化你的类不变式。记住保持类简单,而不是构造简单。
    • 默认构造和接受ctor参数的init() *:这并不比返回optional<>的函数更好,但需要两个构造,并打乱了你的不变量。
    • 乘坐bool& succeed *:这就是我们在optional<>之前所做的。optional<>优越的原因是,你不能错误地(或不小心!)忽略succeed标志并继续使用部分构造的对象。
    • 返回指针的工厂 *:这不太通用,因为它强制动态分配对象。要么返回给定类型的托管指针(并因此限制分配/作用域架构),要么返回裸ptr并冒着客户端泄漏的风险。此外,对于move schematics性能方面,这可能变得不太理想(局部变量,当保持在堆栈上时,非常快且缓存友好)。

示例:

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

    // this version keeps inputs in alternative path
    template<typename Success, typename Alternative>
    static auto in_case_constructible(const double d, Success success, Alternative alternative)
    {
        return d>=0? success( C(d, sqrt(d)) ): alternative(d);
    }

    /*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;
    });
}
kxkpmulp

kxkpmulp4#

对于这种情况,我的建议是,如果你不想让一个构造器因为无法打开文件而失败,那么就避免这种情况。向构造函数传递一个已经打开的文件,如果这是你想要的,那么它就不会失败...

zdwk9cvp

zdwk9cvp5#

一种方法是抛出异常。另一种方法是有一个'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 ( );
jaql4c8m

jaql4c8m6#

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?
使类的完全构造的对象可能无效。这意味着提供验证例程,使用它们,等等。

cwtwac6a

cwtwac6a7#

构造函数可以打开一个文件(不一定是个坏主意),如果文件打开失败,或者如果输入文件不包含兼容的数据,则可能会抛出。
构造函数抛出异常是合理的行为,但是您将受到其使用的限制。

  • 您将无法创建在“main()"之前构造的此类的静态(编译单元文件级)示例,因为构造函数只应在常规流中抛出。
  • 这可以扩展到以后的“首次”延迟求值,即在第一次需要时加载某些内容,例如在boost::once构造中,call_once函数永远不会抛出。
  • 您可以在IOC(控制反转/依赖注入)环境中使用它。这就是为什么IOC环境是有利的。
  • 确保如果你的构造函数抛出,那么你的析构函数将不会被调用。因此,在此之前在构造函数中初始化的任何内容都必须包含在RAII对象中。
  • 更危险的是,如果在析构函数中关闭文件会刷新写缓冲区,那么这种做法可能会更危险。根本没有办法正确处理此时可能发生的任何错误。

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

pgvzfuti

pgvzfuti8#

使用工厂。
工厂可以是一个完整的工厂类“Factory<T>”,用于构建“T”对象(它不一定是模板),也可以是“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或其他“真实的工作”的东西。构造函数是语言中的一个特殊情况,它们基本上缺乏处理错误的能力。

相关问题