Skip to content
academia.sh

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.
  • sizeof confirms the existence of two separate copies in memory size; the common ancestor object reached by two different paths carries different addresses.
  • virtual public inheritance 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close