C++ Kurs

Klassen-Templates

Die Themen:

Wurden im vorherigen Kapitel die Funktions-Templates behandelt, so werden wir uns nun die Klassen-Templates ansehen.

Und auch dazu gleich wieder ein Beispiel. Nachfolgend werden zwei Klassen ShortStack und WinStack definiert, die beide das Verhalten eines Stacks implementieren. Der einzige Unterschied zwischen den beiden Klassen liegt nur im Datentyp des Zeigers auf die Stackdaten. Im ersten Fall ist dies ein short-Zeiger und im zweiten Fall ein Zeiger auf Win-Zeiger. Im Konstruktor der jeweiligen Klasse wird wie gewohnt dynamisch ein Feld mit dem Datentyp der Stackdaten angelegt.

PgmHeader// Stack für short-Werte
class ShortStack
{
    short *pData;
    ....
  public:
    ShortStack(int size)
    {
        pData = new short[size];
        ....
    }
};
// Stack für Zeiger auf Win-Objekte
class WinStack
{
    Win **pData;
    ....
  public:
    WinStack(int size)
    {
        pData = new Win*[size];
        ....
    }
};

Da aber die Memberfunktionen zum Ablegen der Daten auf dem Stack (Memberfunktion Push(...)) und zum Auslesen der Daten (Memberfunktion Pop(...)) in ihrer Funktionalität gleich sein werden, bietet es sich an, hierfür ein Klassen-Template zu entwickeln. Im Folgenden werden wir nun eine allgemein gültige Klasse für einen Stack entwickeln.

HinweisWenn Sie bis hierher den Kurs durchgearbeitet haben, so sollten wissen, dass die Standard-Bibliothek bereits eine solche Klasse enthält. Die hier entwickelte Stack-Klasse dient daher nur zur Übung.

Definition des Klassen-Templates

Auch hier verläuft die Entwicklung eines Klassen-Templates zunächst analog zur Entwicklung eines Funktions-Templates.

Schritt 1:

Im ersten Schritt werden alle Klassendefinitionen bis auf eine entfernt und die Datentypen, die von Klasse zu Klasse unterschiedlich sind, durch einen beliebigen Namen, dem formalen Datentyp, ersetzt (der natürlich aber kein Schlüsselwort sein darf). Im Beispiel wurden die Datentypen wieder durch T ersetzt. Fast von selbst versteht es sich, dass dann auch in den allen Memberfunktionen die unterschiedlichen Datentypen durch den formalen Datentyp ersetzt werden müssen. So wurde beim Aufruf des new Operators im Konstruktor ebenfalls der formale Datentyp T eingesetzt.

PgmHeader// Ersetzen der verschiedenen Datentypen
// Allgemeine Stackklasse

class Stack
{
    T *pData;
    ....
  public:
    Stack(int size)
    {
        pData = new T[size];
        ....
    }
};

Schritt 2:

Im zweiten Schritt müssen wir auch hier wieder dem Compiler etwas helfen. Damit er weiß, dass T nur ein formaler Datentyp ist, wird vor die Klassendefinition wiederum die Anweisung

template <typename T>

gestellt. Auch hier könnte, wie bei den Funktions-Templates, das Schlüsselwort typename durch das ältere Schlüsselwort class ersetzt werden.

PgmHeader// Spezifikation des formalen Datentyps
template <typename T>
class Stack
{
    T *pData;
    ....
  public:
    Stack(int size)
    {
        pData = new T[size];
        ....
    }
};

Und damit ist die Definition des Klassen-Templates im Prinzip auch schon vollständig.

Formaler Datentypen in der Signatur von Memberfunktionen

Der formale Datentyp kann auch als Parameter oder als Returntyp von Memberfunktionen auftreten. So erhalten die Memberfunktionen Push(...) und Pop(...) eine Referenz auf den abzulegenden bzw. auszulesenden Wert.

