Inheritance

Inheritance is perhaps the most distinct feature of object-oriented programming. Pragmatically, from a software engineering perspective, inheritance provides a mechanism for code sharing and code reuse. From a type theoretical point of view, inheritance is one of the mechanisms supporting polymorphism. Operationally, the power of inheritance in C++ comes from the use of virtual functions and dynamic binding.

Abstract classes

The classical example to demonstrate the use of inheritance and the virtues of dynamic binding is a hierarchy of shapes. The hierarchy of shapes consists of an abstract shape from which concrete shapes, such as a circle and a rectangle, may be derived. When deriving concrete shapes, the programmer merely has to provide the appropriate constructors and define the actual method for displaying the shape. An abstract shape is defined as in slide 2-shape.
  class shape { 
\fbox{shape}
public: shape(int x = 0, int y = 0) : _x(x), _y(y) { } void move(int x, int y ) { _x += x; _y += y; } virtual void draw() = 0;
// pure virtual
protected: int _x, _y; };

slide: Abstract shape

A shape, viewed as an abstract entity, contains data members for its origin, and further must provide, apart from a constructor, the methods for moving and drawing a shape. The abstract class shape defines a constructor which sets the origin to $(0,0), unless other values have been provided. The member function move may be implemented for all shapes as simply changing the origin in an appropriate way. On the other hand, drawing a shape is undefined for an abstract shape. For this reason the member function draw is declared as pure virtual, meaning that it must be redefined by a class derived from the class shape. A class with pure virtual functions is an abstract class. An abstract class can have no instances. For example
shape s; 
// error: abstract class
would result in a compiler error. Having an abstract class shape available, we may define concrete shapes, such as circle and rectangle, as in slide 2-concrete.
  class circle : public shape {  
\fbox{circle}
public: circle( int x, int y, int r) : shape(x,y), _radius(r) { } void draw() { cout << "C:" << _x << _y << _radius; } protected: int _radius; }; class rectangle : public shape {
\fbox{rectangle}
public: rectangle( int x, int y, int l, int r ) : shape(x,y), _l(l), _r(r) { } void draw() { cout << "R:" << _x << _y << _l << _r; } protected: int _l,_r; };

slide: Concrete shapes

For a circle we need to define, apart from its origin, a radius. And, similarly, for a rectangle we need to define the length of the sides. Both circle and rectangle inherit the origin and the member function move from the shape class. Instantiating the inherited part takes place, as indicated after the colon, before evaluating the function body of the constructor. Unlike the initialization of instance variables, which may be assigned a value in the body of the constructor, the initialization of the inherited parts must be done in this way. An explicit initializer is required unless a default constructor is available. The difference between the initialization of a data member immediately after the colon or in the function body of the constructor is quite subtle. In the latter case, a default constructor will be applied to create the data member and the subsequent assignment in the function body may lead to the creation of another instance. Generally, it is safer and more efficient to initialize data members immediately after the colon. Unfortunately, it is not always possible to initialize data in the colon-list. Also, there is no way in which to communicate between the initializers, which may result in repeated computations when there is a dependency between the initial values of the data members. A concrete shape class must necessarily (re)define the member function draw, since an abstract shape cannot possibly know how to draw itself.

A code fragment illustrating the use of concrete shapes looks as follows:


  circle c(1,1,2); rectangle r(2,2,1,1);
  
  c.draw(); r.draw();
  
Note that calling draw is for both kinds of shapes the same. The difference between the two distinct shapes, however, becomes visible when calling the function draw. The function draw specified for circle overrides the specification given for the abstract shape, and similarly for rectangle.

Dynamic binding

The reuse of code is one of the most important aspects of inheritance. The principle underlying the efficient reuse of code (by employing inheritance) may be characterized as {\em "programming by stating the difference,"} which means that one has to (re)define the features of the derived class that are added to or different from what is provided by the base class. To fully exploit this principle we need virtual functions, that is functions for which dynamic binding applies. Operationally, dynamic binding may be regarded as a dispatching mechanism that acts like a case statement to select (dynamically) the appropriate procedure in response to a message. In many procedural programs, such a case statement often occurs (explicitly) when a kind of polymorphism is introduced by means of an explicit tag (as, for example, in combination with a union or a variant-record). The use of such tags may become a nightmare when modifying the informal type system, since each case statement then needs to be updated. Using inheritance with dynamic binding, such case statements are, so to speak, implicitly inserted by the compiler or interpreter. The obvious advantage of such a feature, apart from reducing the amount of code that must be written, is that maintenance is greatly facilitated. A possible disadvantage, however, might be that program understanding becomes more difficult since many of the choices are now implicitly made by the dispatching mechanism instead of being written out explicitly.

