Je nettoie les inclusions dans un projet C++ sur lequel je travaille, et je me demande toujours si je dois explicitement inclure tous les en-têtes utilisés directement dans un fichier particulier, ou si je ne dois inclure que 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 directe pour RenderObject
ne soit pas une option.)
Maintenant, je sais que RenderObject.hpp
comprend Texture.hpp
- Je le sais car chaque RenderObject
a un membre Texture
. Pourtant, j'inclus explicitement Texture.hpp
dans Entity.hpp
, car je ne sais pas si c'est une bonne idée de compter sur son inclusion dans RenderObject.hpp
.
Alors: est-ce une bonne pratique ou non?
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:
Texture
dans ce fichier.RenderObject.hpp
n'a pas vraiment besoin de Texture.hpp
lui-même.Un corollaire est que vous ne devez jamais inclure un en-tête dans un autre en-tête à moins qu'il ne soit explicitement nécessaire dans ce fichier.
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 uniquement A.h.
De plus, pendant que nous sommes sur le sujet, vous ne devez 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 en avez seulement besoin dans le .cpp, alors ne l'incluez que là: c'est la différence entre une dépendance publique et privée, et empêchera les utilisateurs de votre classe de glisser des en-têtes dont ils n'ont pas vraiment besoin.
Je continue à me demander si je dois 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 tout à fait logique dans le monde d'inclure, dans chaque unité de traduction, les en-têtes dont vous avez besoin pour l'unité de traduction.
Nous avons des protecteurs d'en-tête pour nous assurer que la double inclusion n'est pas nuisible.
Les opinions divergent 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 tel, tous les fichiers doivent #inclure tous les fichiers d'en-tête dont ils ont besoin - vous ne devez pas supposer qu'un fichier d'en-tête a déjà été inclus précédemment.
C'est vraiment pénible si vous devez ajouter un fichier d'en-tête et découvrir qu'il utilise un élément qui est défini ailleurs, sans l'inclure directement ... vous devez donc aller chercher (et éventuellement vous retrouver avec le mauvais!)
D'un autre côté, peu importe (en règle générale) si vous incluez un fichier dont vous n'avez pas besoin ...
Dans un souci de style personnel, j'organise les fichiers #include par ordre alphabétique, divisés en système et application - cela contribue à renforcer le message "autonome et totalement cohérent".
Il peut y avoir un autre cas: vous avez A.h, B.h et votre C.cpp, B.h inclut A.h
donc en 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 "A.h" ici, que peut-il se passer? dans votre C.cpp, A et B (par exemple la classe) sont utilisés. Plus tard, vous avez changé votre code C.cpp, supprimez les éléments liés à B, mais en laissant B.h inclus.
Si vous incluez à la fois A.h et B.h et qu'à ce stade, les outils qui détectent les inclusions inutiles peuvent vous aider à indiquer que l'inclusion de B.h n'est plus nécessaire. Si vous incluez uniquement B.h comme ci-dessus, il est difficile pour les outils/humains de détecter l'inclusion inutile après le changement de code.
Cela dépend si cette inclusion transitive est par nécessité (par exemple la 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 supprimer ne peut être effectuée qu'après avoir d'abord modifié les interfaces déclarées dans l'en-tête intermédiaire. Comme il s'agit déjà d'un changement de rupture, tout fichier .cpp l'utilisant doit être vérifié de toute façon.
Exemple: A.h est inclus par B.h qui est utilisé par C.cpp. Si B.h a utilisé A.h pour certains détails d'implémentation, alors C.cpp ne doit pas supposer que B.h continuera à le faire. Mais si B.h utilise A.h pour une classe de base, alors C.cpp peut supposer que B.h 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. Supposons que la classe de base utilisée par B.h n'appartienne vraiment pas à A.h et qu'elle soit refactorisée dans B.h elle-même. B.h est maintenant un en-tête autonome. Si C.cpp incluait redondamment A.h, il inclut désormais un en-tête inutile.
J'adopte une approche similaire légèrement différente des réponses proposées.
Dans les en-têtes, incluez toujours juste un strict minimum, juste ce qui est nécessaire pour faire passer la compilation. Utilisez la déclaration directe dans la mesure du possible.
Dans les fichiers source, le montant que vous incluez n'est pas très important. Mes préférences sont encore d'inclure un minimum pour le faire passer.
Pour les petits projets, y compris les en-têtes ici et là ne feront aucune différence. Mais pour les projets de moyenne à grande envergure, cela peut devenir un problème. Même si le dernier matériel est utilisé pour la compilation, la différence peut être notable. 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 la déclaration directe).
Bien qu'un peu dépassé, Conception logicielle C++ à grande échelle (par John Lakos) explique tout cela en détail.