这一部分标准仅告诉你,在构建包含多重继承的基类层次结构的“大型”对象J时,如果你目前正在某个基类子对象H的构造函数中,那么你只能使用H及其直接和间接基类子对象的多态性。你不能在该子层次结构之外使用任何多态性。
例如,考虑以下继承图(箭头从派生类指向基类):
假设我们正在构造一个类型为J的“大”对象。我们当前正在执行类H的构造函数。在H的构造函数内,您可以享受红色椭圆体系结构中子层次结构的典型构造函数限制多态性。例如,您可以调用类型为B的基本子对象的虚函数,并且多态行为将按预期工作在圆形子层次结构内(“按预期”意味着多态行为将延伸到层次结构中低于H的部分,但不会更低)。您还可以调用A、E、X和其他落在红色椭圆内的子对象的虚函数。
但是,如果您以某种方式访问椭圆外部的层次结构并尝试在那里使用多态性,则行为变得未定义。例如,如果您以某种方式从H的构造函数中获得访问G子对象的权限并尝试调用G的虚函数,则行为是未定义的。同样,也可以说从H的构造函数中调用D和I的虚函数的行为是未定义的。
唯一获得对“外部”子层次结构访问权限的方法是,如果某人以某种方式将指针/引用传递给
G
子对象的构造函数。因此,在标准文本中提到“显式类成员访问”的引用(尽管它似乎有些过度)。
标准将
虚拟继承包含在示例中,以演示这个规则是多么包容。在上图中,基础子对象
X
被椭圆形内外的子层次结构共享。标准说,可以从
H
的构造函数调用
X
子对象的虚拟函数。
请注意,即使在
H
的构造之前,
D
、
G
和
I
子对象的构建已经完成,此限制也适用。
这个规范的根源在于实现多态机制的实际考虑。在实际实现中,VMT指针被引入作为数据字段到继承层级结构中最基本的多态类对象布局中。派生类不会引入自己的VMT指针,它们只是为基类引入的指针提供自己特定的值(可能更长的VMT)。
看一下标准中的例子。类A派生自类V。这意味着A的VMT指针物理上属于V子对象。所有由V引入的虚函数调用都通过由V引入的VMT指针进行分派。也就是说,无论何时调用,都是通过VMT来调用。
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
中。为了使受构造函数限制的多态正常工作,编译器将在存储在
V
中的VMT指针中放置一个指向
B
的VMT的指针(因为当
B
的构造函数处于活动状态时,
V
子对象必须作为
B
的一部分)。
如果此时您尝试调用
a->f()
上述算法将在B的V子对象中查找其VMT指针,并尝试通过该VMT调用f()。这显然毫无意义。即,通过B的VMT分派A的虚方法是没有意义的。行为未定义。
这很容易通过实际实验验证。让我们向B添加自己版本的f并执行此操作
#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();
}
int main() {
D d;
}
你希望在这里调用A::f
吗?我尝试了几个编译器,它们实际上都调用了B::f
!同时,在这种调用中B::f
接收到的this
指针值是完全错误的。
http://ideone.com/Ua332
这正是我上面所述的原因(大多数编译器实现了我上面描述的多态性)。这就是语言将这些调用描述为未定义的原因。
有人可能会注意到,在这个特定的例子中,实际上是虚继承导致了这种不寻常的行为。是的,正是因为V子对象在A和B子对象之间共享,才会发生这种情况。如果没有虚继承,行为很可能会更加可预测。然而,语言规范显然决定只按照我的图表所示的方式划线:当你构造H时,无论使用什么继承类型,都不允许你走出H子层次结构的“沙盒”。