我想在类构造函数中打开文件。打开可能会失败,然后可能无法完成对象构造。如何处理此故障?抛出异常?如果可能的话,如何在非抛出构造函数中处理它?
我想在类构造函数中打开文件。打开可能会失败,然后可能无法完成对象构造。如何处理此故障?抛出异常?如果可能的话,如何在非抛出构造函数中处理它?
Answers:
如果对象构造失败,则引发异常。
替代方案是可怕的。如果构造成功,则必须创建一个标志,并在每种方法中进行检查。
istream&参数:)
我想在类构造函数中打开文件。打开可能会失败,然后可能无法完成对象构造。如何处理此故障?抛出异常?
是。
如果可能的话,如何在非抛出构造函数中处理它?
您的选择是:
if (X x) ...即,可以在布尔上下文中评估对象,通常可以通过提供operator bool() const或类似的整数转换来进行评估),但是这样就没有x范围了查询错误的详细信息。从例如这可能是熟悉的if (std::ifstream f(filename)) { ... } else ...;bool worked; X x(&worked); if (worked) ...
if (X* p = x_factory()) ...</li>
<li>X x; //永远无法使用;如果(init_x(&x))...简而言之,C ++旨在为此类问题提供优雅的解决方案:在这种情况下为例外。如果您人为地限制自己使用它们,那么不要指望其他东西能起到一半的作用。
(PS我喜欢传递将由指针修改的变量(worked如上所述),我知道FAQ精简版不鼓励使用它,但不同意推理。除非您没有FAQ所涵盖的内容,否则对讨论不特别感兴趣。)
新的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。
std::variant<ValueType, ErrorInfo>-也不要求可变输入。去boost::variant,如果你没有在你的编译器呢。
我想在类构造函数中打开文件。
几乎可以肯定是个坏主意。在构建期间打开文件的情况很少。
打开可能会失败,然后可能无法完成对象构造。如何处理此故障?抛出异常?
是的,那就是这样。
如果可能的话,如何在非抛出构造函数中处理它?
使您的类的完全构造的对象可能无效。这意味着提供验证例程,使用它们,等等...
istream或ostream对象编写类。这样,您可以进行测试以用stringstream替换流。
open在类中显式使用a ,可以防止例如内存映射文件的任何重用?另一方面,通过使用基类(类似流),您可以传入实现其接口的任何内容。这使得测试和重用变得更加容易。
std::fstream在其构造函数中打开文件。
一种方法是引发异常。另一个是拥有一个'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 ( );
ifstream是一个RAII对象,它管理对文件的引用。与大多数“在其构造函数中打开文件的类”(无论如何我已经看到)相比,情况截然不同。好的C ++代码也没有显式的delete语句,但是没有它就无法实现智能指针。
构造函数很可能会打开文件(不一定是个坏主意),并且如果文件打开失败或输入文件不包含兼容数据,则可能会抛出该异常。
构造函数抛出异常是合理的行为,但是您将对其使用有所限制。
您将无法创建在“ main()”之前构造的此类的静态(编译单元文件级)实例,因为构造器只能在常规流程中抛出。
这可以扩展到以后的“首次”惰性评估,在这种评估中,第一次需要加载某些内容时,例如在boost :: once构造中,永远不要抛出call_once函数。
您可以在IOC(控制反转/依赖注入)环境中使用它。这就是为什么IOC环境具有优势的原因。
确定如果构造函数抛出异常,则不会调用析构函数。因此,在此之前在构造函数中初始化的所有内容都必须包含在RAII对象中。
顺便说一句,如果刷新写入缓冲区,则更危险的是关闭析构函数中的文件。根本无法正确处理此时可能发生的任何错误。
您可以通过使对象处于“失败”状态来毫无例外地处理它。这是在不允许抛出的情况下必须执行的方法,但是当然您的代码必须检查错误。
使用工厂。
工厂可以是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或其他“实际工作”的任何东西。构造函数是该语言的一种特殊情况,它们基本上缺乏执行错误处理的能力。