Chapter 7. Proper Inheritance

FAQ 7.01 What is proper inheritance?

When the advertised behavior of the derived class is substitutable for that of the base class.

Proper inheritance and substitutable behavior can be guaranteed when the specifications of the member functions of the derived class require no more and promise no less than the specifications of the corresponding member functions in the base class(es). The derived class can also add new member functions. The specification for these new member functions can require as much or promise as little as they like, since they are not constrained by existing member functions in any base class.

In the following example, the specification for Derived::f() requires no more (in fact, it imposes a weaker requirement) and promises no less (in fact, it makes a stronger promise) than the specification of Base::f(). This will not break users of the base class, so this is proper inheritance.

image

image

Note that every member function of the derived class must have substitutable behavior with respect to the corresponding specification in the base class(es). If even one derived class member function specifies stronger requirements or weaker promises than the corresponding specification in the appropriate base class, the inheritance relationship is improper.

Finally note that this chapter deals with public inheritance. private and protected inheritance have completely different rules (see FAQ 37.01).

FAQ 7.02 What are the benefits of proper inheritance?

Substitutability and extensibility.

Substitutability: An object of a properly derived class can be freely and safely substituted for an object of its base class. For example, suppose a user defines a function sample(Base& b). If class Derived properly inherits from class Base, sample(Base& b) will work correctly when an object of class Derived is passed instead of an object of class Base. In contrast, there is no guarantee that sample(Base& b) will work correctly when it is passed an object of a class that was produced by improper inheritance.

Extensibility: The properly derived class can be freely and safely used in place of its base class even if the properly derived class is created months or years after the user code was defined. It's easy to extend a system's functionality when you add properly derived classes that contain enhanced functionality. These guarantees cannot be made when improper inheritance is used.

Note that these guarantees work only when the user code relies on the specification of the base class rather than the (possibly more specific) implementation of the base class (see FAQ 6.02).

FAQ 7.03 What is improper inheritance?

A design error that creates a mess.

Improper inheritance occurs when the derived class is not substitutable with respect to its base class(es), that is, when the specification of one or more inherited member functions either requires more or promises less than the corresponding specification in the base class.

One symptom of improper inheritance is that the user code may be forced to use dynamic typing (that is, runtime type checks, capability queries, and downcasting) and to treat objects of different derived classes in different ways (for example, to avoid using some member functions on some derived classes).

Improper inheritance has a nearly unbounded cost on the code that uses base class pointers and references (recall that this is, by design, the bulk of the application; see FAQ 2.24). The initial development cost is greater, since the code that uses base class pointers and references needs to be littered with runtime type checks and complex decision logic. The long-term maintenance costs are also greater: every time a new derived class is added, all the runtime type checks in all the user code must be reanalyzed to ensure that nothing will break.

Most of the benefits of OO technology vanish when improper inheritance is employed. In C++, public inheritance should be tied closely to subtyping, which means public inheritance has a strong semantic meaning. When public inheritance is used improperly, the software usually ends up being brittle and over budget and delivered late.

FAQ 7.04 Isn't the difference between proper and improper inheritance obvious?

Apparently not.

Improper inheritance is a very common design error. This seems to be because developers base inheritance relationships on their intuition rather than the objective criteria of substitutability.

The following inheritance relationships are improper because the derived class either requires more or promises less.

• A Stack is not a kind-of List (assuming List provides member functions that allow insertions in the middle of the list and Stack does not).

• A ListOfApples is not a kind-of ListOfFruit (assuming that any type of Fruit can be put into a ListOfFruit).

The following inheritance relationships may or may not be proper inheritance depending on the specified behavior of the base class and the derived class.

• A Circle may not be a kind-of Ellipse.

• An Ostrich may not be a kind-of Bird.

• An Integer may not be a kind-of RationalNumber.

These examples are explained in Chapter 8.

FAQ 7.05 Is substitutability based on what the code does or what the specification promises the code will do?

The specification, not the implementation.

