为什么使用初始化方法而不是构造函数?

时间:2010-09-24 12:04:16

标签: c++ constructor initialization

我刚刚进入一家新公司,很多代码库都使用初始化方法而不是构造函数。

struct MyFancyClass : theUberClass
{
    MyFancyClass();
    ~MyFancyClass();
    resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
                                redundantArgument arg3=TODO);
    // several fancy methods...
};

他们告诉我这与时间有关。有些事情必须在构造函数失败的构造之后完成。但是大多数构造函数都是空的,我没有看到任何不使用构造函数的原因。

所以我转向你,哦,C ++的向导:你为什么要使用init-method而不是构造函数?

11 个答案:

答案 0 :(得分:65)

因为他们说“计时”,我想这是因为他们希望他们的init函数能够调用对象上的虚函数。这并不总是在构造函数中起作用,因为在基类的构造函数中,对象的派生类部分“尚不存在”,特别是您无法访问派生类中定义的虚函数。相反,如果已定义,则调用函数的基类版本。如果没有定义,(暗示该函数是纯虚函数),则会得到未定义的行为。

init函数的另一个常见原因是希望避免异常,但这是一种非常古老的编程风格(并且它是一个好主意是否是它自己的完整论证)。它与在构造函数中无法工作的事物无关,而与构造函数在某些事情失败时无法返回错误值的事实无关。所以,如果你的同事给你真正的理由,我怀疑这不是它。

答案 1 :(得分:28)

是的,我可以想到几个,但一般来说这不是一个好主意。

大多数时候调用的原因是你只通过构造函数中的异常报告错误(这是真的),而使用经典方法你可以返回错误代码。

但是,在设计合理的OO代码中,构造函数负责建立类不变量。通过允许默认构造函数,您允许一个空类,因此您必须修改不变量,以便接受“null”类和“有意义”类......并且每次使用该类必须首先确保该对象已经建成了......这很糟糕。

现在,让我们揭穿“原因”:

  • 我需要使用virtual方法:使用Virtual Constructor idiom。
  • 还有很多工作要做:那么,无论如何都要完成工作,只需在构造函数中完成
  • 设置可能会失败:抛出异常
  • 我想保留部分初始化的对象:在构造函数中使用try / catch并在对象字段中设置错误原因,不要忘记在每个公共方法的开头assert以确保在尝试使用它之前,该对象是可用的。
  • 我想重新初始化我的对象:从构造函数调用初始化方法,在避免使用完全初始化对象时避免重复代码
  • 我想重新初始化我的对象(2):使用operator=(如果编译器生成的版本不符合您的需要,则使用copy和swap惯用法实现它。)

如上所述,总的来说,不好主意。如果你真的想拥有“void”构造函数,可以使它们private并使用Builder方法。使用NRVO效率很高......如果构造失败,您可以返回boost::optional<FancyObject>

答案 2 :(得分:16)

其他人列出了很多可能的原因(以及为什么大多数这些通常不是一个好主意的正确解释)。让我发布一个 a(或多或少)有效使用init方法的例子,它实际上与时间有关

在之前的项目中,我们有许多服务类和对象,每个都是层次结构的一部分,并以各种方式相互交叉引用。通常,为了创建ServiceA,您需要一个父服务对象,而该服务对象又需要一个服务容器,该容器在初始化时已经依赖于某些特定服务(可能包括ServiceA本身)的存在。原因是在初始化期间,大多数服务将其自身与其他服务一起注册为特定事件的侦听器,和/或通知其他服务有关成功初始化的事件。如果在通知时其他服务不存在,则注册没有发生,因此该服务在以后的应用程序使用期间不会收到重要消息。为了打破循环依赖链,我们必须使用与构造函数分开的显式初始化方法,从而有效地使全局服务初始化成为一个两阶段的过程

所以,尽管一般不应该遵循这个习惯用法,但恕我直言它有一些有效用途。但是,最好尽可能使用构造函数将其使用限制在最小值。在我们的例子中,这是一个遗留项目,我们还没有完全理解它的架构。至少init方法的用法仅限于服务类 - 通过构造函数初始化常规类。我相信可能有一种方法可以重构该架构以消除对服务初始化方法的需求,但至少我没有看到如何做到这一点(坦白说,我当时处理的问题比较紧迫)项目的一部分)。

答案 3 :(得分:7)

我能想到的两个原因:

  • 假设创建一个对象涉及许多繁琐的工作,这些工作可能会以很多很多可怕而微妙的方式失败。如果你使用一个简短的构造函数来设置不会失败的rudamentary事物,然后让用户调用一个初始化方法来完成大工作,你至少可以确保你创建了一些对象,即使大工作失败了。也许该对象包含有关init失败的确切方式的信息,或者由于其他原因保持未成功初始化对象的重要性。
  • 有时您可能希望在创建对象很久之后重新初始化对象。这样,只需再次调用初始化方法而不破坏和重新创建对象。

