C++/Tutorial: 17. Templates
In modernem C++ gibt es ein weiteres Sprachfeature, was nicht mehr wegzudenken ist: sogenannte Templates.
Bei Templates handelt es sich um eine Form von Compilezeitpolymorphie und Kernbestandteil der generischen Programmierung in C++(Wer schon einmal programmiert hat: Java, C# und einige andere Sprachen haben ein ähnliches Konzept, das in den genannten Sprachen auch syntaktisch sehr ähnlich aussieht, sogenannte Generics. Im Gegensatz zu Templates arbeiten Generics jedoch zur Laufzeit. Beide Modelle haben ihre Vorteile und Gründe, C++ Templates sind jedoch mächtiger als Generics). Generische Programmierung bedeutet Funktionen und Klassen allgemein zu entwerfen, sodass sie auf viele unterschiedliche Typen anwendbar sind, solange sie bestimmte Eigenschaften aufweisen, Templates ermöglichen es Typen zu `übergeben` und kommen sowohl bei Klassen als auch bei Funktionen vor.
Inhaltsverzeichnis
Funktionstemplate
Gehen wir einmal zurück zu unserem Beispiel um Makros zu verstehen: eine simple Funktion square, die einen Parameter nimmt und diesen quadriert. Wir haben in Kapitel 6 gezeigt, dass Makros hier viele Fallstricke haben und den Funktionen unterlegen sind, aber eines kann unser Makro-SQUARE, was die square-Funktion nicht konnte: sie konnte jeden beliebigen Typ nehmen, der sich multiplizieren ließ und diesen quadrieren, während unsere square-Funktion nur ein double nimmt. Das mag ausreichend sein für das rechnen mit den Standarddatentypen, aber was wenn wir z.B. eine Bibliothek für große Zahlen verwenden, die einen Typ bereitstellt, der sich nicht in double casten lässt? Bereits in so einem trivialen Fall ist unsere Funktion speziell und könnte verallgemeinert werden. Die Lösung ist anstelle der Funktion ein sogenanntes Funktionstemplate zu erstellen. Die Funktion als Funktionstemplate sähe folgendermaßen aus:
template <typename T> T square(T const& t) { return t*t; }
In der ersten Zeile leiten wir das Funktionstemplate durch das Schlüsselwort template ein. Anschließend listen wir in den spitzen Klammern, die Templateparameter auf mit typename T geben wir an, dass wir einen Typparameter T nehmen(neben Typparametern mit typename sind außerdem auch integrale Datentypen, wie z.B. int möglich). Eine alternative, gleihwertige Syntax von typename T wäre class T. Wir könnten auch, ähnlich den normalen Parametern, mehrere Templateparameter nehmen.
T steht nun als Platzhalter für einen Typ, die Funktion nimmt also einen Parameter vom unbekannten Typ T, mulitpliziert diesen mit sich selbst und gibt den Rückgabewert, ebenfalls vom Typ T, zurück. Wenn wir das Funktionstemplate nun mit einem bestimmten Typ, z.B. einem int aufrufen erstellt der Compiler aus dem Funktionstemplate eine entsprechende Funktion square für ints, rufen wir sie mit einem Typ auf, der diese Multiplikation nicht unterstützt gibt der Compiler dementsprechend einen Fehler aus(es geschieht also alles zur Compilezeit), schauen wir uns den Aufruf nun einmal an:
int main() { cout << square<int>(3) << endl; }
Wie gut zu erkennen ist, wird der Typparameter in spitzen Klammern zwischen dem Bezeichner und der normalen Parameterliste angegeben, notwendig ist dies jedoch nur, wenn der Compiler den Templateparameter nicht selbst bestimmen kann. In diesem Fall können wir auf das <int> auch verzichten, T ist der Typ eines Parameters und indem wir 3 übergeben, erkennt der Compiler bereits, dass es sich um ein int handelt und erstellt automatisch das richtige Funktionstemplate:
int main() { cout << square(3) << endl; //ruft die Funktion square<int> auf }
Templates und Header
Genau wie bei normalen Funktionen ist es möglich die Deklaration und Definition von Funktionstemplates(und Klassentemplates, s.u.) zu trennen. Wenn wir jedoch versuchen die Definition in eine eigene Sourcedatei auszulagern stellen wir fest, dass das nicht möglich ist. Templates müssen immer in der Datei vorhanden sein, in der sie verwendet werden, das heißt sie müssen vollständig im Header vorhanden sein.
Eine weit verbreitete Lösung ist es eine Datei square.impl zu erstellen und diese in square.hpp zu inkludieren, so wird zwar immernoch Deklaration und Definition vollständig in alle Sourcedateien inkludiert, allerdings bleibt zumindest die Trennung von Deklaration und Definition erhalten.
Der C++ Standard definiert eine Lösung für dieses Problem: das Schlüsselwort export. export ist so ziemlich die größte Katastrophe des C++ Standards, da dieses Schlüsselwort überaus problematisch zu implementieren ist. Die Folge: selbst jetzt, fast 12 Jahre nach erscheinen des Standards gibt es kaum einen, der dieses Sprachfeature umsetzt. Der erste war der Comeau-Compiler, weder der GCC noch Visual C++ implementieren dieses Feature derzeit von der Benutzung des Schlüsselwortes ist also abzuraten.
Zugriff auf Membertypes
Nehmen wir an, wir wollen auf den Membertyp A des Templates T zugreifen, an dieser Stelle gibt es einen Unterschied zur einfachen Verwendung der Klassen. Und zwar kann der Compiler(oder manche können es eben doch, aber nicht jeder) aus T::A nicht schlussfolgern, dass es sich um einen Typ handelt und streicht
T::A a;
z.B. als Fehler an. Abhilfe schafft es in dem Fall ein class oder ein typename voranzustellen:
typename T::A a;
Templatespezialisierungen
Manchmal mag es sinnvoll erscheinen, wenn ein generischer Algorithmus sich für bestimmte Typen anders verhält, dies lässt sich durch eine Templatespezialisierung erreichen. Wir erstellen einen solchen Spezialfall für einzelne Typen, indem wir die <> in der Templatedeklaration leerlassen und der Funktion die Typen in <> nachstellen, square für einen Typ some_type spezialisiert sähe demnach so aus:
template <> some_type square<some_type>(some_type const& x) //der Funktionskopf muss zum Template passen. some_type x wäre nicht erlaubt { //tue etwas besonderes mit x vom Typ some_type }
Klassentemplates
Wir können nicht nur Funktionen mit Templateparametern ausstatten, wir können auch Klassentemplates erstellen und haben sogar schon mit Klassentemplates gearbeitet. Die Syntax sieht ähnlich aus und man kennt sie von den Klassen vector, shared_ptr und function, welche beide einen Typen nehmen, die Notwendigkeit sinnvolle Anwendungsbeispiele zu bringen erledigt sich damit auch, Containerklassen, wie vector sind jedoch wohl das klassischste Beispiel.
Wenn wir ein Klassentemplate erstellen wollen, machen wir das ebenso, wir stellen der Klasse ein template <typename T,...> voran(mit den entsprechenden Templateparametern in den <>) und können fortan die Templateparameter in der Klasse verwenden. Schreiben wir mal einen ganz simplen Smartpointer, der mit einem Pointer konstruiert wird und diesen im Destruktor zerstört(noch ohne const-correctness etc.). Zunächst die Schnittstelle:
template <typename T> class simple_ptr { public: simple_ptr(T* ptr); ~simple_ptr(); T& operator*(); T* operator->(); private: simple_ptr(simple_ptr const&); //Die Klasse soll nicht kopierbar sein. simple_ptr& operator=(simple_ptr&); T* m_ptr; };
Okay, ähnelt der Funktionsdefinition, wir haben einen Typparameter und können diesen Typ innerhalb der gesamten Klasse verwenden. Der Konstruktor von simple_ptr nimmt ein T* ptr und speichert diesen in m_ptr. Der Destruktor löscht diesen. Die Operatoren * und -> sind überladen um Zugriff auf den Pointer zu erhalten. Kopierkonstruktor und operator= sind private, die Klasse ist dadurch nicht kopierbar, wir brauchen dann auch keine Implementierung. Wie definieren wir unsere Funktionen nun?
Zunächst: auch hier gilt, alles muss im Header definiert werden.
Als nächstes schauen wir uns einmal die Definition des Konstruktors als Beispiel an:
template <typename T> simple_ptr<T>::simple_ptr(T* ptr) : m_ptr(ptr) {}
Auch hier ist template <typename T> vorangestellt, es muss vor jeder Memberfunktionsdefinition stehen. Ansonsten unterscheidet es sich im Prinzip nicht von einer normalen Definition einer Memberfunktion, wichtig ist noch auf das <T> nach dem Klassennamen aufmerksam zu machen, wir definieren schließlich eine Memberfunktion für die Klasse simple_ptr<T>.
Die restlichen Memberfunktion können in gleicher Weise definiert werden, schauen wir sie uns auch noch an:
template <typename T> simple_ptr<T>::~simple_ptr() { if(m_ptr) delete m_ptr; m_ptr = NULL; } template <typename T> T& simple_ptr<T>::operator*() { return *m_ptr; } template <typename T> T* simple_ptr<T>::operator->() { return m_ptr; }
Gut. Die Definition unserer Klasse steht nun bereits, fehlt noch eine Beispielverwendung für unsere Klasse. Wie wir diese schreiben ist uns bereits bekannt, wir geben die Templateparameter hinter dem Typen an, wie wir es bereits bei vector etc. getan haben:
int main() { simple_ptr<string> some_stringptr(new string("hallo")); cout << *some_stringptr << endl; } //simple_ptr verlässt den scope, das Objekt, das mit new string("hallo") erstellt wurde, wird mit ihm gelöscht.
Defaulttemplateparameter
Klassentemplates erlauben uns noch einige Dinge, die wir mit Funktionstemplates nicht machen können, z.B. Templateparametern einen Defaultwert zuzuweisen. So ist vector z.B. eigentlich ein Klassentemplate, das 2 Templateparameter nimmt
template < class T, class Allocator = allocator<T> > class vector;
der zweite Parameter ist ein Allocator, eine Klasse, die die Speicherverwaltung von std::vector verwaltet. Geben wir keinen zweiten Templateparameter an wird genau wie bei Funktionen der Defaultparameter genommen: allocator<T>, die Standardspeicherverwaltung. Interessant ist noch, dass allocator<T> eine Templateklasse mit dem Parameter T ist, den wir als ersten Templateparameter an das Klassentemplate vector übergeben haben.
Template-Templates
Wenn wir ein Template haben, können wir diesem natürlich als Typ auch eine Klasse eines Templates, wie z.B. vector<int> übergeben, aber manchmal mag es Sinn machen ein Klassentemplate, wie z.B. vector selbst zu übergeben. dazu bedarf es Template-Templateparametern. Wollen wir z.B. eine Funktion foo haben, die einen Template-Templateparameter nimmt, der vector sein kann und die nichts weiter tut, als eine Instanz dieses Template-Templates mit dem Templateparameter int zu erstellen und zurückgeben(sinnlos, ich weiß), würde diese so aussehen:
template <template <typename, typename> class U> U<int, allocator<int> > foo() { return U<int, allocator<int> >(); }
Interessant ist hierbei der Templateparameter
template <typename, typename> class U
Dieser besagt, dass U ein Klassentemplate mit 2 Templateparametern sein soll.
Templatemetaprogrammierung
Zum Schluss möchte ich noch ein paar Worte über die sogenannte Templatemetaprogrammierung verlieren, die im Zusammenhang mit modernem C++ häufig auftaucht, ohne sie hier lehren zu wollen. C++ Templates sind Turingvollständig, das heißt, dass es möglich ist jeden denkbaren Algorithmus in C++-Templates zu realisieren, es handelt sich quasi um eine vollständige Untersprache in der Sprache selbst. Algorithmen, die mit Templatemetaprogrammierung geschrieben wurden, werden vom Compiler umgesetzt und erlauben es somit Rechenzeit auf Compilezeit auszulagern oder auch neue Designkonzepte umzusetzen. Templatemetaprogrammierung würde den Rahmen eines umfassenden C++ Tutorials sprengen, es ist ein Thema über das bereits selbst einige Bücher erschienen sind.