C++ 构造函数 : why is this virtual function call not safe?

标签 c++ c++11 constructor virtual-functions virtual-inheritance

这是来自 C++11 标准 sec 12.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:

    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 的构造函数.在 H 的构造函数中您可以享受红色椭圆内子层次结构的典型构造函数限制多态性。例如,您可以调用类型为 B 的基子对象的虚函数。 ,并且多态行为将在带圆圈的子层次结构内按预期工作(“按预期”意味着多态行为将在层次结构中低至 H,但不会更低)。您也可以调用 A 的虚函数, E , X和其他落在红色椭圆内的子对象。

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

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

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

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

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

    看一下标准中的示例。类(class)A派生自类 V .这意味着A的VMT指针物理上属于V子对象。 V 引入的对虚函数的所有调用通过 V 引入的 VMT 指针调度. IE。每当你打电话

    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。这显然毫无意义。 IE。具有 A 的虚拟方法通过 B 派送的 VMT 没有意义。行为未定义。

    这很容易通过实际实验来验证。让我们添加自己的版本 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;
    }
    

    您期待 A::f在这里被称为?我尝试了几个编译器,它们实际上都调用了 B::f !同时,this指针值 B::f在这样的电话中接收完全是假的。

    http://ideone.com/Ua332

    这正是出于我上面描述的原因(大多数编译器以我上面描述的方式实现多态)。这就是语言将此类调用描述为未定义的原因。

    人们可能会注意到,在这个特定示例中,实际上是虚拟继承导致了这种异常行为。是的,这正是因为 V子对象在 A 之间共享和 B子对象。如果没有虚拟继承,行为很可能会更可预测。然而,语言规范显然决定按照我在图中绘制的方式绘制线条:当您构建 H 时不允许您走出H的“沙箱”的子层次结构,无论使用什么继承类型。

    关于C++ 构造函数 : why is this virtual function call not safe?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11377629/

    相关文章:

    c++ - 避免 C++ 类中的内存泄漏

    python - 在 python gdb 脚本中索引 c++ vector

    java - Java 枚举构造函数中是否允许使用可变参数?

    c++ - 如何用 C++ 编写一个简单的类?

    scala - 覆盖继承的构造函数字段时的差异?

    c++ - [C++ 编译时断言] : Can we throw a compilation error if some condition is not met?

    c++ - 如何使用 C++ 和 Boost Asio 从 HTTP post 请求获取键值

    c++ - 同时具有模板和非模板构造函数的类

    c++ - 如何在编译时查询 constexpr std::tuple?

    c++ - 成员函数声明的参数列表后的单个&符号是什么意思?