Richtige Verwendung von member function templates



  • Hallo, wenn ich die test() Funktion aufrufe, gibt es folgenden Error:

    undefined reference to `ResourceHolder<X, Y>::test()
    

    Der Source Code:

    ResourceHolder.h:
    template <typename T, typename S>
    class ResourceHolder
    {
    public:
        void test();
    };
    
    ResourceHolder.cpp:
    template <typename T, typename S>
    void ResourceHolder<T,  S>::test()
    {}
    
    Implementation:
    ResourceHolder<X, Y> resourceHolder;
    ... 
    : resourceHolder()
    ...
    resourceHolder.test();
    

    Liegt der Fehler im Konzept oder der Implementierung?
    Danke für eure Hilfe



  • Ein Template muss zum Zeitpunkt der Instanziierung komplett bekannt sein. Eine Definition in einer anderen translation unit (Dein ResourceHolder.cpp) gilt nicht.

    ResourceHolder.h:

    #ifndef RESOURCEHOLDER_H_INCLUDED
    #define RESOURCEHOLDER_H_INCLUDED
    
    template <typename T, typename S>
    class ResourceHolder
    {
    public:
        void test()
        {
            // whatever.
        }
    };
    
    #endif /* RESOURCEHOLDER_H_INCLUDED */
    

    [temp.expl.spec]/7



  • @Swordfish Ah vielen Dank; gibt es nicht die möglichkeit, dann eine .inl file zu nutzen? Wie sähe das aus?



  • Du schreibst die Definition der Funktion in eine Datei die Du sonstwie nennst und inkludierst sie im Header. Ob man das machen soll ist eine Glaubensfrage.



  • @Swordfish Ah Danke, spricht etwas dagegen?



  • @daniel sagte in Richtige Verwendung von member function templates:

    @Swordfish Ah Danke, spricht etwas dagegen?

    Es hat den Vorteil, dass man auch bei Templates Interface von Implemetierung sauber in separaten Dateien trennt. Außerdem werden die einzelnen Dateien kleiner und übersichtlicher.

    Nachteil ist, dass es insgesamt mehr zu schreiben und auch für den Compiler etwas mehr zu parsen ist, als wenn man die Template-Funktionen direkt in der Klassendeklaration implementiert.

    Meiner Meinung nach überwiegen jedoch die Vorteile. Ich mache das z.B. auch so.

    Fun Fact am Rande: Eigentlich kennt der Compiler gar keine "Header", "Source", .inl-Files oder wasauchimmer. Für den ist das alles einerlei - Dateien, in denen C++-Code steht (auch wenn es de facto wohl dennoch einige Logik im Compiler gibt, den Verwendungskontext dieser Dateien auseinanderzuhalten) 😉 .



  • @Finnegan sagte in Richtige Verwendung von member function templates:

    auch wenn es de facto wohl dennoch einige Logik im Compiler gibt

    Nitpicking: Präprozessor. *scnr*

    @Finnegan sagte in Richtige Verwendung von member function templates:

    Meiner Meinung nach überwiegen jedoch die Vorteile. Ich mache das z.B. auch so.

    @Swordfish sagte in Richtige Verwendung von member function templates:

    Ob man das machen soll ist eine Glaubensfrage.



  • @Swordfish sagte in Richtige Verwendung von member function templates:

    @Finnegan sagte in Richtige Verwendung von member function templates:

    auch wenn es de facto wohl dennoch einige Logik im Compiler gibt

    Nitpicking: Präprozessor. *scnr*

    Simple Copy-Paste-Maschine, die da auch keinen Unterschied macht. Jede Präprozessor-Direktive kann in jedem Datei-"Typ" vorkommen und es hindert mich nichts daran, meine nicht-inline main()-Implementierung per #include hereinzuholen. Außer vielleicht die Liebe zu meinem Job 😝

    Ob man das machen soll ist eine Glaubensfrage.

    Genau.



  • Wenn ich .cpp zu .inl ändere, die Datei am Ende des Headers inkludiere und die Makefile anpasse, gibt es folgenden Error:

    g++: warning: ResourceHolder.inl: linker input file unused because linking not done
    g++: error: ResourceHolder.o: No such file or directory
    

    in der Makefile:

    ResourceHolder.o: ResourceHolder.inl
    	g++ -c ResourceHolder.inl
    

    Braucht es für .inl eine spezielle flag oder was ist das Problem?



  • @daniel dann erkläre mal, wozu dieser Eintrag im Makefile gut sein soll.



  • @daniel Das braucht nicht ins makefile. Das .inl-Dingsti wird compilifikazioniert weil Du es in einem Header eingebunden hast den Du wiederum in irgendeinem .cpp-File eingebunden hast. Vielleicht fürs Verständnis hilfreich: Code entsteht aus einem Template erst wenn es irgendwo instanziiert wird.
    @Finnegan Ich hab' Dich auch lieb.



  • @Swordfish Ah Danke ja jetzt funktioniert's



  • Noch eine Anmerkung am Rande: Ein weiterer Vorteil der Trennung in .hpp/.inl ist mir gerade erst wieder beim Programmieren aufgefallen (eher fortgeschrittene Problematik):

    Folgendes Beispielszenario mit statischer Vererbung via CRTP ist normalerweise nicht immer ganz einfach sauber hinzubekommen. Bei getrennten .hpp/.inl löst es sich jedoch nahezu ganz von alleine in Wohlgefallen auf:

    // MatrixExpression.hpp
    #pragma once
    ...
    template <typename E>
    struct MatrixExpression
    {
        // Funktion soll ein Column (aus Column.hpp) zurückgeben. Bei dieser
        // reinen Deklaration wird zwar noch kein vollständiger Typ und daher
        // auch kein #include benötigt, das auto spart hier allerdings eine 
        // Forward Declaration.
        auto column(std::size_t j) const;
    };
    ...
    #include <MatrixExpression.inl>
    
    // Column.hpp
    #pragma once
    ...
    // Column ist zu diesem Zeitpunkt noch nicht deklariert. Wenn nun
    // MatrixExpression jedoch einen vollständigen Column-Typen benötigt,
    // wie z.B. in der Definition der Member-Funktion column(), wird es
    // hier knallen.
    #include "MatrixExpression.hpp"
    ...
    // Column erbt von MatrixExpression, es wird daher der vollständige Typ
    // benötigt und man kommt um das #include oben nicht herum.
    template <typename E>
    class Column : public MatrixExpression<Column<E>>
    {
        ...
    };
    
    // MatrixExpression.inl
    #pragma once
    ...
    // Hier zeigt sich der praktische Effekt dieser Trennung: Normalerweise
    // würde Column.hpp auch MatrixExpression.hpp inklusive der nachfolgenden
    // Funktion column() einbinden, *bevor* Column überhaupt deklariert wurde,
    // und somit zu einer Compiler-Fehlermeldung führen.
    //
    // Stattdessen passiert aber nun folgendes:
    //
    // 1. User-Code bindet MatrixExpression.hpp ein. 
    // 2. MatrixExpression.hpp deklariert MatrixExpression und bindet 
    //    MatrixExpression.inl ein.
    // 3. MatrixExpression.inl bindet "Column.hpp" ein.
    // 4. Column.hpp versucht, MatrixExpression.hpp einzubinden, was aber
    //    ignoriert wird, da Include-Guard greift. Column läuft aber dennoch 
    //    nicht auf Fehler, da MatrixExpression bereits in Schritt 2 deklariert
    //    wurde. Column ist nach diesem Schritt vollständig deklariert.
    // 5. MatrixExpression.inl definiert column(), welches letztendlich das
    //    vollständige Column benötigt, was wegen Schritt 4 dann auch
    //    fehlerfrei kompiliert.
    #include "Column.hpp"
    ...
    // Column muss hier ein vollständiger Typ sein, da ein Objekt diesen Typs
    // konstruiert wird.
    template <typename E>
    auto MatrixExpression<E>::column(std::size_t j) const
    {
        return Column{ static_cast<const E&>(*this), j };
    }
    

    Heh... eigentlich wollte ich nicht so viel Text produzieren. Dachte mir nur gerade "Hey, das Thema hatten wir doch gestern" und wollte eigentlich nur einen kurzen Kommentar dazu absetzen. Sorry, bei manchen Dingen tue ich mich extrem schwer mit kurzen und bündigen Beschreibungen 🙄


Log in to reply