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

时间:2020-03-05 18:45:39  来源:igfitidea点击:

在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完全相同,但是他们选择了另一种方法,这可能是产生最少惊讶原则的唯一原因吗?

我们认为哪个是正确的选择?

解决方案

回答

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

我认为C ++方式更有意义,但是当有人查看代码时会导致期望问题。如果我们知道这种情况,则不应有意将代码置于这种情况下,以便以后进行调试。

回答

我认为C ++在具有"最正确的"行为方面提供了最佳的语义……但是,对于编译器而言,这是更多的工作,并且对于以后阅读它的人来说,代码显然是不直观的。

使用.NET方法时,必须非常严格地限制功能,使其不依赖于任何派生的对象状态。

回答

Virtual functions in constructors, why do languages differ?

因为没有一种良好的行为。我发现C ++的行为更有意义(因为首先调用了基类c-tor,所以有理由说他们应该调用基类虚函数-毕竟派生类c-tor尚未运行,因此它可能没有为派生的类虚拟函数设置正确的前提条件。

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

回答

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;

回答

语言如何定义对象的生存时间存在根本差异。在Java和.Net中,在运行任何构造函数之前,将对象成员初始化为零/空,并且此时对象生命周期开始。因此,当我们输入构造函数时,我们已经有了一个初始化的对象。

在C ++中,对象生存期仅在构造函数完成时开始(尽管成员变量和基类在开始之前已完全构建)。这解释了调用虚拟函数时的行为,以及在构造函数主体中存在异常的情况下为何不运行析构函数的原因。

Java / .Net对象生存期定义的问题在于,很难确保对象始终满足其不变性,而不必在初始化对象但构造函数未运行时进行特殊处理。 C ++定义的问题在于,我们有一个奇数时期,其中对象处于混乱状态且未完全构造。

回答

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

BaseClass(){ 为(int i = 0; i &lt;virtualSize(); i ++) initialize_stuff_for_index(i); }

再一次,C ++行为的优点是它阻止了像上面这样的构造器的编写。

我认为调用假定构造函数已完成的方法的问题不是C ++的好借口。如果确实存在问题,则不允许构造函数调用任何方法,因为相同的问题可能适用于基类的方法。

反对C ++的另一点是行为的效率要低得多。尽管构造函数直接知道调用的内容,但是对于每个类,从基到最终都必须更改vtab指针,因为构造函数可能会调用其他将调用虚函数的方法。根据我的经验,与在构造函数中提高虚拟函数的调用效率相比,这浪费的时间远远多于浪费的时间。

更令人烦恼的是,析构函数也是如此。如果我们编写了一个虚拟的cleanup()函数,并且基类的析构函数执行了cleanup(),则它肯定不会达到期望。

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