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 ) and tl
(of type ).
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 .
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{}}
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 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 ,
we simply employ the conversion
operator for ,
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
for it
(which may concisely be written as )
results in a non-zero (pointer) value,
the (de-referenced) result will be written to
standard output.
When 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{}
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
and
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 instance variable it.
The actual work is done by listiter.
Its constructor takes a cell pointer,
which is given to it
when invoking the
conversion function.
To redirect the invocation of
to the operator()
function of the instance of listiter,
the this pointer is given to
when initializing
as the base class.
The implementation of the
function itself
is straightforward.
It delivers zero whenever the
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.