Lesson 11 / 14
Multiple Inheritance and the Diamond Problem
The measurement of how the second base subobject in multiple inheritance does not share the same address as the derived object, and how a common ancestor in diamond-shaped inheritance doubles without virtual inheritance.
Published Updated
In this lesson
- Measures that in multiple inheritance only the first base subobject shares the same address as the derived object
- Shows that a common ancestor in diamond-shaped inheritance splits into two separate copies without virtual inheritance
- Verifies that virtual inheritance reduces the common ancestor to a single, shared base subobject
Contents
The previous two lessons measured single inheritance: a derived object had exactly one base subobject, and that subobject’s address always coincided with the derived object’s address. This lesson covers the case where a class derives from more than one base class at once. Does the first base subobject still stand at the head of the derived object? Does the second one also share the same address? And if the two base classes have a common ancestor, how many times does that ancestor take up space inside the derived object?
In Multiple Inheritance the Second Base Subobject’s Address Differs
Triathlete derives from two unrelated classes: Swimmer and Runner. Does the
first base subobject stand at the head of the derived object, does the second one
also share the same address?
// multiple_inheritance_offset.cpp -- in multiple inheritance the first base is at the same address, the second is not
#include <iostream>
class Swimmer {
public:
int style_ = 1;
};
class Runner {
public:
int distance_ = 5;
};
class Triathlete : public Swimmer, public Runner {
public:
int total_score_ = 0;
};
int main() {
Triathlete t;
std::cout << std::boolalpha;
std::cout << "t and the first base Swimmer address the same "
<< (static_cast<void*>(&t) == static_cast<void*>(static_cast<Swimmer*>(&t))) << "\n";
std::cout << "t and the second base Runner address the same "
<< (static_cast<void*>(&t) == static_cast<void*>(static_cast<Runner*>(&t))) << "\n";
return 0;
}
t and the first base Swimmer address the same true t and the second base Runner address the same false
PS10. Triathlete declares Swimmer first, Runner second in its inheritance
list — the compiler places the base subobjects in this order. The first base
subobject (Swimmer) stands at the head of t, so &t and
static_cast<Swimmer*>(&t) carry the same address, true. The second base
subobject (Runner) stands at an offset following the Swimmer subobject; &t
and static_cast<Runner*>(&t) are different addresses, false. In single
inheritance (the Inheritance lesson) the base subobject’s address always coincided
with the derived object’s, because there was only one base to carry; in multiple
inheritance this holds only for the first base. The class is part: both
base subobjects are a part of t, but only the first shares the same address.
The Diamond Problem: Two Separate Copies from a Common Ancestor
Let Swimmer and Runner have a common ancestor, Living. When Triathlete
derives from both, does reaching the Living subobject through Swimmer and
through Runner land on the same object, or two separate copies?
// diamond_separate.cpp -- without virtual inheritance the common ancestor splits into two separate copies
#include <iostream>
class Living {
public:
int health_ = 100;
};
class Swimmer : public Living {};
class Runner : public Living {};
class Triathlete : public Swimmer, public Runner {};
int main() {
Triathlete t;
Living* via_swimmer = static_cast<Living*>(static_cast<Swimmer*>(&t));
Living* via_runner = static_cast<Living*>(static_cast<Runner*>(&t));
std::cout << std::boolalpha;
std::cout << "Living reached via Swimmer and via Runner the same "
<< (static_cast<void*>(via_swimmer) == static_cast<void*>(via_runner)) << "\n";
std::cout << "Triathlete's size large enough to hold two separate Living "
<< (sizeof(Triathlete) >= 2 * sizeof(Living)) << "\n";
return 0;
}
Living reached via Swimmer and via Runner the same false Triathlete's size large enough to hold two separate Living true
PS11. Swimmer and Runner derive from Living through ordinary
(non-virtual) inheritance; each carries its own Living subobject. When
Triathlete derives from both, these two Living copies both enter Triathlete
— they do not merge. via_swimmer and via_runner carry different addresses,
false: these are two separate Living objects. sizeof(Triathlete) covering
at least two Living sizes confirms this in memory layout, true. This is the
diamond problem: writing t.health_ would stop the compiler because it is
ambiguous which copy is meant, it only compiles with an explicit path like
t.Swimmer::health_ or t.Runner::health_ — this lesson never runs that ambiguous
line, it only shows the existence of the two copies through address and size
measurement. The class is part: Triathlete carries not one but two separate
parts of Living.
Virtual Inheritance: A Single Shared Base Subobject
If Swimmer and Runner derive from Living through virtual inheritance,
when Triathlete derives from both, is the Living subobject still split in two,
or reduced to a single shared object?
// diamond_virtual.cpp -- virtual inheritance reduces the common ancestor to a single shared subobject
#include <iostream>
class Living {
public:
int health_ = 100;
};
class Swimmer : virtual public Living {};
class Runner : virtual public Living {};
class Triathlete : public Swimmer, public Runner {};
int main() {
Triathlete t;
Living* via_swimmer = static_cast<Living*>(static_cast<Swimmer*>(&t));
Living* via_runner = static_cast<Living*>(static_cast<Runner*>(&t));
std::cout << std::boolalpha;
std::cout << "Living reached via Swimmer and via Runner the same "
<< (static_cast<void*>(via_swimmer) == static_cast<void*>(via_runner)) << "\n";
std::cout << "t.health_ and via_swimmer->health_ carry the same address "
<< (static_cast<void*>(&t.health_) == static_cast<void*>(&via_swimmer->health_)) << "\n";
return 0;
}
Living reached via Swimmer and via Runner the same true t.health_ and via_swimmer->health_ carry the same address true
PS12. The virtual public Living declaration tells Swimmer and Runner,
“do not construct your own Living copy, use the single copy shared by the
topmost derived class.” When Triathlete derives from both, there is now only one
Living subobject — via_swimmer and via_runner carry the same address,
true. Thanks to this sharing, t.health_ is no longer ambiguous either, it
compiles directly and points to the same shared object, true. The diamond
problem from the previous measurement — two separate Living copies — is gone
here, because both paths to Living converge on the same single object. The class
is part: Triathlete now carries not two but a single part of Living, and
this part is reached through both Swimmer and Runner.
Reaching Two Separate Copies by Qualified Name and Changing Only One
PS11 said that writing t.health_ does not compile because it is ambiguous which
copy is meant, and that it only compiles with a qualified name like
t.Swimmer::health_ or t.Runner::health_, but never ran that line. When only one
copy is changed with a qualified name, does the other copy really remain
unaffected?
// multiple_inheritance_qualified_access.cpp -- in non-virtual inheritance two separate Living copies are reached by qualified name and one is changed
#include <iostream>
class Living {
public:
int health_ = 100;
};
class Swimmer : public Living {};
class Runner : public Living {};
class Triathlete : public Swimmer, public Runner {};
int main() {
Triathlete t;
t.Swimmer::health_ = 70;
std::cout << "t.Swimmer::health_ after change its value " << t.Swimmer::health_ << "\n";
std::cout << "t.Runner::health_ value remaining unchanged " << t.Runner::health_ << "\n";
std::cout << std::boolalpha
<< "t.Swimmer::health_ and t.Runner::health_ carry the same address "
<< (static_cast<void*>(&t.Swimmer::health_) == static_cast<void*>(&t.Runner::health_)) << "\n";
return 0;
}
t.Swimmer::health_ after change its value 70 t.Runner::health_ value remaining unchanged 100 t.Swimmer::health_ and t.Runner::health_ carry the same address false
PS32. t.Swimmer::health_ = 70; uses the qualified name to explicitly tell the
compiler which copy is meant, and changes only that copy. t.Runner::health_
queried afterward is still its first value, 100 — the change made through
Swimmer never leaks into Runner’s own Living copy. The address comparison
confirms this directly: false, the two qualified accesses really reach two
separate objects. This directly proves what PS11 showed indirectly through memory
size: inside Triathlete, Living exists not once but twice, and qualified access
gives the programmer the ability to choose which of the two copies to work with —
manually making a decision the compiler cannot make on its own. The class is
part: t.Swimmer::health_ and t.Runner::health_ are two different parts of
t, capable of changing independently.
The Most Derived Class Calls the Virtual Base’s Constructor
Living now has a non-default constructor that takes a parameter. Swimmer and
Runner each try to construct Living with their own value in their own
constructors, but Triathlete also constructs Living with a separate value in
its own constructor. In virtual inheritance, which constructor’s value does the
shared health_ carry?
// diamond_virtual_constructor_chain.cpp -- the most derived class directly calls the virtual base's constructor, intermediate ones are bypassed
#include <iostream>
class Living {
public:
explicit Living(int health) : health_(health) {
std::cout << "Living constructor health=" << health_ << "\n";
}
int health_;
};
class Swimmer : virtual public Living {
public:
Swimmer() : Living(1) {
std::cout << "Swimmer constructor (its own Living(1) call will be bypassed)\n";
}
};
class Runner : virtual public Living {
public:
Runner() : Living(2) {
std::cout << "Runner constructor (its own Living(2) call will be bypassed)\n";
}
};
class Triathlete : public Swimmer, public Runner {
public:
Triathlete() : Living(100), Swimmer(), Runner() {
std::cout << "Triathlete constructor, shared health_ = " << health_ << "\n";
}
};
int main() {
Triathlete t;
return 0;
}
Living constructor health=100 Swimmer constructor (its own Living(1) call will be bypassed) Runner constructor (its own Living(2) call will be bypassed) Triathlete constructor, shared health_ = 100
PS33. Living constructor health=100 prints only once, health=1 or
health=2 never appears — the responsibility for calling the virtual base’s
constructor falls not on the intermediate classes, but on the most derived
class. The Living(1) and Living(2) calls in Swimmer‘s and Runner‘s own
mem-initializer lists are ignored when constructed through Triathlete; these
lines would only take effect if Swimmer or Runner were constructed on its
own (without Triathlete). Triathlete‘s own mem-initializer list’s Living(100)
is the only call that really runs — this is why the shared health_ is 100.
Virtual bases, independent of their position in the inheritance chain, are always
initialized by the constructor of the topmost derived class; this is the
constructor-side counterpart of the “single shared object” principle measured as
PS12 — since the shared object is single, the call that constructs it also has to
be single. The class is part: health_ is a single shared part of t, and
only the most derived class’s constructor determines this part’s value.
The Structural Cost of Virtual Inheritance: Fewer Copies, an Added Pointer
PS11 showed that Living took up space twice in the non-virtual diamond, PS12
showed it reduced to a single copy in the virtual diamond. Does the total object
size shrink by the same proportion as the copy count drops, or does virtual
inheritance’s own overhead eat up part of this gain?
// diamond_virtual_structural_cost.cpp -- comparing virtual inheritance's effect on object size against the same class set without it
#include <iostream>
class Living {
public:
int health_ = 1;
};
class SwimmerPlain : public Living {};
class RunnerPlain : public Living {};
class TriathletePlain : public SwimmerPlain, public RunnerPlain {};
class SwimmerVirtual : virtual public Living {};
class RunnerVirtual : virtual public Living {};
class TriathleteVirtual : public SwimmerVirtual, public RunnerVirtual {};
int main() {
std::cout << std::boolalpha
<< "is the virtual diamond's size smaller than the non-virtual diamond's: "
<< (sizeof(TriathleteVirtual) < sizeof(TriathletePlain)) << "\n";
std::cout << "is the virtual diamond's size larger than a single Living (is there overhead): "
<< (sizeof(TriathleteVirtual) > sizeof(Living)) << "\n";
std::cout << "is the non-virtual diamond's size at least two Living: "
<< (sizeof(TriathletePlain) >= 2 * sizeof(Living)) << "\n";
return 0;
}
is the virtual diamond's size smaller than the non-virtual diamond's: false is the virtual diamond's size larger than a single Living (is there overhead): true is the non-virtual diamond's size at least two Living: true
PS34. The expectation was for the virtual diamond to come out smaller since the
copy count dropped, but the measurement does not confirm this: false. Because
Living has only a single int member, the cost of a copy is already small (a
few bytes); against this, Swimmer and Runner each carry a separate record
(a virtual base pointer or a similar mechanism, varies by compiler) to resolve how
to reach their own Living — two bases, two separate records. On this compiler,
the total cost of the added records outweighed the single Living copy that was
saved. The virtual diamond being larger than a single Living (true) was
already expected; what is instructive is that “fewer copies” and “a smaller object”
are not the same thing. This is the multiple-inheritance counterpart of the finding
measured as PS26 for virtual functions, “a vptr is added to every object”: keeping
a shared thing (a method table / a base subobject) in one place loads a small but
non-zero bookkeeping cost onto the object — and this cost can even be larger than
the size of the copy that was eliminated. The class is part: the measured size
difference comes not from t’s virtual base subobject itself, but from the added
parts that carry the path leading to that subobject.
The Bounding Measurement
The virtual inheritance measured in this lesson showed the sharing of data
(health_) and the rule that only the most derived class calls the virtual base
class’s constructor; but the measurement did not cover the extra indirection cost
virtual inheritance carries (every access being resolved through a virtual base
pointer) — it only measured address equality, not how many steps the resolution
takes. The case where Triathlete has its own virtual functions, and Swimmer and
Runner each define different functions with the same signature — a naming
collision and ambiguous redefinition — is outside this lesson’s scope. Qualified
access (t.Swimmer::health_) was shown only under non-virtual inheritance; how
qualified access to the single shared copy behaves under virtual inheritance —
which would be indistinguishable from ordinary access, since the copy is already
single — was not additionally tested by this lesson.
Summary
- In multiple inheritance only the first base class’s subobject shares the same address as the derived object; subsequent base subobjects stand at a different offset.
- If two base classes have a common ancestor and the inheritance is not virtual, that ancestor takes up space inside the derived object as two separate copies — the diamond problem.
sizeofconfirms the existence of two separate copies in memory size; the common ancestor object reached by two different paths carries different addresses.virtual publicinheritance reduces the common ancestor to a single subobject shared by the topmost derived class; the ancestor reached by two paths now carries the same address.- The expression measured in this lesson falls in the part class: the base subobject is a part of the derived object, but in multiple and diamond-shaped inheritance, that part’s address and count change.
Next Step
What this lesson measured were always constructor and destructor calls that
completed in the normal flow; no constructor threw an error. The next lesson,
Exceptions, measures the case where a constructor is interrupted midway — throws
an error: how does an exception thrown before construction completes affect the
destruction of that object’s already-constructed parts, and does an exception
object produce a copy as it is thrown with throw and caught with catch.
What is measured is again which object an expression works with, but this time the
question is the state of an object cut off in the middle of construction.
To keep your progress and take notes, Log in
My notes
Log in to take notes.