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

C++ constructors: why is this virtual function call not safe?

这是从C++ 11标准秒秒7.4。这相当令人困惑。

  • 正文中最后一句话的确切意思是什么?
  • 为什么在B::B中最后一个方法调用是未定义的?它不是应该叫a.A::f吗?
  • 4 Member functions, including virtual functions (10.3), can be called
    during construction or destruction (12.6.2). When a virtual function
    is called directly or indirectly from a constructor or from a
    destructor, including during the construction or destruction of the
    class’s non-static data members, and the object to which the call
    applies is the object (call it x) under construction or destruction,
    the function called is the final overrider in the constructor’s or
    destructor’s class and not one overriding it in a more-derived class.
    If the virtual function call uses an explicit class member access
    (5.2.5) and the object expression refers to the complete object of x
    or one of that object’s base class subobjects but not x or one of its
    base class subobjects, the behavior is undefined. [ Example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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
    }

    —end example ]


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

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

    enter image description here。好的。型

    假设我们正在构造一个J类型的"大"对象。我们目前正在执行类H的构造函数。在EDOCX1[1]的构造函数内部,您可以享受到红色椭圆内部子层次结构的典型构造函数限制多态性。例如,可以调用B类型的基子对象的虚函数,并且多态行为将在循环的子层次结构中按预期工作("按预期"意味着多态行为将在层次结构中与H一样低,但不低)。也可以调用AEX和其他落在红色椭圆内的子对象的虚函数。好的。型

    但是,如果您以某种方式访问椭圆之外的层次结构,并尝试在那里使用多态性,那么行为将变得未定义。例如,如果您从H的构造函数中以某种方式获得对G子对象的访问权,并尝试调用G的虚拟函数,则行为是未定义的。从H的构造函数调用DI的虚函数也可以这样说。好的。型

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

    该标准将虚拟继承包含在示例中,以演示此规则的包容性。在上图中,基础子对象X由椭圆内的次等级和椭圆外的次等级共享。标准规定可以从H的构造函数调用X子对象的虚函数。好的。型

    注意,即使DGI子对象的构建在H的构建开始之前已经完成,此限制也适用。好的。型

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

    请看一下标准中的示例。类A源自类V。这意味着A的vmt指针物理上属于V子对象。所有由V引入的对虚拟函数的调用都通过V引入的vmt指针进行调度。也就是说,每当你打电话好的。

    1
    pointer_to_A->f();

    它实际上被翻译成好的。

    1
    2
    3
    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的指针放入存储在V中的vmt指针中(因为当B的构造函数处于活动状态时,V子对象必须充当B的一部分)。好的。

    如果此时你试图打电话好的。

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

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

    这是相当简单的验证与实际实验。让我们把它自己的版本f添加到B中,然后这样做。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #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子对象在AB子对象之间共享。如果没有虚拟继承,行为很可能更容易预测。然而,语言规范显然决定按照我的图表中绘制的方式画线:当您构建H时,无论使用什么继承类型,都不允许您走出H子层次结构的"沙盒"。好的。好啊。


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

    If the virtual function call uses an explicit class member access and the object expression refers to the complete object of x or one of that object’s base class subobjects but not x or one of its base class subobjects, the behavior is undefined.

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

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

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

    • 正在构造的对象是D对象的B基类子对象(因为这个基类子对象是当前正在构造的对象,所以文本称为x)。

    • 它使用显式类成员访问(在本例中,通过->操作符)

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

    • 对象表达式(A是指x的完整对象的基类子对象(指正在构造的D对象的A基类子对象)。

    • 对象表达式所指的基类子对象不是x,也不是x的基类子对象:A不是BA不是B的基类。

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

    Why is the last method call in B::B undefined? Shouldn't it just call a.A::f?

    您引用的规则规定,在构造期间调用构造函数时,"调用的函数是构造函数类中的最终重写器,而不是在派生类中重写它。"

    在这种情况下,构造函数的类是B。由于B不是从A派生出来的,因此虚拟函数没有最终的重写器。因此,尝试使虚拟调用显示未定义的行为。


    以下是我如何理解的:在对象的构造过程中,每个子对象构造它的部分。在示例中,它意味着V::V()初始化V的成员;A初始化A的成员,依此类推。由于VAB之前初始化,因此它们都可以依赖V的成员进行初始化。

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

    编辑:

    在上面的D中,A是在B之前初始化的,因此不会有任何访问A未初始化内存的权限。另一方面,一旦A被完全构造,其虚拟功能就被D的虚拟功能覆盖(实际上,其vtable在构造期间设置为A的,在构造结束后设置为D的)。因此,在初始化D之前,对a->f()的调用将调用D::f()。因此,无论是在B之前还是在B之后构造-A,都要对未初始化的对象调用方法。

    虚拟函数部分已经在这里讨论过了,但是为了完整性:对EDOCX1的调用(27)使用V::f,因为A尚未初始化,而对于B,这是f的唯一实现。g()调用B::g,因为B重写g