使用object-expression在构造函数内调用虚函数

时间:2014-08-29 05:12:25

标签: c++

代码:

#include <iostream>

using std::cout;
using std::endl;

struct A
{
    virtual void foo()
    {
        cout << "A" << endl;
    }

    A(){ }
};

struct B : A
{
    B();
    virtual void foo()
    {
        cout << "B" << endl;
    }
};

B b;

B::B()
{
    b.foo();
    foo();
}  

struct C : B
{
    virtual void foo()
    {
        cout << "C" << endl;
    }

    C() : B(){ }      
};

C c;

int main(){ }

DEMO

  

直接或间接从a调用虚函数时   构造函数或析构函数,包括在构造期间或   破坏类的非静态数据成员和对象   调用适用的是对象(称之为x)   建设或破坏,所谓的功能是最终的   覆盖在构造函数或析构函数的类中,而不是一个   在更派生的类中重写它。如果是虚函数调用   使用显式类成员访问(5.2.5)和对象   表达式是指x 的完整对象或其中之一   object的基类子对象,但不是x或其基类之一   子对象,行为未定义。

我一直试图收到UB

  

如果虚函数调用使用显式类成员访问   (5.2.5)和对象表达式是指x的完整对象   [...]

目前还不清楚 x 的完整对象是什么意思,其中x是一个对象。它是否与x的完整对象相同?

2 个答案:

答案 0 :(得分:7)

§1.8[intro.object] / p2-3:

  

对象可以包含其他对象,称为子对象。一个子对象   可以是成员子对象(9.2),基类子对象(子句)   10),或数组元素。不是任何子对象的对象   其他对象称为完整对象

     

对于每个对象x,都有一个名为完整对象的对象   x 的确定如下:

     
      
  • 如果x是完整对象,则xx的完整对象。
  •   
  • 否则,x的完整对象是包含x的(唯一)对象的完整对象。
  •   

从本质上讲,您引用的句子会使static_cast<C*>(this)->foo();的构造函数中的B在您的代码中执行未定义的行为,即使构造的完整对象是C。该标准实际上提供了一个很好的例子:

struct V {
    virtual void f();
    virtual void g();
};
struct A : virtual V {
    virtual void f();
};
struct B : virtual V {
    virtual void g();
    B(V*, A*);
};

struct D : A, B {
    virtual void f();
    virtual void g();
    D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
    f();    // calls V::f, not A::f
    g();    // calls B::g, not D::g
    v->g(); // v is base of B, the call is well-defined, calls B::g
    a->f(); // undefined behavior, a’s type not a base of B
}

实际上,您已经可以see the undefined behavior show up in this example if you run it:Ideone的编译器(GCC)实际上在V::f()行上调用a->f();,即使指针指的是完全构造的A子对象。

答案 1 :(得分:2)

这有点棘手,我不得不多次编辑帖子(感谢帮助我的人),我会试着简单地说一下N3690:

§12.7.4陈述

  

成员函数,包括虚函数(10.3),可以在构造或销毁期间调用(12.6.2)。

这就是你在B的构造函数中所做的事情

B::B()
{
    b.foo(); // virtual
    foo(); // virtual
}  

现在这完全合法。 this 指针(在第二个函数调用中隐式使用)始终指向正在构造的对象。

然后标准也说:

  

当直接或间接从构造函数和对象调用虚函数时   call apply是在构造或销毁下的对象(称之为x),被调用的函数是最终的覆盖   在构造函数或析构函数的类中,而不是在更多派生类中重写它(因此忽略函数的更多派生版本)

所以vtable并没有像你想象的那样完全行走,而是停留在虚构函数的构造函数的类版本中(参见http://www.parashift.com/c%2B%2B-faq-lite/calling-virtuals-from-ctors.html)。

仍然合法。

最后到了你的观点:

  

如果虚函数调用使用显式类成员访问,例如   (object.vfunction()或object-&gt; vfunction())并且对象表达式引用x的完整对象或该对象的基类子对象之一但不是x或其基类子对象之一(即不是对象)在构造或其基类子对象之一),行为是未定义的。

理解这句话我们首先需要了解x 的完整对象是什么意思:

§1.8.2

  

对象可以包含其他对象,称为子对象。子对象可以是成员子对象(9.2),基础   class subobject(Clause 10),或数组元素。不是任何其他对象的子对象的对象是   称为完整对象。

     

对于每个对象x,有一个对象称为x的完整对象,确定如下:

     

- 如果x是一个完整的对象,则x是x的完整对象。

     

- 否则,x的完整对象是包含x

的(唯一)对象的完整对象

如果你将上面的段落放在前一个段落中,你得到的就是你不能调用一个虚函数来引用基类的“完整类型”(即尚未构造的派生对象)或拥有的对象成员或数组元素。

如果要在B的构造函数中明确引用C:

B::B() {
    static_cast<C*>(this)->foo(); // Refers to the complete object of B, i.e. C
}

struct C : B
{
    C() : B(){ }
}

然后你会有未定义的行为。

直观(或多或少)的原因是

  • 允许在构造函数中调用虚函数或成员函数,并且在虚函数的情况下,它“停止虚拟层次结构遍历”到该对象并调用其函数的版本(参见http://www.parashift.com/c%2B%2B-faq-lite/calling-virtuals-from-ctors.html)< / p>

  • 无论如何,如果你从一个子对象那里引用该子对象的完整对象(重新阅读标准段落),那么它是未定义的行为

经验法则:don't call virtual functions in your constructors/destructors if you're not really sure you can

如果我出错了,请在下面的评论中告诉我,我会修复帖子。谢谢!