Est-ce une bonne pratique de compter sur les en-têtes inclus de manière transitoire?

38

Je nettoie les inclus dans un projet C ++ sur lequel je travaille et je me demande si je devrais ou non inclure explicitement tous les en-têtes utilisés directement dans un fichier particulier, ou si je devrais inclure uniquement le strict minimum.

Voici un exemple Entity.hpp:

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

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

(Supposons qu'une déclaration avant pour RenderObjectn'est pas une option.)

Maintenant, je sais que cela RenderObject.hppinclut Texture.hpp- je le sais parce que chacun RenderObjecta un Texturemembre. Néanmoins, j’ai explicitement inclus Texture.hppdans Entity.hpp, car je ne sais pas si c’est une bonne idée de s’en remettre à cela RenderObject.hpp.

Donc: est-ce une bonne pratique ou pas?

futlib
la source
19
Où sont les gardes d'inclusion dans votre exemple? Vous venez de les oublier accidentellement, j'espère?
Doc Brown
3
Un problème qui se produit lorsque vous n'incluez pas tous les fichiers utilisés est que l'ordre dans lequel vous incluez les fichiers devient parfois important. C'est vraiment énervant dans le seul cas où cela se produit, mais cela fait parfois boule de neige et vous finissez vraiment par souhaiter que la personne qui a écrit le code comme celui-ci soit défilée devant un peloton d'exécution.
Dunk
C'est pourquoi il y en a #ifndef _RENDER_H #define _RENDER_H ... #endif.
Sampathsris
@Dunk, je pense que vous avez mal compris le problème. Avec l'une de ses suggestions, cela ne devrait pas arriver.
Mooing Duck
1
@DocBrown, #pragma oncerègle-le, non?
Pacerier

Réponses:

65

Vous devez toujours inclure tous les en-têtes définissant les objets utilisés dans un fichier .cpp dans ce fichier, indépendamment de ce que vous savez sur le contenu de ces fichiers. Vous devez inclure des gardes dans tous les fichiers d'en-tête pour vous assurer que l'inclusion d'en-têtes plusieurs fois n'a pas d'importance.

Les raisons:

  • Cela montre clairement aux développeurs qui lisent le source exactement ce que le fichier source en question requiert. Ici, quelqu'un qui regarde les premières lignes du fichier peut voir que vous avez affaire à des Textureobjets de ce fichier.
  • Cela évite les problèmes pour lesquels les en-têtes refactorisés causent des problèmes de compilation lorsqu'ils ne nécessitent plus eux-mêmes d'en-têtes particuliers. Par exemple, supposons que vous réalisiez que vous n’avez RenderObject.hpppas besoin de vous- Texture.hppmême.

Un corollaire est que vous ne devriez jamais inclure un en-tête dans un autre en-tête, sauf si cela est explicitement requis dans ce fichier.

Gort le robot
la source
10
D'accord avec le corollaire - à la condition qu'il devrait TOUJOURS inclure l'autre en-tête s'il en a besoin!
Andrew
1
Je n'aime pas la pratique consistant à inclure directement des en-têtes pour toutes les classes. Je suis en faveur des en-têtes cumulatifs. C'est-à-dire que je pense que le fichier de haut niveau devrait faire référence à un "module" du type qu'il utilise, mais ne doit pas nécessairement inclure directement toutes les parties individuelles.
EdA-qa mort-ora-y
8
Cela conduit à de grands en-têtes monolithiques que chaque fichier inclut, même lorsqu'ils ne requièrent qu'un petit peu de ce qu'il contient, ce qui entraîne de longs temps de compilation et rend le refactoring plus difficile.
Gort le robot
6
Google a mis au point un outil pour l'aider à appliquer précisément ce conseil, appelé inclure-ce-que-vous utilisez .
Matthew G.
3
Le principal problème de temps de compilation avec les grands en-têtes monolithiques n’est pas le moment de compiler le code d’en-tête mais le compilateur de tous les fichiers cpp de votre application à chaque changement d’en-tête. Les en-têtes précompilés n’aident pas cela.
Gort the Robot
23

