C++ Kurs

Template-Spezialitäten I

Die Themen:

HinweisIn diesem und dem nachfolgendem Kapitel weichen wir etwas von der bisherigen Vorgehensweise ab, dass erst am Ende des Kdapitels das Beispiel und die Übung folgen. Wegen der Komplexität von Templates werden zunächst einige Template-Funktionen wiederholt, bei denen Sie sich auch gleich an einer Übung versuchen dürfen.

Wenn Sie den Einsatz von Templates nicht nochmals üben wollen, können Sie auch gleich zum Abschnitt 'Templates als Parameter' übergehen.

Default-Datentypen für Templates (Wiederholung)

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

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

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

In solchen Fällen kann man sich etwas Schreibarbeit sparen, indem man bei der Definition eines Klassen-Templates einen Datentyp als Default-Datentyp vorgibt. Die Vorgabe des Default-Datentyps erfolgt in der Art, dass nach dem formalen Datentyp ein Gleichheitszeichen folgt und dann der Default-Datentyp. Bei der Definition eines Template-Objekts mit 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.

PgmHeadertemplate <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>

ÜbungUnd hier geht's zur Übung.

Non-type Template-Parameter (Wiederholung)

Non-type Template-Parameter sind Template-Parameter, die zum Zeitpunkt des Übersetzens des Programms berechenbar sein müssen. Ein non-type Parameter ist eine Art Konstante, die der Template-Klasse mit auf den Weg gegeben wird. Innerhalb einer Template-Klasse werden non-type Template-Parameter wie Konstanten behandelt. Mit Hilfe eines non-type Template-Parameters kann damit zum Beispiel die Größe eines Feldes innerhalb einer Template-Objekts festgelegt werden.

PgmHeadertemplate <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.

PgmHeadertemplate <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 Definition der Memberfunktion ebenfalls mit angegeben werden. Ein eventueller Default-Wert wird hierbei aber nicht nochmals angegeben.

PgmHeadertemplate <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 existiert ein 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.

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

ÜbungUnd hier geht's zur Übung.

Explizite Typangabe von Template-Parametern (Wiederholung)

In der Regel können die Datentypen der Template-Parameter eines Funktions-Templates aus den Datentypen der Argumente beim Aufruf der Template-Funktion bestimmt werden.

PgmHeadertemplate <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 formaler Datentyp ist. In diesem Fall kann der Datentyp des Rückgabewerts nicht aus dem Funktionsaufruf bestimmt werden. So kann im Beispiel das Funktions-Template Calculate(...) einen beliebigen Datentyp zurückliefern, der sich für die Zuweisung nur irgendwie in den Datentyp der Variablen var2 konvertieren lassen muss.

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

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

Können die Datentypen der Template-Parameter nicht automatisch bestimmt werden, so müssen sie explizit angegeben werden. Die Angabe 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 ist damit der Parameter T1 vom Datentyp double und T2 vom Datentyp float.

PgmHeadertemplate <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 nachfolgenden Beispiel der Datentyp des Parameters T2), so reicht die expliziten Angabe der vorhergehenden Datentypen aus. Der Datentyp der nachfolgenden Template-Parameter wird dann aus dem Datentyp des Funktionsparameters hergeleitet (hier float).

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

ÜbungUnd hier geht's zur Übung.

Spezialisierung bei Funktions-Templates (Wiederholung)

Machmal ist es erforderlich, dass ein Funktions-Template für einen bestimmten Datentyp überschrieben (spezialisiert) werden muss. Das nachfolgende sehr einfache Funktions-Template Any(...) gibt den Inhalt des übergebenen Arguments auf die Standardausgabe aus.

PgmHeadertemplate <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. es wird ein zweites Funktions-Template für den entsprechenden Datentyp definiert. 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 Datentypen des Funktionsaufrufs passen (best match). Dieser Vorgang wird auch als template argument deduction bezeichnet.

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

ÜbungUnd hier geht's zu Übung.

Templates als Parameter

Nach dem wir nun einiges wiederholt haben, sehen wir uns an, wie ein Objekt eines Klassen-Templates an eine Funktion oder Memberfunktion übergeben wird. Hierbei müssen zwei Fälle unterschieden werden:

