Est-ce une bonne pratique de placer des définitions C ++ dans des fichiers d'en-tête?

196

Mon style personnel avec C ++ a toujours mis les déclarations de classe dans un fichier include et les définitions dans un fichier .cpp, tout comme stipulé dans la réponse de Loki aux fichiers d'en-tête C ++, Séparation de code . Certes, une partie de la raison pour laquelle j'aime ce style a probablement à voir avec toutes les années que j'ai passées à coder Modula-2 et Ada, qui ont toutes deux un schéma similaire avec des fichiers de spécifications et des fichiers de corps.

J'ai un collègue, beaucoup plus compétent en C ++ que moi, qui insiste pour que toutes les déclarations C ++ devraient, si possible, inclure les définitions juste là 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 c'est plutôt le nouveau style universellement accepté que tout le monde utilise maintenant pour C ++.

Je ne suis pas aussi souple qu'avant, donc je ne suis pas vraiment impatient de grimper dans son train jusqu'à ce que je voie quelques autres personnes là-haut avec lui. Alors, à quel point cet idiome est-il vraiment courant?

Juste pour donner une certaine structure aux réponses: est-ce maintenant The Way , très commun, quelque peu courant, peu commun ou fou?

TED
la source
Les fonctions d'une ligne (getters et setters) dans l'en-tête sont courantes. Plus long que ne le ferait un second regard interrogateur. Peut-être pour la définition complète d'une petite classe qui n'est utilisée que par une autre dans le même en-tête?
Martin Beckett
jusqu'à présent, j'ai toujours mis toutes mes définitions de classe dans les en-têtes. seules les définitions des classes pimpl font exception. je les déclare uniquement dans les en-têtes.
Johannes Schaub - litb
3
Peut-être qu'il pense que c'est le chemin parce que c'est ainsi que Visual C ++ insiste pour que le code soit écrit. Lorsque vous cliquez sur un bouton, l'implémentation est générée dans le fichier d'en-tête. Je ne sais pas pourquoi Microsoft encouragerait cela, pour les raisons que d'autres ont expliquées ci-dessous.
WKS
4
@WKS - Microsoft préfère que tout le monde programme en C #, et en C #, il n'y a pas de distinction "en-tête" vs "corps", c'est juste un fichier. Ayant été dans les mondes C ++ et C # depuis longtemps maintenant, la manière C # est en fait beaucoup plus facile à gérer.
Mark Lakata
1
@MarkLakata - C'est en effet l'une des choses qu'il a pointées du doigt. Je n'ai pas entendu cet argument de lui récemment, mais l'IIRC faisait valoir que Java et C # fonctionnaient de cette façon, et C # était tout nouveau à l'époque, ce qui en faisait une tendance que tous les langages suivront bientôt
TED

Réponses:

209

Votre collègue a tort, la façon la plus courante est et a toujours été de mettre du code dans des fichiers .cpp (ou n'importe quelle extension que vous aimez) et des déclarations dans les en-têtes.

Il est parfois utile de mettre du code dans l'en-tête, cela peut permettre une compilation plus intelligente par le 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 ennuyeux d'avoir des relations d'objets circulaires (parfois souhaitées) lorsque tout le code est constitué des en-têtes.

En bout de ligne, vous aviez raison, il a tort.

EDIT: J'ai pensé à 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 doit être fait que lorsque vous utilisez des modèles, car c'est la seule façon de le faire lorsque vous les utilisez.

EDIT: Certaines personnes aimeraient un peu plus de clarification, voici quelques réflexions sur les inconvénients de l'écriture de code "en-tête uniquement":

Si vous effectuez une recherche autour de vous, vous verrez beaucoup de gens essayer de trouver un moyen de réduire les temps de compilation en cas de boost. Par exemple: Comment réduire les temps de compilation avec Boost Asio , qui voit une compilation en 14 secondes d'un seul fichier 1K avec boost inclus. Les 14 ne semblent pas "exploser", mais c'est certainement beaucoup plus long que la normale et peuvent s'additionner assez rapidement. Lorsqu'il s'agit d'un grand projet. Les bibliothèques d'en-tête uniquement affectent les temps de compilation d'une manière tout à fait mesurable. Nous le tolérons simplement parce que le boost est si utile.

De plus, il y a beaucoup de choses qui ne peuvent pas être faites uniquement dans les en-têtes (même boost a des bibliothèques 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 des objets globaux simples dans les bibliothèques d'en-tête uniquement (sauf si vous avez recours à l'abomination qui est un singleton) car vous rencontrerez plusieurs erreurs de définition. REMARQUE: les variables en ligne de C ++ 17 rendront cet exemple particulier réalisable à l'avenir.

Enfin, lorsque vous utilisez boost comme exemple de code d'en-tête uniquement, un détail énorme est souvent oublié.

Boost est une bibliothèque, pas un code de niveau utilisateur. donc ça ne change pas si souvent. Dans le code utilisateur, si vous mettez tout dans les en-têtes, chaque petite modification vous obligera à recompiler le projet entier. C'est une perte de temps monumentale (et ce n'est pas le cas pour les bibliothèques qui ne changent pas de compilation en compilation). Lorsque vous divisez des éléments entre en-tête / source et mieux encore, utilisez des déclarations avancées pour réduire les inclusions, vous pouvez économiser des heures de recompilation lorsqu'elles sont ajoutées sur une journée.

