Template (C++)
Templates (englisch für Schablonen oder Vorlagen) sind ein Mittel zur Typparametrierung in C++. Templates ermöglichen generische Programmierung und typsichere Container.
In der C++-Standardbibliothek werden Templates zur Bereitstellung typsicherer Container, wie z. B. Listen, und zur Implementierung von generischen Algorithmen, wie z. B. Sortierverfahren, verwendet. Die Templates in C++ sind wesentlich von den parametrierbaren Modulen in CLU und den Generics in Ada inspiriert.[1]
In anderen Programmiersprachen (z. B. Java[2] oder C#[3]) gibt es das Konzept des generischen Typs, das mit Templates verwandt ist. Generische Typen stellen jedoch keine Codegeneratoren dar, sondern ermöglichen lediglich typsichere Container und Algorithmen.
Arten von Templates
Es gibt in C++ drei Arten von Templates: Funktions-Templates, Klassen-Templates und Variablen-Templates:
Funktions-Templates
Ein Funktions-Template (auch fälschlich Template-Funktion genannt) verhält sich wie eine Funktion, die Argumente verschiedener Typen akzeptiert oder unterschiedliche Rückgabetypen liefert. Die C++-Standardbibliothek enthält beispielsweise das Funktions-Template std::max(x, y)
, welches das größere der beiden Argumente zurückgibt. Es könnte etwa folgendermaßen definiert sein:
template <typename T>
T max(T x, T y) {
T value;
if (x < y)
value = y;
else
value = x;
return value;
}
Dieses Template kann genauso aufgerufen werden wie eine Funktion:
cout << max(3, 7); // gibt 7 aus
Anhand der Argumente entscheidet der Compiler, dass es sich um einen Aufruf an max(int, int)
handelt, und erzeugt eine Variante der Funktion, bei der der generische Typ T zu int festgelegt wird.
Der Template-Parameter könnte auch explizit angegeben werden:
cout << max<int>(3, 7); // gibt ebenfalls "7" aus
Das Funktions-Template max()
lässt sich für jeden Typ instanziieren, für den der Vergleich x < y
eine wohldefinierte Operation darstellt. Bei selbstdefinierten Typen macht man von Operator-Überladung Gebrauch, um die Bedeutung von <
für den Typ festzulegen und dadurch die Verwendung von max()
für den betreffenden Typ zu ermöglichen.
Im Zusammenspiel mit der C++-Standardbibliothek erschließt sich eine enorme Funktionalität für selbstdefinierte Typen durch Definition einiger Operatoren. Allein durch die Definition eines Vergleichsoperators <
(strenge schwache Ordnung) werden die Standardalgorithmen std::sort()
, std::stable_sort()
, und std::binary_search()
für den selbstdefinierten Typ anwendbar.
Klassen-Templates
Ein Klassen-Template (deutsch: Klassenvorlage, auch fälschlich Template-Klasse genannt), wendet das gleiche Prinzip auf Klassen an. Klassen-Templates werden oft zur Erstellung von generischen Containern verwendet. Beispielsweise enthält die C++-Standardbibliothek einen Container, der eine verkettete Liste implementiert. Um eine verkettete Liste von int zu erstellen, schreibt man std::list<int>
. Eine verkettete Liste von Objekten des Datentypes std::string
wird zu std::list<std::string>
. Mit list ist ein Satz von Standardfunktionen definiert, die immer verfügbar sind, unabhängig davon, was man als Argumenttyp in den spitzen Klammern angibt. Die Werte in spitzen Klammern werden Parameter genannt. Wenn ein Klassen-Template mit seinen Parametern dem Compiler übergeben wird, so kann dieser das Template ausprägen. Er erzeugt hierbei zu jedem Parametertyp eine eigene Template-Klasse. Diese ist eine gewöhnliche Klasse, wie jede andere auch. Die Begriffe Klassen-Template und Template-Klasse sind hier voneinander zu unterscheiden. Wie Objekt und Klasse ist die Template-Klasse eine Ausprägung eines Klassen-Templates.
Templates sind sowohl für mit class als auch für mit struct und union definierte Klassen anwendbar. Namespaces (deutsch: Namensräume) lassen sich dagegen nicht als Template anlegen. Eine Möglichkeit, Typ-Definitionen per "typedef" als Templates anzulegen, kam mit C++11 hinzu.
Vererbung
Klassen-Templates können wie normale Klassen in Vererbungshierarchien sowohl als Basis- als auch als abgeleitete Klasse auftreten.
Wird ein Klassen-Template mit verschiedenen Klassenparametern ausgeprägt, so stehen diese grundsätzlich in keiner Vererbungsrelation – auch nicht, wenn die Template-Parameter in einer Vererbungsbeziehung stehen.
- Beispiel
class Base {...};
class Derived: Base { ... };
Base* b = new Derived; // OK. Automatische Typumwandlung, da Basisklasse.
std::vector<Base>* vb = new std::vector<Derived>; // FEHLER!
Es gibt vor allem zwei Gründe, wieso derartige Umwandlungen nicht erlaubt sind:
Zum einen sind es technische Gründe: Ein std::vector<T>
speichert seine Elemente in einem Array an unmittelbar aufeinander folgenden Adressen. Wenn nun ein Objekt vom Typ Derived
eine andere Größe hat als ein Objekt vom Typ Base
, dann stimmt das Speicherlayout von einem std::vector<Base>
nicht mehr mit dem eines std::vector<Derived>
überein und der Zugriff auf die Elemente würde fehlschlagen.
Zum anderen hat es aber auch programmlogische Gründe: Ein Container, der Elemente einer Basisklasse enthält, verweist auf Elemente, deren Datentyp Base
ist oder von Base
abgeleitet sind. Er ist somit einerseits mächtiger als ein Container, der nur Elemente vom Typ Derived
aufnehmen kann, andererseits weniger mächtig, da er nur garantiert Elemente vom Type Base
zurückzugeben.
Für std::vector<T>
illustriert das am einfachsten der []
operator.
- Beispiel
class Base {...};
class Derived: Base { ... };
Base * b = new Base;
Derived* d = new Derived;
std::vector<Base> * vb = new std::vector<Base>;
std::vector<Derived>* vd = new std::vector<Derived>;
Einerseits kann man vb[0] = b
nicht aber vd[0] = b
schreiben.
Andererseits kann man d = vd[0]
nicht aber d = vb[0]
schreiben.
Falls eine Umwandlung von verschiedenen Ausprägungen desselben Klassen-Templates – wie etwa bei std::shared_ptr
– sinnvoll ist, muss dafür explizit ein Typkonvertierungsoperator definiert werden.
Variablen-Templates
Seit C++14 ist es auch möglich, Variablen-Templates zu definieren. Das ermöglicht es, Variablen, die logisch zusammen gehören bzw. bis auf den Typ "dasselbe" sind, entsprechend kenntlich zu machen:
// variable template
template<class T>
constexpr T Pi = T(3.1415926535897932385L);
// function template
template<class T>
T kreisflaeche(T r) {
return Pi<T> * r * r;
}
Die Funktion kreisflaeche()
kann nun für verschiedene arithmetische Datentypen instanziiert werden und greift dabei stets auf eine vom Parametertyp her passende Konstante Pi
zu.
Spezialisierung
Templates lassen sich spezialisieren, d. h., man kann Klassen- und Funktions-Templates (für bestimmte Datentypen als Template-Argumente) gesondert implementieren. Dies erlaubt eine effizientere Implementierung für bestimmte ausgewählte Datentypen, ohne die Schnittstelle des Templates zu verändern. Davon machen auch viele Implementierungen der C++-Standardbibliothek (beispielsweise die der GCC) Gebrauch.
Spezialisierung bei Klassen-Templates
Die Containerklasse std::vector
der C++-Standardbibliothek kann für den Elementtyp bool als Bitmap implementiert werden, um Speicherplatz einzusparen. Auch entnimmt das Klassen-Template std::basic_string
die Informationen zum Umgang mit den einzelnen Zeichen der Struktur char_traits, die für den Datentyp char und beispielsweise auch wchar_t spezialisiert ist.
Die Deklaration von Spezialisierungen ähnelt der von normalen Templates. Allerdings sind die dem Schlüsselwort template folgenden spitzen Klammern leer, und dem Funktions- bzw. Klassennamen folgen die Template-Parameter.
- Beispiel
template<>
class vector<bool> {
// Implementierung von vector als Bitmap
};
Partielle Spezialisierung
Weiterhin gibt es auch die sogenannte partielle Spezialisierung, die die Behandlung von Spezialfällen innerhalb eines Templates ermöglicht.
- Beispiel
template<int zeilen, int spalten>
class Matrix {
// Implementierung einer Matrix-Klasse
};
template<int zeilen>
class Matrix<zeilen, 1> {
// Implementierung einer einspaltigen Matrix-Klasse
};
Die Instanziierung läuft bei der spezialisierten Klasse im Grunde gleich ab, es wird nur der Code aus einem anderen Klassen-Template generiert, nämlich der Spezialisierung:
int main() {
// erste Klasse
Matrix<3,3> a;
// teilweise spezialisiertes Klassen-Template (zweite Klasse)
Matrix<15,1> b;
}
Wichtig zu erwähnen ist, dass beide Klassen voneinander unabhängig sind, d. h., sie erben weder Konstruktoren oder Destruktoren noch Elementfunktionen bzw. Datenelemente voneinander.
Spezialisierung bei Funktions-Templates
Im Unterschied zu Klassen-Templates sind Funktions-Templates laut Standard nicht teilweise spezialisierbar (nur vollständig). Allerdings wird von der Spezialisierung von Funktions-Templates allgemein abgeraten, da die Regeln für die Bestimmung der „am besten passenden“ Funktion sonst zu unintuitiven Ergebnissen führen können.[4]
Durch Überladen von Funktions-Templates mit anderen Funktions-Templates kann man in den meisten Fällen das Gleiche erreichen wie durch die (nicht zulässige) teilweise Spezialisierung. Die Erweiterung selbst läuft gewöhnlich sehr intuitiv ab:
// Generische Funktion
template<class T, class U>
void f(T a, U b) {}
// Überladenes Funktions-Template
template<class T>
void f(T a, int b) {}
// Vollständig spezialisiert; immer noch Template
template<>
void f(int a, int b) {}
Hierbei muss allerdings beachtet werden, dass ein expliziter Aufruf wie f<int,int>()
nicht zum gewünschten Ergebnis führt. Dieser Aufruf würde die generische Funktion statt der voll spezialisierten aufrufen. Ein Aufruf von f<int>()
hingegen ruft nicht die erste überladene Template-Funktion auf, sondern die voll spezialisierte. Unterlässt man explizite Aufrufe, funktioniert normalerweise alles, wie es logisch scheint (f(3, 3)
ruft die voll spezialisierte Funktion auf, f(3.5, 5)
die teilweise spezialisierte (erste überladene Funktion) und f(3.5, 2.0)
die generische). Mit dieser Art der Spezialisierung sollte man also vorsichtig sein und, wenn möglich, gleich vollständig spezialisieren.
Falls diese Technik aus jedwedem Grund im konkreten Fall nicht anwendbar ist – z. B. wenn ein Template von Klassenmethoden spezialisiert werden soll, ohne die Klassendefinition zu erweitern – so kann man auch das Problem der Spezialisierung auf ein Template einer Hilfsklasse verlagern:
class Example {
private:
template<typename T>
struct Frobnicator {
static T do_frobnicate(T param);
};
public:
template<typename T>
T frobnicate(T param);
};
template<typename T>
T Example::frobnicate(T param) {
// Frobnicator soll die eigentliche Arbeit verrichten
return Frobnicator<T>::do_frobnicate(param);
}
template<typename T>
T Example::Frobnicator<T>::do_frobnicate(T param) {
// Standardimplementierung ...
}
template<>
int Example::Frobnicator<int>::do_frobnicate(int param) {
// ints werden auf andere Weise "frobnifiziert"
return (param << 3) + (param % 7) - param + foobar;
}
Template-Parameter
Templates können vier Arten von Parametern haben: Typparameter, Nichttyp-Parameter, Template-Parameter und sogenannte Parameter-Packs, mit denen Templates mit einer variablen Anzahl von Parametern definiert werden können.
Typ-Parameter
Templates mit Typ-Parametern entsprechen in etwa den generischen Typen anderer Programmiersprachen. Als Typ-Parameter kann jeder beliebige Datentyp verwendet werden, wobei je nach Verwendung des Typ-Parameters innerhalb des Templates die Menge der Parametertypen, mit denen das Template instanziiert werden kann, beschränkt ist.
Die Container der C++-Standardbibliothek verwenden unter anderem Typ-Parameter, um für alle Datentypen, auch benutzerdefinierte, geeignete Container zur Verfügung zu stellen.
template<typename T>
class Vector {
public:
Vector(): rep(0) {}
Vector(int _size): rep(new T[_size]), size(_size) {}
~Vector() { delete[] rep; }
private:
T* rep;
int size;
};
Nichttyp-Parameter
Nichttyp-Parameter (engl. non-type template parameter) sind konstante, zur Übersetzungszeit bekannte, Werte, mit denen Größen, Verfahren oder Prädikate als Template-Parameter übergeben werden können. Als Nichttyp-Template-Parameter sind erlaubt:
- ganzzahlige Konstanten (inklusive Zeichenkonstanten),
- Zeigerkonstanten (Daten- und Funktionszeiger, inklusive Zeiger auf Member-Variablen und -Funktionen) und
- Zeichenkettenkonstanten.
Verwendung finden Nichttyp-Parametern z. B. als Größenangabe bei std::array
oder als Sortier- und Suchkriterium bei vielen Algorithmen der Standardbibliothek, wie z. B. std::sort
, std::find_if
oder std::for_each
.
Template-Templates
Als Template-Templates werden Konstruktionen bezeichnet, bei denen Templates wiederum Templates als Parameter übernehmen. Sie stellen einen weiteren Abstraktionsmechanismus zur Verfügung. Im folgenden Beispiel wird sowohl der Typ als auch der verwendete Container angegeben; letzterer mit Hilfe eines Template-Template-Parameters:
template <template <typename, typename> class Container, typename Type>
class Example {
Container<Type, std::allocator <Type> > baz;
};
Beispiel zur Verwendung:
Example <std::deque, int> example;
Parameter-Packs
Seit C++11 gibt es die Möglichkeit, Templates mit variabler Anzahl von Template-Parametern zu definieren. Diese werden – wie bei Funktionen und Makros mit variabler Parameteranzahl – mit ...
gekennzeichnet:
template<typename... Values> class tuple {
// Definition ausgelassen
};
// Verwendung:
tuple<int, int, char, std::vector<int>, double> t;
Siehe auch
Literatur
- David Vandervoorde, Nicolai M. Josuttis: C++ Templates. The Complete Guide. Addison-Wesley Professional, 2003, ISBN 0-201-73484-2.
Einzelnachweise
- Bjarne Stroustrup: Die C++-Programmiersprache. 4. Auflage. Addison-Wesley, 2009, ISBN 978-3-8273-2823-6.
- Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle. (PDF) Abgerufen am 26. Mai 2017.
- An Introduction to C# Generics. Abgerufen am 26. Mai 2017 (englisch).
- Herb Sutter: Why Not Specialize Function Templates? In: C/C++ Users Journal. Band 7, Nr. 19, Juli 2001 (gotw.ca).