Ist es eine gute Praxis, sich darauf zu verlassen, dass Überschriften transitiv aufgenommen werden?

37

Ich bereinige die Includes in einem C ++ - Projekt, an dem ich arbeite, und frage mich immer wieder, ob ich alle Header, die direkt in einer bestimmten Datei verwendet werden, explizit einschließen soll oder ob ich nur das Nötigste einschließen soll.

Hier ist ein Beispiel Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Nehmen wir an, dass eine Forward-Deklaration für RenderObjectkeine Option ist.)

Nun, ich weiß, das RenderObject.hppschließt ein Texture.hpp- ich weiß das, weil jeder RenderObjectein TextureMitglied hat. Ich beziehe dies jedoch ausdrücklich mit Texture.hppein Entity.hpp, da ich nicht sicher bin, ob es eine gute Idee ist, sich darauf zu verlassen, dass es in aufgenommen wird RenderObject.hpp.

Also: Ist es eine gute Übung oder nicht?

futlib
quelle
19
Wo sind die Include-Guards in Ihrem Beispiel? Du hast sie nur versehentlich vergessen, hoffe ich?
Doc Brown
3
Ein Problem, das auftritt, wenn Sie nicht alle verwendeten Dateien einschließen, ist, dass die Reihenfolge, in der Sie die Dateien einschließen, manchmal wichtig wird. Das ist wirklich ärgerlich, nur in dem Einzelfall, in dem es passiert, aber manchmal würden Schneebälle und Sie sich wirklich wünschen, dass die Person, die den Code so geschrieben hat, vor einem Erschießungskommando marschiert.
Dunk
Deshalb gibt es #ifndef _RENDER_H #define _RENDER_H ... #endif.
Sampathsris
@Dunk Ich denke du hast das Problem falsch verstanden. Mit einem seiner Vorschläge sollte das nicht passieren.
Mooing Duck
1
@ DocBrown, regelt #pragma oncees, nein?
Pacerier

Antworten:

65

Sie sollten immer alle Header, die Objekte definieren, die in einer CPP-Datei verwendet werden, in diese Datei aufnehmen, unabhängig davon, was Sie über die Inhalte dieser Dateien wissen. Sie sollten in allen Header-Dateien Schutzmaßnahmen enthalten haben, um sicherzustellen, dass das mehrfache Einfügen von Headern keine Rolle spielt.

Die Gründe:

  • Dies macht Entwicklern, die den Quellcode lesen, klar, was die betreffende Quelldatei erfordert. Hier kann jemand, der sich die ersten Zeilen in der Datei ansieht, feststellen, dass es sich um TextureObjekte in dieser Datei handelt.
  • Auf diese Weise werden Probleme vermieden, bei denen überarbeitete Header Kompilierungsprobleme verursachen, wenn sie selbst keine bestimmten Header mehr benötigen. Nehmen wir zum Beispiel an, Sie stellen fest, dass sich RenderObject.hppdas nicht wirklich von Texture.hppselbst benötigt .

Eine Konsequenz ist, dass Sie niemals einen Header in einen anderen Header einfügen sollten, es sei denn, er wird ausdrücklich in dieser Datei benötigt.

Gort den Roboter
quelle
10
Stimmen Sie mit der Konsequenz überein - mit der Maßgabe, dass der andere Header IMMER eingeschlossen werden sollte, wenn er benötigt wird!
Andrew
1
Ich mag es nicht, Überschriften für alle einzelnen Klassen direkt einzubeziehen. Ich bin für kumulative Überschriften. Das heißt, ich denke, die High-Level-Datei sollte auf ein "Modul" verweisen, das sie verwendet, muss aber nicht alle Einzelteile direkt enthalten.
edA-qa mort-ora-y
8
Dies führt zu großen, monolithischen Headern, die in jeder Datei enthalten sind, auch wenn sie nur kleine Teile des Inhalts benötigen. Dies führt zu langen Kompilierungszeiten und erschwert das Refactoring.
Gort the Robot
6
Google hat ein Tool entwickelt, mit dessen Hilfe genau dieser Rat eingehalten werden kann: Include-what-you-use .
Matthew G.
3
Das Hauptproblem bei der Kompilierung großer monolithischer Header ist nicht die Kompilierung des Header-Codes selbst, sondern die Notwendigkeit, bei jeder Änderung des Headers jede CPP-Datei in Ihrer Anwendung zu kompilieren. Vorkompilierte Header helfen dem nicht.
Gort the Robot
23

Die allgemeine Faustregel lautet: Geben Sie an, was Sie verwenden. Wenn Sie ein Objekt direkt verwenden, fügen Sie dessen Header-Datei direkt hinzu. Wenn Sie ein Objekt A verwenden, das B verwendet, aber B nicht selbst verwendet, geben Sie nur Ah an

