I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

This motivating example helps to illustrate some of the issues surrounding the use of inheritance, especially how to choose between nonpublic inheritance and containment.

The answer to Question 1桰s there any difference between MySet1 and MySet2梚s straightforward: There is no substantial difference between MySet1 and MySet2. They are functionally identical.

Question 2 gets us right down to business: More generally, what is the difference between nonpublic inheritance and containment? Make as comprehensive a list as you can of reasons why you would use inheritance instead of containment.

  • Nonpublic inheritance should always express IS-IMPLEMENTED-IN-TERMS-OF (with only one rare exception, which I'll cover shortly). It makes the using class depend upon the public and protected parts of the used class.

  • Containment always expresses HAS-A and, therefore, IS-IMPLEMENTED-IN-TERMS-OF. It makes the using class depend upon only the public parts of the used class.

It's easy to show that inheritance is a superset of single containment梩hat is, there's nothing we can do with a single MyList<T> member that we couldn't do if we inherited from MyList<T>. Of course, using inheritance does limit us to having just one MyList<T> (as a base subobject); if we needed to have multiple instances of MyList<T>, we would have to use containment instead.

Guideline

graphics/guideline_icon.gif

Prefer containment (a.k.a. "composition", "layering", "HAS-A", "delegation") to inheritance. When modeling IS-IMPLEMENTED-IN-TERMS-OF, always prefer expressing it using containment, not inheritance.


That being the case, what are the extra things we can do if we use inheritance that we can't do if we use containment? In other words, why use nonpublic inheritance? Here are several reasons, in rough order from most to least common. Interestingly, the final item points out a useful(?) application of protected inheritance.

  • We need to override a virtual function. This is one of inheritance's classic raisons d'être.[7] Often, we want to override in order to customize the used class's behavior. Sometimes, however, there's no other choice. If the used class is abstract梩hat is, it has at least one pure virtual function that has not yet been overridden梬e must inherit and override because we can't instantiate directly.

    [7] See also Meyers98 under the index entry, "French, gratuitous use of."

  • We need access to a protected member. This applies to protected member functions[8] in general, and to protected constructors in particular.

    [8] I say "member functions" because you would never write a class that has a public or protected member variable, right? (Regardless of the poor example set by some libraries.)

  • We need to construct the used object before, or destroy it after, another base subobject. If the slightly longer object lifetime matters, there's no way to get it other than by using inheritance. This can be necessary when the used class provides a lock of some sort, such as a critical section or a database transaction, which must cover the entire lifetime of another base subobject.

  • We need to share a common virtual base class or override the construction of a virtual base class. The first part applies if the using class has to inherit from one of the same virtual bases as the used class. If it does not, the second part may still apply. The most-derived class is responsible for initializing all virtual base classes, so if we need to use a different constructor or different constructor parameters for a virtual base, then we must inherit.

  • 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梩hat 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 base 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 (Myers97).

Having said all that, let me add 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.

There is one additional feature we can get using nonpublic inheritance, and it's the only one that doesn't model IS-IMPLEMENTED-IN-TERMS-OF:

  • We need "controlled polymorphism"桳SP IS-A, but in certain code only. Public inheritance should always model IS-A as per the Liskov Substitution Principle (LSP).[9] Nonpublic inheritance can express a restricted form of IS-A, even though most people identify IS-A with public inheritance alone. Given class Derived : private Base, from the point of view of outside code, a Derived object IS-NOT-A Base. So, of course, it can't be used polymorphically as a Base because of the access restrictions imposed by private inheritance. However, inside Derived's own member functions and friends only, a Derived object can indeed be used polymorphically as a Base (you can supply a pointer or reference to a Derived object where a Base object is expected), because members and friends have the necessary access. If instead of private inheritance, you use protected inheritance, then the IS-A relationship is more visible to further-derived classes, which means subclasses can also make use of the polymorphism.

    [9] See www.gotw.ca/publications/xc++/om.htm for several good papers describing LSP.

That's as complete a list as I can make of reasons to use nonpublic inheritance. (In fact, just one additional point would make this a complete list of all reasons to use any kind of inheritance: We need public inheritance to express IS-A. More on that when we get to Question 4.)

All of this brings us to Question 3: Which version of MySet would you prefer?TT>MySet1 or MySet2? Let's analyze the code in Example 1 and see whether any of the above criteria apply.

  • MyList has no protected members, so we don't need to inherit to gain access to them.

  • MyList has no virtual functions, so we don't need to inherit to override them.

  • MySet has no other potential base classes, so the MyList object doesn't need to be constructed before, or destroyed after, another base subobject.

  • MyList has no virtual base classes that MySet might need to share or whose construction it might need to override.

  • MyList is nonempty, so the "empty base class optimization" motive does not apply.

  • MySet IS-NOT-A MyList, not even within MySet's member functions and friends. This last point is interesting, because it points out a (minor) disadvantage of inheritance. Even had one of the other criteria been true so that we would use inheritance, we would have to be careful that members and friends of MySet wouldn't accidentally use a MySet polymorphically as a MyList梐 remote possibility, maybe, but sufficiently subtle that if it did ever happen, it would probably keep the poor programmer who encountered it confused for hours.

In short, MySet should not inherit from MyList. Using inheritance where containment is just as effective only introduces gratuitous coupling and needless dependencies, and that's never a good idea. Unfortunately, in the real world, I still see programmers梕ven experienced ones梬ho implement relationships like MySet's using inheritance.

Astute readers will have noticed that the inheritance-based version of MySet does offer one (fairly trivial) advantage over the containment-based version: Using inheritance, you need to write only a using-declaration to expose the unchanged Size function. Using containment, you have to explicitly write a simple forwarding function to get the same effect.

Of course, sometimes inheritance will be appropriate. For example:



// Example 2: Sometimes you need to inherit 


//


class Base


{


public:


  virtual int Func1();


protected:


  bool Func2();


private:


  bool Func3(); // uses Func1


};


If we need to override a virtual function like Func1 or access a protected member like Func2, inheritance is necessary. Example 2 illustrates why overriding a virtual function may be necessary for reasons other than allowing polymorphism. Here, Base is implemented in terms of Func1 (Func3 uses Func1 in its implementation), so the only way to get the right behavior is to override Func1. Even when inheritance is necessary, however, is the following the right way to do it?



// Example 2(a) 


//


class Derived : private Base // necessary?


{


public:


  int Func1();


  // ... more functions, some of which use


  //     Base::Func2(), some of which don't ...


};


This code allows Derived to override Base::Func1, which is good. Unfortunately, it also grants access to Base::Func2 to all members of Derived, and there's the rub. Maybe only a few, or just one, of Derived's member functions really need access to Base::Func2. By using inheritance like this, we've needlessly made all of Derived's members depend upon Base's protected interface.

Clearly, inheritance is necessary, but wouldn't it be nice to introduce only as much coupling as we really need? Well, we can do better with a little judicious engineering.



// Example 2(b) 


//


class DerivedImpl : private Base


{


public:


  int Func1();


  // ... functions that use Func2 ...


};


class Derived


{


  // ... functions that don't use Func2 ...


private:


  DerivedImpl impl_;


};


This design is much better, because it nicely separates and encapsulates the dependencies on Base. Derived only depends directly on Base's public interface and on DerivedImpl's public interface. Why is this design more successful? Primarily, because it follows the fundamental "one class, one responsibility" design guideline. In Example 2(a), Derived was responsible for both customizing Base and implementing itself in terms of Base. In Example 2(b), those concerns are nicely separated out.

Now for some variants on containment. Containment has some advantages of its own. First, it allows having multiple instances of the used class, which isn't practical, or even always possible, with inheritance. If you need to both derive and have multiple instances, just use the same idiom as in Example 2(b). Derive a helper class (like DerivedImpl) to do whatever needs the inheritance, then contain multiple copies of the helper class.

Second, having the used class be a data member gives additional flexibility. The member can be hidden behind a compiler firewall inside a Pimpl[10] (whereas base class definitions must always be visible), and it can be easily converted to a pointer if it needs to be changed at run-time (whereas inheritance hierarchies are static and fixed at compile-time).

[10] See Items 26 through 30.

Finally, here's a third useful way to rewrite MySet2 from Example 1(b) to use containment in a more generic way.



// Example 1(c): Generic containment 


//


template <class T, class Impl = MyList<T> >


class MySet3


{


public:


  bool   Add( const T& ); // calls impl_.Insert()


  T      Get( size_t index ) const;


                          // calls impl_.Access()


  size_t Size() const;    // calls impl_.Size();


  // ...


private:


  Impl impl_;


};


Instead of just choosing to be IMPLEMENTED-IN-TERMS-OF MyList<T> only, we now have the flexibility of having MySet IMPLEMENTABLE-IN-TERMS-OF any class that supports the required Add, Get, and other functions that we need. The C++ standard library uses this very technique for its stack and queue templates, which are by default IMPLEMENTED-IN-TERMS-OF a deque, but are also IMPLEMENTABLE-IN-TERMS-OF any other class that provides the required services.

Specifically, different user code may choose to instantiate MySet using implementations with different performance characteristics梖or example, if I know I'm going to write code that does many more inserts than searches, I'd want to use an implementation that optimizes inserts. We haven't lost any ease of use, either. Under Example 1(b), client code could simply write MySet2<int> to instantiate a set of ints, and that's still true with Example 1(c), because MySet3<int> is just a synonym for MySet3<int,MyList<int> >, thanks to the default template parameter.

This kind of flexibility is more difficult to achieve with inheritance, primarily because inheritance tends to fix an implementation decision at design time. It is possible to write Example 1(c) to inherit from Impl, but here the tighter coupling isn't necessary and should be avoided.

The most important thing to know about public inheritance can be learned with the answer to Question 4: Make as comprehensive a list as you can of reasons why you would use 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.[11] That is, a publicly derived class object should be able to be used in any context in which the base class object could be used and still guarantee the same semantics. [Note: We covered a rare exception梠r, more correctly, an extension梩o this idea in Item 3.]

[11] See www.gotw.ca/publications/xc++/om.htm for several good papers describing LSP.

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

  • Never use public inheritance when nonpublic 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 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.

  • Never use public inheritance to implement "IS-ALMOST-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. It violates the key precept of public inheritance: "Require no more and promise no less."

When I see people doing this kind of "almost IS-A," I usually 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 in that 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梟amely, that no code then in the system would hit the dangerous differences. However, 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.

Guideline

graphics/guideline_icon.gif

Always ensure that public inheritance models both IS-A and WORKS-LIKE-A according to the Liskov Substitution Principle. All overridden member functions must require no more and promise no less.


Guideline

graphics/guideline_icon.gif

Never inherit publicly to reuse (code in the base class); inherit publicly in order to be reused (by existing code that uses base objects polymorphically).


Conclusion

Use inheritance wisely. If you can express a class relationship using containment/delegation alone, you should always prefer that. If you need inheritance but aren't modeling IS-A, use nonpublic inheritance. If you don't need the combinative power of multiple inheritance, prefer single inheritance. Large inheritance hierarchies, in general, and deep ones, in particular, are confusing to understand and therefore difficult to maintain. Inheritance is a design-time decision and trades off a lot of run-time flexibility.

Some people feel that "it just isn't OO unless you inherit," but that isn't really true. Use the simplest solution that works, and you'll be more likely to enjoy many pleasant years of stable and maintainable code.

    I l@ve RuBoard Previous Section Next Section