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
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.