La règle générale est la suivante: incluez ce que vous utilisez. Si vous utilisez directement un objet, incluez directement son fichier d'en-tête. Si vous utilisez un objet A qui utilise B mais n'utilisez pas B vous-même, incluez seulement Ah

De plus, pendant que nous sommes sur le sujet, vous ne devriez inclure d'autres fichiers d'en-tête dans votre fichier d'en-tête que si vous en avez réellement besoin dans l'en-tête. Si vous n'en avez besoin que dans le fichier .cpp, incluez-le uniquement ici. C'est la différence entre une dépendance publique et une dépendance privée. Cela empêchera les utilisateurs de votre classe d'insérer des en-têtes dont ils n'ont pas vraiment besoin.

Nir Friedman
la source
10

Je me demande toujours si je devrais ou non inclure explicitement tous les en-têtes utilisés directement dans un fichier particulier

Oui.

Vous ne savez jamais quand ces autres en-têtes pourraient changer. Il est logique d’inclure dans chaque unité de traduction les en-têtes dont vous savez qu’elle a besoin.

Nous avons des gardes en-tête pour nous assurer que la double inclusion n'est pas préjudiciable.

Courses de légèreté avec Monica
la source
3

Les opinions diffèrent sur ce point, mais je suis d’avis que chaque fichier (qu’il s’agisse d’un fichier source c / cpp ou d’un fichier d’en-tête h / hpp) devrait pouvoir être compilé ou analysé seul.

En tant que tels, tous les fichiers doivent # inclure tous les fichiers d’en-tête dont ils ont besoin - vous ne devez pas supposer qu’un seul fichier d’en-tête a déjà été inclus auparavant.

C'est vraiment pénible de devoir ajouter un fichier d'en-tête et de constater qu'il utilise un élément défini ailleurs, sans l'inclure directement ... vous devez donc aller chercher (et éventuellement vous retrouver avec le mauvais!)

De l’autre côté, peu importe (en règle générale) si vous incluez un fichier dont vous n’avez pas besoin ...


En tant que style personnel, j’organise les fichiers #include dans l’ordre alphabétique, en système et en application, ce qui renforce le message "autonome et entièrement cohérent".

Andrew
la source
Remarque sur l'ordre des inclusions: l'ordre est parfois important, par exemple lors de l'inclusion d'en-têtes X11. Cela peut être dû à la conception (qui dans ce cas pourrait être considéré comme une mauvaise conception), parfois en raison de problèmes d'incompatibilité malheureux.
Hyde
Remarque concernant l’inclusion d’en-têtes inutiles, il importe de prendre en compte les temps de compilation, tout d’abord directement (surtout s’il s’agit de C ++ lourdement orienté modèles), mais surtout lorsqu’il inclut des en-têtes du même projet ou un projet de dépendance où le fichier d’inclusion change également et déclenche la recompilation des fichiers. tout y compris (si vous avez des dépendances de travail, si vous ne le faites pas, vous devez être en construction propre tout le temps ...).
Hyde
2

Cela dépend si l'inclusion transitive est par nécessité (par exemple, classe de base) ou en raison d'un détail d'implémentation (membre privé).

Pour clarifier, l'inclusion transitive est nécessaire lorsque la suppression est possible uniquement après avoir modifié les interfaces déclarées dans l'en-tête intermédiaire. Comme c'est déjà un changement radical, tout fichier .cpp qui l'utilise doit quand même être vérifié.

Exemple: Ah est inclus par Bh qui est utilisé par C.cpp. Si Bh a utilisé Ah pour certains détails d'implémentation, C.cpp ne devrait pas présumer que Bh continuera à le faire. Mais si Bh utilise Ah pour une classe de base, C.cpp peut alors supposer que Bh continuera d'inclure les en-têtes appropriés pour ses classes de base.

Vous voyez ici l'avantage réel de ne PAS dupliquer les inclusions d'en-tête. Dites que la classe de base utilisée par Bh n’appartient vraiment pas à Ah et qu’elle est refactorisée dans Bh même. Bh est maintenant un en-tête autonome. Si C.cpp a redondé Ah, il inclut maintenant un en-tête inutile.

