1

Virtual inheritance in C++

 2 years ago
source link: https://mariusbancila.ro/blog/2021/11/16/virtual-inheritance-in-c/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Virtual Inheritance in C++

Posted on November 16, 2021November 16, 2021 by Marius Bancila

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 as id, width, and height
  • image and button are classes derived from control, each with its own additional data members
  • image_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;
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:

Object memory layout

What we can see from here is that:

  • image contains everything that control has plus its own data members
  • button contains everything that control has plus its own data members
  • image_button contains everything that image and button has plus its own data members; however, this implies it has two copies of the data members of control.

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
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'
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
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;
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
virtual public control and
public virtual control
public virtual control

are equivalent.

The memory layout of the image_button class looks as follows:

layout3.png

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 and button 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";
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";
   }
};
layout4-1.png

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
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);
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
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:

cd3-1.png

The new hierarchy differs from the previous on as follows:

  • the image class has two base classes: non-virtual base flippable and virtual base control
  • the button class has two base classes also, both virtual: control and clickable
  • the image_button class has three base classes: non-virtual bases image and button, and virtual base class clickable

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";
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:

layout6.png

Again, we can notice several things here:

  • the layout of the image object contains the flippable 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 both button and image_button
  • the memory layout of the two virtual base classes, control and clickable, is located at the end of the image_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
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.

Like this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK