C++ Kurs

Template-Spezialitäten I

Die Themen:

Default-Datentypen für Templates
Non-type Template-Parameter
Explizite Typangabe von Template-Argumenten
Spezialisierung bei Funktions-Templates
Templates als Parameter
Klassen-Templates und Ableitungen

In dieser und der nachfolgenden Lektion weichen wir etwas von der bisherigen Vorgehensweise ab, dass erst am Ende der Lektion das Beispiel und die Übung folgen. Wegen der Komplexität der Templates folgen jetzt am Ende eines Abschnitts gleich die entsprechende Beispiele. Als Ausgleich für die vielen Beispiele wird dafür aber auf eine Übung verzichtet.

Default-Datentypen für Templates

Häufig werden Template-Objekte mit dem gleichen Template-Datentyp definiert. So werden im nachfolgenden Beispiel unter anderem zwei Template-Objekte mit dem Datentyp int definiert.


template <typename T> class Any
{...};

Any<int> objOne;
...
Any<double> objTwo;
...
Any<int> objThree;

Man kann sich in solchen Fällen etwas Schreibarbeit sparen, wenn man bei der Definition des Klassen-Templates einen Datentyp als Default-Datentyp vorgibt. Die Vorgabe des Default-Datentyps erfolgt in der Art, dass nach dem Template-Argument ein Gleichheitszeichen folgt und dann der Default-Datentyp. Bei der Definition eines Template-Objekts mit einen Default-Datentyp kann dann die Angabe des Datentyps entfallen. Auf jeden Fall aber müssen bei der Definition des Objekts die beiden (nun leeren) spitzen Klammern immer mit angegeben werden.


template <typename T=int> class Any
{...};

Any<> objOne;        // definiert Objekt vom Typ Any<int>
...
Any<double> objTwo;  // definiert Objekt vom Typ Any<double>
...
Any<> objThree;      // definiert Objekt vom Typ Any<int>

Beispiel:

Das Beispiel realisiert ein Array, dessen Größe sich zur Programmlaufzeit an die im Array abzulegenden Elemente anpasst. Das Array ist als Template realisiert um beliebige Datentypen aufnehmen zu können.

Wird beim Zugriff auf ein bestimmtes Element (Aufruf des überladenen Index-Operators []) über die obere Feldgrenze hinaus zugegriffen, so wird das Array entsprechend erweitert. Da das erweiterte Array mithilfe des new Operators allokiert wird, werden die neuen Elemente in Array zunächst mit dem Standard-Konstruktor des Elements initialisiert (siehe auch Erstellen von dynamischen Objektfeldern) Bei einem Zugriff auf Elemente mit einem Index kleiner 0 wird eine std::range_error Exception ausgelöst.

Standardmäßig wird das Array für die Aufnahme von int-Daten angelegt.

Die im Beispiel verwendete Klasse Demo dient nur zur Kontrolle, ob auch Objekte im Array richtig verarbeitet werden. Die Klasse Demo ist in einer getrennten Header-Datei abgelegt, die Sie sich hier ansehen können.

Und das komplette int-Feld:
-842150451, -842150451, -842150451, -842150451, 44, -842150451, 66,
, erstes Objekt, ,
Und das komplette Demo-Feld:
, erstes Objekt, , , , zweites Objekt,


// Beispiel für Default-Datentyp in Templates
// Realisiert ein dynamisches Feld
// ACHTUNG!! Werden in dem Feld Objektzeiger abgelegt
// so müssen die dazugehörigen Objekte explizit wieder
// zerstört werden.

#include <iostream>
#include <stdexcept>

// Demo-Klasse einbinden
#include "demo.h"

using std::cout;
using std::endl;

// Klasse implementiert einen gesicherten Zugriff auf
// ein Feld. Wird beim Schreiben über das Ende des Feldes
// hinausgegriffen, so wird das Feld entsprechend erweitert
// Standardmässig wird das Feld für die Ablage von int-
// Daten angelegt
template <typename T = int>
class DynArray
{
    T* data;        // Zeiger auf Datenfeld
    size_t size;    // Grösse des Datenfelds
  public:
    // ctor, reserviert optional ein Feld mit einer
    // bestimmten Anzahl von Elementen
    DynArray(size_t reserveData = 0);
    // dtor
    ~DynArray();
    // Indexoperator für Array-Zugriff
    T& operator[] (size_t index);
    // Ausgabe des DynArrays
    void PrintArray();
};
// ctor
// Standardmässig wird kein Feld allokiert und der Datenzeiger
// mit NULL initialisiert
template <typename T>
DynArray<T>::DynArray(size_t reserveData): data(NULL), size(0)
{
    // Falls Datenfeld angelegt werden soll
    if (reserveData)
    {
        // Datenfeld reservieren
        data = new T[reserveData];
        size = reserveData;
    }
}
// dtor, gibt reserviertes Feld frei
template <typename T>
DynArray<T>::~DynArray()
{
    delete [] data;
}
// Überladener Indexoperator für den Zugriff
// auf die Elemente im DynArray
template <typename T>
T& DynArray<T>::operator[] (size_t index)
{
    // Falls Index < 0 dann eine Standard-Exception auslösen
    if (index<0)
        throw std::range_error("Index kleiner 0!");
    // Falls Index grösser als die Anzahl der Feldelement
    if (index>=size)
    {
        // Neues Feld mit der Grösse 'Index+1' reservieren
        // +1 hier unbedingt notwendig, da z.B. der
        // Index 10 das 11. Element adressiert!
        T* newData = new T[index+1];
        // Bisherige Daten in neues Feld übernehmen
        for (size_t copyOld=0; copyOld<size; copyOld++)
            newData[copyOld] = data[copyOld];
        // Altes Feld nun entfernen
        delete [] data;
        // Und Zeige auf neues Feld abspeichern
        data = newData;
        // Nicht vergessen: die Grösse korrigieren
        size = index+1;
    }
    // Gewünschtes Element zurückliefern
    return data[index];
}
// Komplettes Feld ausgeben
template <typename T>
void DynArray<T>::PrintArray()
{
    for (size_t index=0; index<size; index++)
        cout << data[index] << ", ";
    cout << endl;
}

// main() Funktion
int main()
{
    // DynArray mit Default-Datentyp 'int' und 5 Elementen anlegen
    DynArray<> intArray(5);
    // 5. Element beschreiben
    intArray[4] = 44;
    // 7. Element beschreiben -> führt zur Erweiterung des DynArrays
    intArray[6] = 66;
    // Komplettes Feld ausgeben (7 Elemente)
    cout << "Und das komplette int-Feld:\n";
    intArray.PrintArray();

    // DynArray mit 3 Demo-Objekten anlegen
    DynArray<Demo> objArray(3);
    // 2. Element beschreiben
    objArray[1] = Demo("erstes Objekt");
    // Komplettes Feld ausgeben (3 Elemente)
    objArray.PrintArray();
    // 6. Element beschreiben -> führt zur Erweiterung des DynArrays
    objArray[5] = Demo("zweites Objekt");
    // Komplettes Feld ausgeben (6 Elemente)
    cout << "Und das komplette Demo-Feld:\n";
    objArray.PrintArray();
}

Non-type Template-Parameter

Non-type Template-Parameter sind Template-Parameter die zum Zeitpunkt des Compilevorgangs berechenbar sein müssen. Sie können sich unter einem non-type Parameter eine Art Konstante vorstellen, die Sie der Template-Klasse mit auf den Weg geben. Innerhalb einer Template-Klasse werden non-type Template-Parameter wie Konstanten behandelt. Mithilfe von non-type Template-Parametern können Sie so zum Beispiel die Größe eines Feldes innerhalb einer Template-Klasse festlegen.


template <typename T, int SIZE> class Any
{
   T array[SIZE];
   ...
};

Non-type Template-Parameter können, wie auch alle anderen Template-Parameter, einen Default-Wert besitzen. Beachten müssen Sie bei non-type Template-Parametern nur, dass sie, wie bereits erwähnt, zur Compilezeit berechenbar sein müssen (damit fallen z.B. Adressen von lokalen Variablen/Objekten weg) und dass keine Gleitkommatypen zugelassen sind.


template <typename T, int SIZE=50> class Any
{
   T array[SIZE];
   ...
};

Werden Memberfunktionen von Klassen-Templates mit non-type Parametern außerhalb des Klassen-Templates definiert, so muss der non-type Template-Parameter bei der Memberfunktionen-Definition ebenfalls mit angegeben werden.


template <typename T, int SIZE=50> class Any
{
   ...
   void DoAny(short);
};
template <typename T, int SIZE> void Any<T,SIZE>::DoAny(short v)
{...}

Bei der Definition eines Objekts eines Klassen-Templates mit non-type Template-Parametern muss für den non-type Parameter innerhalb der spitzen Klammer ein entsprechender Ausdruck angegeben werden. Ausnahme: für den non-type Template-Parameter gibt es einen Defaultwert. Somit enthält das Objekt obj1 im nachfolgenden Beispiel ein short-Feld mit 100 Elementen und das Objekt obj2 ein double-Feld mit den standardmäßigen 50 Elementen.


template <typename T, int SIZE=50> class Any
{
   T array[SIZE]
   ...
};
Any<short,100> obj1;
Any<double> obj2;

Beispiel:

Das Beispiel realisiert ein Klassen-Template SafeArray für ein Safe-Array. Die im Safe-Array abzuspeichernden Daten werden in einem Feld abgespeichert. Die maximale Anzahl der abzuspeichernden Daten wird bei der Definition eines SafeArray-Objekts als non-type Template-Parameter spezifiziert. Standardmäßig können in einem Safe-Array 50 Werte abgespeichert werden.

Beachten Sie im Beispiel auch, wie beim Auslesen der Daten aus dem Safe-Array in main(...) der Datentyp der Zielvariablen definiert wird. Dazu wird zum einen der Datentyp des Safe-Array über eine typedef-Anweisung definiert und zum anderen innerhalb des Klassen-Template SafeArray des Datentyp der im Safe-Array abgelegten Daten durch eine weitere typedef-Anweisung definiert.

Durch die Verwendung von typedefs haben Sie den Vorteil, dass der Datentyp der im Safe-Array abgelegten Daten relativ einfach geändert werden kann. Sie müssen dazu nur die typedef-Anweisung für das Safe-Array entsprechend anpassen. Die Anpassung des Datentyps der Zielvariablen erfolgt dann automatisch über die typedef-Anweisung innerhalb des Templates. Versuchen Sie einmal, das Safe-Array für die Ablage der int-Daten so abzuändern, dass double--Daten abgespeichert werden können.

1. Element: Element 0
SafeArray: Erlaubt [0,10), Index war 99
SafeArray: Erlaubt [0,50), Index war -1


// Beispiel fuer non-type Template-Parameter

#include <iostream>
#include <stdexcept>
#include <sstream>
#include "demo.h"

using std::cout;
using std::endl;

// Template fuer SafeArray
// Defaultmässig wird ein int-Array mit 50 Einträgen realisiert
// HINWEIS: Das gleiche Verfahren könnte auch dafür verwendet
// werden, um nicht 0-basierende Arrays zu implementieren
template <typename T=int, int SIZE=50>
class SafeArray
{
    T array[SIZE];
  public:
    typedef T value_type;
    T& operator[](int index);
};
// Implementierung des überladenen Index-Operators
template <typename T, int SIZE>
T& SafeArray<T, SIZE>::operator[] (int index)
{
    // Falls Zugriff ausserhalb der Grenzen, out_of_range werfen
    if ((index<0) || (index>=SIZE))
    {
        std::ostringstream os;
        os << "SafeArray: Erlaubt [0," << SIZE << "), Index war " << index;
        throw std::range_error(os.str());
    }
    // Referenz auf Element zurückgeben
    return array[index];
}

// Voll-spezifiziertes SafeArray für Demo-Objekte und 10 Elemente
typedef SafeArray<Demo,10> arrayType1;
// Standard SafeArrey für 50 int-Elemente
typedef SafeArray<> arrayType2;

// main() Funktion
int main()
{
    // Array mit Demo-Objekten definieren
    arrayType1 myArray;
    // Zugriff testen, die zweite Anweisung greift über das Array hinaus
    try
    {
        myArray[0] = Demo("Element 0");
        cout << "1. Element: " << myArray[0] << endl;
        arrayType1::value_type anyVal = myArray[99];
    }
    catch(std::range_error& ex)
    {
        cout << ex.what() << endl;
    }

    // Default Safe-Array für int-Daten mit 50 Elemente definieren
    arrayType2 yourArray;
    // Zugriff testen, die zweite Anweisung führt einen Zugriff
    // vor das erste Element durch
    try
    {
        arrayType2::value_type anyVal = yourArray[49];
        yourArray[-1] = 10;
    }
    catch(std::range_error& ex)
    {
        cout << ex.what() << endl;
    }
}

Explizite Typangabe von Template-Argumenten

In der Regel kann der Datentyp von Template-Argumenten eines Funktions-Templates aus den Datentypen der übergebenen Parameter abgeleitet werden.


template <typename T> void DoAny(T val)
{...};
float var1;
DoAny(var1);    // erzeugt DoAny(float val)
...
char var2;
DoAny(var2);    // erzeugt DoAny(char val)

Ein Problem taucht hierbei jedoch immer dann auf, wenn der Returntyp des Funktions-Templates ein Template-Argument ist. In diesem Fall kann der Datentyp des Rückgabewerts nicht aus dem Funktionsaufruf hergeleitet werden. So kann im Beispiel das Funktions-Template Calculate(...) einen beliebigen Datentyp zurückliefern, der sich für die Zuweisung nur irgendwie in einen double-Datentyp konvertieren lassen muss.


