在构造函数中调用虚函数

Dav*_*fal 220 c++ constructor overriding virtual-functions

假设我有两个C++类:

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};
Run Code Online (Sandbox Code Playgroud)

如果我写下面的代码:

int main()
{
  B b;
  int n = b.getn();
}
Run Code Online (Sandbox Code Playgroud)

人们可能期望将n其设置为2.

事实证明,n设置为1.为什么?

Jar*_*Par 204

从构造函数或析构函数调用虚函数是危险的,应尽可能避免使用.所有C++实现都应调用当前构造函数中层次结构级别定义的函数版本,不再进一步调用.

C++ FAQ精简版包括这在相当不错的细节部分23.7.我建议您阅读(以及常见问题的其余部分)以获得后续信息.

摘抄:

[...]在构造函数中,虚拟调用机制被禁用,因为尚未发生从派生类的重写.对象是从基础构建的,"在派生之前的基础".

[...]

销毁是在"基类之前的派生类"完成的,因此虚函数的行为与构造函数相同:只使用本地定义 - 并且不会调用重写函数以避免触及对象的(现在销毁的)派生类部分.

编辑纠正最重要(感谢litb)

  • 不是大多数C++实现,但所有C++实现都必须调用当前类的版本.如果有些人没有,那么那些有bug :).我仍然同意你的观点,从基类调用虚函数是不好的 - 但语义是精确定义的. (50认同)
  • 它并不危险,它只是非虚拟的.实际上,如果从构造函数调用的方法被虚拟调用,那么这将是危险的,因为该方法可以访问未初始化的成员. (13认同)
  • ** - 1**"很危险",不,它在Java中是危险的,可以发生下行调用; C++规则通过相当昂贵的机制消除了危险. (9认同)
  • 以什么方式从构造函数中调用虚函数"危险"?这完全是胡说八道. (9认同)
  • 为什么从析构函数调用虚函数是危险的?析构函数运行时对象是否仍然完整,并且只在析构函数完成后销毁? (5认同)
  • 由于没有解释_why_而且基本上只相当于一个纯链接的答案,这是非常高度投票.(以及指向C++常见问题解答的臭名昭着的移动目标的链接......好吧,如果不是最新的内容,请移动URL;) (3认同)
  • 现在,对于 C++11,一种可能的保护措施是将“final”关键字添加到在其构造函数中使用虚拟方法的任何类。这可以防止上述问题并使该技术在某些情况下有用。显然,一个副作用是你不能再继承这个类了。注意:仅将所涉及的虚拟方法标记为“最终”不足以保证 100% 的安全性,因为该方法仍然可以调用另一个尚未定义的虚拟方法。 (3认同)
  • @user207421不,我写的是强制行为。例如,C++11 的 n3797 草案在 12.4.8 [class.dtor] 中指出“_After_执行析构函数的主体 [...],类 X 的析构函数调用 [...] X 直接基类的析构函数类[...]。基类和成员按照其构造函数完成的_相反顺序_被销毁(参见 12.6.2)。” (3认同)
  • [以下FAQ页面](http://www.parashift.com/c%2B%2B-faq-lite/calling-virtuals-from-ctor-idiom.html)详细讨论了如何解决它. (2认同)
  • @Kevin:那么在人们可能想这样做的情况下应该使用什么替代设计呢?(在我考虑这一点的情况下,我遇到了根据派生类型以不同方式初始化基础中的数据字段的问题) (2认同)
  • @SiyuanRen不,对象不再完整.当析构函数运行时,派生的clases对同一对象的析构函数已经运行并完成.因此派生类的不变量(可能)不再有效,并且运行派生类成员(可能)是危险的.与构造函数相同,但顺序相反. (2认同)
  • “所有C ++实现都应” =>“所有符合标准的C ++实现都应”。“应该”太慈善了。语言要求(但不建议这样做)。 (2认同)

Dav*_*eas 80

在大多数OO语言中,从构造函数调用多态函数是一种灾难.遇到这种情况时,不同语言的表现会有所不同.

基本问题是,在所有语言中,Base类型必须在Derived类型之前构建.现在,问题是从构造函数中调用多态方法意味着什么.你期望它的表现如何?有两种方法:在Base级别调用方法(C++样式)或在层次结构底部的未构造对象上调用多态方法(Java方式).

在C++中,Base类将在进入自己的构造之前构建其虚拟方法表的版本.此时,对virtual方法的调用将最终调用方法的Base版本,或者生成一个调用纯虚方法,以防它在层次结构的该级别没有实现.在完全构建Base之后,编译器将开始构建Derived类,并且它将覆盖方法指针以指向层次结构的下一级中的实现.

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run
Run Code Online (Sandbox Code Playgroud)

在Java中,编译器将在构造的第一步,在进入Base构造函数或Derived构造函数之前构建等效的虚拟表.影响是不同的(我的喜欢更危险).如果基类构造函数调用在派生类中重写的方法,则实际将在派生级别处理调用未构造对象上的方法,从而产生意外结果.在构造函数块内初始化的派生类的所有属性尚未初始化,包括"final"属性.具有在类级别定义的默认值的元素将具有该值.

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P
Run Code Online (Sandbox Code Playgroud)

如您所见,调用多态(C++术语中的虚拟)方法是常见的错误来源.在C++中,至少你可以保证它永远不会在一个尚未构造的对象上调用方法......

  • 很好地解释了为什么替代方案(也)容易出错。 (3认同)
  • @VinGarcia对于那些在使用语言之前不费心去实际了解该语言是如何工作的人来说,这只是“意外”,而是假设他们的期望将反映现实而无需检查 - 在这种情况下,我没有同情心:根据定义,语言,是你学到的东西,而不是“期望”的东西。我认为,作为一项设计决策,它有明确的记录并且非常有意义。我不想使用另一种语言,尽管你没有解释为什么它会产生相关的反例。 (3认同)
  • 一个解释!+1,优越的回答imho (2认同)
  • @VinGarcia 什么?在这种情况下,C++ 不会“禁止”任何东西。该调用被简单地视为对当前正在执行其构造函数的类的方法的非虚拟调用。这是对象构建时间表的合乎逻辑的结果 - 不是阻止你做愚蠢事情的严厉决定。事实上,它也恰好满足了后一个目的,这对我来说只是一个奖励。 (2认同)

Dav*_*fal 56

原因是C++对象从内到外构造成洋葱.在派生类之前构造超类.因此,在制作B之前,必须制作A. 当调用A的构造函数时,它还不是B,因此虚函数表仍然具有A的fn()副本的条目.

  • C++通常不使用术语"超类" - 它更喜欢"基类". (16认同)
  • @DavidRodríguez-dribeas其他语言确实可以做到这一点。例如,在Pascal中,首先为整个对象分配内存,然后才调用最派生的构造函数。构造函数必须包含对其父级构造函数的显式调用(不必是第一个动作-只需在某处即可),或者如果不是,则好像该构造函数的第一行进行了该调用。 (2认同)

Aar*_*paa 23

C++ FAQ精简版封面这还算不错:

本质上,在对基类构造函数的调用期间,该对象还不是派生类型,因此调用基类型的虚函数实现而不是派生类型.

  • 清晰,直接,最简单的答案。我仍然希望看到得到一些爱,这仍然是一个功能。我讨厌不得不编写所有这些愚蠢的initializeObject()函数,这些函数在构造后立即被用户调用,对于一个非常常见的用例而言,这只是一种不好的形式。我理解困难。这就是生活。 (2认同)

Tob*_*ias 13

您的问题的一个解决方案是使用工厂方法来创建对象.

  • 为包含虚拟方法afterConstruction()的类层次结构定义公共基类:
class Object
{
public:
  virtual void afterConstruction() {}
  // ...
};
  • 定义工厂方法:
template< class C >
C* factoryNew()
{
  C* pObject = new C();
  pObject->afterConstruction();

  return pObject;
}
  • 像这样使用它:
class MyClass : public Object 
{
public:
  virtual void afterConstruction()
  {
    // do something.
  }
  // ...
};

MyClass* pMyObject = factoryNew();


sta*_*son 5

正如已经指出的,这些对象是在构造时自下而上创建的。当构造基对象时,派生对象还不存在,因此虚函数重写无法工作。

但是,如果您的 getter 返回常量,或者可以用静态成员函数表示,则可以使用使用静态多态性而不是虚函数的多态 getter 来解决此问题,此示例使用 CRTP ( https://en.wikipedia.org/wiki /Curiously_recurring_template_pattern)。

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

int main()
{
    A a;
    B b;
    C c;

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

通过使用静态多态性,基类知道要调用哪个类的 getter,因为信息是在编译时提供的。