C++-Programmierung/ Templates/ Funktionstemplates


Ein Funktionstemplate ist eine „Mustervorlage“ oder „Schablone“, die dem Compiler mitteilt, wie eine Funktion erzeugt werden soll. Aus einem Template können semantisch gleichartige Funktionen mit verschiedenen Parametertypen erzeugt werden. Für alle, die eben in Panik zu Wiktionary oder einem noch schwereren Wörterbuch gegriffen haben: semantisch heißt hier so viel, wie allgemein gehalten in Bezug auf den Datentyp. Die Funktion hat also immer die gleiche Parameteranzahl, die Parameter haben aber unterschiedliche Datentypen. Das ist zwar nicht ganz korrekt ausgedrückt, aber eine genaue Definition würde das Wort wieder so unverständlich machen, dass es ohne Erklärung weniger Schrecken verbreitet hätte. Lesen Sie einfach das Kapitel, dann werden Sie auf einige der „fehlenden Klauseln“ selbst aufmerksam werden.

Das nachfolgende Template kann Funktionen generieren, welche die größere von zwei Zahlen zurückliefern. Der Datentyp spielt dabei erst einmal keine Rolle. Wichtig ist nur, dass es zwei Variablen gleichen Typs sind. Die Funktion liefert dann einen Wert zurück, der ebenfalls dem Datentyp der Parameter entspricht.

template <typename T>
T maxi(T obj1, T obj2){
    if(obj1 > obj2)
        return obj1;
    else
        return obj2;
}

Das Template wird mit dem gleichnamigen, aber kleingeschriebenen Schlüsselwort template eingeleitet. Danach folgt eine spitze Klammer und ein weiteres Schlüsselwort namens typename. Früher hat man an dieser Stelle auch das gleichbedeutende Schlüsselwort class benutzt. Das funktioniert heute immer noch, ist aber nicht mehr üblich. Erstens symbolisiert typename besser was folgt, nämlich einen Platzhalter, der für einen Datentyp steht, und zweitens ist es einfach übersichtlicher. Dies gilt insbesondere bei Klassentemplates, die aber Thema des nächsten Kapitels sind.

Als Nächstes folgt wie schon gesagt der Platzhalter. Er kann die gleichen Namen haben wie eine Variable sie haben könnte. Es ist aber allgemein üblich, den Platzhalter als T (für Typ) zu bezeichnen. Das soll natürlich nur ein Vorschlag sein – viele Programmierer haben da auch ihren eigenen Stil. Wichtig ist nur, dass Sie sich nicht zu viele Bezeichner suchen, es sei denn Sie haben ein ausgesprochenes Talent dafür, aussagekräftige Namen zu finden. Am Ende dieser Einführung wird die spitze Klammer geschlossen. Dann wird ganz normal die Funktion geschrieben mit der einen Ausnahme, dass anstatt eines genauen Datentypes einfach der Platzhalter angegeben wird.

Sie können diese Funktion dann z.B. mit int- oder double-Werten aufrufen:

int a = 3, b = 5;
int m = maxi(a, b);                    // m == 5
m = maxi(3, 7);                        // m == 7
double a = 3.3, b = 3.4;
double m = maxi(a, b);                  // m == 3.4
m = maxi(a, 6.33);                     // m == 6.33

Bei jedem ersten Aufruf einer solchen Funktion erstellt der Compiler aus dem Template eine echte Funktion, wobei er den Platzhalter durch den tatsächlichen Datentyp ersetzt. Welcher Datentyp das ist, entscheidet der Compiler anhand der übergebenen Argumente. Bei diesem Aufruf findet keine Typumwandlung statt. Es ist also nicht möglich, einen int- und einen double-Wert an die Templatefunktion zu übergeben. Das nächste Beispiel wird das verdeutlichen:

int a = 3;
double b = 5.4;
double m;                         // Ergebnis

m = maxi(a, b);                  // funktioniert nicht
m = maxi(4.5, 3);                // funktioniert nicht
m = maxi(4.5, 3.0);              // funktioniert
m = maxi(a, 4.8);                // funktioniert nicht
m = maxi(7, b);                  // funktioniert nicht
m = maxi(a, 5);                  // funktioniert
m = maxi(7.0, b);                // funktioniert

Möchten Sie eine andere Variante als jene, die anhand der Argumente aufgerufen wird benutzen, müssen Sie die gewünschte Variante explizit angeben:

double a = 3.0, b = 5.4;
double m = maxi<int>(a, b);             // m == 5.0
int n = maxi<int>(a, b);             // n == 5

