Mehrere Klassen in einer Header-Datei im Vergleich zu einer einzelnen Header-Datei pro Klasse


74

Aus irgendeinem Grund verfügt unser Unternehmen über eine Kodierungsrichtlinie, in der Folgendes angegeben ist:

Each class shall have it's own header and implementation file.

Wenn wir also schrieb genannt eine Klasse MyStringwürden wir ein zugehöriges brauchen MyStringh.h und MyString.cxx .

Macht das noch jemand? Hat jemand irgendwelche Auswirkungen auf die Kompilierungsleistung gesehen? Kompilieren 5000 Klassen in 10000 Dateien genauso schnell wie 5000 Klassen in 2500 Dateien? Wenn nicht, ist der Unterschied spürbar?

[Wir codieren C ++ und verwenden GCC 3.4.4 als unseren täglichen Compiler]


74
Ihre Unternehmensrichtlinie enthält einen Tippfehler.
Leichtigkeitsrennen im Orbit

2
Übrigens ist das flüchtig. Was für eine dumme, aufgeblähte Anforderung.
Leichtigkeitsrennen im Orbit

8
Der Tippfehler ist der streunende Apostroph. (besitzergreifend von "es" ist "sein") ("es ist" ist eine Kontraktion von "es ist").
Strg-Alt-Delor

9
In welche Datei habe ich die Klasse MyString eingefügt? Ich bin sicher, ich habe es hier irgendwo gelassen. Gibt es eine Möglichkeit, das Erinnern zu erleichtern? Ich weiß was, wenn wir ...
Strg-Alt-Delor

5
Ich habe Code gesehen, der mehrere Klassendefinitionen in einer Header-Datei hat, und die cpp-Datei definiert Mitgliedsfunktionen mehrerer Klassen. Ein Debugging-Albtraum
UmNyobe

Antworten:


81

Der Begriff hier ist Übersetzungseinheit, und Sie möchten wirklich (wenn möglich) eine Klasse pro Übersetzungseinheit haben, dh eine Klassenimplementierung pro CPP-Datei mit einer entsprechenden .h-Datei mit demselben Namen.

Es ist normalerweise effizienter (vom Kompilieren / Verknüpfen), Dinge auf diese Weise zu tun, insbesondere wenn Sie Dinge wie inkrementelle Verknüpfungen usw. ausführen. Die Idee ist, dass Übersetzungseinheiten so isoliert sind, dass Sie beim Ändern einer Übersetzungseinheit nicht viele Dinge neu erstellen müssen, wie Sie es tun müssten, wenn Sie anfangen würden, viele Abstraktionen in einer einzigen Übersetzungseinheit zusammenzufassen.

Außerdem werden Sie feststellen, dass viele Fehler / Diagnosen über den Dateinamen ("Fehler in Myclass.cpp, Zeile 22") gemeldet werden. Dies ist hilfreich, wenn zwischen Dateien und Klassen eine Eins-zu-Eins-Entsprechung besteht. (Oder ich nehme an, Sie könnten es eine 2 zu 1 Korrespondenz nennen).


1
Sie möchten Ihre Klassen nicht in separaten Dateien aufbewahren, wenn sie logisch miteinander verbunden sind. Dies ist einer der vielen Mythen, die wir in der Programmierung haben. Das Problem ist, dass die Anzahl der Dateien, die wir erhalten, explodiert und nicht mehr verwaltet werden kann. Wenn Ihre Dateien sehr groß werden, liegt dies wahrscheinlich daran, dass Ihr Design falsch ist. Versuchen Sie, Zeiger (nackt, geteilt und einzigartig) zu entfernen, und beenden Sie die Zuweisung. Das wird Ihr Programm auf ganzer Linie verbessern.
Klarer

69

Überwältigt von Tausenden von Codezeilen?

Ein Satz von Header- / Quelldateien pro Klasse in einem Verzeichnis zu haben, kann übertrieben erscheinen. Und wenn die Anzahl der Klassen auf 100 oder 1000 steigt, kann es sogar beängstigend sein.

Aber nachdem wir mit Quellen gespielt haben, die der Philosophie "Lasst uns alles zusammenstellen" folgen, ist die Schlussfolgerung, dass nur derjenige, der die Datei geschrieben hat, die Hoffnung hat, nicht im Inneren verloren zu gehen. Selbst mit einer IDE ist es leicht, Dinge zu übersehen, denn wenn Sie mit einer Quelle von 20.000 Zeilen spielen, schließen Sie einfach Ihren Verstand für alles, was sich nicht genau auf Ihr Problem bezieht.

