Axel Rogat
Objektorientiertes Programmieren mit C++ und JAVA
 
16.1: Parametrisierte Funktionen Kapitel 16 16.3.1: Array-Klasse  
 
  16.2 Parametrisierte Klassen  
 

Genau wie für einzelne Funktionen kann man auch für ganze Klassen eine Schablone angeben, aus der bei Bedarf dann tatsächliche Klassen erzeugt werden. Diese Schablonen heißen auch templates, parametrisierte Klassen, generische Klassen.

Besonders naheliegend ist die Verwendung von Schablonen bei Klassen, die dazu dienen, Objekte in bestimmter Art und Weise zu verwalten -- etwa auf einem Stack, in einem Array, in einem Baum, etc., wobei es auf die Art der Objekte (fast) nicht ankommt.

Beispielsweise wollen wir unsere Stack-Klasse nicht nur zur Speicherung von int, sondern für beliebige Datentypen verwenden können.

Version 1: Wenn man wieder mit Präprozessor-Makros arbeiten möchte, muß man diesmal die gesamte Bibliothek in ein Makro packen!

#define MakeStackLib(type) \ struct element \ { \ type value; \ struct element *next; \ } \ ...
Wenn mehrere typparametrisierte Klassen zusammenarbeiten sollen oder voneinander erben sollen, gerät man so in unlösbare Schwierigkeiten.

Version 2: Eine andere Möglichkeit wäre es, in den Knoten nicht direkt den Typ zu speichern (z.B. int), sondern nur einen Pointer darauf. Als Typ für den Pointer könnte man void* verwenden und bei Bedarf in/aus dem richtigen Typ casten. Die Größe bytes des Typs im Speicher wird ein Member. Es müßte jedesmal Speicher für eine Kopie des Objekts angelegt werden, z.B. mit new char[bytes].

So wird leider jede Typüberprüfung ausgeschaltet. Wenn versehentlich Pointer auf andere Objekte als die geplanten an die Elementfunktionen wie push übergeben werden, wird Datenmüll gespeichert. Außerdem verlangsamt sich das Programm durch die doppelte Verzeigerung (neuer Speicher für den Knoten und für die eigentlichen Daten).

Version 3: Die Klassendefinition von früher, durch templates erweitert, sonst beibehalten (insbesondere ohne Copy-Konstruktor und ohne =), sieht wie folgt aus:

template <class T> struct element // parametrisierte Knoten-Struktur { T value; // eigentliche Daten: parametrisiert element<T> *next; // Zeiger auf den n[["]]achsten Knoten }; template <class T> class stack // parametrisierte Stack-Klasse { private: element<T> *first; // Daten-Member: parametrisierter Knoten public: stack(); bool isempty(); void push(T); // push mit T als Parameter void pop(); T top(); // top liefert T zur[["]]uck ~stack(); };
Vor den Klassennamen gehört die template-Angabe. Ein Objekt einer speziellen Instanz definiert man dann beispielsweise mit stack<int> is; oder stack<string> ss;

Code für eine spezielle Instanz wird funktionenweise erst dann vom Compiler erzeugt, wenn er im Quelltext das erste Mal benötigt wird. Möglicherweise werden also gar nicht immer alle Elementfunktionen übersetzt, insbesondere könnten Fehler längere Zeit gar nicht bemerkt werden.

Instanzen sind schachtelbar, beispielsweise

stack<stack<string> > sss;
(zwischen den beiden > muß Whitespace stehen, da der Compiler sonst einen Shift-Operator >> erkennen würde).

Der angegebene Typ T kann im Rumpf wie jeder andere Typ verwendet werden. Von ihm können weitere Typen abhängig gemacht werden, wie hier element<T> für die einzelnen Knoten der Liste.

Die Elementfunktionen sind alle automatisch durch T parametrisierte Funktionen -- daher entfällt innerhalb der Klassendefinition eine template-Angabe vor ihrem Prototypen. Werden die Funktionen außerhalb nachgetragen, muß das template explizit (wie bei einfachen typ-parametrisierten Funtktionen) angegeben werden. Ebenso ist mit stack innerhalb der Klassendefinition immer automatisch der durch T parametrisierte Typ gemeint, außerhalb muß explizit die template-Form verwendet werden.

top innerhalb der Implementationsdatei sieht beispielsweise so aus:

template <class T> T stack<T>::top() { if (isempty()) error(ERR_TOP_EMPTY); return first->value; }
Ein kurzes Beispielprogramm, das Stacks über zwei verschiedenen Typen benutzt, ist folgendes:
void main(void) { stack<int> s1; stack<teststruct> s2; int i; s1.push(1); s2.push(data[0]); s1.push(2); s2.push(data[1]); s1.push(3); s2.push(data[2]); s1.push(4); for (i=0;i<4;++i) { cout << s1.top() << " " << s2.top().name << endl; s1.pop(); s2.pop(); } }
Bei der Deklaration stack<int> s1; wird der Code für den entsprechenden Konstruktor der int-Instanz erzeugt, beim s2.push(data[0]) der Code für das push der teststruct-Instanz. Es wird kein Code für isempty erzeugt, für keine der beiden Klassen-Instanzen.

 
16.1: Parametrisierte Funktionen Startseite 16.3.1: Array-Klasse  
 

© 1998 Axel Rogat