Why do the constructor of the derived classes want to initialize the virtual base class in C++?
Asked Answered
T

3

8

My understanding, for instance reading this, is that the constructor of a derived class does not call its virtual base class' constructor.

Here is a simple example I made:

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;
}

For some reason, the constructors B::B() and C::C() are trying to initialize A (which, again in my understanding, should have already been initialized by D at this point):

$ 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

I'm certain there is something very basic I misunderstood or am doing wrong, but I can't figure what.

Tressa answered 3/11, 2020 at 2:6 Comment(2)
During construction of a derived class, all bases are constructed. Virtual bases are special, in that they are constructed first, based on the initialiser list of the constructor most derived class. If the derived class constructor does not explicitly initialise its virtual base(s) then the virtual base is, by default, initialised using its default constructor. This is the case because the virtual bases are initialised BEFORE non-virtual bases - which means they are initialised before the constructors of other bases are called (or their initialiser lists have effect).Celestyna
Think "separate compilation"Gielgud
L
4

The constructor of virtual base is constructed. It is constructed conditionally. That is, the constructor of the most derived class calls the constructor of the virtual base. If - this is the condition - the derived class with virtual base is not the concrete class of the constructed object, then it will not construct the virtual base because it has already been constructed by the concrete class. But otherwise it will construct the virtual base.

So, you must correctly initialise the virtual base class in constructors of all derived classes. You simply must know that specific initialisation doesn't necessarily happen in case the concrete class is not the one which you are writing. The compiler doesn't and cannot know whether you will ever create direct instances of those intermediate classes, so it cannot simply ignore their broken constructors.

If you made those intermediate classes abstract, then the compiler would know that they are never the most concrete type and thus their constructor would not be required to initialise the virtual base.

Lennielenno answered 3/11, 2020 at 2:15 Comment(9)
But why do we have to call the constructors of B and C in D when all they do is initialize A?Erickericka
@RsFps Why do you think that we have to call those constructors?Lennielenno
In case they do anything extra?Erickericka
I've been thinking about it since I posted and it's really bugging me; can you please tell me the reason? Thanks!Erickericka
@RsFps I don't think I understand your question. If you create an object of a class that has a constructor, then the constructor will be called. If you don't create an object, then a constructor isn't called. You don't generally have to create objects unless you want to.Lennielenno
the constructors of B and C do nothing but initialize A. For this reason can't we skip over the initialization of B and C, because they do nothing?Erickericka
@RsFps You cannot "skip over" initialisation in C++. Unless you count trivial types for which default initialisation does nothing.Lennielenno
I'm sorry I'm not getting this; can you please elaborate? Cheers!Erickericka
@RsFps To elaborate a bit: There is no concept of "skipping over" of constructors.Lennielenno
W
3

For some reason, the constructors B::B() and C::C() are trying to initialize A (which, again in my understanding, should have already been initialized by D at this point):

But what should compiler do if somebody constructs C solo? The final object D will call the constructor of A but you define constructor to C which implies that it can be constructed but the constructor is faulty cause it cannot construct A.

Wellnigh answered 3/11, 2020 at 2:19 Comment(2)
I thought having the constructor protected was sufficient to make the compiler understand I want the class to be abstract. Certainly in this case no one except D can construct C.Tressa
@Tressa The abstract class thought is good, however... From abstract class @ cppreference.com: "An abstract class is a class that either defines or inherits at least one function for which the final overrider is pure virtual." Period. End of definition. The compiler is not allowed to extend this definition to classes that it might think you want to be abstract.Extort
I
0

Putting aside more complex class hierarchies, for any derived type there is exactly one copy of its virtual base. The rule is that the constructor for the most-derived type constructs that base. The compiler has to generate code to handle the bookkeeping for that:

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

So far, it's easy enough to picture, since the initialization is done the same way as it would be if B was not a virtual base.

But then you do this:

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

If the base wasn't virtual, the I1 constructor would initialize its B subject, and the I2 constructor would initialize its B subobject. But because it's virtual, there's only one B object. So which constructor should initialize it? The language says that the D constructor is responsible for that.

And the next complication:

struct D1 : D { };
D1 d1; // `D1` constructor initializes `B` subobject

So, along the way we've created five different objects, each with a virtual base of type B, and each with the B subobject being constructed from a different constructor.

Putting the responsibility on the most-derived type makes the initialization easy to understand and to visualize. There could have been other rules, but this one really is the simplest.

Irena answered 3/11, 2020 at 15:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.