代码:
#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(){ }
直接或间接从a调用虚函数时 构造函数或析构函数,包括在构造期间或 破坏类的非静态数据成员和对象 调用适用的是对象(称之为x) 建设或破坏,所谓的功能是最终的 覆盖在构造函数或析构函数的类中,而不是一个 在更派生的类中重写它。如果是虚函数调用 使用显式类成员访问(5.2.5)和对象 表达式是指x 的完整对象或其中之一 object的基类子对象,但不是x或其基类之一 子对象,行为未定义。
我一直试图收到UB
如果虚函数调用使用显式类成员访问 (5.2.5)和对象表达式是指x的完整对象 [...]
目前还不清楚 x 的完整对象是什么意思,其中x
是一个对象。它是否与x
的完整对象相同?
答案 0 :(得分:7)
§1.8[intro.object] / p2-3:
对象可以包含其他对象,称为子对象。一个子对象 可以是成员子对象(9.2),基类子对象(子句) 10),或数组元素。不是任何子对象的对象 其他对象称为完整对象。
对于每个对象
x
,都有一个名为完整对象的对象x
的确定如下:
- 如果
x
是完整对象,则x
是x
的完整对象。- 否则,
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。
如果我出错了,请在下面的评论中告诉我,我会修复帖子。谢谢!