Einmal schreiben, für jeden Typ verwenden
Auf der vorherigen Seite hast du einen vector<int> mit std::sort sortiert. Aber std::sort sortiert auch einen vector<string>, einen vector<double> oder ein Array deiner eigenen Structs - ohne dass jemand für jeden ein eigenes sort schreibt. Das ist weder Zauberei noch Überladung. Es ist ein Template: ein einziges Stück Code, das der Compiler für jeden Typ wiederverwendet, den du ihm gibst.
Ohne Templates müsstest du dieselbe Logik für jeden Typ per Copy-and-paste duplizieren. Hier ist dieselbe Funktion maximum dreimal geschrieben - genau die Duplizierung, die Templates beseitigen sollen:
int maximum(int a, int b) { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }
Die Rümpfe sind identisch. Nur die Typen unterscheiden sich. Ein Template erlaubt dir zu sagen „das funktioniert für jeden Typ T" und es einmal zu schreiben.
Funktions-Templates
Du machst aus einer Funktion ein Template, indem du template <typename T> davorsetzt und T überall dort verwendest, wo normalerweise ein konkreter Typ stünde.
Beachte, dass du nie maximum<int> oder maximum<double> geschrieben hast. Der Compiler betrachtet die Argumente und ermittelt, was T sein soll - das ist die Template-Argument-Deduktion. Jeder unterschiedliche Typ, mit dem du sie aufrufst, veranlasst den Compiler, hinter den Kulissen eine separate konkrete Funktion zu instanziieren (zu erzeugen).
Du kannst den Typ explizit angeben, wenn die Deduktion nicht helfen kann, indem du spitze Klammern verwendest:
In der Deduktion lauert eine häufige Falle. Da T ein einzelner Typ sein muss, bricht das Mischen von Argumenttypen sie:
maximum(3, 7.5); // FEHLER: Ist T int oder double? Der Compiler weigert sich zu raten.
Du kannst das beheben, indem du explizit bist - maximum<double>(3, 7.5) - oder indem du jedem Parameter seinen eigenen Typparameter gibst, was wir als Nächstes tun.
Mehrere Typparameter
Ein Template ist nicht auf einen Typ beschränkt. Liste so viele auf, wie du brauchst, durch Kommas getrennt. So schreibst du eine Funktion, deren Parameter unterschiedliche Typen haben können:
Wenn der Rückgabetyp von den Parametern abhängt, lass den Compiler ihn mit auto ermitteln (C++14 und später), das sich natürlich mit Templates verbindet:
Klassen-Templates
Templates sind nicht nur für Funktionen da - ganze Klassen können als Template definiert werden. Genau so funktionieren die Standardcontainer: vector<int>, die Schlüssel-Wert-map und pair<A, B> sind allesamt Klassen-Templates. Du schreibst die Datenstruktur einmal, und sie speichert den Typ, mit dem du sie parametrisierst.
Hier ist eine winzige generische Box, die einen Wert beliebigen Typs hält:
Der entscheidende Unterschied zu Funktions-Templates: Bei einem Klassen-Template musst du den Typ normalerweise in spitzen Klammern angeben - Box<int> - weil es in älteren Standards keine Konstruktorargumente gibt, aus denen man ihn ableiten könnte. (C++17 fügte die Klassen-Template-Argument-Deduktion hinzu, sodass auch Box b(42); funktioniert, aber explizit zu sein ist immer sicher und liest sich klar.)
Die Fehler werden gewaltig sein - hier ist der Grund
Das ist der Teil, an dem alle stolpern, deshalb sagen wir es ganz deutlich. Ein Template wird erst vollständig geprüft, wenn es mit einem echten Typ instanziiert wird. Du kannst ein Template schreiben, das < verwendet, und es kompiliert für sich allein einwandfrei - der Fehler erscheint erst in dem Moment, in dem du es mit einem Typ instanziierst, der kein < hat.
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b; // erfordert, dass T > unterstützt
}
struct Point { int x, y; };
// maximum(Point{1,2}, Point{3,4});
// FEHLER: kein operator > für Point. Die Meldung nennt Point UND
// zitiert diese ganze Funktion, oft über viele Zeilen hinweg.
Da der Compiler den vollständigen Typ in das Template einsetzt und Fehler aus dem Inneren des erzeugten Codes meldet, kann ein einziger Fehler eine Wand aus Ausgabe erzeugen, die Bibliotheks-Interna erwähnt. Zwei Überlebenstipps:
- Lies die erste Fehlermeldung, nicht die letzte. Spätere Fehler sind meist Folgeerscheinungen des ersten.
- Durchsuche die Meldung nach dem Namen deines eigenen Typs (hier
Point). Das sagt dir, welche Instanziierung schiefgegangen ist.
Die eigentliche Lösung besteht darin, sicherzustellen, dass dein Typ alles unterstützt, was das Template braucht - für maximum heißt das, Point durch das Überladen von operator> zu erweitern, was Thema einer späteren Seite ist. Die Concepts des modernen C++20 können diese Fehler nach vorne verlagern und lesbar machen, aber das darunterliegende Substitutionsmodell bleibt dasselbe.
Weiter: Klassen
Du hast gerade ein Box-Klassen-Template gebaut - eine Klasse mit privaten Daten, einem Konstruktor und Methoden - während der Fokus auf den Templates lag. Die nächste Seite wird langsamer und vermittelt Klassen richtig: wie man Daten mit den Funktionen bündelt, die auf ihnen operieren, was public und private wirklich steuern und wie Methoden auf den eigenen Zustand des Objekts zugreifen. Templates und Klassen werden in echtem C++ ständig kombiniert, daher macht ein solides Verständnis von Klassen das Schreiben von generischem Code weit einfacher.
Häufig gestellte Fragen
Was ist ein Template in C++?
Ein Template ist eine Vorlage, mit der du eine Funktion oder Klasse einmal schreibst und der Compiler für jeden Typ, mit dem du sie verwendest, eine eigene Version erzeugt. Du schreibst template <typename T> und verwendest dann T als Platzhalter für den echten Typ. Der Compiler erzeugt eine konkrete Version - das nennt man Instanziierung.
Was ist der Unterschied zwischen typename und class in einem C++-Template?
typename und class in einem C++-Template?In einer Template-Parameterliste bedeuten template <typename T> und template <class T> genau dasselbe. Heute wird meist typename bevorzugt, weil es ehrlicher zu lesen ist: T kann jeder beliebige Typ sein, nicht nur eine Klasse. Die Wahl des Schlüsselworts hat keinerlei Einfluss auf den erzeugten Code.
Warum sind C++-Template-Fehlermeldungen so lang?
Templates werden geprüft, wenn sie mit einem echten Typ instanziiert werden, nicht wenn sie geschrieben werden. Wenn ein Typ eine von dir verwendete Operation nicht unterstützt (wie < zum Sortieren), erscheint der Fehler tief im Bibliothekscode, mit dem vollständig ausgeschriebenen instanziierten Typ, und erzeugt seitenweise Ausgabe. Lies die erste Fehlermeldung und suche darin nach dem Namen deines Typs.