104 votes

Comment un fichier d'en-tête C++ peut-il inclure une implémentation ?

Ok, je ne suis en aucun cas un expert en C/C++, mais je pensais que le but d'un fichier d'en-tête était de déclarer les fonctions, puis que le fichier C/CPP était de définir l'implémentation.

Cependant, en examinant du code C++ ce soir, j'ai trouvé ceci dans le fichier d'en-tête d'une classe...

public:
    UInt32 GetNumberChannels() const { return _numberChannels; } // <-- Huh??

private:
    UInt32 _numberChannels;

Alors pourquoi y a-t-il une implémentation dans un en-tête ? Est-ce que cela a un rapport avec le const mot-clé ? Est-ce que cela met en ligne une méthode de classe ? Quel est exactement l'avantage ou l'intérêt de procéder de cette manière plutôt que de définir l'implémentation dans le fichier CPP ?

166voto

Remy Lebeau Points 130112

Ok, je ne suis en aucun cas un expert en C/C++, mais je pensais que le but d'un fichier d'en-tête était de déclarer les fonctions, puis que le fichier C/CPP était de définir l'implémentation.

Le véritable objectif d'un fichier d'en-tête est de partager le code entre plusieurs fichiers sources. Il est communément utilisé pour séparer les déclarations des implémentations pour une meilleure gestion du code, mais ce n'est pas une obligation. Il est possible d'écrire du code qui ne dépend pas des fichiers d'en-tête, et il est possible d'écrire du code qui n'est constitué que de fichiers d'en-tête (les bibliothèques STL et Boost en sont de bons exemples). N'oubliez pas que lorsque le préprocesseur rencontre un #include il remplace l'instruction par le contenu du fichier auquel il fait référence, puis l'instruction compilateur ne voit que le code pré-traité terminé.

Ainsi, par exemple, si vous avez les fichiers suivants :

Foo.h :

#ifndef FooH
#define FooH

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

#endif

Foo.cpp :

#include "Foo.h"

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

Bar.cpp :

#include "Foo.h"

Foo f;
UInt32 chans = f.GetNumberChannels();

En préprocesseur analyse séparément les fichiers Foo.cpp et Bar.cpp et produit le code suivant, que l'on peut lire sur le site Web de l'entreprise. compilateur puis l'analyse syntaxique :

Foo.cpp :

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

Bar.cpp :

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

Foo f;
UInt32 chans = f.GetNumberChannels();

Bar.cpp se compile dans Bar.obj et contient une référence pour appeler dans Foo::GetNumberChannels() . Foo.cpp se compile dans Foo.obj et contient l'implémentation réelle de Foo::GetNumberChannels() . Après la compilation, le linker fait ensuite correspondre les fichiers .obj et les relie entre eux pour produire l'exécutable final.

Alors pourquoi y a-t-il une implémentation dans un en-tête ?

En incluant l'implémentation de la méthode à l'intérieur de la déclaration de la méthode, celle-ci est implicitement déclarée comme inlined (il y a une méthode de type inline qui peut également être utilisé explicitement). Indiquer au compilateur qu'une fonction doit être inline n'est qu'une indication qui ne garantit pas que la fonction sera effectivement inline. Mais si c'est le cas, quel que soit l'endroit d'où la fonction inline est appelée, le contenu de la fonction est copié directement dans le site d'appel, au lieu de générer un fichier de type CALL pour entrer dans la fonction et revenir à l'appelant en sortant. Le compilateur peut alors prendre en compte le code environnant et optimiser davantage le code copié, si possible. 

Cela a-t-il un rapport avec le mot-clé const ?

Non. Le const indique simplement au compilateur que la méthode ne modifiera pas l'état de l'objet sur lequel elle est appelée au moment de l'exécution.

Quel est exactement l'avantage ou l'intérêt de procéder de cette manière plutôt que de définir l'implémentation dans le fichier CPP ?

Lorsqu'il est utilisé efficacement, il permet au compilateur de produire généralement un code machine plus rapide et mieux optimisé.

48voto

Agentlien Points 3163

Il est parfaitement valable d'avoir une implémentation d'une fonction dans un fichier d'en-tête. Le seul problème que cela pose est la rupture de la règle de la définition unique. En effet, si vous incluez l'en-tête dans plusieurs autres fichiers, vous obtiendrez une erreur de compilation.

