Inheritance of interface is different from inheritance of implementatino.
Under public inheritance, derived classes always inherit base class interfaces, but may act differently in terms of base class implementation inheritance:
- Pure virtual functions specify inheritance of interface only
- Simple (impure) virtual functions specify inheritance of interface plus inheritance of a default implementation
- Non-virtual functions specify inheritance of interface plus inheritance of a mandatory implementation
To show the implementation differences above, we can define following classes for an example:
|
|
Since derived classes Rectangle and Ellipse are both public inherited, according to item 32, this means is-a, so anything that is true of the base class must also apply to derived classes. Thus, the member function interfaces are always inherited.
Pure virtual functions
For pure virtual functions, there are two features we have to note:
- they must be redeclared by any concrete class that inherits them
- they typically have no definition in abstract classes
From these feature, we can conclude that:
The purpose of declaring a pure virtual function is to have derived classes inherit a function interface only.
Here, the declaration of Shape::draw says to the client of the Shape that, “you have to provide a draw function, but I don’t know how you’re going to implement it.”
Incidentally, C++ allows us to provide an implementation for Shape::draw (example: pure virtual destructor in item 7). However, the only way to call it would be to qualify the call with the class name:
|
|
This feature is generally of limited utility, except that it can be employed as a mechanism for providing a safer-than-usual default implementation for simple virtual functions as we’ll see below.
Simple (impure) virtual functions
Simple virtual functions provide an implementation that derived classes may override, which means that
The purpose of declaring a simple virtual function is to have derived classes inherit a function interface as well as default implementation.
For example, the declaration of error function tells us that “You have to supoort an error function, but if you don’t want to write your own, you can fall back on the default version in the Shape class.”
Potential danger
However, in the perspective of class design, there’s a potential danger to allow simple virtual functions to specify both a function interface and a default implementation. That is: a derived class is allowed to inherit the default implementation without explicitly saying that it wanted to. For example:
|
|
Suppose both ModelA and ModelB inherit the base class Airplane without re-implementing the simple virtual function fly. Chances are that ModelB is actually a new type of model, yet its programmer simply forgets to redefine the fly function.
Separate interface from default implementation
To make our design more foolproof, we may separate functions for providing interface and default implementation, such as below:
|
|
|
|
|
|
Take use of pure virtual function
Some people may feel this design is redundant, arguing that this will polllute the class namespace with a proliferation of closely related function names. Then we may take advantage of the fact that pure virtual functions, which insists on redeclaring in concrete derived classes, may also have implementations of their own:
|
|
|
|
|
|
In essence, this design breaks fly into two fundamental components:
fly’s declaration specifies its interface, which derived classes must usefly’s definition specifies its default behavior, which derived classes may use by explicitly request it
However, in merging fly and defaultFly, we have lost the ability to give the two functions different protection levels: previously protected code in defaultFly is now public in fly.
Non-virtual function
A non-virtual member function specifies an invariant over specialization (a point discussed in item 36), identifying behavior that is not supposed to change, no matter how specialized a derived class becomes. Thus:
The purpose of declaring a non-virtual function is to have derived classes inherit a function interface as well as a mandatory implementation.
The declaration for Shape::objectId is basically say, “Every Shape object has a function that yields an object identifier, and that identifier is always conputed the same way. That way is determined by the definition of Shape::objectID, and no derived class should try to chagne how it’s done.”
Summary
The differences in declarations for pure virtual, simple virtual and non-virtual functions allow us to specify with precision what we want derived classes to inherit: interface only, interface and a default implementation, or interface and a mandatory implementation, respectively.