Evan Teran
la source
16
Je suis presque sûr que c'est de là qu'il vient. Chaque fois que cela arrive, il fait apparaître des modèles. Son argument est à peu près que vous devriez faire tout le code de cette façon pour plus de cohérence.
TED le
14
c'est un mauvais argument qu'il fait, respectez vos armes :)
Evan Teran
11
Les définitions de modèle peuvent être dans des fichiers CPP si le mot clé "export" est pris en charge. C'est un coin sombre de C ++ qui n'est généralement même pas implémenté par la plupart des compilations, à ma connaissance.
Andrei Taranchenko le
2
Voir le bas de cette réponse (le haut est quelque peu alambiqué) pour un exemple: stackoverflow.com/questions/555330/…
Evan Teran
3
Il commence à avoir un sens à cette discussion à "Hourra, pas d'erreurs de l'éditeur de liens."
Evan Teran
158

Le jour où les codeurs C ++ s'accordent sur The Way , les agneaux se coucheront avec les lions, les Palestiniens embrasseront les Israéliens et les chats et les chiens seront autorisés à se marier.

La séparation entre les fichiers .h et .cpp est principalement arbitraire à ce stade, vestige des optimisations du compilateur depuis longtemps. À mon avis, les déclarations appartiennent à l'en-tête et les définitions appartiennent au fichier d'implémentation. Mais c'est juste une habitude, pas une religion.

Oui - ce Jake.
la source
140
"Le jour où les codeurs C ++ s'accorderont sur la Voie ..." il ne restera plus qu'un seul codeur C ++!
Brian Ensink
9
Je pensais qu'ils étaient déjà d'accord sur le chemin, les déclarations en .h et les définitions en .cpp
hasen
6
Nous sommes tous des aveugles et C ++ est un éléphant.
Roderick Taylor
habitude? qu'en est-il de l'utilisation de .h pour définir la portée? par quoi a-t-il été remplacé?
Hernán Eche
28

Le code dans les en-têtes est généralement une mauvaise idée car il force la recompilation de tous les fichiers qui incluent 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 dans chaque fichier qui inclut l'en-tête.

Une raison pour 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.

Laserallan
la source
1
"cela force la recompilation de tous les fichiers qui incluent l'en-tête lorsque vous modifiez le code réel plutôt que les déclarations" Je pense que c'est la raison la plus authentique; va également avec le fait que les déclarations dans les en-têtes changent moins fréquemment que l'implémentation dans les fichiers .c.
Ninad
20

Ce qui pourrait vous informer, collègue, est une notion selon laquelle la plupart du code C ++ devrait être basé sur des modèles pour permettre une utilisation maximale. Et s'il est modélisé, alors tout devra être dans un fichier d'en-tête, afin que le code client puisse le voir et l'instancier. Si c'est assez bon pour Boost et la STL, c'est assez bon pour nous.

Je ne suis pas d'accord avec ce point de vue, mais c'est peut-être d'où il vient.