Die Funktion erhält immer Objekte desselben Klassen-Templates

Da der Datentyp des Funktionsparameters indirekt wegen des formalen Template-Datentyps des übergebenen Klassen-Templates variieren kann, muss die Funktion als Funktions-Template definiert werden. Der Datentyp des übergebenen Objekts ist in diesem Fall eine (const-)Referenz vom Typ des Klassen-Templates. 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 formalen Template-Arguments des Klassen-Templates definiert werden (T val im Beispiel).

PgmHeader
// Klassen-Template
template <typename T>
class MyClass
{
   T GetData();
   ...
};
// Funktionstemplate mit Template als Parameter
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 das Klassen-Template in der Parameterklammer des Funktions-Templates anzugeben, wird nur noch das formale Template-Argument angegeben.

PgmHeader
// Template-Deklararionen
template <typename T>
class MyClass;
template <typename T>
class YourClass;
// Funktionstemplate mit Template-Parameter
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 noch ein kleines Problem bestehen, das es zu lösen gilt, wenn aus der Template-Funktion heraus eine  Memberfunktion des übergebenen Objekts aufgerufen wird, die wiederum einen vom formalen Template-Parameter abhängigen Returnwert zurückliefert. Welchen Datentyp hat dann die Variable bzw. das Objekt, in dem der von der Funktion zurückgegebene Wert abgelegt werden kann? Nun, da der Compiler beim Übersetzen des Programms den Returnwert der aufgerufenen Memberfunktion ja kennt, lassen wir ihn auch den Datentyp der Variable bzw. des Objekts bestimmen. Wie? Ganz einfach, indem wir den Datentyp auto verwenden.

PgmHeader
// Klassen-Template
template <typename T>
class MyClass
{
  public:
    T GetData() const;
    ...
};
// Funktions-Template mit Template-Parameter
template <typename T>
void DoAny(T& obj)
{
   auto val = obj.GetData();
   ...
}

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

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

Klassen-Templates und Ableitungen

Beim Ableiten von Klassen 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 Ableitung folgt 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 normale Klasse definiert werden (erste Ableitung MyClass). Variiert jedoch der Datentyp des Template-Arguments, so muss die abzuleitende Klasse ebenfalls als Klassen-Template definiert werden, wobei das Template-Argument nun an die Basisklasse weitergegeben wird (zweite Ableitung YourClass).

PgmHeader// 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 als Basisklasse das Template-Argument anzugeben ist.

PgmHeader// 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 weitere Template-Klasse

Auch hier ist die abzuleitende Klasse als Klassen-Template zu definieren, die als Template-Argument 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.

PgmHeader// 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;
AchtungLeiten Sie niemals Klassen von Standard-Bibliotheks 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.

BeispielBeispiel:

Vorgegeben ist ein Klassen-Template SpinButton für einen Drehschalter. Ein Drehschalter repräsentiert hier einen Wert, der über entsprechende Memberfunktionen inkrementiert und dekrementiert werden kann. 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 variieren kann. Beachten Sie, wie dieser Bereich im Konstruktor von SpinButton initialisiert wird.

Von diesem SpinButton Klassen-Template wird eine Klasse ImageSpinButton abgeleitet, die anstelle des numerischen Wertes ein entsprechendes Symbol darstellt. Die Basisklasse hierfür ist immer eine Klasse SpinButton<unsigned int>. Die Symbole für die einzelnen Werte werden als C-String Feld dem Konstruktor übergeben.

PgmHeader// Beispiel für Templates als Basis-Klasse

#include <iostream>
#include <limits>
#include <typeinfo>

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) != nullptr)
            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",nullptr};
    // Image-Spinbutton definieren
    ImageSpinButton ImageSpin(imageList1);
    ImageSpin.ShowProperties();
    ImageSpin.StepUp();
    ImageSpin.ShowProperties();
}
ProgrammausgabeDatentyp: i
Min/Max: (-2147483648/2147483647), akt. Wert: -2147483648
Datentyp: i
Min/Max: (-2147483648/2147483647), akt. Wert: -2147483647
Datentyp: 15ImageSpinButton
Min/Max: (0/2), akt. Image: Image one
Datentyp: 15ImageSpinButton
Min/Max: (0/2), akt. Image: Image two