Cependant, il y a une exception. Si vous déclarez qu'une fonction est en ligne, elle est exemptée de la règle de la définition unique. C'est ce qui se passe ici, puisque les fonctions membres définies dans une définition de classe sont implicitement inline.

L'inline en soi est un indice pour le compilateur qu'une fonction peut être un bon candidat pour l'inline. C'est-à-dire qu'il faut étendre tout appel à cette fonction dans la définition de la fonction, plutôt que dans un simple appel de fonction. Il s'agit d'une optimisation qui échange la taille du fichier généré contre un code plus rapide. Dans les compilateurs modernes, cette indication d'inlining pour une fonction est généralement ignorée, sauf pour les effets qu'elle a sur la règle de la définition unique. De plus, un compilateur est toujours libre de mettre en ligne toute fonction qu'il juge appropriée, même si elle n'a pas été déclarée. inline (explicitement ou implicitement).

Dans votre exemple, l'utilisation de const après la liste des arguments signale que la fonction membre ne modifie pas l'objet sur lequel elle est appelée. En pratique, cela signifie que l'objet désigné par this et, par extension, tous les membres de la classe, seront considérés comme étant const . C'est-à-dire qu'en essayant de les modifier, on génère une erreur de compilation.

9voto

juanchopanza Points 115680

Il est implicitement déclaré inline en vertu du fait qu'il s'agit d'une fonction membre défini dans la déclaration de la classe. Cela ne signifie pas que le compilateur tiene pour le mettre en ligne, mais cela signifie que vous ne romprez pas les règle d'une seule définition . Il n'est pas du tout lié à const * . Elle n'est pas non plus liée à la longueur et à la complexité de la fonction.

S'il s'agissait d'une fonction non membre, il faudrait alors la déclarer explicitement en tant que inline :

inline void foo() { std::cout << "foo!\n"; }

* Voir aquí pour en savoir plus sur const à la fin d'une fonction membre.

5voto

steveha Points 24808

Même en C simple, il est possible de mettre du code dans un fichier d'en-tête. Si vous le faites, vous devez habituellement le déclarer static ou bien plusieurs fichiers .c comprenant le même en-tête provoqueront une erreur de type "fonction définie de façon multiple".

Le préprocesseur inclut textuellement un fichier d'inclusion, de sorte que le code dans un fichier d'inclusion devient une partie du fichier source (du moins du point de vue du compilateur).

Les concepteurs du C++ voulaient permettre une programmation orientée objet avec une bonne dissimulation des données, ils s'attendaient donc à voir beaucoup de fonctions getter et setter. Ils ne voulaient pas d'une pénalité de performance déraisonnable. Ils ont donc conçu le C++ de manière à ce que les getters et setters puissent non seulement être déclarés dans l'en-tête, mais aussi être implémentés, de sorte qu'ils soient inline. La fonction que vous avez montrée est un getter, et lorsque ce code C++ sera compilé, il n'y aura pas d'appel de fonction ; le code pour récupérer cette valeur sera simplement compilé à la place.

Il est possible de créer un langage informatique qui ne fasse pas la distinction entre fichier d'en-tête et fichier source, mais qui comporte simplement des "modules" que le compilateur comprend. (Le C++ n'a pas fait cela ; il s'est simplement construit par-dessus le modèle C réussi de fichiers sources et de fichiers d'en-tête inclus textuellement). Si les fichiers sources sont des modules, il serait possible pour un compilateur d'extraire du code du module et de le mettre en ligne. Mais la façon dont le C++ a procédé est plus simple à mettre en œuvre.

2voto

Spook Points 11276

Pour autant que je sache, il existe deux types de méthodes qui peuvent être implémentées en toute sécurité dans le fichier d'en-tête.

  • Méthodes en ligne - leur mise en œuvre est copiée aux endroits où elles sont utilisées, de sorte qu'il n'y a pas de problème d'erreurs de liaison de double définition ;
  • Méthodes de modèle - elles sont en fait compilées au moment de l'instanciation du modèle (par exemple, lorsque quelqu'un entre un type à la place du modèle), il n'y a donc aucune possibilité de problème de double définition.

Je crois que votre exemple correspond au premier cas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X