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.
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:
::
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 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:
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:
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.
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:
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 {};
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
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.
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.
A guide to multiple inheritance in C++, including its common problems and how to solve them
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.