13

这是来自 C++11 标准 sec 12.7.4。这相当令人困惑。

  1. 文中最后一句究竟是什么意思?
  2. 为什么最后一个方法调用B::B未定义?它不应该只是打电话a.A::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
}

—结束示例]

4

3 回答 3

19

标准的那部分只是告诉您,当您构建一些J其基类层次结构包括多重继承的“大”对象时,并且您当前位于某个基本子对象的构造函数中H,那么您只能使用H和的多态性它的直接和间接基础子对象。不允许在该子层次结构之外使用任何多态性。

例如,考虑这个继承图(从派生类指向基类的箭头)

在此处输入图像描述

假设我们正在构建一个类型为“大”的对象J。我们目前正在执行 class 的构造函数H。在您的构造函数内部,H您可以享受红色椭圆内子层次结构的典型构造函数限制多态性。例如,您可以调用类型为 的基本子对象的虚函数,B多态行为将在圆圈子层次结构内按预期工作(“如预期”意味着多态行为将H与层次结构一样低,但不会更低)。您还可以调用位于红色椭圆内的 、 和其他子对象AE虚函数。X

但是,如果您以某种方式访问​​椭圆之外的层次结构并尝试在那里使用多态性,则行为将变得未定义。例如,如果您以某种方式G从 of 的构造函数获得对子对象的访问权H并尝试调用 of 的虚函数G- 行为是未定义的。D关于调用 的构造函数和I从构造函数调用虚函数也是如此H

获得对“外部”子层次结构的这种访问的唯一方法是,如果有人以某种方式将G子对象的指针/引用传递给H. 因此,标准文本中对“显式类成员访问”的引用(尽管似乎过度)。

该标准在示例中包含了虚拟继承,以展示该规则的包容性。在上图中,基本子对象X由椭圆形内部的子层次结构和椭圆形外部的子层次结构共享。标准说可以XH.

D请注意,即使GI子对象的构造在 的构造开始之前已经完成,此限制也适用H


该规范的根源导致了实现多态机制的实际考虑。在实际实现中,VMT 指针作为数据字段被引入到层次结构中最基本的多态类的对象布局中。派生类不引入自己的 VMT 指针,它们只是为基类(可能还有更长的 VMT)引入的指针提供自己的特定值。

看看标准中的例子。类A派生自类V。这意味着A物理上的 VMT 指针属于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's VMT 的指针放入存储在 VMT 指针中V(因为当B' 构造函数处于活动状态时V,子对象必须充当 的一部分B)。

如果此时您以某种方式尝试调用

a->f(); // as in the example

上述算法将找到B存储在其子对象中的 VMT 指针,V并尝试f()通过该 VMT 进行调用。这显然毫无意义。A即通过 's VMT 调度的虚拟方法是B没有意义的。行为未定义。

通过实际实验验证这一点相当简单。让我们添加自己的fto版本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都不允许走出 's 子层次结构的“沙箱” 。H

于 2012-07-07T19:29:02.103 回答
1

您引用的规范性文本的最后一句话如下:

如果虚函数调用使用显式类成员访问并且对象表达式引用x该对象的基类子对象的完整对象或其中之一,但不是x其基类子对象或其基类子对象之一,则行为未定义。

诚然,这是相当复杂的。这句话的存在是为了限制在存在多重继承的情况下,在构造过程中可以调用哪些函数。

该示例包含多重继承: D派生自Aand B(我们将忽略V,因为不需要演示为什么行为未定义)。在D对象的构造过程中,都会调用AB构造函数来构造对象的基类子D对象。

B调用构造函数时,完整对象的x类型为D。在该构造函数中,a是一个指向 的A基类子对象的指针x。所以,我们可以说以下关于a->f()

  • 正在构建B对象是一个对象的基类子D对象(因为这个基类子对象就是当前正在构建的对象,所以文中所指的就是x)。

  • 它使用显式的类成员访问(在这种情况下通过->运算符)

  • 的完整对象的x类型是D,因为这是正在构造的最派生类型

  • 对象表达式( a) 指的是完整对象的一个​​基类x对象(它指的是正在构造A的对象的基类子对象)D

  • 对象表达式所引用的基类子对象不是也不x是 的基类子对象xA不是也不BA的基类B

因此,根据我们从一开始就开始的规则,调用的行为是未定义的。

为什么最后一个方法调用B::B未定义?它不应该只是打电话a.A::f吗?

您引用的规则指出,在构造过程中调用构造函数时,“调用的函数是构造函数类中的最终覆盖者,而不是在派生更多的类中覆盖它的函数。”

在这种情况下,构造函数的类是B. 因为B不是从 派生的A,所以虚函数没有最终覆盖器。因此,进行虚拟调用的尝试表现出未定义的行为。

于 2012-07-07T18:51:33.890 回答
0

我是这样理解的:在对象的构造过程中,每个子对象都构造了它的一部分。在示例中,这意味着V::V()初始化V的成员;A初始化A的成员,依此类推。由于在和V之前初始化,它们都可以依赖的成员来初始化。ABV

在示例中,B的构造函数接受两个指向自身的指针。它的V部分已经构建好了,所以可以安全地调用v->g(). 但是,此时D' 的A部分尚未初始化。因此,调用a->f()访问未初始化的内存,这是未定义的行为。

编辑:

D上面,是在A之前初始化的B,所以不会有任何访问A的未初始化内存。另一方面,一旦A完全构建,它的虚函数就会被那些虚函数覆盖D(实际上:它的 vtableA在构建过程中被设置为 's,D一旦构建结束就被设置为 's)。因此,调用a->f()will invoke D::f(), beforeD已经被初始化。因此,无论哪种方式 -A在之前或之后构造B- 您都将在未初始化的对象上调用方法。

虚函数部分已经在这里讨论过了,但为了完整起见:对f()uses的调用V::f因为A尚未初始化,就目前B而言,这是f. g()调用B::g,因为B覆盖g.

于 2012-07-07T18:46:57.433 回答