I l@ve RuBoard |
![]() ![]() |
Solution![]() 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.
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
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.
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:
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.
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). 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.]
In particular, following this rule will avoid two common pitfalls.
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
Guideline
ConclusionUse 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 |
![]() ![]() |