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

Tho*_*son 64 c++

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

BЈо*_*вић 36

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

替代方案很可怕.如果构造成功,则必须创建一个标志,并在每个方法中检查它.

  • 实际上提出一个"真正的"解决方案也很棒,例如传递`istream&`参数:) (4认同)
  • @Nils 在任何正常的代码审查中这都会被称为令人厌恶的事情。另外,在对其进行操作之前,您必须检查该对象是否处于正常状态。 (2认同)

Ton*_*roy 27

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

是.

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

你的选择是:

  • 重新设计应用程序,因此它不需要构造函数非投掷 - 真的,如果可能的话,做它
  • 添加一个标志并测试是否成功构建
    • 你可以让每个成员函数在构造函数测试标志后立即被合法地调用,理想情况下抛出它是否已设置,否则返回错误代码
      • 这很丑陋,如果你有一群不稳定的开发人员在编写代码,那么很难保持正确.
      • 您可以通过让对象多态地遵循两个实现中的任何一个来获得一些编译时检查:成功构造的一个和始终错误的版本,但这会引入堆使用和性能成本.
    • 您可以通过在使用对象之前记录他们调用某些"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; //永远不会用 if(init_x(&x))...`
    • 等等...

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

(PS我喜欢传递将被指针修改的变量 - worked如上所述 - 我知道FAQ lite不鼓励它但不同意推理.除非你没有涉及FAQ的内容,否则对它的讨论并不特别感兴趣.)


jco*_*der 15

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

  • 海报只表示他想在构造函数中打开文件.显然,如果他做得更多,那么它可能会以其他方式失败,需要妥善处理. (5认同)
  • 如果文件内容为空怎么办?或包含无效数据? (3认同)

lor*_*rro 13

新的C++标准在很多方面重新定义了它,以便重新审视这个问题.

最佳选择:

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

  • 功能风格:好消息是,(非命名)构造函数永远不是虚拟的.因此,您可以使用静态模板成员函数替换它们,该函数除了构造函数参数之外还需要两个(或更多)lambdas:如果成功则为1,如果失败则为1.'真正的'构造函数仍然是私有的,不能失败.这可能听起来有点矫枉过正,但编译器会对lambdas进行精彩优化.您甚至if可以通过这种方式省去可选项.

好的选择:

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

  • Builder类:如果构造很复杂,那么有一个类可以进行验证,并且可能需要进行一些预处理,以确保操作不会失败.让它有办法返回状态(是的,错误功能).我个人只是堆叠,所以人们不会传递它; 然后让它有一个.build()构造另一个类的方法.如果构建器是朋友,构造函数可以是私有的.甚至可能只需要构建器可以构造的内容,以便记录此构造函数仅由构建器调用.

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

  • 标志:不要因为"无效"状态而弄乱你的班级不变量.这正是我们拥有的原因optional<>.想想optional<T>那可能是无效的,T那不可能.仅适用于有效对象的(成员或全局)函数T.一个肯定会返回有效的作品T.可能返回无效对象的一个​​返回optional<T>.可能使对象无效的对象采用非const optional<T>&optional<T>*.这样,您就不需要检查对象有效的每个函数(并且那些if可能变得有点贵),但是在构造函数中也不会失败.

  • 默认构造和setter:这与Flag基本相同,只是这次你被迫拥有一个可变模式.忘记了setter,他们不必要地使你的类不变复杂化.记住保持你的课堂简单,而不是简单的建设.

  • 默认构造,init()它接受一个ctor参数:这没有什么比一个返回一个的函数更好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;
    });
}
Run Code Online (Sandbox Code Playgroud)


sas*_*alm 6

一种方法是抛出异常。另一个方法是使用“bool is_open()”或“bool is_valid()”函数,如果构造函数中出现问题,该函数将返回 false。

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

explicit ifstream ( const char * filename, ios_base::openmode mode = ios_base::in );
Run Code Online (Sandbox Code Playgroud)

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

bool is_open ( );
Run Code Online (Sandbox Code Playgroud)


Edw*_*nge 5

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

几乎可以肯定这是一个坏主意。在极少数情况下,在施工期间打开文件是合适的。

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

是的,就是这样。

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

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

  • 为什么在构造函数中打开文件或任何其他资源是一个坏主意? (6认同)
  • @Jörgen Sigvardsson:因为最好根据任何给定的“istream”或“ostream”对象编写类。这样您就可以测试用字符串流替换流。 (4认同)