.h .cpp inline namespace => best practice?



  • Hallo zusammen,

    ich hätte mal eine (banale) Frage, wie die Aufteilung des Codes am besten vorgenommen wird. Beispiel:

    //foo.h
    namespace foos
    {
        class foo
        {
                int i;
            public:
                foo(int i):i(i){}; //ctor 1. Möglichkeit
                int get() { return i; }; //get() 1. Möglichkeit
        }
    
        inline foo::foo(int i):i(i){}; //ctor 2. Möglichkeit (innerhalb namespace)
    
        inline int foo::get() { return i; } //get() 2. Möglichkeit (innerhalb namespace)
    }
    inline foos::foo::foo(int i):i(i){}; //ctor 3. Möglichkeit (außerhalb namespace)
    
    inline int foos::foo::get() { return i; } //get() 3. Möglichkeit (außerhalb namespace)
    
    //foo.cpp
    foos::foo::foo(int i):i(i){}; //ctor 4. Möglichkeit
    
    int foos::foo::get() { return i; } //get() 4. Möglichkeit
    

    In der Regel verwende ich die 4. Variante und ab und zu die Variante 2 (3), wenn es sich wie im Beispiel nur um ein "return" handelt.

    Bei 2 und 3 bin ich mir nicht sicher, ob es einen Unterschied macht.

    Noch ein weitere Frage, die evtl. auch damit zu tun hat: Manche im inet zu findende "Frameworks" werben damit, dass sie "header only" sind. Das bedeutet doch, dass der komplette Code im Headerfile steht, oder? Was bringt das für einen Vorteil?

    Danke,
    temi



  • Geschmackssache. Ich persönlich verwende fast ausschiesslich Variante 2 und 4, allerdings ein wenig anders:

    //foo.h
    namespace foos
    {
        class foo
        {
            ...
        }
    }
    
    #include "foo.inl"
    
    // foo.inl
    namespace foos
    {
        inline foo::foo(int i):i(i){};
        inline int foo::get() { return i; }
    }
    

    Ich trenne gerne auch bei Inline/Template-Funktionen Interface und Implementation in separate Dateien.
    Die Header-Datei ist für mich auch eine Art Dokumentation, die ich nur ungerne zu sehr zumülle.

    Die .cpp sähe bei mir so aus:

    //foo.cpp
    namespace foos
    {
        foo::foo(int i):i(i){};
        int foo::get() { return i; }
    }
    

    Besonders bei verschachtelten Namespaces macht es Sinn auch in der .cpp -Datei den Namespace aufzuspannen,
    sonst findet man irgendwann den Funktionsnamen vor lauter Bäumen nicht mehr.

    temi schrieb:

    Noch ein weitere Frage, die evtl. auch damit zu tun hat: Manche im inet zu findende "Frameworks" werben damit, dass sie "header only" sind. Das bedeutet doch, dass der komplette Code im Headerfile steht, oder? Was bringt das für einen Vorteil?

    Hauptsächlich weniger Ärger beim kompilieren und einbinden. Zusätzlich kann der Code im Header so auch ohne Link-Time-Optimization (LTO) im Kontext der
    Übersetzungseinheit optimiert werden, welche den Header einbindet, und nicht nur im Kontext der Bibliothek. Beispielsweise kann der Compiler eine
    Bibliotheksfunktion in deinem Programm inlinen, wenn das für die Art und Wiese, wie du sie verwendest, Sinn macht. Bei gelinkten Bibliotheken geht das ohne
    LTO eigentlich nicht. Das kann übrigens auch ein Argument dafür sein das Schlüsselwort inline exzessiver zu nutzen, als es oft empfohlen wird.
    Ich persönlich mache das allerdings nur bei sehr kurzen Funktionen, für alles andere vertraue ich da auf LTO/PGO.

    Finnegan



  • @finnegan: Danke für die Erläuterungen.

    "Geschmacksache" würde ja bedeuten, dass es keinen Unterschied gibt, egal wie man es macht. Ist das tatsächlich so?

    Die Varianten 2 und 3 sind vermutlich tatsächlich identisch, für 3 ist es noch zusätzlich notwendig den namespace mit anzugeben, was für Variante 2 spricht.

    Aus der Antwort zu "header only" entnehme ich, dass die signifikanten Unterschiede, auch für die Aufteilung in .h und .cpp, wohl beim Compilieren und Linken zu finden sind. Das ist der Fluch einer IDE, da klickt man einfach auf den grünen Pfeil und das Programm startet ohne dass man sich um irgendetwas kümmern muss. Gibt es eine für Anfänger verständliche Erklärung, was und wie das abläuft?



  • temi schrieb:

    @finnegan: Danke für die Erläuterungen.

    "Geschmacksache" würde ja bedeuten, dass es keinen Unterschied gibt, egal wie man es macht. Ist das tatsächlich so?

    das eine ist mehr tipparbeit als das andere. mehr tipparbeit heißt auch, mehr möglichkeiten, fehler zu machen.

    Gibt es eine für Anfänger verständliche Erklärung, was und wie das abläuft?

    jede .cpp-datei wird vom preprozessor zu einer "übersetzungseinheit" gemacht. der preprozessor ist eine reine textersetzungsmaschine: überall, wo er #include sieht, kopiert er einfach den inhalt der datei herein. der preprozessor kümmert sich auch um das auflösen von makros (alles mit #define , #ifdef , etc).

    jede übersetzungseinheit wird zu einer objektdatei kompiliert und muss "für sich" "korrekt" sein, u.a. heißt das: alle namen müssen korrekt deklariert sein, bevor sie verwendet werden - daher kommt das mit dem #include : die inkludierten dateien sollten so gut es geht nur deklarationen beinhalten.

    die kompilierten übersettzungseinheiten werden vom linker zu einer ausführbaren datei "zusammengebunden". an dieser stelle setzt die one-definition-rule an, die im prinzip besagt, das alles, was du verwendest (variablen, funktionen, klassen, etc.) nur einmal definiert - d.h.nicht in jeder übersetzungseinheit - definiert sein dürfen, außer sie sind inline , dann müssen sie in jeder verwendeten übersetzungseinheit (in gleicher form) definiert sein.

    der unterschied zwischen deklaration und definition ist eine der wesentlichen dinge, mit denen man sich als C++-einsteiger bald beschäftigen sollte.
    siehe folgende FAQ-einträge aus diesem themenkreis:
    Unterschied: Deklaration, Definition, Initialisierung
    inline-FAQ
    #include
    Projekt mit vielen Dateien
    undefined reference to XXYY

    in wahrheit ist das alles noch sehr viel komplizierter, aber für einen einsteiger reicht das zum verstehen des prozesses aus.



  • Es gibt noch Möglichkeit 5, die ich z.B. benutze:

    .h Datei

    namespace foo {
    
    class Bar
    {
       int Value_;
    public:
       Bar( int v );
    
       int   value() const { return Value_; }
       void  set_value( int v ) { Value_ = v; }
    };
    
    } // namespace foo
    

    .cpp Datei

    #include "bar.h"
    
    namespace foo {
    
    Bar::Bar( int v ) :
       Value_( v )
    {
    }
    
    } // namespace foo
    

    Getter/Setter, die sich als kurzer Einzeiler schreiben lassen, implementiere ich im Header, der Rest wandert in die .cpp Datei.

    Edit:
    Lese grad, dass Finnegan das schon vorgeschlagen hat. Naja, dann hab ich´s wenigstens nummeriert 😉



  • Bei kleinen Klassen (=wenige Member/Memberfunktionen) wo alles ins .h File kommt, schreibe ich alles in die Klassendefinition (1).
    Bei kleinen Klassen wo nur ein Teil ins .h File kommt, verwende ich für die Teile die ins .h File kommen auch oft (1).

    Bei grösseren Klassen tendiere ich eher zu (2) falls ich Teile ins .h File schreiben möchte. (3) verwende ich nie (bin mir jetzt ohne nachzugucken nichtmal sicher ob das überhaupt standardkonform ist).

    Was 1-2-3 vs. 4 angeht, also welche Teile mit ins Header File kommen und welche nicht...
    Bei Projekten die keine "whole program optimization"/"link time code generation" verwenden (warum auch immer), schreibe ich das was ich für gute und vor allem sinnvolle Inlining-Kandidaten halte ins Header File (und den Rest ins .cpp File).

    Bei Projekten die "whole program optimization"/"link time code generation" verwenden schreibe ich meistens gar nichts ins Header File. Ausser natürlich Templates und ggf. super-triviale Klassen die super klein sind und auch nur super-kurze Funktionen enthalten.

    temi schrieb:

    Noch ein weitere Frage, die evtl. auch damit zu tun hat: Manche im inet zu findende "Frameworks" werben damit, dass sie "header only" sind. Das bedeutet doch, dass der komplette Code im Headerfile steht, oder? Was bringt das für einen Vorteil?

    Der wohl grösste Vorteil ist dass man solche Libraries super-einfach einbinden kann. Einfach die Header-Files irgendwo hinkopieren, evtl. noch Include-Pfad setzen wenn nötig - fertig. Kein zusätzliches Make-Target nötig, kein rummachen mit weiteren .lib Files etc.

    Ein weiterer potentieller Vorteil (der aber auch ein Nachteil sein kann): Der Compiler kann alles inlinen wenn er will, auch ohne "whole program optimization"/"link time code generation".



  • Vielen Dank an alle, die hier geantwortet haben, das war sehr aufschlussreich.

    Ich werde jetzt bei (2) und (4), bzw. (5) bleiben. In dem GotW-Beitrag (Link von dove) wird ja sinngemäß sogar empfohlen grundsätzlich jeden inline-Code zu vermeiden (bis durch Profiling (?) erforderliche Stellen gefunden sind). Damit bleiben letztlich ja nur (4) und (5) übrig.

    Gruß,
    temi



  • temi schrieb:

    VIn dem GotW-Beitrag (Link von dove) wird ja sinngemäß sogar empfohlen grundsätzlich jeden inline-Code zu vermeiden (bis durch Profiling (?) erforderliche Stellen gefunden sind).

    Hier möchte ich dem "Guru" nochmal widersprechen, da - bis auf die evtl. längeren Compile-Zeiten und das "Coupling" - alle Argumente darauf aufbauen, dass auch tatsächlich ge-" inline d" wird.
    Diese Entscheidung liegt jedoch ausschiesslich beim Compiler (sofern nicht compiler-spezifische Attribute wie __forceinline oder __attribute__((always_inline) ) verwendet wurden) - und dieser
    trifft diesbezüglich meistens ganz gute Entscheidungen. Wenn man jetzt mal LTO (von hustbear als "whole program optimization"/"link time code generation" bezeichnet) außer acht lässt, könnte
    man sogar argumentieren, dass eine Funktion nicht inline zu machen schädlich sein kann, da das bei modulübergreifenden Funktionen nicht selten auf ein implizites " always_noinline " hinausläuft.

    Worauf ich jedoch eigentlich hinaus will: In meinen Augen schaden inline -Funktionen zumindest nicht. Man darf also ruhig aus dem Bauch heraus entscheiden ob eine Funktion ein inline -Kandidat
    sein könnte. Wenn die Entscheidung schlecht war, wird sie der Compiler ohnehin revidieren, und der weiss es schon aus dem Grund besser, weil er die Zielarchitektur genau kennt und auch weiss wie
    letztendlichlich der Maschinencode und die verfügbaren Register um den Funktionsaufruf herum aussehen.

    Es verbleiben also lediglich eventuell längere Compile-Zeiten und mögliche Probleme, wenn man das Programm mit einer anderen Bibliotheks-Version lediglich neu linkt (ohne es neu zu kompilieren),
    da sich in der neuen Bibliothek die inline -Funktionen auf inkompatible Weise geändert haben könnten, in der Objektdatei deines Programms noch die alten Versionen "eingebacken" sind.

    Finnegan

    P.S. (Off Topic): Noch eine kleine Kritik an der "always profile first"-Philosophie: Prinzipiell ein guter Leitsatz, den man jedoch nicht zu dogmatisch verfolgen sollte. Manchmal summieren sich viele kleine
    Ineffiziezen in Programmen derart auf, dass das ganze Programm gleichförmig "irgendwie zu langsam" ist, ohne dass man das mit einem Profiler (die trotz allem eine relativ grobe "Auflösung"
    haben) auf einen bestimmten heißen Codepfad festnageln könnte. Sicherlich sollte man in einem frühen Entwicklungsstadium keine sehr spezifischen Optimierungen machen, aber es schadet
    auch nicht den Code direkt so zu formulieren, dass Compiler und CPU einen leichteren Job haben. Das können z.B. inline -Getter/Setter sein, oder solche Sachen wie einen Algorithmus auf
    einem Array so zu formulieren, dass möglichst linear auf die Elemente zugregriffen wird (statt random access, siehe CPU-Cache) - oder aber auch die verwendeten Datenstruktiuren zu kennen,
    und dem Rechner keine unnötige Arbeit aufzubürden:

    // Anstatt...
    std::map<int, int> map;
    mach_was(map[5132]);
    mach_was_anderes(map[5132]);
    mach_nochwas_anderes(map[5132])
    // ... einfach nur einmal den O(log(n))-Algorithmus ausführen, der ein map-Element findet:
    auto& element = map[5132];
    mach_was(element);
    mach_was_anderes(element);
    mach_nochwas_anderes(element);
    

    Das sind alles Dinge, die zwar auch gewissermassen "Opimierungen" sind, welche man aber meines Erachtens dennoch tun können sollte, ohne gleich dafür der "Premature Optimization" bezichtigt
    zu werden - ohne solche konsequenten kleinen Früh-Optimierungen wage ich es zu prophezeien, dass man es in der späteren Profiling/Optimierungs-Phase des Projekts schwer haben wird bezüglich
    Gesamt-Perfomance eine gewisse "Grund-Lahmheit" rauszubekommen 😃

    P.P.S: Das heisst natürlich nicht dass ich frühen "Optimierungs-Voodoo", wie manuelles Loop-Unrolling oder allzu arge Verrenkungen, um zusätzliche Kopien zu vermeiden gutheisse - nur "natürliche"
    Optimierungen, die den Code nicht wirklich komplexer machen und keine spezifischen Annahmen über Compiler und Zielsystem machen.



  • Ergänzend zu Finnegans Empfehlung hier ein Talk von Chandler Carruth:
    CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures"


Anmelden zum Antworten