为什么C++中派生类的构造函数要初始化虚基类?

8

据我理解,例如阅读此处的内容,派生类的构造函数不会调用其虚拟基类的构造函数。

这里是我制作的一个简单示例:

class A {
    protected:
        A(int foo) {}
};

class B: public virtual A {
    protected:
        B() {}
};

class C: public virtual A {
    protected:
        C() {}
};

class D: public B, public C {
    public:
        D(int foo, int bar) :A(foo) {}
};


int main()
{
    return 0;
}

出于某些原因,构造函数B::B()C::C()试图初始化 A(在我的理解中,此时应该已经被D初始化):

$ g++ --version
g++ (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ g++ test.cpp
test.cpp: In constructor ‘B::B()’:
test.cpp:8:13: error: no matching function for call to ‘A::A()’
    8 |         B() {}
      |             ^
test.cpp:3:9: note: candidate: ‘A::A(int)’
    3 |         A(int foo) {}
      |         ^
test.cpp:3:9: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(const A&)’
    1 | class A {
      |       ^
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(A&&)’
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided
test.cpp: In constructor ‘C::C()’:
test.cpp:13:13: error: no matching function for call to ‘A::A()’
   13 |         C() {}
      |             ^
test.cpp:3:9: note: candidate: ‘A::A(int)’
    3 |         A(int foo) {}
      |         ^
test.cpp:3:9: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(const A&)’
    1 | class A {
      |       ^
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(A&&)’
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided

我确定有些非常基础的东西我可能误解了或者做错了,但我无法弄清楚。


2
在派生类构建时,所有的基类都会被构建。虚拟基类是特殊的,因为它们是根据最终派生类的构造函数初始化器列表首先被构造的。如果派生类构造函数没有显式初始化其虚拟基类,则默认情况下使用其默认构造函数进行初始化。这是因为虚拟基类要在非虚拟基类之前初始化 - 这意味着它们要在其他基类的构造函数被调用(或其初始化器列表生效)之前进行初始化。 - Peter
请考虑 "分离编译"。 - curiousguy
3个回答

4
虚基类的构造函数是有条件地被构建的。也就是说,最派生类的构造函数会调用虚基类的构造函数来完成初始化,但前提是:如果派生类不是构造对象的具体类,则不会构造虚基类,因为虚基类已经被具体类构造了。否则,它将构造虚基类。

因此,在所有派生类的构造函数中都必须正确初始化虚基类。你必须知道,特定的初始化在具体类不是你正在编写的类的情况下并不一定会发生。编译器不知道也无法知道你是否会创建那些中间类的实例,因此不能简单地忽略它们破损的构造函数。

如果你将这些中间类定义为抽象类,则编译器会知道它们永远不是最具体的类型,从而不需要它们的构造函数来初始化虚基类。


但是为什么在D中我们必须调用B和C的构造函数,当它们所做的只是初始化A呢? - Rs Fps
1
@RsFps 你为什么认为我们必须调用那些构造函数? - eerorika
万一他们做了其他额外的事情呢? - Rs Fps
1
@RsFps,我不认为我理解你的问题。如果你创建一个有构造函数的类的对象,那么构造函数将被调用。如果你不创建对象,那么构造函数就不会被调用。通常情况下,你不必创建对象,除非你想要这样做。 - eerorika
B和C的构造函数仅初始化A。因此,我们能否跳过B和C的初始化,因为它们什么也没做? - Rs Fps
显示剩余4条评论

3
由于某些原因,构造函数B::B()和C::C()试图初始化A(根据我的理解,在这一点上A应该已经被D初始化):
但是如果有人单独构造C,编译器应该怎么做呢?最终对象D将调用A的构造函数,但你定义了C的构造函数,这意味着它可以构造,但是构造函数是有缺陷的,因为它不能构造A。

我原以为将构造函数设置为protected就足以让编译器理解我想要这个类是抽象的。在这种情况下,除了D之外,没有人可以构造C - scozy
@scozy 抽象类的思想很好,但是...来自抽象类@cppreference.com:"一个抽象类是指至少定义或继承了一个函数,该函数的最终覆盖者是纯虚函数。" 仅此而已。定义结束。编译器不允许将此定义扩展到它认为您希望成为抽象的类。 - JaMiT

0

暂时搁置更复杂的类层次结构,对于任何派生类型而言,其虚拟基类都只有一个副本。规则是最派生类型的构造函数构造该基类。编译器必须生成处理此过程中簿记的代码:

struct B { };
struct I1 : virtual B { };
struct I2 : virtual B { };
struct D : I1, I2 { };

B b;   // `B` constructor initializes `B`
I1 i1; // `I1` constructor initializes `B` subobject
I2 i2; // `I2` constructor initializes `B` subobject

到目前为止,这很容易理解,因为初始化的方式与如果B不是虚基类时相同。

但是然后你做了这个:

D d; // which constructor initializes `B` subobject?

如果基类不是虚拟的,那么I1构造函数将初始化其B主题,而I2构造函数将初始化其B子对象。但由于它是虚拟的,只有一个B对象。那么哪个构造函数应该初始化它呢?语言规定D构造函数负责这个。
接下来是下一个复杂问题:
struct D1 : D { };
D1 d1; // `D1` constructor initializes `B` subobject

所以,沿途我们创建了五个不同的对象,每个对象都有一个类型为B的虚基类,并且每个B子对象都是从不同的构造函数构建而来。

将责任放在最派生的类型上使得初始化易于理解和可视化。可能还有其他规则,但这个规则确实是最简单的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接