Einführung in die Programmierung mit Templates |
Mittwoch, den 21. Dezember 2005 um 15:00 Uhr |
Inhalt:
EinführungWem ging das nicht schon mal so: Eine (ähnliche, ja fast gleiche) Funktion oder Klasse musste mehrfach implementiert werden, weil wir sie für verschiedene Typen einsetzen wollten. Das Paradebeispiel sind Container-Klassen wie Listen: Einmal brauchen wir eine für int, dann eine für std::string und schließlich noch eine für unsere eigenen Klassen. Jedesmal eine Liste speziell für einen Typ zu schreiben, das wäre sehr zeitaufwändig und auch mühsam, abgesehen davon würden sich wahrscheinlich Fehler einschleichen, da viel mit Copy & Paste gearbeitet würde. Glücklicherweise bietet uns C++ aber ein Werkzeug an, mit dem wir "typunabhängig" programmieren können: Templates! Praktisch die gesamte C++-Standardbibliothek besteht aus Templates, angefangen bei std::string über std::vector bis zu den vielen Algorithmen wie std::copy oder std::find. Die Compiler-FrageDer immer noch sehr weitverbreitete und veraltete "VC++ 6"-Compiler ist leider nicht besonders gut für die (insbesondere fortgeschrittene) Template-Programmierung geeignet, für ein vernünftiges Arbeiten ist mindestens der VC 7.1 notwendig. Mit einem aktuellen g++ ist man auch auf der sicheren Seite. Normale Templates wie Container sind für den VC++ 6 kein Problem, aber bei komplizierteren Deklarationen streikt er ziemlich schnell. Die Template-Klassen aus Abschnitt 5 z.B. wird der VC++ 6 mit vielen Fehlermeldungen quittieren, obwohl der Code korrekt ist (im Extremfall kann der Compiler abstürzen). Im Übrigen ist selbst der sehr gute g++ nicht unverwundbar. Es gibt einige Sachen, die der Compiler einfach (noch) nicht verträgt. Wir empfehlen die aktuelle Visual C++ Express Edition zu benutzen. Gerade als Anfänger hat man keinen Grund, noch mit dem veralteten VC++ 6 zu beginnen, sondern sollte gleich mit einer neueren, besseren Version in die C++-Programmierung einsteigen. Die neuen Compiler werden sich auch abseits des Template-Schlachtfelds positiv mit schnellerem und besserem Code bemerkbar machen. Ich habe die Beispiele alle mit dem g++ 3.3.6 problemlos kompilieren können. Man sollte sich im Übrigen von den "umfangreichen" Fehlermeldungen des Compilers bei Templates nicht einschüchtern lassen, auch wenn sie zu Beginn kaum lesbar erscheinen, mit der Zeit gewöhnt man sich daran. Definition von TemplatesUnd los geht's: Um dem Compiler mitzuteilen, dass man ein Template definieren möchte, bedient man sich folgendem Präfix, welches einer Funktion oder Klasse vorangestellt wird: template <class T> // oder: template <typename T> T stellt einen Parameter mit einem beliebigen Typ dar, und obwohl hier das Schlüsselwort class steht, kann man auch char oder double einsetzen. Das Schlüsselwort typename ist gleichwertig mit class, allerdings kann man die Verwendung von beiden wie folgt einteilen: typename wird verwendet, wenn ein built-in oder eine Klasse als Parameter kommen kann, class wird benutzt, wenn ausschließlich Klassen erwartet werden. Diese Einteilung dient nur der Übersichtlichkeit und hat sonst keine Auswirkungen. Selbstverständlich kann man auch mehrere Template-Parameter angeben: // Zwei Parameter, einer vom Typ T und einer vom Typ U template <class T, class U> ... template <class T, int number> // Ein Parameter vom Typ T und einer vom Typ int ... Für "nicht-Typ-Parameter", also built-ins, gelten folgende Einschränkungen:
Im Übrigen werden built-ins als Konstanten behandelt, auf diesem Wege könnten wir z.B. bei einem Array Es ist jedoch möglich, Referenzen oder Zeiger auf Gleitpunkt-Typen als Parameter anzugeben: template <class T, float &f> ... Außerdem kann man den Parametern, wie gewohnt, Default-Werte geben: // FastCopy ist irgendeine Klasse template <class T=FastCopy, int number=10> ... Hierbei gelten die gleichen Regeln wie bei normalen Default-Parametern:
Funktions-TemplatesFrüher, als die Gummistiefel noch auch Holz waren ;) , war min ein äußerst beliebtes und bekanntes Makro, um den kleineren von zwei Werten herauszufinden: #include <iostream> #define MIN(a,b) ((a < b) ? a : b) using namespace std; int main (int argc, char **argv) { int x = 5,y = 6; int z = MIN(x, y); cout << z << '\n'; // Gibt 5 aus return 0; } Das war in C vielleicht noch gut, aber in C++ haben wir Templates, um solche Dinge sauber zu implementieren (die STL enthält bereits eine Template-Funktion namens min): #include <iostream> // Ermittelt das Minimum aus a und b template <typename T> inline const T& minimum(const T &a, const T &b) { return a < b ? a : b; }; int main (int argc, char **argv) { int x = 5, y = 6; // Explizite Instanzierung (siehe "Übergabe von Argumenten"): int z = minimum<int>(x, y); std::cout << z << '\n'; // Funktioniert auch für chars: char a = 'a', b = 'b'; // Implizite Instanzierung (siehe "Übergabe von Argumenten"): std::cout << minimum(a, b) << '\n'; // Gibt a aus return 0; } Das Funktions-Template minimum hat zwei Parameter vom Typ Referenz auf const-T und als Rückgabewert ebenfalls eine Referenz auf const-T. Was T ist bzw. später mal sein wird, das interessiert uns nicht. Das braucht nur der Aufrufer zu wissen. Der Maschinencode für ein Funktions-Template wird bei der ersten Instanzierung für einen Typ erzeugt, bei der Definition selber wird nichts erzeugt. So wird im obigen Beispiel zuerst eine Funktion für den Typ int erzeugt, und dann noch eine weitere für den Typ char. Vereinfacht gesagt geht der Compiler hin und setzt für jedes T den von uns gewählten Typ ein. Vom Compiler werden Templates zweimal auf Fehler überprüft: zuerst beim Kompilieren der Template-Definition und dann noch einmal bei der Instanzierung. Beim ersten Drübergehen werden typunabhängige Fehler (z.B. Syntaxfehler) erkannt. Fehler, die vom Typ abhängen, z.B. ein fehlender Operator des Typs, werden dann beim zweiten Mal angezeigt. Klassen-TemplatesDa es möglich ist, Funktions-Templates zu bilden, muss es auch möglich sein, ein Klassen-Template zu erstellen. Am Beispiel der Klasse Pair (die STL enthält bereits ein Klassen-Template namens pair), die ein Wertepaar darstellt, werden wir uns dies anschauen: #include <iostream> #include <string> // Diesmal zwei Parameter, U ist per default gleich T template <typename T, typename U=T> struct Pair { // Zwei Datenelemente T first; U second; Pair(const T &a, const U &b) : first(a), second(b) {} Pair(const Pair &p) : first(p.first), second(p.second) {} ~Pair() {} Pair& operator=(const Pair&); }; // Definition außerhalb der Klasse: template <typename T, typename U> Pair<T,U>& Pair<T,U>::operator=(const Pair<T,U> &p) { if (this == &p) return *this; first = p.first; second = p.second; return *this; }; int main (int argc, char **argv) { // Wir bilden ein Paar vom Typ float: Pair<float> floatPair(5.1, 8.9); std::cout << floatPair.first << '\t' << floatPair.second << '\n'; // Erster Parameter ist ein string, der zweite ein int: Pair<std::string, int> mixPair("zwanzig", 20); std::cout << mixPair.first << '\t' << mixPair.second << '\n'; return 0; } Bei der ersten Instanzierung von Pair wird zuerst der Maschinencode aller Methoden generiert (die Methoden der Klasse Pair sind im Grunde nur Funktions-Templates), und erst dann das Objekt floatPair aufgebaut. Der Maschinencode von mixPair unterscheidet sich im Übrigen von dem Maschinencode von floatPair! Hier wird auch deutlich, dass wir mit Templates den Maschinencode nicht reduzieren, aber sehr wohl das Duplizieren von Sourcecode vermeiden können. Außerdem sollte man sich vor Augen führen, dass Templates, bedingt durch ihre Natur, sehr statische Konstrukte sind. Verwendung finden Templates auch bei der Implementierung von Container-Klassen wie einem Stack, gerade hier kann man durch Verwendung von Templates richtig Zeit einsparen, anstatt einen IntStack, einen CharStack usw. zu schreiben, schreibt man eine Template-Klasse: template <typename T> class Stack { public: Stack(size_t); Stack(const Stack&); ~Stack(); void push(const T&); T pop(); const T& peek() const; void clear(); bool empty() const; Stack& operator=(const Stack&); private: T *arr; size_t sz, tip; }; Übergabe von ArgumentenBei der Übergabe von Argumenten an Templates gibt es einige Regeln, die man unbedingt kennen sollte: Die Typen der Argumente müssen exakt mit den Typen der Template-Parameter übereinstimmen, bei einer impliziten Instanzierung findet nicht einmal eine, sonst übliche, implizite Konvertierung wie z.B. von int nach long statt: long l = 7; int i = 8; // minimum Template von oben, ein long und ein int cout << minimum(l, i) << '\n'; // Implizit: Eeeh, Fehler! cout << minimum<long>(l, i) << '\n'; // Explizit: Funktioniert! Bei der expliziten Instanzierung kann man den gewünschten Typ angeben und es wird eine Typumwandlung durchgeführt. Die explizite Instanzierung ist ebenfalls notwendig, wenn der Typ nicht als Parameter einer Funktion erscheint, sondern nur intern verwendet wird: template <typename T> void foo() { T tmp; //... }; // Explizite Instanzierung notwendig! foo<int>(); Für Template-Argumente gibt es wiederum einige Einschränkungen, diese gelten aber nur für built-ins:
Überladen (Spezialisierung) von Funktions-TemplatesManchmal passiert es, dass ein Template für einen bestimmten Typ kein vernünftiges Ergebnis liefert oder eine spezialisierte Funktion effizienter arbeiten könnte. Unser minimum-Template funktioniert z.B. für int ganz ausgezeichnet, aber was ist mit C-Strings? Da würde unser Template versagen bzw. einfach den C-String mit der kleineren Adresse zurückgeben, nicht gerade das, was wir wollen: #include <iostream> #include <cstring> using namespace std; // Ermittelt das Minimum aus a und b template <typename T> inline const T& minimum(const T &a, const T &b) { return a < b ? a:b; }; // Spezialisierung für C-Strings inline const char* minimum(const char *str1, const char *str2) { return ( (strcmp(str1, str2) < 0 ) ? str2 : str1 ); }; int main (int argc, char **argv) { //Aufruf der "normalen" Template-Funktion cout << minimum(8, 10) << '\n'; //Aufruf der spezialisierten Funktion cout << minimum("HALLO", "hallo") << '\n'; return 0; } Eigentlich haben wir jetzt die Funktion minimum überladen, um unser Ziel, also die Spezialisierung, zu erreichen. Der Compiler geht bei der Auswahl der passenden Funktion folgendermaßen vor:
Wird keine oder mehrere passende Funktion(en) gefunden, ist dies ein Fehler. Laut ANSI-Standard führt obiger Code zu einer Fehlermeldung, da dort normale und Template-Funktionen nicht unterschieden werden. Um dies zu vermeiden, muss man vor der Spezialisierung noch ein template<> einfügen. Nun ist die spezialisierte Version von minimum aber eine gewöhnliche Funktion, d.h. es wird sobald der Header von mehreren Sourcefiles eigebunden wird, Linkerfehler wegen mehrfacher Definition von minimum hageln. Um dies zu vermeiden, sollten wir die Funktion in eine Implementationdatei auslagern. Des Weiteren könnten wir sie inline oder static klassifizieren oder wir betten sie in einen (anonymen) namespace ein. Die namespace-Variante hat allerdings den Nachteil, dass minimum in mehreren Objektdateien präsent ist. Vollständige/Partielle Spezialisierung von Klassen-TemplatesManchmal ist es wichtig, dass eine Template-Klasse bei einer gewissen Kombination der Typ-Parameter etwas ganz Spezielles tut, dies erreicht man durch die partielle bzw. vollständige Spezialisierung von Template-Klassen. Bevor wir die partielle Spezialisierung sehen, zuerst eine vollständige Spezialisierung der Pair-Klasse: #include <iostream> template <typename T, typename U=T> struct Pair { // wie oben }; struct MyServer {}; struct MyClient {}; // MyServer und MyClient sind hier irgendwelche Klassen, für die das Template // vollständig spezialisiert wird: template <> struct Pair<MyServer, MyClient> { Pair() { std::cout << "Vollstaendige Spezialisierung aufgerufen!" << '\n'; } //... }; int main(int argc, char **argv) { Pair<MyServer, MyClient> myPair; // Instanzierung des Templates return 0; } Wenn wir jetzt also die Klasse Pair mit diesen speziellen Parametern, nämlich MyServer und MyClient, aufrufen, dann wird das spezialisierte Template benutzt. Andernfalls wird die generische Implementierung verwendet. Kommen wir nun zu der partiellen Spezialisierung, bei der es (mal wieder) ein paar Regeln zu beachten gilt:
Und so kann partielle Spezialisierung aussehen: // Spezialisierung für MyServer und ein beliebiges U template <typename U> struct Pair<MyServer, U> { //... }; //Spezialisierung für ein beliebiges T und MyClient template <typename T> struct Pair<T, MyClient> { //... }; int main(int argc, char **argv) { // Aufruf der ersten Spezialisierung Pair<MyServer, UnknownClient> firstPair; // Aufruf der zweiten Spezialisierung Pair<SomeServer, MyClient> secondPair; // Aufruf der generischen Implementation Pair<SomeServer, UnknownClient> thirdPair; return 0; } Das Spielchen kann man ziemlich weit treiben, denn der Algorithmus zur Bestimmung des am meisten spezialisierten Templates ist sehr exakt und wählt die Implementierung mit der höchsten Übereinstimmung aus. Das Schlüsselwort exportBei den vorangegangenen Beispielen war es noch nicht notwendig, aber wenn man größere Klassen oder Bibliotheken implementiert, dann möchte man die Schnittstelle in eine .hpp-Datei schreiben und die Implementation in eine .cpp-Datei auslagern. Der Vorteil hierbei ist, dass man u.a. den zeitlichen Aufwand für das Kompilieren schmälern kann. Bei Templates funktioniert das leider nicht, da laut Standard das Template bei der Instanzierung, dem Compiler vollständig bekannt sein muss. Das Schlüsselwort export sollte hier eigentlich Abhilfe schaffen, allerdings konnte es, abgesehen von EDG-Compilern wie dem Comeau, noch kein Hersteller umsetzen und die Lösung des Comeau Compilers ist nur bedingt befriedigend, da sie z.B. die Zeiten für die Kompilierung erhöht und u.U. anderen Code als ohne export erzeugt! Es ist jedoch möglich, diese Einschränkung mit einem Trick zu umgehen: Wir inkludieren einfach eine .impl-Datei (normale Source-Datei mit .impl-Endung) in die .hpp-Datei: // stack.hpp template <typename T> class Stack { // wie oben, nur die Schnittstelle }; // Achtung: #include "stack.impl" // stack.impl template<typename T> inline bool Stack<T>::empty() const { return (tip == 0) ? true : false; }; //... Es ist zwar kein export, aber so kann man zumindest die Schnittstelle von der Implementation sauber trennen. Zum Schluss...Das war jetzt nur eine kleine Einführung, es gibt noch so vieles, was man mit Templates machen kann, von Policy-basiertem Klassendesign über Typlisten bis zu Objektfabriken. Templates können einem das Leben extrem erleichtern. Für einen tieferen Einstieg in die Materie empfehle ich "C++ Templates: The Complete Guide" von David Vandevoorde und Nicolai M. Josuttis und "Modernes C++ Design" von Andrei Alexandrescu . "Gehobenes Niveau", aber sehr lesenswert. |
Zuletzt aktualisiert am Dienstag, den 22. Januar 2008 um 23:12 Uhr |
AUSWAHLMENÜ | ||||||||
|