DOC HOME SITE MAP MAN PAGES GNU INFO SEARCH PRINT BOOK
 
The Design of C++ Standard Components

Two uses of inheritance

This section describes two ways our components use inheritance. One is an internal use that is invisible to clients; we use this strictly for efficiency. The other is visible at the component interface; we use this to solve certain problems in the type system.

Using inheritance to factor out common code

When developing a container class, it is tempting to put all the implementation code into the ``implement'' macro. However, a little forethought will reveal that much of this code is independent of the type parameter. This type-independent code can be factored out and placed in a library. Derivation can be used as the factoring mechanism.

The List(3C++) component, for example, provides the template class List<T>, where T is the type parameter. The List.h header file, however, defines two List classes: (the actual names have been simplified here):

       List.h
           class List_base {
           protected:
               internal List functions that do not depend on T
           public:
               client-visible List functions that do not depend on T };
           template <class T> class List : public List_base {
           public:
               functions that depend on T        };
           inline functions
   

List.c client-visible List template functions that depend on T

List1.c internal List functions that do not depend on T

As indicated, the List_base member functions are implemented in one or more .c files; the corresponding object files are placed in a library. The client-visible List template functions are implemented in file List.c which is placed in the same directory as List.h for template instantiation reasons.

This use of inheritance is also profitable with pointer containers (for example, List_of_p). A pointer container is little more than a ``casted'' version of a container of void*. For example:

       List.h
           typedef void* voidP;
           template <class T> class List_of_p : public List<voidP> {
               List_of_p<T>& put(const T *t) {
                   return (List_of_p<T>&)
                         List<voidP>::put((voidP)t);
               }
               T* get() {
                   return (T*)List<voidP>::get();
               }
               ...
           };

Notice that the put and get functions simply call the base class functions, casting the type of the pointer appropriately. The same is true of all the other List_of_p functions, both inline and non-inline.

This implementation strategy results in extremely little code being generated for pointer containers, since they all share the implementation of the base container (List<voidP> in this example). The latter itself comes pre-instantiated in the library .o files, as a convenience.

Using inheritance to overcome problems in the type system

The second way we use inheritance is to allow iterators to work properly for both constant and non-constant containers. Iterators typically provide a means not only of visiting the elements of a container, but also of modifying those elements as the iteration progresses. First consider what happens if we allow iterators to be created only for non-constant objects:

       List.h
           template <class T> class Listiter {
           public:
               Listiter(List<T>& x);
               ...
           };
   

client.c void reasonable(const List<int>& x) { Listiter<int> i(x); // compiler error while (i.step_next()) { ... } }

Allowing iterator creation for constant containers, on the other hand, opens a loophole in the type system, since constant containers can be modified through the iterator:

       List.h
           template <class T> class Listiter {
           public:
               Listiter(const List<T>& x);
               ...
           };
       client.c        void sneaky(const List<int>& x) {
               Listiter<int> i(x);
               while (i.step_next())
                  i.remove_prev();    // compiler does not
                                      // warn
           }

The way out of this dilemma is to define a base iterator class that does not allow modification of the container, and a derived class that does:

       List.h
           template <class T> class Const_Listiter {
           public:
               Const_listiter(const List<T>& x);
               functions that do not modify the List        };
   

template <class T> class Listiter : public Const_listiter<T> { public: Listiter(List<T>& x);

functions that modify the List };

Now the client can do reasonable things (like iterate over a constant List) while being prevented from doing sneaky things (like trying to breach the type system):

       client.c
           void reasonable(const List<int>& x) {
               Const_listiter<int> i(x);
               while (i.step_next()) {
                   ...
               }
           }
           void sneaky(const List<int>& x) {
               // compiler error (no such constructor)
               Listiter<int> i(x);
               while (i.step_next())
                   i.remove_prev();
           }

Next topic: Conclusions
Previous topic: Inheritance: why we have mostly avoided it

© 2004 The SCO Group, Inc. All rights reserved.
UnixWare 7 Release 7.1.4 - 27 April 2004