Während wir uns mit dem Thema befassen, sollten Sie andere Header-Dateien nur dann in Ihre Header-Datei aufnehmen, wenn Sie sie tatsächlich im Header benötigen. Wenn Sie es nur in der CPP-Datei benötigen, schließen Sie es nur dort ein: Dies ist der Unterschied zwischen einer öffentlichen und einer privaten Abhängigkeit und verhindert, dass Benutzer Ihrer Klasse Header einfügen, die sie nicht wirklich benötigen.

Nir Friedman
quelle
10

Ich frage mich immer wieder, ob ich alle Header, die direkt in einer bestimmten Datei verwendet werden, explizit einschließen soll oder nicht

Ja.

Sie wissen nie, wann sich diese anderen Header ändern könnten. Es ist auf der ganzen Welt sinnvoll, in jede Übersetzungseinheit die Überschriften aufzunehmen, von denen Sie wissen, dass sie von der Übersetzungseinheit benötigt werden.

Wir haben Header-Guards, um sicherzustellen, dass Doppeleinschlüsse nicht schädlich sind.

Leichtigkeit Rennen mit Monica
quelle
3

Diesbezüglich gehen die Meinungen auseinander, aber ich bin der Ansicht, dass jede Datei (ob c / cpp-Quelldatei oder h / hpp-Headerdatei) für sich kompiliert oder analysiert werden kann.

Aus diesem Grund sollten alle Dateien alle benötigten Header-Dateien enthalten. Sie sollten nicht davon ausgehen, dass bereits eine Header-Datei enthalten war.

Es ist ein echtes Problem, wenn Sie eine Header-Datei hinzufügen und feststellen müssen, dass sie ein Element verwendet, das an einer anderen Stelle definiert ist, ohne es direkt einzuschließen ... Sie müssen also suchen (und möglicherweise die falsche finden!)

Andererseits spielt es (in der Regel) keine Rolle, ob Sie eine Datei einschließen, die Sie nicht benötigen ...


Aus persönlichen Gründen ordne ich #include-Dateien in alphabetischer Reihenfolge an, teile sie in System und Anwendung auf - dies verstärkt die "in sich geschlossene und vollständig kohärente" Botschaft.

Andrew
quelle
Anmerkung zur Reihenfolge der Includes: Manchmal ist die Reihenfolge wichtig, zum Beispiel beim Einfügen von X11-Headern. Dies kann auf das Design zurückzuführen sein (was in diesem Fall als schlechtes Design angesehen werden kann). Manchmal ist dies auf unglückliche Inkompatibilitätsprobleme zurückzuführen.
Hyde
Ein Hinweis zum Einfügen unnötiger Header ist für die Kompilierungszeiten von Bedeutung, und zwar zunächst direkt (insbesondere wenn es sich um vorlagenintensives C ++ handelt), aber insbesondere beim Einfügen von Headern desselben oder eines Abhängigkeitsprojekts, bei dem sich die Include-Datei ebenfalls ändert und die Neukompilierung von auslöst alles inklusive (wenn du Abhängigkeiten hast, wenn du keine hast, musst du die ganze Zeit sauber bauen ...).
Hyde
2

Es hängt davon ab, ob diese transitiven Inklusion durch Notwendigkeit (z. B. Basisklasse) oder aufgrund eines Implementierungsdetails (privates Mitglied) erfolgt.

Zur Verdeutlichung ist die transitive Einbeziehung beim Entfernen nur dann erforderlich, wenn zuerst die im Zwischenheader deklarierten Schnittstellen geändert wurden. Da dies bereits eine wichtige Änderung ist, muss jede CPP-Datei, die diese verwendet, trotzdem überprüft werden.

Beispiel: Ah wird von Bh eingeschlossen, das von C.cpp verwendet wird. Wenn Bh Ah für einige Implementierungsdetails verwendet, sollte C.cpp nicht davon ausgehen, dass Bh dies auch weiterhin tun wird. Wenn Bh jedoch Ah für eine Basisklasse verwendet, geht C.cpp möglicherweise davon aus, dass Bh weiterhin die relevanten Header für seine Basisklassen enthält.

Sie sehen hier den eigentlichen Vorteil, Header-Einschlüsse NICHT zu duplizieren. Sagen Sie, dass die von Bh verwendete Basisklasse wirklich nicht zu Ah gehört und in Bh selbst überarbeitet wurde. Bh ist jetzt ein eigenständiger Header. Wenn C.cpp Ah redundant enthielt, enthält es jetzt einen unnötigen Header.

MSalters
quelle
2

Es kann einen anderen Fall geben: Sie haben Ah, Bh und Ihr C.cpp, Bh enthält Ah

