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

C++ C++;构造函数:为什么这个虚拟函数调用不安全?,c++,c++11,constructor,virtual-functions,virtual-inheritance,C++,C++11,Constructor,Virtual Functions,Virtual Inheritance,这来自C++11标准第12.7.4节。这相当令人困惑 课文中最后一句话的确切意思是什么 为什么B::B中的最后一个方法调用未定义?它不应该只调用a.a::f 可以调用4个成员函数,包括虚拟函数(10.3) 施工或破坏期间(12.6.2)。当一个虚拟函数 直接或间接地从构造函数或 析构函数,包括在构建或销毁 类的非静态数据成员,以及调用 应用是正在构造或销毁的对象(称为x), 调用的函数是构造函数的or中的最终重写器 析构函数的类,而不是在更派生的类中重写它的类。 如果虚拟函数调用使用显式类成员访

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

  • 课文中最后一句话的确切意思是什么
  • 为什么
    B::B
    中的最后一个方法调用未定义?它不应该只调用a.a::f 可以调用4个成员函数,包括虚拟函数(10.3) 施工或破坏期间(12.6.2)。当一个虚拟函数 直接或间接地从构造函数或 析构函数,包括在构建或销毁 类的非静态数据成员,以及调用 应用是正在构造或销毁的对象(称为x), 调用的函数是构造函数的or中的最终重写器 析构函数的类,而不是在更派生的类中重写它的类。 如果虚拟函数调用使用显式类成员访问 (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
    }
    
    -[结束示例]


    我是这样理解的:在构建一个对象的过程中,每个子对象构建它的一部分。在本例中,这意味着
    V::V()
    初始化
    V
    的成员
    A
    初始化
    A
    的成员,依此类推。由于
    V
    是在
    A
    B
    之前初始化的,因此它们都可以依赖
    V
    的成员进行初始化

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

    编辑:

    在上面的
    D
    中,
    A
    B
    之前被初始化,因此将无法访问
    A
    的未初始化内存。另一方面,一旦
    A
    完全构建,其虚拟功能将被
    D
    的虚拟功能覆盖(实际上:其vtable在构建期间设置为
    A
    ,在构建结束后设置为
    D
    )。因此,对
    a->f()
    的调用将在
    D
    初始化之前调用
    D::f()
    。因此,无论哪种方式-
    A
    都是在
    B
    之前或之后构造的-您将对未初始化的对象调用一个方法


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

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

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

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

    该示例包含多重继承:
    D
    派生自
    A
    B
    (我们将忽略
    V
    ,因为不需要演示行为未定义的原因)。在构造
    D
    对象期间,将调用
    a
    B
    构造函数来构造
    D
    对象的基类子对象

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

    • 正在构造的对象是
      D
      对象的
      B
      基类子对象(因为此基类子对象是当前正在构造的对象,所以文本将其称为
      x

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

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

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

    • 对象表达式引用的基类子对象不是
      x
      ,也不是
      x
      的基类子对象:
      a
      不是
      B
      a
      不是
      B
      的基类

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

    为什么
    B::B
    中的最后一个方法调用未定义?它不应该只调用
    a.a::f

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


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

    标准的这一部分是simp
    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
    
    a->f(); // as in the example
    
    #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;
    }