PgmHeadertemplate <typename T>
class Stack
{
    T *pData;
    ....
  public:
    Stack(int size)
    {
        pData = new T[size];
        ....
    }
    bool Push(const T& val);
    bool Pop(T& val);
};
AchtungBeachten Sie besonders bei Memberfunktion im Zusammenhang mit Klassen-Templates, dass Sie Parameter entweder über Zeigern oder als Referenzen übergeben sollten. Vermeiden Sie nach Möglichkeit die direkte Übergabe eines Werts. Bei der Übergabe eines Objekts werden sonst unter Umständen relativ viele Daten kopiert.

Definition von Memberfunktionen

Womit wir schon beim nächsten Thema sind, der Deklaration bzw. Definition von Memberfunktionen, die formale Datentypen als Parameter oder als Returntyp besitzen. Und hierbei muss unterschieden werden, ob eine Memberfunktion innerhalb oder außerhalb der Klasse definiert wird.

HinweisBeachten Sie, dass die Memberfunktionen bis zu diesem Zeitpunkt durch den Compiler nur 'vorläufig' definiert werden können, da sie ja noch formale Datentypen besitzen. Erst wenn eine Template-Klasse instanziiert wird, werden auch die entsprechenden Memberfunktion angelegt.

Definition von Memberfunktionen innerhalb der Klasse

Werden Memberfunktionen innerhalb der Klasse definiert, so erfolgt die Definition der Memberfunktion wie gewohnt.

PgmHeadertemplate <typename T>
class Stack
{
    T *pData;
    ....
  public:
    Stack(int size)
    {
        ....
    }
    bool Push(const T& val)
    {
        pData[sIndex++] = val;
        ....
    }
    bool Pop(T& val)
    {
        val = pData[--sIndex];
        ....
    }
};

Und noch ein Hinweis: Soll die dargestellte Klasse Stack auch Objekte verarbeiten können, so sollte die Klasse der Objekte u.a. den Zuweisungsoperator '=' definieren, da in den Memberfunktionen Push(...) und Pop(...) Objektzuweisungen stattfinden!

Definition von Memberfunktionen außerhalb der Klasse

Werden Memberfunktionen außerhalb der Klasse definiert, so ist eine auf den ersten Blick etwas verwirrende Definition erforderlich. Die allgemeine Syntax für die Definition einer Memberfunktion eines Klassen-Templates außerhalb der Klasse lautet:

template <typename T> RTYP CLASS<T>::MNAME(....)

T ist wieder der formale Datentyp, RTYP der Returntyp der Memberfunktion, CLASS der Name des Klassen-Templates und MNAME schließlich noch der Name der Memberfunktion. Das nachfolgende Bespiel zeigt die Definitionen der Memberfunktionen Push(...) und Pop(...) der vorherigen Klasse Stack.

PgmHeadertemplate <typename T>
bool Stack<T>::Push(const T& val)
{....}
template <typename T>
bool Stack<T>::Pop(T& val)
{....}

Es versteht fast von selbst, dass die Memberfunktionen im Klassen-Template natürlich deklariert sein müssen.

Überschreiben von Template-Memberfunktionen

Steigern wir den Schwierigkeitsgrad nun langsam. Wie bei Funktions-Templates können für bestimmte Datentypen die Memberfunktionen eines Klassen-Templates ebenfalls überschrieben werden. Wie dies geht ist nachfolgend dargestellt. Dort werden die Memberfunktionen Push(...) und Pop(...) des Klassen-Templates Stack überschrieben, um auf dem Stack auch char-Zeiger (für C-Strings) abzulegen. Soll ein C-String mittels Push(...) auf dem Stack abgelegt werden, so wird eine Kopie des C-Strings erzeugt und letztendlich der Zeiger auf diese Kopie auf dem Stack abgelegt. Beim Auslesen des C-Strings mittels Pop(...) erhält die Applikation dann den Zeiger auf diese Kopie zurück. Die Applikation ist nun aber auch dafür verantwortlich, dass der Speicherplatz für den zurückgelieferten String irgendwann freigegeben wird. Ein solches Verhalten muss natürlich sehr gut dokumentiert werden damit keine "Speicherlöcher" (memory leaks) entstehen. Außerdem darf z.B. an die Memberfunktion Pop(...) nur ein char-Zeiger übergeben werden, der auf keinen Fall auf einen bereits reservierten Speicherbereich verweisen darf. Da der Zeiger von Pop(...) überschrieben wird, könnte danach nicht mehr der ursprünglich reservierte Speicherbereich freigeben werden.

