Generic types

Instructor's Guide


intro polymorphism idioms patterns events summary, Q/A, literature
Reuse of software is one of the ways in which to produce reliable systems, that is, the reuse of reliable software. Many of the data types that are used to specify software systems have a lot in common. For instance, there is little difference between a stack of integers or a stack of strings, or a stack for any other data type. So it would be more convenient to have a generic stack, which may be used as or instantiated to a stack of integers, strings or whatever. Evidently, the various container types such as stack, list, bag and set are likely candidates for being realized by generic types.

Generic types

Generic types are essential for programming in the large
slide: Generic types

The C++ language supports two kinds of generic types, polymorphic (pointer) and parametrized. See slide 2-generic. In section inheritance we have already seen that an abstract or generic shape type may be specified and later refined into concrete types, giving rise to a polymorphic hierarchy. The base class from which this hierarchy is derived may be regarded as a constraint with respect to the possible element types of a generic container. A similar notion of type hierarchy is exploited in the solution of generic types involving void pointers. The use of void* was one of the two ways to deal with unconstrained generic types in C++ before templates were introduced in version 3.0. The other way was the use of equally error-prone macro definitions. The use of void* will be illustrated as a warning; this is how it should not be done. The preferred way in which to specify generic types in C++ is to employ the template construct, which allows the programmer to parametrize a class or function definition with a type parameter.

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.

Template classes

Template classes are supported only in the later releases of C++ (AT\&T 3.0, Zortech 3.1). Before the actual support of template classes, and even with the support for template classes available, programmers have extensively used the macro preprocessor to define generic classes. A disadvantage of such an approach is that it easily leads to deviant notation and the use of non-standard constructs or preprocessor facilities. Another important disadvantage of using the preprocessor is that there is no direct compiler support for checking whether a template has correctly been defined and instantiated. And similar objections hold equally as strong for defining constants, inline functions or template functions as macros. In slide 2-template, the example of a generic stack has been rewritten to employ template classes.
  template<class type>
  
  class stack  { 
\fbox{stack}
public: stack( int n ) { 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: Template class

One (subtle) difference between the previous definition, using void pointers, and the current definition that uses templates is that an instantiated template stack may hold objects (or references to objects), whereas the other version only allows the storage of pointers to objects. See section 2-references for a discussion on the difference between objects, references and pointers.

Another, very important, difference is illustrated by the code fragment showing the use of a (template) stack

  stack<char> s(100);
  s.push('+'); s.push('+'); s.push('c');
  while ( !s.empty() ) {
    		cout << s.pop();
    		}
  

When creating the stack, the user must explicitly indicate what type of elements the stack contains. Other uses of the stack do not require any explicit type conversions and are completely type checked by the compiler. Note that the template construct actually adds to the power of the language, since a stack of void pointers may easily be defined by giving the right instantiation parameters.

Using templates

A practical disadvantage of template classes is that they may result in generating lots of (instantiation) code. To avoid excessive code generation, we may employ generically typed wrapper classes providing safe access to an implementation employing void*. The rationale underlying such an approach is that we may use any implementation technique as long as we provide the user with a type-safe interface.

We will conclude this section on generic types by looking at a type-safe generic list class, employing void to represent the actual list structure. (This example involves some rather complex features of C++.)


@s lib/cell.s
slide: The cell representation of a list

An unconstrained, yet unsafe, implementation of a list is given by the definition of a cell, as depicted in slide 2-l-cell.
  template< class E > 
\fbox{list<E>}
class list { friend class listiter<E>; public: list() { c = 0; } ~list() { if(c) delete c; } void insert(const E& el); operator iter<E>() { return listiter<E>(c); }
(*)
private: cell* c; }; template< class E >
\c{\fbox{list<E>::insert}}
void list<E>::insert(const E& el) { void* x = (void*) ⪙ if (!c) c = new cell(x); else c->insert(x); }

slide: A template list wrapper

As an example of a generic type-safe wrapper class that may be used to access the list structure, look at the definition of the template list class given in slide 2-l-list. Apart from a constructor which initializes the inner cell to zero and a destructor which destroys the inner cell, the class interface for list defines the function insert, which is used to insert references to objects of type E, and a conversion operator that delivers an instance of class iter<E>, where E is the instantiation parameter type of the list. Instances of iter<E> may be used as an iterator giving access to the elements of the list (see below).

For inserting an element, we must convert the typed reference into a void pointer by taking the address of the argument of insert. We then create a new cell if the inner cell is still zero and employ cell::insert otherwise.


  template< class E >  
\fbox{iter}
class iter { public: iter(iter* x) : it(x) {} virtual E* operator()() { return (*it)(); }
\c{// indirect}
private: iter<E>* it; };

slide: The definition of iterators

Iterators provide a convenient method to access a variety of structures in a uniform way. In the literature various styles of iterators are employed, some using explicit first, next and exist functions and others using the more concise applicative notation, as used for defining the class iter given in slide 2-l-iter.

Below, an example is given of how an iterator may be used to traverse the list

  void main() {
  list<int> lst;
  lst.insert(1); lst.insert(2);
  iter<int> it = lst; 
\c{// get the iterator}
int* p = 0;
start

while ( p = it() ) { cout << "item;" << *p << endl;
\c{// take the value}
} }
Note that to obtain an iterator, that is an instance of iter, we simply employ the conversion operator for iter, which is automatically applied when assigning the list to it.

To obtain the elements of the list, a pointer to int is initialized to zero. As long as invoking iter::operator() for it (which may concisely be written as it()) results in a non-zero (pointer) value, the (de-referenced) result will be written to standard output. When it() produces a zero pointer value, we have reached the end of the list.


template< class E >  
\fbox{listiter}
class listiter : public iter<E> { public: listiter( cell* c ) : iter<E>(this), p(c) {}
virtuality
~listiter() { cout << "~listiter" << endl; } E* operator()();
the iterator function
private: cell* p; };

The operator() function

template< class E > 
\fbox{listiter::operator()}
E* listiter<E>::operator()() { void* x = p?p->el:0; if (p) p = p->next; return (E*) x;
conversion to (E*)
}

slide: The listiter class

The actual definition of both iter and listiter is somewhat complicated due to the fact that C++ employs dynamic binding only when virtual members are invoked through pointers or references.

The constructor for iter expects an instance of iter as a parameter. The iter::operator() function in its turn invokes the operator() function for the iter* instance variable it.

The actual work is done by listiter. Its constructor takes a cell pointer, which is given to it when invoking the list::operator iter conversion function. To redirect the invocation of iter::operator() to the operator() function of the instance of listiter, the this pointer is given to iter when initializing iter as the base class.

The implementation of the listiter::operator() function itself is straightforward. It delivers zero whenever the cell* instance variable is zero. Otherwise it delivers the element of the cell, which is converted to the proper type and sets the cell pointer to the next element in the list.