MSalters
la source
2

Il peut y avoir un autre cas: vous avez Ah, Bh et votre C.cpp, Bh comprend Ah

donc dans C.cpp, vous pouvez écrire

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

Donc, si vous n'écrivez pas #include "Ah" ici, que peut-il se passer? dans votre C.cpp, A et B (par exemple, classe) sont utilisés. Plus tard, vous avez changé votre code C.cpp, supprimez les éléments liés à B, mais en laissant Bh inclus.

Si vous incluez à la fois Ah et Bh, les outils permettant de détecter les inclusions inutiles peuvent vous aider à indiquer que l'inclusion de Bh n'est plus nécessaire. Si vous n'incluez que Bh comme ci-dessus, il est difficile pour les outils / humains de détecter l'inclusion inutile après le changement de code.

roi des insectes
la source
1

Je prends une approche similaire légèrement différente des réponses proposées.

Dans les en-têtes, incluez toujours juste un minimum, juste ce qui est nécessaire pour que la compilation passe. Utilisez la déclaration anticipée chaque fois que possible.

Dans les fichiers source, peu importe combien vous incluez. Mes préférences sont toujours d'inclure le minimum pour le faire passer.

Pour les petits projets, inclure les en-têtes ici et là ne fera aucune différence. Mais pour les projets de moyenne à grande envergure, cela peut devenir un problème. Même si le matériel le plus récent est utilisé pour la compilation, la différence peut être perceptible. La raison en est que le compilateur doit toujours ouvrir l'en-tête inclus et l'analyser. Donc, pour optimiser la construction, appliquez la technique ci-dessus (incluez le strict minimum et utilisez forward, déclarez).

Bien que légèrement obsolète, la conception logicielle C ++ à grande échelle (par John Lakos) explique tout cela en détail.

BЈовић
la source
1
En désaccord avec cette stratégie ... si vous incluez un fichier d'en-tête dans un fichier source, vous devez alors rechercher toutes ses dépendances. Il vaut mieux inclure directement que d'essayer de documenter la liste!
Andrew
@Andrew, il existe des outils et des scripts pour vérifier ce qui est inclus et combien de fois.
Octobre
1
J'ai remarqué une optimisation sur certains des derniers compilateurs pour gérer cela. Ils reconnaissent une déclaration de garde typique et la traitent. Puis, en y incluant à nouveau, ils peuvent optimiser le chargement du fichier complètement. Cependant, votre recommandation de déclarations anticipées est très sage pour réduire le nombre d'inclusions. Une fois que vous commencez à utiliser les déclarations anticipées, cela devient un équilibre entre le temps d'exécution du compilateur (amélioré par les déclarations anticipées) et la convivialité (amélioré par des ajouts supplémentaires pratiques), qui correspond à un équilibre défini différemment par chaque entreprise.
Cort Ammon - Rétablir Monica
1
@CortAmmon Un en-tête typique inclut des gardes, mais le compilateur doit toujours l'ouvrir, ce qui est une opération lente
BЈовић
4
@ BЈовић: En fait, ils ne le font pas. Tout ce qu'ils ont à faire est de reconnaître que le fichier a des gardes d'en-tête "typiques" et de les signaler afin qu'il ne l'ouvre qu'une fois. Gcc, par exemple, dispose d'une documentation concernant le moment et où elle applique cette optimisation: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Ammon Cort - Réintégrer Monica
-4

Une bonne pratique consiste à ne pas s'inquiéter de votre stratégie d'en-tête tant qu'elle est compilée.

La section d'en-tête de votre code est juste un bloc de lignes que personne ne devrait même regarder jusqu'à ce que vous obteniez une erreur de compilation facile à résoudre. Je comprends le désir d'un style "correct", mais aucune des deux ne peut vraiment être décrite comme correcte. L'inclusion d'un en-tête pour chaque classe est plus susceptible de provoquer des erreurs de compilation gênantes basées sur les commandes, mais ces erreurs de compilation reflètent également des problèmes qu'un codage soigneux pourrait résoudre (même si on peut dire qu'ils ne valent pas la peine d'être résolus).

