我目前正和我的老师讨论关于课堂设计的问题,我们谈到Initialize()
职能,他大力宣传。例如:
class Foo{
public:
Foo()
{ // acquire light-weight resources only / default initialize
}
virtual void Initialize()
{ // do allocation, acquire heavy-weight resources, load data from disk
}
// optionally provide a Destroy() function
// virtual void Destroy(){ /*...*/ }
};
当然,所有东西都带有可选参数。
现在,他还强调了类层次结构中的可扩展性和使用(他是一个游戏开发者,他的公司出售游戏引擎),并带有以下参数(逐字逐句,仅翻译):
反对构造函数的论点:
Initialize()
函数的参数:
我一直被教导直接在构造函数中进行实际初始化,并且不提供这样的Initialize()
函数。也就是说,我肯定没有他在部署库/引擎时那么多的经验,所以我想我会问好吧。
那么,支持和反对这些Initialize()
函数的论据到底是什么?它是否取决于应该使用的环境?如果是,请为图书馆/引擎开发人员提供推理,如果可以的话,请为游戏开发人员提供推理。
编辑:我应该提到过,这些类只会在其他类中用作成员变量,因为其他任何东西对它们都没有意义。遗憾。
答案 0 :(得分:7)
对于Initialize
:正是您的老师所说的,但在精心设计的代码中,您可能永远不会需要它。
反对:非标准,如果虚假使用,可能会破坏构造函数的目的。更重要的是:客户需要记得致电Initialize
。因此,任何一个实例在构建时都会处于不一致状态,或者它们需要大量额外的簿记来防止客户端代码调用其他任何内容:
void Foo::im_a_method()
{
if (!fully_initialized)
throw Unitialized("Foo::im_a_method called before Initialize");
// do actual work
}
防止此类代码的唯一方法是开始使用工厂函数。因此,如果您在每个类中使用Initialize
,则每个层次结构都需要一个工厂。
换句话说:如果没有必要,不要这样做;始终检查代码是否可以根据标准结构重新设计。当然不要添加public Destroy
成员,这是析构函数的任务。无论如何,析构函数都可以(并且在继承情况下必须是virtual
。
答案 1 :(得分:5)
我反对C ++中的'双初始化'。
反对构造函数的论点:
- 无法被派生类重写
- 无法调用虚拟功能
如果必须编写此类代码,则表示您的设计错误(例如MFC)。设计你的基类,以便可以覆盖的所有必要信息都通过其构造函数的参数传递,因此派生类可以像这样覆盖它:
Derived::Derived() : Base(GetSomeParameter())
{
}
答案 2 :(得分:5)
这是一个可怕的,可怕的想法。问问自己 - 如果你以后必须打电话给Initialize()
,那么构造函数的重点是什么?如果派生类想要覆盖基类,则不派生。
当构造函数完成时,使用该对象应该是有意义的。如果没有,你做错了。
答案 3 :(得分:4)
在构造函数中优先初始化的一个参数:它使得更容易确保每个对象都具有有效状态。使用两阶段初始化,有一个窗口,其中对象格式不正确。
反对使用构造函数的一个论点是,发出问题的唯一方法是抛出异常;没有能力从构造函数返回任何东西。
单独的初始化函数的另一个好处是它可以更容易地支持具有不同参数列表的多个构造函数。
与所有事情一样,这实际上是一个设计决策,应该根据手头问题的具体要求做出,而不是全面推广。
答案 4 :(得分:3)
这里有一种纠纷的声音。
您可能正在一个除了分离构造和初始化之外别无选择的环境中工作。欢迎来到我的世界。不要告诉我找到不同的环境; 我别无选择。我创建的产品的首选实施例不在我手中。
告诉我如何针对对象C初始化对象B的某些方面,关于对象A的其他方面;关于对象B的对象C的一些方面,关于对象A的其他方面。下一次情况可以很好地逆转。我甚至不会讨论如何初始化对象A.可以解决明显循环的初始化依赖关系,但不能解析构造函数。
类似的问题涉及破坏与关闭。该对象可能需要经过关闭,它可能需要重新用于蒙特卡罗目的,并且可能需要从三个月前转储的检查点重新启动。将所有释放代码直接放在析构函数中是一个非常糟糕的主意,因为它会泄漏。
答案 5 :(得分:1)
忘记Initialize()
函数 - 这是构造函数的作用。
创建对象时,如果构造成功传递(没有抛出异常),则应该完全初始化对象。
答案 6 :(得分:1)
虽然我同意在构造函数中完全进行初始化的缺点,但我认为这些实际上是设计不良的迹象。
派生类应该不需要完全覆盖基类初始化行为。这是一个应该治愈的设计缺陷,而不是引入Initialize()
- 函数作为解决方法。
答案 7 :(得分:1)
不会轻易地调用Initialize
,并且不会为您提供正确构造的对象。它也不遵循RAII原则,因为在构造/破坏对象时有单独的步骤:如果Initialize
失败会发生什么(如何处理无效对象)?
通过强制默认初始化,您可能最终会在构造函数中进行更多的工作。
答案 8 :(得分:1)
忽略其他人已经充分涵盖的RAII含义,虚拟初始化方法使您的设计大大复杂化。您不能拥有任何私有数据,因为为了能够覆盖初始化例程,所有派生对象都需要访问它。所以现在类的不变量不仅需要由类维护,还要由每个继承自它的类维护。首先避免这种负担是继承背后的一部分,构造函数的工作方式与子对象创建方式相同。
答案 9 :(得分:1)
其他人一直反对使用Initialize
,我自己也看到了一种用法:懒惰。
例如:
File file("/tmp/xxx");
foo(file);
现在,如果foo
从不使用file
(毕竟),那么完全没必要尝试阅读它(而且确实会浪费资源)。
在此情况下,我支持延迟初始化,然而它不应该依赖于调用该函数的客户端,而是依赖于每个成员函数应该检查是否有必要初始化。在此示例中,name()
不需要它,但encoding()
会这样做。
答案 10 :(得分:0)
如果您在创建时没有可用的数据,则仅使用初始化函数。
例如,您正在动态构建数据模型,并且必须在描述对象参数的数据之前使用确定对象层次结构的数据。
答案 11 :(得分:0)
如果您使用它,那么您应该将构造函数设为私有,并使用工厂方法代替为您调用initialize()
方法。例如:
class MyClass
{
public:
static std::unique_ptr<MyClass> Create()
{
std::unique_ptr<MyClass> result(new MyClass);
result->initialize();
return result;
}
private:
MyClass();
void initialize();
};
也就是说,初始化方法不是很优雅,但是出于老师所说的确切原因它们可能很有用。我本不认为他们“错”。如果您的设计很好,那么您可能永远不会需要它们。但是,现实生活中的代码有时会迫使你做出妥协。
答案 12 :(得分:0)
有些成员必须在构造时具有值(例如,引用,const
值,为RAII设计的对象,没有默认构造函数)......它们不能在initialise()
函数中构造,有些人不能重新分配。
因此,一般情况下,它不是构造函数与initialise()
的选择,而是您是否最终会将代码分割为两者之间的问题。
以后可以初始化的基础和成员,对于派生类来说,这意味着它们不是private
;如果你为了延迟初始化而使基础/成员非 - private
,你打破封装 - OOP的核心原则之一。打破封装可以防止基类开发人员推断类应该保护的不变量;他们无法开发他们的代码而不会有破坏派生类的风险 - 他们可能无法看到它们。
其他时候它可能但有时效率低下如果你必须默认构建一个你永远不会使用的值的基数或成员,那么很快就会为它分配一个不同的值。优化器可能有所帮助 - 特别是如果两个函数都被内联并快速连续调用 - 但可能不会。
- [构造函数]无法被派生类重写
...所以你实际上可以依赖他们做基类需要的东西......
- [constructors]无法调用虚函数
CRTP允许派生类注入功能 - 这通常是比单独的initialise()
例程更好的选择,速度更快。
Initialize()函数的参数:
- 派生类可以完全替换初始化代码
如上所述,我说这是反对的论点。
- 派生类可以在自己的初始化期间随时进行基类初始化
这是灵活但有风险的 - 如果基类没有初始化,派生类很容易最终(由于代码演变过程中的疏忽)调用依赖于该基础的东西被初始化并且因此在运行时失败。
更一般地说,存在可靠的调用,使用和错误处理的问题。对于initialise
,客户端代码必须记住在运行时而非编译时显而易见的故障。可以使用返回类型而不是异常或状态来报告问题,这有时会更好。
如果需要调用initialise()
来设置指向nullptr
的指针或者将析构函数的值安全地设置为delete
,但是其他一些数据成员或代码首先抛出,所有地狱打破了。
initialise()
也强制整个类在客户端代码中不是const
,即使客户只是想创建一个初始状态并确保它不会被进一步修改 - 基本上你已经抛出了const
- 窗外的正确性。
代码执行p_x = new X(values, for, initialisation);
,f(X(values, for initialisation)
,v.push_back(X(values, for initialisation))
这样的事情是不可能的 - 强制啰嗦和笨拙的替代方案。
如果还使用destroy()
功能,上述许多问题都会加剧。