Einführung in die Programmierung mit Templates



  • Das stimmt allerdings, wenn man nicht schon genug Vorwissen im Bereich Templates hat, kann einem Modernes C++ Design wirklich Schwierigkeiten machen. Ich werde deine Empfehlung in den Artikel einbauen.



  • Sehr schöne Einführung 👍.

    Vielleicht könnte man Punkt 6 noch ergänzen indem man erwähnt, dass man Spezialisierungen doch in eine gewöhnliche Implementationsdatei schreiben muss, wenn man sie nicht inline oder static deklariert, damit es keine Linkerfehler aufgrund mehrfacher Definitionen gibt.



  • Hallo,

    hagman schrieb:

    Sehr schöne Einführung 👍.

    Vielen Dank 🙂

    Vielleicht könnte man Punkt 6 noch ergänzen indem man erwähnt, dass man Spezialisierungen doch in eine gewöhnliche Implementationsdatei schreiben muss, wenn man sie nicht inline oder static deklariert, damit es keine Linkerfehler aufgrund mehrfacher Definitionen gibt.

    Bei welchem Compiler tritt dieses Problem auf? Bei meinem g++ 3.3.6 kann ich dies bei folgendem Beispiel nicht nachvollziehen:

    //min.hpp
    #ifndef MIN_HPP
    #define MIN_HPP
    
    template <typename T>
    const T& minimum(const T &a, const T &b) {
      return a < b ? a:b;
    };
    
    const char* minimum(const char *str1, const char *str2) {
      return ( (strcmp(str1,str2) < 0 ) ? str2 : str1 );
    };
    
    #endif //MIN_HPP
    
    //main.cpp
    #include <iostream>
    #include "min.hpp"
    
    int main() {
      std::cout<<minimum<int>(5,7)<<'\n'
               <<minimum("hallo","HALLO")<<'\n';
      return EXIT_SUCCESS;
    }
    

    Oder meintest du etwas anderes?

    Mfg

    GPC



  • Und auch erwähnen sollte man, dass partielle spezialisierungen von klassentemplates im selben scope stattfinden müssen.

    ISO 14882:2003 14.5.4p6 schrieb:

    A class template partial specialization may be declared
    or redeclared in any namespace scope in which its
    definition may be defined (14.5.1 and 14.5.2).

    Deshalb geht z.B.

    namespace foo
    {
        template<class T, class U>
        struct s
        {
        }
    }
    
    namespace bar
    {
        template<class T>
        struct foo::s<T, int>
        {
        }
    }
    

    nicht.



  • Genau das meine ich. Hast du min.hpp im selben Programm noch in eine andere Übersetzunseinheit inkludiert? Das hätte ich vielleicht noch erwähnen sollen. Also ich habe dein Beispiel mal unverändert genommen und innerhalb eines Programms in zwei verschiedene Implementierungsdateien inkludiert. Folgende Linkerfehler habe ich erhalten:

    VC 2003 und VC 2005 schrieb:

    Error 1 error LNK2005: "char const * __cdecl minimum(char const *,char const *)" (?minimum@@YAPBDPBD0@Z) already defined in main.obj foo.obj
    Error 2 fatal error LNK1169: one or more multiply defined symbols found E:\test\console\Debug\console.exe

    gcc 4.0.2 schrieb:

    g++ -c main.cpp
    g++ -c min.cpp
    g++ -c foo.cpp
    g++ -o test main.o min.o foo.o
    foo.o: In function minimum(char const*, char const*)': foo.cpp:(.text+0x0): multiple definition ofminimum(char const*, char const*)'
    main.o:main.cpp:(.text+0x0): first defined here
    collect2: ld returned 1 exit status
    make: *** [test] Error 1

    Das liegt wohl daran, dass minimum durch die Spezialisierung zu einer fertig implementierten, "gewöhnlichen" Funktion wird. Deshalb gelten für das spezialisierte minimum die Regeln wie für normale Funktionen auch: Definition in die Implementationsdatei oder als inline oder static spezifizieren.



  • Hallo (sorry für die lange Reaktionszeit),

    hagman schrieb:

    Genau das meine ich. Hast du min.hpp im selben Programm noch in eine andere Übersetzunseinheit inkludiert?
    Das hätte ich vielleicht noch erwähnen sollen.

    Yo, dann haben wir ein Problem mit unserem Freund, dem Linker. Aber das macht nichts, denn anstatt die Spezialisierung static zu machen (oder inline, was manchmal einfach nicht geht), setzen wir sie in einen (anonymen) namespace:

    //min.hpp
    #ifndef MIN_HPP
    #define MIN_HPP
    
    namespace {
    
    template <typename T>
    const T& minimum(const T &a, const T &b) {
      return a < b ? a:b;
    };
    
    const char* minimum(const char *str1, const char *str2) {
      return ( (strcmp(str1,str2) < 0 ) ? str2 : str1 );
    };
    
    };  //namespace
    
    #endif //MIN_HPP
    

    Jetzt alles klar?

    @ness
    Auch deine Anmerkung wird Einzug im Artikel finden, danke dir für den Hinweis.

    Mfg

    GPC



  • naja, aber der anonyme namespace ist doch eine ziemlich "unsaubere" Lösung, finde ich. Entweder du wilst die funktionen inlinen, dann benutz das schlüsselwort, oder eine vollständig spezialisierte funktion gehört in eine implementierungsdatei.



  • Hallo,

    ness schrieb:

    naja, aber der anonyme namespace ist doch eine ziemlich "unsaubere" Lösung, finde ich. Entweder du wilst die funktionen inlinen, dann benutz das schlüsselwort, oder eine vollständig spezialisierte funktion gehört in eine implementierungsdatei.

    das kann man jetzt halten, wie man will. Für gewöhnlich lagere ich die spezialisierte Funktion auch in eine Implementationsdatei aus, oft habe ich aber schon benannte Namensräume, und dann lass ich sie einfach im Header.

    Btw. Was findest du, ist unsauber daran?

    mfg

    GPC



  • naja, weil du dann den code mehrmals (in jeder objektdatei) hast. In dem sinne kannst du fast alles in unbenannte namespace packen.



  • Hi,

    ness schrieb:

    naja, weil du dann den code mehrmals (in jeder objektdatei) hast. In dem sinne kannst du fast alles in unbenannte namespace packen.

    stimmt, das ist bei größeren Funktionen/Klassen ein Argument. Werd mich über's WE mal ransetzen, sofern ich Zeit finde.

    Mfg

    GPC



  • Hi, cooler Artikel 👍 ( nochmal Grundlagen aufgefrischt 😃 )

    GPC schrieb:

    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 sein

    Vielleicht sollte man hier auch noch erwähnen das built-ins als template paramterer als Konstanten betrachtet werden(Bei generischen Klassen).

    Weil sie zur Compilezeit bekannt sind un man sie dadurch als Größenangabe für Array's verwenden kann und sie auch nicht verändert werden können.



  • Hallo,

    Freak_Coder schrieb:

    Hi, cooler Artikel 👍 ( nochmal Grundlagen aufgefrischt 😃 )

    Vielen Dank.

    GPC schrieb:

    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 sein

    Vielleicht sollte man hier auch noch erwähnen das built-ins als template paramterer als Konstanten betrachtet werden(Bei generischen Klassen).

    Dachte eigentlich, das sei klar, nachdem ich erwähnt habe, dass eine Auswertung zur Compilezeit erfolgt.

    Weil sie zur Compilezeit bekannt sind un man sie dadurch als Größenangabe für Array's verwenden kann und sie auch nicht verändert werden können.

    Aber ich pack's noch dazu, damit alle Klarheiten beseitigt sind.

    Mfg

    GPC



  • Guter Artikel 👍

    Allerdings komme ich bei folgendem nicht ganz mit:

    GPC schrieb:

    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.

    Und genau das ist doch auch erwünscht, wenn ich ein Template ändere, oder nicht? Mir ist deswegen nicht ganz klar, warum man Teile der Template-Implementierung (die effektiv Teil des Templates sind) irgendwohin auslagern wollen könnte. Um auf das verwendete Beispiel einzugehen:

    //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;
    };
    //...
    

    "Normalerweise" hätte man die Template-Funktion Stack<>::empty() ebenfalls im Header stack.hpp. Ändere ich nun den, wird jede .cpp-Datei neu kompiliert, die diesen Header eingefügt hat, weil sich ja etwas für den Quellcode interessantes geändert haben könnte. Mit der gezeigten Methode, eine Datei stack.impl zu benutzen, könnte ich stack.impl ändern und (sofern ich mein Build-System davon nicht benachrichtigt habe) mir einen Wolf kompilieren, ohne daß die Änderungen wirksam werden, weil das Änderungsdatum des Headers nicht neuer ist als das der kompilierten Objekte.
    Genauso kann ich aber etwas an stack.hpp ändern und werde trotzdem den Rattenschwanz der stack.impl hinter mir herziehen, weil dann mein Build-System merkt: "Aha, Header neuer als Objekt, Kompilation anstoßen", der Compiler (Präprozessor) wird merken, daß da eine eigenartige stack.impl eingefügt wird und diese korrekterweise in den header einfügen, worauf dann der Compiler wieder den vollständigen Template-Code "sieht" und das Template so oft instanziiert, wie es nunmal nötig ist.
    Vielleicht liegt's nur an mir, aber mit verschließt sich der Sinn dieses Vorgehens (von der Erhöhung der Übersichtlichkeit mal abgesehen, aber es wird ja auf die Compile-Performance eingegangen). 😕



  • tommie-lie schrieb:

    Guter Artikel 👍

    Danke.

    Und genau das ist doch auch erwünscht, wenn ich ein Template ändere, oder nicht?

    Hat sich was geändert, soll es neu kompiliert werden, korrekt.

    "Normalerweise" hätte man die Template-Funktion Stack<>::empty() ebenfalls im Header stack.hpp. Ändere ich nun den, wird jede .cpp-Datei neu kompiliert, die diesen Header eingefügt hat, weil sich ja etwas für den Quellcode interessantes geändert haben könnte. Mit der gezeigten Methode, eine Datei stack.impl zu benutzen, könnte ich stack.impl ändern und (sofern ich mein Build-System davon nicht benachrichtigt habe) mir einen Wolf kompilieren, ohne daß die Änderungen wirksam werden, weil das Änderungsdatum des Headers nicht neuer ist als das der kompilierten Objekte.

    Die Trennung ist in dem Fall auch rein optischer Natur, denn am Ende inkludiere ich ja die cpp datei in die hpp Datei und damit bin ich gleich weit, wie wenn ich alles in die hpp Datei geschrieben hätte. Es ist nur eine Erleichterung der Übersicht.

    Vielleicht liegt's nur an mir, aber mit verschließt sich der Sinn dieses Vorgehens (von der Erhöhung der Übersichtlichkeit mal abgesehen, aber es wird ja auf die Compile-Performance eingegangen). 😕

    Es dient nur der Erhöhung der Übersichtlichkeit... hab ich das nicht auch irgendwo geschrieben? Egal.

    MfG

    GPC



  • GPC schrieb:

    Vielleicht liegt's nur an mir, aber mit verschließt sich der Sinn dieses Vorgehens (von der Erhöhung der Übersichtlichkeit mal abgesehen, aber es wird ja auf die Compile-Performance eingegangen). 😕

    Es dient nur der Erhöhung der Übersichtlichkeit... hab ich das nicht auch irgendwo geschrieben?

    Klang für mich nicht so. In den ersten beiden Absätzen beschwerst du (oder 7H3 N4C3R) dich über die mitunter längeren Kompilierungszyklen, die entstehen, wenn man seine 100 Templates neu durch den Compiler jagt. Ich dachte, daß damit auch Augenmerk auf die Performance gelegt werden soll und die beschriebene Lösung die Performance verbessern soll.
    Aber der letzte Satz des Absatzes sagt ja, daß man dmait die Schnittstelle von der Implementierung trennt, vielleicht hätte ich mehr auf den Satz als auf die ersten beiden Absätze hören sollen 😉
    Aber vielleicht sollte man über eine Warnung nachdenken, daß man sein Build-System irgendwie davon überzeugen sollte, daß stack.impl nun ebenfalls Teil des entsprechenden Moduls ist. Ich denke nicht, daß die GNU autotools da ohne Handarbeit automatisch drauf reagieren.



  • Hallo,

    tommie-lie schrieb:

    GPC schrieb:

    Vielleicht liegt's nur an mir, aber mit verschließt sich der Sinn dieses Vorgehens (von der Erhöhung der Übersichtlichkeit mal abgesehen, aber es wird ja auf die Compile-Performance eingegangen). 😕

    Es dient nur der Erhöhung der Übersichtlichkeit... hab ich das nicht auch irgendwo geschrieben?

    Klang für mich nicht so. In den ersten beiden Absätzen beschwerst du (oder 7H3 N4C3R) dich über die mitunter längeren Kompilierungszyklen, die entstehen, wenn man seine 100 Templates neu durch den Compiler jagt. Ich dachte, daß damit auch Augenmerk auf die Performance gelegt werden soll und die beschriebene Lösung die Performance verbessern soll.

    schön wär's. Aber leider hat das performancetechnisch keine Auswirkungen.

    Aber der letzte Satz des Absatzes sagt ja, daß man dmait die Schnittstelle von der Implementierung trennt, vielleicht hätte ich mehr auf den Satz als auf die ersten beiden Absätze hören sollen 😉

    kein Thema.

    Aber vielleicht sollte man über eine Warnung nachdenken, daß man sein Build-System irgendwie davon überzeugen sollte, daß stack.impl nun ebenfalls Teil des entsprechenden Moduls ist. Ich denke nicht, daß die GNU autotools da ohne Handarbeit automatisch drauf reagieren.

    äh, nein. Das tun sie nicht. denn die impl Datei ist ja nicht "direkt" am Build-Prozess beteiligt. Du siehst, die impl-Lösung zieht nen Rattenschwanz von Problemen nach sich. Ich pers. verzichte auf diese Möglichkeit und knall alles direkt in den Header.

    MfG

    GPC



  • GPC schrieb:

    Du siehst, die impl-Lösung zieht nen Rattenschwanz von Problemen nach sich.

    Ich weiß 😉

    GPC schrieb:

    Ich pers. verzichte auf diese Möglichkeit und knall alles direkt in den Header.

    Ich auch 😉



  • haha, okay, dann sind wir uns ja einig... 😉



  • GPC schrieb:

    haha, okay, dann sind wir uns ja einig... 😉

    Jupp. Ich bin aufgrund des ersten Absatzes nur mit ganz anderen Vorstellung an diesen Abschnitt des Tutorials herangegangen und dachte, du würdest ernsthaft denken, daß dieses Verfahren die Performance verbessert. Da dem nicht so ist (beides, es verbessert du Performance nicht und du denkst auch nicht, daß es das tun würde), bleibt die Diskussion nur eine Anmerkung für alle weiteren Leser.



  • Kann ich eigentlich in einem template Ausnahmebehandlung für bestimmte Typen einbauen. Also sowas in der Art

    template<typename T>
    void f()
    {
      if (typename(T)==TypeInt)
         T++;
      foo(T);
    }
    

    danke


Anmelden zum Antworten