Wie sollte man beim Programmieren mit C++ den Code aufsplitten?

Peter_2498

Lieutenant
Registriert
Apr. 2018
Beiträge
583
Es geht mir hier vor allem um die "sinnvolle" Aufteilung von Code und nicht so sehr innerhalb der Datei sondern in Form von Auslagerungen von Funktionen und Klassen in Header-Dateien für die Deklaration und CPP-Dateien für die Implementation.

Ich weiß jetzt nicht, ob das Erfahrungssache ist wann man was auslagert und wann nicht. Hab schon bisschen was dazu gelesen und ja, hier und da erweckt das schon den Anschein, als ob man das fast schon dogmatisch für jede Funktion und jede Klasse machen sollte.

So weit ich richtig gelesen habe, sind allerdings manche Klassen aus der Standardbibliothek von C++ wohl einfach samt Implementation komplett in eine Header geschrieben(das habe ich früher auch so gemacht...).

Gibt es da eine Art ungeschriebene Regel oder sowas?
 
Die Standard Bibliothek besteht zum größten Teil aus Templates und ist deshalb in Headern implementiert. Für gewöhnlich würde ich die Aufteilung in Header (Deklarationen) und Sourcen (Implementierung) empfehlen, einteilige Funktionen kann man auch im Headern, eventuell als Inline deklariert, implementieren. Keep it simple keep it small.
 
Ja, das Auslagern in die CPP-Dateien ist sinnvoll und fast immer die beste Wahl. Ein wichtiger Grund ist die Arbeitsweise des Compilers. Der verarbeitet "Compilation-Units" und erzeugt aus jeder eine Objektdatei. Diese werden beim Linken zum gewünschten Resultat (Bibliothek oder ausführbares Programm) zusammengesetzt.
Wäre sämtlicher Code im Header-Dateien, müsste bei jeder Codeänderung der gesamte Code neu kompiliert und natürlich auch gelinkt werden muss. Effektiv kopiert der Präprozessor nur die Inhalte der Header an die Stelle der jeweiligen include-Anweisung. Das mag bei kleinen Projekten noch vertretbar sein, eskaliert aber schnell ins Unerträgliche.
Beim "Auslagern" entstehen mehrere unabhängige Compilation-Units. Bei einer Änderung im jeweiligen CPP muss nur dieser Teil neu kompiliert werden und mit dem Rest verlinkt werden.

Bei Templates sieht die Sache etwas anders aus. In diesem Fall muss der Compiler die komplette Definition (des Templates) kennen, um den korrekten Code an der Stelle des Aufrufers zu erzeugen. Die reine Deklaration in Header reicht nicht.
 
Zuletzt bearbeitet:
Öhm, seit C++20 bietet C++ Module als Nachfolger an.
Warum sollte man sich jetzt noch mit Headers rumschlagen wollen?
 
Wenn man was neu anfängt, dann kann man sicherlich auch den vollen Umfang von C++20 verwenden. Meistens schlägt man sicher aber mit älteren Projekten herum. Bei Einsatz von Templates wird der gesamter Code generiert, wobei die Typplatzhalter durch den realen Datentyp ersetzt werden.
 
Danke erstmal für die hilfreichen Tipps.

Also ich habe das mit den .h und .cpp Dateien bisher folgendermaßen verstanden:

Erstmal werden Header-Dateien im Gegensatz zu CPP-Dateien nicht kompiliert. An der Stelle wo ein #include Makro für eine Header-Datei steht, wird VOR der Kompilierung der entsprechenden CPP-Datei einfach der Code aus dieser Header-Datei reinkopiert. Das passiert so bei jeder CPP-Datei und dann werden die CPP-Dateien einzeln kompiliert.

So jetzt kommt ja der Linker ins Spiel, der versucht sozusagen alle "Unbekannten"(Reine Deklarationen, seien es Funktionen, Klassen oder Objekte) aus den CPP-Dateien "aufzulösen"(ich glaube man nannte das so). Sofern ich das richtig verstanden habe, durchsucht der Linker jetzt sinngemäß bei einer Deklaration wohl jede CPP-Datei (oder .obj Datei?) nach einer passenden Definition (wahrscheinlich aber auch nur dann, wenn die Funktion, Klasse oder das Objekt auch wirklich irgendwo VERWENDET wird).

@aphex.matze
Wenn das grob so richtig ist, wie ich das aufgefasst habe, dann macht das natürlich auch Sinn die Implementation in einer separaten CPP-Datei auszulagern, damit die Implementation nicht überall wo die Header-Datei eingefügt wird, "mitkompiliert" wird. Außerdem läuft man ja somit Gefahr, dass in verschiedenen CPP-Dateien identische Funktionsdefinitionen oder Objekt/Variablendefinitionen vorkommen und es zum Linker-Error kommt.

1. Stimmt das alles so bisher?

2. Wieso hat der Linker anscheinend kein Problem damit, wenn in zwei verschiedenen CPP-Dateien die gleiche Klassendefinition zu finden ist?

3. Wenn man trotzdem die Implementation einer Funktion mit in die Header-Datei packen möchte und diese Funktion in mehreren CPP-Dateien inkludieren will, wäre das dann ein passender Einsatzzweck für "static"?

4. Ich selber habe in meinem Programm fast nur template-Klassen und @JaKno hat geschrieben:

