C++ 为什么使用初始化方法而不是构造函数?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3786853/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Why use an initialization method instead of a constructor?
提问by bastibe
I just got into a new company and much of the code base uses initialization methods instead of constructors.
我刚进入一家新公司,大部分代码库使用初始化方法而不是构造函数。
struct MyFancyClass : theUberClass
{
MyFancyClass();
~MyFancyClass();
resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2,
redundantArgument arg3=TODO);
// several fancy methods...
};
They told me that this had something to do with timing. That some things have to be done afterconstruction that would fail in the constructor. But most constructors are empty and I don't really see any reason for not using constructors.
他们告诉我这与时间有关。在构造函数中必须完成的一些事情会在构造函数中失败。但是大多数构造函数都是空的,我真的看不出有什么理由不使用构造函数。
So I turn to you, oh wizards of the C++: why would you use an init-method instead of a constructor?
所以我转向你,哦,C++ 的巫师:你为什么要使用 init 方法而不是构造函数?
回答by Steve Jessop
Since they say "timing", I guess it's because they want their init functions to be able to call virtual functions on the object. This doesn't always work in a constructor, because in the constructor of the base class, the derived class part of the object "doesn't exist yet", and in particular you can't access virtual functions defined in the derived class. Instead, the base class version of the function is called, if defined. If it's not defined, (implying that the function is pure virtual), you get undefined behavior.
既然他们说“计时”,我猜是因为他们希望他们的 init 函数能够在对象上调用虚函数。这在构造函数中并不总是有效,因为在基类的构造函数中,对象的派生类部分“尚不存在”,特别是您无法访问派生类中定义的虚函数。而是调用函数的基类版本(如果已定义)。如果它没有定义,(暗示该函数是纯虚拟的),你会得到未定义的行为。
The other common reason for init functions is a desire to avoid exceptions, but that's a pretty old-school programming style (and whether it's a good idea is a whole argument of its own). It has nothing to do with things that can't work in a constructor, rather to do with the fact that constructors can't return an error value if something fails. So to the extent that your colleagues have given you the real reasons, I suspect this isn't it.
init 函数的另一个常见原因是希望避免异常,但这是一种非常老派的编程风格(它是否是一个好主意是一个完整的论点)。它与不能在构造函数中工作的事情无关,而是与构造函数在失败时无法返回错误值的事实有关。因此,就您的同事给您的真正原因而言,我怀疑这不是事实。
回答by Matthieu M.
Yes I can think of several, but generally it's not a good idea.
是的,我可以想到几个,但通常这不是一个好主意。
Most of the times the reason invoked is that you only report errors through exceptions in a constructor (which is true) whereas with a classic method you can return an error code.
大多数情况下,调用的原因是您仅通过构造函数中的异常报告错误(这是真的),而使用经典方法您可以返回错误代码。
However in properly designed OO-code the constructor is responsible for establishing the class invariants. By allowing a default constructor, you allow an empty class, thus you have to modify the invariants so that is accepted both the "null" class and the "meaningful" class... and each use of the class must first ensure that the object has been properly built... it's crass.
然而,在正确设计的 OO 代码中,构造函数负责建立类不变量。通过允许默认构造函数,您允许一个空类,因此您必须修改不变量,以便接受“空”类和“有意义”类......并且每次使用该类必须首先确保对象已经被正确地建造了……这太糟糕了。
So now, let's debunk the "reasons":
所以现在,让我们揭穿“原因”:
- I need to use a
virtual
method: use the Virtual Constructor idiom. - There is a lot of work to be done: so what, the work will be done anyway, just do it in the constructor
- The setup may fail: throw an exception
- I want to keep the partially initialized object: use a try/catch within the constructor and set the error cause in an object field, don't forget to
assert
at the beginning of each public method to make sure the object is usable before trying to use it. - I want to reinitialize my object: invoke the initialization method from the constructor, you'll avoid duplicate code while still having a fully initialized object
- I want to reinitialize my object (2): use
operator=
(and implement it using the copy and swap idiom if the compiler generated version does not suit your need).
- 我需要使用一种
virtual
方法:使用 Virtual Constructor 习语。 - 有很多工作要做:那又怎样,工作无论如何都会做,就在构造函数里做
- 设置可能失败:抛出异常
- 我想保留部分初始化的对象:在构造函数中使用 try/catch 并在对象字段中设置错误原因,不要忘记
assert
在每个公共方法的开头确保对象在尝试使用之前可用它。 - 我想重新初始化我的对象:从构造函数调用初始化方法,您将避免重复代码,同时仍然拥有一个完全初始化的对象
- 我想重新初始化我的对象 (2): use
operator=
(如果编译器生成的版本不适合您的需要,则使用复制和交换习语来实现它)。
As said, in general, bad idea. If you really want to have "void" constructor, make them private
and use Builder methods. It's as efficient with NRVO... and you can return boost::optional<FancyObject>
in case the construction failed.
如上所述,总的来说,这是个坏主意。如果您真的想要“void”构造函数,请制作它们private
并使用 Builder 方法。它与 NRVO 一样高效......boost::optional<FancyObject>
如果构建失败,您可以返回。
回答by Péter T?r?k
Others have listed lots of possible reasons (and proper explanations of why most of these are generally not a good idea). Let me post one example of a (more or less) valid use of init methods, which actually has to do with timing.
其他人列出了许多可能的原因(并正确解释了为什么其中大多数通常不是一个好主意)。让我发布一个(或多或少)有效使用 init 方法的例子,它实际上与时间有关。
In a previous project, we had lots of Service classes and objects, each of which were part of a hierarchy, and cross referencing each other in various ways. So typically, for creating a ServiceA, you needed a parent service object, which in turn needed a service container, which already depended on the presence of some specific services (possibly including ServiceA itself) at initialization time. The reason was that during initialization, most of the services registered itself with other services as listeners to specific events, and/or notified other services about the event of successful initialization. If the other service did not exist at the time of notification, the registration did not happen, thus this service would not receive important messages later, during the usage of the application. In order to break the chain of circular dependencies, we had to use explicit initialization methods separate from constructors, thus effectively making global service initialization a two-phase process.
在之前的项目中,我们有很多 Service 类和对象,每个类和对象都是层次结构的一部分,并以各种方式相互引用。因此,通常,为了创建 ServiceA,您需要一个父服务对象,而后者又需要一个服务容器,该服务容器在初始化时已经依赖于某些特定服务(可能包括 ServiceA 本身)的存在。原因是在初始化期间,大多数服务将自己注册为其他服务作为特定事件的侦听器,和/或将成功初始化的事件通知其他服务。如果在通知时其他服务不存在,则不会进行注册,因此在应用程序使用期间,此服务将不会在以后接收重要消息。为了打破循环依赖链,我们不得不使用与构造函数分离的显式初始化方法,从而有效地使全局服务初始化成为一个两阶段过程。
So, although this idiom should not be followed in general, IMHO it has some valid uses. However, it is best to limit its usage to the minimum, using constructors whenever possible. In our case, this was a legacy project, and we didn't yet fully understand its architecture. At least the usage of init methods was limited to the service classes - regular classes were initialized via constructors. I believe there might be a way to refactor that architecture to eliminate the need for service init methods, but at least I did not see how to do it (and to be frank, we had more urgent issues to deal with at the time I was part of the project).
因此,尽管一般不应遵循此习语,但恕我直言,它有一些有效用途。但是,最好将其使用限制在最低限度,尽可能使用构造函数。在我们的例子中,这是一个遗留项目,我们还没有完全理解它的架构。至少 init 方法的使用仅限于服务类 - 常规类是通过构造函数初始化的。我相信可能有一种方法可以重构该架构以消除对 service init 方法的需求,但至少我没有看到如何去做(坦率地说,当时我们有更紧迫的问题需要处理)项目的一部分)。
回答by gspr
Two reasons I can think of off the top of my head:
我能想到的两个原因:
- Say creating an object involves lots and lots of tedious work that can fail in lots and lots of horrible and subtle ways. If you use a short constructor to set up rudamentary things that won't fail, and then have the user call an initialization method to do the big job, you can at least be sure that you have some object created even if the big job fails. Maybe the object contains information about precisely what way the init failed, or maybe it's important to keep unsuccessfully initialized objects around for other reasons.
- Sometimes you might want to reinitialize an object long after it has been created. In this way, it's just a matter of calling the initialization method again without destroying and recreating the object.
- 假设创建一个对象涉及很多繁琐的工作,这些工作可能会以很多可怕和微妙的方式失败。如果您使用短构造函数来设置不会失败的基本事物,然后让用户调用初始化方法来完成大工作,那么即使大工作失败,您至少也可以确保创建了一些对象. 也许该对象包含有关 init 失败的确切方式的信息,或者由于其他原因保留未成功初始化的对象很重要。
- 有时您可能希望在对象创建很久之后重新初始化它。这样,只需再次调用初始化方法,而无需销毁和重新创建对象。
回答by Manoj R
One more use of such initialization can be in object pool. Basically you just request the object from the pool. The pool will have already some N objects created which are blank. It's the caller now which can call any method he/she likes to set the members. Once caller has done with the object it will tell the pool to destory it. The advantage is until the object is being used the memory will be saved, and the caller can use it's own suitable member method of initializing the object. An object may be serving a lot of purpose but the caller may not need all, and also may not need to initialize all the member of the objects.
这种初始化的另一种用途是在对象池中。基本上你只是从池中请求对象。该池将已经创建了一些空白的 N 个对象。现在是调用者可以调用他/她喜欢设置成员的任何方法。一旦调用者完成了对象,它就会告诉池销毁它。好处是直到对象被使用时,内存才会被保存,调用者可以使用它自己合适的成员方法来初始化对象。一个对象可能有很多用途,但调用者可能不需要全部,也可能不需要初始化对象的所有成员。
Typically think of database connections. A pool can have bunch of connection object, and the caller can fill the username, password etc.
通常会想到数据库连接。一个池可以有一堆连接对象,调用者可以填写用户名、密码等。
回答by TRISAbits
init() function are good when your compiler doesn't support exceptions, or your target application cannot use a heap (exception are usually implemented using a heap to create and destroy them).
当您的编译器不支持异常,或者您的目标应用程序不能使用堆(异常通常使用堆创建和销毁它们来实现)时,init() 函数是很好的。
init() routines are also useful when the order of construction needs to be defined. That is to say, if you globally allocate objects, the order in which the constructor is invoked is not defined. For instance:
当需要定义构造顺序时,init() 例程也很有用。也就是说,如果全局分配对象,调用构造函数的顺序是没有定义的。例如:
[file1.cpp]
some_class instance1; //global instance
[file2.cpp]
other_class must_construct_before_instance1; //global instance
The standard provides no guarantee that must_construct_before_instance1's constructor will be invoked before instance1's constructor. When it's tied to hardware, order in which things initialize can be crucial.
该标准不保证must_construct_before_instance1的构造函数将在instance1的构造函数之前被调用。当它与硬件相关联时,初始化的顺序可能至关重要。
回答by Christopher
It's useful for doing resource management. Say you have classes with destructors to automatically deallocate resources when the object's lifetime is over. Say you also have a class that holds these resource classes, and you initiate them in the constructor of this upper class. What happens when you use the assignment operator to initiate this higher class? Once the contents are copied, the old higher class becomes out of context, and the destructors are called for all the resource classes. If these resource classes have pointers that were copied during assignment, then all these pointers are now bad pointers. If you instead initiate the resource classes in a seperate init function in the higher class, you completely bypass the resource class' destructor from ever being called, because the assignment operator never has to create and delete these classes. I believe this is what was meant by the "timing" requirement.
它对进行资源管理很有用。假设您有带有析构函数的类,可以在对象的生命周期结束时自动释放资源。假设您还有一个包含这些资源类的类,并且您在这个上层类的构造函数中启动它们。当你使用赋值运算符来启动这个更高的类时会发生什么?一旦内容被复制,旧的更高类就会脱离上下文,并且所有资源类都会调用析构函数。如果这些资源类具有在分配期间复制的指针,那么所有这些指针现在都是坏指针。如果您改为在更高类中的单独 init 函数中启动资源类,您将完全绕过资源类的析构函数被调用,因为赋值运算符永远不必创建和删除这些类。我相信这就是“时间”要求的含义。
回答by Tarik
And also I like to attach a code sample to answer #1 --
而且我喜欢附上代码示例来回答 #1 --
Since also msdn says :
由于 msdn 也说:
When a virtual method is called, the actual type that executes the method is not selected until run time. When a constructor calls a virtual method, it is possible that the constructor for the instance that invokes the method has not executed.
当调用虚拟方法时,直到运行时才选择执行该方法的实际类型。当构造函数调用虚方法时,调用该方法的实例的构造函数可能尚未执行。
Example :The following example demonstrates the effect of violating this rule. The test application creates an instance of DerivedType, which causes its base class (BadlyConstructedType) constructor to execute. BadlyConstructedType's constructor incorrectly calls the virtual method DoSomething. As the output shows, DerivedType.DoSomething() executes, and does so before DerivedType's constructor executes.
示例:以下示例演示了违反此规则的影响。测试应用程序创建 DerivedType 的一个实例,这会导致其基类 (BadlyConstructedType) 构造函数执行。BadlyConstructedType 的构造函数错误地调用了虚方法 DoSomething。如输出所示,DerivedType.DoSomething() 执行,并且在 DerivedType 的构造函数执行之前执行。
using System;
namespace UsageLibrary
{
public class BadlyConstructedType
{
protected string initialized = "No";
public BadlyConstructedType()
{
Console.WriteLine("Calling base ctor.");
// Violates rule: DoNotCallOverridableMethodsInConstructors.
DoSomething();
}
// This will be overridden in the derived type.
public virtual void DoSomething()
{
Console.WriteLine ("Base DoSomething");
}
}
public class DerivedType : BadlyConstructedType
{
public DerivedType ()
{
Console.WriteLine("Calling derived ctor.");
initialized = "Yes";
}
public override void DoSomething()
{
Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized);
}
}
public class TestBadlyConstructedType
{
public static void Main()
{
DerivedType derivedInstance = new DerivedType();
}
}
}
Output :
输出 :
Calling base ctor.
调用基本ctor。
Derived DoSomething is called - initialized ? No
派生的 DoSomething 被称为初始化?不
Calling derived ctor.
调用派生ctor。
回答by Kajetan Abt
More of a special case: If you create a listener, you might want to make it register itself somewhere (such as with a singleton or GUI). If you do that during its constructor, it leaks a pointer/reference to itself which is not yet safe, since the constructor has not completed (and might even fail completely). Assume the singleton that collects all listeners and sends them events when things happen receives and event, and then loops through its list of listeners (one of them is the instance we are talking about), to send them each a message. But this instance is still mid-way in its constructor, so the call can fail in all kinds of bad ways. In this case, it makes sense to have registration in a separate function, which you obviously do notcall from the constructor itself (that would defeat the purpose entirely), but from the parent object, after construction has completed.
更多的特殊情况:如果您创建一个侦听器,您可能希望让它在某处注册自己(例如使用单例或 GUI)。如果您在其构造函数期间这样做,它会泄漏指向自身的指针/引用,这还不安全,因为构造函数尚未完成(甚至可能完全失败)。假设收集所有侦听器并在事件发生时向它们发送事件的单例接收和事件,然后循环遍历其侦听器列表(其中一个是我们正在谈论的实例),向它们每个发送一条消息。但是这个实例仍然在它的构造函数的中间,所以调用可能会以各种糟糕的方式失败。在这种情况下,在单独的函数中进行注册是有意义的,这显然不是从构造函数本身调用(这将完全破坏目的),但在构造完成后从父对象调用。
But that is a specific case, not the general one.
但那是一种特殊情况,不是一般情况。
回答by Swiss Frank
A few more cases:
还有几个案例:
COOKING ARGS
烹饪 ARGS
A constructor can't call another constructor, but an init method can call another init.
一个构造函数不能调用另一个构造函数,但一个 init 方法可以调用另一个 init。
For instance say we have a initializer that takes a list of conventional args. We have another initializer that takes a dictionary of name=value pairs. The second can query the dictionary for the arguments the first initializer accepts and call the first with them.
例如,假设我们有一个初始化程序,它接受一个常规参数列表。我们有另一个初始化器,它接受名称=值对的字典。第二个可以在字典中查询第一个初始化器接受的参数并用它们调用第一个。
This is fine when the initializers are an init methods, but not when the initializers are constructors.
当初始化器是 init 方法时这很好,但当初始化器是构造器时就不行了。
CHICKEN OR EGG
鸡肉或鸡蛋
We might have a car class whose initializer must have a pointer to a motor object, and the motor class initializer must have a pointer to its car object. This is simply impossible with constructors, but trivial with init methods.
我们可能有一个 car 类,它的初始化器必须有一个指向 motor 对象的指针,并且 motor 类初始化器必须有一个指向它的 car 对象的指针。这对于构造函数来说是不可能的,但是对于 init 方法来说却是微不足道的。
BREAKING UP ARG LIST
分解 ARG 列表
There may be a huge number of args that couldbe specified but don't need to be (perhaps the default values suffice, or perhaps some parameters are only needed depending on values for other parameters). We may wish to have several initializers instead of a single one.
可能有args来一个巨大的数字可能被指定,但并不需要是(也许是默认值就足够了,或者是依赖于其他参数的值只需要一些参数)。我们可能希望有多个初始化器而不是一个。
Again, it's simply impossible to break up a constructor, but trivial to break up an initializer.
同样,分解构造函数是根本不可能的,但分解初始化器是微不足道的。