C++构造函数:为什么这个虚函数调用不安全?

Tho*_*eod 13 c++ constructor virtual-functions virtual-inheritance c++11

这是来自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
}
Run Code Online (Sandbox Code Playgroud)

- 末端的例子]

AnT*_*AnT 19

标准的那一部分只是告诉你,当你构建一个J基类层次包含多重继承的"大"对象时,你当前正在某个基础子对象的构造函数中H,那么你只能使用H和的多态性它的直接和间接基础子对象.您不能在该子层次结构之外使用任何多态性.

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

在此输入图像描述

假设我们正在构建一个"大"类型的对象J.我们正在执行类的构造函数H.在H你的构造函数内部,可以享受红色椭圆内部子层次结构的典型构造函数限制多态.例如,您可以调用类型的基础子对象的虚函数B,并且多态行为将在带圆圈的子H层次结构内按预期工作("按预期"表示多态行为将低于层次结构中的低,但不低).您也可以调用虚函数A,E,X落在了红色椭圆内等子对象.

但是,如果您以某种方式获取对椭圆外部的层次结构的访问权并尝试在其中使用多态,则行为将变为未定义.例如,如果您以某种方式G从构造函数获取对子对象的访问权H并尝试调用虚函数G- 则行为未定义.关于调用构造函数的虚函数DI从构造函数调用虚函数也可以这么说H.

获得对"外部"子层次结构的这种访问的唯一方法是,如果有人以某种方式将指向/引用传递给G子对象到构造函数中H.因此,在标准文本中引用了"显式类成员访问"(虽然它似乎过多).

该标准包括示例中的虚拟继承,以演示此规则的包容性.在上图中,基础子对象X由椭圆内部的子层次结构和椭圆形外部的子层次结构共享.标准说可以X从构造函数中调用子对象的虚函数H.

请注意,此限制适用于以下情况的建设D,GI施工前的子对象已完成H开始.


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

看看标准中的示例.该类A派生自类V.这意味着A物理上属于V子对象的VMT指针.所引入的所有虚拟函数调用V都是通过引入的VMT指针调度的V.即只要你打电话

pointer_to_A->f();
Run Code Online (Sandbox Code Playgroud)

它实际上被翻译成了

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
Run Code Online (Sandbox Code Playgroud)

但是,在标准的示例中,V也嵌入了相同的子对象B.为了使构造函数限制的多态性正常工作,编译器会将指向BVMT的指针放入存储在其中的VMT指针中V(因为虽然B构造函数是活动V子对象必须作为其中的一部分B).

如果此时你以某种方式试图打电话

a->f(); // as in the example
Run Code Online (Sandbox Code Playgroud)

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

通过实际实验验证这一点非常简单.让我们添加的自己的版本fB,做这个

#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;
}
Run Code Online (Sandbox Code Playgroud)

你希望A::f在这里被称为?我尝试了几个编译器,所有这些编译器实际上都在调用B::f!同时,在这种调用中接收的this指针值B::f完全是假的.

http://ideone.com/Ua332

这恰好是出于我上面描述的原因(大多数编译器按照我上面描述的方式实现多态).这就是语言将此类调用描述为undefined的原因.

有人可能会注意到,在这个具体的例子中,实际上是虚拟继承导致了这种不寻常的行为.是的,这恰好是因为V子对象在子对象AB子对象之间共享.很可能没有虚拟继承,行为就会更加可预测.但是,语言规范显然决定只绘制在我的图中绘制它的方式:在构建H时,H不管使用什么继承类型,都不允许跳出子层次结构的"沙箱" .