The C++ language supports the concept of multiple inheritance. This means one class can have multiple base classes. This feature is not available in other languages, such as C# or Java. The designers of these languages didn’t considered the benefits of supporting this feature to worth the effort. And probably one of the reasons is that multiple inheritance may lead to the so-called diamond inheritance problem, when one class derives from two different classes, that in turn derive from the same class. In this article, I will discuss the problem as well as the solution in C++.
The problem
To understand the problem, let’s start with the following class hierarchy:
This is a simple hierarchy with the following classes:
control
is the base class of all visual elements and has some data members such asid
,width
, andheight
image
andbutton
are classes derived from control, each with its own additional data membersimage_button
that is both an image and a button and inherits from these two classes, also with its own additional data members
This can be represented in code as follows:
struct control { int id; int width; int height; }; struct image : public control { int stretch_style; int stretch_direction; }; struct button : public control { int text_alignment; }; struct image_button : public image, public button { int content_alignment; };
The image above shows the inheritance hierarchy, but the object memory layout is different. This actually looks like this:
What we can see from here is that:
image
contains everything thatcontrol
has plus its own data membersbutton
contains everything thatcontrol
has plus its own data membersimage_button
contains everything thatimage
andbutton
has plus its own data members; however, this implies it has two copies of the data members ofcontrol
.
As a result, trying to access any of the data members from control
using an image_button
object results in a compiler error.
image i; i.id = 1; // OK button b; b.id = 2; // OK image_button ib; ib.id = 3; // error
error C2385: ambiguous access of 'id' message : could be the 'id' in base 'control' message : could be the 'id' in base 'control'
In this example, we only had data members but the same problem occurs with member functions.
A slightly modified version of the class hierarchy with a virtual function draw()
overridden in each class, and a member function resize()
in the control
base class.
Memory layout now contains additional a pointer to a virtual table.
struct control { int id; int width; int height; void resize(int const w, int const h, bool const redraw = true) { width = w; height = h; if(redraw) draw(); } virtual void draw() { std::cout << "control::draw\n"; } }; struct image : public control { int stretch_style; int stretch_direction; virtual void draw() override { control::draw(); std::cout << "image::draw\n"; } }; struct button : public control { int text_alignment; virtual void draw() override { control::draw(); std::cout << "button::draw\n"; } }; struct image_button : public image, public button { int content_alignment; virtual void draw() override { button::draw(); image::draw(); std::cout << "image_button::draw\n"; } }; int main() { image i; i.id = 1; // OK i.resize(32, 32); // OK button b; b.id = 2; // OK b.resize(100, 20); // OK image_button ib; ib.id = 3; // error ib.resize(100, 20); // error }
The solution
Here is where virtual inheritance comes to rescue. By declaring a base class as virtual
you are ensuring that the memory layout does not duplicate the base class members.
struct control { int id; int width; int height; }; struct image : virtual public control { int stretch_style; int stretch_direction; }; struct button : virtual public control { int text_alignment; }; struct image_button : public image, public button { int content_alignment; };
Note: the virtual
keyword can be use either before or after the access specifier. Therefore virtual public control
and public virtual control
are equivalent.
The memory layout of the image_button class looks as follows:
From this representation, we can see that:
- there is no duplication of the data members from the
control
base class - the data members from the
control
class are present at the end of the layout - there is a pointer to a virtual base table for both the
image
andbutton
classes
With virtual functions added to these classes, the memory layout will also contain a pointer to the virtual function table in the control
base class.
struct control { int id; int width; int height; void resize(int const w, int const h, bool const redraw = true) { width = w; height = h; if(redraw) draw(); } virtual void draw() { std::cout << "control::draw\n"; } }; struct image : virtual public control { int stretch_style; int stretch_direction; virtual void draw() override { control::draw(); std::cout << "image::draw\n"; } }; struct button : virtual public control { int text_alignment; virtual void draw() override { control::draw(); std::cout << "button::draw\n"; } }; struct image_button : public image, public button { int content_alignment; virtual void draw() override { button::draw(); image::draw(); std::cout << "image_button::draw\n"; } };
However, now we can write the following snippet without getting any more errors:
int main() { image i; i.id = 1; // OK i.resize(32, 32); // OK button b; b.id = 2; // OK b.resize(100, 20); // OK image_button ib; ib.id = 3; // OK ib.resize(100, 20); // OK }
Construction and destruction of objects
When we have a virtual hierarchy, constructors and destructors are invoked as follows:
- virtual base classes are constructed before non-virtual base classes; therefore, their constructors are called first in the order they appear in a depth-first, left-to-right traversal of the graph of base classes
- constructors for the rest of the classes are then called, from base class to derived class
- destructors are called in the opposite order of construction
Let’s look at the following example:
struct control { int id; int width; int height; control(int const i) :id(i) { std::cout << "control ctor\n"; } virtual ~control() { std::cout << "control dtor\n"; } void resize(int const w, int const h, bool const redraw = true) { width = w; height = h; if(redraw) draw(); } virtual void draw() { std::cout << "control::draw\n"; } }; struct image : virtual public control { int stretch_style; int stretch_direction; image(int const i) :control(i) { std::cout << "image ctor\n"; } virtual ~image() { std::cout << "image dtor\n"; } virtual void draw() override { control::draw(); std::cout << "image::draw\n"; } }; struct button : virtual public control { int text_alignment; button(int const i) :control(i) { std::cout << "button ctor\n"; } virtual ~button() { std::cout << "button dtor\n"; } virtual void draw() override { control::draw(); std::cout << "button::draw\n"; } }; struct image_button : public image, public button { int content_alignment; image_button(int const i) : image(i), button(i), control(i) { std::cout << "image_button ctor\n"; } ~image_button() { std::cout << "image_button dtor\n"; } virtual void draw() override { button::draw(); image::draw(); std::cout << "image_button::draw\n"; } }; int main() { image_button ib{ 3 }; ib.resize(100, 20); }
The output of this program is as follows:
control ctor image ctor button ctor image_button ctor control::draw button::draw control::draw image::draw image_button::draw image_button dtor button dtor image dtor control dtor
A class may have both virtual and non-virtual base classes. We can change the previous example in order to demonstrate what happens in this case. Let’s consider the following modified class hierarchy:
The new hierarchy differs from the previous on as follows:
- the
image
class has two base classes: non-virtual baseflippable
and virtual basecontrol
- the
button
class has two base classes also, both virtual:control
andclickable
- the
image_button
class has three base classes: non-virtual basesimage
andbutton
, and virtual base classclickable
The modified implementation of these classes is shown below:
struct control { int id; int width; int height; control(int const i) :id(i) { std::cout << "control ctor\n"; } virtual ~control() { std::cout << "control dtor\n"; } void resize(int const w, int const h, bool const redraw = true) { width = w; height = h; if(redraw) draw(); } virtual void draw() { std::cout << "control::draw\n"; } }; struct flippable { int axis; flippable() { std::cout << "flippable ctor\n"; } virtual ~flippable() { std::cout << "flippable dtor\n"; } }; struct image : public flippable, virtual public control { int stretch_style; int stretch_direction; image(int const i) :control(i) { std::cout << "image ctor\n"; } virtual ~image() { std::cout << "image dtor\n"; } virtual void draw() override { control::draw(); std::cout << "image::draw\n"; } }; struct clickable { using fn_clicked = void(*)(); fn_clicked callback = nullptr; clickable() { std::cout << "clickable ctor\n"; } virtual ~clickable() { std::cout << "clickable dtor\n"; } }; struct button : virtual public clickable, virtual public control { int text_alignment; button(int const i) :control(i) { std::cout << "button ctor\n"; } virtual ~button() { std::cout << "button dtor\n"; } virtual void draw() override { control::draw(); std::cout << "button::draw\n"; } }; struct image_button : public image, public button, virtual public clickable { int content_alignment; image_button(int const i) : image(i), button(i), control(i) { std::cout << "image_button ctor\n"; } ~image_button() { std::cout << "image_button dtor\n"; } virtual void draw() override { button::draw(); image::draw(); std::cout << "image_button::draw\n"; } };
The new memory layout of the image_button
class is shown in the following image:
Again, we can notice several things here:
- the layout of the
image
object contains theflippable
object, as this class is a non-virtual base - there is only one copy of the
clickable
object layout, as this class is a virtual base class for bothbutton
andimage_button
- the memory layout of the two virtual base classes,
control
andclickable
, is located at the end of theimage_button
layout
The new output of the program is listed here:
control ctor clickable ctor flippable ctor image ctor button ctor image_button ctor control::draw button::draw control::draw image::draw image_button::draw image_button dtor button dtor image dtor flippable dtor clickable dtor control dtor
The order of the constructor calls, as seen here, as well as the destructor calls is following the several rules listed at the beginning of this section.
Alternatives
Because of this diamond problem, and perhaps because other languages do not support multiple inheritance, there is a considerable opposition to using multiple inheritance. That doesn’t necessary mean that multiple inheritance is evil or it can’t be used successfully in various scenarios. Inheritance in general should be used when it has benefits not for the purpose of reusing code. There are many cases when aggregation is a better solution than inheritance.
If you do use multiple inheritance, in general, it’s preferred that the virtual base classes are pure abstract base classes. That means only pure virtual methods and, if possible, no data members either. That is basically the equivalent of interfaces in C# or Java. Using this approach, multiple inheritance becomes equivalent to the single inheritance in these other programming languages.
An alternative to multiple inheritance is using some design patterns. A good example is the bridge design pattern that allows you to separate abstractions from the implementations.
References
You can read more about virtual inheritance here: ISO C++: Inheritance – multiple and virtual inheritance.
The memory layout images in this article were created using Struct Layout – an extension for Visual Studio.
The class diagram images in this article were created using Visual Paradigm Online – a free tool for drawing class diagrams and other UML diagrams.
Thank you for the article.
In the first listing you also use a virtual Inheritance, what was confused me a lot =)
Oops. Thanks for noticing. I had various code samples and copy-pasted the wrong one. It’s fixed now!
great Marius. I really liked the diagrams!
Small question – what do you think of removing the virtual keyword when using override? same question on destructor, what do you think of just having override per core guidelines ?
This does not seem to be an accurate statement: “The designers of these languages didn’t considered the benefits of supporting this feature to worth the effort.” The benefits were considered — at least in case of Java or C# — but deemed not worth the (multiple) costs.
What does VBTablePtr pointer holds ?
Virtual base classes have another useful feature due to their placement in the initialization sequence: If a virtual base class lacks the default constructor then all of its inheriting classes must supply parameters for the virtual base class in their own constructor. This can be used to enforce all subclasses to perform some operation which the virtual base class cannot do because of missing some inheriting-class-specific information.
@Boris, perhaps I should have said “not worth the implications” rather than “not worth the effort”. Point taken.
@Vinoth, A class that has a virtual base must be able to compute the location of the virtual base from a pointer to the derived class. This is that the “virtual base table pointer” does. in VC++, objects of a class that have a virtual base have this field. It’s a pointer to a table of displacements from the vbptr address to the address of the virtual base(s). This vbptr table is shared by all the instances of the class.