JohnMcG
la source
Je pense que vous avez raison à ce sujet. Quand nous discutons , il est toujours en utilisant l'exemple de modèles, où vous plus ou moins avez à faire. Je suis également en désaccord avec le "devoir", mais mes alternatives sont plutôt compliquées.
TED le
1
@ted - pour le code modèle, vous devez mettre l'implémentation dans l'en-tête. Le mot-clé «export» permet à un compilateur de prendre en charge la séparation de la déclaration et la définition des modèles, mais la prise en charge de l'exportation est pratiquement inexistante. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Michael Burr le
Un en- tête, oui, mais ce n'est pas nécessairement le même en-tête. Voir la réponse de l'inconnu ci-dessous.
TED le
Cela a du sens, mais je ne peux pas dire que j'ai rencontré ce style auparavant.
Michael Burr
14

Je pense que votre collègue est intelligent et vous avez également raison.

Les choses utiles que j'ai trouvées que tout mettre dans les en-têtes est que:

  1. Pas besoin d'écrire et de synchroniser les en-têtes et les sources.

  2. La structure est simple et aucune dépendance circulaire n'oblige le codeur à créer une "meilleure" structure.

  3. 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:

  1. Le changement de fichier source est très susceptible de modifier les fichiers d'en-tête, ce qui entraîne la recompilation de l'ensemble du projet.

  2. La vitesse de compilation est beaucoup plus rapide qu'auparavant. Et si vous avez un projet à construire à long terme et à haute fréquence, cela peut indiquer que la conception de votre projet a des défauts. Séparez les tâches en différents projets et le module peut éviter ce problème.

Enfin, je veux juste soutenir votre collègue, juste à mon avis personnel.

XU Bin
la source
3
+1. Personne mais vous avez eu l'idée que dans un en-tête, seuls les longs temps de compilation du projet peuvent suggérer trop de dépendances, ce qui est une mauvaise conception. Bon point! Mais ces dépendances peuvent-elles être supprimées dans une mesure où le temps de compilation est réellement court?
TobiMcNamobi
@TobiMcNamobi: J'adore l'idée de "relâcher" pour obtenir de meilleurs commentaires sur les mauvaises décisions de conception. Cependant, dans le cas de l'en-tête uniquement vs compilé séparément, si nous nous contentons de cette idée, nous nous retrouvons avec une seule unité de compilation et des temps de compilation énormes. Même lorsque le design est vraiment super.
Jo So
En d'autres termes, la séparation entre l'interface et l'implémentation fait en fait partie de votre conception. En C, vous devez exprimer vos décisions d'encapsulation par séparation dans l'en-tête et la mise en œuvre.
Jo So
1
Je commence à me demander s'il y a des inconvénients à supprimer les en-têtes comme le font les langues modernes.
Jo So
12

Souvent, je mettrai des fonctions membres triviales dans le fichier d'en-tête, pour leur permettre d'être intégrées. Mais y mettre tout le corps du code, juste pour être cohérent avec les modèles? Ce sont des noix simples.

Rappelez-vous: une consistance stupide est le hobgobelin des petits esprits .

Mark Ransom
la source
Ouais, je fais ça aussi. La règle générale que j'utilise semble être quelque chose comme "s'il tient sur une seule ligne de code, laissez-le dans l'en-tête".
TED le
Que se passe-t-il lorsqu'une bibliothèque fournit le corps d'une classe de modèle A<B>dans un fichier cpp et que l'utilisateur souhaite un A<C>?
jww
@jww Je ne l'ai pas indiqué explicitement, mais les classes de modèle doivent être entièrement définies dans les en-têtes afin que le compilateur puisse l'instancier avec tous les types dont il a besoin. C'est une exigence technique, pas un choix stylistique. Je pense que le problème dans la question d'origine est que quelqu'un a décidé si c'était bon pour les modèles, c'était bien aussi pour les classes régulières.
Mark Ransom
7

Comme l'a dit Tuomas, votre en-tête doit être minimal. Pour être complet je développerai un peu.

J'utilise personnellement 4 types de fichiers dans mes C++projets:

  • Publique:
  • En-tête de transfert: dans le cas de modèles, etc., ce fichier obtient les déclarations de transfert qui apparaîtront dans l'en-tête.
  • En-tête: ce fichier comprend l'en-tête de transfert, le cas échéant, et déclare tout ce que je souhaite rendre public (et définit les classes ...)
  • Privé:
  • En-tête privé: ce fichier est un en-tête réservé à l'implémentation, il inclut l'en-tête et déclare les fonctions / structures auxiliaires (pour Pimpl par exemple ou les prédicats). Ignorez si inutile.
  • Fichier source: il inclut l'en-tête privé (ou en-tête s'il n'y a pas d'en-tête privé) et définit tout (non modèle ...)

