Mon style personnel avec C++ doit toujours mettre les déclarations de classe dans un fichier include et les définitions dans un fichier .cpp, comme indiqué dans la réponse Réponse de Loki à Fichiers d'en-tête C++, Séparation de code = . Certes, ce style est probablement dû en partie à toutes les années passées à coder Modula-2 et Ada, qui ont toutes deux un schéma similaire avec des fichiers de spécification et des fichiers body.
J'ai un collègue, beaucoup plus compétent en C++ que moi, qui insiste pour que toutes les déclarations C++ intègrent, si possible, les définitions directement dans le fichier d'en-tête. Il ne dit pas que c'est un style alternatif valide, ou même un style légèrement meilleur, mais qu'il s'agit plutôt du nouveau style universellement accepté que tout le monde utilise maintenant pour C++.
Je ne suis plus aussi souple qu'avant, alors je ne suis pas vraiment impatient de suivre ce mouvement jusqu'à ce que je voie quelques personnes de plus avec lui. Alors, à quel point cet idiome est-il courant?
Juste pour structurer les réponses: est-il maintenant comme moyen , très commun, plutôt commun, inhabituel ou insensé?
Votre collègue a tort, la méthode habituelle est et a toujours été de mettre du code dans les fichiers .cpp (ou l’extension de votre choix) et des déclarations dans les en-têtes.
Il est parfois intéressant de mettre du code dans l'en-tête, cela peut permettre une intégration plus intelligente du compilateur. Mais en même temps, cela peut détruire vos temps de compilation car tout le code doit être traité chaque fois qu’il est inclus par le compilateur.
Enfin, il est souvent gênant d’avoir des relations d’objets circulaires (parfois souhaitées) lorsque tout le code correspond aux en-têtes.
En bout de ligne, vous aviez raison, il a tort.
EDIT: J'ai réfléchi à votre question. Il y a un cas où ce qu'il dit est vrai. modèles. De nombreuses bibliothèques "modernes" plus récentes, telles que boost, font un usage intensif des modèles et sont souvent "en-tête uniquement". Cependant, cela ne devrait être fait que si vous utilisez des modèles, car c'est le seul moyen de le faire quand vous les utilisez.
EDIT: Certaines personnes souhaiteraient un peu plus de précision, voici quelques réflexions sur les inconvénients de l'écriture de code "en-tête uniquement":
Si vous effectuez une recherche, vous verrez beaucoup de personnes essayer de trouver un moyen de réduire les temps de compilation lorsqu’il s’agit de boost. Par exemple: Comment réduire les temps de compilation avec Boost Asio , qui affiche une compilation en 14 secondes d'un seul fichier 1K avec boost inclus. Les 14s peuvent ne pas sembler "exploser", mais ils durent certainement beaucoup plus longtemps que d'habitude et peuvent s'additionner assez rapidement. Lorsqu'il s'agit d'un grand projet. Les bibliothèques contenant uniquement des en-têtes affectent les temps de compilation de manière tout à fait mesurable. Nous le tolérons parce que le boost est très utile.
De plus, il existe de nombreuses choses qui ne peuvent pas être faites uniquement dans les en-têtes (même les bibliothèques boost que vous devez lier pour certaines parties telles que les threads, le système de fichiers, etc.). Un exemple principal est que vous ne pouvez pas avoir d’objets globaux simples uniquement dans les bibliothèques d’en-tête (sauf si vous avez recours à l’abomination qui est un singleton) car vous rencontrerez plusieurs erreurs de définition. NOTE: Les variables en ligne de C++ 17 rendront cet exemple particulier faisable à l'avenir.
Enfin, lorsque vous utilisez boost comme exemple de code avec en-tête uniquement, vous perdez souvent un détail énorme.
Boost est une bibliothèque, pas un code de niveau utilisateur. donc ça ne change pas souvent. Dans le code utilisateur, si vous mettez tout dans les en-têtes, chaque petit changement vous obligera à recompiler tout le projet. C'est une perte de temps monumentale (et ce n'est pas le cas des bibliothèques qui ne changent pas de compilation en compilation). Lorsque vous divisez des éléments entre en-tête/source et mieux, utilisez des déclarations en aval pour réduire les inclus, vous pouvez économiser des heures de recompilation s’ils sont ajoutés sur une journée.
Le jour où les codeurs C++ s’entendent sur The Way, les agneaux s’allongeront avec les lions, les Palestiniens embrasseront les Israéliens, et les chiens et les chats seront autorisés à se marier.
La séparation entre les fichiers .h et .cpp est pour l’essentiel arbitraire, un vestige d’optimisations du compilateur bien révolues. Pour moi, les déclarations appartiennent à l'en-tête et les définitions appartiennent au fichier d'implémentation. Mais, ce n'est que l'habitude, pas la religion.
Le code dans les en-têtes est généralement une mauvaise idée car il oblige la recompilation de tous les fichiers incluant l'en-tête lorsque vous modifiez le code réel plutôt que les déclarations. Cela ralentira également la compilation car vous devrez analyser le code de chaque fichier contenant l'en-tête.
Une raison d'avoir du code dans les fichiers d'en-tête est qu'il est généralement nécessaire pour que le mot clé inline fonctionne correctement et lors de l'utilisation de modèles instanciés dans d'autres fichiers cpp.
Ce qui pourrait peut-être informer votre collègue est une notion selon laquelle la plupart du code C++ devrait être modélisé pour permettre une utilisation maximale. Et si elle est basée sur un modèle, tout devra être dans un fichier d’en-tête, de sorte que le code client puisse le visualiser et l’instancier. Si c'est assez bon pour Boost et la STL, c'est assez bien pour nous.
Je ne suis pas d'accord avec ce point de vue, mais c'est peut-être d'où ça vient.
Je pense que votre collègue est intelligent et que vous avez également raison.
Les choses utiles que j'ai trouvées et qui mettent tout dans les en-têtes sont les suivantes:
Pas besoin d'écrire et de synchroniser les en-têtes et les sources.
La structure est simple et aucune dépendance circulaire ne force le codeur à créer une "meilleure" structure.
Portable, facile à intégrer à un nouveau projet.
Je suis d'accord avec le problème du temps de compilation, mais je pense que nous devrions remarquer que:
Les modifications du fichier source risquent fort de modifier les fichiers d’en-tête, ce qui entraîne la recompilation de l’ensemble du projet.
La vitesse de compilation est beaucoup plus rapide qu'auparavant. Et si vous avez un projet à construire avec une longue durée et une fréquence élevée, cela peut indiquer que la conception de votre projet présente des défauts. Séparer les tâches dans différents projets et modules peut éviter ce problème.
Enfin, je veux juste soutenir votre collègue, juste de mon point de vue personnel.
Souvent, je mets des fonctions membres triviales dans le fichier d'en-tête pour leur permettre d'être en ligne. Mais pour y mettre tout le corps de code, juste pour être cohérent avec les modèles? C'est des noix simples.
Rappelez-vous: ne consistance idiote est le hobgoblin des petits esprits .
Comme Tuomas l'a dit, votre entête devrait être minimale. Pour être complet, je vais développer un peu.
J'utilise personnellement 4 types de fichiers dans mon C++
projets:
De plus, je couple ceci avec une autre règle: Ne définissez pas ce que vous pouvez transmettre, déclarez. Bien sûr, je suis raisonnable là-dessus (utiliser Pimpl partout est assez compliqué).
Cela signifie que je préfère une déclaration avancée à un #include
directive dans mes en-têtes chaque fois que je peux m'en tirer.
Enfin, j'utilise également une règle de visibilité: je limite autant que possible les portées de mes symboles afin qu'elles ne polluent pas les portées extérieures.
En résumé:
// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;
// example.hpp
#include "project/example_fwd.hpp"
// Those can't really be skipped
#include <string>
#include <vector>
#include "project/pimpl.hpp"
// Those can be forward declared easily
#include "project/foo_fwd.hpp"
namespace project { class Bar; }
namespace project
{
class MyClass
{
public:
struct Color // Limiting scope of enum
{
enum type { Red, Orange, Green };
};
typedef Color::type Color_t;
public:
MyClass(); // because of pimpl, I need to define the constructor
private:
struct Impl;
pimpl<Impl> mImpl; // I won't describe pimpl here :p
};
template <class T> class MyClassT: public MyClass {};
} // namespace project
// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"
template <class T> void check(MyClass<T> const& c) { }
// example.cpp
#include "example_impl.hpp"
// MyClass definition
La bouée de sauvetage ici est que la plupart du temps, l'en-tête forward est inutile: nécessaire uniquement dans le cas de typedef
ou template
, de même que l'en-tête d'implémentation;)
Généralement, lors de l'écriture d'une nouvelle classe, je mettrai tout le code dans la classe, ainsi je n'aurai pas besoin de chercher dans un autre fichier. Après que tout fonctionne, je décompose le corps des méthodes dans le fichier cpp. en laissant les prototypes dans le fichier hpp.
Pour ajouter plus d'amusement, vous pouvez ajouter .ipp
fichiers contenant l’implémentation du modèle (qui est inclus dans .hpp
), tandis que .hpp
contient l'interface.
En dehors du code modélisé (selon le projet, il peut s'agir d'une majorité ou d'une minorité de fichiers), il existe code normal et ici, il est préférable de séparer les déclarations et les définitions. Fournissez également des déclarations en aval si nécessaire - cela peut avoir une incidence sur le moment de la compilation.
Personnellement, je le fais dans mes fichiers d'en-tête:
// class-declaration
// inline-method-declarations
Je n'aime pas mélanger le code pour les méthodes avec la classe car je trouve pénible de regarder les choses rapidement.
Je ne mettrais pas TOUTES les méthodes dans le fichier d'en-tête. Le compilateur ne pourra (normalement) pas utiliser les méthodes virtuelles en ligne et ne fera (probablement) que mettre en place de petites méthodes sans boucles (dépend totalement du compilateur).
Faire les méthodes dans la classe est valide ... mais du point de vue de la lisibilité, je ne l'aime pas. Si vous mettez les méthodes dans l'en-tête, cela signifie que, si possible, elles seront alignées.
Si cette nouvelle voie est vraiment la voie , nous aurions peut-être été dans une direction différente dans nos projets.
Parce que nous essayons d'éviter toutes les choses inutiles dans les en-têtes. Cela inclut d'éviter la cascade d'en-tête. Le code dans les en-têtes nécessitera probablement l'inclusion d'un autre en-tête, ce qui nécessitera un autre en-tête, etc. Si nous sommes obligés d'utiliser des modèles, nous essayons d'éviter de surcharger l'en-tête avec des éléments de modèles.
Nous utilisons également "opaque pointer" -pattern lorsque applicable.
Avec ces pratiques, nous pouvons construire plus rapidement que la plupart de nos pairs. Et oui ... changer le code ou les membres de la classe ne provoquera pas de grandes reconstructions.
Je pense qu'il est absolument absurde de mettre TOUTES les définitions de vos fonctions dans le fichier d'en-tête. Pourquoi? Parce que le fichier d'en-tête est utilisé comme interface PUBLIC avec votre classe. C'est l'extérieur de la "boîte noire".
Lorsque vous devez consulter une classe pour savoir comment l'utiliser, vous devez consulter le fichier d'en-tête. Le fichier d'en-tête devrait donner une liste de ce qu'il peut faire (commenté pour décrire en détail comment utiliser chaque fonction), et devrait inclure une liste des variables membres. Il NE DEVRAIT PAS inclure la manière dont chaque fonction est mise en œuvre, car il s'agit d'un chargement d'informations inutiles et ne fait qu'encombrer le fichier d'en-tête.
Je mets toute l'implémentation hors de la définition de classe. Je veux avoir les commentaires doxygen hors de la définition de classe.
À mon humble avis, il n'a de mérite que s'il utilise des modèles et/ou des métaprogrammations. Nous avons déjà mentionné de nombreuses raisons pour lesquelles vous limitez les fichiers d'en-tête à de simples déclarations. Ils sont juste que ... les en-têtes. Si vous souhaitez inclure du code, vous le compilez en tant que bibliothèque et vous le liez.
Cela ne dépend-il pas vraiment de la complexité du système et des conventions internes?
En ce moment, je travaille sur un simulateur de réseau de neurones incroyablement complexe, et le style accepté que je devrais utiliser est le suivant:
Définitions de classe dans classname.h
Code de classe dans classnameCode.h
code exécutable dans classname.cpp
Cela divise les simulations créées par l'utilisateur à partir des classes de base créées par le développeur et fonctionne mieux dans cette situation.
Cependant, je serais surpris de voir des gens faire cela dans, par exemple, une application graphique ou toute autre application dont le but n'est pas de fournir aux utilisateurs un code de base.
Je pense que votre collègue a raison tant qu'il n'entre pas dans le processus pour écrire du code exécutable dans l'en-tête. Je pense que le bon équilibre consiste à suivre le chemin indiqué par GNAT Ada, où le fichier .ads donne une définition d'interface parfaitement adéquate du paquet pour ses utilisateurs et pour ses enfants.
À propos, Ted, avez-vous jeté un coup d'oeil sur ce forum à la question récente sur la liaison d'Ada à la bibliothèque CLIPS que vous avez écrite il y a plusieurs années et qui n'est plus disponible (les pages Web pertinentes sont maintenant fermées). Même si cette version est destinée à une ancienne version de Clips, cette liaison pourrait constituer un bon exemple de départ pour une personne souhaitant utiliser le moteur d'inférence CLIPS dans un programme Ada 2012.
Le code de modèle doit être dans les en-têtes uniquement. En dehors de cela, toutes les définitions, à l'exception des inlines, doivent être en .cpp. Le meilleur argument serait les implémentations de la bibliothèque std qui suivent la même règle. Vous ne voudriez pas que les développeurs de std lib aient raison à ce sujet.