Multiple Inheritance and Virtual Base Classes

A guide to multiple inheritance in C++, including its common problems and how to solve them
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In the introductory course, we covered inheritance, which allows us to declare a class as a child of another class. In the following example, Human inherits from Character:

class Character {};
class Human : public Character{};

C++ supports multiple inheritance. This allows our classes to have multiple base classes. We separate base classes using a comma:

class Human {};

class Elf {};

class HalfElf : public Human, public Elf {};

With this setup, any HalfElf object will inherit the functions and variables of both the Human and Elf class.

Multiple inheritance sounds like a powerful concept, but it has some problems. Two of the most immediate ones are naming conflicts and the diamond problem, which we’ll cover next.

Naming Conflicts

The first problem that can arise with multiple inheritance is that two (or more) of the base classes can have functions or variables with the same name. Below, we demonstrate this with a variable called Agility:

#include <iostream>

class Human {
public:
  int Agility{8};
};

class Elf {
public:
  int Agility{10};
};

class HalfElf : public Human, public Elf {};

int main(){
  HalfElf Elrond;
  std::cout << "Elrond Agility: "
    << Elrond.Agility;
}

In this scenario, does Elrond have the agility of a Human, or of an Elf?

It turns out, he has both, and the compiler throws an error because we haven’t specified which value we want

error C2385: ambiguous access of 'Agility'
could be the 'Agility' in base 'Human'
or could be the 'Agility' in base 'Elf'

There are two common ways we specify which variable or function we want to use:

  • Through the scope resolution operator, ::
  • through a call to static_cast

The following example demonstrates both:

#include <iostream>

class Human {/*...*/}
class Elf {/*...*/} class HalfElf : public Human, public Elf {}; int main(){ HalfElf Elrond; std::cout << "Elrond Agility (Human): " << Elrond.Human::Agility; std::cout << "\nElrond Agility (Elven): " << static_cast<Elf&>(Elrond).Agility; }
Elrond Agility (Human): 8
Elrond Agility (Elven): 10

The Diamond Problem

The second issue that can occur with multiple inheritance is the so-called diamond problem.

This happens when a class inherits the same base class multiple times, through different paths. It’s called the diamond problem, because the simplest variation of it involves a class inheriting from two classes, and each of those classes inheriting from the same base class.

This creates a class hierarchy that looks like a diamond:

Diagram illustrating the diamond problem

In code, it looks like this:

class Character {};

class Human : public Character {};

class Elf : public Character {};

class HalfElf : public Human, public Elf {};

Let's consider the previous Agility problem, now converted to a diamond hierarchy:

#include <iostream>

class Character {
public:
  Character(int Agility) : Agility{Agility}{}
  int Agility;
};

class Human : public Character {
public:
  Human() : Character(8){}
};

class Elf : public Character {
public:
  Elf() : Character(10){}
};

class HalfElf : public Human, public Elf {};

int main(){
  HalfElf Elrond;
  std::cout << "Elrond Agility: "
    << Elrond.Agility;
}

We now have the same problem - the attempt to retrieve Agility needs to be clarified. This time, the error message isn’t so helpful:

ambiguous access of 'Agility'
could be the 'Agility' in base 'Character'
or could be the 'Agility' in base 'Character'

The problem is, we are now inheriting from Character twice. The term "diamond" is somewhat misleading because our class hierarchy looks more like this:

Diagram illustrating how the diamond problem is implemented

We can still specify which Agility we want, by specifying the intermediate class, in the same way as before:

#include <iostream>

class Character {/*...*/}
Elrond Agility (Human): 8
Elrond Agility (Elven): 10

But, we rarely want to be inheriting the same class twice, so usually we want to do something else instead. Typically, we want virtual inheritance.

Virtual Base Classes

When implementing inheritance, we can specify that we want to use a virtual base class:

class Character {
public:
  Character(int Agility) : Agility{Agility}{}
  int Agility;
};

class Human : virtual public Character {
public:
  Human() : Character(8){}
};

class Elf : virtual public Character {
public:
  Elf() : Character(10){}
};

When we instantiate a class that uses virtual inheritance directly, everything works as normal:

#include <iostream>

class Character {/*...*/}
class Human : virtual public Character {/*...*/}
class Elf : virtual public Character {/*...*/} int main(){ Human Aragorn; std::cout << "Aragorn Agility: " << Aragorn.Agility; Elf Legolas; std::cout << "\nLegolas Agility: " << Legolas.Agility; }
Aragorn Agility: 8
Legolas Agility: 10

However, things work differently when we add a more derived class to this tree, such as HalfElf:

class Character {};

class Human : virtual public Character {};

class Elf : virtual public Character {};

class HalfElf : public Human, public Elf {};

