3

In my code, I have a basic diamond pattern:

     CommonBase
        /  \
       /    \
 DerivedA  DerivedB
       \    /
        \  /
       Joined

It's implemented like this, with the common base class having a default constructor and a constructor taking a parameter:

struct CommonBase {
    CommonBase() : CommonBase(0) {}

    CommonBase(int val) : value(val) {}

    const int value;
};

struct DerivedA : public virtual CommonBase {
    void printValue() {
        std::cout << "The value is " << value << "\n";
    }
};

struct DerivedB : public virtual CommonBase {
    void printValueTimes2() {
        std::cout << "value * 2 is " << value * 2 << "\n";
    }
};

struct Joined : public DerivedA,
                public DerivedB {
    Joined(int val) : CommonBase(val) {
        std::cout << "Constructor value is " << val << "\n";
        std::cout << "Actual value is " << value << "\n";
    }
};

The Joined class initializes the virtual base using the constructor that takes a parameter, and everything works as expected.

However, when I derive a class from the Joined class, something strange happens - the CommonBase's default constructor is called unless I explicitly initialize CommonBase in the derived classes constructor as well.

This is demonstrated using this code:

struct JoinedDerivedA : public Joined {
    JoinedDerivedA() : Joined(99) {
        printValue();
    }
};

struct JoinedDerivedB : public Joined {
    JoinedDerivedB() : Joined(99), CommonBase(99) {
        printValue();
    }
};

int main() {
    std::cout << "======= Joined =======\n";
    Joined j(99);
    j.printValue();
    std::cout << "\n=== JoinedDerivedA ===\n";
    JoinedDerivedA a;
    std::cout << "\n=== JoinedDerivedB ===\n";
    JoinedDerivedB b;

    return 0;
}

The output of this code is

======= Joined =======
Constructor value is 99
Actual value is 99
The value is 99

=== JoinedDerivedA ===
Constructor value is 99
Actual value is 0 // <-- unexpected behaviour
The value is 0

=== JoinedDerivedB ===
Constructor value is 99
Actual value is 99
The value is 99

Why is this the case? Is it possible to not have to explicitly initialize the common base class in derived classes again?

Here's the code on ideone, so you can run it yourself: https://ideone.com/Ie94kb

CrushedPixel
  • 1,152
  • 2
  • 13
  • 26
  • 1
    Possible duplicate of [Understanding virtual base classes and constructor calls](https://stackoverflow.com/questions/6461784/understanding-virtual-base-classes-and-constructor-calls) – Michael Kenzel Dec 17 '18 at 11:51
  • 2
    When you have virtual inheritance, there is only one shared base class subobject for everyone that derives virtually from that base class. And that subobject lives in the most derived class. Where else should it live? There's not really any other place you can always put it in a well-defined manner. You may want to have a look at https://stackoverflow.com/questions/6461784/understanding-virtual-base-classes-and-constructor-calls – Michael Kenzel Dec 17 '18 at 11:54
  • @MichaelKenzel I understand that there's only one instance of the shared base class, however I've expected it to live in the `Joined` class, since this class explicitly calls the base class constructor, similar to non-virtual inheritance. – CrushedPixel Dec 17 '18 at 11:57
  • And what if there's another base class that also calls the constructor of the virtual base class in its constructor? – Michael Kenzel Dec 17 '18 at 12:06
  • I see. So there's no way to nicely do this? – CrushedPixel Dec 17 '18 at 12:20
  • In any case, I'd be thankful if you could post this as an answer, so I can accept it. – CrushedPixel Dec 17 '18 at 12:20
  • 1
    This is a very good question and it would be nice if someone could explain **why** is it made this way in C++ instead of just stating that the most derived class must initialize the virtual base. – r3mus n0x Dec 17 '18 at 12:25

1 Answers1

1

This is specified in Initializing bases and members [class.base.init] (12.6.2 in draft n4567). We can read in §13 (emphasize mine):

(13) In a non-delegating constructor, initialization proceeds in the following order:

(13.1) — First, and only for the constructor of the most derived class (1.8), virtual base classes are initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base classes in the derived class base-specifier-list.

(13.2) — Then, direct base classes are initialized in declaration order as they appear in the base-specifier-list (regardless of the order of the mem-initializers).

(13.3) — Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).

(13.4) — Finally, the compound-statement of the constructor body is executed.

[ Note: The declaration order is mandated to ensure that base and member subobjects are destroyed in the reverse order of initialization. —end note ]

That means that the virtual base class will be initialized before the initialization of Joined. So in DerivedJoinedA, it is default initialized with a value of 0. Then when initializing Joined, the initialization of CommonBase is ignored because it has already been initialized and value keeps its 0 value.

That is the reason why you have to initialize the virtual base classes in the most derived class.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252