Uses & Abuses of Inh... Part 2

Home Blog Talks Books & Articles Training & Consulting

Prev
Up
Next

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original article substantially as first published. See the book Exceptional C++ (Addison-Wesley, 2000) for the most current version of this article. The versions in the book have been revised and expanded since their initial appearance in print. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard.

Uses and Abuses of Inheritance, Part 2

This article appeared in C++ Report, 11(1), January 1999.

 

This is the second of two columns about inheritance, and how to use it judiciously. In the previous column,[1] I focused on private and protected inheritance, showing how these are often abused to express IS-IMPLEMENTED-IN-TERMS-OF when plain old containment would do just as well or better, and along the way I detailed a pretty exhaustive list of the reasons to use nonpublic inheritance. Thanks to Astute Reader Alex Martelli for his followup remarks about two more situations where using inheritance instead of containment can be appropriate; I'll start this month's column by covering one of them. After that, I'll touch briefly on public inheritance, and then bring things together by covering some multiple inheritance issues and techniques.

Another Reason to Use Nonpublic Inheritance

Here is an addition to the previous column's list of reasons to use inheritance instead of containment:

o         We benefit substantially from the empty base class optimization. Sometimes the class you are IMPLEMENTING-IN-TERMS-OF may have no data members at all; that is, it's just a bundle of functions. In this case, there can be a space advantage to using inheritance instead of containment because of the empty base class optimization. In short, compilers are allowed to let an empty base subobject occupy zero space, whereas an empty member object must occupy nonzero space even if it doesn't have any data:

class B { /* ... functions only, no data ... */ };

// Containment: incurs some space overhead
//
class D {
  B b; // b must occupy at least one byte,
};     // even though B is an empty class

// Inheritance: can incur zero space overhead
//
class D : private B {
       // the B subobject need not occupy
};     // any space at all

For a detailed discussion of the empty base optimization, see Nathan Myers' excellent article on this topic in Dr. Dobb's Journal.[2]

Having said all that, let me end with a caution for the overzealous: Not all compilers actually perform the empty base class optimization, and even if they do you probably won't benefit significantly unless you know there will be many (say, tens of thousands) of these objects in your system. Unless the space savings are very important to your application, and you know that your compiler will actually perform the optimization, it would be a mistake to introduce the extra coupling of the stronger inheritance relationship instead of using simple containment.

The Most Important Thing to Know About Public Inheritance

There is only one point I want to stress about public inheritance, and if you follow this advice it will steer you clear of the most common abuses: Only use public inheritance to model true IS-A, as per the Liskov Substitution Principle (LSP).[3] That is, a publicly-derived class object should be able to be used in any context where the base class object could be used and still guarantee the same semantics.

In particular, following this rule will avoid two common pitfalls:

o         Never use public inheritance when non-public inheritance will do. Public inheritance should never be used to model IS-IMPLEMENTED-IN-TERMS-OF without true IS-A. This may seem obvious, but I've noticed that some programmers routinely make inheritance public out of habit. That's not a good idea, and this point is in the same spirit as my advice last time to never use inheritance (of any kind) when good old containment/membership will do. If the extra access and tighter coupling aren't needed, why use them? If a class relationship can be expressed in more than one way, use the weakest relationship that's practical.

o         Never use public inheritance to implement "almost IS-A." I've seen some programmers, even experienced ones, inherit publicly from a base and implement "most" of the overridden virtual functions in a way that preserved the semantics of the base class. In other words, in some cases using the Derived object as a Base would not behave quite the way that a reasonable Base client could expect. An example often cited by Robert Martin is the usually misguided idea of inheriting a Square class from a Rectangle class "because a square is a rectangle." That may be true in mathematics, but it's not necessarily true in classes. For example, say that the Rectangle class has a virtual SetWidth(int) function; then Square's implementation to set the width would also naturally set the height, so that the object remains square. Yet there may well exist code elsewhere in the system that works polymorphically with Rectangle objects, and would not expect that changing the width would also change the height -- after all, that's not true of Rectangles in general! This is a good example of public inheritance that would violate LSP, because the derived class does not deliver the same semantics as the base class.

Usually when I see people doing this kind of "almost IS-A" I'll try to point out to them that they're setting themselves up for trouble. After all, someone somewhere is bound to try to use derived objects polymorphically in one of the ways that would occasionally give unexpected results, right? "But it's okay," came one reply, "it's only a little bit incompatible, and I know that nobody uses Base-family objects that way [in that particular way that would be dangerous]." Well, being "a little bit incompatible" is a lot like being "a little bit pregnant"--now, I had no reason to doubt that the programmer was right, namely that no code then in the system would hit the dangerous differences; but I also had every reason to believe that someday, somewhere, a maintenance programmer was going to make a seemingly innocuous change, run afoul of the problem, and spend hours analyzing why the class was poorly designed and then spend additional days fixing it.

Don't be tempted. Just say no. If it doesn't behave like a Base, it's NOT-A Base, so don't derive and make it look like one.

Multiple Inheritance -- Nutshell Recap

This brings us to the main topic of this article -- multiple in