【问题标题】:Why use an initialization method instead of a constructor?为什么使用初始化方法而不是构造函数?
【发布时间】:2010-09-24 12:04:15
【问题描述】:

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

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

他们告诉我,这与时间安排有关。有些事情必须在构造函数中失败之后完成。但是大多数构造函数都是空的,我真的看不出有什么理由不使用构造函数。

所以我转向你,哦 C++ 的巫师:你为什么要使用 init 方法而不是构造函数?

【问题讨论】:

  • 如果你自己的公司连解释都解释不了,我就闻到了糟糕的代码。
  • 好像缺少void
  • 我同意迈克的观点。开始找工作。
  • 我是一名工程师。该公司是一家工程公司。只要算法很棒,代码就会很臭。他们是! (如果我再问,他们会进一步解释。只是你们可以更好更快地解释它,所以我问你;-))
  • 在 Wiki 上查看有关最新 C++ 标准的此链接,因为它解决了使用 init 函数的一个有效原因:en.wikipedia.org/wiki/C%2B%2B11#Object_construction_improvement

标签: c++ constructor initialization


【解决方案1】:

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

使用 init 函数的另一个常见原因是希望避免异常,但这是一种相当老派的编程风格(而且它是否是一个好主意是它自己的一个完整的论点)。它与无法在构造函数中工作的事情无关,而是与构造函数在失败时无法返回错误值的事实有关。所以就你的同事给你的真实原因而言,我怀疑不是这样。

【讨论】:

  • 第一个确实是一个正当的理由。它也被称为两阶段构造。如果我需要这样的东西,我会把它藏在物体的内部。我永远不会把它暴露给我班的用户。
  • 至于第二个原因,构造函数仍然可以与辅助函数一起使用来管理可能出现异常的资源(如果启用了异常)。如果构造函数失败,则不会在部分构造的对象上调用析构函数。这就是为什么有时您会看到手动初始化/清理代码。
  • @brita_:是的,尽管可以说这是他们的代码存在的问题,他们应该通过向其抛出更多 RAII 来解决。如果构造函数失败,则类的非静态数据成员的析构函数都被调用(仅那些已经初始化的析构函数,以防在其中一个初始化期间发生故障)。因此,如果您在构造函数中使用实例数据和/或变量来捕获需要清理的任何内容,那么您不需要任何手动清理异常。不过,有时说起来容易做起来难。
【解决方案2】:

是的,我能想到几个,但通常这不是一个好主意。

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

然而,在正确设计的 OO 代码中,构造函数负责建立类不变量。通过允许默认构造函数,您允许一个空类,因此您必须修改不变量,以便“空”类和“有意义”类都接受......并且每次使用该类必须首先确保对象已正确构建...它很粗鲁。

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

  • 我需要使用virtual 方法:使用Virtual Constructor 习惯用法。
  • 还有很多工作要做:那又怎样,反正工作要做,在构造函数里做就行了
  • 设置可能失败:抛出异常
  • 我想保留部分初始化的对象:在构造函数中使用try/catch并在对象字段中设置错误原因,不要忘记在每个公共方法的开头assert以确保对象在尝试使用之前可用。
  • 我想重新初始化我的对象:从构造函数调用初始化方法,您将避免重复代码,同时仍然拥有完全初始化的对象
  • 我想重新初始化我的对象 (2):使用 operator=(如果编译器生成的版本不适合您的需要,请使用复制和交换习惯用法实现它)。

如前所述,总的来说,这是个坏主意。如果您真的想拥有“void”构造函数,请将它们设为 private 并使用 Builder 方法。与 NRVO 一样高效...如果构造失败,您可以返回 boost::optional<FancyObject>

【讨论】:

  • 我会选择另一种揭穿方式,“我希望能够重新初始化我的对象:实现复制和交换operator=
  • 我想了想,然后意识到可能有优化机会。例如,想想vector 对象上的assign 方法:通过使用assign,您可以重新使用已分配的存储空间,因此不需要虚假的内存分配。但是正确地做到这一点很棘手,所以我同意在一般情况下应该使用复制和交换。
  • 同意,有一个重新初始化功能并没有错,但这不是我要开始的。如果不出意外,对于“优化机会”,请阅读“基本异常保证”。
  • @Steve:是的,我们同意,正如所说的“正确地做到这一点很棘手”:-) 我想这是使用 STL 作为示例可能不是最明智的选择的情况之一它是由专家程序员编写的,而不是普通程序员。
  • vector::assign 一种重新初始化的方法,按照上面的建议进行。