Beispiel aus dem wirklichen Leben: Die in diesen Quellen mit tausend Zeilen definierte Klassenhierarchie schloss sich zu einer Diamantvererbung zusammen, und einige Methoden wurden in untergeordneten Klassen von Methoden mit genau demselben Code überschrieben. Dies wurde leicht übersehen (wer möchte einen Quellcode mit 20.000 Zeilen untersuchen / überprüfen?), Und als die ursprüngliche Methode geändert wurde (Fehlerkorrektur), war der Effekt nicht so universell wie ausgenommen.

Abhängigkeiten werden kreisförmig?

Ich hatte dieses Problem mit Vorlagencode, aber ich sah ähnliche Probleme mit normalem C ++ - und C-Code.

Wenn Sie Ihre Quellen in 1 Header pro Struktur / Klasse aufteilen, können Sie:

  • Beschleunigen Sie die Kompilierung, da Sie die Symbol-Vorwärtsdeklaration verwenden können, anstatt ganze Objekte einzuschließen
  • Zirkuläre Abhängigkeiten zwischen Klassen haben (§) (dh Klasse A hat einen Zeiger auf B und B hat einen Zeiger auf A)

Im quellengesteuerten Code können Klassenabhängigkeiten dazu führen, dass Klassen regelmäßig in der Datei nach oben und unten verschoben werden, damit der Header kompiliert wird. Sie möchten die Entwicklung solcher Bewegungen nicht untersuchen, wenn Sie dieselbe Datei in verschiedenen Versionen vergleichen.

Durch separate Header wird der Code modularer, schneller zu kompilieren und es ist einfacher, seine Entwicklung anhand verschiedener Versionsunterschiede zu untersuchen

Für mein Vorlagenprogramm musste ich meine Header in zwei Dateien aufteilen: Die HPP-Datei mit der Deklaration / Definition der Vorlagenklasse und die INL-Datei mit den Definitionen der genannten Klassenmethoden.

Wenn Sie den gesamten Code in einen einzigen Header einfügen, müssen Sie die Klassendefinitionen am Anfang dieser Datei und die Methodendefinitionen am Ende einfügen.

Und wenn jemand nur einen kleinen Teil des Codes mit der Lösung nur für einen Header benötigt, muss er immer noch für die langsamere Kompilierung bezahlen.

(§) Beachten Sie, dass Sie zirkuläre Abhängigkeiten zwischen Klassen haben können, wenn Sie wissen, welche Klasse welche besitzt. Dies ist eine Diskussion über Klassen, die Kenntnisse über die Existenz anderer Klassen haben, nicht über Antipattern für zirkuläre Abhängigkeiten von shared_ptr.

Ein letztes Wort: Überschriften sollten autark sein

Eines muss jedoch von einer Lösung aus mehreren Headern und mehreren Quellen beachtet werden.

Wenn Sie einen Header einfügen, egal welcher Header, muss Ihre Quelle sauber kompiliert werden.

Jeder Header sollte autark sein. Sie sollten Code entwickeln und nicht nach Schätzen suchen, indem Sie Ihr Projekt mit mehr als 10.000 Quelldateien durchsuchen, um herauszufinden, welcher Header das Symbol im Header mit 1.000 Zeilen definiert, den Sie nur aufgrund einer Aufzählung einfügen müssen.

Dies bedeutet, dass entweder jeder Header alle verwendeten Symbole definiert oder deklariert oder alle erforderlichen Header (und nur die erforderlichen Header) enthält.

Frage zu zirkulären Abhängigkeiten

Unterstrich-d fragt:

Können Sie erklären, wie sich die Verwendung separater Header auf zirkuläre Abhängigkeiten auswirkt? Ich glaube nicht. Wir können trivialerweise eine zirkuläre Abhängigkeit erstellen, selbst wenn beide Klassen vollständig im selben Header deklariert sind, indem wir einfach eine im Voraus vorwärts deklarieren, bevor wir im anderen ein Handle dafür deklarieren. Alles andere scheint großartige Punkte zu sein, aber die Idee, dass separate Header zirkuläre Abhängigkeiten ermöglichen, scheint weit entfernt zu sein