Diese Templatefunktion kann nun mit allen Datentypen aufgerufen werden, die sich nach der Ersetzung des Platzhalters durch den konkreten Datentyp auch übersetzen lassen. In unserem Fall wäre die einzige Voraussetzung, dass auf den Datentyp der Operator > angewandt werden kann und ein Kopierkonstruktor vorhanden ist, da die Werte mit „call by value“, also als Kopie übergeben werden.

Das ist beispielsweise auch bei C++-Zeichenketten, also Objekte der Klasse std::string mit Headerdatei: string, der Fall. Daher ist folgendes problemlos möglich:

std::string s1("alpha");
std::string s2("omega");
std::string smax = maxi(s1,s2);        //smax == omega

Spezialisierung

Bearbeiten

Im Gegensatz zu normalen Funktionen können Templatefunktionen nicht einfach überladen werden, da das Template ja eine Schablone für Funktionen ist und somit bereits für jeden Datentyp eine Überladung bereitstellt. Manchmal ist es aber so, dass das allgemein gehaltene Template für bestimmte Datentypen keine sinnvolle Funktion erzeugt. In unserem Beispiel würde dies für C-Strings zutreffen:

const char* maxi(const char* str1, const char* str2){
    if(str1 > str2)
        return str1;
    else
        return str2;
}

Beim Aufruf von maxi("Ich bin ein String!", "Ich auch!") würde die oben stehende Funktion aus dem Template erzeugt. Das Ergebnis wäre aber nicht wie gewünscht die dem ASCII-Code nach größere Zeichenkette, sondern einfach die, dessen Adresse im Arbeitsspeicher größer ist. Um C-Strings zu vergleichen gibt es eine Funktion in der Headerdatei cstring (oder in der veralteten Fassung string.h).

Um nun dem Compiler beizubringen, wie er maxi() für C-Strings zu implementieren hat, muss diese Variante spezialisiert werden. Das heißt, es wird für einen bestimmten Datentyp (in diesem Fall const char*) eine von der Mustervorlage (also dem Template) abweichende Version definiert. Für das nächste Beispiel muss die Headerdatei cstring inkludiert werden.

template <>
const char* maxi(const char* str1, const char* str2){
    if(strcmp(str1, str2) > 0) // strcmp vergleicht zwei C-Strings
        return str1;
    else
        return str2;
}

Eine Spezialisierung leitet man immer mit dem Schlüsselwort template gefolgt von einer öffnenden und einer schließenden spitzen Klammer ein. Der Name der Funktion ist identisch mit dem des Templates (maxi), und alle T werden durch den konkreten Typ const char* ersetzt. Der Rumpf der Funktion kann dann völlig beliebig gestaltet werden. Natürlich ist es Sinn und Zweck der Sache, ihn so zu schreiben, dass die Spezialisierung das gewünschte sinnvolle Ergebnis liefert.

Übrigens wäre es auch möglich, den als Beispiel dienenden Aufruf von oben als maxi<std::string>("Ich bin ein String!", "Ich auch!") zu schreiben. Dann würden die beiden C-Strings vor dem Aufruf in C++-Strings umgewandelt, und die können ja problemlos mit > verglichen werden. Diese Methode hat aber drei entscheidende Nachteile gegenüber einer Spezialisierung für C-Strings:

  • Die Headerdatei string muss jedes Mal inkludiert werden
  • Der Aufruf von maxi("Ich bin ein String!", "Ich auch!") ist weiterhin problemlos möglich und liefert auch weiterhin ein dem Zufall überlassenes Ergebnis
  • Diese Art der Lösung ist viel langsamer als die Variante mit der Spezialisierung

Zwischen Spezialisierung und Überladung

Bearbeiten

Die folgende Überladung der Templatefunktion maxi() ist ebenfalls problemlos möglich und scheint auf den ersten Blick prima zu funktionieren:

// template <> - ohne das ist es keine Spezialisierung, sondern eine Überladung
const char* maxi(const char* str1, const char* str2){
    if(strcmp(str1, str2) > 0)
        return str1;
    else
        return str2;
}
Hinweis

Um es gleich vorwegzunehmen: das hier beschriebene Problem lässt sich leicht herbeiführen, ist aber nur sehr schwer zu finden. Daher achten Sie darauf, es zu vermeiden. Vergessen Sie auf keinen Fall template <> voranzustellen, wenn Sie ein Template spezialisieren.

Das Ausführen der nächsten beiden Codezeilen führt zum Aufruf verschiedener Funktionen. Die Funktionen haben identische Parametertypen und Namen sowie die gleiche Parameteranzahl, aber eben nicht den gleichen Code.

