C++-Programmierung/ Templates
Zielgruppe:
Anfänger
Lernziel:
Lernen was Templates sind und wie man sie einsetzt.
Funktionstemplates [Bearbeiten]
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.
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:
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:
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:
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;
}
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
}
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
}
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.
inline
[Bearbeiten]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.
Klassentemplates [Bearbeiten]
Wie bereits erwähnt, lassen sich auch Klassen als Template (oder Mustervorlage) definieren. Im Gegensatz zu Funktionen ist für Klassen überhaupt keine Überladung möglich. Schließlich hat der Compiler bei Klassen keine Parameter oder ähnliches, anhand dessen er die gemeinte Klasse erkennen könnte. Somit müssen bei der Konkretisierung eines Templatetyps alle Parametertypen bzw. Werte explizit angegeben werden. Die Parameter des Konstruktors können übrigens auch nicht zum Erkennen der Templateparameter herangezogen werden, da es mehrere Konstruktoren geben kann und diese obendrein ihrerseits Funktionstemplates sein können.
template < «Templateparameterliste» >
class «Klassenname»{
// Definition
};
//----
Templateparameterliste: «Listeneintrag», «Templateparameterliste»
Templateparameterliste: «Leere Liste»
Listeneintrag: typename «Platzhaltername»
Listeneintrag: class «Platzhaltername»
Listeneintrag: «Ganzzahliger Typ» «Platzhaltername»
Praktische Beispiele
[Bearbeiten]Zunächst werden wir uns ein einfaches Klassentemplate ansehen:
#include <iostream>
template < typename T1, typename T2, typename T3 >
class Klasse{
public:
Klasse(T1 t1, T2 t2, T3 t3):
t1_(t1), t2_(t2), t3_(t3)
{
std::cout << t1_ << '\n' << t2_ << '\n' << t3_ << std::endl;
}
T1 getFirst(){ return t1_; }
private:
T1 t1_;
T2 t2_;
T3 t3_;
};
…
Nun können wir unser Klassentemplate verwenden. Durch die Angabe konkreter Templateparameter konkretisieren wir unseren Typ. Anschließend können wir unsere Variable verwenden, wie jede andere auch.
…
int main(){
// Konkretisierung, Initialisierung
Klasse< double, char, char const* > object(3.141, 'c', "Toll!!!");
std::cout << "T1 ist: " << object.getFirst() << std::endl;
}
Analog zu Funktionstemplates lassen sich natürlich auch bei Klassentemplates neben Typparametern Ganzzahlparameter verwenden. Außerdem kann mittels typedef
jederzeit ein neuer Name für einen konkretisierten Templatetyp erzeugt werden. Das nächste Beispiel wird beides demonstrieren:
#include <iostream>
template < std::size_t N, typename T >
class Array{
public:
Array(){
for(std::size_t i = 0; i < N; ++i){
values_[i] = i;
}
}
T& operator[](std::size_t i){ return values_[i % N]; }
private:
T values_[N];
};
int main(){
typedef Array< 3, int > Array3Int; // Konkretisierung
Array3Int object;
for(std::size_t i = 0; i < 5; ++i){
std::cout << object[i] << std::endl;
}
std::cout << std::endl;
object[4] = 500; // Identisch mit object[1] = 500; wegen 4 % 3 == 1
for(std::size_t i = 0; i < 3; ++i){
std::cout << object[i] << std::endl;
}
}
0
1
2
0
1
0
500
2
Diese typedef
-Technik findet auch in der Standardbibliothek viele Anwendungen. Die folgende kurze Liste zeigt einige Beispiele aus der Standardbibliothek:
typedef basic_string< char > string;
typedef basic_string< wchar_t > wstring;
typedef basic_ostream< char > ostream;
typedef basic_iofstream< char > iofstream;
typedef basic_istringstream< wchar_t > wistringstream;
Standardparameter
[Bearbeiten]Analog zu Standardparametern für Funktionen können auch für Templates Standardparameter angegeben werden. Die Regeln sind dabei ebenfalls analog zu Funktionen: jeder Parameter, der über einen Standard-Typ/-Wert verfügt, muss entweder ganz rechts stehen oder auf der rechten Seite einen Nachbarn besitzen, der ebenfalls über einen Standard-Typ/-Wert verfügt. Somit müssen Parameter, die in den meisten Fällen gleich sind, nicht bei der Konkretisierung des Templates angegeben werden. In den Fällen, wo dann doch mal ein anderer Typ/Wert angegeben werden muss, ist dies jedoch weiterhin problemlos möglich. Wichtig ist, dass die Parameter, die links vom Aktuellen stehen, im Standardparameter verwendet werden können.
template < typename T, char = 'N' >
class A{};
template < typename T1 = A< char >, typename T2 = T1*, char N = 'A', typename T3 = A< T1, N > >
class B{};
Auch von dieser Technik macht die Standardbibliothek reichlich Gebrauch. Einige Beispiele sind:
template < typename charT, typename traits = char_traits< charT >, typename Allocator = allocator< charT > > class basic_string;
template < typename T, typename Container = deque< T > > class queue;
template < typename charT, typename traits = char_traits< charT > > class basic_iostream;
Spezialisierung
[Bearbeiten]Klassentemplates lassen sich ebenso spezialisieren wie Funktionstemplates. Sie lassen sich jedoch, wie schon erwähnt, im Gegensatz zu diesen nicht Überladen. Die Syntax für eine Spezialisierung sieht folgendermaßen aus:
template < «Templateparameterliste» >
class «Klassenname» < «Konkretisierung» >{
// Definition
};
//----
Templateparameterliste: «Listeneintrag», «Templateparameterliste»
Templateparameterliste: «Leere Liste»
Listeneintrag: typename «Platzhaltername»
Listeneintrag: class «Platzhaltername»
Listeneintrag: «Ganzzahliger Typ» «Platzhaltername»
In der „Templateparameterliste“ können hier wiederum Platzhalter angegeben werden, die dann in der Konkretisierung verwendet werden können. Wenn ein Template spezialisiert wurde, wird die am besten passende Spezialisierung verwendet. Das folgende Beispiel wird dies demonstrieren, indem im Konstruktor je nach Spezialisierung ein anderer Text ausgegeben wird. Statt des Schlüsselwortes class
wird in diesem Beispiel struct
verwendet, was uns das public:
vor dem Konstruktor erspart.
#include <iostream>
template < typename T >
struct A{
A(){ std::cout << "A< T >" << std::endl; }
};
template < typename T >
struct A< T* >{
A(){ std::cout << "A< T* >" << std::endl; }
}; // Spezialisierung für alle Zeiger
template <>
struct A< int* >{
A(){ std::cout << "A< int* >" << std::endl; }
}; // Spezialisierung für Zeiger auf int
template <>
struct A< double >{
A(){ std::cout << "A< double >" << std::endl; }
}; // Spezialisierung für double
template < typename T >
struct A< T const >{
A(){ std::cout << "A< T const >" << std::endl; }
}; // Spezialisierung für alle Konstanten
template < typename Result, typename T1, typename T2 >
struct A< Result (*)(T1, T2) >{
A(){ std::cout << "Result (*)(T1, T2)" << std::endl; }
}; // Spezialisierung für Zeiger auf Funktionen mit 2 Parametern
template < typename Result >
struct A< Result (*)(char, double) >{
A(){ std::cout << "Result (*)(char, double)" << std::endl; }
}; // Spezialisierung für Zeiger auf Funktionen mit den Parametertypen char und double
int main(){
A< char > a; // A< T >
A< int > b; // A< T >
A< double > c; // A< double >
A< char* > d; // A< T* >
A< int* > e; // A< int* >
A< double* > f; // A< T* >
A< char const > g; // A< T const >
A< int const > h; // A< T const >
A< double const > i; // A< T const >
A< char* const > j; // A< T const >
A< int* const > k; // A< T const >
A< double* const > l; // A< T const >
A< char const* > m; // A< T* >
A< int const* > n; // A< T* >
A< double const* > o; // A< T* >
A< int (*)(char, double) > p; // Result (*)(char, double)
A< void (*)(long const**const, float[]) > q; // Result (*)(T1, T2)
A< void (*)(int) > r; // A< T* >
}
Anhand der Ausgabe beim Konstruktoraufruf lässt sich sehr schön erkennen, welche Spezialisierung der Compiler als am passendsten auserkoren hat. Die Kommentare entsprechen der Ausgabe bei der jeweiligen Zeile. Wenn Sie die Beispiele mal selbst genau unter die Lupe nehmen, werden Sie feststellen, dass sich die Wahl des Compilers leicht nachvollziehen lässt. Werden in einer Spezialisierung keine Platzhalter benötigt, wie hier zwei Mal zu sehen, muss dennoch eine leere Parameterliste angegeben werden, damit der Compiler weiß, dass es sich um eine Spezialisierung eines vorhanden Templates handelt.
Ein Beispiel für Spezialisierung in der Standardbibliothek ist vector< bool >
. In den meisten Fällen wird es nicht nötig sein, eine Templateklasse zu überladen, sofern Sie diese als Verallgemeinerung einer gewöhnlichen Klasse schreiben. Sie werden jedoch später noch einiges über Template-Meta-Programmierung lernen, und dabei ist Spezialisierung extrem häufig notwendig. Klassen dienen bei dieser Programmiertechnik meist nur als Hilfsmittel, womit sie also nicht im herkömmlichen Sinne verwendet werden. Dies ist jedoch eine fortgeschrittene C++-Technik.
Definition von Membern
[Bearbeiten]Bisher war der einzige Klassenmember, den wir definiert haben, der Konstruktor. Natürlich ergibt es selten Sinn, derartige Templates zu schreiben. Daher wenden wir uns als Nächstes der Deklaration und Definition von einfachen Membervariablen und Methoden zu. Die Deklaration erfolgt wie in einer gewöhnlichen Klasse. Bei unseren Konstruktorbeispielen haben wir bereits gesehen, dass Methoden, wie auch bei gewöhnlichen Klassen, innerhalb der Klassentemplatedefinition gemacht werden können. Für alle Memberdefinitionen außerhalb der Templateklassendefinition gelten etwas andere Regeln, als bei gewöhnlichen Klassen. Da die Templateparameter des Klassentemplates dort nicht zur Verfügung stehen, müssen Sie erneut angegeben werden. Insbesondere muss nicht nur der Klassenname (getrennt durch den Bereichsoperator ::
) vor dem Membernamen angegeben werden, sondern eben auch alle Parameter, die zum Klassentemplate gehören.
// (statische) Variablen
template < «Templateparameterliste» >
«Typ» «Klassenname»< «Konkretisierung» >::«Membername»(«Wert»);
// Methoden
template < «Templateparameterliste» >
«Rückgabetyp» «Klassenname»< «Konkretisierung» >::«Membername»(«Funktionsparameter»){
// Definition
}
//----
Templateparameterliste: «Listeneintrag», «Templateparameterliste»
Templateparameterliste: «Leere Liste»
Listeneintrag: typename «Platzhaltername»
Listeneintrag: class «Platzhaltername»
Listeneintrag: «Ganzzahliger Typ» «Platzhaltername»
Folgendes Beispiel führt einige Definitionen vor:
#include <stdexcept>
template < std::size_t N, typename T >
class Array{
public:
Array();
~Array();
T& operator[](std::size_t i);
T& at(std::size_t i);
static std::size_t get_count();
private:
T values_[N];
static std::size_t count;
};
template < std::size_t N, typename T >
Array< N, T >::Array(){
for(std::size_t i = 0; i < N; ++i){
values_[i] = i;
}
++count;
}
template < std::size_t N, typename T >
Array< N, T >::~Array(){
--count;
}
template < std::size_t N, typename T >
T& Array< N, T >::operator[](std::size_t i){
return values_[i % N];
}
template < std::size_t N, typename T >
T& Array< N, T >::at(std::size_t i){
if(i >= N) throw std::out_of_range();
return operator[](i);
}
template < std::size_t N, typename T >
std::size_t Array< N, T >::get_count(){
return count;
}
template < std::size_t N, typename T >
std::size_t Array< N, T >::count = 0; // oder: count(0)
Natürlich können Sie auf diese Weise leicht einzelne Member der Klasse für beliebige Templateargumente spezialisieren. Sie müssen nur die spezialisierten Argumente in den spitzen Klammern nach dem Klassennamen angeben. Genau wie Funktionstemplates dürfen natürlich auch Member Klassentemplates nur in der aktuellen Datei definiert werden, da der Compiler bei einem Template immer die komplette Definition kennen muss.
Templatemember
[Bearbeiten]Nachdem Sie nun Funktionen und Klassen zu Templates gemacht haben, steht noch die Kombination aus beidem aus, denn auch die Methoden von Klassentemplates lassen sich natürlich als Methodentemplates schreiben. Das einzig Neue dabei ist die Definition der Methoden außerhalb der Klasse, bei der beide Templates spezifiziert werden müssen. Im Folgenden sehen Sie ein Klassentemplate, das einen Templatekonstruktor besitzt, um gleich eine ganze Anzahl von Datentypen zur Initialisierung zuzulassen.
#include <cstddef> // Für std::size_t
template < std::size_t N, typename T >
class Array{
public:
Array();
template < typename Iterator >
Array(Iterator iter); // Deklaration des Methodentemplates
T& operator[](std::size_t i);
private:
T values_[N];
};
template < std::size_t N, typename T >
Array< N, T >::Array(){
for(std::size_t i = 0; i < N; ++i){
values_[i] = i;
}
}
template < std::size_t N, typename T > // Klassentemplate
template < typename Iterator > // Methodentemplate
Array< N, T >::Array(Iterator iter){ // Definition der Methode
for(std::size_t i = 0; i < N; ++i){
values_[i] = *iter++;
}
}
template < std::size_t N, typename T >
T& Array< N, T >::operator[](std::size_t i){
return values_[i % N];
}
#include <vector>
#include <list>
#include <iostream>
int main(){
std::vector< int > a;
std::list< int > b;
int c[5];
for(std::size_t i = 0; i < 5; ++i){
a.push_back(i);
b.push_back(i);
c[i] = i;
}
Array< 5, int > a_t(a.begin());
Array< 5, int > b_t(b.begin());
Array< 5, int > c_t(c);
for(std::size_t i = 0; i < 5; ++i){
std::cout << a_t[i] << ' ' << b_t[i] << ' ' << c_t[i] << std::endl;
}
}
0 0 0
1 1 1
2 2 2
3 3 3
4 4 4
Wie Sie sehen, können Sie nun jeden beliebigen Datentyp für die Initialisierung verwenden, für den der Postfix-Increment- und Dereferenzierungsoperator entsprechend überladen ist. Die hier gezeigte Definition ist nicht besonders gut, da keinerlei Bereichsüberprüfung durchgeführt wird. Der Konstruktor muss sich darauf verlassen, das über den Iterator genügend gültige Elemente zur Verfügung stehen, aber es demonstriert sehr schön, welche Möglichkeiten diese Technik bietet.
Templatefreunde [Bearbeiten]
Dieses Kapitel ist leider noch nicht vorhanden… | |
Wenn Sie Lust haben können Sie das Kapitel [[C++-Programmierung/ {{{Name}}}/ {{{Kapitel}}}|{{{Kapitel}}}]] selbst schreiben oder einen Beitrag dazu leisten. |
Die zweite Bedeutung von typename [Bearbeiten]
Dieses Kapitel ist leider noch nicht vorhanden… | |
Wenn Sie Lust haben können Sie das Kapitel [[C++-Programmierung/ {{{Name}}}/ {{{Kapitel}}}|{{{Kapitel}}}]] selbst schreiben oder einen Beitrag dazu leisten. |
Templates als Templateparameter [Bearbeiten]
Dieses Kapitel ist leider noch nicht vorhanden… | |
Wenn Sie Lust haben können Sie das Kapitel [[C++-Programmierung/ {{{Name}}}/ {{{Kapitel}}}|{{{Kapitel}}}]] selbst schreiben oder einen Beitrag dazu leisten. |
Zusammenfassung [Bearbeiten]
Zu diesem Abschnitt existiert leider noch keine Zusammenfassung… | |
Wenn Sie Lust haben können Sie die [[C++-Programmierung/ {{{Name}}}/ Zusammenfassung|Zusammenfassung zum Abschnitt {{{Name}}}]] selbst schreiben oder einen Beitrag dazu leisten. |