template <typename T1, typename T2> T1 Calulate(T2 val)
{...};

// Welchen Datentyp liefert Calculate(...)?
float var1;
double var2 = Calculate(var1);

In einen solchen Fall müssen Sie den Datentyp des Template-Arguments explizit vorgeben. Die Vorgabe der Datentypen erfolgt in der Art, dass beim Aufruf der Funktion nach dem Funktionsnamen innerhalb der spitzen Klammern die entsprechenden Datentypen angegeben werden. Im nachfolgenden Beispiel entspricht damit das Argument T1 dem Datentyp double und T2 dem Datentyp float.


template <typename T1, typename T2> T1 Calulate(T2 val)
{...}; 
// Explizite Typangabe fuer Template-Argumente
float var1;
double var2 = Calculate<double,float>(var1);

Kann einer der Datentypen implizit beim Aufruf durch den Compiler hergeleitet werden (so wie im Beispiel unten der Datentyp des Parameters T2), so reicht die expliziten Angabe der vorhergehenden Datentypen aus. Der Datentyp des zweiten Template-Arguments wird dann aus dem Datentyp des Funktionsparameters hergeleitet (hier float).


template <typename T1, typename T2> T1 Calulate(T2 val)
{...}; 
// Explizite Typangabe des 1. Arguments
float var1;
double var2 = Calculate<double>(var1);

Beispiel:

Das Beispiel definiert ein Funktions-Template zur byteweisen 'Übertragung' eines beliebigen Datentyps. Die einzelnen Bytes der Daten werden für die Übertragung nach ASCII konvertiert. Damit der Empfänger der Daten diese auswerten kann, wird zusätzlich der Datentyp der übertragenen Daten noch mit übertragen.

Werden über diese Template-Funktion Variablen übertragen, so kann der Compiler aus dem Datentyp der Variablen die zu erzeugende Funktion herleiten. Anders sieht es aus, wenn Literale (unbenannte Konstanten) übertragen werden. Da Literale einen Standard-Datentyp besitzen, muss zum Beispiel bei der Übertragung eines char-Literals entweder das Literal beim Aufruf der SendTo(...) Funktion mittels Typkonvertierung explizit in ein char konvertiert werden oder aber explizit die SendTo(...) Funktion für ein char-Datum aufgerufen werden.

sending bytes: int 0a 00 00 00
sending bytes: char 41
sending bytes: int 61 00 00 00
sending bytes: long 61 00 00 00
sending bytes: char 61
sending bytes: short 34 12


// Beispiel zur expliziten Typ-Angabe bei Funktions-Templates

#include <iostream>

using std::cout;
using std::endl;

// Funktions-Template zur byteweisen 'Übertragung' eines
// Wertes in ASCII-Form
template <typename T>
void SendTo(std::ostream& os, T value)
{
    // Datentyp ausgeben
    os << "sending bytes: ";
    os << typeid(T).name() << ' ';
    // char-Zeiger auf Datenbeginn aufsetzen
    const char* p = reinterpret_cast<const char*>(&value);
    // Alle Bytes durchlaufen
    for (size_t i=0; i<sizeof(T); i++)
    {
        // Low-Nibble nach ASCII konvertieren
        char lowNibble = (*p)&0x0f;
        lowNibble = (lowNibble>9) ? lowNibble+0x57 : lowNibble|0x30;
        // Heigh-Nibble nach ASCII konvertieren
        char highNibble = ((*p)>>4)&0x0f;
        highNibble = (highNibble>9) ? highNibble+0x57 : highNibble|0x30;
        // Datum in ASCII-Form 'übertragen'
        os << highNibble << lowNibble << ' ';
        // Zeiger auf nächstes Byte
        p++;
    }
    cout << endl;
}

// main() Funktion
int main()
{
    // Auszugebende Daten
    int intVal = 10;
    char charVal = 0x41;   // entspricht 'A'

    // Aufruf der Template-Funktion
    // Der Template-Datentyp ergibt sich aus dem Datentyp des Parameters
    SendTo(cout,intVal);
    SendTo(cout,charVal);

    // Aufruf der Template-Funktion mit Literalen
    // Standardmässig werden Literale als ints behandelt
    // Soll SendTo(...) mit anderem Datentyp aufgerufen werden,
    // so kann entweder der Datentyp explizit festgelegt werden
    // (2. Aufruf) oder aber der Template-Datentyp explizit
    // angegeben werden (3. + 4. Aufruf)
    SendTo(cout,0x61);
    SendTo(cout,0x61L);
    SendTo<char>(cout,0x61);
    SendTo<short>(cout,0x1234);
}