In C.cpp können Sie also schreiben

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Also, wenn Sie hier nicht #include "Ah" schreiben, was kann passieren? In Ihrem C.CPP werden sowohl A als auch B (z. B. Klasse) verwendet. Später haben Sie Ihren C.cpp-Code geändert, B-bezogene Inhalte entfernt und Bh dort belassen.

Wenn Sie sowohl Ah als auch Bh einschließen und zu diesem Zeitpunkt, können Tools, die unnötige Einschlüsse erkennen, Ihnen dabei helfen, darauf hinzuweisen, dass Bh-Einschlüsse nicht mehr benötigt werden. Wenn Sie nur Bh wie oben einschließen, ist es für Tools / Menschen schwierig, das unnötige Einschließen nach einer Codeänderung zu erkennen.

Käfer König
quelle
1

Ich verfolge einen etwas anderen Ansatz als die vorgeschlagenen Antworten.

Geben Sie in den Kopfzeilen immer nur ein Minimum an, genau das, was für den Kompilierungsdurchlauf erforderlich ist. Verwenden Sie nach Möglichkeit die Forward-Deklaration.

In den Quelldateien ist es nicht so wichtig, wie viel Sie einschließen. Meine Präferenzen sind immer noch mindestens, um es passieren zu lassen.

Für kleine Projekte, einschließlich Überschriften hier und da, wird es keinen Unterschied machen. Bei mittleren bis großen Projekten kann dies jedoch zu einem Problem werden. Selbst wenn die neueste Hardware zum Kompilieren verwendet wird, kann der Unterschied spürbar sein. Der Grund dafür ist, dass der Compiler den enthaltenen Header noch öffnen und analysieren muss. Um den Build zu optimieren, wenden Sie die obige Technik an (schließen Sie das Nötigste ein und verwenden Sie die Vorwärtsdeklaration).

Obwohl etwas veraltet, erklärt Large Scale C ++ Software Design (von John Lakos) dies alles im Detail.