// Überladene Version - Ergebnis korrekt
cout << maxi("Ich bin ein String!", "Ich auch!");

// Vom Template erzeugte Version - Ergebnis zufällig (Speicheradressen)
cout << maxi<const char*>("Ich bin ein String!", "Ich auch!");

So etwas sollten Sie um jeden Preis vermeiden! Selbst ein erfahrener Programmierer dürfte über einen Fehler dieser Art erst einmal sehr erstaunt sein. Sie können die beiden Codezeilen ja jemandem zeigen, der sich gut auskennt und ihn fragen, was der Grund dafür sein könnte, dass die Varianten zwei verschiedene Ergebnisse liefern. Ohne die Implementierung von maxi() zu sehen wird er höchst wahrscheinlich nicht darauf kommen. Ein solcher Fehler ist nur schwer zu finden. Es ist bereits eine Katastrophe, ihn überhaupt erst einmal zu machen. Merken Sie sich also unbedingt, dass man template <> nur vor eine Funktion schreibt, wenn man für einen bestimmten Datentyp die Funktionsweise ändern will, weil die Standardvorlage kein sinnvolles Ergebnis liefert.

Überladen von Template-Funktionen

Bearbeiten

Ein Template ist in der Regel eine Vorlage für Funktionen, deren Parameter sich lediglich im Typ unterscheiden. Das Überladen von Funktionen ist aber auch durch eine andere Parameterzahl oder eine andere Anordnung der Parametertypen möglich. Unter dieser Überschrift wird die maxi()-Template um einen optionalen dritten Parameter erweitert. Was bisher vorgestellt wurde, wird in diesem Beispielprogramm zusammengefasst:

#include <iostream>          // Ein und Ausgabe
#include <string>            // C++-Strings
#include <cstring>           // Vergleiche von C-Strings

using namespace std;

// Unsere Standardvorlage für maxi()
template <typename T>
T maxi(T obj1, T obj2){
    if(obj1 > obj2)
        return obj1;
    else
        return obj2;
}

// Die Spezialisierungen für C-Strings
template <>                                           // Spezialisierung
const char* maxi(const char* str1, const char* str2){ // statt T, const char*
    if(strcmp(str1, str2) > 0)
        return str1;
    else
        return str2;
}

int main(){
    int    a = 3;                         // Ganzzahlige Variable
    double b = 5.4;                       // Gleitkommazahl-Variable

    int    m;                             // Ganzzahl-Ergebnis
    double n;                             // Kommazahl-Ergebnis

    // Je nachdem, wo die Typumwandlungen stattfinden,
    // sind die Ergebnisse unterschiedlich
    m = maxi<int>(a, b);                  // m == 5
    n = maxi<int>(a, b);                  // n == 5.0
    m = maxi<double>(a, b);               // m == 5
    n = maxi<double>(a, b);               // n == 5.4

    // Aufgerufen wird unabhänngig vom Ergebnistyp m:
    m = maxi(3, 6);                       // maxi<int>()
    m = maxi(3.0, 6.0);                   // maxi<double>()
    m = maxi<int>(3.0, 6);                // maxi<int>()
    m = maxi<double>(3, 6.0);             // maxi<double>()

    // Aufruf von maxi<std::string>()
    string s1("alpha");
    string s2("omega");
    string smax = maxi(s1,s2);           // smax == omega

    // Aufruf von maxi<const char*>()
    smax = maxi("alpha", "omega");        // Spezialisierung wird aufgerufen

    return 0;                            // Programm erfolgreich durchlaufen
}

Eine Ausgabe enthält das Programm noch nicht, aber das sollten Sie ja problemlos schaffen. Die Datei iostream ist auch schon inkludiert.

Nun aber zur Überladung der Templatefunktion: um von maxi() zu verlangen, den größten von drei Werten zurückzugeben, muss der Parameterliste ein weiterer Platzhalter, also noch ein T obj3 hinzugefügt werden. Gleiches gilt für unsere C-String-Spezialisierung. Da wir ja aber auch die Möglichkeit haben wollen, nur den größeren von zwei Werten zu bestimmen, müssen wir die bisherige Variante stehen lassen und die mit drei Argumenten hinzufügen. Das Ganze sieht dann so aus:

#include <iostream>          // Ein und Ausgabe
#include <string>            // C++-Strings
#include <cstring>           // Vergleiche von C-Strings

using namespace std;

// Unsere Standardvorlage für maxi() mit 2 Argumenten
template <typename T>
T maxi(T obj1, T obj2){
  if (obj1 > obj2)
    return obj1;
  else
    return obj2;
}

// Die Spezialisierungen für C-Strings mit zwei Argumenten
template <> // Spezialisierung
const char* maxi(const char* str1, const char* str2){
    if(strcmp(str1, str2) > 0)
        return str1;
    else
        return str2;
}

// Unsere Standardvorlage für maxi() mit drei Argumenten
template <typename T>
T maxi(T obj1, T obj2, T obj3){
    if(obj1 > obj2)
        if(obj1 > obj3)
            return obj1;
        else
            return obj3;
    else if(obj2 > obj3)
        return obj2;
    else
        return obj3;
}

// Die Spezialisierungen für C-Strings mit drei Argumenten
template <> // Spezialisierung
const char* maxi(const char* str1, const char* str2, const char* str3){
    if(strcmp(str1, str2) > 0){
        if(strcmp(str1, str3) > 0)
            return str1;
        else
            return str3;
    }
    else if(strcmp(str2, str3) > 0)
        return str2;
    else
        return str3;
}

int main(){
    cout << "Beispiele für Ganzzahlen:\n";
    cout << "      (2, 7, 4) -> " << maxi(2, 7, 4) << endl;
    cout << "         (2, 7) -> " << maxi(2, 7)    << endl;
    cout << "         (7, 4) -> " << maxi(7, 4)    << endl;

    cout << "\nBeispiele für Kommazahlen:\n";
    cout << "(5.7, 3.3, 8.1) -> " << maxi(5.7, 3.3, 8.1) << endl;
    cout << "     (7.7, 7.6) -> " << maxi(7.7, 7.6)      << endl;
    cout << "     (1.9, 0.4) -> " << maxi(1.9, 0.4)      << endl;

    cout << "\nBeispiele für C-Strings:\n";
    cout << "(ghi, abc, def) -> " << maxi("ghi", "abc", "def") << endl;
    cout << "     (ABC, abc) -> " << maxi("ABC", "abc")        << endl;
    cout << "     (def, abc) -> " << maxi("def", "abc")        << endl;

    return 0;                            // Programm erfolgreich durchlaufen
}
Ausgabe:
Beispiele für Ganzzahlen:
      (2, 7, 4) -> 7
         (2, 7) -> 7
         (7, 4) -> 7

Beispiele für Kommazahlen:
(5.7, 3.3, 8.1) -> 8.1
     (7.7, 7.6) -> 7.7
     (1.9, 0.4) -> 1.9

Beispiele für C-Strings:
(ghi, abc, def) -> ghi
     (ABC, abc) -> abc
     (def, abc) -> def

So Sie verstanden haben, wie aus einer Template eine konkrete Funktion erzeugt wird, fragen Sie sich vielleicht, ob das wirklich so einfach ist. Glücklicherweise kann diese Frage bejaht werden. Man stellt sich einfach das Funktionstemplate als mehrere separate Funktionen vor, die durch unterschiedliche Parametertypen überladen sind und wendet dann die üblichen Regeln zur Überladung von Funktionen an. Wenn man, wie in diesem Fall getan, ein weiteres Funktionstemplate anlegt, werden dadurch natürlich auch gleich wieder eine ganze Reihe einzelner Funktionen hinzugefügt. Im Endeffekt überlädt man also nicht das Template, sondern immer noch die aus ihm erzeugten Funktionen. Was Spezialisierungen angeht, gehören die natürlich immer zu dem Template, das eine Verbindung zu seiner semantischen Parameterliste besitzt.

Templates mit mehreren Parametern

Bearbeiten

Natürlich kann ein Template auch mehr als nur einen Templateparameter übernehmen. Nehmen Sie an, Sie benötigen eine kleine Funktion, welche die Summe zweier Argumente bildet. Über den Typ der Argumente wissen Sie nichts, aber der Rückgabetyp ist Ihnen bekannt. Eine solche Funktion könnte folgendermaßen implementiert werden:

#include <iostream>          // Ein und Ausgabe

using namespace std;

// Unsere Vorlage für die Summenberechnung
template <typename R, typename Arg1, typename Arg2>
R summe(Arg1 obj1, Arg2 obj2){
    return obj1 + obj2;
}

int main(){
    double a = 5.4;                      // Gleitkommazahl-Variable
    int   b = -3;                       // Ganzzahlige Variable

    cout << summe<int>(a, b)             << endl; // int           summe(double obj1, int obj2);
    cout << summe<double>(a, b)           << endl; // double       summe(double obj1, int obj2);
    cout << summe<double>(a, b)          << endl; // double        summe(double obj1, int obj2);
    cout << summe<unsigned long>(a, b)   << endl; // unsigned long summe(double obj1, int obj2);

    cout << summe<double, int, int>(a, b) << endl; // double         summe(int  obj1, int obj2);

    cout << summe<double, long>(a, b)     << endl; // double         summe(long obj1, int obj2);

    return 0;                            // Programm erfolgreich durchlaufen
}
Ausgabe:
2
2.4
2.4
2
2
2

