Einleitung
Definition eines Funktions-Templates
Aufruf von Funktions-Templates
Spezialisierung und Überschreiben von
Funktions-Templates
Lokale Daten mit formalen Datentyp
Mehrere Template-Parameter
Beispiel und Übung
In dieser und der folgenden Lektion werden wir uns mit Templates befassen. C++ kennt zwei Arten von Templates: Funktions-Templates und Klassen-Templates. Sie können sich unter einem Template eine Art Vorlage oder Vorschrift vorstellen, die dem Compiler mitteilt, wie eine Funktion oder Klasse generiert werden soll. In dieser Lektion werden wir uns zunächst einmal den einfacheren Fall ansehen, das Funktions-Template.
Ein Funktions-Template ist eine Vorlage für gleichartige Funktionen, die sich in einem oder mehreren der folgenden Punkte unterscheiden:
Entscheidend bei dieser Aufzählung ist aber das, was nicht dort steht, nämlich was die letztendlich aus einem Funktions-Templates erzeugten Funktionen gemeinsam haben. So müssen alle Funktionen die aus einem Template generiert werden z.B. die gleiche Anzahl von Parametern besitzen, was sie von überladenen Funktionen unterscheidet. Und außerdem besitzen alle Funktionen auch den gleichen Ablauf!
Vielleicht fragen Sie sich nun, was kann ich denn mit diesem Funktions-Template anfangen, wenn der Ablauf sowieso immer der gleiche ist? Sehen Sie sich dazu einmal die drei Funktionen unten an. Dort werden drei Funktionen Max(...) definiert, die alle das Gleiche tun: sie liefern von zwei als Parametern übergebenen Werten den größten Wert zurück. Der Unterschied liegt hier einzig und allein in den Datentypen der Parameter. Und damit sind die Funktionen ein geradezu klassisches Beispiel für ein Funktions-Template.
short Max(short p1, short p2) { return ((p1>p2)? p1 : p2); } long Max(long p1, long p2) { return ((p1>p2)? p1 : p2); } float Max(float p1, float p2) { return ((p1>p2)? p1 : p2); } |
Wenn Sie den Kurs bis hierher komplett durchgearbeitet haben, so sollten Sie eine solche Template-Funktion ja schon aus der Standard-Bibliothek her kennen.
|
|
Wie ein Funktions-Template prinzipiell definiert wird, soll jetzt anhand der vorhin aufgeführten Funktionen Max(...) demonstriert werden.
1. Schritt
Im ersten Schritt werden alle Funktionen bis auf eine entfernt und die Datentypen, die von Funktion zu Funktion unterschiedlich sind, durch einen beliebigen Namen ersetzt (der natürlich aber kein Schlüsselwort sein darf). Dieser beliebige Name wird auch als formaler Datentyp bezeichnet. Im nachfolgenden Beispiel wurden die Datentypen durch den Namen (Buchstaben) T ersetzt. T ist eine allgemein übliche Bezeichnung für einen Template-Datentyp.
// Ersetzen der Datentypen T Max(T p1, T p2) { return ((p1>p2)? p1 : p2); } |
2. Schritt
Im zweiten Schritt müssen wir dem Compiler nun etwas unter die Arme greifen. Damit er weiß, dass im Beispiel T nur ein Platzhalter für einen später noch festzulegenden Datentyp ist, wird vor die Funktion noch die Anweisung
| template <typename T> |
gesetzt. Wohlgemerkt, der Name des formalen Datentyps ist (fast) beliebig.
// Spezifikation des formalen Datentyps template <typename T> T Max(T p1, T p2) { return ((p1>p2)? p1 : p2); } |
Und fertig ist die Definition eines Funktions-Templates. Beachten Sie bitte, dass es sich hier nur um eine 'unvollständige' Definition handelt, d.h. der Compiler erzeugt noch keinen Code da er den tatsächlichen Datentyp des formalen Datentyps (hier T) natürlich zu diesem Zeitpunkt noch nicht kennt.
|
Hier wird anstelle des Schlüsselworts typename noch class verwendet. typename ist eines der jüngeren C++ Schlüsselwörter. Beide Template-Definitionen sind aber gleichwertig. |
Ist das Funktions-Template definiert, können Sie die damit festgelegte Funktion wie jede normale Funktion aufrufen (siehe Beispiel).
Trifft der Compiler beim Übersetzen des Programms auf den Aufruf einer Funktion, so führt er folgende Schritte durch:
// Templatedefinition
template <typename T> T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
// Funktionsaufrufe shortMax = Max(shortV1, shortV2); longMax = Max(longV1, longV2); floatMax = Max(floatV1, floatV2); // vom Compiler generierte Funktionen short Max(short p1, short p2) {....} long Max(long p1, long p2) {....} float Max(float p1, float p2) {....} |
|
|
|
|
Soweit, so gut. Doch was passiert nun, wenn Sie die Funktion Max(...) mit zwei C-Strings (char-Zeigern) aufrufen, um die Strings miteinander zu vergleichen? Da der Compiler beim Aufruf der Funktion die formalen Datentypen durch die tatsächlichen ersetzt, generiert er Ihnen die nachfolgend dargestellte Funktion. Doch diese vergleicht nicht die Strings sondern nur deren Adressen im Speicher!
// Fehlerhafter Einsatz eines Funktions-Templates // Templatedefinition template <typename T> T Max(T p1, T p2) { return ((p1>p2)? p1 : p2); } // Aufruf der Funktion char *pName1, *pName2, *pMax; ..... pMax = Max(pName1, pName2); // Vom Compiler generierte Funktion char* Max(char* p1, char* p2) { return ((p1>p2)? p1 : p2); } |
Was also tun? Zum einen können Templates für bestimmte Datentypen spezialisiert werden. Um für einen bestimmten Datentyp ein spezielles Funktions-Template zu erstellen, wird zunächst die template-Anweisung angegeben, jetzt jedoch mit einer leeren spitzen Klammer. Der Datentyp, für den dieses Funktions-Templates verwendet werden soll, wird dann nach dem Funktionsnamen in spitzen Klammer angegeben. Ergibt sich dieser Datentyp quasi von alleine aus den Parametern der Funktion, so kann die Angabe des Datentyps auch entfallen. Damit könnten Sie das Funktions-Template auch wie folgt schreiben:
// Allgemeine Templatedefinition
template <typename T> T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
// Spezielles Funktions-Template // Alternative Schreibweise // template <> char* Max(char *p1, char *p2) template<> char* Max<char*>(char *p1, char *p2) { if (strcmp(p1,p2) > 0) return p1; else return p2; } // Aufruf des speziellen Funktions-Template pMax = Max(pName1, pName2); |
Eine andere Möglichkeit besteht darin, explizit eine Funktion vorzugeben. Wie Sie bereits zuvor erfahren haben, schaut der Compiler vor der Generierung einer Funktion aus einem Funktions-Template zuerst immer nach, ob es schon eine Funktion mit den entsprechenden Datentypen der Parameter definiert ist. Gibt es eine solche, so ruft er auch diese auf. Für unseren Fall müssen Sie dazu explizit eine Funktion Max(...) schreiben, die zwei Parameter vom Typ char-Zeiger besitzt (und dort natürlich wie angegeben die Strings richtig vergleichen).
// Allgemeine Templatedefinition
template <typename T> T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
// Explizite Funktion zum Vergleich von C-Strings
char* Max(char* p1, char* p2)
{
if (strcmp(p1,p2) > 0)
return p1;
else
return p2;
}
|
Aber formale Datentypen können nicht nur als Parameter bei Funktions-Templates eingesetzt werden. Benötigen Sie innerhalb eines Funktions-Templates eine Variable vom gleichen Datentyp wie einer der Parameter, so können Sie auch hier anstelle eines bestimmten Datentyps den formalen Datentyp angeben. Bei der Generierung der Funktion durch den Compiler wird dann auch dieser formale Datentyp wieder durch den tatsächlichen Datentyp ersetzt. Die unten aufgeführte Funktion Swap(...) dient zum Vertauschen von Werten. Hierzu muss zuerst einer der Werte in eine lokale Variable umkopiert werden, die natürlich den gleichen Datentyp wie der übergebene Wert besitzen muss.
// Funktions-Template template <typename T> void Swap (T& p1, T& p2) { T temp = p1; p1= p2; p2 = temp; } // Aufruf der Funktion short var1, var2; .... Swap(var1, var2); // Vom Compiler generierte Funktion void Swap (short& p1, short& p2) { short temp = p1; p1 = p2; p2 = temp; } |
Und auch das ist möglich: Funktions-Templates können auch mehrere formale Datentypen besitzen. Die Anzahl der formalen Datentypen ist nicht begrenzt.
Wenn Sie sich das nachfolgende Beispiel einmal ansehen, werden Sie feststellen, dass die Parameter beim Aufruf des Funktions-Templates Func(...) unterschiedliche Datentypen besitzen. Und damit müssen Sie auch bei der Spezifikation des Funktions-Templates zwei formale Datentypen angeben. Wie dies zu erfolgen hat, ist im Beispiel dargestellt. Beachten Sie dabei bitte, dass auch beide formalen Datentypen innerhalb der spitzen Klammer der template-Anweisung angegeben werden müssen.
// Funktions-Template template <typename T1, typename T2> void Func(T1 p1, T2 p1) { T1 loc1 = p1; T2 loc2 = p2; .... } // Aufruf der Funktion
float fVar;
char *pChar;
....
Func (fVar, pChar);
// Vom Compiler generierte Funktion void Func(float p1, char* p2) { float loc1 = p1; char* loc2 = p2; .... } |
Außer dass Funktions-Template formale Datentypen als Parameter besitzen, können selbstverständlich Parameter auch 'normale' Datentypen besitzen. Im Beispiel erhält das Funktions-Template Func(...) als zweiten Parameter p2 einen int-Wert, der hier zusätzlich noch einen Defaultwert besitzt.
template <typename T> void Func(T p1, int p2=10)
{
....
}
|
|
|
Unsortierte long: Unsortiert double: |
// Beispiel zu Funktionstemplates // Zuerst Dateien einbinden #include <iostream> using std::cout; using std::endl; // Funktionstemplate zum Tauschen von Werten beliebigen Datentyps template <typename T> void Swap(T& val1, T& val2) { T temp(val1); // temp mit val1 initialisieren! val1 = val2; // Damit könnten Sie sogar Objekt tauschen val2 = temp; // wenn diese den Operator = definieren } // Funktionstemplate zum Sortieren von // beliebigen Datentypen innerhalb eines Feldes // pValues ist der Zeiger auf den Beginn des Datenfeldes // und nNoOfValues enthält die Anzahl der Daten template <typename T> void Sort(T pValues, int const noOfValues) { bool changed; // Tauschflag // Tauschschleife do { // Tauschflag löschen changed = false; // Alle Elemente vergleichen for (int index=0; index<noOfValues-1; index++) { // Falls getauscht werden muss if (pValues[index]>pValues[index+1]) { // Werte tauschen Swap(pValues[index],pValues[index+1]); // Tauschflag setzen changed = true; } } } while (changed); // Schleife so lange durchlaufen, bis nicht mehr getauscht wurde } // main() Funktion int main() { int index; // Schleifenindex // Felder mit den zu sortierenden Daten long longArray[] = {1,-10,-2,20}; double doubleArray[] = {1.1, 0.9, -1.2, 5.5}; // Anzahl der Elemente in den Feldern berechnen! const int noOfLongs = sizeof(longArray)/sizeof(longArray[0]); const int noOfDouble = sizeof(doubleArray)/sizeof(doubleArray[0]); // unsortiertes long-Feld ausgeben cout << "Unsortierte long:\n"; for (index=0; index<noOfLongs; index++) cout << longArray[index] << ", "; cout << endl; // -> erzeugt Funktion: void Sort(long*, int); Sort(longArray,noOfLongs); // sortiertes long-Feld ausgeben cout << "Sortierte long:\n"; for (index=0; index<noOfLongs; index++) cout << longArray[index] << ", "; cout << endl << endl; // unsortiertes double Feld ausgeben cout << "Unsortiert double:\n"; for (index=0; index<noOfDouble; index++) cout << doubleArray[index] << ", "; cout << endl; // -> erzeugt Funktion: void Sort(double*, int); Sort(doubleArray,noOfDouble); // sortiertes double-Feld ausgeben cout << "Sortierte double:\n"; for (index=0; index<noOfDouble; index++) cout << doubleArray[index] << ", "; cout << endl << endl; } |
Mit Hilfe der im Beispiel aufgeführten Funktions-Templates Swap(...) und Sort(...) soll nun eine Adressenliste alphabethisch sortiert werden.
Die Adressdaten Name und Wohnort sind als string-Objekte innerhalb der Adresse abgelegt.
Die Adressenliste selbst wird durch ein Objektfeld vom Typ Address implementiert und ist im nachfolgenden Ausgangslisting ebenfalls bereits vorgegeben. Für die Ausgabe der Adressdaten wird der überladene Operator << verwendet.
In main() wird eine Adressenliste für vier Einträge dynamisch erstellt und mit Adressdaten belegt. Zur Kontrolle werden die Adressdaten ausgeben.
Ihre Aufgabe ist es nun, diese Adressenliste mit Hilfe der beiden Funktions-Templates Sort(...) und Swap(...) alphabethisch nach Namen zu sortieren.
So einfach diese Übung am Anfang auch scheinen mag, hier steckt die Schwierigkeit im Detail. Damit Sie sich nicht all zu sehr verirren, noch ein paar Hinweise zur Lösung:
So und nun viel Spaß bei der Lösung der Aufgabe. Aber nicht gleich aufgeben wenn es innerhalb der ersten 15 Minuten nicht gleich funktionieren sollte.
Das Ausgangslisting:
// Ausgangslisten zur Übung zu Funktionstemplates // Zuerst Dateien einbinden #include <iostream> #include <string> using std::cout; using std::endl; // Funktionstemplate zum Tauschen von Werten beliebigen Datentyps template <typename T> void Swap(T& val1, T& val2) { T temp(val1); // temp mit val1 initialisieren! val1 = val2; // Parameter vertauschen val2 = temp; } // Funktionstemplate zum Sortieren von // beliebigen Datentypen innerhalb eines Feldes // pValues ist der Zeiger auf den Beginn des Datenfeldes // und nNoOfValues enthält die Anzahl der Daten template <typename T> void Sort(T pValues, int const noOfValues) { bool changed; // Tauschflag // Tauschschleife do { // Tauschflag löschen changed = false; // Alle Elemente vergleichen for (int index=0; index<noOfValues-1; index++) { // Falls getauscht werden muss if (pValues[index]>pValues[index+1]) { // Werte tauschen Swap(pValues[index],pValues[index+1]); // Tauschflag setzen changed = true; } } } while (changed); // Schleife so lange durchlaufen, bis nicht mehr getauscht wurde } // Definition der Klasse für die Adressdaten class Address { std::string name; // Name std::string location; // Ort public: Address() // ctor, hat nichts zu tun {} // Adressdaten setzen void SetData(const char* const pN, const char* const pL); // Überladener << Operator für Ausgabe friend std::ostream& operator << (std::ostream& os, const Address& obj2); }; // Definition der Memberfunktionen // Setzen der Adressdaten void Address::SetData(const char* const pN, const char* const pL) { name = pN; location = pL; } // Überladener Operator << für Ausgabe std::ostream& operator << (std::ostream& os, const Address& addr) { os << "Name: " << addr.name; os << " Ort: " << addr.location << endl; return os; } // main() Funktion int main() { int index; // Schleifenindex // Objektfeld für Adressdaten anlegen const int SIZE = 4; Address *pAddress = new Address[SIZE]; // Objektfeld mit Daten belegen pAddress[0].SetData("Karl Maier","AStadt"); pAddress[1].SetData("Agathe Mueller","XDorf"); pAddress[2].SetData("Xaver Lehmann","CHausen"); pAddress[3].SetData("Berta Schmitt","FStadt"); // unsortiertes Objektfeld ausgeben cout << "Unsortierte Adressen:\n"; for (index=0; index<SIZE; index++) cout << pAddress[index]; // Hier für die Übung die Adressen sortieren und // erneut ausgeben // Objektfeld löschen delete [] pAddress; } |
|
Unsortierte Adressen: Sortierte Adressen: |