Now, when we use the HalfElf class, we can imagine the created object searches upwards, through the inheritance tree, to find any virtual base classes.

Any virtual base classes it finds get moved to become a direct descendant. If the virtual class was already a direct descendant (either explicitly, or moved there from some other part of the inheritance tree), it gets removed entirely.

This has the effect of flatting and deduplicating virtual base classes within our inheritance tree for those objects:

Diagram showing the effect of virtual base classes

This reorganization happens on-demand, at run time. As a result, similar to virtual methods, virtual inheritance incurs a performance cost. Therefore, we should not use it by default - we should only use it when needed.

Below, we implement the previous diagram in code:

class Character {
public:
  Character() : Agility{5}{}
  Character(int Agility) : Agility{Agility}{}
  int Agility;
};

class Human : virtual public Character {
public:
  Human() : Character(8){}
};

class Elf : virtual public Character {
public:
  Elf() : Character(10){}
};

class HalfElf : public Elf, public Human {};

Implications of Virtual Inheritance

Now that we’re using virtual inheritance, two major things have changed from our earlier examples.

Firstly, we no longer have duplicate variables and methods in the object we created. We now only have one copy of the base class, and can access variables and methods of that class without needing to specify which intermediate class they came from:

#include <iostream>

class Character {/*...*/}
class Human : virtual public Character {/*...*/}
class Elf : virtual public Character {/*...*/} class HalfElf : public Elf, public Human {}; int main(){ HalfElf Elrond; std::cout << "Elrond Agility: " << Elrond.Agility; }

Secondly, the classes that used virtual inheritance (Human and Elf) are no longer constructing the base Character object. In this scenario, Character is now a direct ancestor of HalfElf, so it falls to HalfElf to initialize it.

In this example, we didn’t do any explicit initialization, so Character was created using the default constructor. The default Character constructor sets Agility to 5, which we can see from the output:

Elrond Agility: 5

When an intermediate class is virtual, the derived class can initialize its base class directly. If the base class doesn’t have a default constructor, the derived class must initialize it.

Below, we’ve removed the default constructor from Character, and called the Character(int) constructor from the HalfElf class:

#include <iostream>

class Character {
public:
  Character(int Agility) : Agility{Agility}{}
  int Agility;
};

class Human : virtual public Character {/*...*/}
class Elf : virtual public Character {/*...*/} class HalfElf : public Elf { public: HalfElf() : Character(9){} }; int main(){ Human Aragorn; std::cout << "Aragorn Agility: " << Aragorn.Agility; HalfElf Elrond; std::cout << "\nElrond Agility: " << Elrond.Agility; Elf Legolas; std::cout << "\nLegolas Agility: " << Legolas.Agility; }
Aragorn Agility: 8
Elrond Agility: 9
Legolas Agility: 10

Should I use Multiple Inheritance?

Even with virtual base classes, multiple inheritance is still quite messy and can lead to excessively complex class hierarchies. As such, it’s generally something that should be used with caution.

Multiple inheritance is never necessary. There is always an alternative solution to implement any design. This is evidenced by the fact that many object-oriented programming languages don’t support it, yet those languages are still used to create complex software.

If we are going to use multiple inheritance, it can be worth self-imposing some restrictions on how it is used. These restrictions can be designed to get most of the benefit while bypassing most of the problems.

The Google style guide, and many others, recommend the following:

Multiple inheritance is permitted, but multiple implementation inheritance is strongly discouraged.

Using "non-implementation inheritance" refers to design patterns like interfaces and abstract classes.

In C++, these design patterns are created using pure virtual functions, which we’ll introduce in the next lesson.

Summary

In this lesson, we explored multiple inheritance, including the challenges of naming conflicts, the diamond problem, and how virtual base classes can resolve these issues.

Key Takeaways

  • C++ supports multiple inheritance, allowing a class to have more than one base class.
  • Naming conflicts can occur with multiple inheritance, requiring explicit specification of which base class member to use.
  • The diamond problem arises when a class inherits from two classes that both inherit from the same base class.
  • Virtual inheritance is used to solve the diamond problem by ensuring a class inherits only one instance of a base class, even through multiple inheritance paths.
  • Virtual base classes introduce a performance cost due to dynamic resolution.
  • Multiple inheritance should be used with caution due to its complexity and potential for creating overly complex class hierarchies.

Was this lesson useful?

Next Lesson

Pure Virtual Functions

Learn how to create interfaces and abstract classes using pure virtual functions
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Multiple Inheritance and Virtual Base Classes

A guide to multiple inheritance in C++, including its common problems and how to solve them

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Pure Virtual Functions

Learn how to create interfaces and abstract classes using pure virtual functions
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved