[X] Einführung in die Programmierung mit Templates
-
In 1.1 steht noch was von Template-Template-Parametern :
Für diesen Artikel reicht der VC++ 6 Compiler aus, außer für den Abschitt über partielle Template-Spezialisierung und Template-Template Parameter.
Ich finde, die wesentlichen Nachteile von export fehlen eigentlich noch:
- Template-Sourcecode muss trotz separater Datei immernoch vollständig mitgegeben werden (1.größter Irrglaube)
- Durch Auslagerung der Implementierung in die .cpp Datei verschwinden die Abhängigkeiten nicht, sondern werden nur versteckt (2.größter Irrglaube)
- export ist unterspezifiziert - EDG-basierte Compiler (z.B. Comeau) haben nur eine mögliche Implementierung von export
- im Regelfall höhrere Compilezeiten
- export macht es dem Compiler wesentlich schwieriger, brauchbare Fehlermeldungen zu Templates zu geben
- export kann die Semantik des Codes ändern (!!) (schwieriger in einem Einsteiger-Artikel zu erklären)
-
Also erstmal danke für die Hinweise.
Jetzt zu export: Meiner Meinung nach ist das eher unwichtig, da es praktisch nicht unterstützt wird, deshalb habe ich mich auch noch nicht damit beschäftigt. Wenn du aber das nötige Know-how und Zeit hast, dann überlasse ich dir gerne diesen Abschnitt. Ansonsten mache ich das selber. Was meinst dazu?
-
Das stimmt schon, sooo wichtig ist es auch nicht. Ich finde nur, dass ein so ein zentraler Einsteiger-Artikel ein guter Platz wäre, mit falschen Gerüchten aufzuräumen. Nicht das sich jemand nur für export den Comeau-Compiler kauft und sich drüber wundert, dass garnicht das macht, was man will.
Zeit ist so eine Sache... Mit meinem Threads-Artikel bin ich auch noch nicht so weit wie ich wollte. Aber nur den Abschnitt zu export krieg ich schon hin. Ggf. kannst du den ja dann deinem Schreibstil anpassen, damit es einheitlicher wirkt
Gruß
-
Also gut, dann gehört der Abschnitt dir Gibst mir dann bescheid wenn du soweit bist, ich bau ihn dann mit entsprechenden Credits für dich ein.
-
In diesem Artikel versuche ich, einen kleinen Einblick in die Welt der generischen Programmierung mit Templates in C++ zu geben. Abschnitt 6 wurde von 7H3 N4C3R beigesteuert, an dieser Stelle ein Dankeschön von mir.
Inhalt:
1. Einführung
1.1 Die Compiler-Frage
2. Definition von Templates
2.1 Funktions-Templates
2.2 Klassen-Templates
3. Übergabe von Argumenten
4. Überladen (Spezialisierung) von Funktions-Templates
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
6. Das Schlüsselwort export
7. Zum Schluss...######################################################################
1. Einführung
######################################################################Wem 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 von std::string, über std::vector bis zu den vielen Algorithmen wie std::copy oder std::find.
######################################################################
1.1 Die Compiler-Frage
######################################################################Der weitverbreitete VC++ 6 Compiler ist leider nicht besonders gut für die (insbesondere fortgeschrittene) Template-Programmierung geeignet, mit Version 7 hat sich zwar viel getan, aber es gibt im Vergleich zum g++ immer noch Defizite. Für diesen Artikel reicht der VC++ 6 Compiler aus, außer für den Abschitt über partielle Template-Spezialisierung. 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.######################################################################
2. Definition von Templates
######################################################################Und 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:
1. Sie dürfen nicht verändert werden
2. Sie dürfen nur ganzzahlig seinEs 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:
1. Wenn ein Parameter einen Default-Wert bekommt, so müssen alle nachfolgenden Parameter einen bekommen
2. Wird bei der Instantiierung ein Argument weggelassen, so müssen alle nachfolgenden Argumente weggelassen werden######################################################################
2.1 Funktions-Templates
######################################################################Frü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 EXIT_SUCCESS; };
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 Instantiierung (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 Instantiierung (siehe "Übergabe von Argumenten"): std::cout<<minimum(a,b)<<'\n'; //Gibt a aus return EXIT_SUCCESS; };
Das Funktions-Template minimum hat zwei Parameter vom Typ const-Referenz auf T, und als Rückgabewert ebenfalls eine const-Referenz auf 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 Instanziierung 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 Instantiierung. 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.
######################################################################
2.2 Klassen-Templates
######################################################################Da 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), das 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 EXIT_SUCCESS; };
Bei der ersten Instanziierung 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; };
######################################################################
3. Übergabe von Argumenten
######################################################################Bei 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 Instantiierung 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 Instantiierung kann man den gewünschten Typ angeben und es wird eine Typumwandlung durchgeführt. Die explizite Instantiierung 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 Instantiierung notwendig! foo<int>();
Für Template-Argumente gibt es wiederum einige Einschränkungen, diese gelten aber nur für built-ins:
1. Ist der Parameter ein Zeiger, so dürfen nur Adressen mit globalem Geltungsbereich übergeben werden
2. Ist der Parameter eine Referenz, so dürfen nur Objekte mit globalem oder statischem Geltungsbereich übergeben werden
3. Ist der Parameter weder Referenz, noch Zeiger, so dürfen nur konstante Werte übergeben werden######################################################################
4. Überladen (Spezialisierung) von Funktions-Templates
######################################################################Manchmal 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 EXIT_SUCCESS; };
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:1. Findet der Compiler eine normale Funktion, die er ohne Typumwandlung aufrufen kann, so wird diese aufgerufen
2. Bei einem oder mehreren, unterschiedlich spezialisierten Templates, wählt der Compiler stets das Template bzw. das spezialisierteste aus
3. Konnte keine passende Funktion gefunden werden, so werden normale Funktionen überprüft, bei denen Typumwandlungen zum Erfolg führenWird keine oder mehrere passende Funktionen gefunden, ist dies ein Fehler.
Laut ANSI-Standard führt dieser 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.
######################################################################
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
######################################################################Manchmal 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; //Instanziierung des Templates return EXIT_SUCCESS; };
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:
1. Ein Klassen-Template kann sowohl vollständig, als auch partiell spezialisiert werden
2. Eine Member-Methode eines Klassen-Templates kann nur vollständig spezialisiert werden
3. Eine Funktion auf namespace Ebene kann nicht partiell spezialisiert werden, wobei die Überladung (siehe oben), als eine Art Ersatz angesehen werden kann.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.
######################################################################
6. Das Schlüsselwort export
######################################################################//Hier steht noch mein Kram bis 7H3 N4C3R diesen Abschnitt fertig hat.
Bei 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, und die Implementation in eine .cpp Datei schreiben.
Bei Template-Klassen hingegen schreibt man die Definition in die selbe Header Datei, wie die Schnittstelle, denn so können wir sicher sein, dass der Compiler weiß, wo sich die Definition befindet, um den korrekten Maschinencode für die Argumente zu erzeugen.Das Schlüsselwort export sollte eigentlich dafür sorgen, dass wir unsere Definition "auslagern" können, allerdings bietet AFAIK nur der Comeau-Compiler dieses Feature an.
Es ist jedoch möglich, diese Einschränkung mit einem Trick zu umgehen: Wir inkludieren einfach die .cpp Datei in die .hpp Datei://stack.hpp template <typename T> class Stack { //wie oben, nur die Schnittstelle }; //Achtung: #include "stack.cpp"
//stack.cpp template<typename T> inline bool Stack<T>::empty() const { return (tip==0) ? true : false; }; //...
Es ist zwar ein Hack, aber so kann man zumindest die Schnittstelle von der Implementation sauber trennen.
######################################################################
7. 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 "Modernes C++ Design" von Andrei Alexandrescu. "Gehobenes Niveau", aber sehr lesenswert.[/quote]
-
So, jetzt noch mein Teil zu export
(Huch, das ist ja doch einiges geworden. Wenn Du (GPC) willst, pass es ruhig deinem Schreibstil an. Ich hoffe, der Text ist für Einsteiger immernoch zumutbar )
Edit: kleine Rechtschreibfehler behoben
#############################
Bei 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, und die Implementation in eine .cpp Datei schreiben.
Die Motivation dahinter ist, dass jede Änderung an einer Headerdatei dazu führt, dass all der Code neu kompiliert werden muss, der diese Headerdatei benutzt. Gerade in Projekten mit vielen Dateien löst das große Neu-Kompilier-Wellen aus, die ziemlich lange dauern können.Leider geht das bei Templates so nicht. Das liegt daran, dass das Template erst überall dort in den Code eingesetzt wird, wo es auch verwendet wird. Vorher ist es "nur ein Stück Text", erst beim Einsetzen bekommt es seine Bedeutung. Genau das verursacht aber das oben beschriebene Verhalten vom Neu-Kompilieren.
Der/die eine oder andere hat nun aber vielleicht schon vom Schlüsselwort export gehört, welches nun doch genau diese Trennung ermöglichen soll. In die Headerdatei schreibt man vor das template einfach nur das Wörtchen export, und schon kann man eine .cpp-Datei mit der Implementation füllen.
Stopp. Soweit so gut, das war die Theorie. In der Praxis steht es um export aber völlig anders. Zuerst: Kaum ein Compiler unterstützt es überhaupt. Lediglich die Front-Ends, die auf den EDG-Compiler (Edison Design Group) aufsetzen, beherrschen es. Das ist im Wesentlichen der Comeau-Compiler. Das und die Tatsache, dass die Entwicklungszeit für dieses Compilerfeature drei Mannjahre betrug, sollte einen stutzig machen (eine mittelgroße bis große Individualanwendung hat ca. zwei Mannjahre Entwicklungszeit). Was stimmt also mit export nicht?
Im Wesentlichen kann export nicht das halten, was man sich von der Trennung in .hpp und .cpp Datei verspricht.
Man könnte denken, dass man bei einer Auslieferung einer selbst geschriebenen Bibliothek nur die .hpp Datei mitgegeben werden muss und die Implementierung in der .cpp Datei versteckt bleibt (als kompilierte .o Datei zur Bibliothek dazugelinkt). Das ist nicht so. Die kurze Antwort ist, dass der Standard verlangt, dass das Template bei seiner Instanziierung vollständig (inklusive Implementation im Quellcode-Format) bekannt ist.
Auch ein anderer scheinbarer Vorteil ist nicht gegeben. Nämlich dass durch das Auslagern der Implementation nicht mehr soviel Quellcode neu übersetzt werden muss. Das ist zwar so schon richtig, dafür muss aber die .cpp Datei des Templates für jeden Datentyp, für den das Template instanziiert wird, übersetzt werden. Die Abhängigkeiten, die durch das Verlagern der Implementation in die .cpp Datei verschwinden, schlagen hinterrücks wieder zu. Denn sie sind nur versteckt, aber nicht aus der Welt.
Andere Nachteile von export sind in der Regel höhere Compilezeiten, auch wenn sie in der Theorie eigentlich sinken sollten. Außerdem haben EDG-basierte Compiler nur eine mögliche Implementierung von export. Das liegt daran, dass der Standard dieses Schlüsselwort nicht genau genug beschreibt. Es wäre möglich, dass andere Compilerhersteller export nach der Beschreibung im Standard korrekt implementieren, es sich aber überall unterschiedlich verhält und unterschiedlichen Code produziert.
Der wohl schlimmste Nachteil von export (dessen Erklärung hier im Detail wohl zu weit führt) ist, dass es die Bedeutung von definierten Sprachfeatures gefährlich verändert. Im Endeffekt muss man höllisch aufpassen, um mit export den selben Code zu schreiben, wie ohne.
Deshalb das Fazit (zumindest für die nächste Zeit) : Finger weg von export.
-
In diesem Artikel versuche ich, einen kleinen Einblick in die Welt der generischen Programmierung mit Templates in C++ zu geben. Abschnitt 6 wurde von 7H3 N4C3R beigesteuert, an dieser Stelle ein Dankeschön von mir.
Inhalt:
1. Einführung
1.1 Die Compiler-Frage
2. Definition von Templates
2.1 Funktions-Templates
2.2 Klassen-Templates
3. Übergabe von Argumenten
4. Überladen (Spezialisierung) von Funktions-Templates
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
6. Das Schlüsselwort export
7. Zum Schluss...######################################################################
1. Einführung
######################################################################Wem 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 von std::string, über std::vector bis zu den vielen Algorithmen wie std::copy oder std::find.
######################################################################
1.1 Die Compiler-Frage
######################################################################Der weitverbreitete VC++ 6 Compiler ist leider nicht besonders gut für die (insbesondere fortgeschrittene) Template-Programmierung geeignet, mit Version 7 hat sich zwar viel getan, aber es gibt im Vergleich zum g++ immer noch Defizite. Für diesen Artikel reicht der VC++ 6 Compiler aus, außer für den Abschitt über partielle Template-Spezialisierung. 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.######################################################################
2. Definition von Templates
######################################################################Und 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:
1. Sie dürfen nicht verändert werden
2. Sie dürfen nur ganzzahlig seinEs 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:
1. Wenn ein Parameter einen Default-Wert bekommt, so müssen alle nachfolgenden Parameter einen bekommen
2. Wird bei der Instantiierung ein Argument weggelassen, so müssen alle nachfolgenden Argumente weggelassen werden######################################################################
2.1 Funktions-Templates
######################################################################Frü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 EXIT_SUCCESS; };
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 Instantiierung (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 Instantiierung (siehe "Übergabe von Argumenten"): std::cout<<minimum(a,b)<<'\n'; //Gibt a aus return EXIT_SUCCESS; };
Das Funktions-Template minimum hat zwei Parameter vom Typ const-Referenz auf T, und als Rückgabewert ebenfalls eine const-Referenz auf 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 Instanziierung 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 Instantiierung. 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.
######################################################################
2.2 Klassen-Templates
######################################################################Da 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), das 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 EXIT_SUCCESS; };
Bei der ersten Instanziierung 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; };
######################################################################
3. Übergabe von Argumenten
######################################################################Bei 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 Instantiierung 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 Instantiierung kann man den gewünschten Typ angeben und es wird eine Typumwandlung durchgeführt. Die explizite Instantiierung 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 Instantiierung notwendig! foo<int>();
Für Template-Argumente gibt es wiederum einige Einschränkungen, diese gelten aber nur für built-ins:
1. Ist der Parameter ein Zeiger, so dürfen nur Adressen mit globalem Geltungsbereich übergeben werden
2. Ist der Parameter eine Referenz, so dürfen nur Objekte mit globalem oder statischem Geltungsbereich übergeben werden
3. Ist der Parameter weder Referenz, noch Zeiger, so dürfen nur konstante Werte übergeben werden######################################################################
4. Überladen (Spezialisierung) von Funktions-Templates
######################################################################Manchmal 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 EXIT_SUCCESS; };
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:1. Findet der Compiler eine normale Funktion, die er ohne Typumwandlung aufrufen kann, so wird diese aufgerufen
2. Bei einem oder mehreren, unterschiedlich spezialisierten Templates, wählt der Compiler stets das Template bzw. das spezialisierteste aus
3. Konnte keine passende Funktion gefunden werden, so werden normale Funktionen überprüft, bei denen Typumwandlungen zum Erfolg führenWird keine oder mehrere passende Funktionen gefunden, ist dies ein Fehler.
Laut ANSI-Standard führt dieser 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.
######################################################################
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
######################################################################Manchmal 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; //Instanziierung des Templates return EXIT_SUCCESS; };
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:
1. Ein Klassen-Template kann sowohl vollständig, als auch partiell spezialisiert werden
2. Eine Member-Methode eines Klassen-Templates kann nur vollständig spezialisiert werden
3. Eine Funktion auf namespace Ebene kann nicht partiell spezialisiert werden, wobei die Überladung (siehe oben), als eine Art Ersatz angesehen werden kann.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.
######################################################################
6. Das Schlüsselwort export
######################################################################Bei 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, und die Implementation in eine .cpp Datei schreiben.
Die Motivation dahinter ist, dass jede Änderung an einer Headerdatei dazu führt, dass all der Code neu kompiliert werden muss, der diese Headerdatei benutzt. Gerade in Projekten mit vielen Dateien löst das große Neu-Kompilier-Wellen aus, die ziemlich lange dauern können.Leider geht das bei Templates so nicht. Das liegt daran, dass das Template erst überall dort in den Code eingesetzt wird, wo es auch verwendet wird. Vorher ist es "nur ein Stück Text", erst beim Einsetzen bekommt es seine Bedeutung. Genau das verursacht aber das oben beschriebene Verhalten vom Neu-Kompilieren.
Der eine oder andere hat nun aber vielleicht schon vom Schlüsselwort export gehört, welches nun doch genau diese Trennung ermöglichen soll. In die Headerdatei schreibt man vor das template einfach nur das Wörtchen export, und schon kann man eine .cpp-Datei mit der Implementation füllen.
Stopp. Soweit so gut, das war die Theorie. In der Praxis steht es um export aber völlig anders. Zuerst: Kaum ein Compiler unterstützt es überhaupt. Lediglich die Front-Ends, die auf den EDG-Compiler (Edison Design Group) aufsetzen, beherrschen es. Das ist im Wesentlichen der Comeau-Compiler. Das und die Tatsache, dass die Entwicklungszeit für dieses Compilerfeature drei Mannjahre betrug, sollte einen stutzig machen (eine mittelgroße bis große Individualanwendung hat ca. zwei Mannjahre Entwicklungszeit). Was stimmt also mit export nicht?
Im Wesentlichen kann export nicht das halten, was man sich von der Trennung in .hpp und .cpp Datei verspricht.
Man könnte denken, dass man bei einer Auslieferung einer selbst geschriebenen Bibliothek nur die .hpp Datei mitgegeben werden muss und die Implementierung in der .cpp Datei versteckt bleibt (als kompilierte .o Datei zur Bibliothek dazugelinkt). Das ist nicht so. Die kurze Antwort ist, dass der Standard verlangt, dass das Template bei seiner Instanziierung vollständig (inklusive Implementation im Quellcode-Format) bekannt ist.
Auch ein anderer scheinbarer Vorteil ist nicht gegeben. Nämlich dass durch das Auslagern der Implementation nicht mehr soviel Quellcode neu übersetzt werden muss. Das ist zwar so schon richtig, dafür muss aber die .cpp Datei des Templates für jeden Datentyp, für den das Template instanziiert wird, übersetzt werden. Die Abhängigkeiten, die durch das Verlagern der Implementation in die .cpp Datei verschwinden, schlagen hinterrücks wieder zu. Denn sie sind nur versteckt, aber nicht aus der Welt.
Andere Nachteile von export sind in der Regel höhere Compilezeiten, auch wenn sie in der Theorie eigentlich sinken sollten. Auerdem haben EDG-basierte Compiler nur eine mögliche Implementierung von export. Das liegt daran, dass der Standard dieses Schlüsselwort nicht genau genug beschreibt. Es wäre mglich, dass andere Compilerhersteller export nach der Beschreibung im Standard korrekt implementieren, es sich aber überall unterschiedlich verhält und unterschiedlichen Code produziert.
Der wohl schlimmste Nachteil von export (dessen Erklärung hier im Detail wohl zu weit führt) ist, dass es die Bedeutung von definierten Sprachfeatures gefährlich verändert. Im Endeffekt muss man höllisch aufpassen, um mit export den selben Code zu schreiben, wie ohne.
Deshalb das Fazit (zumindest für die nächste Zeit) : Finger weg von export.
Es ist jedoch möglich, diese Einschränkung mit einem Trick zu umgehen: Wir inkludieren einfach die .cpp Datei in die .hpp Datei:
//stack.hpp template <typename T> class Stack { //wie oben, nur die Schnittstelle }; //Achtung: #include "stack.cpp"
//stack.cpp 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.
######################################################################
7. 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 "Modernes C++ Design" von Andrei Alexandrescu. "Gehobenes Niveau", aber sehr lesenswert.
-
7H3 N4C3R: Danke für deinen Teil, gefällt mir sehr gut, habe nur die Leerzeilen etwas zurückgedreht und eine kleine Korrektur bei "Der/die eine oder andere..." gemacht(die rausgeworfen). Sonst ist alles wie von dir. Werd's heute Abend zur Rechtschreibprüfung raushauen, vllt. wird's übers Wochenende noch fertig.
-
Mach das "Danke" mal nach unten oder in den betroffenen Abschnitt.
-
man bindet eigentlich bei Template klassen nicht .cpp files ein sondern .impl
Nur so als kleiner hinweisBR Vinzenz
-
So, ich habe mal wieder als Ahnungslose probegelesen.
1.1 Warum ist der VC6 nicht so geeignet, wie äußert sich das? Bekomme ich Compilerfehler, obwohl alles richtig ist?
6 Was ist eine hpp Datei? Ist das nur eine andere Endung für eine h Datei oer was ist das?
Sonst ist nichts drin, wo ich so direkt drüber stolpere.
-
estartu_de schrieb:
6 Was ist eine hpp Datei? Ist das nur eine andere Endung für eine h Datei oer was ist das?
Habe ich einfach mal so übernommen aus GPCs Teil Ist denke ich mal einfach nur eine Analogschreibweise für c -> cpp ( h -> hpp ). Ist sicherlich Geschmackssache.
Ansonsten, danke fürs "Danke"
-
Das Problematische an VC6 ist das er mit Templates nicht wirklich gut arbeiten kann.
Bei normalen Templatesachen ist das kein Problem. Aber bei krasseren Templategeschichten wie z.b.template < template class < typename A , B , template class < typename D > C > A > class StrangeStuff{};
kann der VC6 ganz schön ins Schleudern kommen.
Deswegen brauchte der VC6 für Loki auch ein Workaround ( Das hat btw Hume geschrieben )Bemerkbar kann sich das durch Compilerfehler oder gar Compilersegfaults (Compiler kackt einfach ab ) machen.
Selbst g++ ist davor nicht gefeit. Es gibt einige Sachen die der Compiler einfach nicht verträgt. Gerade SFINAE (Substantiation failure is not an error ) ist da sehr tödlich für einige Compiler.
Aber VC6 ist eben etwas altersschwach und noch nicht so fit wie die aktuelleren und gerät eben viel schneller aus dem Takt.
-
evilissimo schrieb:
man bindet eigentlich bei Template klassen nicht .cpp files ein sondern .impl
Nur so als kleiner hinweisBR Vinzenz
Ich persönlich stehe ich nicht auf die .impl Geschichte, aber ich mach's dir zuliebe rein
estartu_de schrieb:
1.1 Warum ist der VC6 nicht so geeignet, wie äußert sich das? Bekomme ich Compilerfehler, obwohl alles richtig ist?
Ja. Besonders bei partieller Spezialisierung und Policies oder z.B. Compiler-Assertionen.
6 Was ist eine hpp Datei? Ist das nur eine andere Endung für eine h Datei oder was ist das?
Genau, so wie's 7H3 N4C3R geschrieben hat.
-
In diesem Artikel versuche ich, einen kleinen Einblick in die Welt der generischen Programmierung mit Templates in C++ zu geben.
Inhalt:
1. Einführung
1.1 Die Compiler-Frage
2. Definition von Templates
2.1 Funktions-Templates
2.2 Klassen-Templates
3. Übergabe von Argumenten
4. Überladen (Spezialisierung) von Funktions-Templates
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
6. Das Schlüsselwort export
7. Zum Schluss...######################################################################
1. Einführung
######################################################################Wem 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 von std::string, über std::vector bis zu den vielen Algorithmen wie std::copy oder std::find.
######################################################################
1.1 Die Compiler-Frage
######################################################################Der weitverbreitete VC++ 6 Compiler ist leider nicht besonders gut für die (insbesondere fortgeschrittene) Template-Programmierung geeignet, mit Version 7 hat sich zwar viel getan, aber es gibt im Vergleich zum g++ immer noch Defizite. Für diesen Artikel reicht der VC++ 6 Compiler aus, außer für den Abschitt über partielle Template-Spezialisierung. 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.######################################################################
2. Definition von Templates
######################################################################Und 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:
1. Sie dürfen nicht verändert werden
2. Sie dürfen nur ganzzahlig seinEs 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:
1. Wenn ein Parameter einen Default-Wert bekommt, so müssen alle nachfolgenden Parameter einen bekommen
2. Wird bei der Instantiierung ein Argument weggelassen, so müssen alle nachfolgenden Argumente weggelassen werden######################################################################
2.1 Funktions-Templates
######################################################################Frü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 EXIT_SUCCESS; };
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 Instantiierung (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 Instantiierung (siehe "Übergabe von Argumenten"): std::cout<<minimum(a,b)<<'\n'; //Gibt a aus return EXIT_SUCCESS; };
Das Funktions-Template minimum hat zwei Parameter vom Typ const-Referenz auf T, und als Rückgabewert ebenfalls eine const-Referenz auf 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 Instanziierung 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 Instantiierung. 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.
######################################################################
2.2 Klassen-Templates
######################################################################Da 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), das 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 EXIT_SUCCESS; };
Bei der ersten Instanziierung 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; };
######################################################################
3. Übergabe von Argumenten
######################################################################Bei 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 Instantiierung 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 Instantiierung kann man den gewünschten Typ angeben und es wird eine Typumwandlung durchgeführt. Die explizite Instantiierung 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 Instantiierung notwendig! foo<int>();
Für Template-Argumente gibt es wiederum einige Einschränkungen, diese gelten aber nur für built-ins:
1. Ist der Parameter ein Zeiger, so dürfen nur Adressen mit globalem Geltungsbereich übergeben werden
2. Ist der Parameter eine Referenz, so dürfen nur Objekte mit globalem oder statischem Geltungsbereich übergeben werden
3. Ist der Parameter weder Referenz, noch Zeiger, so dürfen nur konstante Werte übergeben werden######################################################################
4. Überladen (Spezialisierung) von Funktions-Templates
######################################################################Manchmal 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 EXIT_SUCCESS; };
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:1. Findet der Compiler eine normale Funktion, die er ohne Typumwandlung aufrufen kann, so wird diese aufgerufen
2. Bei einem oder mehreren, unterschiedlich spezialisierten Templates, wählt der Compiler stets das Template bzw. das spezialisierteste aus
3. Konnte keine passende Funktion gefunden werden, so werden normale Funktionen überprüft, bei denen Typumwandlungen zum Erfolg führenWird keine oder mehrere passende Funktionen gefunden, ist dies ein Fehler.
Laut ANSI-Standard führt dieser 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.
######################################################################
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
######################################################################Manchmal 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; //Instanziierung des Templates return EXIT_SUCCESS; };
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:
1. Ein Klassen-Template kann sowohl vollständig, als auch partiell spezialisiert werden
2. Eine Member-Methode eines Klassen-Templates kann nur vollständig spezialisiert werden
3. Eine Funktion auf namespace Ebene kann nicht partiell spezialisiert werden, wobei die Überladung (siehe oben), als eine Art Ersatz angesehen werden kann.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.
######################################################################
6. Das Schlüsselwort export
######################################################################Dieser Abschnitt wurde von 7H3 N4C3R beigesteuert, an dieser Stelle ein Dankeschön von mir.
Bei 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, und die Implementation in eine .cpp Datei schreiben.
Die Motivation dahinter ist, dass jede Änderung an einer Headerdatei dazu führt, dass all der Code neu kompiliert werden muss, der diese Headerdatei benutzt. Gerade in Projekten mit vielen Dateien löst das große Neu-Kompilier-Wellen aus, die ziemlich lange dauern können.Leider geht das bei Templates so nicht. Das liegt daran, dass das Template erst überall dort in den Code eingesetzt wird, wo es auch verwendet wird. Vorher ist es "nur ein Stück Text", erst beim Einsetzen bekommt es seine Bedeutung. Genau das verursacht aber das oben beschriebene Verhalten vom Neu-Kompilieren.
Der eine oder andere hat nun aber vielleicht schon vom Schlüsselwort export gehört, welches nun doch genau diese Trennung ermöglichen soll. In die Headerdatei schreibt man vor das template einfach nur das Wörtchen export, und schon kann man eine .cpp-Datei mit der Implementation füllen.
Stopp. Soweit so gut, das war die Theorie. In der Praxis steht es um export aber völlig anders. Zuerst: Kaum ein Compiler unterstützt es überhaupt. Lediglich die Front-Ends, die auf den EDG-Compiler (Edison Design Group) aufsetzen, beherrschen es. Das ist im Wesentlichen der Comeau-Compiler. Das und die Tatsache, dass die Entwicklungszeit für dieses Compilerfeature drei Mannjahre betrug, sollte einen stutzig machen (eine mittelgroße bis große Individualanwendung hat ca. zwei Mannjahre Entwicklungszeit). Was stimmt also mit export nicht?
Im Wesentlichen kann export nicht das halten, was man sich von der Trennung in .hpp und .cpp Datei verspricht.
Man könnte denken, dass man bei einer Auslieferung einer selbst geschriebenen Bibliothek nur die .hpp Datei mitgegeben werden muss und die Implementierung in der .cpp Datei versteckt bleibt (als kompilierte .o Datei zur Bibliothek dazugelinkt). Das ist nicht so. Die kurze Antwort ist, dass der Standard verlangt, dass das Template bei seiner Instanziierung vollständig (inklusive Implementation im Quellcode-Format) bekannt ist.
Auch ein anderer scheinbarer Vorteil ist nicht gegeben. Nämlich dass durch das Auslagern der Implementation nicht mehr soviel Quellcode neu übersetzt werden muss. Das ist zwar so schon richtig, dafür muss aber die .cpp Datei des Templates für jeden Datentyp, für den das Template instanziiert wird, übersetzt werden. Die Abhängigkeiten, die durch das Verlagern der Implementation in die .cpp Datei verschwinden, schlagen hinterrücks wieder zu. Denn sie sind nur versteckt, aber nicht aus der Welt.
Andere Nachteile von export sind in der Regel höhere Compilezeiten, auch wenn sie in der Theorie eigentlich sinken sollten. Auerdem haben EDG-basierte Compiler nur eine mögliche Implementierung von export. Das liegt daran, dass der Standard dieses Schlüsselwort nicht genau genug beschreibt. Es wäre mglich, dass andere Compilerhersteller export nach der Beschreibung im Standard korrekt implementieren, es sich aber überall unterschiedlich verhält und unterschiedlichen Code produziert.
Der wohl schlimmste Nachteil von export (dessen Erklärung hier im Detail wohl zu weit führt) ist, dass es die Bedeutung von definierten Sprachfeatures gefährlich verändert. Im Endeffekt muss man höllisch aufpassen, um mit export den selben Code zu schreiben, wie ohne.
Deshalb das Fazit (zumindest für die nächste Zeit) : Finger weg von export.
Es ist jedoch möglich, diese Einschränkung mit einem Trick zu umgehen: Wir inkludieren einfach eine .impl (normale Source-Datei mit .impl Endung) Datei 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.
######################################################################
7. 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 "Modernes C++ Design" von Andrei Alexandrescu. "Gehobenes Niveau", aber sehr lesenswert.
-
evilissimo schrieb:
Das Problematische an VC6 ist das er mit Templates nicht wirklich gut arbeiten kann.
Bei normalen Templatesachen ist das kein Problem. Aber bei krasseren Templategeschichten wie z.b.template < template class < typename A , B , template class < typename D > C > A > class StrangeStuff{};
kann der VC6 ganz schön ins Schleudern kommen.
Deswegen brauchte der VC6 für Loki auch ein Workaround ( Das hat btw Hume geschrieben )Bemerkbar kann sich das durch Compilerfehler oder gar Compilersegfaults (Compiler kackt einfach ab ) machen.
Selbst g++ ist davor nicht gefeit. Es gibt einige Sachen die der Compiler einfach nicht verträgt. Gerade SFINAE (Substantiation failure is not an error ) ist da sehr tödlich für einige Compiler.
Aber VC6 ist eben etwas altersschwach und noch nicht so fit wie die aktuelleren und gerät eben viel schneller aus dem Takt.Die Erklärung ist klasse - kann man die noch irgendwie übernehmen?
GPC schrieb:
evilissimo schrieb:
man bindet eigentlich bei Template klassen nicht .cpp files ein sondern .impl
Nur so als kleiner hinweisBR Vinzenz
Ich persönlich stehe ich nicht auf die .impl Geschichte, aber ich mach's dir zuliebe rein
So ist es auch weniger verwirrend, finde ich. (Also mit impl)
6 Was ist eine hpp Datei? Ist das nur eine andere Endung für eine h Datei oder was ist das?
Genau, so wie's 7H3 N4C3R geschrieben hat.
Ahja, okay. Ich hab das nur mal kurz gelernt und dann nie wieder gesehen.
-
estartu_de schrieb:
evilissimo schrieb:
Das Problematische an VC6 ist das er mit Templates nicht wirklich gut arbeiten kann.
Bei normalen Templatesachen ist das kein Problem. Aber bei krasseren Templategeschichten wie z.b.template < template class < typename A , B , template class < typename D > C > A > class StrangeStuff{};
kann der VC6 ganz schön ins Schleudern kommen.
Deswegen brauchte der VC6 für Loki auch ein Workaround ( Das hat btw Hume geschrieben )Bemerkbar kann sich das durch Compilerfehler oder gar Compilersegfaults (Compiler kackt einfach ab ) machen.
Selbst g++ ist davor nicht gefeit. Es gibt einige Sachen die der Compiler einfach nicht verträgt. Gerade SFINAE (Substantiation failure is not an error ) ist da sehr tödlich für einige Compiler.
Aber VC6 ist eben etwas altersschwach und noch nicht so fit wie die aktuelleren und gerät eben viel schneller aus dem Takt.Die Erklärung ist klasse - kann man die noch irgendwie übernehmen?
Wenn ihr möchtet könnt ihr das gerne anpassen und übernehmen.
Edit: aber dann solltet ihr den code so übernehmen:
template < template < typename A , typename B , template < typename D > class C > class A > class StrangeStuff{};
Weil ich das nicht getestet habe sondern einfach nur hingeschrieben habe.
-
Danke, ich werd's noch einbauen. Kriegst auch noch nen kleinen Credit à la wie man seinen VC++ 6 zur Strecke bringt *g*
-
In diesem Artikel versuche ich, einen kleinen Einblick in die Welt der generischen Programmierung mit Templates in C++ zu geben.
Inhalt:
1. Einführung
1.1 Die Compiler-Frage
2. Definition von Templates
2.1 Funktions-Templates
2.2 Klassen-Templates
3. Übergabe von Argumenten
4. Überladen (Spezialisierung) von Funktions-Templates
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
6. Das Schlüsselwort export
7. Zum Schluss...######################################################################
1. Einführung
######################################################################Wem 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 von std::string, über std::vector bis zu den vielen Algorithmen wie std::copy oder std::find.
######################################################################
1.1 Die Compiler-Frage
######################################################################Der weitverbreitete VC++ 6 Compiler ist leider nicht besonders gut für die (insbesondere fortgeschrittene) Template-Programmierung geeignet, mit Version 7 hat sich zwar viel getan, aber es gibt im Vergleich zum g++ immer noch Defizite. Normale Templates wie Container sind keine Problem, aber bei komplizierteren Deklarationen streikt er ziemlich schnell. Folgende, von evilissimo vorgeschlagene, Template-Klasse wird der VC++ 6 mit vielen Fehlermeldungen quittieren, obwohl der Code korrekt ist (im Extremfall kann der Compiler abstürzen):
template < template < typename A , typename B , template < typename D > class C > class A > struct BreakCompilerBack{};
Im Übrigen ist selbst der sehr gute g++ nicht unverwundbar. Es gibt einige Sachen die der Compiler einfach nicht verträgt. Gerade SFINAE (Substantiation failure is not an error ) ist da sehr tödlich für einige Compiler.
Für diesen Artikel reicht der VC++ 6 Compiler jedoch aus, außer für den Abschitt über partielle Template-Spezialisierung. 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.
######################################################################
2. Definition von Templates
######################################################################Und 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:
1. Sie dürfen nicht verändert werden
2. Sie dürfen nur ganzzahlig seinEs 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:
1. Wenn ein Parameter einen Default-Wert bekommt, so müssen alle nachfolgenden Parameter einen bekommen
2. Wird bei der Instantiierung ein Argument weggelassen, so müssen alle nachfolgenden Argumente weggelassen werden######################################################################
2.1 Funktions-Templates
######################################################################Frü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 EXIT_SUCCESS; };
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 Instantiierung (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 Instantiierung (siehe "Übergabe von Argumenten"): std::cout<<minimum(a,b)<<'\n'; //Gibt a aus return EXIT_SUCCESS; };
Das Funktions-Template minimum hat zwei Parameter vom Typ const-Referenz auf T, und als Rückgabewert ebenfalls eine const-Referenz auf 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 Instanziierung 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 Instantiierung. 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.
######################################################################
2.2 Klassen-Templates
######################################################################Da 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), das 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 EXIT_SUCCESS; };
Bei der ersten Instanziierung 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; };
######################################################################
3. Übergabe von Argumenten
######################################################################Bei 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 Instantiierung 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 Instantiierung kann man den gewünschten Typ angeben und es wird eine Typumwandlung durchgeführt. Die explizite Instantiierung 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 Instantiierung notwendig! foo<int>();
Für Template-Argumente gibt es wiederum einige Einschränkungen, diese gelten aber nur für built-ins:
1. Ist der Parameter ein Zeiger, so dürfen nur Adressen mit globalem Geltungsbereich übergeben werden
2. Ist der Parameter eine Referenz, so dürfen nur Objekte mit globalem oder statischem Geltungsbereich übergeben werden
3. Ist der Parameter weder Referenz, noch Zeiger, so dürfen nur konstante Werte übergeben werden######################################################################
4. Überladen (Spezialisierung) von Funktions-Templates
######################################################################Manchmal 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 EXIT_SUCCESS; };
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:1. Findet der Compiler eine normale Funktion, die er ohne Typumwandlung aufrufen kann, so wird diese aufgerufen
2. Bei einem oder mehreren, unterschiedlich spezialisierten Templates, wählt der Compiler stets das Template bzw. das spezialisierteste aus
3. Konnte keine passende Funktion gefunden werden, so werden normale Funktionen überprüft, bei denen Typumwandlungen zum Erfolg führenWird keine oder mehrere passende Funktionen gefunden, ist dies ein Fehler.
Laut ANSI-Standard führt dieser 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.
######################################################################
5. Vollständige/Partielle Spezialisierung von Klassen-Templates
######################################################################Manchmal 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; //Instanziierung des Templates return EXIT_SUCCESS; };
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:
1. Ein Klassen-Template kann sowohl vollständig, als auch partiell spezialisiert werden
2. Eine Member-Methode eines Klassen-Templates kann nur vollständig spezialisiert werden
3. Eine Funktion auf namespace Ebene kann nicht partiell spezialisiert werden, wobei die Überladung (siehe oben), als eine Art Ersatz angesehen werden kann.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.
######################################################################
6. Das Schlüsselwort export
######################################################################Dieser Abschnitt wurde von 7H3 N4C3R beigesteuert, an dieser Stelle ein Dankeschön von mir.
Bei 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, und die Implementation in eine .cpp Datei schreiben.
Die Motivation dahinter ist, dass jede Änderung an einer Headerdatei dazu führt, dass all der Code neu kompiliert werden muss, der diese Headerdatei benutzt. Gerade in Projekten mit vielen Dateien löst das große Neu-Kompilier-Wellen aus, die ziemlich lange dauern können.Leider geht das bei Templates so nicht. Das liegt daran, dass das Template erst überall dort in den Code eingesetzt wird, wo es auch verwendet wird. Vorher ist es "nur ein Stück Text", erst beim Einsetzen bekommt es seine Bedeutung. Genau das verursacht aber das oben beschriebene Verhalten vom Neu-Kompilieren.
Der eine oder andere hat nun aber vielleicht schon vom Schlüsselwort export gehört, welches nun doch genau diese Trennung ermöglichen soll. In die Headerdatei schreibt man vor das template einfach nur das Wörtchen export, und schon kann man eine .cpp-Datei mit der Implementation füllen.
Stopp. Soweit so gut, das war die Theorie. In der Praxis steht es um export aber völlig anders. Zuerst: Kaum ein Compiler unterstützt es überhaupt. Lediglich die Front-Ends, die auf den EDG-Compiler (Edison Design Group) aufsetzen, beherrschen es. Das ist im Wesentlichen der Comeau-Compiler. Das und die Tatsache, dass die Entwicklungszeit für dieses Compilerfeature drei Mannjahre betrug, sollte einen stutzig machen (eine mittelgroße bis große Individualanwendung hat ca. zwei Mannjahre Entwicklungszeit). Was stimmt also mit export nicht?
Im Wesentlichen kann export nicht das halten, was man sich von der Trennung in .hpp und .cpp Datei verspricht.
Man könnte denken, dass man bei einer Auslieferung einer selbst geschriebenen Bibliothek nur die .hpp Datei mitgegeben werden muss und die Implementierung in der .cpp Datei versteckt bleibt (als kompilierte .o Datei zur Bibliothek dazugelinkt). Das ist nicht so. Die kurze Antwort ist, dass der Standard verlangt, dass das Template bei seiner Instanziierung vollständig (inklusive Implementation im Quellcode-Format) bekannt ist.
Auch ein anderer scheinbarer Vorteil ist nicht gegeben. Nämlich dass durch das Auslagern der Implementation nicht mehr soviel Quellcode neu übersetzt werden muss. Das ist zwar so schon richtig, dafür muss aber die .cpp Datei des Templates für jeden Datentyp, für den das Template instanziiert wird, übersetzt werden. Die Abhängigkeiten, die durch das Verlagern der Implementation in die .cpp Datei verschwinden, schlagen hinterrücks wieder zu. Denn sie sind nur versteckt, aber nicht aus der Welt.
Andere Nachteile von export sind in der Regel höhere Compilezeiten, auch wenn sie in der Theorie eigentlich sinken sollten. Auerdem haben EDG-basierte Compiler nur eine mögliche Implementierung von export. Das liegt daran, dass der Standard dieses Schlüsselwort nicht genau genug beschreibt. Es wäre mglich, dass andere Compilerhersteller export nach der Beschreibung im Standard korrekt implementieren, es sich aber überall unterschiedlich verhält und unterschiedlichen Code produziert.
Der wohl schlimmste Nachteil von export (dessen Erklärung hier im Detail wohl zu weit führt) ist, dass es die Bedeutung von definierten Sprachfeatures gefährlich verändert. Im Endeffekt muss man höllisch aufpassen, um mit export den selben Code zu schreiben, wie ohne.
Deshalb das Fazit (zumindest für die nächste Zeit) : Finger weg von export.
Es ist jedoch möglich, diese Einschränkung mit einem Trick zu umgehen: Wir inkludieren einfach eine .impl (normale Source-Datei mit .impl Endung) Datei 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.
######################################################################
7. 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 "Modernes C++ Design" von Andrei Alexandrescu. "Gehobenes Niveau", aber sehr lesenswert.
-
Welcher Artikel gilt jetzt?
Ich glaube, ich muss die Anleitung nochmal überarbeiten...
Ein Thread pro Artikel bis auf die zu veröffentlichende Version, bitte.Nicht pro "Status" ein neuer.
PS: Poste den anderen bitte nochmal hierdran, ich räume den anderen dann weg.
Tallas Doppelthread sollte eigentlich der einzige Ausrutscher bleiben.
Es ist so einfach übersichtlicher.