Base class hierarchies

Inheritance allows the programmer to define a class hierarchy which corresponds to a hierarchy of (polymorphic) pointer types. In particular, the root class (at the top of the hierarchy) corresponds to a pointer type that acts as a generic type with respect to the pointer types corresponding with the other classes in the hierarchy. In addition, the root class specifies an interface common to all classes lower in the hierarchy.

Class hierarchies

  class shapelist { 
\fbox{shapelist}
public: shapelist(shape* el=0, shapelist* sl=0) : hd(el), tl(sl) { } shapelist* insert(shape* el) { require( el );
el must exist
if (!hd) hd = el; else return new shapelist(el,this); } shape* head() { return hd; } shapelist* tail() { return tl; } private: shape* hd; shapelist* tl; }

slide: Base class polymorphism

The polymorphic property of (base class) pointers allows for the definition of a generic container for element types ranging over the pointer types corresponding to the descendants of the base class.

As an example, look at the shapelist defined in slide 2-base. The class shapelist declares instance variables hd (of type shape*) and tl (of type shapelist*). The insertion of a new element results in the creation of a new shapelist which has the element as a head and the original list as its tail. Note that the head of each list is a pointer to an instance of shape or one of its derived classes. A pointer is needed since the size of the actual object will vary depending on the actual type of the object inserted. For example, a circle extends a shape by including an additional instance variable giving its radius.

Employing base class hierarchies offers a safe, yet limited, means by which to define generic (container) types. (Limited, because the base class imposes constraints on the actual types for which the generic data type may be used.)

The $void$ pointer

Employing void pointers allows for the definition of unconstrained generic types, although in a rather crude way. To understand how that works, it is necessary to reflect on the meaning of a type system. One important aspect of a type system is that it may protect the programmer from a number of common errors, ranging from trivial typos to inconsistent structures. In particular, when the type system supports subtyping, the compiler may check whether the actual relation satisfies the subtyping relation. A type gives information concerning the object to which a variable or expression refers. The more specific a type, the more a compiler needs to know in order to assist the programmer in specifying correct programs. From this perspective, the void pointer figures as the top of the type hierarchy, since the compiler cannot be assumed to have any knowledge concerning its (correct) use. Consequently, the compiler leaves the responsibility entirely to the user, who may convert the void pointer to any type at will. Because of this absence of type information, the void pointer may indeed be called a generic type. An example of employing the void pointer to define a generic stack is given in slide 2-void.
  typedef void* type;  
generic void*
class stack {
\fbox{stack}
public: stack( int n = 12 ) { top = -1; impl = new type[n]; } ~stack() { delete[] impl; } bool empty() { return top == -1; } void push( type it ) { impl[++top] = it; } type pop() { return impl[top--]; } private: int top; type* impl; };

slide: Using the void pointer

For storing the contents of the stack, an array of void pointers is created when evaluating the constructor, which is deleted when the destructor is called. Provisions for dynamically enlarging the size of the stack and for testing its bounds have been omitted, but this is easily provided for.

An example of using the stack may look as follows:


  stack s(100);
  char plus = '+'; char c = 'c';
  s.push(&plus); s.push(&plus); s.push(&c);
  while ( !s.empty() ) {
    		cout << *(char*) s.pop();
    		}
  
To retrieve a value, first the pointer must be cast to a pointer of the appropriate type, and then it may be de-referenced to deliver the actual value. This code clearly illustrates that the user is entirely responsible for correctly using the stack. Now when we look at the code, to push elements on the stack, it is sufficient to take the address of the value inserted. However, when removing elements from the stack, the user must know precisely what the type of the element popped is. In the example, this first requires the conversion of the void pointer to a char pointer, and then a de-reference with an explicit cast to char. Evidently, generic types of this kind are error-prone, not to say ugly.