C++ Kurs

Template-Spezialitäten II

Die Themen:

Member-Templates
Template Spezialisierungen
Partielle Template-Spezialisierung
Partielle Template-Spezialisierung und non-type Argumente
Template-Template-Parameter
Templates als Compile-Zeit Ausdrücke

Member-Templates

Außer dass komplette Klassen als Templates definiert werden können, können auch Memberfunktionen innerhalb von Klassen als Templates realisiert werden. Diese Templates werden auch als Member-Templates bezeichnet.

Bevor wir auf die Realisierung solcher Member-Templates eingehen, stellt sich zunächst die Frage: wo setzt man solche Member-Templates ein? Stellen Sie sich dazu einmal eine Klasse vor, die diverse überladene Memberfunktionen enthält, um zum Beispiel Daten mit verschiedenen Datentypen nach irgendwo hin zu übertragen. Da der Algorithmus für die Übertragung weitgehend unabhängig von dem zu übertragenden Datum ist, werden viele der Memberfunktionen den gleichen Code enthalten (der sich nur durch den Datentyp des zu übertragenden Datums unterscheidet). Und dies ist genau eines der Einsatzgebiet für Member-Templates. Anstelle nun für jeden zu übertragenden Datentyp eine eigene Memberfunktion zu schreiben, wird ein entsprechendes Member-Template definiert. Im nachfolgenden Beispielen finden Sie eine solche Klasse, die allerdings die Daten 'nur' in eine Datei überträgt.

Sehen wir uns jetzt die Implementierung eines Member-Templates an.

Ein Member-Template wird innerhalb der Klasse genauso definiert, wie ein 'normales' Funktions-Template. Die Klasse selbst, die das Member-Template enthält, wird wie eine gewöhnliche Klasse definiert, d.h. sie erhält keine Template-Deklaration. Wird ein Objekt einer Klasse mit einem Member-Template definiert, so braucht hier zunächst kein Typ für das Member-Template angegeben werden (Klasse enthält ja keine Template-Deklaration). Erst beim Aufruf der Memberfunktion des Member-Templates wird vom Compiler automatisch eine entsprechende Memberfunktion erzeugt. D.h. die Klasse enthält letztendlich auch nur die Memberfunktionen, die Sie tatsächlich benötigen. Mithilfe von Member-Templates sparen Sie sich unter Umständen. eine Menge Schreibarbeit (und unnötigen Code), da Sie nun nicht mehr unzählige überladene Memberfunktionen definieren müssen.


class Any
{
   ...
   // Member-Template
   template <typename T> void Write (const T& val)
   {...}
};
// Objekt definieren
Any obj;
// Aufruf erzeugt entsprechende Memberfunktion
obj.Write(10);      // erzeugt: Write(const int&)
obj.Write(3.1);     // erzeugt: Write(const double&)

Wird das Member-Template nicht innerhalb der Klasse sondern außerhalb definiert, so müssen Sie der Memberfunktion eine Template-Deklaration voranstellen. Beachten Sie bitte, dass das Template-Argument nun nicht mehr nach dem Klassennamen angegeben werden darf wie bei der Definition von Template-Memberfunktionen.


class Any
{
   ...
   // Member-Template deklarieren
   template <typename T> void Write (const T& val);
};
// Member-Template ausserhalb definieren
template <typename T> void Any::Write(const T& val)
{...}

Steigern wir das Ganze jetzt noch etwas und sehen uns nun an, wie die Definition einem Member-Templates aussieht wenn die Klasse selbst ein Template ist?

Wird das Member-Template innerhalb des Klassen-Templates definiert, so ergibt sich keine Abweichung zum vorherigen Vorgehen. Das Einzige auf das Sie achten sollten ist, dass Sie für die Template-Argumente des Klassen-Templates und des Member-Templates unterschiedliche Bezeichner (z.B. T1 und T2) verwenden sollten. Falls Sie für das Klassen-Template und das Member-Template den gleichen Bezeichner (z.B. T) verwenden, so verdeckt der Bezeichner des Member-Templates den Bezeichner des Klassen-Templates, d.h. Sie können aus dem Member-Template heraus nicht mehr den Datentyp des Klassen-Templates verwenden. Aber ein solches Verhalten kennen Sie im Prinzip schon von der Überlagerung von lokalen und globalen Daten mit gleichem Namen.


template <typename T1> class Any
{
   ...
   // Member-Template
   template <typename T2> void Write (const T2& val)
   {...}
};
// Objekt definieren
Any<short> obj;
// Aufruf erzeugt entsprechende Memberfunktion
obj.Write(10);     // erzeugt: Write(const int&)
obj.Write(3.1);    // erzeugt: Write(const double&)

Etwas komplizierter sieht die Sache aus, wenn Sie das Member-Template außerhalb des Klassen-Templates definieren. In diesem Fall müssen Sie zwei Template-Deklarationen angeben, eine für das Klassen-Template und eine für das Member-Template.


template <typename T1> class Any
{
   ...
   // Member-Template deklarieren
   template <typename T2> void Write (const T2& val);
};
// Member-Template ausserhalb definieren
template <typename T1> template <typename T2>
void Any<T1>::Write(const T2& val)
{...}

Beispiel:

Die Klasse SaveFile soll zum Schreiben und Lesen von beliebigen Daten in eine Datei dienen. Die Besonderheit der Klasse besteht darin, dass beim Einlesen überprüft wird, ob der einzulesende Datentyp auch mit dem Datentyp übereinstimmt, der ursprünglich in die Datei geschrieben wurde.

Die Überprüfung erfolgt in der Weise, dass beim Schreiben der Daten zusätzlich zum Datum noch dessen Datentyp mit ausgegeben wird. Beim Einlesen wird dann der in der Datei abgespeicherte Datentyp mit des einzulesenden Datums verglichen. Stimmen die Datentypen nicht überein, so wird eines Exception ausgelöst.

Damit nun nicht für jeden erdenklichen Datentyp eine Write(...) und Read(...) Memberfunktion geschrieben werden muss, sind diese Memberfunktionen als Member-Templates definiert.

Wenn Sie sich die in der Datei abgelegten Daten und die eingelesenen Daten genauer ansehen, werden Sie feststellen, dass beim Einlesen nicht der komplette String eingelesen wird. Um den kompletten String einzulesen, müssen Sie eine spezialisiertes Member-Template erstellen. Versuchen Sie sich auch einmal daran.

Eingelesene Daten: A,1,Hier


// Beispiel für Member-Templates

#include <fstream>
#include <iostream>
#include <sstream>
#include <limits>

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