Beim fünften Aufruf sind alle Argumente explizit angegeben. Wie Sie unschwer erkennen werden, kann der Compiler bei dieser Funktion den Rückgabetyp nicht anhand der übergebenen Funktionsparameter ermitteln. Daher müssen Sie ihn immer explizit angeben, wie die ersten vier Aufrufe es zeigen.

Die Templateargumente werden beim Funktionsaufruf in der gleichen Reihenfolge angegeben, wie sie bei der Deklaraton des Templates in der Templateargumentliste stehen. Wenn dem Compiler weniger Argumente übergeben werden als bei der Deklaration angegeben, versucht er die übrigen anhand der Funktionsargumente zu bestimmen.

Das bedeutet für Sie, dass Argumente, die man immer explizit angeben muss, auch bei der Deklaration an vorderster Stelle der Templateargumentliste stehen müssen. Das folgende Beispiel verdeutlicht die Folgen bei Nichteinhaltung dieser Regel:

#include <iostream>          // Ein und Ausgabe

using namespace std;

// Unsere Vorlage für die Summenberechnung
template <typename Arg1, typename Arg2, typename R> // Rückgabetyp als letztes angegeben
R summe(Arg1 obj1, Arg2 obj2){
    return obj1 + obj2;
}

int main(){
    double a = 5.4;                       // Gleitkommazahl-Variable
    int   b = -3;                        // Ganzzahlige Variable

    cout << summe<int>(a, b) << endl; // ??? summe(int obj1, int  obj2);
    cout << summe<double>(a, b) << endl; // ??? summe(double obj1, int  obj2);
    cout << summe<double>(a, b) << endl; // ??? summe(double obj1, int  obj2);
    cout << summe<unsigned long>(a, b) << endl; // ??? summe(unsigned long obj1, int  obj2);

    cout << summe<double, int, int>(a, b) << endl; // int summe(double obj1, int  obj2);

    cout << summe<double, long>(a, b) << endl; // ??? summe(double obj1, long obj2);

    return 0;                            // Programm erfolgreich durchlaufen
}

Nur der Aufruf (Nr. 5), bei dem auch das letzte Templateargument, also der Rückgabetyp explizit angegeben wurde, ließe sich übersetzen. Alle anderen bemängelt der Compiler. Sie müssten also alle Templateargumente angeben, um den Rückgabetyp an den Compiler mitzuteilen. Das schmeißt diese wundervolle Gabe des Compilers, selbständig anhand der Funktionsparameter die richtige Templatefunktion zu erkennen, völlig über den Haufen. Hinzu kommt in diesem Beispiel noch, dass die Reihenfolge bei einer expliziten Templateargumentangabe alles andere als intuitiv ist.

Nichttypargumente

Bearbeiten

Neben Typargumenten können Templates auch ganzzahlige Konstanten als Argument übernehmen. Die folgende kleine Templatefunktion soll alle Elemente eines Arrays (Datenfeldes) beliebiger Größe mit einem Startwert initialisieren.

#include <cstddef>          // Für den Typ size_t

template <std::size_t N, typename T>
void array_init(T (&array)[N], T const &startwert){
    for(std::size_t i=0; i!=N; ++i)
        array[i]=startwert;
}

Die Funktion übernimmt eine Referenz auf ein Array vom Typ T mit N Elementen. Jedem Element wird der Wert zugewiesen, welcher der Funktion als Zweites übergeben wird. Der Typ std::size_t wird übrigens von der C++-Standardbibliothek zur Verfügung gestellt. Er ist in der Regel als typedef auf unsigned long deklariert.

Ihr Compiler sollte in der Lage sein, sowohl den Typ T als auch die Größe des Arrays N selbst zu ermitteln. In einigen Fällen kann es jedoch vorkommen, dass der Compiler nicht in der Lage ist, die Größe N zu ermitteln. Da sie als erstes Templateargument angegeben wurde, reicht es aus, nur sie explizit anzugeben und den Compiler in diesen Fällen zumindest den Typ T selbst ermitteln zu lassen.

Obwohl Funktionstemplates im Header definiert werden müssen, sind sie nicht automatisch auch inline. Wenn Sie eine Funktionstemplate als inline deklarieren möchten, geben Sie das Schlüsselwort zwischen Templateparameterliste und dem Prototypen an.