underscore_d, 13. November um 23:20 Uhr

Angenommen, Sie haben zwei Klassenvorlagen, A und B.

Angenommen, die Definition der Klasse A (bzw. B) hat einen Zeiger auf B (bzw. A). Nehmen wir auch an, die Methoden der Klasse A (bzw. B) rufen tatsächlich Methoden von B (bzw. A) auf.

Sie haben eine zirkuläre Abhängigkeit sowohl bei der Definition der Klassen als auch bei der Implementierung ihrer Methoden.

Wenn A und B normale Klassen wären und die Methoden von A und B in CPP-Dateien enthalten wären, gäbe es kein Problem: Sie würden eine Vorwärtsdeklaration verwenden, einen Header für jede Klassendefinition haben, dann würde jeder CPP beide HPP enthalten.

Da Sie jedoch Vorlagen haben, müssen Sie diese Muster tatsächlich oben reproduzieren, jedoch nur mit Überschriften.

Das heisst:

  1. ein Definitionsheader A.def.hpp und B.def.hpp
  2. einen Implementierungsheader A.inl.hpp und B.inl.hpp
  3. der Einfachheit halber ein "naiver" Header A.hpp und B.hpp

Jeder Header weist die folgenden Merkmale auf:

  1. In A.def.hpp (bzw. B.def.hpp) haben Sie eine Vorwärtsdeklaration der Klasse B (bzw. A), mit der Sie einen Zeiger / Verweis auf diese Klasse deklarieren können
  2. A.inl.hpp (bzw. B.inl.hpp) umfasst sowohl A.def.hpp als auch B.def.hpp, wodurch Methoden von A (bzw. B) die Klasse B (bzw. A) verwenden können. .
  3. A.hpp (bzw. B.hpp) umfasst direkt sowohl A.def.hpp als auch A.inl.hpp (bzw. B.def.hpp und B.inl.hpp).
  4. Natürlich müssen alle Header autark und durch Header Guards geschützt sein

Der naive Benutzer wird A.hpp und / oder B.hpp einschließen, wodurch das ganze Durcheinander ignoriert wird.

Und mit dieser Organisation kann der Bibliotheksschreiber die zirkulären Abhängigkeiten zwischen A und B lösen, während beide Klassen in separaten Dateien gespeichert werden. Sobald Sie das Schema verstanden haben, können Sie leicht navigieren.

Bitte beachten Sie, dass es sich um einen Randfall handelte (zwei Vorlagen kennen sich). Ich erwarte, dass der meiste Code diesen Trick nicht benötigt.


Können Sie erklären, wie sich die Verwendung separater Header auf zirkuläre Abhängigkeiten auswirkt? Ich glaube nicht. Wir können trivialerweise eine zirkuläre Abhängigkeit erstellen, selbst wenn beide Klassen vollständig im selben Header deklariert sind, indem wir einfach eine im Voraus vorwärts deklarieren, bevor wir im anderen ein Handle dafür deklarieren. Alles andere scheint großartige Punkte zu sein, aber die Idee, dass separate Header zirkuläre Abhängigkeiten ermöglichen, scheint weit entfernt zu sein.
underscore_d

@underscore_d: Ich habe deine Frage in meiner Antwort beantwortet. Bitte sag mir, ob ich etwas verpasst habe.
Paercebal

1
Danke, das deckt alles ab! Tatsächlich wurde mir beim weiteren Lesen peinlich klar, dass ich dieses Problem bereits bei Vorlagenklassen hatte und es im Grunde auf die gleiche Weise mit .hppund .tppDateien löste ... aber mein Gedächtnis reichte nicht aus, um die Zuordnung herzustellen - oder vielleicht Ich habe es ausgeblendet. : D Zum Glück habe ich diesen Bereich seitdem so umgestaltet, dass er weit weniger verwirrend ist, da es, wie Sie angedeutet haben, wahrscheinlich ein Muster ist, das in der Praxis selten benötigt wird.
underscore_d

11

Wir machen das bei der Arbeit, es ist einfach einfacher, Dinge zu finden, wenn die Klasse und die Dateien den gleichen Namen haben. Was die Leistung betrifft, sollten Sie wirklich nicht 5000 Klassen in einem einzelnen Projekt haben. In diesem Fall ist möglicherweise ein Refactoring angebracht.