PgmHeader// Stack-Memberfunktionen für die Ablage von C-Strings
template<>
bool Stack<const char*>::Push(const char* &ptr)
{
    pData[sIndex] = new char[strlen(ptr)+1];
    strcpy(pData[sIndex],ptr);
    sIndex++;
    ....
}
template<>
bool Stack<const char*>::Pop(const char* &ptr)
{
    sIndex--;
    ptr = pData[sIndex];
    ....
}
AchtungBeachten Sie bitte die Datentypen der Parameter! Da Push(...) und Pop(...) laut Deklaration Referenzen erwarten, müssen an diese Memberfunktionen Referenzen auf char-Zeiger übergeben werden. Ferner müssen Sie bei einer solchen Lösung auch beachten, dass eventuell nicht alle Werte vom Stack geholt werden. Sie müssen zusätzlich noch einen eigenen Destruktor Stack<char*>::~Stack() erstellen, um den Speicher für die nicht abgeholten C-Strings freizugeben.

Definition von Objekten

HinweisWenn Sie aus der Einführung der Standard-Bibliothek noch wissen, wie Objekte von Klassen-Templates definiert werden und dass ein Template auch mehrere formale Datentypen besitzen kann, dann können Sie gleich zu den non-type Parameter übergehen.

Nach dem wir uns angesehen haben, wie Klassen-Templates definiert werden, wollen wir jetzt an die Definitionen von Objekten von Klassen-Templates gehen. Unten ist "zur Auffrischung" die Definition eines Objekts der normalen Klasse Stack zur Ablage von short-Werten dargestellt. Der Konstruktor der Klasse erhält als Parameter die Anzahl der maximal auf dem Stack abzulegenden Werte.

PgmHeader// Bisherige Definitionen:
// Klassendefinition

class Stack
{
    short *pnData
  public:
    Stack(int size);
    ....
};

// Objektdefinition
Stack myStack(10);

Sehen wir uns jetzt die Definition eines entsprechenden Objekts eines Klassen-Templates an. Da bei der Definition des Klassen-Templates nur der formale Datentyp angegeben wurde, muss nun bei der Definition des Objekts der tatsächlich zu verwendende Datentyp nach dem Klassennamen in spitzen Klammern angegeben werden. Im Beispiel wird somit zuerst ein Stack Objekt für die Aufnahme von 10 long-Werten definiert und danach ein weiteres Stack Objekt für die Aufnahme von 50 char-Zeigern.

PgmHeader// Definition eines Objekts eines Klassen-Templates:
// Template-Definition

template <typename T>
class Stack
{
    T *pData;
  public:
    Stack(int size);
    ....
};

// Objektdefinition + Template-Instanziierung
Stack<long> longStack(10);
Stack<const char*> charStack(50);

Im Beispiel zu diesem Kapitel können Sie sich dann die vollständige Implementierung des Klassen-Templates Stack nachher einmal ansehen.

Mehrere formale Datentypen

Machen wir jetzt den nächsten Schritt. Genauso wie Funktions-Templates nicht nur einen formalen Datentyp besitzen können, können auch Klassen-Templates mehrere formale Datentypen besitzen. Die formalen Datentypen werden dann bei der Definition des Klassen-Templates einfach aufgelistet. Dabei ist aber zu beachten, dass das Schlüsselwort typename vor jedem formalen Datentyp anzugeben ist. Im Beispiel enthält das Klassen-Template MyClass die beiden formalen Datentypen T1 und T2. Und selbstverständlich muss dann bei der Definition eines Objekts des Klassen-Templates auch die entsprechende Anzahl von Datentypen angegeben werden.

