虚拟继承对构造函数的影响

Virtual inheritance impact on constructor

我已经开始学习虚拟继承(以及它如何解决 class 派生自两个父 class 具有相同父的问题)。为了更好地理解其背后的机制,我举了一个例子:

class A {
public: 
    A(string text = "Constructor A") { cout << text << endl; }
};

class B:  public A {
public:
    B(): A("A called from B") { cout << "Constructor B" << endl; }
};

class C : virtual public A {
public:
    C() : A("A called from C") { cout << "Constructor C" << endl; }
};

class D :  public B,  public C {
public: 
    D() { cout << "Constructor D" << endl; }
};

我有 class Aclass B 来自 Aclass C 实际上来自 Aclass D 来自 BC。在主要我只是形成一个对象 class D: D d; 我得到以下输出

Constructor A
A called from B
Constructor B
Constructor C
Constructor D

令我困扰的是,为什么 "Constructor A" 表明它没有被 class BC 调用。还有为什么"Constructor C"之前没有"A called from C"。对于后一个,我知道它与虚拟派生的 class C 有关,所以我猜它不会再次调用 Constructor A,因为来自 class A 的对象已经形成(实际上是两次) .

编辑:

主要我只做了一个对象类型D。

int main() {
  D d;
}

问题

您的代码忘记了一些东西,您在 D 对象中仍然有两个 A 对象。您可以通过将 public int test 成员添加到 A 来验证此声明,然后尝试以下代码。您将获得两个不同的地址:

D d;
cout << &d.B::test <<endl;   // the non virtual A subobject of B
cout << &d.C::test <<endl;   // the virtual A subobject of C

Online demo

解决方案

所有 classes 虚拟共享 A 并且直接继承自 A 必须声明虚拟继承。所以你需要更正 class B:

class B:   virtual public A {...}  // you forgot the virtual here 

上面的代码片段将按预期工作,您甚至可以解决 d.test 而不会出现任何歧义错误。 Online demo

编辑:A 的精巧结构

C++ 规则要求每个具有虚拟 A 子对象的对象为 A 提供构造函数。在您的情况下,D 应该提供虚拟 A 的构造。

由于没有显式构造函数,D 将寻找默认构造函数 A。它会找到一个,因为您为 A 构造函数提供了默认参数。它具有误导性,因为它告诉您 "A constructor" 而实际上是 D 使用它。

如果删除此默认参数,您的代码将无法再编译。然后你需要这样的东西来正确构造 A

class A {
public: 
    int test; 
    A(string text) { cout << "A is constructed: "<<text << endl; }
};

class D :  public B,  public C {
public: 
    D() : A("Mandatory, if there is no default consructor") { cout << "Constructor D" << endl; }
};

Online demo

为什么C++构造规则是这样的?因为当你有虚拟继承时,没有理由BA构造是C 绘制在结构上。也不是相反。另一方面,BC 都定义了如何构造它们的 A 子对象。为了解决选择正确结构的歧义,决定了这条规则。如果虚拟 class 没有默认构造函数,这可能会很痛苦。

首先,由于 B 非虚拟地从 A 派生,D 以两个 A 子对象结束(就好像你没有使用 virtual 继承)。

非虚AB()构造(故"A called from B"),虚"Constructor A"构造时打印

这是因为虚(可能是间接)基的构造函数总是由最派生的 class(D)的构造函数调用,而不是由任何中间基(C).

这意味着 C() 中的 : A("A called from C") 将被忽略(因为您没有构建独立的 C)。由于 D() 没有在其成员初始化列表中提及 A,虚拟 A 是在没有任何参数的情况下构造的(就像 : A())。


此外,值得一提的是,所有虚拟基地都是 constructed before 非虚拟基地。

所以你的代码保证在 A called from B.

之前打印 Constructor A

当一个类型有一个虚基时,虚基是从最派生类型的构造函数构造的。所以 "Constructor A" 是从 D 的构造函数中调用的。

更详细一点:

#include <iostream>

struct base {
    base() { std::cout << "base()\n"; }
    base(int) { std::cout << "base(int)\n"; }
};

struct i1 : virtual base {
    i1() : base(0) { std::cout << "i1()\n"; }
};

struct i2 : virtual base {
    i2() : base(1) { std::cout << "i2()\n"; }
};

struct d : i1, i2 {
};

现在,如果代码创建类型为 i1 的对象,i1 的默认构造函数调用 base(int),如所写。

但是当你创建一个d类型的对象时,d的构造函数负责构造base对象。由于 d 没有默认构造函数,因此编译器会在调用 i1i2.[=30 的默认构造函数之前生成一个调用 base 默认构造函数的构造函数=]

int main() {
    d d_obj;
    return 0;
}

这里的输出是

[temp]$ Clang++ test.cpp
[temp]$ ./a.out
base()
i1()
i2()
[temp]$ 

请注意 i1i2 的构造函数没有 构造 base 子对象。编译器注意了这一点:基应该只初始化一次,d 的构造函数做到了。

如果你想对基础对象进行不同的初始化,请在构造函数中写入:

d::d() : base(2) {}

将其添加到 class d 会产生以下输出:

[temp]$ Clang++ test.cpp
[temp]$ ./a.out
base(int)
i1()
i2()
[temp]$