Multiple Inheritance Considered Useful

Arguments against multiple inheritance range from the philosophical to the practical, but in the end only one question matters: Is it useful?


February 01, 2006
URL:http://www.drdobbs.com/cpp/multiple-inheritance-considered-useful/184402074

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;

The language guarantees that these all work. In particular, it does not matter that ptrC really points to part of a D object, or is just a C object. But if the A object is in a different place in a D object than it is in a C object, how does the compiler make this work? The answer is with the classic "another level of indirection." As shown in Figures 5 and 6, each object that has a virtual base class replaces the embedded sub-object with a pointer to the sub-object. Now, whenever the compiler needs to do an implicit conversion to a virtual base class, it has to go through the indirection. Call this multiple-inheritance overhead #4 and #5. First, we get extra pointers in the object—one that points to the virtual base class sub-object, and an extra vtable pointer in the base class sub-object itself (of course, this really only matters when we use virtual base classes in single inheritance; in a multiple-inheritance situation, the extra pointers are more than compensated by the fact that there is only one sub-object in the final object). Second, implicit conversions involve an additional level of indirection. Frankly, this is probably similar to the pointer adjustment overhead previously mentioned, but it is a different kind of overhead, and it applies to every object that has a virtual base class and every conversion.

You might expect that down casts have the same overhead, but now we hit the wall of quirkiness:

D* ptrD1 = static_cast<D*>(ptrB);	// ok
D* ptrD2 = static_cast<D*>(ptrC);	// ok
D* ptrD3 = static_cast<D*>(ptrA);	// error-will not compile

The last statement gives the following error on one of my compilers: "cannot convert from base 'A' to derived type 'D' via virtual base 'A'." That is pretty clear: You cannot cast from a virtual base class to a derived class. Given the layout just shown, you can understand why—we had to chase a pointer to get from a B, C, or D object pointer to the A sub-object, but there is no way to go the opposite direction.

Now, stop and consider for a moment: In classical C++ (before the ISO Standard), you only had the old C-style cast. Unfortunately, it didn't work any better than the newer static_cast for this purpose. In other words, in classical C++, there was no way to cast down an inheritance hierarchy from a virtual base class. This wasn't just a programming quirk, it was basically a showstopper. Yes, there are ways around this problem, but they have the same problems all workarounds have: They are nonstandard, so everybody always does it a little differently. This significantly aggravates the problem of combining different class hierarchies in an application. I think this was one of the fundamental reasons that so many people came to recommend against using multiple inheritance in C++.

Before we look at the solution to this, lets finish up the example by looking at virtual function calls:

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

Note that virtual function calls work as expected. Of course, this means that the virtual function call mechanism can do something that we cannot do with a cast; that is, recover a D object pointer via a pointer to a virtual base class.

Of course, by now you know the answer to our showstopper—use a dynamic_cast:

D* ptrD = dynamic_cast<D*>(ptrA);

This will compile and either return a valid pointer-to-D or return a null if the object pointed to by ptrA is not really a D (or something derived from D). This is the crux of the complaints against multiple inheritance—dynamic_casts add more overhead, sometimes a lot more.

It is difficult to estimate with any certainty how much overhead is involved in a dynamic_cast. We can say in general terms that a dynamic_cast must find out the actual type of the object by chasing the vptr to the object's vtable. Then it must search the class's Run Time Type Information (RTTI)—which is usually some form of table chained off the vtable—to determine whether the cast is valid. Finally, assuming the cast is valid, then some type of pointer adjustment may be necessary. The obvious problem is: How is the RTTI information organized and how is it searched? Unfortunately, the only valid answer is: It depends on the compiler. On some compilers, dynamic_cast is amortized constant time while on other compilers, the RTTI search is a linear comparison of strings. Needless to say, there is a world of difference. Unfortunately, the Microsoft compiler (one of the most widely used C++ compilers) is one of those in which dynamic_cast can be expensive.

While the actual overhead of a dynamic_cast is an implementation issue, where and when a dynamic_cast is actually used is more or less under the control of the programmer. So, the overhead is real, but if it is a problem, there are things that can be done to minimize it. These are exactly the same things that could be done to get around the inability to downcast from a virtual base class in classical C++, but instead of being the only choice, now these workarounds can be limited to the few situations where the overhead of a dynamic_cast really is an issue.

I would be remiss if I did not mention one final quirk of virtual base classes—initialization order. Again, a virtual base class sub-object belongs to the most derived object. Therefore, it is the most derived object that is responsible for actually initializing the virtual base sub-object. This becomes a problem when the virtual base class initialization is not completely self contained via a default constructor.

Consider our little hierarchy, and suppose that A now looks like this:

class A {
    int _a;
public:
    A(int x) : _a(x) {}
    // _ as before
};