Spezialisierung bei Funktions-Templates

In manchen Fällen kann es notwendig sein, dass ein Funktions-Template für einen bestimmte Datentyp überschrieben (spezialisiert) werden muss. Das nachfolgende allgemeine Funktions-Template Any(...) gibt zum Beispiel in Inhalt des übergebenen Parameters auf die Standardausgabe aus.


template <typename T> void Any (const T& data)
{
   cout << data << endl;
}
...
int val = 10;
Any(val);     // Gibt val aus
...
Any(&val);    // Gibt ??? aus

Was aber passiert, wenn an Any(...) ein Zeiger übergeben wird, so wie im letzten Aufruf? Nun, dann wird wie üblich der Inhalt des übergebenen Datums ausgegeben, also die Adresse die im Zeiger abgelegt ist.

Vermutlich aber sollte nicht der Inhalt des Zeigers ausgegeben werden sondern die Daten, auf die der Zeiger verweist. Was ist also zu tun?

Die Antwort auf diese Frage laut: Spezialisierung des Funktions-Templates. D.h. man definiert ein zweites Funktions-Template für den entsprechenden Datentyp, für den die Template-Funktion aufgerufen werden soll. Bei der Auflösung des späteren Funktionsaufrufs sucht der Compiler immer nach einer Funktion oder einem Funktions-Template, dessen Parametertypen möglichst genau zu den Parametertypen des Funktionsaufrufs passt (best match). Dieser Vorgang wird auch als template argument deduction bezeichnet. Soll also für Zeiger ein eigenes Funktions-Template definiert werden, so ist innerhalb der Parameterklammer der Template-Funktion ein Zeiger anzugeben. Beachten Sie genau, wo der Zeiger (d.h. das '*') steht!


// Allgemeines Funktions-Template
template <typename T> void Any (const T& data)
{...}
// Funktions-Template für Zeiger
template <typename T> void Any (T* data)
{
  cout << *data <<endl;
  ...
}
...
int val = 10;
Any(val);     // Gibt val aus
...
Any(&val);    // Gibt dereferenzierten Zeiger aus

Vielleicht hätten Sie in einem ersten Schritt zur Lösung dieses Problems versucht, das Funktions-Template durch eine entsprechende Any(...) Funktion zu überladen. Diese ist generell auch möglich, jedoch hätten Sie dann für jeden Zeigertyp (d.h. für einen char*, einen short*, einen int* usw.) eine eigene Funktion schreiben müssen!

Wenn Sie Funktions-Templates spezialisieren, achten Sie immer auf die Datentypen! Sehen Sie sich dazu die nachfolgenden Aufrufe von Template-Funktionen an:


// Funktions-Templates
template <typename T> void Any(T& val)
{...}

template <typename T> void Any(const T& val)
{...}

template <typename T> void Any(T* val)
{...}

template <typename T> void Any(const T* val)
{...} 	

// main() Funktion
int main()
{
  int v1 = 10;
  const int v2 = 20;

  Any(v1);    // Any(T& val)
  Any(v2);    // Any(const T& val)
  Any(&v1);   // Any(T* val)
  Any(&v2);   // Any(const T* val)
  Any(10);    // Any(const T& val)
  Any<const char* const>("Text");  // Any(const T& val)
}

Bei der Übergabe des C-Strings müssen Sie explizit die aufzurufende Template-Funktion angeben, da hier der Compiler nicht das zu verwendende Funktions-Template herleiten kann.

Beispiel:

Das Funktions-Template SendTo(...) aus dem vorherigen Beispiel wird nun so erweitert, dass bei der Übergabe eines Zeigers an die Template-Funktion die dereferenzierten Daten und nicht mehr der Zeiger selbst 'übertragen' werden.

sending bytes: int 0a 00 00 00
sending bytes: char 41
sending bytes: int 0a 00 00 00
sending bytes: char 41


// Beispiel zur expliziten Typ-Angabe bei Funktions-Templates

#include <iostream>

using std::cout;
using std::endl;