To illustrate the power of virtual functions (and dynamic binding) we will add a compound shape to our hierarchy of shapes. See slide 2-compound.


  
  
  
class compound : public shape { 
\ifsli{}{\fbox{compound}
public: compound( shape* s = 0 ) : fig(s) { next = 0; } void add( shape* s ) { if (next) next->add(s); else next = new compound(s); } void move(int x, int y) { if (fig) fig->move(x,y); if (next) next->move(x,y); } void draw() { if (fig) fig->draw(); if (next) next->draw(); } private: shape* fig; compound* next; };

slide: Compound shapes

A compound shape is actually a linked list of shapes. To add shapes to the list, the class compound extends the class shape with a member function add. Both the member functions move and draw are redefined in order to manipulate the list of shapes in the appropriate way. The list is traversed by recursively invoking the function for the objects stored in the next pointer unless next is empty, which indicates the end of the list. The class compound is made a subclass of shape to allow a compound shape to be treated as a shape.

As an example of the use of a compound shape, consider the following fragment:

compound s;
  s.add( new circle(1,1,2) );
  s.add( new rectangle(2,2,3,5) );
  s.draw(); s.move(7,7); s.draw();
  
After creating an empty compound shape, two shapes, respectively a circle and a rectangle, are added. The compound shape is asked to draw itself, it is moved, and then asked to draw itself again. The compound shape object, when moving and drawing the list of shapes, has no knowledge of what actual shapes are contained in the list, which may be compound shapes themselves. This illustrates how we may achieve polymorphic behavior by using inheritance.

A more explicit example of the polymorphic behavior of shapes is given by the following code fragment.

shape* fig[3];
  fig[0] = &s; 
the compound shape
fig[1] = new circle(3,3,5); fig[2] = new rectangle(4,4,5,5); for( int i = 0; i < 3; i++ ) fig[i]->draw();
After storing some actual shapes, including a compound shape, in an array of (pointers to) shapes, a simple loop with a uniform request for drawing is sufficient to display all the shapes contained in the array, independent of their actual type.

This example is often used to demonstrate that when adopting an object-oriented approach the programmer no longer needs to include lengthy case statements to choose between the various drawing operations on the basis of an explicit type tag.

The careful reader may have noted that the absence of the declaration virtual for the member function move may lead to problems. Indeed, this leads to erroneous behavior since moving only the origin of the compound shape will not do. In our slightly wasteful implementation of a compound shape, the member variables inherited from shape play no role. Instead, each shape in the list must be moved. This could be repaired either by declaring the function shape::move as virtual or by redefining compound::draw and eliminating compound::move. This illustrates that it takes careful consideration to decide whether or not to make a member function virtual. Some even suggest making member functions virtual by default, unless it is clear that they may be declared non-virtual.

Multiple inheritance

Graphical shapes are a typical example of objects allowing for a tree-shaped taxonomy. Sometimes, however, we wish to define a class not from a single base class, but by deriving it from multiple base classes, by employing multiple inheritance.
class student { ... };
  class assistant { ... };
  
  class student_assistant
  		: public student, public assistant {
  public:
  student_assistant( int id, int sal ) 
  		: student(id), assistant(sal) {}
  };
  

slide: Multiple inheritance

In slide 2-multi-1, one of the classical examples of multiple inheritance is depicted, defining a student_assistant by inheriting from student and assistant.

Dynamic binding for instances of a class derived by multiple inheritance works in the same way as in the case of single inheritance. However, ambiguities between member function names must be resolved by the programmer.


class person { };
  class student : virtual public person { ... }
  class assistant : virtual public person { ... }
  
  class student_assistant
  	: public student, public assistant { ... };
  

slide: Virtual base classes

When using multiple inheritance, one may encounter situations where the classes involved are derived from a common base class, as illustrated in slide 2-multi-2.

To ensure that {\em student_assistant} contains only one copy of the person class, both the student and assistant classes must indicate that the person is inherited in a virtual manner. Otherwise, we may not have a declaration of the form

  person* p = new student_assistant(20,6777,300);
  
since the compiler would not know which person was meant (that is, how to apply the conversion from {\em student_assistant} to person).