Because A no longer has a default constructor, we must deal with this in classes that derive from A. So you would expect to do something like this:

class B : public virtual A {;
public :
    B(int x) : A(x) {}
};

Similarly for C. But what do we do when we get to D? The obvious does not compile:

class D : public B, public C {
public:
    D(int x) : B(x), C(x) {}
};

This generates an error complaining about the lack of a default constructor for A. To make this work we have to write:

D(int x) : A(x), B(x), C(x) {}

While this does work, it is kind of silly because the invocation of the initializer for A is ignored in the B and C constructors when invoked for a D object.

If we do not have to have A completely initialized before constructing B and C, we can adopt the two-step initialization idiom for A:

class A {
    int _a;
public:
    A() {}
    void init(int x) { _a = x; }
};

Now B and C can provide constructors that initialize A, and default constructors that do nothing and let any derived class do the initialization. For example:

class D : public B, public C {
public:
    D(int x) { init(x); } 
};

In this case, all the base classes get default constructed, then A gets initialized via the call to init().

If we have to completely initialize A before B and C can be constructed, then we have to do initialization with the constructor and we are back to what I showed at first. Alternatively, perhaps you can get by with something like the following:

class B : public virtual A {  // class C is similar
public:
    B(int x) : A(x) {}
protected:
    B() : A(0) {}
};
class D : public B, public C {
public:
    D(int x) : A(x) {}  // B and C are default constructed
};

In B and C, we know the default constructors will not invoke the A constructor, but we have to supply it anyway. This is pretty quirky. In fact, the use of virtual base classes and their need to be initialized by the most derived class can come as a shock to someone who thinks they are doing straightforward single inheritance. Given the definitions we have above:

class E : public D {
public:
    E() : D(10) {}
};

will not compile. The compiler complains about the missing default A constructor call in the definition of the class E constructor.

To summarize: Using multiple inheritance in C++ (versus single inheritance) adds extra overhead in the form of:

Multiple inheritance increases the possibility for ambiguities. This in turn means that more casts are needed to resolve the ambiguities. Using multiple inheritance without using virtual base classes runs the risk of having multiple sub-objects of a given type in a derived object. This causes ambiguity problems and is not usually what is desired. Furthermore, in such cases some care must be taken when resolving the ambiguities to make sure you are pointing to the actual sub-object you desire, otherwise you may find that virtual function calls are not resolving to the functions you expect. If you are using static_casts, you also run the risk of making a mistake and ending up lying to your compiler.

Using virtual inheritance resolves some of the ambiguity problems but introduces its own overhead and programming quirks:

Whew. I guess it is not hard to understand why the majority of C++ programmers avoid multiple inheritance, and the majority of C++ experts recommend that you stick with single inheritance if possible. They also tend to recommend that if you do use multiple inheritance, you should try to avoid using virtual base classes. I am afraid I disagree (you really didn't think I would, did you?).

Recommendations

First, let's deal with the overhead issue. If you have a design situation where multiple inheritance is appropriate, then the only alternative is some sort of aggregation solution. When you look at what actually happens "under the hood" when you start trying to use aggregated sub-objects instead of multiple base classes, you find exactly the same types of overhead, both storage and runtime, as you do with multiple inheritance. In fact, I argue that the chances are poor to pathetic that you can hand craft any kind of aggregate solution that has less overhead than the compiler would generate using multiple inheritance. For this reason, I have always felt that arguments against the use of multiple inheritance based on what it costs were essentially nonsense—with one glaring exception. The exception is the need to use dynamic_casts to cast down an inheritance hierarchy from a virtual base class. If you are in a situation where that kind of overhead really matters, then—and only then—is it reasonable to consider alternative approaches.

When you start worrying about overhead costs, it is important to realize that some overhead is the inevitable result of trying to do anything. Practically any solution you craft will involve some overhead simply because it is necessary to get the job done. Almost inevitably, unnecessary overhead in C++ comes down to bad code on the part of the programmer, not a problem with the language itself. It is quite possible that a multiple inheritance solution will be the one with the least amount of unnecessary overhead.

What about the programming quirks? Let's cut to the chase: The real issue is with virtual base classes. A key point that gets overlooked in all the discussion about negatives is that multiple inheritance combined with virtual base classes provides capabilities that simply cannot be obtained any other way. In particular, only virtual inheritance gives you the possibility of eliminating multiple base class sub-objects in a complex derived class. This is impossible with any aggregate solution. So, rather than avoiding multiple inheritance in general and virtual base classes in particular, let's see if we can figure out a disciplined approach to overcoming their quirks.

If you are designing class hierarchies that are internal to a specific application, then you can do whatever you want. On the other hand, if your are building libraries of reusable software, here is what I recommend.

Recommendation #1: Make all base classes of abstract classes virtual. That's really pretty simple, isn't it? This way, if multiple inheritance is used at some point down the road, things are a lot easier for your client. Note that I did not recommend making all base classes virtual, just base classes of abstract classes. Concrete classes intended to be used as they are, can use whatever inheritance mechanism they prefer. I agree with most C++ experts; however, good design should not inherit one concrete class from another concrete class. If you are building a reusable library, and you find this happening, you probably need to refactor the library and create an intermediate abstract class. When you do, remember this recommendation.

At this point, I can almost see people cringing. If you follow this recommendation, then every class derived from some class in the library will basically have to be aware of every base class in the hierarchy—remember they are all supposed to be virtual base classes. That means that ordinary single inheritance just got a lot more difficult. Right? Maybe not! It depends upon how we set up the initialization of all those base classes.

Recommendation #2: Try to give your abstract base classes default constructors. Try hard. Try real hard. Do this even if you really do require some outside initialization. Default constructors make virtual base classes much easier to deal with.

Recommendation #3: If your class requires outside initialization, create a protected init() function that takes the appropriate parameters and does the initialization. Note: Do only class specific initialization in the init() function; do not call any base class init() functions.

At this point, we have enough functionality to actually initialize all our base classes. Now we are going to write initializing constructors. We will separate the constructors into two categories: the single-inheritance constructors and the multiple-inheritance constructors (for lack of better terms). The single-inheritance constructors are provided for those clients who derive a concrete class from an abstract base class using single inheritance and want things to look and work like normal. The multiple-inheritance constructors are intended for clients that derive a concrete class from multiple abstract base classes and presumably know what that means.

Recommendation #4(a): Create single-inheritance constructors that initialize all base classes and all local data members. If the class has no base classes, then this is just your typical initializing constructor. If the class has any base classes that need initialization, then the constructor calls the base class init() function(s) from the constructor body. Remember, this is an abstract class, so all its base classes are virtual (Recommendation #1). The base classes will have been default constructed by the derived class's constructor. When the constructor body executes, it finishes the initialization. When the constructor finishes execution, then all base classes will have been initialized—just like in single inheritance.

Recommendation #4(b): Create multiple-inheritance constructors that initialize only the data members of the class. The canonical form of such a constructor just calls the init() function for the class itself, although I typically use the initializer list both from habit and for the slight efficiency gain it provides.

Obviously, if a class has no base classes that require outside initialization, then the constructors created in Recommendations #4(a) and #4(b) are the same. Let's put together an example library to make things clear:

class A {
    int _a;
public:
    virtual ~A() = 0;
protected:
    A() {}
    A(int x) : _a(x) {}  // #4
    void init(int x) { _a = x; }
};
class B : public virtual A {
public:
    virtual ~B() = 0;
protected:
    B() {}
    B(int x) { A::init(x); }  // #4(a)
};
class C : public virtual A {
    double _c;
public:
    virtual ~C() = 0;
protected:
    C() {}
    C(double x) : _c(x) {} // #4(b)
    C(int x, double y) { A::init(x); init(y); } // #4(a)
    void init(double x) { _c = x; }
};

Now, if we use single inheritance to derive from either B or C (or A), then we can just do the normal thing in our constructor:

class Single : public B {
public:
    Single(int x) : B(x) {}
};

and it works. If you create a class using multiple inheritance, then we take a different approach:

class Multi: public B, public C {
public:
  Multi(int x, double y) : A(x), C(y) {}
};

The key to everything is the consistent application of the four recommendations in every abstract base class in the library.

One final note is in order. While it gives the appearance of ordinary single inheritance, the two-step construction for the virtual base classes can be less efficient than normal single-inheritance construction that takes full advantage of every constructor's initializer list. Lets call this multiple-inheritance performance hit #5. If you encounter a situation where this matters, the solution is simple: Just use the multiple-inheritance constructors and not the single-inheritance constructors. Yes, that means even your single-inheritance derived classes will need to initialize the virtual base classes, but if you are worried about the overhead of a constructor, you probably need to be aware of all the base classes in a hierarchy anyway.

Like I said, multiple inheritance can be useful. And if we design our libraries properly, it isn't even that difficult to use.


Jack Reeves is a senior software developer specializing in object-oriented software design and development for high-performance systems. He can be contacted at [email protected].

February, 2006: Multiple Inheritance Considered Useful

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

February, 2006: Multiple Inheritance Considered Useful

Figure 3: The deadly diagram of death.

February, 2006: Multiple Inheritance Considered Useful

Figure 4: Possible D object layout.

February, 2006: Multiple Inheritance Considered Useful

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

February, 2006: Multiple Inheritance Considered Useful

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

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.