我目前负责在我们的代码库中查找所有不良做法,并说服我的同事修复有问题的代码。在我的洞穴探险期间,我注意到这里有很多人使用以下模式:
class Foo
{
public:
Foo() { /* Do nothing here */ }
bool initialize() { /* Do all the initialization stuff and return true on success. */ }
~Foo() { /* Do all the cleanup */ }
};
现在我可能错了,但对我来说这个initialize()
方法很糟糕。我相信它取消了拥有构造函数的整个目的。
当我问我的同事为什么做出这个设计决定时,他们总是回答说他们别无选择,因为你不能在没有投掷的情况下退出构造函数(我猜他们认为投掷总是坏)
到目前为止我没能说服他们,我承认我可能缺乏有价值的论据......所以这是我的问题:我是对的,这种结构是一种痛苦,如果是这样,你看到了什么问题它?
谢谢。
答案 0 :(得分:8)
单步(构造函数)初始化和两步(使用init方法)初始化都是有用的模式。我个人认为排除任何一个是错误的,尽管如果你的约定完全禁止使用例外,那么你禁止对可能失败的构造函数进行单步初始化。
一般来说,我更喜欢单步初始化,因为这意味着您的对象可以拥有 更强的不变量。当我认为对象能够以“未初始化”状态存在时,我只使用两步初始化。
通过两步初始化,您的对象处于未初始化状态是有效的 - 因此每个与该对象一起工作的方法都需要知道并正确处理 事实上,它可能处于未初始化的状态。这类似于使用指针,假设指针不是NULL,它的形式很差。相反,如果您在构造函数中进行了所有初始化并且出现异常,则可以将“对象始终初始化”添加到不变量列表中,因此它变得更容易,更安全 做出关于对象状态的假设。
答案 1 :(得分:4)
这通常称为两阶段或多阶段初始化,它特别糟糕,因为一旦构造函数调用成功完成,您应该有一个可以使用的对象,在这种情况下,您将无法使用宾语。
我不禁对以下内容更加强调:
在失败的情况下从构造函数中抛出异常是处理对象构造失败的最佳和唯一简洁方法。
答案 2 :(得分:1)
这取决于对象的语义。如果初始化是对类本身的数据结构至关重要的事情,那么通过从构造函数中抛出异常(例如,如果你的内存不足)或者通过断言(如果你知道你的代码实际上不应该失败。
另一方面,如果构造的成功或否则取决于用户输入,则失败不是异常情况,而是您需要测试的正常预期运行时行为的一部分。在这种情况下,您应该有一个默认构造函数,它创建一个处于“无效”状态的对象,以及一个初始化函数,可以在构造函数或更高版本中调用,并且可能成功也可能不成功。以std::ifstream
为例。
所以你班级的骨架看起来像这样:
class Foo
{
bool valid;
bool initialize(Args... args) { /* ... */ }
public:
Foo() : valid(false) { }
Foo(Args... args) : valid (false) { valid = initialize(args...); }
bool reset(Args... args) // atomic, doesn't change *this on failure
{
Foo other(args...);
if (other) { using std::swap; swap(*this, other); return true; }
return false;
}
explicit operator bool() const { return valid; }
};
答案 3 :(得分:0)
这取决于具体情况。
如果构造函数因某些参数而失败,则应抛出异常。但是,当然,您需要记录自己从构造函数中抛出异常。
如果Foo
包含对象,它们将在initialize
方法中初始化两次,一次在构造函数中,这是一个缺点。
IMO,最大的缺点是你必须记住打电话给initialize
。如果对象无效,创建对象的意义何在?
因此,如果他们唯一的论点是他们不想从构造函数中抛出异常,那么这是一个非常糟糕的论点。
但是,如果他们想要某种延迟初始化,那么它是有效的。
答案 4 :(得分:0)
这是一种痛苦,但如果你想避免从构造函数中抛出异常,你别无选择。还有另一种选择,同样痛苦:在构造函数中进行所有初始化,然后你必须检查对象是否已成功构建(例如转换运算符为bool或IsOK
方法)。生活很艰难,.....然后你就死了:(
答案 5 :(得分:0)
我意识到这是一个非常老的线程,但是我想添加一些未明确说明(或可能只是暗示)的内容。在C ++中,当构造函数引发异常时,该对象不被视为“构造的”对象,因此不会在异常展开过程中调用其析构函数。
对于使用initialize()方法而不是在构造函数中进行操作,这可能是一个非常真实的激励因素。如果构造函数抛出异常,则复杂的对象需要大量的内存分配等工作,因此必须手动释放所有工作。
如果使用initialize()方法,则在初始化时对象已经“构造”,因此将调用对象的析构函数。
因此,是的,在构造函数中进行初始化是“更简单的”,但是如果出现问题,这也给程序员正确清理提供了更大的负担。零星的清理方法将使代码非常难看。
因此,在某些情况下,接受实用主义而非理想主义可能更好。