Es gibt jedoch Fälle, in denen mehrere Klassen in einer Datei vorhanden sind. Und dann ist es nur eine private Hilfsklasse für die Hauptklasse der Datei.


12
Sie sollten wirklich nicht 5000 Klassen in einem einzigen Projekt haben <- Ähm ... warum nicht? Natürlich möchten Sie sie nicht ständig neu erstellen, aber wenn das System so groß ist, ist es so groß.
Billy ONeal

4
"... du solltest wirklich nicht 5000 Klassen in einem einzigen Projekt haben". Das ist ein schrecklicher Rat. Sie sollten viele Klassen haben, da das Projekt einigermaßen wartbar sein muss.
Birgersp

8

+1 zur Trennung. Ich bin gerade auf ein Projekt gestoßen, bei dem sich einige Klassen in Dateien mit einem anderen Namen befinden oder mit einer anderen Klasse zusammengefasst sind, und es ist unmöglich, diese schnell und effizient zu finden. Sie können mehr Ressourcen auf einen Build werfen - Sie können verlorene Programmiererzeit nicht wettmachen, da er nicht die richtige Datei zum Bearbeiten findet.


8

Die Trennung von Klassen in separate Dateien ist nicht nur einfach "klarer", sondern erleichtert es auch mehreren Entwicklern, sich nicht gegenseitig auf die Zehen zu treten. Es wird weniger Zusammenführungen geben, wenn Änderungen an Ihrem Versionskontroll-Tool vorgenommen werden müssen.


1
Die Versionskontrolle ist ein ausgezeichneter Punkt. Ich möchte hinzufügen, dass sich die Vorteile aus gleichwertigen Gründen auch auf Programmierer erstrecken, die sich selten um das Zusammenführen kümmern müssen, einschließlich solcher wie ich, die unabhängig programmieren. Die Verwendung von mehr und kleineren Dateien erleichtert das Lesen von Commit-Historien erheblich, egal ob direkt oder mit grepoder was auch immer. Zum Beispiel ist es viel einfacher zu tippen git log SmallFile.cppals git log GiganticFile.cpp | grep -b4 -a4 SureHopeIPutTheMagicWordInEveryCommitMsg(ein erfundenes Beispiel, aber Sie bekommen die Idee).
underscore_d

5

Die meisten Orte, an denen ich gearbeitet habe, haben diese Praxis befolgt. Ich habe tatsächlich Codierungsstandards für BAE (Aust.) Zusammen mit den Gründen geschrieben, warum anstatt nur etwas ohne wirkliche Rechtfertigung in Stein zu schnitzen.

In Bezug auf Ihre Frage zu Quelldateien ist es nicht so viel Zeit zum Kompilieren, sondern vielmehr ein Problem, das relevante Code-Snippet überhaupt zu finden. Nicht jeder verwendet eine IDE. Und zu wissen, dass Sie nur nach MyClass.h und MyClass.cpp suchen, spart wirklich Zeit im Vergleich dazu, "grep MyClass *. (H | cpp)" über eine Reihe von Dateien auszuführen und dann die # include MyClass.h-Anweisungen herauszufiltern ...

Wohlgemerkt, es gibt Workarounds für die Auswirkung einer großen Anzahl von Quelldateien auf die Kompilierungszeiten. Eine interessante Diskussion finden Sie unter Large Scale C ++ Software Design von John Lakos.

Vielleicht möchten Sie auch Code Complete von Steve McConnell lesen, um ein hervorragendes Kapitel über Codierungsrichtlinien zu erhalten. Tatsächlich ist dieses Buch eine großartige Lektüre, auf die ich regelmäßig zurückkomme


3

Dies ist gängige Praxis, insbesondere um .h in die Dateien aufnehmen zu können, die es benötigen. Natürlich ist die Leistung beeinträchtigt, aber versuchen Sie nicht, über dieses Problem nachzudenken, bis es auftritt :).
Beginnen Sie besser mit den getrennten Dateien und versuchen Sie anschließend, die üblicherweise verwendeten .hs zusammenzuführen, um die Leistung zu verbessern, wenn dies wirklich erforderlich ist. Es kommt alles auf Abhängigkeiten zwischen Dateien an und dies ist für jedes Projekt sehr spezifisch.