// Funktions-Template zur byteweisen 'Übertragung' eines
// Wertes in ASCII-Form
template <typename T>
void SendTo(std::ostream& os, T value)
{
    // Datentyp ausgeben
    os << "sending bytes: ";
    os << typeid(T).name() << ' ';
    // char-Zeiger auf Datenbeginn aufsetzen
    const char* p = reinterpret_cast<const char*>(&value);
    // Alle Bytes durchlaufen
    for (size_t i=0; i<sizeof(T); i++)
    {
        // Low-Nibble nach ASCII konvertieren
        char lowNibble = (*p)&0x0f;
        lowNibble = (lowNibble>9) ? lowNibble+0x57 : lowNibble|0x30;
        // Heigh-Nibble nach ASCII konvertieren
        char highNibble = ((*p)>>4)&0x0f;
        highNibble = (highNibble>9) ? highNibble+0x57 : highNibble|0x30;
        // Datum in ASCII-Form 'übertragen'
        os << highNibble << lowNibble << ' ';
        // Zeiger auf nächstes Byte
        p++;
    }
    cout << endl;
}
// Spezialisierung für 'Übertragung' von Daten die
// über einen Zeiger referenziert werden
template <typename T>
void SendTo(std::ostream& os, T* value)
{
    os << "sending bytes: ";
    os << typeid(T).name() << ' ';
    // char-Zeiger auf Datenbeginn aufsetzen
    const char* p = reinterpret_cast<const char*>(value);
    // Alle Bytes durchlaufen
    for (size_t i=0; i<sizeof(T); i++)
{ // Low-Byte nach ASCII konvertieren char lowByte = (*p)&0x0f; lowByte = (lowByte>9) ? lowByte+0x57 : lowByte|0x30; // Heigh-Byte nach ASCII konvertieren char highByte = ((*p)>>4)&0x0f; highByte = (highByte>9) ? highByte+0x57 : highByte|0x30; // Datum in ASCII-Form 'übertragen' os << highByte << lowByte << ' '; // Zeiger auf nächstes Byte p++; } cout << endl; }
// main() Funktion
int main()
{
    // Auszugebende Daten
    int intVal = 10;
    char charVal = 0x41;   // entspricht 'A'

    // Aufruf der Template-Funktion
    // Der Template-Datentyp ergibt sich aus dem Datentyp des Parameters
    SendTo(cout,intVal);
    SendTo(cout,charVal);
     // Aufruf der spezialisierten Template-Funktion
    SendTo(cout,&intVal);
    SendTo(cout,&charVal);
}

Templates als Parameter

Nach dem Sie nun einiges über Funktions-Templates erfahren haben, gehen wir jetzt über zu den Klassen-Templates.

Wie Sie Objekte von Klassen-Templates definieren, das haben Sie ja bereits erfahren. Wie aber übergibt man ein Objekt eines Klassen-Templates an eine Funktion oder Memberfunktion? Hierbei müssen zwei Fälle unterschieden werden:

Die Funktion erhält immer Objekte eines Klassen-Templates

Da ja der Datentyp des Funktionsparameters indirekt über das Argument T des Klassen-Templates variieren kann, muss die Funktion als Funktions-Template definiert werden. Innerhalb der Parameterklammer des Funktions-Templates wird eine Referenz (oder const-Referenz) vom Typ des Klassen-Templates angegeben. Das Template-Argument T des Funktions-Templates entspricht dann dem Datentyp, mit dem das übergebene Objekt des Klassen-Templates instanziiert wurde. Innerhalb des Funktions-Templates können somit auch Daten vom Typ des Template-Arguments des Klassen-Templates definiert werden (T val im Beispiel).


template <typename T> class MyClass
{
   T GetData();
   ...
};

template <typename T> void DoAny(MyClass<T>& obj)
{
   ...
   T val = obj.GetData();
}

MyClass<int> intClass;
DoAny(intClass);              // T in DoAny(): int
MyClass<float> floatClass;
DoAny(floatClass);            // T in DoAny(): float

Die Funktion erhält Objekte unterschiedlicher Klassen-Templates

Auch hier ist die Verwendung eines Funktions-Templates erforderlich. Anstatt nun jedoch das Klassen-Template in der Parameterklammer des Funktions-Templates anzugeben, wird nur noch das Template-Argument T angegeben. Damit wird das Template-Argument T der Funktions-Templates innerhalb der Funktion durch das Klassen-Template 'ersetzt'.


template <typename T> class MyClass;
template <typename T> class YourClass;

template <typename T> void DoAny(T& obj)
{...}

MyClass<float> myObj;
DoAny(myObj);               // T in DoAny(): MyClass<float>
YourClass<int> yourObj;
DoAny(yourObj);             // T in DoAny(): YourClass<int>

Bleibt nur noch ein kleines Problem bestehen, wenn unterschiedliche Klassen-Templates an eine Funktion übergeben werden sollen. Wie kommt man in diesem Fall an den Datentyp, mit dem das Objekt des Klassen-Templates instanziiert wurde?

Die Lösung naht in Form eines typedefs innerhalb des Klassen-Templates. Innerhalb des Klassen-Templates wird mittels typedef für das Template-Argument T ein Synonym definiert (zum Beispiel value_type). Dieses Synonym kann dann für den Datentyp des Template-Arguments des Klassen-Templates innerhalb der Funktion eingesetzt werden. Beachten müssen Sie hierbei aber unbedingt, dass vor dem Synonym das Schlüsselwort typename stehen muss. Ansonsten kann der Compiler nicht unterscheiden, ob hier eine Definition oder ein Datentyp steht.


template <typename T> class MyClass
{
  public:
    typedef T value_type;
    T GetData();
    ...
};

template <typename T> void DoAny(T& obj)
{
   typename T::value_type val = obj.GetData();
   ...
}

MyClass<float> myObj;
DoAny(myObj);              // T in DoAny(): MyClass<float>

Beispiel:

Das Funktions-Template SendTo(...) wurde so umgeschrieben, dass es nun Objekte der Standard Bibliothek Templates stack und priority_queue verarbeiten kann.

sending bytes: 4,0,0,0,3,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,
sending bytes: 12,0,11,0,10,0,


// Templates als Funktionsparameter

#include <iostream>
#include <stack>
#include <queue>
using std::cout;
using std::endl;
using std::stack;
using std::priority_queue;

// Template-Funktion zur Übertragung der Container
// stack und priority_queue
// Nach dem Verlassen der Funktion ist der übergebene
// Container geleert!!
template <typename T>
void SendTo(std::ostream& os, T& container)
{
    os << "sending bytes: ";
    // Kompletten Container leeren
    while(!container.empty())
    {
        // Einen Wert aus dem Container holen. Der aktuelle Datentyp
        // im Container ist als typedef value_type schon definiert
        typename T::value_type actVal = container.top();
        // Wert nun byteweise ausgeben
        char *ptr = reinterpret_cast<char*>(&actVal);
        for (size_t i=0; i<sizeof(typename T::value_type); i++)
            os << static_cast<int>(*(ptr++)) << ',';
        // Element aus Container entfernen
        container.pop();
    }
    os << endl;
}

// main() Funktion
int main()
{
    // int-Stack erzeugen und füllen
    stack<int> intVector;
    for (int i=0; i<5; i++)
        intVector.push(i);
    // Vektor jetzt byteweise an cout übertragen
    SendTo(cout,intVector);

    // short-Priority-Queue erzeugen und füllen
    priority_queue<short> shortQueue;
    for (int x=0; x<3; x++)
        shortQueue.push(x+10);
    // und ebenfalls an cout übertragen
    SendTo(cout, shortQueue);
}

Klassen-Templates und Ableitungen

Beim Ableiten von Klassen-Templates können drei Fälle auftreten:

Basisklasse ist ein Klassen-Template mit definierten Datentypen

In diesem Fall erfolgt die Ableitung wie gewohnt, d.h. nach dem Zugriffsrecht der Name der Basisklasse. Da die Basisklasse nun aber eine Template-Klasse ist, muss zusätzlich noch in spitzen Klammern der Datentyp des Template-Arguments der Basisklasse angegeben werden. Ist der Datentyp fest vorgegeben, so kann die abzuleitende Klasse als gewöhnliche Klasse definiert werden (erste Ableitung MyClass). Variiert jedoch der Datentyp des Template-Arguments, so muss die abzuleitenden Klasse ebenfalls als Klassen-Template definiert werden, wobei das Template-Argument nun an die Basisklasse durchgereicht wird (zweite Ableitung YourClass).


// Die Basisklasse
template <typename T> class Any
{...};

// Abgeleitete Klassen
class MyClass: public Any<int>
{...};
template <typename T>
class YourClass: public Any<T>
{...};

// Objekte definieren
Any<int> obj1;          // Basisklassen-Objekt
MyClass obj2;           // Basisklasse: Any<int>
YourClass<short> obj3;  // Basisklasse: Any<short>

Basisklasse wird über ein Template-Argument definiert

Hierbei ist die abzuleitende Klasse immer als Klassen-Template zu definieren, wobei die Basisklasse dann als Template-Argument spezifiziert wird.


// Zwei gewöhnliche Basisklassen
class Any
{...};
class Some
{...};

// Abgeleitete Klasse, Basisklasse wird über Template-Argument festgelegt
template <typename T> class MyClass: public T
{...};

// Objekte definieren
MyClass<Any> anyBase;       // Basisklasse Any
MyClass<Some> someBase;     // Basisklasse Some

Basisklasse ist eine beliebige Template-Klasse

Auch hier ist die abzuleitende Klasse als Klassen-Template zu definieren, die als Template-Argument dann die Template-Klasse der Basisklasse erhält.

Bei der Definition eines Objekts der abgeleiteten Klasse muss dann das Basisklassen-Template inklusive dessen Template-Argument angegeben werden.


// Klassen-Templates als Basisklassen
template <typename T> class Base1
{...};
template <typename T> class Base2
{...};

// Abgeleitete Klasse
template <typename T> class Der: public T
{...};

// Objekte definieren
Der<Base1<short> > obj1;
Der<Base2<float> > obj2;

 

Beachten Sie unbedingt das Leerzeichen zwischen den beiden spitzen Klammern bei der Definition eines solchen Objekts! Ohne dieses Leerzeichen würde der Compiler die beiden spitzen Klammern als Schiebeoperator interpretieren.

Und leiten Sie niemals Klassen von Standard Bibliothek Containern (wie z.B. stack oder queue) ab! Die Container enthalten keinen virtuellen Destruktor und damit werden diese nicht ordnungsgemäß gelöscht wenn das abgeleitete Objekt gelöscht wird.

Beispiel:

Vorgegeben ist ein Klassen-Template SpinButton für einen Oberflächen-Element Drehschalter. Da der Drehschalter beliebige Datentypen verarbeiten können soll, wird der Datentyp als Template-Argument definiert. Außer dem aktuellen Wert des Drehschalters enthält das Klassen-Template noch einen Bereich, innerhalb dessen der Drehschalterwert liegen muss. Beachten Sie, wie dieser Bereich im Konstruktor von SpinButton initialisiert wird.

Von diesem SpinButton Klassen-Template wird eine Klasse ImageSpinButton abgeleitet, die anstelle von nummerischen Werten entsprechende Symbole darstellen können soll. Die Klasse ImageSpinButton hat als feste Basisklasse immer die Klasse SpinButton<unsigned int>.

Datentyp: int
Min/Max: (-2147483648/2147483647), akt. Wert: -2147483648
Datentyp: int
Min/Max: (-2147483648/2147483647), akt. Wert: -2147483647
Datentyp: class ImageSpinButton
Min/Max: (0/2), akt. Image: Image one
Datentyp: class ImageSpinButton
Min/Max: (0/2), akt. Image: Image two


// Beispiel für Templates als Basis-Klasse

#include <iostream>
#include <limits>

using std::cout;
using std::endl;

// Basisklassen-Template für 'normalen' Drehschalter
// Default-Datentyp des Button ist int
template <typename T=int>
class SpinButton
{
  protected:
    T value;       // akt. Wert
    T minValue;    // min. Wert
    T maxValue;    // max. Wert
  public:
    SpinButton()
    {
        minValue = std::numeric_limits<T>::min();
        maxValue = std::numeric_limits<T>::max();
        value = minValue;
    }
    void ShowProperties()
    {
        cout << "Datentyp: " << typeid(T).name() << '\n';
        cout << "Min/Max: (" << minValue << '/' << maxValue;
        cout << "), akt. Wert: " << value << endl;
    }
    void StepUp()
    {
        if (value < maxValue)
           value++;
    }

};

// Von der Template-Klasse SpinButton abgeleitete Klasse
// ImageSpinButton. Datentyp des Werts von SpinButton ist
// unsigned int. Die in der Basisklasse definierte Eigenschaft
// value dient als Imagelisten-Index.
class ImageSpinButton: public SpinButton<unsigned int>
{
    const char* const *imList;   // ACHTUNG! char-Zeiger Feld!
  public:
    ImageSpinButton(const char* const il[]): imList(il)
    {
        // Anzahl der Images in der Liste bestimmen
        maxValue = minValue;
        while(*(il+maxValue) != NULL)
            maxValue++;
        maxValue--;
    }
    void ShowProperties()
    {
        cout << "Datentyp: " << typeid(*this).name() << '\n';
        cout << "Min/Max: (" << minValue << '/' << maxValue;
        cout << "), akt. Image: " << imList[value] << endl;
    }
};

// main() Funktion
int main()
{
    // Spinbutton mit Default-Datentyp definieren
    SpinButton<> intSpin;
    intSpin.ShowProperties();
    intSpin.StepUp();
    intSpin.ShowProperties();

    // Imageliste für Image-Spinbutton
    const char* const imageList1[] =
          {"Image one","Image two","Image three",NULL};
    // Image-Spinbutton definieren
    ImageSpinButton ImageSpin(imageList1);
    ImageSpin.ShowProperties();
    ImageSpin.StepUp();
    ImageSpin.ShowProperties();
}