PgmHeader// Template-Definition
template <typename T1, typename T2>
class MyClass
{....};
// Objektdefinitionen
MyClass<int, const char*> myObj1;
MyClass<float, double> myObj2;

Non-type Parameter

Erweitern wir die Klassen-Templates noch etwas. Bisher enthielt die Klassen-Templates als Template-Parameter nur formale Datentypen. Klassen-Templates können aber auch sogenannte non-type Parameter erhalten, welche als eine Art von Konstanten fungieren, die dem Klassen-Template mit auf den Weg gegeben werden können. Im Beispiel unten ist einen Auszug aus einer erweiterten Version der Klasse SArray dargestellt, die ein Safe-Array implementiert. Was ein Safe-Array ist, wurde in einem Beispiel im Kapitel Überladen von speziellen Operatoren beim Überladen es Index-Operators erklärt. Das Klassen-Template erhält jetzt im zweiten Parameter die Größe des anzulegenden Feldes im Safe-Array. Innerhalb der Klasse wird dieser zweite Parameter wie eine Konstante behandelt, d.h. der Wert dieses Parameters kann nicht mehr verändert werden. Für solche non-type Parameter sind nur die folgenden Datentypen zugelassen: ganzzahlige Konstante oder Literal, ein weiterer non-type Template-Parameter, eine Funktion, ein Objekt, die Adresse einer Funktion oder eines Objekts oder ein Memberzeiger. D.h. es kann keine Gleitkommazahl oder gar einen char-Zeiger als non-type Parameter eingesetzt werden.

PgmHeader// Definition Klassen-Template
template <typename T, int SIZE>
class SArray
{
    T pData[SIZE];     // Datenfeld
  public:
    ....
    T& operator[] (int index);
};
// Überladener Indexoperator
template <typename T, int SIZE>
T& SArray<T, SIZE>::operator [](int index)
{
    ....
    // Falls Index ueber das Feld hinausgreift
    if (index>=SIZE)
    {
        .....
    }
    ....
}
// Objektdefintion
SArray<short,10> myArray;

Sehen Sie sich auch die Definition des überladenen Indexoperators [ ] an. Auch hier muss nun in der Template-Anweisung der non-type Parameter ebenfalls mit angegeben werden. Innerhalb der Operator-Memberfunktion wird dieser non-type Parameter dann zur Prüfung verwendet, ob über das Feld hinaus zugegriffen werden soll.

Ebenfalls geändert hat sich auch die Definition eines Objektes des Klassen-Templates, da auch hier der non-type Parameter mit angegeben werden muss.

HinweisSie können für diesen non-type Parameter auch einen Defaultwert vorgeben. Dann sieht die Definition des Klassen-Templates wie folgt aus:

template <typename T, int SIZE=5> class SArray
{....};

Damit können Sie dann ein Objekt wie folgt definieren:

SArray<float> myFloatArray;

In diesem Fall wird ein Safe-Array für 5 float-Werte erstellt.

 

Default-Datentyp

Dass Funktionen und Memberfunktionen Parameter mit Defaultwerten besitzen können, sollte in der Zwischenzeit geläufig sein. Und prinzipiell das Gleiche gilt auch für Klassen-Templates, nur dass hier nicht ein Defaultwert vorgegeben wird sondern ein Default-Datentyp. Dazu wird nach dem formalen Datentyp der Zuweisungsoperator angegeben und anschließend der Default-Datentyp. Wird dann bei der Definition eines Objekts des Klassen-Templates kein Datentyp explizit angegeben, so wird der Default-Datentyp verwendet. Im Beispiel wird zuerst ein Stack für int-Daten erzeugt und dann ein Stack für float-Daten.

