Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Multiple Inheritance Considered Useful


The title of this article, "Multiple Inheritance Considered Useful," is a bit of a philosophical statement. I do not claim that I use multiple inheritance in C++ all the time, but on occasion, I do find it quite useful. I know I am not the only person who feels this way, but nevertheless, you find a lot of corporate C++ coding standards and even recommendations by C++ experts that suggest you should avoid multiple inheritance. Furthermore, there are a number of other object-oriented languages, several of them newer than C++, that do not support multiple inheritance.

I am not interested in arguments about whether you "need" multiple inheritance or whether you can get by with single inheritance (maybe with the addition of "interfaces"). I consider the arguments vacuous. It is not a question of need. When you look at it objectively, you do not need even single inheritance (C doesn't have it, and lots of people still use C). In fact, you do not need a lot of things that even C has.

The question is not one of need, but whether something is useful. I am not particularly interested in programming languages where the designer has deliberately decided I don't need something. I prefer programming languages that give me features that might be useful and let me decide what I need in a given situation.

That is the philosophical side. The practical side is that a lot of programmers avoid using multiple inheritance in C++ because they find both its syntax and semantics quirky and hard to remember, and they worry that it adds extra overhead. Last but not least, multiple inheritance in classical C++ (before the ISO Standard) had shortcomings that were not easily overcome. For all of these reasons, multiple inheritance in C++ has gotten a bad reputation among the rank-and-file of the C++ programming community.

In this article, I present an overview of how multiple inheritance works in C++. In so doing, I address some of the issues about both its overhead and quirks. An understanding of these issues goes a long way toward defusing the objections to the use of multiple inheritance. Finally, I offer recommendations about library design to facilitate the use of multiple inheritance in Standard C++.

How it Works

Let's start with a review of single inheritance. Suppose you have a couple of classes:

class A {
public:
    virtual ~A();
    virtual void foo();
};
class B : public A {
public:
    virtual ~B();
    virtual void foo();
};

Figure 1 shows one possible layout of an object of class B in memory. In this diagram, I show that a B object begins with an A object, followed by any extra data for B (shown as B'). At the beginning of the object, I show the existence of the vptr. Let me emphasize, this is a possible layout—compiler writers are free to use a different layout.

Figure 1: Layout of an object of class B in memory.

Now consider a couple of simple statements:

B* ptrB = new B;
A* ptrA = ptrB

This compiles and works just like you expect. I am using public inheritance here for illustration purposes. Because I am using public inheritance, the C++ specification lets you implicitly convert a pointer to an object into a pointer to its public base. If you use the layout in Figure 1, then this implicit conversion does not actually change the value of the pointer, just how the compiler treats its type. In other words, there is no extra overhead associated with this assignment.

Going the opposite direction is not an implicit conversion, but can be accomplished with an explicit cast:

ptrB = static_cast<B*>(ptrA);

Again, we expect there to be no extra overhead beyond the assignment.

Now consider a virtual function call made through ptrA:

ptrA->foo();

You will note that there is only one vptr in the B object. It is also the vptr for the A object. Naturally, because the real object is a B, the vptr points to the vtable for class B. The function call just presented is a typical virtual function call. The object pointer that gets passed to the function is a B pointer, but because this is the same as the A pointer, nothing has to be done other than invoke the function. Again, this is a possible layout, but something similar to it is common in typical C++ implementations.

Moving to a simple example of multiple inheritance, let's add another base class C, and a derived class D:

class C {
public:
    virtual ~C();
    virtual void foobar();
};

class D : public B, public C {
public:
    virtual ~D();
    virtual void foo();
    virtual void foobar();
};

Figure 2 shows the possible memory layout for an object of type D. Here, the D object begins with a B object, which in turn contains an A object followed by the data unique to B. The B object is followed by a C object, and then everything is followed by the unique data for D.

Figure 2: Memory layout for an object of type D.

Take a look at what this means to our simple conversions and function calls:

D* ptrD = new D;
B* ptrB = ptrD;
A* ptrA = ptrD;
C* ptrC = ptrD; // ???

This still compiles fine. The implicit conversion from a pointer to an object into a pointer to a sub-object of a public base class applies to multiple inheritance the same as for single inheritance. The first three statements are similar to the single-inheritance case—all you have to do is change the type of the pointer; the value stays the same. This is not the case with the implicit conversion from a pointer-to-D into a pointer-to-C (the last statement). In this case, the C object that is embedded in the D object is offset some amount from the beginning of the object. To obtain a valid pointer-to-C, you have to adjust the actual value of the pointer.

Converting pointers in the other direction is clearly possible, again with a cast:

ptrD = static_cast<D*>(ptrA);
ptrD = static_cast<D*>(ptrB);
ptrD = static_cast<D*>(ptrC);

The same situation applies: While the first two explicit casts do not need to change the underlying value of the pointer (using the layout shown), the third cast must adjust the pointer. So we have "extra overhead hit #1." It is not much overhead, just the addition or subtraction of an offset from the pointer, but it is extra overhead compared to the single-inheritance case—at least, given the object layout presented here.

Note that I am using static_casts. This tells the compiler that we really have a D object, and the compiler is taking us at our word. Because the compiler knows the layout of a D object, and because it knows that a C object embedded in the D object is offset by such-and-such an amount, it doesn't have any problem doing the cast, but the cast will involve some overhead.

Assume that you have a valid D object and consider some function calls—in particular, virtual function calls:

ptrA->foo();    // calls D::foo()
ptrB->foo();    // calls D::foo();
ptrC->foobar(); // calls D::foobar();

All of this compiles and works as expected. But consider the last call. You are making a virtual function call through a pointer-to-C. First off, it is obvious that the C object embedded in the D object has to retain its own vptr. That makes "extra overhead hit #2." In single inheritance, only one vptr is needed for the object, no matter how many base class levels are present. In multiple inheritance, every embedded object must have its own vptr. But what does that vptr actually point to?

In single inheritance, the vptr points to the vtable of the actual object. In multiple inheritance, things are more complicated. While we make the function call using a pointer-to-C, when we actually enter the D::foobar() function, it must have a this pointer that is a valid pointer-to-D. This means that every time we make a virtual function call through our ptrC, the compiler must somehow perform the same pointer offset adjustment as was necessary in the explicit conversion shown earlier. In the case of the explicit conversion, we tell the compiler what to do at compile time. In the case of the virtual function call, the compiler has to figure out what must be done at runtime.

While there are a number of different schemes that could be used to effect this conversion, they all involve some type of extra information in the vtable. This is the only thing that is going to tell the compiler it really has a D object instead of some other type of object singly inherited from class C. This means, in all likelihood, there will be multiple sections of the vtable for D—one part will be pointed at by the vptr in the embedded B object, and a different section will be used by the vptr of the embedded C object. The latter section contains the extra information needed to offset the pointer-to-C into a valid pointer-to-D. Thus, multiple inheritance requires larger vtables, which is "extra overhead #3."

So far we have picked up some extra overhead in the form of multiple vtable pointers in each object, larger vtables for the class itself, and the need to offset the pointer value when making virtual function calls through a pointer to an embedded sub-object (the same situation applies in reverse if we make a nonvirtual function call to a C::func() using a D object). The overhead is real enough, but whether it would be significant would depend on the application. In my experience, the vast majority of applications would not find it noticeable.

At this point, note that nothing is really quirky about the coding. Everything works exactly like it did in the single-inheritance case.

Now, I introduce a common base class by having our class C also derive from class A:

class C : public A {
public:
    virtual ~C();
    virtual void foo();
    virtual void foobar();
};

See Figure 3. It amazes me how much fear and loathing this simple diagram generates. I have even heard some people refer to this as "the deadly diagram of death." Stop for a moment and note that C++ is unusual among object-oriented programming languages in that it does not provide a common root class for the inheritance hierarchy. In other object-oriented languages that support multiple inheritance (yes, there are some) and have a common root class, the situation in Figure 3 is the norm for all multiple inheritance. Therefore, I must admit to being both bemused and disgusted by the reaction this situation causes among developers who claim to be object-oriented programmers.

Figure 3: The deadly diagram of death.

Unfortunately, while the diamond inheritance diagram is probably more common than not in multiple inheritance, things start to get quirky in C++. Figure 4 shows a possible layout of our new D object.

Figure 4: Possible D object layout.

This is a straightforward extension of what we have already seen, but as you can see, a D object now contains two separate A objects—one as part of the embedded B object, and another as part of the embedded C object. This is probably not what you want. If you are new to C++, it is also probably not what you expected. Ignore that for a moment and consider what this does to our conversion and function call examples:

D* ptrD = new D;
B* ptrB = ptrD;
C* ptrC = ptrD;
A* ptrA = ptrD;

The first three of these compile just as they did before, but now the fourth assignment fails. One compiler I use produces the error message: "'A' is an ambiguous base of 'D'."

The key word is "ambiguous." In other words, there are now two A objects in the D object, and without further guidance from the programmer, the compiler does not know which one we want to point to. To resolve the ambiguity, we have to tell the compiler whether we want the A-in-B or the A-in-C. We can do that with an explicit cast:

A* ptrAofB = static_cast<B*>(ptrD);	// point to the A-in-B

or:

A* ptrAofC = static_cast<C*>(ptrD);	// point to the A-in-C

Casting back the other way has exactly the same problem (in fact, on my compiler it produces exactly the same error message):

D* ptrD1 = static_cast<D*>(ptrB);    // no problem
D* ptrD2 = static_cast<D*>(ptrC);    // adjust pointer
D* ptrD3 = static_cast<D*>(ptrAofB); // ambiguous

Again, we can resolve the ambiguity of the last cast with yet another explicit cast:

D* ptrD = static_cast<D*>(static_cast<B*>(ptrAofB));

or:

D* ptrD = static_cast<D*>(static_cast<C*>(ptrAofC));

Besides the simple fact that this is starting to get annoying to type, nesting casts such as this significantly increases the likelihood that at some point you will make a mistake. This results in "lying to your compiler." Don't forget, we are using static_casts here, and the fundamental fact of a static_cast is that the compiler trusts us. Lying to your compiler is a Bad Idea.

Consider some function calls:

ptrD->foo();	// no problem
ptrB->foo();	// no problem
ptrC->foo(); 	// no problem
ptrA->foo();	// no problem ??

The last call works correctly (it calls D::foo()) no matter which ptrA you use. This may seem a little surprising, but really should not. After all, virtual functions are found via dynamic binding at runtime. Thus, if we have a D object and we have a virtual function D::foo(), then we expect to invoke that function if we make a virtual function call through any pointer or reference to any of D's base classes.

Unfortunately, while that makes sense, consider what happens if class D does not provide a version of function foo(). Now, the first of the four calls shown earlier will be flagged as ambiguous by the compiler, but the other three calls continue to compile and work. As you would probably expect, ptrB->foo() now calls B::foo(), ptrC-foo() now calls C::foo(), and ptrA->foo() will call either B::foo() or C::foo() depending upon the A sub-object to which we are actually pointing. As noted, even though we really have a D object, each of the base class sub-objects will have its own section in the vtable. If D does not provide an override of virtual function foo(), then each of those subsections will point to their own different versions of foo().

If you are like me, you probably find this somewhat, well, quirky. The extra casts are a problem to type, and the possibility that virtual function calls could actually resolve to different functions, depending upon which sub-object pointer we are using, also raises just a bit of concern. Unfortunately, as long as there are two possible interpretations of which the A object is being referred to, then we can not avoid the ambiguity. Call this programming quirk #1. What can we do about it?

This situation is probably not what you expect when you look at an inheritance diagram like Figure 3. What you probably want is for there to be only one A object in the D object. You can have that situation in C++, but now we get into really quirky. In order to have only one A object in our D object, we have to use virtual base classes (often called "virtual inheritance"). It looks like this:

class B : public virtual A {
public:
   virtual ~B();
   virtual void foo();
};

class C : public virtual A {
public:
   virtual ~C();
   virtual void foo();
   virtual void foobar();
};

For base classes, "virtual" means "create only one sub-object of this type in any derived object." The first thing you probably notice is that even though the problem occurs when we create a D object, in order to fix it we have to go back and change the definition of classes that derive from A. You will also note that we have to change both of them—if any class (C, for instance) uses ordinary inheritance, it still contains an embedded A object just like before. Call this "programming quirk #2." Put that aside for a moment and consider what happens to our examples, assuming we get the virtual inheritance done properly:

D* ptrD = new D;
B* ptrB = ptrD;
C* ptrC = ptrD;
A* ptrA = ptrD;

This all compiles and works just fine. In spite of the quirks in getting here, virtual inheritance has returned us to the simple situation we had with single inheritance—at least for the case of implicitly converting to a base class. But how does it work?

Figure 5 shows a possible layout of a class D object using virtual inheritance. You are probably tired of hearing this, but I must emphasize that this is only a possible layout—compilers are free to do something else, and in this case, they probably will do something else. I show this layout just to make a point.

Figure 5: Possible layout of class D using virtual inheritance.

Again, there is only one object of a virtual base class in any derived object. The point I want to emphasize is that the A object does not belong to either the B object or the C object, but instead belongs to the D object (I'll explain the extra pointers below). Using this scheme, the layout of a B object might be as in Figure 6.

Figure 6: Possible layout of class B using virtual inheritance.

Now consider:

A* ptrA1 = ptrB;
A* ptrA2 = ptrC;
A* ptrA3 = ptrD;


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.