【解决方案3】:

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

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

因此,尽管一般不应遵循此成语,但恕我直言,它有一些有效的用途。但是,最好将其使用限制在最低限度,尽可能使用构造函数。在我们的案例中,这是一个遗留项目,我们还没有完全理解它的架构。至少 init 方法的使用仅限于服务类——常规类是通过构造函数初始化的。我相信可能有一种方法可以重构该架构以消除对服务 init 方法的需求,但至少我不知道该怎么做(坦率地说,当时我们有更紧迫的问题要处理项目的一部分)。

【讨论】:

  • 听起来你在使用单例。 (不要那样做。)
  • @Roger,不,这些不是单身人士。实际上,您可以同时打开多个所谓的“模型”,每个模型都包含每种服务类型的一个专用实例。我很清楚单身人士的问题,但仍然感谢:-)
  • 如果您认为侦听器的注册是初始化的一部分,那么它只是两个阶段的初始化。在一个动态的世界观中,服务不断涌现,监听器的注册是系统生命的一部分。
  • @Matthieu,这不是一个动态的系统 - 可能的现有服务集及其关系是严格硬编码的,其中大多数只是 必须 存在在每个模型中。虽然我们本可以尝试重构系统以开始朝这个方向发展,但这将是一项巨大的努力,我们根本没有资源。
  • 啊资源...我们都到了那里:)
【解决方案4】:

我能想到的两个原因:

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

【讨论】:

  • +1 我想到了你的第二个论点;事实上,我认为这在 C++ 中被认为是一种不好的做法,应该使用一些辅助对象来实现。
  • 您的第二个要点可能是我有时使用初始化函数的最常见原因。是的,构造函数可以调用 init,但这只会使代码不那么清晰。
  • 这些是看到初始化或清理函数的典型原因,但它不会成为类公共接口的一部分,而是避免代码重复数据删除的内部帮助函数。其他帖子涵盖了系统地执行此操作的原因。
【解决方案5】:

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

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

【讨论】:

    【解决方案6】:

    init() 函数在您的编译器不支持异常,或者您的目标应用程序不能使用堆(异常通常使用堆来创建和销毁它们)时很好。

    init() 例程在需要定义构造顺序时也很有用。也就是说,如果你全局分配对象,构造函数被调用的顺序是没有定义的。例如:

    [file1.cpp]
    some_class instance1; //global instance
    
    [file2.cpp]
    other_class must_construct_before_instance1; //global instance
    

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

    【讨论】:

      【解决方案7】:

      我还想附上一个代码示例来回答 #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();
              }
          }
      }
      

      输出:

      调用基础ctor。

      Derived DoSomething 被称为 - 初始化?没有

      调用派生的ctor。

      【讨论】:

        【解决方案8】:

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

        但这是一种特殊情况,不是一般情况。

        【讨论】:

          【解决方案9】:

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

          【讨论】:

            【解决方案10】:

            还有几个案例:

            烹饪工具

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

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

            当初始化器是 init 方法时这很好,但当初始化器是构造器时则不行。

            鸡肉或鸡蛋

            我们可能有一个汽车类,其初始化程序必须有一个指向电机对象的指针,而电机类初始化程序必须有一个指向其汽车对象的指针。这对于构造函数来说是根本不可能的,但对于 init 方法来说是微不足道的。

            分解 ARG 列表

            可能有大量的参数可以被指定但不需要(也许默认值就足够了,或者可能只需要一些参数,具体取决于其他参数的值) .我们可能希望有多个初始化器而不是一个。

            同样,分解构造函数是不可能的,但分解初始化程序却很简单。

            【讨论】:

              【解决方案11】:

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

              A *a = new A;
              

              A 类的初始化器需要设置 a,那么显然你需要类似的东西:

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

              【讨论】:

              • 我不买这个。然后,您可以在构造函数的最后一行中私下调用 init。 (虽然我没有投反对票)
              • 这是对问题的重述,而不是答案。 为什么构建后需要初始化对象?
              • 如果构造函数依赖于被设置的变量a,我就遇到过这种情况。
              猜你喜欢
              • 2017-12-09
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多