【问题标题】:virtual inheritance constructor order虚拟继承构造函数顺序
【发布时间】:2015-06-01 05:44:28
【问题描述】:

我正在尝试更好地理解虚拟继承的概念,以及它的危险。

我在另一篇文章 (Why is Default constructor called in virtual inheritance?) 中读到它(= 虚拟继承)改变了构造函数调用的顺序(首先调用“祖母”,而没有虚拟继承则不会)。

所以我尝试了以下方法,看看我明白了(VS2013):

#define tracefunc printf(__FUNCTION__); printf("\r\n")
struct A
{
    A(){ tracefunc; }

};

struct B1 : public A
{
    B1(){ tracefunc; };
};

struct B2 : virtual public A
{
    B2() { tracefunc; };
};

struct C1 : public B1
{
    C1() { tracefunc; };
};

struct C2 : virtual public B2
{
    C2() { tracefunc; };
};

int _tmain(int argc, _TCHAR* argv[])
{
    A* pa1 = new C1();
    A* pa2 = new C2();
}

输出是:

A::A
B1::B1
C1::C1
A::A
B2::B2
C2::C2

这不是我所期望的(我预计 2 个类的顺序会有所不同)。

我错过了什么?有人可以解释或指导我找到解释更好的来源吗?

谢谢!

【问题讨论】:

  • 你期待什么? “祖母”总是先被叫到。你使用什么样的继承并不重要。如果不同,低级类如何在其构造函数中使用父级的数据?
  • 虚拟继承仅在多重继承的情况下有所不同。提供它是为了通过继承图共享一些结构。

标签: c++ inheritance virtual-inheritance


【解决方案1】:

在您的示例中,您的输出是预期的。 Virtual inheritance 在实例中发挥作用,当您有一个具有多重继承的类时,其父类也继承自同一类/类型(即“钻石问题”)。在您的示例中,您的类可能设置为虚拟继承(如果代码中的其他地方需要),但它们不一定根据您的示例“虚拟继承”,因为没有一个派生类(B1/B2/C1/C2)做更多直接从A继承。

为了扩展,我调整了你的例子来解释更多:

#include <cstdio>

#define tracefunc printf(__FUNCTION__); printf("\r\n")
struct A
{
    A() { tracefunc; }
    virtual void write() { tracefunc; }
    virtual void read() { tracefunc; }
};

struct B1 : public A
{
    B1() { tracefunc; };
    void read(){ tracefunc; }
};

struct C1 : public A
{
    C1() { tracefunc; };
    void write(){ tracefunc; }
};

struct B2 : virtual public A
{
    B2() { tracefunc; };
    void read(){ tracefunc; }
};

struct C2 : virtual public A
{
    C2() { tracefunc; };
    void write(){ tracefunc; }
};

// Z1 inherits from B1 and C1, both of which inherit from A; when a call is made to any
// of the base function (i.e. A::read or A::write) from the derived class, the call is
// ambiguous since B1 and C1 both have a 'copy' (i.e. vtable) for the A parent class.
struct Z1 : public B1, public C1
{
    Z1() { tracefunc; }
};

// Z2 inherits from B2 and C2, both of which inherit from A virtually; note that Z2 doesn't
// need to inherit virtually from B2 or C2. Since B2 and C2 both virtual inherit from A, when
// they are constructed, only 1 copy of the base A class is made and the vtable pointer info
// is "shared" between the 2 base objects (B2 and C2), and the calls are no longer ambiguous
struct Z2 : public B2, public C2
{
    Z2() { tracefunc; }
};


int _tmain(int argc, _TCHAR* argv[])
{
    // gets 2 "copies" of the 'A' base since 'B1' and 'C1' don't virtually inherit from 'A'
    Z1 z1;
    // gets only 1 "copy" of 'A' base since 'B2' and 'C2' virtualy inherit from 'A' and thus "share" the vtable pointer to the 'A' base
    Z2 z2;

    z1.write(); // ambiguous call to write (which one is it .. B1::write() (since B1 inherits from A) or A::write() ?)
    z1.read(); // ambiguous call to read (which one is it .. C1::read() (since C1 inherits from A) or A::read() ?)

    z2.write(); // not ambiguous: z2.write() calls C2::write() since it's "virtually mapped" to/from A::write() 
    z2.read(); // not ambiguous: z2.read() calls B2::read() since it's "virtually mapped" to/from A::read() 
    return 0;
}

虽然对于我们人类来说,在 z1 变量的情况下我们打算进行调用可能是“显而易见的”,但由于 B1 没有 write 方法,我会“期待”编译器选择C1::write 方法,但是由于对象的内存映射如何工作,它会出现一个问题,因为C1 对象中A 的基本副本可能具有与对象不同的信息(指针/引用/句柄) B1 对象中 A 基址的副本(因为从技术上讲,A 基址有 2 个副本);因此,调用B1::read() { this-&gt;write(); } 可能会产生意想不到的行为(尽管不是未定义的)。

基类说明符上的 virtual 关键字明确表明,实际上从同一基类型继承的其他类只能获得基类型的 1 个副本。

请注意,上面的代码应该无法编译,编译器错误解释了对 z1 对象的模棱两可的调用。如果您注释掉 z1.write();z1.read(); 行,输出(至少对我而言)如下:

A::A
B1::B1
A::A
C1::C1
Z1::Z1
A::A
B2::B2
C2::C2
Z2::Z2
C2::write
B2::read

