Wie kann ich die Kopfhölle verhindern?

45

Wir starten ein neues Projekt von Grund auf. Ungefähr acht Entwickler, etwa ein Dutzend Subsysteme mit jeweils vier oder fünf Quelldateien.

Was können wir tun, um "Header Hell", AKA "Spaghetti Headers", zu verhindern?

  • Ein Header pro Quelldatei?
  • Plus eins pro Subsystem?
  • Typdefs, Stucts & Enums von Funktionsprototypen trennen?
  • Subsystem intern von Subsystem extern trennen?
  • Bestehen Sie darauf, dass jede einzelne Datei, ob Header oder Quelle, eigenständig kompilierbar sein muss?

Ich frage nicht nach einem „besten“ Weg, sondern zeige nur, worauf zu achten ist und was Trauer verursachen kann, damit wir versuchen können, es zu vermeiden.

Dies wird ein C ++ - Projekt sein, aber C-Info würde zukünftigen Lesern helfen.

Mawg
quelle
16
Holen Sie sich eine Kopie von Large-Scale C ++ Software Design , die nicht nur Probleme mit Kopfzeilen vermeidet, sondern auch viele weitere Probleme in Bezug auf physische Abhängigkeiten zwischen Quell- und Objektdateien in einem C ++ - Projekt.
Doc Brown
6
Alle Antworten hier sind großartig. Ich wollte hinzufügen, dass die Dokumentation zur Verwendung von Objekten, Methoden und Funktionen in den Header-Dateien enthalten sein sollte. Ich sehe immer noch doc in den Quelldateien. Lass mich nicht die Quelle lesen. Das ist der Punkt der Header-Datei. Ich sollte die Quelle nicht lesen müssen, es sei denn, ich bin ein Implementierer.
Bill Door
1
Ich bin mir sicher, dass ich schon einmal mit Ihnen zusammengearbeitet habe. Oft :-(
Mawg
5
Was Sie beschreiben, ist kein großes Projekt. Gutes Design ist immer willkommen, aber es kann sein, dass Sie niemals mit Problemen in Bezug auf "Large Scale Systems" konfrontiert werden.
Sam
2
Boost hat tatsächlich einen umfassenden Ansatz. Jedes einzelne Feature hat eine eigene Header-Datei, aber jedes größere Modul hat auch einen Header, der alles enthält. Dies stellt sich als sehr leistungsfähig heraus, um die Header-Hölle zu minimieren, ohne dass Sie gezwungen sind, jedes Mal ein paar hundert Dateien einzuschließen.
Cort Ammon

Antworten:

39

Einfache Methode: Ein Header pro Quelldatei. Wenn Sie über ein vollständiges Subsystem verfügen, von dem nicht erwartet wird, dass Benutzer die Quelldateien kennen, verfügen Sie über einen Header für das Subsystem, der alle erforderlichen Headerdateien enthält.

Jede Header-Datei sollte für sich kompilierbar sein (oder sagen wir, eine Quelldatei mit einem einzelnen Header sollte kompiliert werden). Es ist ein Schmerz, wenn ich finde, welche Header-Datei das enthält, was ich will, und dann muss ich die anderen Header-Dateien suchen. Eine einfache Möglichkeit, dies zu erzwingen, besteht darin, dass jede Quelldatei zuerst die Headerdatei enthält (danke doug65536, ich glaube, ich mache das die meiste Zeit, ohne es zu merken).

Stellen Sie sicher, dass Sie verfügbare Tools verwenden, um die Kompilierungszeiten niedrig zu halten. Jeder Header darf nur einmal enthalten sein. Verwenden Sie vorkompilierte Header, um die Kompilierungszeiten niedrig zu halten. Verwenden Sie vorkompilierte Module, wenn möglich, um die Kompilierungszeiten weiter niedrig zu halten.

gnasher729
quelle
Subsystemübergreifende Funktionsaufrufe mit Parametern von Typen, die im anderen Subsystem deklariert sind, sind problematisch.
Mawg
6
Tricky or not, "#include <subsystem1.h>" sollte kompiliert werden. Wie Sie das erreichen, liegt ganz bei Ihnen. @FrankPuffer: Warum?
gnasher729
13
@Mawg Dies bedeutet, dass Sie entweder ein separates, gemeinsam genutztes Subsystem benötigen, das die Gemeinsamkeiten der einzelnen Subsysteme enthält, oder dass Sie vereinfachte "Schnittstellen" -Header für jedes Subsystem benötigen (die dann von den internen und systemübergreifenden Implementierungs-Headern verwendet werden). . Wenn Sie die Schnittstellenheader nicht ohne Cross-Includes schreiben können, ist Ihr Subsystemdesign durcheinander und Sie müssen die Dinge neu gestalten, damit Ihre Subsysteme unabhängiger sind. (Dies kann das Herausziehen eines gemeinsamen Subsystems als drittes Modul beinhalten.)
RM
8
Eine gute Technik für einen Header sichergestellt ist unabhängig eine Regel haben , dass die Quelldatei immer seinen eigenen Header enthält erste . Dies fängt Fälle ab, in denen Sie Abhängigkeiten aus der Implementierungsdatei in die Header-Datei verschieben müssen.
Doug65536
4
@FrankPuffer: Bitte löschen Sie Ihre Kommentare nicht, besonders wenn andere darauf antworten, da dies die Antworten kontextlos macht. Sie können Ihre Aussage jederzeit in einem neuen Kommentar korrigieren. Vielen Dank! Ich bin interessiert zu wissen, was du gesagt hast, aber jetzt ist es weg :(
MPW
18

Die bei weitem wichtigste Anforderung besteht darin, die Abhängigkeiten zwischen Ihren Quelldateien zu verringern. In C ++ ist es üblich, eine Quelldatei und einen Header pro Klasse zu verwenden. Wenn Sie also ein gutes Design haben, werden Sie der Hölle nicht einmal nahe kommen.

Sie können dies auch in umgekehrter Richtung anzeigen: Wenn Ihr Projekt bereits eine Kopfzeile enthält, können Sie sicher sein, dass das Softwaredesign verbessert werden muss.

Um Ihre spezifischen Fragen zu beantworten:

  • Ein Header pro Quelldatei? → Ja, dies funktioniert in den meisten Fällen gut und erleichtert das Auffinden von Inhalten. Aber mach es nicht zu einer Religion.
  • Plus eins pro Subsystem? → Nein, warum sollten Sie das tun?
  • Typdefs, Stucts & Enums von Funktionsprototypen trennen? → Nein, Funktionen und verwandte Typen gehören zusammen.
  • Subsystem intern von Subsystem extern trennen? → Ja natürlich. Dadurch werden Abhängigkeiten reduziert.
  • Bestehen Sie darauf, dass jede einzelne Datei, ob Header oder Quelle von Standalones, kompatibel ist? → Ja, fordern Sie niemals, dass ein Header vor einem anderen Header eingefügt werden muss.
Frank Puffer
quelle
12

Zusätzlich zu den anderen Empfehlungen zur Verringerung der Abhängigkeiten (hauptsächlich für C ++):

  1. Geben Sie nur an, was Sie wirklich brauchen, wo Sie es brauchen (niedrigstes Niveau möglich). Z.B. Nicht in eine Kopfzeile aufnehmen, wenn die Aufrufe nur in der Quelle benötigt werden.
  2. Verwenden Sie nach Möglichkeit Forward-Deklarationen in Headern (Header enthält nur Zeiger oder Verweise auf andere Klassen).
  3. Bereinigen Sie die Includes nach jedem Refactoring (kommentieren Sie sie aus, sehen Sie, wo die Kompilierung fehlschlägt, verschieben Sie sie dorthin, entfernen Sie die noch kommentierten Include-Zeilen).
  4. Packen Sie nicht zu viele allgemeine Einrichtungen in dieselbe Datei. Teilen Sie sie nach Funktionen auf (z. B. Logger ist eine Klasse, also ein Header und eine Quelldatei; SystemHelper dito usw.).
  5. Halten Sie sich an die OO-Prinzipien, auch wenn Sie nur eine Klasse erhalten, die ausschließlich aus statischen Methoden (anstelle von eigenständigen Funktionen) besteht - oder verwenden Sie stattdessen einen Namespace .
  6. Für bestimmte allgemeine Einrichtungen ist das Singleton-Muster eher nützlich, da Sie die Instanz nicht von einem anderen, nicht verwandten Objekt anfordern müssen.
Murphy
quelle
5
Auf # 3 kann das Tool include-what-you-use helfen, ohne den manuellen Neukompilierungsansatz erraten und überprüfen zu müssen.
RM
1
Können Sie erklären, welchen Nutzen Singletons in diesem Zusammenhang haben? Ich verstehe es wirklich nicht.
Frank Puffer
@FrankPuffer Mein Grundprinzip ist: Ohne einen Singleton besitzt eine Instanz einer Klasse normalerweise die Instanz einer Hilfsklasse, wie z. B. ein Logger. Wenn eine dritte Klasse sie verwenden möchte, muss sie vom Eigentümer eine Referenz der Hilfsklasse anfordern, das heißt, Sie verwenden zwei Klassen und schließen natürlich deren Header ein - auch wenn der Benutzer sonst nichts mit dem Eigentümer zu tun hat. Mit einem Singleton müssen Sie nur den Header der Hilfsklasse einschließen und können die Instanz direkt von dieser anfordern. Sehen Sie einen Fehler in dieser Logik?
Murphy
1
# 2 (Forward-Deklarationen) kann einen großen Unterschied in der Kompilierungszeit und der Abhängigkeitsprüfung bewirken. Da diese Antwort ( stackoverflow.com/a/9999752/509928 ) zeigt, gilt es sowohl für C ++ und C
Dave Compton
3
Sie können Forward-Deklarationen auch bei der Übergabe von Werten verwenden, sofern die Funktion nicht inline definiert ist. Eine Funktionsdeklaration ist kein Kontext, in dem die vollständige Typdefinition benötigt wird (eine Funktionsdefinition oder ein Funktionsaufruf ist ein solcher Kontext).
StoryTeller - Unslander Monica
6

Ein Header pro Quelldatei, der definiert, was die Quelldatei implementiert / exportiert.

In jeder Quelldatei sind so viele Header-Dateien wie erforderlich enthalten (beginnend mit einem eigenen Header).

Vermeiden Sie es, Header-Dateien in andere Header-Dateien aufzunehmen (die Aufnahme zu minimieren) (um zirkuläre Abhängigkeiten zu vermeiden). Einzelheiten finden Sie in dieser Antwort auf "Können sich zwei Klassen mithilfe von C ++ gegenseitig sehen?"

Zu diesem Thema gibt es ein ganzes Buch, Large-Scale C ++ Software Design von Lakos. Es beschreibt das Vorhandensein von "Ebenen" von Software: Ebenen auf hoher Ebene verwenden Ebenen auf niedrigerer Ebene und nicht umgekehrt, wodurch wiederum kreisförmige Abhängigkeiten vermieden werden.

ChrisW
quelle
4

Ich würde behaupten, Ihre Frage ist grundsätzlich unbeantwortet, da es zwei Arten von Header-Hölle gibt:

  • Die Art, in der Sie eine Million verschiedener Header einfügen müssen, und wer zum Teufel kann sich überhaupt an alle erinnern? Und diese Listen von Kopfzeilen pflegen? Pfui.
  • Die Art, in der Sie eine Sache einschließen und herausfinden, dass Sie den gesamten Turm von Babel einschließen (oder sollte ich Turm von Boost sagen? ...)

Die Sache ist, wenn Sie versuchen, das erstere zu vermeiden, landen Sie in gewissem Maße beim letzteren und umgekehrt.

Es gibt auch eine dritte Art von Hölle, nämlich kreisförmige Abhängigkeiten. Diese können auftauchen, wenn Sie nicht aufpassen ... Es ist nicht sehr kompliziert, sie zu vermeiden, aber Sie müssen sich die Zeit nehmen, um darüber nachzudenken, wie Sie es tun können. Sehen Sie John Lakos Vortrag über Levelization auf der CppCon 2016 (oder nur die Folien ).

einpoklum - Monica wieder einsetzen
quelle
1
Sie können kreisförmige Abhängigkeiten nicht immer vermeiden. Ein Beispiel ist ein Modell, in dem sich die Entitäten aufeinander beziehen. Zumindest können Sie versuchen, die Zirkularität im Subsystem zu begrenzen. Wenn Sie also einen Header des Subsystems einbeziehen, haben Sie die Zirkularität abstrahiert.
Nalply
2
@nalply: Ich wollte die zirkuläre Abhängigkeit der Header vermeiden, nicht des Codes. Wenn Sie die zirkuläre Abhängigkeit der Header nicht vermeiden, werden Sie wahrscheinlich nicht in der Lage sein, diese zu erstellen. Aber ja, Punkt genommen, +1.
einpoklum - wieder Monica
1

Entkopplung

Es geht mir letztendlich darum, mich am Ende des Tages auf der grundlegendsten Designebene zu entkoppeln, ohne die Nuance der Eigenschaften unserer Compiler und Linker. Ich meine, Sie können beispielsweise festlegen, dass jeder Header nur eine Klasse definiert, Pimpls verwendet, Deklarationen an Typen weiterleitet, die nur deklariert werden müssen, nicht definiert, oder sogar Header verwenden, die nur Forward-Deklarationen enthalten (z. B. <iosfwd>), einen Header pro Quelldatei , organisieren Sie das System konsistent basierend auf der Art der deklarierten / definierten Dinge usw.

Techniken zum Reduzieren von "Abhängigkeiten bei der Kompilierung"

Und einige der Techniken können einiges helfen, aber Sie können feststellen, dass diese Praktiken anstrengend sind und Ihre durchschnittliche Quelldatei in Ihrem System immer noch eine zweiseitige Präambel benötigt #includeAnweisungen, die mit schnellen Erstellungszeiten nichts Sinnvolles anstellen, wenn Sie sich zu sehr auf die Reduzierung der Kompilierungszeitabhängigkeiten auf Kopfzeilenebene konzentrieren, ohne die logischen Abhängigkeiten in Ihren Schnittstellendesigns zu reduzieren, und obwohl dies streng genommen nicht als "Spaghetti-Kopfzeilen" betrachtet werden kann Ich würde immer noch sagen, dass dies zu ähnlichen nachteiligen Auswirkungen auf die Produktivität in der Praxis führt. Am Ende des Tages, wenn Ihre Kompilierungseinheiten immer noch eine Schiffsladung von Informationen benötigen, um sichtbar zu sein, wird dies zu einer Erhöhung der Build-Zeiten führen und die Gründe multiplizieren, die Sie haben, um möglicherweise zurück zu gehen und Dinge zu ändern, während Sie Entwickler machen Ich habe das Gefühl, als würden sie das System einfach überraschen, um ihre tägliche Programmierung zu beenden. Es'

Sie können beispielsweise festlegen, dass jedes Subsystem eine sehr abstrakte Header-Datei und Schnittstelle bereitstellt. Wenn die Subsysteme jedoch nicht voneinander entkoppelt sind, erhalten Sie wieder etwas Ähnliches wie Spaghetti mit Subsystemschnittstellen, abhängig von anderen Subsystemschnittstellen mit einem Abhängigkeitsdiagramm, das wie ein Durcheinander aussieht, um zu funktionieren.

Deklarationen an externe Typen weiterleiten

Von all den Techniken, die ich erschöpft habe, um zu versuchen, eine frühere Codebasis zu erhalten, die zwei Stunden in Anspruch nahm, während die Entwickler manchmal zwei Tage auf ihren Einsatz bei CI auf unseren Build-Servern warteten Um Schritt zu halten und Fehler zu machen, während Entwickler ihre Änderungen vorantreiben, war es für mich am fraglichsten, Typen zu deklarieren, die in anderen Headern definiert sind. Und ich habe es geschafft, diese Codebasis in kleinen Schritten auf ungefähr 40 Minuten zu reduzieren, während ich versucht habe, "Header - Spaghetti" zu reduzieren, was im Nachhinein die fragwürdigste Praxis ist (da ich die grundlegende Natur von aus den Augen verlor) Das Design, während der Tunnelblick auf Header-Abhängigkeiten gerichtet war, deklarierte Typen, die in anderen Headern definiert waren.

Wenn Sie sich einen Foo.hppHeader vorstellen, der ungefähr so ​​aussieht:

#include "Bar.hpp"

Und es verwendet Barim Header nur eine Art und Weise, die eine Deklaration erfordert, keine Definition. Dann scheint es ein Kinderspiel zu sein, zu deklarieren class Bar;, um zu vermeiden, dass die Definition von Barin der Kopfzeile sichtbar wird. Außer in der Praxis oft werden Sie entweder die meisten der Kompilierungseinheiten finden, verwenden Foo.hppimmer noch am Ende brauchen Barsowieso definiert werden mit der zusätzlichen Last, enthalten Bar.hppsich auf der Oberseite Foo.hpp, oder Sie laufen in ein anderes Szenario , in dem die wirklich Hilfe tut und 99 % Ihrer Kompilierungseinheiten können ohne Einschluss arbeiten Bar.hpp, außer dann wirft sie die grundlegendere Entwurfsfrage auf (oder zumindest denke ich, dass dies heutzutage der Fall sein sollte), warum sie überhaupt die Deklaration von Barund warum sehen müssenFoo Man muss sich sogar die Mühe machen, davon zu erfahren, wenn es für die meisten Anwendungsfälle irrelevant ist (warum sollte man ein Design mit Abhängigkeiten zu einem anderen belasten, das kaum jemals verwendet wurde?).

Weil konzeptionell haben wir nicht wirklich entkoppelt Foovon Bar. Wir haben es gerade so gemacht, dass der Header von Foonicht so viele Informationen über den Header von benötigt Bar, und das ist bei weitem nicht so aussagekräftig wie ein Design, das diese beiden wirklich völlig unabhängig voneinander macht.

Embedded Scripting

Dies ist wirklich für größere Codebasen gedacht, aber eine andere Technik, die ich als äußerst nützlich empfinde, ist die Verwendung einer eingebetteten Skriptsprache für mindestens die übergeordneten Teile Ihres Systems. Ich fand heraus, dass ich Lua in einen Tag einbetten und alle Befehle in unserem System einheitlich aufrufen konnte (die Befehle waren zum Glück abstrakt). Leider bin ich auf eine Straßensperre gestoßen, bei der die Entwickler der Einführung einer anderen Sprache misstrauten und, vielleicht am bizarrsten, Leistung als größten Verdacht empfanden. Obwohl ich andere Bedenken verstehen konnte, sollte die Leistung kein Problem sein, wenn wir das Skript nur zum Aufrufen von Befehlen verwenden, wenn Benutzer beispielsweise auf Schaltflächen klicken, die selbst keine großen Schleifen ausführen (was versuchen wir zu tun, Sorgen Sie sich über Nanosekunden-Unterschiede in den Reaktionszeiten bei einem Tastenklick?).

Beispiel

Der effektivste Weg, den ich je gesehen habe, nachdem ich Techniken zur Verkürzung der Kompilierungszeiten in großen Codebasen angewendet habe, sind Architekturen, die die Menge an Informationen, die für das Funktionieren einer Sache im System erforderlich sind, wirklich reduzieren, und nicht nur das Entkoppeln eines Headers von einem anderen von einem Compiler Die Benutzer dieser Schnittstellen müssen jedoch das Nötigste tun, um zu wissen, was sie tun müssen (sowohl vom menschlichen Standpunkt als auch vom Standpunkt des Compilers aus, echte Entkopplung, die über Compiler-Abhängigkeiten hinausgeht).

Das ECS ist nur ein Beispiel (und ich schlage nicht vor, dass Sie eines verwenden), aber als ich es sah, konnte ich feststellen, dass Sie einige wirklich epische Codebasen haben können, die sich immer noch überraschend schnell aufbauen lassen, während Sie Vorlagen und viele andere Extras verwenden, weil das ECS Natur schafft eine sehr entkoppelte Architektur, in der Systeme nur über die ECS-Datenbank Bescheid wissen müssen und in der Regel nur eine Handvoll Komponententypen (manchmal nur eine), um ihre Aufgabe zu erfüllen:

Bildbeschreibung hier eingeben

Design, Design, Design

Und diese Art von entkoppelten Architekturentwürfen auf menschlicher, konzeptioneller Ebene ist effektiver im Hinblick auf die Minimierung der Kompilierzeiten als alle Techniken, die ich oben untersucht habe, da Ihre Codebasis wächst und wächst und wächst, da sich dieses Wachstum nicht in Ihrem Durchschnitt niederschlägt Kompilierungseinheit, die die zum Zeitpunkt der Kompilierung und Verknüpfung für die Arbeit erforderlichen Mengeninformationen multipliziert (bei jedem System, bei dem ein durchschnittlicher Entwickler eine Schiffsladung von Dingen einbeziehen muss, um etwas zu tun, müssen auch diese Informationen vorhanden sein, und nicht nur der Compiler muss über eine Vielzahl von Informationen Bescheid wissen, um etwas zu tun ). Es hat auch mehr Vorteile als reduzierte Erstellungszeiten und das Entwirren von Headern, da Ihre Entwickler nicht viel über das System wissen müssen, was unmittelbar erforderlich ist, um etwas damit zu tun.

Wenn Sie zum Beispiel einen erfahrenen Physikentwickler beauftragen, eine Physik-Engine für Ihr AAA-Spiel zu entwickeln, die Millionen von LOC umfasst, und der sehr schnell mit der Kenntnis des absoluten Minimums an verfügbaren Informationen wie Typen und Schnittstellen beginnen kann Neben Ihren Systemkonzepten bedeutet dies natürlich auch, dass sowohl er als auch der Compiler weniger Informationen benötigen, um seine Physik-Engine zu erstellen. Gleichzeitig bedeutet dies eine erhebliche Verkürzung der Build-Zeiten und im Allgemeinen, dass es nichts gibt, was Spaghetti ähnelt überall im System. Und das ist es, was ich vorschlage, um all diesen anderen Techniken Vorrang einzuräumen: wie Sie Ihre Systeme entwerfen. Erschöpfende andere Techniken werden das i-Tüpfelchen sein, wenn Sie es tun, während, sonst

Drachen Energie
quelle
1
Eine ausgezeichnete Antwort! Trotzdem musste ich ein bisschen graben, um herauszufinden , was Pickel sind :-)
Mawg
0

Es ist Ansichtssache. Siehe diese und jene Antwort . Und es hängt auch stark von der Projektgröße ab (wenn Sie glauben, dass Sie Millionen von Quelltextzeilen in Ihrem Projekt haben, ist dies nicht dasselbe wie ein paar Dutzendtausende davon).

Im Gegensatz zu anderen Antworten empfehle ich einen (ziemlich großen) öffentlichen Header pro Subsystem (der "private" Header enthalten könnte, möglicherweise mit separaten Dateien für die Implementierung vieler inline Funktionen). Sie könnten sogar einen Header mit nur mehreren #include Direktiven in Betracht ziehen .

Ich denke nicht, dass viele Header-Dateien empfohlen werden. Insbesondere empfehle ich nicht eine Header-Datei pro Klasse oder viele kleine Header-Dateien mit jeweils ein paar Dutzend Zeilen.

(Wenn Sie viele kleine Dateien haben, müssen Sie viele davon in jede kleine Übersetzungseinheit einbinden , und die gesamte Erstellungszeit kann darunter leiden.)

Was Sie wirklich wollen, ist, für jedes Subsystem und jede Datei den Hauptentwickler zu identifizieren, der dafür verantwortlich ist.

Schließlich ist es für ein kleines Projekt (z. B. mit weniger als hunderttausend Zeilen Quellcode) nicht sehr wichtig. Während des Projekts können Sie den Code ganz einfach umgestalten und in verschiedenen Dateien neu organisieren. Sie kopieren einfach Codestücke in neue (Header-) Dateien und fügen sie ein, was keine große Sache ist (was schwieriger ist, klug zu gestalten, wie Sie Ihre Dateien neu organisieren würden, und das ist projektspezifisch).

(Ich persönlich bevorzuge es, zu große und zu kleine Dateien zu vermeiden. Ich habe oft Quelldateien mit jeweils mehreren tausend Zeilen. Ich habe keine Angst vor einer Header-Datei mit eingebetteten Funktionsdefinitionen mit mehreren hundert Zeilen oder sogar ein paar Zeilen Tausende von ihnen)

Beachten Sie, dass Sie, wenn Sie vorkompilierte Header mit GCC verwenden möchten (was manchmal sinnvoll ist, um die Kompilierungszeit zu verkürzen), eine einzelne Header-Datei benötigen (einschließlich aller anderen und auch der System-Header).

Beachten Sie, dass in C ++ in Standardheaderdateien viel Code abgerufen wird . Zum Beispiel #include <vector>zieht mehr als zehntausend Zeilen auf meinem GCC 6 unter Linux (18100 Zeilen). Und #include <map> erweitert sich auf fast 40KLOC. Wenn Sie also viele kleine Header-Dateien einschließlich Standardheader haben, werden beim Erstellen viele tausend Zeilen erneut analysiert, und die Kompilierungszeit leidet darunter. Aus diesem Grund mag ich nicht viele kleine C ++ - Quelltextzeilen (höchstens einige hundert Zeilen), aber ich bevorzuge es, weniger, aber größere C ++ - Dateien (mit mehreren tausend Zeilen) zu haben.

(Hunderte kleiner C ++ - Dateien, die - auch indirekt - immer mehrere Standardheaderdateien enthalten, bedeuten eine enorme Erstellungszeit, die die Entwickler ärgert.)

In C-Code werden Header-Dateien häufig auf eine kleinere Größe erweitert, sodass der Kompromiss anders ist.

Informieren Sie sich auch über frühere Übungen in vorhandenen freien Softwareprojekten (z . B. auf Github ).

Beachten Sie, dass Abhängigkeiten mit einem guten Build-Automatisierungssystem behandelt werden können. Studieren Sie die Dokumentation von GNU make . Beachten Sie die verschiedenen -MPräprozessor-Flags für GCC (nützlich, um automatisch Abhängigkeiten zu generieren).

Mit anderen Worten, Ihr Projekt (mit weniger als einhundert Dateien und einem Dutzend Entwicklern) ist wahrscheinlich nicht groß genug, um wirklich von der "Header-Hölle" betroffen zu sein, sodass Ihre Sorge nicht gerechtfertigt ist . Sie könnten nur ein Dutzend Header-Dateien (oder noch viel weniger) haben, Sie könnten sich für eine Header-Datei pro Übersetzungseinheit entscheiden, Sie könnten sich sogar für eine einzelne Header-Datei entscheiden, und was auch immer Sie tun, es wird keine sein "Header Hölle" (und Refactoring und Reorganisation Ihrer Dateien würde einigermaßen einfach bleiben, so dass die anfängliche Auswahl nicht wirklich wichtig ist ).

(Konzentrieren Sie Ihre Bemühungen nicht auf "Header Hell", was für Sie kein Problem ist, sondern konzentrieren Sie sich darauf, eine gute Architektur zu entwerfen.)

Basile Starynkevitch
quelle
Die von Ihnen genannten technischen Details sind möglicherweise korrekt. Nach meinem Verständnis bat das OP jedoch um Hinweise, wie die Wartbarkeit und Organisation des Codes verbessert werden kann, und nicht um die Kompilierungszeit. Und ich sehe einen direkten Konflikt zwischen diesen beiden Zielen.
Murphy
Aber es ist immer noch eine Ansichtssache. Und das OP startet anscheinend ein nicht so großes Projekt.
Basile Starynkevitch