Member-Templates
Template Spezialisierungen
Partielle Template-Spezialisierung
Partielle Template-Spezialisierung und non-type Argumente
Template-Template-Parameter
Templates als Compile-Zeit Ausdrücke
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) {...} |
|
|
|
|
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; } |
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 |
|
|
|
|
|
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 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(); } |
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); |
|
|
00000001,00000002,11223344, |
// 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(); } |
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 |
|
|
Ausgangsmatrix:
Ausgangsmatrix: |
// 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(); } |
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; |
|
|
|
|
int mit MallocAlloc:
string mit NewAlloc:
float mit Default-Template:
Demo mit MallocAlloc:
Und alles wieder entfernen: |
// 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"; } |
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.
|
|
// 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.