注意在构造 Z1 之前对 A ctor (A::A) 的 2 次调用,而 Z2 仅对 A 构造函数的 1 次调用。

我建议阅读the following on virtual inheritance,因为它更深入地介绍了需要注意的其他一些陷阱(例如虚拟继承的类需要使用初始化列表来进行基类 ctor 调用,或者您应该在进行此类继承时避免使用 C 风格的强制转换)。

它还进一步解释了您最初提到的构造函数/析构函数排序,更具体地说,使用多重虚拟继承时排序是如何完成的。

希望这能帮助你理清头绪。

【讨论】:

    【解决方案2】:

    编译器的输出是正确的。其实这就是关于虚拟继承的目标。虚拟继承旨在解决多重继承中的“钻石问题”。比如B继承A,C继承A,D继承B,C。示意图是这样的:

        A
       | |
       B C
       | |
        D
    

    所以,D 有来自 B 和 C 的两个实例 A。如果 A 有虚函数,那就有问题了。

    例如:

    struct A
    {
        virtual void foo(){__builtin_printf("A");}
        virtual void bar(){}
    };
    
    struct B : A
    {
        virtual void foo(){__builtin_printf("B");}
    };
    
    struct C : A
    {
        virtual void bar(){}
    };
    
    struct D : B, C
    {
    
    };
    
    int main()
    {
        D d;
        d.foo(); // Error
    }
    

    如果我使用我的 xlC 编译器来编译和运行:

    xlC -+ a.C
    

    错误信息是这样的:

    a.C:25:7: error: member 'foo' found in multiple base classes of different types
        d.foo(); // Error
          ^
    a.C:9:18: note: member found by ambiguous name lookup
        virtual void foo(){__builtin_printf("B");}
                     ^
    a.C:3:18: note: member found by ambiguous name lookup
        virtual void foo(){__builtin_printf("A");}
                     ^
    1 error generated.
    Error while processing a.C.
    

    错误信息很清楚,成员'foo'在多个不同类型的基类中找到。如果我们添加虚拟继承,问题就解决了。因为A的施工权由D处理,所以A的实例只有一个。

    回到你的代码,继承图是这样的:

    A     A
    |     |
    B1    B2
    |     |
    C1    C2
    

    不存在“钻石问题”,这只是单继承。所以,构造顺序也是A->B2->C2,输出没有区别。

    【讨论】:

      【解决方案3】:

      您将无法在输出中看到任何差异,因为以下任何类层次结构中的输出都是相同的:

      层次结构 1

      class A {};
      
      class  B2 : virtual public A {};
      
      class  C2 : virtual public B2 {};
      

      层次结构 2

      class A {};
      
      class  B2 : public A {};
      
      class  C2 : virtual public B2 {};
      

      层次结构 3

      class A {};
      
      class  B2 : virtual public A {};
      
      class  C2 : public B2 {};
      

      层次结构 3

      class A {};
      
      class  B2 : public A {};
      
      class  C2 : public B2 {};
      

      在所有这些情况下,将首先执行A::A(),然后是B2::B2(),然后是C2::C2()

      它们之间的区别在于A::A() 何时被调用。是从B2::B2() 还是C2::C2() 调用的?

      我对 Hiarchy 1 的答案不是 100% 清楚。我认为B2::B2() 应该从C2::C2 调用,因为B2C 的虚拟基类。 A::A() 应该从B2:B2() 调用,因为AB2 的虚拟基类。但我可能在确切的顺序上错了。

      层次结构 2 中,A::A() 将从 B2::B2() 调用。因为B2C2virtual 基类,所以B2::B2()C2::C2() 中调用。因为AB2 的普通基类,所以A::A()B2::B2() 调用。

      Hierarchy 2中,A::A() 将从C2::C2() 调用。由于A 是一个虚拟基类,所以A::A() 会从C2::C2() 调用。 B2::B2() 在对 A::A() 的调用完成后被调用。

      Hierarchy 4 中,A::A() 将从 B2::B2() 调用。我觉得这个案子不用解释了。

      为了澄清我对 Hiarchy 1 的疑问,我使用了以下程序:

      #include <iostream>
      
      class A
      {
         public:
            A(char const *from) { std::cout << "Called from : " << from << std::endl; }
      
      };
      
      class  B2 : virtual public A
      {
         public:
            B2() : A("B2::B2()") {}
      };
      
      class  C2 : virtual public B2
      {
         public:
            C2() : A("C2::C2()") {}
      };
      
      int main()
      {
         C2 c;
      }
      

      我得到以下输出:

      Called from : C2::C2()
      

      这证实了@T.C 在他的评论中指出的,这与我的预期不同。 A::A() 是从 C2::C2 调用的,而不是从 B2::B2 调用的。

      【讨论】:

      • 虚基总是由最派生类的构造函数构造;它们的构造顺序是深度优先从左到右遍历基类的 DAG。因此,在1中,C2::C2()会先调用A::A(),再调用B2::B2()
      • ...其中“从左到右”是指代码中出现的顺序,遍历是后序
      猜你喜欢
      • 2019-10-11
      • 1970-01-01
      • 2015-06-21
      • 1970-01-01
      • 1970-01-01
      • 2013-10-24
      • 2018-04-18
      • 2021-06-02
      • 1970-01-01
      相关资源
      最近更新 更多