// SaveFile implementiert eine Klasse zum sicheren Schreiben
// und Lesen von Daten aus eine Datei. Beim Einlesen der Daten
// wird der in der Datei mit abgelegte Datentyp mit dem einzulesenden
// Datentyp verglichen. Sind die Datentypen unterschiedlich,
// so wird eine Exception ausgelöst
class SaveFile
{
    std::fstream ioFile;
public:
    // ctor, öffnet Datei zum Lesen und Schreiben
    SaveFile(const char* fileName);
    // dtor, schliesst Datei
    ~SaveFile();
    // Reset setzt den Schreib- und Lesezeiger zurück
    void Reset();
    // Member-Template zum Schreiben des Datum
    template <typename T> void Write(const T& value);
    // Member-Template zum Einlesen eines Datums
    template <typename T> void Read(T& value);
};
// ctor, öffnet Datei zum Lesen und Schreiben
SaveFile::SaveFile(const char* fileName)
{
    ioFile.open(fileName,std::ios::in|std::ios::out|std::ios::trunc);
    if (!ioFile)
       throw string("Fehler beim Datei oeffnen!");
}
// dtor, schliesst Datei
SaveFile::~SaveFile()
{
    ioFile.close();
}
// Reset, setzt den Schreib- und Lesezeiger zurück
void SaveFile::Reset()
{
    ioFile.seekg(0);
    ioFile.seekp(0);
}
// Member-Template zum Schreiben des Datum
// Vor dem eigentlichen Datum wird der Datentyp mit
// in die Datei geschrieben. Trennzeichen zwischen
// Datentyp und Datum ist '@'
template <typename T>
void SaveFile::Write(const T& value)
{
    ioFile << typeid(T).name() << '@' << value << std::endl;
}
// Member-Template zum Einlesen eines Datums
template <typename T>
void SaveFile::Read(T& value)
{
    // Datentyp und Datum einlesen
    string inputLine;
    std::getline(ioFile, inputLine);
    // Suche nach @ um Datentyp zu extrahieren
    string::size_type delimiterPos = inputLine.find('@');
    if (delimiterPos == string::npos)
       throw string("Datentyp konnte nicht bestimmt werden!");
    // Datentyp extrahieren
    string inputDType;
    inputDType.assign(inputLine,0,delimiterPos);
    // Falls einzulesender Datentyp und Dateidatentyp nicht
    // identisch sind, Exception auslösen
    if (inputDType != typeid(T).name())
    {
        string err("Falscher Datentyp -> ist: ");
        err += inputDType + ", soll: " + typeid(T).name();
        throw err;
    }
    // Datentyp entfernen
    inputLine.erase(0,delimiterPos+1);
    // ASCII-Datum nach binär konvertieren
    std::istringstream is(inputLine);
    is >> value;
    if (is.fail())
       throw string("Fehler bei der Datenkonvertierung!");
}

// main() Funktion
int main()
{
    // Datei-Objekt definieren
    SaveFile testFile("test.dat");
    // 3 Daten in Datei schreiben
    char cVal = 'A';
    long double ldVal = 1.0;
    std::string anyString("Hier geht's ab!");
    testFile.Write(cVal);
    testFile.Write(ldVal);
    testFile.Write(anyString);
    // Daten zuruecksetzen damit Einlesen kontrolliert werden kann
    cVal = 0;
    ldVal = 0;
    anyString = "";
    // Dateizeiger zurücksetzen
    testFile.Reset();
    // Nun versuchen alle Daten wieder einzulesen
    // Zum Testen mal die 1. oder 2. Zeile auskommentieren
    try
    {
        testFile.Read(cVal);
        testFile.Read(ldVal);
        testFile.Read(anyString);
    }
    catch (const string& errMsg)
    {
        cout << "Fehler: " << errMsg << endl;
        exit(1);
    }
    // Eingelesen Daten zur Kontrolle ausgeben
    cout << "Eingelesene Daten: " << cVal
         << ',' << ldVal << ',' << anyString << endl;
}

Template Spezialisierungen

Angenommen, Sie haben ein allgemeines Klassen-Template für die Verarbeitung von beliebigen Daten definiert. Nur für einen Datentyp passt dieses Klassen-Template nicht so richtig, sei es aufgrund von irgend welchen Einschränkungen die dieser Datentyp besitzt, oder aus Geschwindigkeitsgründen. Was also tun? Sie könnten z.B. eine neue Klasse mit einem eigenen Namen schreiben, die genau zu diesem Datentyp passt. Aber es geht auch anders: spezialisieren Sie das Klassen-Template für diesen einen Datentyp.

Nachfolgend ist ein solcher Fall einmal beispielhaft dargestellt. Das Klassen-Template Store dient zur Ablage eines beliebigen Datums. Um zum abgelegten Datum einen Wert zu addieren, besitzt das Klassen-Template die Memberfunktion Add(...). Werden nun Objekte von diesem Klassen-Template definiert die numerische Daten verarbeiten, so ist alles noch in Ordnung. Wenn Sie nun aber ein Objekt definieren, das anstelle eines numerischen Datum zum Beispiel einen C-String (char-Zeiger) ablegt, so würde die Add(...) Memberfunktion nun eine Zeiger-Addition durchführen und nicht, wie vielleicht beabsichtigt, die C-Strings verbinden.


// Klassen-Template
template <typename T> class Store
{
   T data;
   ...
   Add(const T& param)
   {
      data += param;
   }
};
// Objekte definieren
Store<int> intObj;
intObj.Add(10);          // int Addition
Store<char*> cptrObj;
cptrObj.Add("huhu");     // char* Addition

Was Sie in diesem Fall tun können, ist das Klassen-Template für den Datentyp char* zu spezialisieren.

Um ein Klassen-Template für einen bestimmten Datentyp zu spezialisieren, bleibt die spitze Klammer bei der Template-Deklaration des spezialisierten Templates leer. Der Datentyp, für die das Klassen-Template spezialisiert werden soll, wird nach dem Namen des Klassen-Template ebenfalls in spitzen Klammern angegeben. Das so spezialisierte Klassen-Template hat aber außer dem Namen mit dem allgemeinen Klassen-Template nichts mehr gemeinsam, d.h. es muss nicht die gleichen Eigenschaften und Memberfunktionen wie das allgemeine Klassen-Template besitzen.


// allgemeines Klassen-Template
template <typename T> class Store
{...};
// spezialisiertes Klassen-Template
template <> class Store<char*>
{...};
// Objekte definieren
Store<int> intObj;
intObj.Add(10);          // allg. Template-Objekt
Store<char*> cptrObj;
cptrObj.Add("huhu");     // char* Template-Objekt

Sind nur eine oder zwei Memberfunktionen des allgemeinen Templates nicht für einen bestimmten Datentyp geeignet, so können Sie auch diese Template-Memberfunktion explizit überschreiben (siehe hier).

Beispiel:

Das Klassen-Template Parameter dient zum Abspeichern eines beliebigen Datums einschließlich dessen zulässigen Wertebereichs. Das Klassen-Template enthält außer dem Konstruktor noch die beiden Memberfunktionen SetMinMax(...) um den Wertebereichs zu setzen und Show(...) um die aktuellen Eigenschaften eines Parameter-Objekts auszugeben.

Beachten Sie, wie die Memberfunktion SetMinMax(...) den bisherigen Wertebereich zurückliefert. Da der Wertebereich aus zwei Werten besteht, eignet sich hierfür der Datentyp pair (siehe hier) hervorragend.

Um nun auch string Objekte im Klassen-Template Parameter ablegen zu können, wird ein entsprechend spezialisiertes Klassen-Template eingesetzt. Dieses Klassen-Template besitzt jetzt keine Memberfunktion SetMinMax(...), da strings von Haus aus keinen Wertebereich haben.

Datentyp: int
Wert: 10, Min/Max: (-2147483648/2147483647)
---------------------------
Datentyp: float
Wert: -10, Min/Max: (1.17549e-038/3.40282e+038)
---------------------------
Datentyp: float
Wert: -10, Min/Max: (-100/100)
---------------------------
Urspruenglicher float-Bereich war: (1.17549e-038/3.40282e+038

Der String hat den Inhalt: Dies ist der C-String


// Beispiel zur Template-Spezialisierung

#include <iostream>
#include <limits>
#include <utility>
#include <string>

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

// Allgemeines Template für die Ablage
// von Werten mit Wertebereiche (min/max)
template <typename T>
class Parameter
{
    T   value;      // Wert
    T   minValue;   // zulässiger Wertebereich
    T   maxValue;
  public:
    // ctor
    Parameter(const T& v);
    // Wertebereich setzen
    std::pair<T,T> SetMinMax(const T& min, const T& max);
    // Anzeige der Daten
    void Show() const;
};
// ctor
template <typename T>
Parameter<T>::Parameter(const T& v): value(v)
{
    // Standardmässig ist zulässiger Bereich der
    // gesamte Bereich des Datentyps
    minValue = std::numeric_limits<T>::min();
    maxValue = std::numeric_limits<T>::max();
}
// Wertebereich setzen
// Memberfunktion liefert ursprünglichen Bereich als pair zurück
template <typename T>
std::pair<T,T> Parameter<T>::SetMinMax(const T& min, const T& max)
{
    std::pair<T,T> oldMinMax = std::make_pair(minValue, maxValue);
    minValue = min;
    maxValue = max;
    return oldMinMax;
}
// Anzeige der Daten
template <typename T>
void Parameter<T>::Show() const
{
    cout << "Datentyp: " << typeid(T).name() << '\n';
    cout << "Wert: " << value
         << ", Min/Max: (" << minValue << '/' << maxValue << ")\n";
    cout << "---------------------------" << endl;
}

// Spezialisiertes Template für string-Datentyp
// enthält kein Bereich
template<>
class Parameter<string>
{
    string value;   // string
  public:
    Parameter(const string& s);
    void Show();
};
// Definition der Memberfunktionen des spezialisierten Templates
// Beachten Sie, dass die Memberfunktionen keine Template-Deklarationen
// besitzen!
Parameter<string>::Parameter(const string& s): value(s)
{}
void Parameter<string>::Show()
{
    cout << "Der String hat den Inhalt: " << value << endl;
}

// main() Funktion
int main()
{
    // int und float Parameter-Objekte definieren
    Parameter<int> intParam(10);
    Parameter<float> floatParam(-10.0f);

    // Daten ausgeben
    intParam.Show();
    floatParam.Show();
    // Für floats neuen Bereich setzen
    std::pair<float,float> fMinMax;
    fMinMax = floatParam.SetMinMax(-100.0f, 100.0f);
    floatParam.Show();
    // Bisherigen Bereich ausgeben
    cout << "Urspruenglicher float-Bereich war: (" << fMinMax.first
         << '/' << fMinMax.second << '\n' << endl;

    // string Parameter-Objekt definieren
    // Der Aufruf von SetMinMax() würde zur Compile-Zeit
    // zu einem Fehler führen!
    Parameter<string> stringParam("Dies ist der C-String");
    stringParam.Show();
}

Partielle Template-Spezialisierung

Steigern wir die Spezialisierung von Templates noch etwas. Besitzt ein Template mehrere Template-Argumente, so kann ein solches Template auch für ein oder mehrere dieser Template-Argumente spezialisiert werden.

Nehmen wird einmal an, Sie wollen ein Template schreiben um ein beliebiges Datum in einen beliebigen Stream zu übertragen. Damit besitzt das Klassen-Template zwei Template-Argumente, eines für den Datentyp des einzufügenden Datums und eines für den Stream. Die Übertragung soll byteweise erfolgen. Bei numerischen Daten müssen Sie die Zerlegung des Datums in Einzelbytes selbst vornehmen, da von Haus aus keine solche Funktionalität zur Verfügung steht. Wird jedoch ein string übertragen, so können Sie auf die einzelnen Bytes des strings direkt über den string-Iterator zugreifen (siehe hier). Sie könnten nun für den Fall, dass ein string in einen Stream eingefügt werden soll, eine eigene Klasse hierfür anlegen. Aber es geht wieder einmal auch anders.

Das Zauberwort heißt: partielle Template-Spezialisierung. Im vorherigen Abschnitt haben Sie bereits erfahren, dass sich Klassen-Templates für bestimmte Datentypen spezialisieren lassen. Besitzt nun ein Klassen-Template nicht nur ein Template-Argument sondern mehrere, so kann für jedes dieser Template-Argumente das Template spezialisiert werden. Bei der partiellen Template-Spezialisierung bleibt dann aber mindestens eines dieser Template-Argumente erhalten, d .h. es wird nicht spezialisiert.

Die partielle Template-Spezialisierung erfolgt in der Art, dass in der Template-Deklaration des spezialisierten Templates nur noch die übrig gebliebenen Template-Argumente aufgeführt werden. Anschließend werden dann nach dem Klassennamen des Templates in spitzen Klammern alle Template-Argumente und die Datentypen aufgelistet, für die diese partielle Template-Spezialisierung gelten soll. Im untenstehenden Beispiel wird das Klassen-Template Any für den Fall spezialisiert, dass der Datentyp des zweiten Template-Arguments von Typ string ist. Der Datentyp des ersten Template-Arguments kann weiterhin beliebig sein.


// Allgemeines Klassen-Template
template <typename T1, typename T2> class Any
{
   T1 out;
   void Transmit(const T2& data)
   {
      out << ... // In Einzelbytes zerlegen
   }
};
// Partiell spezialisiertes Klassen-Template für den Datentyp string
template <typename T1> class Any<T1, string>
{
   T1 out;
   void Transmit(const string& data)
   {
      out << ... // Bytes über Iterator
   }
};
// Objekt Definitionen
Any<ostream,int> iTrans(cout);
Any<ostream,string> sTrans(outFile);

Beispiel:

Das Klassen-Template Collector sammelt beliebige Daten ein und übertragt diese dann in einen beliebigen Stream. Die einzusammelnden Daten werden in der Memberfunktion AddData(...) in einer STL Queue zwischengespeichert. Die Übertragung der eingesammelten Daten in den Stream erfolgt dann in der Memberfunktion Transmit(...). Transmit(...) überträgt die Daten byteweise in den Stream, wobei gilt, dass das High-Byte eines Datums immer zuerst übertragen wird. Da die Daten auf Intel-Prozessoren in umgekehrter Reihenfolge im Speicher liegen (Low-Byte zuerst), wird das Datum sozusagen rückwärts ausgegeben.

Dieses Verfahren funktioniert bei ganzzahligen Daten immer. Wird zum Beispiel jedoch ein Collector-Objekt für das Aufsammeln von strings definiert, so schlägt die Übertragung fehl. Dies liegt zum einen daran, dass der sizeof(...) Operator in der Memberfunktion Transmit(...) nicht die Länge des zu übertragenden Strings liefert und zum anderen dürfen strings auch nicht für die Übertragung umgedreht werden.

Aus diesem Grund wird für den Datentyp string das Klassen-Template partiell spezialisiert. Unabhängig vom Stream wird für den Datentyp string immer dieses partiell spezialisierte Klassen-Template vom Compiler verwendet.

Sie können zum Testen der Ausgabe in main(...) auch einmal anstelle des Standard-Ausgabestreams cout den Dateistream outFile an den Konstruktor der Template-Klasse übergeben. Die Ausgabe erfolgt dann in die Datei test.dat.

00000001,00000002,11223344,
Eins,Zwei,Drei,


// Beispiel für partielle Template-Spezialisierung

#include <iostream>
#include <fstream>
#include <iomanip>
#include <queue>
#include <string>

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

// Template-Klasse für das Aufsammeln von Daten die auf
// Anforderung in einen Stream übertragen werden können
// TOut ist der stream in den die Daten geschrieben werden
// und TData der Datentyp der Daten
template <typename TOut, typename TData>
class Collector
{
    TOut& out;                    // Referenz auf Ausgabestream
    std::queue<TData> data;       // Queue mit Daten
  public:
    // ctor, initialisiert Ausgabestream-Referenz
    Collector(TOut& out_): out(out_)
    {}
    // Datum hinzufügen
    void AddData(TData data_)
    {
        data.push(data_);
    }
    // Daten aus der Queue übertragen. Es wird immer das
    // High-Byte zuerst ausgegeben
    void Transmit()
    {
        // Ausgabe in Hex und mit führender 0
        out << std::hex << std::setfill('0');
        // Alle Daten aus der Queue ausgeben
        while (!data.empty())
        {
            // char-Zeiger auf aktuelles Datum setzen
            char* ptr = reinterpret_cast<char*>(&data.front());
            // Zeiger auf High-Byte setzen
            ptr += sizeof(TData)-1;
            // Nun alle Bytes nacheinander ausgeben, von High-Byte nach Low-Byte
            for (size_t byteCount=0; byteCount<sizeof(TData); byteCount++)
                out << std::setw(2) << static_cast<int>(*ptr--);
            out << ',';
            // Element aus Queue nach der Übertragung entfernen
            data.pop();
        }
        // Ausgabe wieder auf 'normal' zurückstellen
        out << std::dec << std::setfill(' ');
        out << endl;
    }
};
// Partiell spezialisiertes Template für den Datentyp string
// Strings werden niemals von High-Byte nach Low-Byte übertragen
template <typename TOut>
class Collector<TOut, string>
{
    TOut& out;
    std::vector<string> data;
  public:
    Collector(TOut& out_): out(out_)
    {}
    void AddData(const string& data_)
    {
        data.push_back(data_);
    }
    // Vereinfachte Version für string
    void Transmit()
    {
        for (typename std::vector<string>::iterator iter=data.begin(); iter!=data.end(); ++iter)
            out << *iter << ',';
        out << endl;
    }
};

// main() Funktion
int main()
{
    // Für Datenausgabe in Datei muss bei der Definition
    // eines Collector-Objekts der Ausgabestream 'cout'
    // durch 'outFile' ersetzt werden
    // std::ofstream outFile("test.dat");

    // Collector für numerische Daten definieren
    Collector<std::ostream, int> intCol(cout);
    intCol.AddData(1);
    intCol.AddData(2);
    intCol.AddData(0x11223344);
    intCol.Transmit();
    // Collector für string definieren
    Collector<std::ostream, std::string> stringCol(cout);
    stringCol.AddData(string("Eins"));
    stringCol.AddData(string("Zwei"));
    stringCol.AddData(string("Drei"));
    stringCol.Transmit();
}

Partielle Template-Spezialisierung und non-type Argumente

Außer für Datentypen lassen sich Templates auch für bestimmte Werte von non-type Template-Argumente spezialisieren. Solche Spezialisierungen werden zum Beispiel immer dann eingesetzt, wenn für bestimmte Werte der im allgemeinen Klassen-Template verwendete Algorithmus vereinfacht werden kann um die Ausführungsgeschwindigkeit des Programms zu erhöhen.

Um ein Klassen-Template für einen bestimmten Wert eines non-type Arguments zu spezialisieren, wird in der Template-Deklaration nur noch das Template-Argument, also nicht mehr das non-type Argument, angegeben. Zusätzlich muss dann nach dem Klassennamen in spitzen Klammern das Template-Argument sowie der Wert des non-type Arguments, für den das spezialisierte Klassen-Template verwendet werden soll, angegeben werden

Im Beispiel wird das Klassen-Template Any für den Fall spezialisiert, dass für das non-type Argument der Wert 32 angegeben wird.


// Allgemeines Klassen-Template
template <typename T, int SIZE> class Any
{...};
// Partiell spezialisiertes Klassen-Template für den Fall SIZE=32
tempalte <typename T> class Any<T,32>
{...};
// Objekte Definitionen
Any<short,5> obj1;     // Allg. Klassen-Template
Any<float,32> obj2;    // Spezialisiertes Klassen-Template

Enthält eine Template-Deklaration zwei non-type Argumente, so kann das Klassen-Template sogar für den Fall spezialisiert werden, dass beide non-type Argumente den gleich Wert besitzen, und dies sogar unabhängig vom eigentlichen Wert.

So wird im Beispiel Any für den Fall spezialisiert, dass das Template-Argument row den gleichen Wert besitzt wie das Template-Argument col. Beachten Sie dabei, dass innerhalb der Template-Deklaration nun nur noch ein non-type Argument steht, aber nach dem Klassennamen in der spitzen Klammer beide non-type Argumente aufgeführt werden müssen.


// Allgemeines Klassen-Template für unterschiedliche row, col Werte
template <typename T, int row, int col> class Any
{...};
// Partiell spezialisiertes Klassen-Template
// für den Fall, dass row gleich col ist
tempalte <typename T, int size> class Any<T,size,size>
{...};
// Objekte Definitionen
Any<short,3,5> obj1;     // Allg. Klassen-Template
Any<float,4,4> obj2;     // Spezialisiertes Klassen-Template

Zu guter Letzt kann ein Klassen-Template sogar dann spezialisiert werden, wenn überhaupt keine Template-Argumente in der Template-Deklaration vorhanden sind.

Die nachfolgende Klasse Image soll zum Zeichnen von Grafiken dienen. Für den Fall, dass eine Grafik mit der Größe 32x32 gezeichnet werden soll, kann zum Beispiel ein optimierter Algorithmus zum Zeichnen der Grafik verwendet werden. Beachten Sie, dass die Template-Deklaration des Klassen-Templates nun leer ist.


// Allgemeines Klassen-Template
template <int width, int height> class Image
{...};
// Partiell spezialisiertes Klassen-Template für den Fall width=height=32
template <> class Image<32,32>
{...};
// Objekte Definitionen
Image<640,480> obj1;     // Allgemeines Klassen-Template
Image<32,32>   obj2;     // Spezialisiertes Klassen-Template

Beispiel:

Das Klassen-Template Matrix dient zur Verarbeitung von Matrizen. Damit das Beispiel übersichtlich bleibt, enthält die Klassen lediglich Memberfunktionen zum Setzen der Matrix-Elemente, zur Ausgabe der Matrix und zum Transponieren der Matrix.

Das Transponieren einer Matrix erfolgt in der Form, dass die Spalten der Ausgangsmatrix zu Zeilen der Zielmatrix werden und umgekehrt. D.h. aus einer 3x4 Matrix wird nach dem Transponieren eine 4x3 Matrix.

Bei rechteckigen Matrizen muss für die neue, transponierte Matrix zuerst ein entsprechendes 2-dimensionales Feld dynamisch reserviert werden. Danach kann mit dem Umkopieren der Matrixelemente begonnen werden. Zum Schluss muss nach dem Kopiervorgang noch die ursprüngliche Matrix gelöscht werden.

Sollen quadratischen Matrizen (Zeilenanzahl gleich Spaltenanzahl) transponiert werden, so lässt sich dieser Algorithmus wesentlich vereinfachen. Zum einen muss kein neues 2-dimensionales Feld reserviert werden, da die Größe der Ausgangsmatrix ja identisch mit der Größe der neuen Matrix ist. Und zum anderen lässt sich auch der Kopiervorgang etwas verkürzen

Aus diesem Grund kann das Klassen-Template Matrix für den Fall einer quadratischen Matrix spezialisiert werden.

Ausgangsmatrix:
11 21 31 41
12 22 32 42
13 23 33 43
Transponiere rechteckige Matrix
Transponierte Matrix:
11 12 13
21 22 23
31 32 33
41 42 43

Ausgangsmatrix:
10.1 10.2 10.3 10.4
20.1 20.2 20.3 20.4
30.1 30.2 30.3 30.4
40.1 40.2 40.3 40.4
Transponiere quadratische Matrix
Transponierte Matrix:
10.1 20.1 30.1 40.1
10.2 20.2 30.2 40.2
10.3 20.3 30.3 30.4
10.4 20.4 40.3 40.4


// Beispiel für partielle Template-Spezialisierung
// aufgrund eines bestimmten Wertes

#include <iostream>
#include <iomanip>
#include <string>
#include <algorithm>

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

// Klassen-Template für allgemeine Matrix
// T = Datentyp der Elemente
// r = Anzahl der Zeilen
// c = Anzahl der Spalten
template <typename T, int r, int c>
class matrix
{
    T **data;      // Zeiger-Zeiger(!) auf dyn. 2D-Feld
    int col;       // Spalten
    int row;       // Zeilen
  public:
    // ctor, reserviert 2D-Feld dynamisch
    matrix()
    {
        row = r;   // Zeilen/Spalten merken
        col = c;
        // 2D-Feld dyn. reservieren
        data = new T*[row];
        for (int i=0; i<row; i++)
            data[i] = new T[col];
    }
    // Memberfunktionen zum Setzen eines Matrix-Elements
    // Zeilen/Spalten-Nr. beginnen ab 1!
    void SetData(int row_, int col_, T val)
    {
        if ((col_<1) || (col_>col) ||
            (row_<1) || (row_>row))
            throw "Falsch Zeile oder Spalte";
        data[row_-1][col_-1] = val;
    }
    // Matrix transponieren (Zeilen/Spalten vertauschen)
    void Transpose()
    {
        cout << "Transponiere rechteckige Matrix\n";
        // Neues 2D-Feld anlegen mit vertauschter Grösse
        // Spalte->Zeile, Zeile->Spalte
        T **temp;
        temp = new T*[col];
        for (int i=0; i<col; i++)
            temp[i] = new T[row];
        // Matrix transponieren
        for (int ri=0; ri<row; ri++)
            for (int ci=0; ci<col; ci++)
                temp[ci][ri] = data[ri][ci];
        // Alte Matrix nun löschen
        for (int i1=0; i1<row; i1++)
            delete [] data[i1];
        delete [] data;
        // Zeiger auf neue Matrix abspeichern
        data = temp;
        // Zeilen/Spalten Anzahl vertauschen
        std::swap(col,row);
    }
    // Matrix ausgeben
    void Print()
    {
        for (int ri=0; ri<row; ri++)
        {
            for (int ci=0; ci<col; ci++)
                cout << std::setw(5) << data[ri][ci];
            cout << endl;
        }
    }
};

// Partiell spezialisierte Matrix für eine quadratische
// Matrix (Zeilen=Spalten)
template <typename T, int s>
class matrix<T,s,s>
{
    T **data;      // dyn. 2D-Feld
    int size;      // Grösse der quadratischen Matrix
  public:
    // ctor, reserviert dyn. 2D-Feld
    matrix()
    {
        size = s;
        data = new T*[size];
        for (int i=0; i<size; i++)
            data[i] = new T[size];
    }
    // Memberfunktionen zum Setzen eines Matrix-Elements
    // Zeilen/Spalten-Nr. beginnen ab 1!
    void SetData(int row_, int col_, T val)
    {
        if ((col_<1) || (col_>size) ||
            (row_<1) || (row_>size))
            throw "Falsch Zeile oder Spalte";
        data[row_-1][col_-1] = val;
    }
    // Matrix transponieren
    // Bei quadratischen Matrizen muss kein temporäres 2D-Feld
    // angelegt werden; es können direkt die Matrix-Elemente
    // vertauscht werden
    void Transpose()
    {
        cout << "Transponiere quadratische Matrix\n";
        for (int ri=0; ri<size-2; ri++)
            for (int ci=ri+1;ci<=size-1; ci++)
                std::swap(data[ci][ri],data[ri][ci]);
    }
    // Matrix ausgeben
    void Print()
    {
        for (int ri=0; ri<size; ri++)
        {
            for (int ci=0; ci<size; ci++)
                cout << std::setw(5) << data[ri][ci];
            cout << endl;
        }
    }
};

// main() Funktion
int main()
{
    // Rechteckige Matrix anlegen
    matrix<short,3,4> rectMat;
    // Und mit Werten belegen
    for (int r=1; r<=3; r++)
        for (int c=1; c<=4; c++)
            rectMat.SetData(r,c,c*10+r);
    // Matrix ausgeben
    cout << "Ausgangsmatrix:\n";
    rectMat.Print();
    // Matrix transponieren und ausgeben
    rectMat.Transpose();
    cout << "Transponierte Matrix:\n";
    rectMat.Print();

    // Quadratische Matrix anlegen
    matrix<double,4,4> sqrMat;
    // Und mit Werten belegen
    for (int r=1; r<=4; r++)
        for (int c=1; c<=4; c++)
            sqrMat.SetData(r,c,double(r*100+c)/10.0);
    // Matrix ausgeben
    cout << "\nAusgangsmatrix:\n";
    sqrMat.Print();
    // Matrix transponieren und ausgeben
    sqrMat.Transpose();
    cout << "Transponierte Matrix:\n";
    sqrMat.Print();
}

Template-Template-Parameter

Nähern wir uns jetzt langsam dem Höhepunkt der Template-Spezialitäten und sehen uns die so genannten Template-Template-Parameter (TTP) an.

Wie der Name schon sagt, sind TTPs Parameter die selbst wiederum Templates sind. Innerhalb der Standard-Bibliothek werden solche TTPs sehr häufig eingesetzt, zum Beispiel um die Reservierung von Speicher vorzunehmen. Sehen wir uns dieses Einsatzgebiet von TTPs auch gleich anhand eines praktischen Beispiels an.

Vorgegeben sei das Klassen-Template Store zum Abspeichern von Daten. Der für das Datum erforderliche Speicherplatz soll je nach Anforderung entweder mittels des new-Operators oder mit der der malloc(...)-Funktion reserviert werden. Dazu erhält die Template-Deklaration einen bool-Wert als non-type Parameter. Besitzt der Parameter den Wert true, so erfolgt die Speicherreservierung mittels new und ansonsten mittels malloc(...).


// Klassen-Template
template <typename T, bool newAlloc> class Store
{
   T* data;
   Store(...)
   {
      if (newAlloc)
         data = new T;
      else
         data = static_cast<T*>(malloc(sizeof(T)));
         ...
   }
   ...
};
// Objekt Definitionen
Store<int,true>    intNew;
Store<float,false> floatMalloc;

Da die beiden verwendeten Verfahren zur Reservierung von Speicher auch noch an anderen Stellen eingesetzt werden sollen, werden sie aus dem ursprünglichen Klassen-Template herausgelöst und als eigenständige Klassen-Templates definiert.


// Klassen-Templates zum Reservieren von Speicher
template <typename T> struct NewAlloc    // Reserviert mit new
{...};
template <typename T> struct MallocAlloc // Reserviert mit malloc()
{...};
// Klassen-Template für Daten
template <typename T, ???> class Store
{...};
// Objekt Definitionen
Store<int,???>    intNew;
Store<float,???>  floatMalloc;

Bleibt damit aber die Frage offen, wie das neue Klassen-Template NewAlloc bzw. MallocAlloc für die Reservierung von Speicher an das Klassen-Template Storeübergeben wird und wie dann Objekte vom Typ Store definiert werden.

Wie Sie der Überschrift dieses Abschnitts entnehmen können, wird das neue Klassen-Template NewAlloc bzw. MallocAlloc wahrscheinlich als Template-Parameter übergeben werden.

Beginnen wir mit der Template-Deklaration eines Templates das einen Template-Parameter besitzt. Da das Template-Argument nun selbst ein Template ist, muss dies bei der Template-Deklaration wie nachfolgend dargestellt angegeben werden. Beachten Sie, dass beim <typename> des TTPs kein formaler Datentyp für den einzusetzenden Datentyp steht.

Wenn wir nun weiter davon ausgehen, dass die beide Klassen-Templates für die Speicherreservierung die Memberfunktion Create() enthalten um den Speicher anzufordern, so wird diese Memberfunktion wie angegeben aufgerufen. Der Platzhalter T gibt hier den Datentyp an, für den Speicher zu reservieren ist.


// Klassen-Templates für Speicherreservierung
// Sowohl NewAlloc wie auch MallocAlloc enthalten die
// Memberfunktion Create() (hier nicht dargestellt)
template <typename T> struct NewAlloc;
template <typename T> struct MallocAlloc;

// Klassen-Template für Daten
template <typename T, template <typename> class TAlloc>
class Store
{
   T* data;
   Store(...)
   {
      // Speicher reservieren!
      data = TAlloc<T>::Create();
   }
   ...
};

Ja es ist sogar möglich, für den TTP ein Default-Klassen-Template vorzugeben. Dazu wird einfach innerhalb der Template-Deklaration nach dem Namen des TTPs der Zuweisungsoperator gefolgt vom Namen des Default-Templates anzugeben.


// Klassen-Templates für Speicherreservierung
template <typename T> struct NewAlloc;
template <typename T> struct MallocAlloc;

// Klassen-Template für Daten
template <typename T, template <typename> class TAlloc=NewAlloc>
class Store
{...};

Bei der Definition eines Objekts eines Klassen-Templates mit einem TTP muss das als TTP zu verwendende Klassen-Template innerhalb der spitzen Klammern mit angegeben. Besitzt das Klassen-Template ein Default-TTP, so kann diese Angabe selbstverständlich auch entfallen.


// Objekt Definitionen
Store<int, NewAlloc> newInt;
Store<Demo, MallocAlloc> mallocObj;
Store<float> newFloat;     // Verwendet Default-Template

Im obigen Beispiel werden drei Objekte der Template-Klasse Store definiert. Das erste Objekt newInt speichert einen int-Wert ab und die Speicherreservierung erfolgt über das Klassen-Template NewAlloc. Im zweiten Objekte mallocObj wird ein Objekt der Klasse Demo abgespeichert, dessen Speicher über das Klassen-Template MallocAlloc reserviert wird. Und zu guter Letzt verwendet das Objekt newFloat das Default-Klassen-Template für die Speicherreservierung um einen float-Wert abzulegen.

Gingen die bisherigen Beispiele immer davon aus, dass das Klassen-Template des TTP den gleichen Datentyp verwendet wie das eigentliche Klassen-Template (d.h. NewAlloc bzw. MallocAlloc verwenden das gleiche T wie Store), so muss dies nicht immer zwangsläufig so sein. Im Beispiel unten verwendet das Klassen-Template Any den 'Datentyp' T1 und das Klassen-Template TP den Datentyp T2. Wird nun Objekt vom Typ Any definiert, so sind beide Datentypen und das Klassen-Template des TTPs zu spezifizieren.


// Klassen-Template mit TTP
template <typename T1, typename T2, template <typename> class TP>
class Any
{
   T1 data;
   DoSome(...)
   {
      TP<T2>::AnyMethode();
   }
   ...
};
// Objekt Definition
Any<short, float, TTPClass> obj;

Beispiel:

Das nachfolgende Beispiel verwendet zwei Klassen-Templates um Speicher dynamisch zu reservieren. Das Klassen-Template NewAlloc verwendet hierfür den new-Operator und das Klassen-Template MallocAlloc die malloc(...) Funktion. Beide Klassen-Templates enthalten die Memberfunktionen Create() und Destroy() um den Speicher zu reservieren bzw. ihn wieder freizugeben.

Eine 'kleine' Besonderheit ist zu beachten, wenn über das Klassen-Template MallocAlloc Speicher für Objekte reserviert werden soll. Da malloc(...) ja nur den Speicher anfordert, und nicht wie der new-Operator automatisch den Konstruktor des Objekts aufruft, muss der Konstruktoraufruf hier explizit erfolgen. Der Aufruf des Konstruktors erfolgt über den so genannten placement-new Operator. Der placement-new Operator reserviert selbst keinen Speicher, sondern initialisiert nur das an ihn übergebene Objekt. Die Speicheradresse des Objekts (wurde mittels malloc(...) vorher geholt) wird an den placement-new Operator übergeben. Ebenso muss beim Zerstören des Objekts nun auch der Destruktor explizit aufgerufen werden. Und erst nach dem Aufruf des Destruktors darf der Speicher freigegeben werden!

Vielleicht fragen Sie sich nun, wofür zwei Algorithmen zur Speicherreservierung eigentlich nützlich sein sollen. In der Praxis kann es durchaus einmal vorkommen, dass Objekte in einem bestimmten Speicherbereich abgelegt werden müssen. Denken Sie zum Beispiel einmal an eine Kommunikation zwischen zwei Prozessoren über ein so genanntes shared-memory. Ein solches shared-memory liegt in der Regel auf einer bestimmten Adresse und ist von mehreren Prozessoren aus erreichbar. Sollen jetzt die Eigenschaften eines Objekts von beiden Prozessor verwendet werden, so können Sie nicht einfach den new-Operator für die Definition des Objekts verwenden, da new Speicher auf dem Heap reserviert und nicht, wie für diesen Fall erforderlich, im shared-memory. Also schreiben Sie sich ein jetzt ein 'kleines' Klassen-Template um den Speicher in diesem shared-memory zu verwalten und Objekte darin anlegen zu können. Wenn Sie nun in Ihrer Anwendung Objekte definieren, so können Sie durch Einsatz von TTPs bestimmen, in welchem Speicherbereich das Objekt angelegt werden soll.

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

int mit MallocAlloc:
MallocAlloc::Create()
Datum: 10

string mit NewAlloc:
NewAlloc::Create()
Datum: Another string

float mit Default-Template:
NewAlloc::Create()
Datum: 3.1416

Demo mit MallocAlloc:
MallocAlloc::Create()
Datum:
Datum: A new string

Und alles wieder entfernen:
MallocAlloc::Destroy()
NewAlloc::Destroy()
NewAlloc::Destroy()
MallocAlloc::Destroy()


// Beispiel zu Template-Template Parameter

#include <iostream>
#include <string>
#include "demo.h"

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

// Templates zur Reservierung von Speicher.
// Templates werden nachher als Parameter eines weiteren
// Templates verwendet um die Art der Speicherreservierung
// zu bestimmen
// Hinweis: Die Templates sind jetzt vom Typ struct da
// alle Memberfunktionen public sind
// 1. Speicherreservierung mittels new
template <class T>
struct NewAlloc
{
    static T* Create()
    {
        cout << "NewAlloc::Create()" << endl;
        return new T;
    }
    static void Destroy(T *ptr)
    {
        cout << "NewAlloc::Destroy()" << endl;
        delete ptr;
    }
};
// 2. Speicherreservierung mittels malloc
// In der Praxis könnte z.B anstelle von malloc(...) die
// Reservierung von Speicher in einem bestimmten Bereich
// (z.B. shared-Memory) erfolgen
template <class T>
struct MallocAlloc
{
    static T* Create()
    {
        cout << "MallocAlloc::Create()" << endl;
        void *pMem = std::malloc(sizeof(T));
        // Wenn mit malloc Speicher fuer Objekte reserviert
        // wird, so muss der ctor des Objekts explizit mit
        // dem placement-new Operator aufgerufen werden
        if (pMem != NULL)
           return new(pMem) T;
        return NULL;
    }
    static void Destroy(T* ptr)
    {
        cout << "MallocAlloc::Destroy()" << endl;
        // Explizter Aufruf des dtors erforderlich!
        ptr->~T();
        free(ptr);
    }
};

// Template zum Abspeichern von Daten
// Template erhaelt als ersten Parameter den Datentyp des Datums
// und als zweiten Parameter das Template zur Reservierung des
// Speichers. Das Template zur Speicherreservierung erhaelt seinen
// Template-Parameter automatisch aus dem Datentyp-Parameter
template <typename T, template <typename> class TAlloc=NewAlloc >
class StoreData
{
    T* pData;      // Zeiger auf das abzulegende Datum
  public:
    StoreData()
    {
        // Aufruf der entsprechenden Create()-Memberfunktion
        // TAlloc ist Klassen-Template!
        pData = TAlloc<T>::Create();
    }
    ~StoreData()
    {
        // Aufruf der entsprechenden Destroy() Memberfunktion
        TAlloc<T>::Destroy(pData);
    }
    // Datum setzen
    void SetData(const T& data)
    {
        *pData = data;
    }
    // Datum ausgeben
    void PrintData()
    {
        cout << "Datum: " << *pData << endl;
    }
};

// main() Funktion
int main()
{
    // Platz fuer int-Wert mit malloc anlegen
    cout << "int mit MallocAlloc:\n";
    StoreData<int, MallocAlloc> intParam;
    intParam.SetData(10);
    intParam.PrintData();
    // Platz fuer string-Wert mit new anlegen
    cout << "\nstring mit NewAlloc:\n";
    StoreData<string, NewAlloc> stringParam;
    stringParam.SetData("Another string");
    stringParam.PrintData();
    // Platz fuer float-Wert mit Default-Allokierung anlegen
    cout << "\nfloat mit Default-Template:\n";
    StoreData<float> floatParam;
    floatParam.SetData(3.1416f);
    floatParam.PrintData();
    // Platz fuer Demo-Objekt mit malloc anlegen
    cout << "\nDemo mit MallocAlloc:\n";
    StoreData<Demo, MallocAlloc> classParam;
    classParam.PrintData();
    classParam.SetData(Demo("A new string"));
    classParam.PrintData();
    // Ab hier wird wieder aufgeräumt
    cout << "\nUnd alles wieder entfernen:\n";
}

Templates als Compile-Zeit Ausdrücke

Und nun folgt die Krönung und auch gleichzeitig der Abschluss der Template-Programmierung: Templates als Compile-Zeit Ausdrücke.

Mit der Einführung von Templates ergab sich eine (am Anfang nicht beabsichtigte) Möglichkeit, bereits zur Compile-Zeit Anweisungen ausführen zu lassen. Dadurch wirkt der Compiler quasi wie ein Interpreter, der das Ergebnis seiner 'Berechnungen' direkt in den ausführbaren Code einbauen kann.

Sehen wir uns dies an dem klassischen (aber nicht sehr praxisnahen) Beispiel der Fakultätsberechnung an.


// Beispiel für Compile-Time Ausdrücke
// Max. berechenbar ist 12! da ansonsten der Werteberich
// des unsigned long überschritten wird
// Sie wissen vielleicht noch, dass für non-type
// Template-Parameter keine Gleitkommazahlen zulässig sind!
#include <iostream>

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

// Allgemeines Klassen-Template für die Fakultätsberechnung
// Innerhalb des Klassen-Template wird ein neues Klassen-Template
// instanziiert das als Template-Argument den bisherigen Wert-1
// erhält
template <int val>
struct Fakultaet
{
   static const unsigned long value = val*Fakultaet<val-1>::value;
};
// Spezialisiertes Klassen-Template für Fakultät(0)
// Dies ist der Ausgang aus der Schleife der Template-Instanziierungen
template <>
struct Fakultaet<0>
{
   static const unsigned long value = 1;
};

// main() Funktion
int main()
{
   cout << Fakultaet<5>::value << endl;
}

Was passiert hier nun? Zunächst soll in main(...) die Eigenschaft value des Templates Fakultaet<5> ausgegeben werden. Also erzeugt der Compiler zunächst eines Instanz dieses Templates:

struct Fakultaet
{
   static const unsigned long value = 5*Fakulaet<4>::value;
}

Die obigen Literale 5 und 4 wurden aus dem Wert des non-type Template-Parameter val abgeleitet

Für die Berechnung von value benötigt er aber eine weitere Instanz dieses Templates, diesmal aber vom 'Typ' Fakultaet<4>:

struct Fakultaet
{
   static const unsigned long value = 5*4*Fakulaet<3>::value;
}

Und diese Spiel wiederholt sich so oft, bis der Wert des Template-Arguments schließlich 0 ist. Für diesen Wert ist das Template spezialisiert und liefert als value immer den Wert 1. Gleichzeitig wird die 'Schleife' der Template-Instanziierungen damit beendet und das endgültige Ergebnis kann nun in die cout-Anweisung in main(...) eingesetzt werden. Beachten Sie bitte, dass diese Berechnung komplett zur Compiler-Zeit vorgenommen wird, da sie letztendlich nur aus der Multiplikation von Konstanten besteht! Wenn Sie mit einem Debugger sich die cout-Anweisung einmal ansehen werden Sie feststellen, dass dort tatsächlich der Wert 120 (=5!) eingesetzt wurde.

Dies ist zugegeben ein recht 'einfaches' aber nicht praxisnahes Beispiel. Das nachfolgende Beispiel ist da schon etwas praxisnäher.

Da sich der Datentyp char (nicht signed-char oder unsigned-char!) in der Regel über Compiler-Optionen auf signed oder unsigned einstellen lässt, kann es unter Umständen zu Fehlern kommen, wenn das gleiche Programm mit unterschiedlichen Compiler-Optionen übersetzt. Das Klassen-Template WrongCharFormat hilft einen solchen Fehler schon zum Zeitpunkt des Übersetzens des Programms aufzudecken.

Unter Visual C++ können Sie den Datentyp eines char über die Eigenschaften des Projekts (Alt-F7) einstellen:



// Beispiel für Typueberpruefung zur Compile-Time

// BCC32: char = signed (default)
//               unsigned (Compile-Switch -K)
// VC++ : char = signed (default)
//               unsigned (Compile-Switch /J)
// MinGW: char = signed (default)
//               unsigned (Compile-Switch -funsigned-signed)

#include <iostream>
#include <limits>

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

// Vorwärtsdeklaration eines Templates das als
// Template-Argument einen bool-Datentyp besitzt
template <bool> struct WrongCharFormat;
// Vollständiges Template für den Fall dass der bool-
// Template-Parameter true ist.
template<> struct WrongCharFormat<true>
{};
// Funktion zur Typüberprüfung des char-Datentyps auf
// signed char.
// Dazu wird ein Objekt des Klassen-Templates WrongCharFormat
// instanziiert, dessen Template-Parameter nur dann true
// ist, wenn char einem signed char entspricht. Im anderen
// Fall wird versucht ein Objekt des allgemeinen Klassen-Templates
// WrongCharFormat zu erstellen, das aber aufgrund der
// unvollständigen Template-Definition fehlschlägt und 
// damit zur Compile-Time einen Fehler erzeugt
inline void CheckCharFormat()
{
   WrongCharFormat<std::numeric_limits<char>::is_signed == true>();
};

int main()
{
   // char-Format auf signed char testen
   CheckCharFormat();

}

Durch Abwandeln des Template-Arguments in der Funktion CheckCharFormat() lassen sich auch andere Datentyp-Prüfungen zur Compile-Zeit durchführen.

So, damit soll es jetzt genug sein mit der Deklaration und Definition von Templates! Das Thema 'Templates als Compile-Zeit Ausdrücke' ist sehr ergiebig. So lassen sich mit solchen Template-Konstruktionen u.a. Schleifen und Verzweigungen programmieren. Wer mehr darüber wissen will, sollte im Internet einmal nach dem Stichwort 'template metaprograms' suchen.