The specification of an overridden member function must require no more and promise no less than the specification of the corresponding member function in the base class. The overriden member functions must also correctly implement whatever specifications they provide. However, when the base class gives an adaptable specification, the code of the override doesn't necessarily have to do the same as the code of the base class.

In the following example, Base::f() provides an adaptable specification: the code is more specific than the strict minimum guaranteed by the specification. The code of the derived class isn't substitutable with respect to the implementation of the base class, but it is substitutable with respect to the specification of the base class. Since the user code, sample(Base& b), relies only on the specification of Base::f() rather than the more specific implementation, this user code won't break when it is passed a Derived object. However, if the user code had relied on the implementation of Base::f(), it would break when passed a Derived object (it would also break if a legitimate, substitutable modification was made to Base::f(), such as returning 42).

image

image

Never assume that a class will always be implemented as it is currently implemented (see FAQ 6.02).

FAQ 7.06 Is it proper to revoke (hide) an inherited public: member function?

No.

Never. The rest of the software was written based on the promises made by the base class (see FAQ 6.02). Since the main idea of polymorphism and dynamic binding is to pass a derived class object to a function that takes a base class pointer or reference, this “rest of the system” often works with derived class objects without knowing it (see FAQ 2.24). Everything works fine as long as the derived class objects behave like the base class promises they will behave. However if the derived class is not substitutable for the base class, all that code that was written based on the base class's promises (the “rest of the system”) will break.

In order to be substitutable, the derived class object must support all the member functions of the base class. If even one of the member functions promised by the base class has been revoked (hidden) by a derived class, a large portion of the rest of the system could break.

Public inheritance requires keeping promises—and hiding or revoking promised behavior will surprise the rest of the system. Too often, programmers try to use inheritance even when it doesn't fit the constraints of substitutability. Typically this means creating a derived class, then trying to revoke (hide) whatever inherited public: member functions don't make sense in the derived class.

Revoking an inherited public: member function is evil.

Recall that this chapter covers the guidelines for public inheritance. private and protected inheritance are different: it is appropriate and proper to revoke (hide) inherited public: member functions in a private or protected derived class (see FAQ 37.01).

FAQ 7.07 What is specialization?

A major source of confusion and design errors.

Some people assume that proper inheritance can be determined by the vague concept of specialization. For example, if Derived is a special Base, some people assume that Derived can be derived properly from Base. While this simple rule works some of the time, it is incorrect often enough to be misleading. The guiding principle is for the derived class to behave like the base class, and specialization sometimes leads designers in the wrong direction.

One problem with the concept of specialization is that it can be ambiguous. Does it mean “better than” (JetPlane is a specialized Plane) or “more skilled” (Doctor is a specialized Person) or “subset of” (Circle is a specialized Ellipse) or “more specific” (Unix is a specialized Operating System) or “restricted” (a List that can only contain Circle objects is a specialized List that can contain any Shape)? Because of the potential ambiguity, it is hard to make a blanket statement about specialization as a reliable approach to public inheritance.

Not only is it ambiguous, but in certain cases it is completely incorrect. In particular, specialization does not imply that Derived must support all the operations defined by Base, as is necessary for proper inheritance.

Forget specialization and learn about substitutability.

FAQ 7.08 What do subsets have to do with proper inheritance?

Nothing.

Remember, the goal is to avoid breaking existing code that uses base class pointers and references. Since elements of a subset don't necessarily behave in a backward compatible way with respect to the superset, users might be surprised if they are passed an element of a subset, and all that code using base class pointers/references might break. It doesn't matter that intuition says otherwise; the fact remains that the whole subset notion is wrong often enough that it is not a reliable design principle. Consider the following two examples:

• The set of circles is a subset of the set of all ellipses, yet circles can't deform themselves asymmetrically, which may be a member function of class Ellipse (see FAQ 8.08).

• The set of ostriches is a subset of the set of all birds, yet ostriches cannot fly, which may be a member function of class Bird (see FAQ 8.04).

The root problem with the subsets-mean-subclasses idea is that subsets deal with values, where objects normally have mutative member functions (deform asymmetrically, fly, and so on).

Forget subsets and learn about substitutability.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset