web-dev-qa-db-fra.com

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

Ok, ce n’est pas un expert en C/C++, mais j’ai pensé que le but d’un fichier d’en-tête était de déclarer les fonctions, alors 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? Cela a-t-il à voir avec le mot clé const? Est-ce que cela intègre une méthode de classe? Quel est exactement le bénéfice/intérêt de le faire de cette manière par rapport à la définition de la mise en œuvre dans le fichier CPP?

50
MarqueIV

Ok, ce n’est pas un expert en C/C++, mais j’ai pensé que le but d’un fichier d’en-tête était de déclarer les fonctions, alors 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 du code entre plusieurs fichiers source. C'est couramment 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 s'appuie pas sur des fichiers d'en-tête et d'écrire du code constitué uniquement 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 une instruction #include, il remplace l'instruction par le contenu du fichier référencé. Le compilateur ne voit que le code pré-traité terminé.

Donc, 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();

Le préprocesseur analyse Foo.cpp et Bar.cpp séparément et produit le code suivant que le {compilateur} _ analyse ensuite:

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 est compilé dans Bar.obj et contient une référence à appeler dans Foo::GetNumberChannels(). Foo.cpp est compilé dans Foo.obj et contient l'implémentation réelle de Foo::GetNumberChannels(). Après la compilation, le éditeur de liens met ensuite en correspondance les fichiers .obj et les lie pour créer 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 dans la déclaration de la méthode, celle-ci est implicitement déclarée comme étant en ligne (il existe un mot clé inline qui peut également être utilisé explicitement). Indiquer que le compilateur doit intégrer une fonction en ligne n’est qu’un indice qui ne garantit pas que la fonction sera réellement en ligne. Mais si tel est le cas, le contenu de la fonction est directement copié directement sur le site d’appel, quelle que soit l’appel de la fonction en ligne, au lieu de générer une instruction CALL pour accéder à la fonction et revenir à l’appelant lors de la sortie. Le compilateur peut alors prendre en compte le code environnant et optimiser davantage le code copié, si possible.

Est-ce que cela a à voir avec le mot clé const?

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

Quel est exactement le bénéfice/intérêt de le faire de cette manière par rapport à la définition de la mise en œuvre dans le fichier CPP?

Utilisé efficacement, le compilateur produit généralement un code machine plus rapide et mieux optimisé.

82
Remy Lebeau

Il est parfaitement valable d'avoir une implémentation d'une fonction dans un fichier d'en-tête. Le seul problème avec cela est la violation de la règle de définition unique. Autrement dit, si vous incluez l'en-tête de plusieurs autres fichiers, vous obtiendrez une erreur du compilateur.

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

Inline lui-même est un indice pour le compilateur qu'une fonction peut être un bon candidat pour l'inline. C'est-à-dire, en développant n'importe quel appel dans la définition de la fonction, plutôt que dans un simple appel de fonction. C'est une optimisation qui échange la taille du fichier généré pour un code plus rapide. Dans les compilateurs modernes, la fourniture de cette indication en ligne pour une fonction est généralement ignorée, à l'exception de ses effets sur la règle à une définition. En outre, un compilateur est toujours libre d’inscrire 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 d'arguments indique que la fonction de 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 du groupe, sera considéré comme const. Autrement dit, essayer de les modifier générera une erreur de compilation.

24
Agentlien

Il est implicitement declareinline du fait qu’il est une fonction membre define dans la déclaration de classe. Cela ne signifie pas que le compilateur doit le mettre en ligne, mais cela signifie que vous ne violerez pas la règle de définition one . Il n’a aucun lien avec const*. Il n’est pas non plus lié à la longueur et à la complexité de la fonction.

S'il s'agissait d'une fonction non membre, vous devrez alors explicitement la déclarer comme inline:

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

* Voir ici pour plus d’informations sur const à la fin d’une fonction membre.

4
juanchopanza