3

Wie andere bereits gesagt haben, besteht die beste Vorgehensweise darin, jede Klasse aus Sicht der Codepflege und der Verständlichkeit in einer eigenen Übersetzungseinheit zu platzieren. Bei großen Systemen ist dies jedoch manchmal nicht ratsam. Weitere Informationen zu den Kompromissen finden Sie im Abschnitt "Vergrößern dieser Quelldateien" in diesem Artikel von Bruce Dawson.



1

Es ist sehr hilfreich, nur eine Klasse pro Datei zu haben. Wenn Sie jedoch über Bulkbuild-Dateien erstellen, die alle einzelnen C ++ - Dateien enthalten, werden die Kompilierungen beschleunigt, da die Startzeit für viele Compiler relativ lang ist.


1

Ich bin überrascht, dass fast jeder dafür ist, eine Datei pro Klasse zu haben. Das Problem dabei ist, dass es im Zeitalter des 'Refactorings' schwierig sein kann, die Datei- und Klassennamen synchron zu halten. Jedes Mal, wenn Sie einen Klassennamen ändern, müssen Sie auch den Dateinamen ändern. Dies bedeutet, dass Sie auch überall dort Änderungen vornehmen müssen, wo die Datei enthalten ist.

Ich persönlich gruppiere verwandte Klassen in einer einzigen Datei und gebe einer solchen Datei dann einen aussagekräftigen Namen, der sich nicht ändern muss, selbst wenn sich ein Klassenname ändert. Wenn Sie weniger Dateien haben, können Sie auch einfacher durch einen Dateibaum scrollen. Ich verwende Visual Studio unter Windows und Eclipse CDT unter Linux und beide haben Tastenkombinationen, mit denen Sie direkt zu einer Klassendeklaration gelangen, sodass das Auffinden einer Klassendeklaration einfach und schnell ist.

Trotzdem denke ich, dass es sinnvoll sein kann, eine Klasse pro Datei zu haben, wenn ein Projekt abgeschlossen ist oder seine Struktur sich „verfestigt“ hat und Namensänderungen selten werden. Ich wünschte, es gäbe ein Tool, mit dem Klassen extrahiert und in unterschiedlichen .h- und .cpp-Dateien abgelegt werden könnten. Aber ich sehe das nicht als wesentlich an.

Die Wahl hängt auch von der Art des Projekts ab, an dem gearbeitet wird. Meiner Meinung nach verdient das Problem keine Schwarz-Weiß-Antwort, da jede Wahl Vor- und Nachteile hat.


AFAICT Es ist kein Problem, eine Reihe von Dateinamen in einer kompetenten Entwicklungsumgebung zu ändern. Ich meine, in einer Shell nehmen Sie einfach den Befehl, den Sie bereits verwendet haben, um die Namen in Dateien zu ersetzen, und ändern ihn leicht: sed -i 's/\<TheClassName\>/TheNewName/g' In*.?pp=> rename 's/\<TheClassName\>/TheNewName/' In*.?pp. Bei Bedarf in einigen Unterverzeichnissen wiederholen. Und wenn das sedohne Konflikte funktioniert hat (oder Sie einen genaueren geschrieben haben), dann renameist das viel wahrscheinlicher. Ich habe fast keine Erfahrung mit GUI-IDEs, aber ich hoffe, dass sie dies noch einfacher machen, sodass ich kein Problem sehe.
underscore_d

0

Die gleiche Regel gilt hier, es werden jedoch einige Ausnahmen angegeben, wo dies zulässig ist.

  • Erbbäume
  • Klassen, die nur in einem sehr begrenzten Umfang verwendet werden
  • Einige Dienstprogramme werden einfach in ein allgemeines 'utils.h' eingefügt.

0

Zwei Wörter: Ockhams Rasiermesser. Behalten Sie eine Klasse pro Datei mit dem entsprechenden Header in einer separaten Datei. Wenn Sie etwas anderes tun, z. B. ein Stück Funktionalität pro Datei behalten, müssen Sie alle Arten von Regeln darüber erstellen, was ein Teil der Funktionalität ausmacht. Es gibt viel mehr zu gewinnen, wenn Sie eine Klasse pro Datei behalten. Und selbst halbwegs anständige Tools können große Mengen von Dateien verarbeiten. Halte es einfach, mein Freund.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.