Lorsque nous concevons des classes en Java, Vala ou C #, nous plaçons la définition et la déclaration dans le même fichier source. Mais en C++, il est généralement préférable de séparer la définition et la déclaration en deux fichiers ou plus.
Que se passe-t-il si je viens d'utiliser un fichier d'en-tête et de tout mettre dans celui-ci, comme Java? Y at-il une pénalité de performance ou quelque chose?
La réponse dépend du type de classe que vous créez.
Le modèle de compilation de C++ remonte à l'époque de C et sa méthode d'importation des données d'un fichier source dans un autre est comparativement primitive. La directive #include
copie littéralement le contenu du fichier que vous incluez dans le fichier source, puis traite le résultat comme s'il s'agissait du fichier que vous aviez écrit depuis le début. Vous devez faire attention à cela à cause d'une stratégie C++ appelée une règle de définition (ODR) qui stipule, sans surprise, que chaque fonction et classe doit avoir au plus une définition. Cela signifie que si vous déclarez une classe quelque part, toutes les fonctions membres de cette classe doivent être soit non définies du tout, soit définies exactement une fois dans exactement un fichier. Il y a quelques exceptions (j'y reviendrai dans une minute), mais pour le moment, considérez cette règle comme s'il s'agissait d'une règle absolue, sans exception.
Si vous prenez une classe non-template et placez la définition de la classe et l'implémentation dans un fichier d'en-tête, vous risquez de rencontrer des problèmes avec la règle de définition unique. En particulier, supposons que je compile deux fichiers .cpp différents, chacun d'eux #include
votre en-tête contenant à la fois l'implémentation et l'interface. Dans ce cas, si j'essaie de relier ces deux fichiers, l'éditeur de liens trouvera que chacun d'eux contient une copie du code d'implémentation des fonctions membres de la classe. À ce stade, l'éditeur de liens signalera une erreur car vous avez enfreint la règle de définition unique: il existe deux implémentations différentes de toutes les fonctions membres de la classe.
Pour éviter cela, les programmeurs C++ divisent généralement les classes en un fichier d'en-tête contenant la déclaration de la classe, ainsi que les déclarations de ses fonctions membres, sans implémentation de ces fonctions. Les implémentations sont ensuite placées dans un fichier .cpp séparé qui peut être compilé et lié séparément. Cela permet à votre code d'éviter d'avoir des problèmes avec l'ODR. Voici comment. Tout d’abord, chaque fois que vous #include
z le fichier d’en-tête de classe en plusieurs fichiers .cpp différents, chacun d’entre eux reçoit simplement une copie des déclarations des fonctions membres, et non de leurs définitions , et ainsi de suite. des clients de votre classe se retrouveront avec les définitions. Cela signifie que n'importe quel nombre de clients peut #include
votre fichier d'en-tête sans rencontrer de problèmes au moment du lien. Étant donné que votre propre fichier .cpp avec l'implémentation est le seul fichier contenant les implémentations des fonctions membres, vous pouvez le fusionner au moment du lien avec un nombre quelconque d'autres fichiers d'objet client sans tracas. C'est la raison principale pour laquelle vous avez séparé les fichiers .h et .cpp.
Bien sûr, l'ODR a quelques exceptions. Le premier de ceux-ci vient avec des fonctions de modèle et des classes. L'ODR indique explicitement que vous pouvez avoir plusieurs définitions différentes pour la même classe de modèle ou la même fonction de modèle, à condition qu'elles soient toutes équivalentes. Ceci est principalement destiné à faciliter la compilation de modèles - chaque fichier C++ peut instancier le même modèle sans entrer en conflit avec d’autres fichiers. Pour cette raison et quelques autres raisons techniques, les modèles de classe ont tendance à ne contenir qu'un fichier .h sans fichier .cpp correspondant. N'importe quel nombre de clients peut #include
le fichier sans problème.
L'autre exception majeure à l'ODR concerne les fonctions en ligne. La spécification indique spécifiquement que l'ODR ne s'applique pas aux fonctions inline. Par conséquent, si vous avez un fichier d'en-tête avec une implémentation d'une fonction de membre de classe marquée en ligne, c'est très bien. N'importe quel nombre de fichiers peut #include
ce fichier sans rompre l'ODR. Fait intéressant, toute fonction membre déclarée et définie dans le corps d'une classe est implicitement intégrée, donc si vous avez un en-tête comme celui-ci:
#ifndef Include_Guard
#define Include_Guard
class MyClass {
public:
void DoSomething() {
/* ... code goes here ... */
}
};
#endif
Alors vous ne risquez pas de casser l'ODR. Si vous réécrivez cela en tant que
#ifndef Include_Guard
#define Include_Guard
class MyClass {
public:
void DoSomething();
};
void MyClass::DoSomething() {
/* ... code goes here ... */
}
#endif
alors vous voudriez rompez l'ODR, puisque la fonction membre n'est pas marquée en ligne et si plusieurs clients #include
ce fichier, il y aura plusieurs définitions de MyClass::DoSomething
.
Donc, pour résumer, vous devriez probablement diviser vos classes en une paire .h/.cpp pour éviter de casser l'ODR. Cependant, si vous écrivez un modèle de classe, vous n'avez pas besoin du fichier .cpp (et ne devriez probablement pas en avoir du tout), et si vous acceptez de marquer chaque fonction membre de votre classe en ligne, vous pouvez également éviter le fichier .cpp.
L'inconvénient de la définition de la définition dans les fichiers d'en-tête est la suivante: -
Fichier d'en-tête A - contient la définition de metahodA ()
Fichier d'en-tête B - inclut le fichier d'en-tête A.
Maintenant, disons que vous modifiez la définition de methodA. Vous auriez besoin de compiler les fichiers A et B à cause de l'inclusion du fichier d'en-tête A dans B.
La plus grande différence est que chaque fonction est déclarée comme une fonction en ligne. Généralement, votre compilateur sera suffisamment intelligent pour que cela ne pose pas de problème, mais dans le pire des cas, il provoquera régulièrement des erreurs de page et ralentira votre code de manière embarrassante. Généralement, le code est séparé pour des raisons de conception et non pour des performances.
En général, il est recommandé de séparer la mise en œuvre des en-têtes. Cependant, il existe des exceptions dans des cas tels que les modèles où l'implémentation est insérée dans l'en-tête même.
Deux problèmes particuliers pour tout mettre dans l'en-tête:
Les temps de compilation seront augmentés, parfois considérablement. Les temps de compilation C++ sont suffisamment longs pour que ce ne soit pas quelque chose que vous souhaitiez.
Si vous avez des dépendances circulaires dans l'implémentation, il est difficile, voire impossible, de tout conserver dans les en-têtes. par exemple:
header1.h
struct C1
{
void f();
void g();
};
header2.h
struct C2
{
void f();
void g();
};
impl1.cpp
#include "header1.h"
#include "header2.h"
void C1::f()
{
C2 c2;
c2.f();
}
impl2.cpp
#include "header2.h"
#include "header1.h"
void C2::g()
{
C1 c1;
c1.g();
}