web-dev-qa-db-fra.com

Définition du constructeur dans le fichier d'en-tête par rapport au fichier d'implémentation (.cpp)

Je peux définir le corps d'un constructeur de classe dans le fichier class . H ou dans le fichier d'implémentation . Cpp. Ces deux styles sont probablement identiques en ce qui concerne le compilateur dans un projet spécifique (projet pour moi signifie DLL ). Il en va de même pour toutes les fonctions membres: elles peuvent être définies dans le fichier d'en-tête ou simplement déclarées là, puis définies dans le fichier cpp.

Cependant, j'ai trouvé que si je dois inclure de tels fichiers d'en-tête de classe dans différents projets (ce qui signifie qu'en fin de compte, le code qui utilise le fichier d'en-tête se retrouve dans une autre DLL ) puis l'implémentation réelle dans le fichier d'en-tête provoque des maux de tête lors de la compilation (pas lors de la liaison ... je ne parviens même pas à ce point). Pourquoi? Eh bien, je n'entrerai pas trop dans les détails, mais le compilateur essaie évidemment de résoudre toutes les fonctions qui pourraient être définies dans d'autres fichiers d'en-tête, etc., forçant le pauvre développeur à commencer à extraire divers fichiers d'en-tête, etc.

N'est-il pas toujours préférable de garder les fichiers d'en-tête exempts de toute implémentation et de les utiliser uniquement pour les "déclarations"? Cela faciliterait leur inclusion dans plusieurs projets sans avoir à transporter beaucoup de déchets supplémentaires.

Quelle est votre opinion à ce sujet?

29
Andrea

Gardez vos en-têtes exempts d'implémentations, sauf si vous souhaitez que les implémentations soient alignées (par exemple, des getters/setters triviaux). Et à moins qu'il ne s'agisse de modèles, bien sûr.

Je ne vois aucune raison de faire une exception pour les constructeurs. Mettez-les dans le fichier .cpp.

27
Thomas

Un point important à noter est que si une fonction membre est définie dans un fichier d'en-tête, elle doit être soit à l'intérieur du corps de la classe, soit marquée explicitement comme inline. En d'autres termes, il est tout à fait faux de le faire dans un fichier d'en-tête:

class A {
  public:
    A();
};

A::A() {
  // constructor body
}

La raison pour laquelle c'est faux est parce que le compilateur inclura la définition dans chaque unité de compilation, alors qu'il est évident que toute fonction ne doit être définie qu'une seule fois. Voici les bonnes façons de faire la même chose:

class A {
  public:
    inline A();
};

inline A::A() {
  // constructor body
}

Ou:

class A {
  public:
    inline A() { // inline isn't required here, but it's a good style
     // constructor body
    }
};

Dans les deux cas, le constructeur est en ligne. La seule façon correcte d'en faire une fonction hors ligne régulière serait de la définir dans le fichier d'implémentation, pas dans l'en-tête. Il s'agit de la différence la plus importante entre ces deux approches.

Maintenant, il convient de noter que l'inline est une optimisation. Et comme toujours avec les optimisations, il vaut mieux les éviter jusqu'à ce qu'elles s'avèrent nécessaires. Parmi les autres problèmes que peut entraîner l'inline, il y a le problème de compatibilité: si vous changez le corps d'une fonction qui n'est pas en ligne, il vous suffit de recompiler l'unité où elle est définie, et tout le monde commence immédiatement à utiliser la nouvelle implémentation. Avec les fonctions intégrées, vous devez recompiler chaque unité qui inclut l'en-tête correspondant, ce qui peut être pénible, surtout si l'en-tête est utilisé dans différents projets par différentes personnes.

En d'autres termes, utilisez des définitions hors ligne régulières dans la mesure du possible jusqu'à ce qu'il soit prouvé par profilage qu'un appel de fonction particulier est un goulot d'étranglement des performances. La seule exception raisonnable à cette règle est les setters et les getters triviaux, et même avec eux, il vaut mieux être prudent - un jour, ils peuvent devenir non triviaux et cela signifiera beaucoup de recompilation et de rupture de compatibilité.

25
Sergei Tachenov

Une autre remarque à prendre en compte: toute modification d'un fichier d'en-tête nécessite de reconstruire tous les fichiers qui incluent ce fichier d'en-tête. La plupart des systèmes de génération reconstruisent les fichiers source (* .cpp/.cc) qui dépendent du fichier d'en-tête modifié.

Si vous modifiez une méthode d'une classe définie dans un fichier d'en-tête, tous les fichiers sources, y compris le fichier d'en-tête, seront reconstruits. Si vous modifiez une méthode dans un fichier source, seul le fichier source est reconstruit. Cela pourrait être un problème pour les projets de moyenne à grande envergure.

Pour simplifier le processus de génération, la plupart des méthodes d'une classe doivent être définies dans un fichier source. Les petites méthodes et autres candidats pour l'inlining doivent être définis dans le fichier d'en-tête.

1
Thomas Matthews