C++ Kurs

Funktions-Templates

Die Themen:

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

Einleitung

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:

  1. dem Returntyp der Funktion
  2. den Datentypen der Parameter
  3. den Datentypen von lokalen Variablen

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.

Mit Ihrem bisherigen Wissen könnten Sie hier vielleicht auf die Idee kommen, für eine solche Funktion ein #define Makro einzusetzen (siehe hier). Dies wäre hier prinzipiell auch möglich, doch wird damit die Typüberprüfung der Parameter durch den Compiler umgangen. Wenn Sie anstelle einer Funktion Max(...) ein entsprechendes Makro einsetzen, so können hierbei die beiden Parameter unterschiedliche Datentypen besitzen ohne das es zu einer Fehlermeldung kommt.

Definition eines Funktions-Templates

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.

In vielen Programmen werden Sie Funktions-Templates noch wie folgt definiert finden:

template <class T> T Max(T p1, T p2)
{
    return ((p1>p2)? p1 : p2);
}

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.

Aufruf von Funktions-Templates

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:

  1. Zuerst wird abgeprüft, ob es bereits eine Funktion gibt die exakt zu den angegebenen Datentypen beim Funktionsaufruf passt. Ist dies der Fall, wird diese Funktion aufgerufen.
  2. Gibt es keine entsprechende Funktion, so wird nach einem Funktions-Template gesucht. Gibt es ein solches, so wird der formale Datentyp (im Beispiel T) durch den tatsächlichen Datentyp der Parameter beim Funktionsaufruf ersetzt und eine entsprechende Funktion automatisch durch den Compiler generiert. Im nachfolgenden Beispiel werden also drei Funktionen durch den Compiler erstellt, wobei der formale Datentyp T nacheinander durch die Datentypen short, long und float ersetzt wird.

// 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)
{....}

Arbeiten Sie mit getrennten Dateien für Deklarationen von Funktionen (Header-Dateien) und deren Definitionen (Quellcode-Dateien), so müssen Sie das Funktions-Template immer mit in die Header-Datei aufnehmen! Die endgültige Funktion wird ja erst beim Aufruf der Funktion aus dem Funktions-Template generiert, und dazu benötigt der Compiler den Code der Funktion.

Bei der Auflösung eines Templates wird niemals eine automatische Typkonvertierung vorgenommen. Wird z.B. versucht, für den ersten Parameter der Funktion Max(...) einen short Wert anzugeben und für den zweiten Parameter einen char Wert, so müsste der Compiler den formalen Datentyp durch den Datentyp short und char ersetzen. Da aber der formale Datentyp T nur für einen bestimmten Datentyp stehen kann, meldet der Compiler hier einen Fehler! Wollen Sie trotzdem diesen Vergleich durchführen (ohne ein weiteres Funktions-Template zu spezifizieren), so müssen Sie beim Aufruf der Funktion eine entsprechende Typkonvertierung vornehmen.

Spezialisierung und Überschreiben von Funktions-Templates

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;
}

Lokale Daten mit formalen Datentyp

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;
}

Mehrere Template-Parameter

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)
{
    ....
}

Beispiel und Übung

Beispiel:

Das Beispiel zeigt die Anwendung von Funktions-Templates zum Sortieren von Feldern.

Das Funktions-Template Sort(...) zum Sortieren erhält als Parameter einen Zeiger auf den Beginn des zu sortierenden Feldes sowie die Anzahl der Feldelemente. Innerhalb von Sort(...) wird ein weiteres Funktions-Template Swap(...) aufgerufen, das zwei beliebige Datenelemente vertauscht.

In main() wird dann ein long- und ein double-Feld definiert und mit beliebigen Werten gefüllt. Sehen Sie sich in diesem Zusammenhang auch noch einmal an, wie die Anzahl der Feldelemente berechnet wird. Da die Feldgrößen durch die Anzahl der Elemente bei der Felddefinition festgelegt wird, muss hier die Anzahl der Elemente explizit berechnet werden.

Beide Felder werden dann einmal im unsortierten Zustand ausgegeben, dann durch Aufruf des Funktions-Template Sort(...) sortiert und zum Schluss im sortierten Zustand nochmals ausgegeben.

Unsortierte long:
1, -10, -2, 20,
Sortierte long:
-10, -2, 1, 20,

Unsortiert double:
1.1, 0.9, -1.2, 5.5,
Sortierte double: -1.2, 0.9, 1.1, 5.5,


// 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;

}

Übung:

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:

  • An den beiden Funktions-Templates sind keinerlei Änderungen notwendig.
  • Die Klasse Address benötigt zwei zusätzliche überladene Operatoren. Welche das sind, das sollen Sie selbst herausfinden. Als kleiner Tipp: Sehen Sie sich die Funktions-Templates einmal genauer an, welche Operatoren dort verwendet werden.
  • Zusätzlich müssen Sie der Klasse Address noch einen weiteren, ganz bestimmten Konstruktor hinzufügen. Dieser wird vom Funktions-Template Swap(...) benötigt.

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:
Name: Karl Maier Ort: AStadt
Name: Agathe Mueller Ort: XDorf
Name: Xaver Lehmann Ort: CHausen
Name: Berta Schmitt Ort: FStadt

Sortierte Adressen:
Name: Agathe Mueller Ort: XDorf
Name: Berta Schmitt Ort: FStadt
Name: Karl Maier Ort: AStadt
Name: Xaver Lehmann Ort: CHausen

Lösung ansehen!