【问题标题】:Virtual functions in constructors, why do languages differ?构造函数中的虚函数,为什么语言不同?
【发布时间】:2010-09-07 09:37:31
【问题描述】:

在 C++ 中,当从构造函数中调用虚函数时,它的行为不像虚函数。

我认为第一次遇到这种行为的每个人都感到惊讶,但第二次认为这是有道理的:

只要派生的构造函数还没有被执行,对象就不是还是一个派生的实例。

那么如何调用派生函数呢?前提条件还没来得及设置。示例:

class base {
public:
    base()
    {
        std::cout << "foo is " << foo() << std::endl;
    }
    virtual int foo() { return 42; }
};

class derived : public base {
    int* ptr_;
public:
    derived(int i) : ptr_(new int(i*i)) { }
    // The following cannot be called before derived::derived due to how C++ behaves, 
    // if it was possible... Kaboom!
    virtual int foo()   { return *ptr_; } 
};

Java 和 .NET 完全一样,但他们选择了另一种方式,这可能是最小意外原则的唯一原因?

你认为哪个是正确的选择?

【问题讨论】:

    标签: java .net c++ language-agnostic


    【解决方案1】:

    语言定义对象生命周期的方式存在根本差异。在 Java 和 .Net 中,对象成员在任何构造函数运行之前被初始化为零/空,此时对象生命周期开始。所以当你进入构造函数时,你已经得到了一个初始化的对象。

    在 C++ 中,对象生命周期仅在构造函数完成时才开始(尽管成员变量和基类在它开始之前已完全构造)。这解释了调用虚函数时的行为,以及为什么在构造函数的主体中存在异常时不运行析构函数。

    Java/.Net 中对象生命周期定义的问题在于,当对象已初始化但构造函数尚未运行时,很难确保对象始终满足其不变量,而不必考虑特殊情况。 C++ 定义的问题在于,您有一个奇怪的时期,对象处于不确定状态且未完全构造。

    【讨论】:

      【解决方案2】:

      这两种方式都可能导致意想不到的结果。最好的选择是根本不在构造函数中调用虚函数。

      我认为 C++ 方式更有意义,但当有人审查您的代码时会导致预期问题。如果您知道这种情况,则为了以后的调试,您应该故意不要将您的代码置于这种情况下。

      【讨论】:

        【解决方案3】:

        构造函数中的虚函数,为什么语言不同?

        因为没有一个好的行为。我发现 C++ 的行为更有意义(由于首先调用基类 c-tor,因此它们应该调用基类虚函数是理所当然的——毕竟派生类 c-tor 还没有运行,所以它可能没有为派生类虚函数设置正确的前提条件)。

        但有时,如果我想使用虚函数来初始化状态(因此在未初始化状态的情况下调用它们并不重要),C#/Java 的行为会更好。

        【讨论】:

          【解决方案4】:

          我认为 C++ 在具有“最正确”行为方面提供了最好的语义......但是它对编译器的工作量更大,而且代码对于以后阅读它的人来说肯定是不直观的。

          使用 .NET 方法,函数必须非常有限,不能依赖任何派生对象状态。

          【讨论】:

          • 这对编译器有什么作用?它只是归结为在调用基类的构造函数之后设置 vptr。我想说其他语义更难实现,因为您需要确保在派生类构造函数中设置 vptr 后,它不能被基类构造函数覆盖。 (这是假设动态调度是通过指向虚拟方法表的指针来处理的,这是最常见的方法。)
          • @LucTouraille 很晚 +1,这是真的。实际上,C++ 方法更简洁、更直接
          【解决方案5】:

          Delphi 在 VCL GUI 框架中很好地利用了虚拟构造函数:

          type
            TComponent = class
            public
              constructor Create(AOwner: TComponent); virtual; // virtual constructor
            end;
          
            TMyEdit = class(TComponent)
            public
              constructor Create(AOwner: TComponent); override; // override virtual constructor
            end;
          
            TMyButton = class(TComponent)
            public
              constructor Create(AOwner: TComponent); override; // override virtual constructor
            end;
          
            TComponentClass = class of TComponent;
          
          function CreateAComponent(ComponentClass: TComponentClass; AOwner: TComponent): TComponent;
          begin
            Result := ComponentClass.Create(AOwner);
          end;
          
          var
            MyEdit: TMyEdit;
            MyButton: TMyButton;
          begin
            MyEdit := CreateAComponent(TMyEdit, Form) as TMyEdit;
            MyButton := CreateAComponent(TMyButton, Form) as TMyButton;
          end;
          

          【讨论】:

            【解决方案6】:

            我发现 C++ 行为非常烦人。例如,您不能编写虚函数来返回所需的对象大小,并让默认构造函数初始化每个项目。例如,这样做会很好:

            BaseClass() { for (int i=0; i<virtualSize(); i++) initialize_stuff_for_index(i); }

            C++ 行为的优势在于它不鼓励编写上述构造函数。

            我不认为调用假定构造函数已经完成的方法的问题是 C++ 的一个很好的借口。如果这确实是个问题,那么构造函数将不允许调用 任何 方法,因为同样的问题可能适用于基类的方法。

            反对 C++ 的另一点是该行为的效率要低得多。尽管构造函数直接知道它调用了什么,但必须将每个类的 vtab 指针从基类更改为最终类,因为构造函数可能会调用其他将调用虚函数的方法。根据我的经验,这比在构造函数中更高效地调用虚函数所节省的时间要多得多。

            更烦人的是,析构函数也是如此。如果你写了一个虚拟的 cleanup() 函数,而基类析构函数做了 cleanup(),它肯定不会像你期望的那样。

            这一点以及 C++ 在退出时对静态对象调用析构函数的事实确实让我很生气。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-06-20
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多