BЈовић
quelle
1
Nicht einverstanden mit dieser Strategie ... Wenn Sie eine Header-Datei in eine Quelldatei aufnehmen, müssen Sie alle Abhängigkeiten aufspüren. Es ist besser, direkt aufzunehmen, als die Liste zu dokumentieren!
Andrew
@Andrew gibt es Tools und Skripte, mit denen überprüft werden kann, was und wie oft enthalten ist.
BЈовић
1
Ich habe festgestellt, dass einige der neuesten Compiler optimiert wurden, um damit umzugehen. Sie erkennen eine typische Wachaussage und verarbeiten sie. Wenn sie es dann wieder aufnehmen, können sie die Dateiladung vollständig optimieren. Ihre Empfehlung von Forward-Deklarationen ist jedoch sehr sinnvoll, um die Anzahl der Includes zu verringern. Sobald Sie mit der Verwendung von Forward-Deklarationen beginnen, stellt sich ein Gleichgewicht zwischen Compiler-Laufzeit (verbessert durch Forward-Deklarationen) und Benutzerfreundlichkeit (verbessert durch praktische zusätzliche #includes) ein, das jedes Unternehmen unterschiedlich festlegt.
Cort Ammon
1
@CortAmmon Ein typischer Header hat auch Wachen, aber der Compiler muss ihn noch öffnen, und das ist ein langsamer Vorgang
BЈовић
4
@ BЈовић: Eigentlich nicht. Sie müssen lediglich erkennen, dass die Datei über "typische" Header-Guards verfügt, und diese so kennzeichnen, dass sie nur einmal geöffnet wird. Beispielsweise verfügt Gcc über eine Dokumentation darüber, wann und wo diese Optimierung angewendet wird
Cort Ammon,
-4

Es empfiehlt sich, sich keine Gedanken über Ihre Header-Strategie zu machen, solange diese kompiliert wird.

Der Header-Abschnitt Ihres Codes ist nur ein Zeilenblock, den sich niemand ansehen sollte, bis ein leicht zu behebender Kompilierungsfehler auftritt. Ich verstehe den Wunsch nach 'richtigem' Stil, aber keiner von beiden kann wirklich als richtig bezeichnet werden. Das Einschließen eines Headers für jede Klasse führt mit größerer Wahrscheinlichkeit zu lästigen auftragsbezogenen Kompilierungsfehlern. Diese Kompilierungsfehler spiegeln jedoch auch Probleme wider, die durch sorgfältiges Codieren behoben werden könnten (obwohl es sich nicht lohnt, sie zu beheben).

Und ja, Sie werden diese auftragsbezogenen Probleme haben, sobald Sie anfangen, an friendLand zu kommen.

Sie können sich das Problem in zwei Fällen vorstellen.


Fall 1: Sie haben eine kleine Anzahl von Klassen, die miteinander interagieren, beispielsweise weniger als ein Dutzend. Sie fügen diese Header regelmäßig hinzu, entfernen sie und ändern sie auf andere Weise, so dass sich dies auf ihre gegenseitigen Abhängigkeiten auswirkt. Dies ist der Fall, den Ihr Codebeispiel vorschlägt.

Die Kopfzeilen sind so klein, dass es nicht schwierig ist, auftretende Probleme zu lösen. Alle schwierigen Probleme werden behoben, indem ein oder zwei Header neu geschrieben werden. Wenn Sie sich Gedanken über Ihre Header-Strategie machen, lösen Sie Probleme, die es nicht gibt.


Fall 2: Sie haben Dutzende von Klassen. Einige der Klassen stellen das Rückgrat Ihres Programms dar, und das Umschreiben ihrer Header würde Sie dazu zwingen, einen großen Teil Ihrer Codebasis neu zu schreiben / neu zu kompilieren. Andere Klassen verwenden dieses Rückgrat, um Dinge zu erreichen. Dies ist ein typisches Geschäftsumfeld. Überschriften sind über Verzeichnisse verteilt und Sie können sich nicht realistisch an die Namen von allem erinnern.

Lösung: An diesem Punkt müssen Sie sich Ihre Klassen in logischen Gruppen vorstellen und diese Gruppen in Überschriften zusammenfassen, die Sie davon abhalten, immer wieder neu zu beginnen #include. Dies vereinfacht nicht nur das Leben, sondern ist auch ein notwendiger Schritt, um vorkompilierte Header zu nutzen .

Sie beenden den #includeUnterricht, den Sie nicht brauchen, aber wen interessiert das ?

In diesem Fall würde Ihr Code so aussehen ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}
QuestionC
quelle
13
Ich musste dies -1, weil ich ehrlich glaube, dass jeder Satz, der in der Form "Gute Praxis ist, sich keine Sorgen um Ihre ____ Strategie zu machen, solange er kompiliert", zu einem schlechten Urteil führt. Ich habe festgestellt, dass der Ansatz sehr schnell zu Unlesbarkeit führt, und Unlesbarkeit ist FAST so schlimm wie "funktioniert nicht". Ich habe auch viele große Bibliotheken gefunden, die mit den von Ihnen beschriebenen Ergebnissen beider Fälle nicht übereinstimmen. Beispiel: Boost DOES erstellt die von Ihnen in Fall 2 empfohlenen "Collections" -Header, macht jedoch auch die Bereitstellung von Class-by-Class-Headern für den Fall, dass Sie sie benötigen, zu einem großen Teil zum Erfolg.
Cort Ammon
3
Ich habe persönlich miterlebt, dass "Keine Sorge, wenn es kompiliert" sich in "unsere Anwendung umwandelt". Die Kompilierung dauert 30 Minuten, wenn Sie einer Aufzählung einen Wert hinzufügen. Wie zum Teufel können wir das beheben? "
Gort the Robot
Ich habe das Problem der Kompilierzeit in meiner Antwort angesprochen. Tatsächlich ist meine Antwort eine von nur zwei (von denen keiner gut abschneidet), die dies tun. Aber wirklich, das ist tangential zur Frage von OP; Dies ist ein "Soll ich meine Variablennamen in eine Kamelschachtel schreiben?" Frage eingeben. Mir ist klar, dass meine Antwort unbeliebt ist, aber es gibt nicht immer für alles eine bewährte Methode, und dies ist einer dieser Fälle.
QuestionC
Stimmen Sie mit # 2 überein. In Bezug auf die früheren Ideen - ich hoffe auf Automatisierung, die den lokalen Header-Block aktualisieren würde - befürworte ich bis dahin eine vollständige Liste.
chux
Der Ansatz "Alles einschließen und das Spülbecken" kann Ihnen zunächst einige Zeit sparen - Ihre Header-Dateien sehen möglicherweise sogar kleiner aus (da die meisten Dinge indirekt von ... irgendwoher eingefügt werden). Bis Sie zu dem Punkt kommen, an dem jede Änderung eine Neukompilierung Ihres Projekts von mehr als 30 Minuten bewirkt. Und Ihre IDE-Smart-Autovervollständigung enthält Hunderte irrelevanter Vorschläge. Und Sie verwechseln versehentlich zwei zu ähnlich benannte Klassen oder statische Funktionen. Und Sie fügen eine neue Struktur hinzu, aber dann schlägt der Build fehl, weil Sie irgendwo eine Namespace-Kollision mit einer völlig unabhängigen Klasse haben ...
CharonX