De plus, je couple ceci avec une autre règle: ne définissez pas ce que vous pouvez transmettre déclarer. Bien sûr, je suis raisonnable là-bas (utiliser Pimpl partout est assez compliqué).

Cela signifie que je préfère une déclaration #include directive dans mes en-têtes chaque fois que je peux m'en sortir.

Enfin, j'utilise également une règle de visibilité: je limite le plus 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 avant est inutile: seulement nécessaire dans le cas de typedefou templateet est donc l'en-tête de mise en œuvre;)

Matthieu M.
la source
6

Pour ajouter plus de plaisir, vous pouvez ajouter des .ippfichiers qui contiennent l'implémentation du modèle (qui est inclus dans .hpp), tandis que .hppcontient l'interface.

Comme en dehors du code modélisé (selon le projet, il peut s'agir d'une majorité ou d'une minorité de fichiers), il existe un code normal et ici, il est préférable de séparer les déclarations et les définitions. Fournissez également des déclarations avancées si nécessaire - cela peut avoir un effet sur le temps de compilation.

Anonyme
la source
C'est aussi ce que j'ai commencé à faire avec les définitions de modèles (même si je ne suis pas sûr d'avoir utilisé la même extension ... cela fait un moment).
TED le
5

Généralement, lors de l'écriture d'une nouvelle classe, je mettrai tout le code dans la classe, donc je n'ai pas besoin de chercher dans un autre fichier. Après que tout fonctionne, je casse le corps des méthodes dans le fichier cpp , laissant les prototypes dans le fichier hpp.

EvilTeach
la source
4

Si cette nouvelle voie est vraiment La Voie , nous aurions peut-être eu une direction différente dans nos projets.

Parce que nous essayons d'éviter toutes les choses inutiles dans les en-têtes. Cela implique d'éviter la cascade d'en-tête. Le code dans les en-têtes aura probablement besoin d'un autre en-tête à inclure, qui aura besoin d'un autre en-tête et ainsi de suite. Si nous sommes obligés d'utiliser des modèles, nous essayons d'éviter de surcharger les en-têtes avec des trucs de modèles.

Nous utilisons également le modèle "pointeur opaque", le cas échéant.

Avec ces pratiques, nous pouvons réaliser des builds plus rapidement que la plupart de nos pairs. Et oui ... changer le code ou les membres de la classe ne provoquera pas de reconstructions énormes.

Virne
la source
4

Personnellement, je fais cela dans mes fichiers d'en-tête:

// class-declaration

// inline-method-declarations

Je n'aime pas mélanger le code des méthodes avec la classe car je trouve difficile de rechercher rapidement les choses.

Je ne mettrais pas TOUTES les méthodes dans le fichier d'en-tête. Le compilateur ne sera (normalement) pas en mesure d'inline les méthodes virtuelles et ne sera (probablement) que les petites méthodes en ligne sans boucles (dépend totalement du compilateur).

Faire les méthodes dans la classe est valide ... mais d'un point de vue de lisibilité, je n'aime pas ça. Mettre les méthodes dans l'en-tête signifie que, si possible, elles seront intégrées.

TofuBeer
la source
2

À mon humble avis, il n'a de mérite que s'il fait des modèles et / ou une métaprogrammation. Il y a de nombreuses raisons déjà mentionnées pour limiter les fichiers d'en-tête aux seules déclarations. Ce ne sont que ... des en-têtes. Si vous souhaitez inclure du code, vous le compilez en tant que bibliothèque et le liez.

spoulson
la source
2

J'ai mis toute l'implémentation hors de la définition de classe. Je veux avoir les commentaires doxygen hors de la définition de classe.

Jesus Fernandez
la source
1
Je sais qu'il est tard, mais les downvoters (ou sympathisants) veulent commenter pourquoi? Cela me semble une déclaration raisonnable. Nous utilisons Doxygen, et le problème s'est certainement posé.
TED
2

Je pense qu'il est absolument absurde de mettre TOUTES vos définitions de fonctions dans le fichier d'en-tête. Pourquoi? Parce que le fichier d'en-tête est utilisé comme interface PUBLIC pour votre classe. C'est l'extérieur de la "boîte noire".

Lorsque vous devez regarder une classe pour savoir comment l'utiliser, vous devez regarder le fichier d'en-tête. Le fichier d'en-tête doit donner une liste de ce qu'il peut faire (commenté pour décrire les détails de l'utilisation de chaque fonction), et il doit inclure une liste des variables membres. Il NE DEVRAIT PAS inclure COMMENT chaque fonction individuelle est implémentée, car c'est une charge d'informations inutiles et ne fait qu'encombrer le fichier d'en-tête.

SeanRamey
la source
1

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 qui est incroyablement complexe, et le style accepté que je suis censé utiliser est:

Définitions de classe dans classname.h
Code de classe dans classnameCode.h
Code exécutable dans classname.cpp

Cela sépare les simulations créées par l'utilisateur des classes de base développées par le développeur et fonctionne mieux dans la situation.

Cependant, je serais surpris de voir des gens faire cela dans, disons, une application graphique ou toute autre application dont le but n'est pas de fournir aux utilisateurs une base de code.

Ed James
la source
1
Quelle est exactement la distinction entre "Code de classe" et "Code exécutable"?
TED
Comme je l'ai dit, c'est un simulateur neuronal: l'utilisateur crée des simulations exécutables qui sont construites sur un grand nombre de classes qui agissent comme des neurones, etc. Notre code est donc simplement des classes qui ne peuvent rien faire par elles-mêmes, et l'utilisateur crée le code exécutable qui fait faire des choses au simulateur.
Ed James
En général, ne pourriez-vous pas dire "ne peut rien faire par lui-même" pour la grande majorité (sinon l'intégralité) de la plupart des programmes? Voulez-vous dire que le code "principal" va dans un cpp, mais rien d'autre ne le fait?
TED le
Dans cette situation, c'est un peu différent. Le code que nous écrivons est essentiellement une bibliothèque, et l'utilisateur construit ses simulations en plus de cela, qui sont en fait exécutables. Pensez-y comme openGL -> vous obtenez un tas de fonctions et d'objets mais sans fichier cpp qui peut les exécuter, ils sont inutiles.
Ed James
0

Le code du modèle ne doit figurer que dans les en-têtes. En dehors de cela, toutes les définitions, à l'exception des lignes en ligne, doivent être en .cpp. Le meilleur argument pour cela serait les implémentations de la bibliothèque std qui suivent la même règle. Vous ne seriez pas en désaccord avec le fait que les développeurs de std lib auraient raison à ce sujet.

Abhishek Agarwal
la source
Quels stdlibs? Le GCC libstdc++semble (AFAICS) ne mettre presque rien srcet presque tout include, qu'il soit ou non «en-tête». Je ne pense donc pas que ce soit une citation précise / utile. Quoi qu'il en soit, je ne pense pas que stdlibs soit un modèle de code utilisateur: ils sont évidemment écrits par des codeurs hautement qualifiés, mais pour être utilisés , pas lus: ils résument une complexité élevée à laquelle la plupart des codeurs ne devraient pas avoir à penser , besoin de laid _Reserved __namespartout pour éviter les conflits avec l'utilisateur, les commentaires et l'espacement sont inférieurs à ce que je conseillerais, etc. Ils sont exemplaires de manière étroite.
underscore_d
0

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. Le bon équilibre, je pense, est de 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.

Soit dit en passant, Ted, avez-vous jeté un œil sur ce forum à la récente question sur la liaison 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 elle est faite pour une ancienne version de Clips, cette liaison pourrait être un bon exemple de départ pour quelqu'un qui souhaite utiliser le moteur d'inférence CLIPS dans un programme Ada 2012.

Emile
la source
1
Lol. 2 ans plus tard, c'est une façon bizarre de mettre la main sur quelqu'un. Je vais vérifier si j'en ai encore une copie, mais probablement pas. Je l'ai fait pour une classe d'IA afin de pouvoir faire mon code dans Ada, mais j'ai fait exprès ce projet CC0 (essentiellement sans droit d'auteur) dans l'espoir que quelqu'un le prenne sans vergogne et fasse quelque chose avec.
TED