答案 4 :(得分:5)

此类初始化的另一个用途可以是对象池。基本上你只是从池中请求对象。池中已经创建了一些空白的N个对象。现在是调用者可以调用他/她喜欢设置成员的任何方法。一旦调用者完成了对象,它就会告诉池将其破坏。优点是在使用对象之前将保存内存,并且调用者可以使用它自己的合适的成员方法来初始化对象。对象可能有很多用途,但调用者可能不需要全部,也可能不需要初始化对象的所有成员。

通常会想到数据库连接。一个池可以有一堆连接对象,调用者可以填写用户名,密码等。

答案 5 :(得分:5)

当你的编译器不支持异常,或者你的目标应用程序不能使用堆时,

init()函数很好(异常通常使用堆来创建和销毁它们)。

当需要定义构造顺序时,

init()例程也很有用。也就是说,如果全局分配对象,则不会定义调用构造函数的顺序。例如:

[file1.cpp]
some_class instance1; //global instance

[file2.cpp]
other_class must_construct_before_instance1; //global instance

该标准不保证在 instance1 的构造函数之前调用 must_construct_before_instance1 的构造函数。当它与硬件绑定时,初始化的顺序至关重要。

答案 6 :(得分:1)

我还想附上一个代码示例来回答#1 -

因为msdn也说:

  

当调用虚方法时,   执行该方法的实际类型   直到运行时才选择。当一个   构造函数调用一个虚方法   有可能是构造函数   调用该方法的实例   没有执行。

示例: 以下示例演示了违反此规则的效果。测试应用程序创建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();
        }
    }
}

输出

致电基地。

调用Derived DoSomething - 初始化?否

调用派生的ctor。

答案 7 :(得分:1)

更多特殊情况:如果您创建了一个侦听器,您可能希望将其自身注册到某个位置(例如使用单例或GUI)。如果你在它的构造函数中这样做,它会泄漏一个指针/引用到它自己还不安全,因为构造函数还没有完成(甚至可能完全失败)。 假设收集所有侦听器的单例并在事件发生时发送事件接收和事件,然后遍历其监听器列表(其中一个是我们正在讨论的实例),向它们发送每条消息。但是这个实例在构造函数中仍处于中间位置,因此调用可能会以各种不良方式失败。在这种情况下,在一个单独的函数中进行注册是有意义的,你显然是从构造函数本身 not 调用(这完全会破坏目的),但是从父对象开始,在构造之后完成。

但这是一个具体案例,而不是一般情况。

答案 8 :(得分:1)

它对于进行资源管理很有用。假设你有一些带有析构函数的类,当对象的生命周期结束时自动释放资源。假设您还有一个包含这些资源类的类,并在此上层类的构造函数中启动它们。使用赋值运算符启动此更高级别时会发生什么?复制内容后,旧的高级类将脱离上下文,并为所有资源类调用析构函数。如果这些资源类具有在赋值期间复制的指针,则所有这些指针现在都是错误的指针。如果您在较高级别的单独init函数中启动资源类,则完全绕过资源类的析构函数,因为赋值运算符永远不必创建和删除这些类。我相信这就是“时间”要求的含义。

答案 9 :(得分:0)

更多情况:

烹饪用具

一个构造函数不能调用另一个构造函数,但是一个init方法可以调用另一个init。

例如,说我们有一个初始化器,它接收常规参数列表。我们还有另一个初始化程序,它使用名称=值对的字典。第二个可以查询字典以获取第一个初始化程序接受的参数,并用它们调用第一个。

这在初始化程序是init方法时很好,但在初始化程序是构造函数时则不行。

鸡肉或鸡蛋

我们可能有一个汽车类,其初始值设定项必须具有指向马达对象的指针,而汽车类的初始值设定项必须具有指向其汽车对象的指针。对于构造函数来说,这根本是不可能的,但是对于init方法来说,这是微不足道的。

打破阿根廷名单

可能有很多可以指定 的args,但不一定要指定(也许默认值就足够了,或者也许只需要某些参数,这取决于其他参数的值) 。我们可能希望有几个初始化程序,而不是一个。

同样,要破坏一个构造函数是不可能的,但是要破坏一个初始化函数却是微不足道的。

答案 10 :(得分:-2)

如果需要在创建类之后调用初始化程序,则使用初始化方法而不是构造函数。因此,如果A类创建为:

A *a = new A;

并且A类的initisalizer要求设置,然后显然你需要这样的东西:

A *a = new A;
a->init();