Même en clair, il est possible de mettre du code dans un fichier d’en-tête. Si vous le faites, vous devez généralement le déclarer static, sinon plusieurs fichiers .c incluant le même en-tête provoqueront une erreur "multiplier la fonction définie".

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

Les concepteurs de C++ voulaient permettre une programmation orientée objet avec une bonne dissimulation de données. Ils s'attendaient donc à voir un grand nombre de fonctions de lecture et de définition. Ils ne voulaient pas d'une pénalité de performance déraisonnable. Ainsi, ils ont conçu C++ de manière à ce que les getters et les setters puissent être non seulement déclarés dans l’en-tête, mais également mis en œuvre, de sorte qu’ils soient intégrés. Cette fonction que vous avez montrée est un getter, et lorsque ce code C++ est compilé, il n’y aura pas d’appel de fonction; Le code pour extraire cette valeur sera simplement compilé sur place.

Il est possible de créer un langage informatique ne faisant pas la distinction fichier d’en-tête/fichier source, mais ayant simplement des "modules" réels que le compilateur comprend. (C++ n'a pas fait cela; ils ont simplement construit sur le modèle C de fichiers source et d'en-tête inclus textuellement.) Si les fichiers source sont des modules, un compilateur pourrait extraire du code du module puis en ligne ce code. Mais la façon dont C++ l'a fait est plus simple à implémenter.

2
steveha

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 implémentation est copiée dans les emplacements où elles sont utilisées, de sorte qu'il n'y a aucun problème avec les erreurs de l'éditeur de liens à double définition;
  • Méthodes de gabarit - elles sont en fait compilées au moment de l'instanciation du gabarit (par exemple, lorsque quelqu'un entre un type à la place du gabarit), il n'y a donc pas de risque de problème de double définition.

Je crois que votre exemple correspond au premier cas.

1
Spook

Citations standard C++

Le brouillon standard C++ 17 N4659 10.1.6 "Le spécificateur inline" indique que les méthodes sont implicitement inline:

4 Une fonction définie dans une définition de classe est une fonction en ligne.

et plus bas, nous voyons que les méthodes en ligne non seulement peuvent, mais doivent être définies sur toutes les unités de traduction:

6 Une fonction ou une variable en ligne doit être définie dans chaque unité de traduction dans laquelle elle est utilisée et doit avoir exactement la même définition dans tous les cas (6.2).

Ceci est également mentionné explicitement dans une note au 12.2.1 "Fonctions membres":

1 Une fonction membre peut être définie (11.4) dans sa définition de classe. Dans ce cas, il s'agit d'une fonction membre en ligne (10.1.6) [...]

3 [Remarque: il peut exister au plus une définition d'une fonction membre non intégrée dans un programme. Il peut y avoir plus d'une définition de fonction membre en ligne dans un programme. Voir 6.2 et 10.1.6. - note de fin]

Implémentation de GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

Compiler et afficher les symboles:

g++ -c main.cpp
nm -C main.o

sortie:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

nous voyons alors de man nm que le symbole MyClass::myMethod est marqué comme étant faible sur les fichiers d'objet ELF, ce qui implique qu'il peut apparaître sur plusieurs fichiers d'objet:

"W" "w" Le symbole est un symbole faible qui n'a pas été spécifiquement étiqueté en tant que symbole d'objet faible. Lorsqu'un symbole défini faible est lié à un symbole défini normal, le symbole défini normal est utilisé sans erreur. Lorsqu'un symbole faible non défini est lié et que le symbole n'est pas défini, la valeur du symbole est déterminée de manière spécifique au système sans erreur. Sur certains systèmes, les majuscules indiquent qu'une valeur par défaut a été spécifiée.

Garder l'implémentation dans le fichier d'en-tête de classe fonctionne, car je suis sûr que vous savez si vous avez compilé votre code. Le mot clé const garantit que vous ne modifiez aucun membre, il conserve l'instance immutable pendant la durée de l'appel de la méthode.

0
Jonas Byström