Et oui, vous aurez ces problèmes liés aux ordres une fois que vous commencerez à vous immerger friend.

Vous pouvez penser au problème dans deux cas.


Cas 1: Vous avez un petit nombre de classes en interaction, disons moins d'une douzaine. Vous ajoutez régulièrement, supprimez et modifiez ces en-têtes de manière à affecter leurs dépendances les unes par rapport aux autres. C'est le cas suggéré par votre exemple de code.

L'ensemble des en-têtes est suffisamment petit pour qu'il ne soit pas compliqué de résoudre les problèmes éventuels. Tous les problèmes difficiles sont résolus en réécrivant un ou deux en-têtes. S'inquiéter de votre stratégie d'en-tête, c'est résoudre des problèmes qui n'existent pas.


Cas 2: Vous avez des dizaines de cours. Certaines classes représentent l’épine dorsale de votre programme, et réécrire leurs en-têtes vous obligerait à réécrire / recompiler une grande partie de votre base de code. D'autres classes utilisent cette colonne vertébrale pour accomplir des choses. Cela représente un contexte commercial typique. Les en-têtes sont répartis sur des répertoires et vous ne pouvez pas vous rappeler de manière réaliste les noms de tout.

Solution: À ce stade, vous devez penser à vos classes dans des groupes logiques et regrouper ces groupes dans des en-têtes qui vous évitent d’être obligés de vous #includerépéter. Cela ne simplifie pas la vie, mais constitue également une étape nécessaire pour tirer parti des en- têtes précompilés .

Vous finissez par avoir des #includecours dont vous n'avez pas besoin, mais qui s'en soucie ?

Dans ce cas, votre code ressemblerait à ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}
QuestionC
la source
13
Je devais le faire parce que je crois sincèrement que toute phrase qui se présente sous la forme "La bonne pratique, c'est de ne pas s'inquiéter de votre stratégie de ____ tant qu'elle compile" conduit les gens à un mauvais jugement. J'ai trouvé que cette approche conduit très rapidement à l'illisibilité, et l'illisibilité est PRESQUE aussi mauvaise que "ne fonctionne pas". J'ai également trouvé de nombreuses bibliothèques importantes qui ne sont pas d'accord avec les résultats des deux cas que vous décrivez. Par exemple, Boost ne fait que les en-têtes de "collections" que vous recommandez dans le deuxième cas, mais cela facilite également la tâche de fournir des en-têtes classe par classe lorsque vous en avez besoin.
Cort Ammon - Réintégrer Monica
3
J'ai personnellement assisté à la transformation de "ne vous inquiétez pas si elle compile" en "notre application prend 30 minutes à compiler lorsque vous ajoutez une valeur à une enum, comment diable pouvons-nous résoudre ce problème!?"
Gort le robot
J'ai abordé la question du temps de compilation dans ma réponse. En fait, ma réponse est l’un des deux seuls (qui n’ont pas tous deux obtenu de bons résultats). Mais, en réalité, cela est inhérent à la question de OP; c'est un "Dois-je cameler les noms de mes variables?" tapez une question. Je me rends compte que ma réponse est impopulaire, mais il n'y a pas toujours une meilleure pratique pour tout, et c'est l'un de ces cas.
QuestionC
D'accord avec # 2. En ce qui concerne les idées précédentes - j'espère une automatisation permettant de mettre à jour le bloc d'en-tête local - je préconisais jusque-là une liste complète.
Chux
L’approche «tout inclure et l’évier de la cuisine» peut vous faire gagner un peu de temps au départ - vos fichiers d’en-tête peuvent même paraître plus petits (puisque la plupart des éléments sont inclus indirectement à partir de… quelque part). Jusqu'à ce que vous arriviez au point où tout changement, quelque soit le lieu, provoque une reconstruction de votre projet de plus de 30 minutes. Et votre autocomplete IDE-smart apporte des centaines de suggestions non pertinentes. Et vous mélangez accidentellement deux classes ou fonctions statiques trop similaires. Et vous ajoutez une nouvelle structure mais la construction échoue parce que vous avez une collision d'espace de noms avec une classe totalement indépendante quelque part ...
CharonX