这是从C ++ 11标准秒12.7.4。 这是相当混乱。
- 是什么在文字的最后一句意味着什么?
- 为什么在过去的方法调用
B::B
不确定? 它Shoudn't就叫aA::f
?
4个成员函数,包括虚拟功能(10.3),可构造或破坏(12.6.2)中被调用。 当虚拟函数是从一个构造或从析构函数直接或间接调用,包括类的非静态数据成员的建设或毁灭期间,以及该呼叫应用的对象是对象(称为X)在建或销毁,调用的函数是在构造函数和析构函数的类,而不是一个覆盖它更派生类的最终超控器。 如果虚拟函数调用使用的显式类成员访问(5.2.5)和对象表达是指x的完整对象或对象的基类的子对象中的一个,但不是x或它的基类的子对象中的一个,所述行为是未定义的。 [实施例:
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 }
末端示例]
标准的部分,是简单地告诉你,当你建造一些“大”对象J
其基类层次结构包括多重继承,目前你正坐在一些基本的子对象的构造函数中H
,那么你只被允许使用多态的H
和其直接和间接基子对象。 您不允许使用任何多态性该子层级之外。
例如,考虑这继承图(从箭头派生类指向基类)
比方说,我们正在建设类型的“大”对象J
。 我们目前正在执行类的构造函数H
。 的构造函数中的H
允许您享受红色椭圆内的子层级的典型构造限制的多态性。 例如,你可以调用类型的基类子对象的虚函数B
,和多态行为可以发挥预期的圆圈子层级内(“预期”的意思是多态行为会低至H
的层次,但不低于)。 您也可以调用虚函数A
, E
, X
落在了红色椭圆内等子对象。
但是,如果你以某种方式获得的椭圆形外的层次,并尝试那里使用多态,行为变得不确定。 例如,如果你以某种方式获取G
从构造子对象H
,并试图调用虚函数G
-的行为是不确定的。 同样可以有关调用虚拟功能可以说D
和I
从构造H
。
获得这样获得了“外”子层级的唯一方法是,如果有人以某种方式传递一个指针/参考G
子对象进入的构造H
。 因此,参照在标准文本“显式类成员访问”(虽然这似乎是过度)。
该标准包括虚拟继承到例子来说明这条规则如何包容性的。 另外,在上述图基子对象X
是由两个椭圆外的椭圆形和子层级内的子层级共享。 该标准说,这是确定调用虚函数X
子对象从构造H
。
请注意,即使建设这个限制适用D
, G
和I
之前的建设子对象已完成H
开始。
本规范的根源导致实现多态机制的实际考虑。 在实际的实现中,VMT指针被引入作为数据字段到在层次结构中最基础的多态类的对象布局。 派生类不介绍自己的VMT的指针,他们只是提供自己的具体数值由基类(和可能的话,再VMTS)推出的指针。
看看从标准的例子。 类A
从类派生V
。 这意味着的VMT指针A
物理属于V
子对象。 通过引入虚拟函数的调用V
是通过引入VMT指针出动V
。 也就是说,只要你打电话
pointer_to_A->f();
它实际上是翻译成
V *v_subobject = (V *) pointer_to_A; // go to V
vmt = v_subobject->vmt_ptr; // retrieve the table
vmt[index_for_f](); // call through the table
然而,在从标准的例子非常相同的V
子对象也被嵌入到B
。 为了正确地使构造限制的多态性工作,编译器将放置一个指向B
的VMT到存储在VMT指针V
(因为在B
的构造是有源V
子对象具有作为一部分B
)。
如果这时你会莫名其妙地尝试调用
a->f(); // as in the example
上述算法将找到B
存储在其的VMT指针V
子对象和将尝试呼叫f()
通过VMT。 这显然是没有意义的。 即,具有虚拟方法A
分派通过B
的VMT是没有意义的。 行为是不确定的。
这是相当简单的验证与实际的实验。 让我们添加了自己的版本f
到B
和做到这一点
#include <iostream>
struct V {
virtual void f() { std::cout << "V" << std::endl; }
};
struct A : virtual V {
virtual void f() { std::cout << "A" << std::endl; }
};
struct B : virtual V {
virtual void f() { std::cout << "B" << std::endl; }
B(V*, A*);
};
struct D : A, B {
virtual void f() {}
D() : B((A*)this, this) { }
};
B::B(V* v, A* a) {
a->f(); // What `f()` is called here???
}
int main() {
D d;
}
您预计A::f
在这里叫什么名字? 我试了编译器,一个所有的人实际调用B::f
! 同时, this
指针值B::f
在这种呼叫接收是完全伪造的。
http://ideone.com/Ua332
这种情况正是我针对上述原因(大多数编译器实现多态性我上面描述的方式)。 这是语言描述了这样的呼叫未定义的原因。
人们可能会注意到,在这个具体的例子,它实际上是虚拟继承导致此异常行为。 是的,它发生正是由于V
子对象之间共享A
和B
子对象。 这是很可能的,如果没有虚拟继承的行为将可预测性更高。 然而,语言规范显然决定只画线的是在我的图的绘制方式:当你构建H
你不准走出的“沙箱”的H
的子层级,无论使用何种遗传型。
你引用的规范性文本的最后一句内容如下:
如果虚拟函数调用使用的显式类成员访问和对象表达指的完整对象x
或该对象的基类的子对象中的一个,但不是x
或它的基类的子对象中的一个,所述行为是未定义的。
这是无可否认的,而令人费解。 这句话存在限制可以建设多重继承的存在过程中可以调用什么功能。
示例包含多重继承: D
派生自A
和B
(我们将忽略V
,因为它没有说明为什么该行为是不确定的要求)。 在施工期间的一个D
物体,无论是A
和B
构造将被称为构造的基类的子对象D
对象。
当B
构造函数被调用, 的完整对象的类型x
是D
。 在该构造, a
是一个指针,指向A
的基类子对象x
。 因此,我们可以说一下下面的a->f()
在建的对象是B
一个的基类子对象D
对象(因为此基类子对象目前正在构造中的物体,它是什么文本指的是作为x
)。
它使用显式类成员访问 (通过->
运算符,在这种情况下)
的类型的完整对象的x
是D
,因为这是要构造的最派生的类型
对象表达 ( a
)是指完整对象的基类子对象 x
(它指的是A
所述的基类子对象D
正在构造的对象)
基类子对象到该对象的表达指不是x
和不是基类的子对象x
: A
不是B
和A
不是基类的B
。
因此,调用的行为是不明确的,按我们从一开始启动的规则。
为什么在过去的方法调用B::B
不确定? 难道不应该只是叫aA::f
?
你引用的规则规定,当一个构造施工过程中被称为“调用的函数是在构造函数的类最终置换器,而不是一个更派生类中重写它。”
在这种情况下,构造函数的类是B
。 因为B
不从派生A
,存在用于所述虚拟功能没有最终超控器。 因此,为了使虚拟呼叫尝试表现出不确定的行为。
Here's how I understand this: During the construction of an object, each sub-object constructs its part. In the example, it means that V::V()
initializes V
's members; A
initializes A
's members, and so on. Since V
is initialized before A
and B
, they can both rely on V
's members to be initialized.
In the example, B
's constructor accepts two pointers to itself. Its V
part is already constructed, so it's safe to call v->g()
. However, at that point D
's A
part has not been initialized yet. Therefore, the call a->f()
accesses uninitialized memory, which is undefined behavior.
Edit:
In the D
above, A
is initialized before B
, so there won't be any access to A
's uninitialized memory. On the other hand, once A
has been fully constructed, its virtual functions are overridden by those of D
(in practice: its vtable is set to A
's during construction, and to D
's once the construction is over). Therefore, the call to a->f()
will invoke D::f()
, before D
has been initialized. So either way - A
is constructed before B
or after - you're going to call a method on an uninitialized object.
The virtual functions part has already been discussed here, but for completeness: the call to f()
uses V::f
because A
has not been initialized yet, and as far as B
is concerned, that's the only implementation of f
. g()
calls B::g
because B
overrides g
.