JaKno schrieb:
Die Standard Bibliothek besteht zum größten Teil aus Templates und ist deshalb in Headern implementiert.

Soll ich dann meine Template-Klassen auch komplett samt Implementation in eine Header-Datei stecken?
 
Der Compiler erzeugt zB. aus einem Funktionsaufruf ein Symbol (mehr Infos unter dem Stichwort Name Mangling). Wenn das Symbol innerhalb der Compilation Unit aufgelöst werden kann, wird der Aufruf eben direkt durch eine Sprungadresse ersetzt. Wenn nicht, verbleibt ein externes Symbol. Das muss dann später der Linker auflösen, wenn alle Objekt-Dateien zusammengefügt werden. Verleiben dann weiterhin unaufgelöste Symbole, meckert der Linker (beim Erzeugen eines ausführbaren Programms).

2)
Wenn das gleiche Symbol (zB die gleiche Funktion) mehrfach definiert ist, sollte der Linker ebenfalls meckern.

3)
Dann landet ggf. unter Umständen das gleiche Symbol in mehreren Compilation-Units. Damit siehe 2)

4)
Das geht kaum anders. Templates werden in der Compile-Zeit instanziiert. Sieht der Compiler also einen Aufruf eines Templates, muss er die passende Instanziierung aus dem Template generieren. Das geht nur, wenn er die Definition komplett kennt. Die Deklaration würde nicht reichen. (Wenn das Template irgendwie explizit mit dem benötigten Typ instanziiert wird, geht es aber irgendwie doch, Deklaration und Definition zu trennen...)
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Peter_2498
JaKno schrieb:
Wenn man was neu anfängt, dann kann man sicherlich auch den vollen Umfang von C++20 verwenden. Meistens schlägt man sicher aber mit älteren Projekten herum. Bei Einsatz von Templates wird der gesamter Code generiert, wobei die Typplatzhalter durch den realen Datentyp ersetzt werden.
Man sollte nicht jedes Feature nur deswegen nutzen weil es da ist. Auch Templates sollte man nur verwenden wenn man daraus wirklich einen Nutzen für die Lesbarkeit und damit Wartbarkeit des Codes zieht.

Die meisten neueren Features haben nämlich ein großes Problem. Sie entziehen dem Compiler noch mehr Kenntnis vom Code als eh schon...

Gut Modules sollten wohl hier eine löbliche Ausnahme sein meines Wissens nach, aber im Allgemeinen ist obige Aussage richtig. C++ Codes kranken oft daran ab einer gewissen Größe mehr mit der Verwaltung durch C++ zu tun zu haben als mit dem eigentlichen ausführen des Codes.

Ich sag nur mal crossfile inlineing. Das kann teils zweistellige Performancezuwächse bringen, weil irgendwo ne simple Funktion aufgerufen wird...
 
Gibt es eigentlich so eine Seite wo man sich gute Tipps für C++ anschauen kann?

Ich programmiere halt immer noch eher in altem C++(C-Arrays benutze ich allerdings fast gar nicht) und kenne viele dieser neueren Sachen nicht. Ich hab lange ganz "normal" Klassen-Member initialisiert über den Konstruktor, von "Initializer-Lists" wusste ich nichts. Dass Initialisierung über diese "Initializer-List" sogar (unter Umständen) performanter ist, habe ich auch erst später mitbekommen. Gleiches mit diesen if-else Abfragen, wo es ja so eine Methode mit dem Fragezeichen gibt (benutze ich , Stand jetzt, noch nicht). Tipps in dieser Richtung wären hilfreich zu wissen.

Auch "standard" Tipps für Performance wären nicht schlecht. Natürlich kann man überall wohl irgendwas an Performance gewinnen, aber mir geht es um "Standardwissen", um die gängigsten Performance-Fallen.

Kennt ihr da vielleicht eine gute Seite?
 
  • Gefällt mir
Reaktionen: Peter_2498 und Skysnake
Der Standardtipp ist wohl keine fancy Features zu benutzen und es simpel zu halten.

Auch objektorientiert sollte man mit bedacht einsetzen. Grundsätzlich kannst du sagen, das bei allem was du mehre hundert oder gar tausende male pro Sekunde machst aufpassen solltest.

Aber wie immer gelten zwei Dinge preoptimisation is the root of all evil und auf der anderen Seite 80-20. Also 80% Code und 20% der Zeit und 20% Code in 80% der Zeit.

Man sollte NIE etwas optimieren weil man GLAUBT das etwas ein Problem sein könnte. Aber man sollte sich immer Gedanken machen ob etwas ein Problem IST bevor man große Dinge anfängt.

Ich habe beides schon gesehen. Leute die viel Zeit in Optimierungen versenkt haben und sich das Leben nur schwer gemacht haben genau wie Leute die sich keine Gedanken gemacht haben bevor Sie angefangen haben und man quasi den halben Code wegschmeißen konnte...

Wenn du aber nicht weißt, und ich meine wirklich weißt, das du ein Performanceproblem haben wirst, dann designt den Code so das er wartbar ist. Schau aber ruhig mal drauf wie es so wuppt und wenn es nicht einfach Realtime ist, dann schau mit Tools drauf die die Execution analysieren wie Perf zum Beispiel. Dann WEISST du wo das Problem liegt und das kannst du optimieren.
 
  • Gefällt mir
Reaktionen: mental.dIseASe und Peter_2498
Zurück
Oben