PgmHeader// Definition des Klassen-Templates
template <typename T=int>
class Stack
{
    ....
};
// Definition von Objekten
Stack<> intStack;
Stack<float> floatStack;

Damit haben wir die Grundlagen von Klassen-Templates kennengelernt. Nicht verschwiegen werden soll an dieser Stelle, dass mit Klassen-Templates noch wesentlich mehr möglich ist. So können innerhalb eines Klassen-Templates weitere Klassen-Templates definiert werden oder auch Klassen-Templates für bestimmte Datentypen explizit definiert werden. Diesen Template-Besonderheiten sind später noch zwei weitere Kapitel gewidmet.

using-Anweisung und Templates

Mit Hilfe der using-Anweisung ist es auch möglich, Synonyme für Templates zu definieren. Auch dies lässt sich am besten wieder anhand eines Beispiels veranschaulichen.

PgmHeader// Definition Klassen-Template
template <typename T, int SIZE>
class SArray
{
    ....
};

// Synonyme definieren
template <int S>
using shortArray = SArray<short,S>;    // SArray fuer short Werte
template <int S>
using longArray = SArray<long,S>;      // SArray fuer long Werte

// Objektdefintion

shortArray<10> array1;                 // Array fuer 10 short Werte
longArray<50> array2;                  // Array fuer 50 long Werte

Extern und Klassen-Templates

Genauso wie bei den Funktions-Templates instanziiert der Compiler defaultmäßig erst dann ein Klassen-Template, wenn ein entsprechendes Objekt definiert wird. Dieses Verhalten führt nun aber dazu, dass so ohne Weiteres keine Bibliothek auf Basis von Templates aufgebaut werden könnte, da ein Objekt des Klassen-Templates ja erst in der Anwendung erzeugt wird.

Aber es gibt eine Lösung für dieses Problem. Hierbei wird das Klassen-Template explizit instanziiert und in der Anwendung dann mittels extern auf diese Instanz verwiesen. D.h. für Klassen-Templates kann das gleiche Verfahren angewandt werden wie für 'normale' externe Variablen. Sehen wir uns dies an einem Beispiel an.

PgmHeaderDatei object.h mit der Template-Deklaration
template <typename T>
class Any
{
    T data;
  public:
    Any (T val): data(val)
    {}
    T GetData()
    {
        return data;
    }
};

Datei object.cpp mit den Template-Instanzen
#include <string>
#include "object.h"

template class Any<int>;           // Template mit int instanziieren
template class Any<std::string>;   // Template mit string instanziieren

Applikation
#include "object.h"

extern template class Any<int>;         // Externe Instanzen
extern template class Any<std::string>; // des Klassen-Templates

// main() Funktion
int main()
{
    // Die folgenden Anweisung legen keine neue
    // Instanz des Klassen-Templates mehr an

    Any<int> ival(11);
    Any<std::string> sval("hurra!");
    ....
}

In der Datei object.h wird das Klassen-Template zunächst wie gewohnt deklariert. Anschließend werden in der Datei object.cpp zwei Instanzen des Klassen-Templates erzeugt, einmal für int-Daten und einmal für string-Daten. Diese expliziten Instanziierungen führen dazu, dass alle Memberfunktionen des Klassen-Template für int- und string-Daten durch den Compiler erstellt werden, aber kein Speicher für die Eigenschaften reserviert wird. In der Anwendung nun wird mittels extern nur auf diese Instanzen verwiesen, d.h. es wird kein Code für die Memberfunktion erneut erzeugt. Ohne die Angabe von extern würde der Compiler erneut in der Anwendung Code für die verwendeten Memberfunktionen des Klassen-Templates erzeugen. Erst beim Linken der Dateien würden dann diese "überzähligen" Memberfunktionen wieder entfernt werden. Was aber bei der Instanziierung des Klassen-Templates weiterhin erfolgt, ist die Reservierung von Speicher für die Eigenschaften des Objekts.

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.