web-dev-qa-db-fra.com

Pourquoi mes protections d'inclusion n'empêchent-elles pas l'inclusion récursive et les définitions de symboles multiples?

Deux questions courantes sur inclure les gardes :

  1. PREMIÈRE QUESTION:

    Pourquoi les gardes ne protègent-ils pas mes fichiers d'en-tête contre l'inclusion mutuelle et récursive ? Je continue à recevoir des erreurs sur des symboles inexistants qui sont évidemment là ou même des erreurs de syntaxe plus étranges chaque fois que j'écris quelque chose comme ceci:

    "a.h"

    #ifndef A_H
    #define A_H
    
    #include "b.h"
    
    ...
    
    #endif // A_H
    

    "b.h"

    #ifndef B_H
    #define B_H
    
    #include "a.h"
    
    ...
    
    #endif // B_H
    

    "main.cpp"

    #include "a.h"
    int main()
    {
        ...
    }
    

    Pourquoi ai-je des erreurs lors de la compilation de "main.cpp"? Que dois-je faire pour résoudre mon problème?


  1. DEUXIÈME QUESTION:

    Pourquoi les gardes inclus n'empêchent-ils pas les définitions multiples ? Par exemple, lorsque mon projet contient deux fichiers qui incluent le même en-tête, parfois l'éditeur de liens se plaint que certains symboles soient définis plusieurs fois. Par exemple:

    "header.h"

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    

    "source1.cpp"

    #include "header.h"
    ...
    

    "source2.cpp"

    #include "header.h"
    ...
    

    Pourquoi cela arrive-t-il? Que dois-je faire pour résoudre mon problème?

68
Andy Prowl

PREMIÈRE QUESTION:

Pourquoi ne pas inclure des gardes protégeant mes fichiers d'en-tête contre l'inclusion récurrente mutuelle ?

Ce sont .

Ce qu'ils n'aident pas, c'est les dépendances entre les définitions des structures de données dans les en-têtes mutuellement inclus. Pour voir ce que cela signifie, commençons par un scénario de base et voyons pourquoi les gardes inclus aident aux inclusions mutuelles.

Supposons que vos fichiers d'en-tête a.h Et b.h Mutuellement inclus aient un contenu trivial, c'est-à-dire que les ellipses dans les sections de code du texte de la question sont remplacées par la chaîne vide. Dans cette situation, votre main.cpp Se fera un plaisir de compiler. Et ce n'est que grâce à vos gardes inclus!

Si vous n'êtes pas convaincu, essayez de les supprimer:

//================================================
// a.h

#include "b.h"

//================================================
// b.h

#include "a.h"

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

Vous remarquerez que le compilateur signalera un échec lorsqu'il atteindra la limite de profondeur d'inclusion. Cette limite est spécifique à l'implémentation. Conformément au paragraphe 16.2/6 de la norme C++ 11:

Une directive de prétraitement #include peut apparaître dans un fichier source qui a été lu en raison d'une directive #include dans un autre fichier, jusqu'à une limite d'imbrication définie par l'implémentation .

Alors, que se passe-t-il ?

  1. Lors de l'analyse de main.cpp, Le préprocesseur respectera la directive #include "a.h". Cette directive indique au préprocesseur de traiter le fichier d'en-tête a.h, De prendre le résultat de ce traitement et de remplacer la chaîne #include "a.h" Par ce résultat;
  2. Lors du traitement de a.h, Le préprocesseur respectera la directive #include "b.h", Et le même mécanisme s'applique: le préprocesseur doit traiter le fichier d'en-tête b.h, Prendre le résultat de son traitement, et remplacez la directive #include par ce résultat;
  3. Lors du traitement de b.h, La directive #include "a.h" Indiquera au préprocesseur de traiter a.h Et de remplacer cette directive par le résultat;
  4. Le préprocesseur recommencera à analyser a.h, Répondra à nouveau à la directive #include "b.h", Ce qui mettra en place un processus récursif potentiellement infini. Lorsqu'il atteint le niveau d'imbrication critique, le compilateur signale une erreur.

Lorsque des gardes d'inclusion sont présents , cependant, aucune récursion infinie ne sera configurée à l'étape 4. Voyons pourquoi:

  1. ( comme avant) Lors de l'analyse de main.cpp, le préprocesseur respectera la directive #include "a.h". Cela indique au préprocesseur de traiter le fichier d'en-tête a.h, De prendre le résultat de ce traitement et de remplacer la chaîne #include "a.h" Par ce résultat;
  2. Lors du traitement de a.h, Le préprocesseur respectera la directive #ifndef A_H. Étant donné que la macro A_H N'a pas encore été définie, elle continuera à traiter le texte suivant. La directive suivante (#defines A_H) Définit la macro A_H. Ensuite, le préprocesseur rencontrera la directive #include "b.h": Le préprocesseur devra maintenant traiter le fichier d'en-tête b.h, Prendre le résultat de son traitement et remplacer la directive #include Par ce résultat ;
  3. Lors du traitement de b.h, Le préprocesseur respectera la directive #ifndef B_H. Étant donné que la macro B_H N'a pas encore été définie, elle continuera à traiter le texte suivant. La directive suivante (#defines B_H) Définit la macro B_H. Ensuite, la directive #include "a.h" Indiquera au préprocesseur de traiter a.h Et de remplacer la directive #include Dans b.h Par le résultat du prétraitement a.h ;
  4. Le compilateur recommencera le prétraitement a.h Et respectera à nouveau la directive #ifndef A_H. Cependant, lors du prétraitement précédent, la macro A_H A été définie. Par conséquent, le compilateur ignorera cette fois le texte suivant jusqu'à ce que la directive #endif Correspondante soit trouvée et que la sortie de ce traitement soit la chaîne vide (en supposant que rien ne suit la directive #endif, Bien sûr) . Le préprocesseur remplacera donc la directive #include "a.h" Dans b.h Par la chaîne vide, et retracera l'exécution jusqu'à ce qu'elle remplace la directive #include D'origine dans main.cpp .

Ainsi, inclure les gardes protègent contre l'inclusion mutuelle . Cependant, ils ne peuvent pas aider avec les dépendances entre les définitions de vos classes dans les fichiers mutuellement inclus:

//================================================
// a.h

#ifndef A_H
#define A_H

#include "b.h"

struct A
{
};

#endif // A_H

//================================================
// b.h

#ifndef B_H
#define B_H

#include "a.h"

struct B
{
    A* pA;
};

#endif // B_H

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

Étant donné les en-têtes ci-dessus, main.cpp Ne sera pas compilé.

Pourquoi cela arrive-t-il?

Pour voir ce qui se passe, il suffit de recommencer les étapes 1 à 4.

Il est facile de voir que les trois premières étapes et la plupart de la quatrième étape ne sont pas affectées par ce changement (il suffit de les lire pour s'en convaincre). Cependant, quelque chose de différent se produit à la fin de l'étape 4: après avoir remplacé la directive #include "a.h" Dans b.h Par la chaîne vide, le préprocesseur commencera à analyser le contenu de b.h Et, en particulier, la définition de B. Malheureusement, la définition de B mentionne la classe A, qui n'a jamais été rencontrée auparavant exactement parce que des gardes d'inclusion!

Déclarer une variable membre d'un type qui n'a pas été précédemment déclaré est, bien sûr, une erreur, et le compilateur le signalera poliment.

Que dois-je faire pour résoudre mon problème?

Vous avez besoin de déclarations avant .

En fait, la définition de la classe A n'est pas requise pour définir la classe B, car un pointeur to A est déclaré comme une variable membre et non comme un objet de type A. Puisque les pointeurs ont une taille fixe, le compilateur n'aura pas besoin de connaître la disposition exacte de A ni de calculer sa taille afin de définir correctement la classe B. Par conséquent, il suffit de déclarer en amont classe A dans b.h Et informer le compilateur de son existence:

//================================================
// b.h

#ifndef B_H
#define B_H

// Forward declaration of A: no need to #include "a.h"
struct A;

struct B
{
    A* pA;
};

#endif // B_H

Votre main.cpp Va maintenant certainement se compiler. Quelques remarques:

  1. Non seulement briser l'inclusion mutuelle en remplaçant la directive #include Par une déclaration directe dans b.h Était suffisant pour exprimer efficacement la dépendance de B sur A: en utilisant les déclarations avant chaque fois que cela est possible/pratique est également considérée comme une bonne pratique de programmation , car elle permet d'éviter les inclusions inutiles, réduisant ainsi le temps de compilation global. Cependant, après avoir éliminé l'inclusion mutuelle, main.cpp Devra être modifié en #include À la fois a.h Et b.h (Si ce dernier est nécessaire), parce que b.h n'est plus indirectement #include d à a.h;
  2. Alors qu'une déclaration directe de classe A suffit au compilateur pour déclarer des pointeurs vers cette classe (ou pour l'utiliser dans tout autre contexte où des types incomplets sont acceptables), le déréférencement des pointeurs vers A (pour par exemple pour appeler une fonction membre) ou calculer sa taille sont des opérations illégales sur des types incomplets: si cela est nécessaire, la définition complète de A doit être disponible pour le compilateur, ce qui signifie que le fichier d'en-tête qui le définit doit être inclus. C'est pourquoi les définitions de classe et l'implémentation de leurs fonctions membres sont généralement divisées en un fichier d'en-tête et un fichier d'implémentation pour cette classe (classe modèles sont une exception à cette règle): les fichiers d'implémentation, qui ne sont jamais #include d par d'autres fichiers du projet, peuvent en toute sécurité #include tous les en-têtes nécessaires pour rendre les définitions visibles. Les fichiers d'en-tête, d'autre part, ne seront pas #include D'autres fichiers d'en-tête à moins que ils doivent vraiment le faire (par exemple, pour faire la définition d'un classe de base visible), et utilisera des déclarations directes chaque fois que cela sera possible/pratique.

DEUXIÈME QUESTION:

Pourquoi les gardes inclus n'empêchent-ils pas les définitions multiples ?

Ce sont .

Ce qu'ils ne vous protègent pas, c'est de multiples définitions dans des unités de traduction séparées. Ceci est également expliqué dans ce Q&A sur StackOverflow.

Pour voir cela, essayez de supprimer les gardes d'inclusion et de compiler la version modifiée suivante de source1.cpp (Ou source2.cpp, Pour ce qui importe):

//================================================
// source1.cpp
//
// Good luck getting this to compile...

#include "header.h"
#include "header.h"

int main()
{
    ...
}

Le compilateur se plaindra certainement ici de la redéfinition de f(). C'est évident: sa définition est incluse deux fois! Cependant, source1.cpp ci-dessus se compilera sans problème lorsque header.h Contient les gardes d'inclusion appropriés . C'est prévu.

Pourtant, même lorsque les gardes d'inclusion sont présents et que le compilateur cessera de vous déranger avec un message d'erreur, le linker insistera sur le fait que plusieurs définitions soient trouvées lors de la fusion du code objet obtenu de la compilation de source1.cpp et source2.cpp, et refusera de générer votre exécutable.

Pourquoi cela arrive-t-il?

Fondamentalement, chaque fichier .cpp (Le terme technique dans ce contexte est unité de traduction) dans votre projet est compilé séparément et indépendamment. Lors de l'analyse d'un fichier .cpp, Le préprocesseur traitera toutes les directives #include Et étendra toutes les invocations de macros qu'il rencontre, et la sortie de ce traitement de texte pur sera donnée en entrée au compilateur pour la traduction en code objet. Une fois que le compilateur a terminé de produire le code objet pour une unité de traduction, il passe à la suivante et toutes les définitions de macro rencontrées lors du traitement de l'unité de traduction précédente sont oubliées.

En fait, compiler un projet avec n unités de traduction (fichiers .cpp) Revient à exécuter le même programme (le compilateur) n fois, à chaque fois avec une entrée différente: différente les exécutions du même programme ne partageront pas l'état des précédentes exécutions de programme . Ainsi, chaque traduction est effectuée indépendamment et les symboles du préprocesseur rencontrés lors de la compilation d'une unité de traduction ne seront pas mémorisés lors de la compilation d'autres unités de traduction (si vous y réfléchissez un instant, vous vous rendrez facilement compte qu'il s'agit en fait d'un comportement souhaitable).

Par conséquent, même si des gardes d'inclusion vous aident à empêcher les inclusions mutuelles récursives et redondant les inclusions du même en-tête dans une unité de traduction, ils ne peuvent pas détecter si la même définition est incluse dans différent unité de traduction.

Pourtant, lors de la fusion du code objet généré à partir de la compilation de tous les fichiers .cpp De votre projet, l'éditeur de liens will voit que le même symbole est défini plusieurs fois, et puisque cela viole la règle de définition unique . Conformément au paragraphe 3.2/3 de la norme C++ 11:

Chaque programme doit contenir exactement une définition de chaque fonction ou variable non en ligne qui est utilisée par odr dans ce programme; aucun diagnostic requis. La définition peut apparaître explicitement dans le programme, elle peut être trouvée dans la bibliothèque standard ou définie par l'utilisateur, ou (le cas échéant) elle est implicitement définie (voir 12.1, 12.4 et 12.8). Une fonction en ligne doit être définie dans chaque unité de traduction dans laquelle elle est utilisée par odr .

Par conséquent, l'éditeur de liens émettra une erreur et refusera de générer l'exécutable de votre programme.

Que dois-je faire pour résoudre mon problème?

Si vous souhaitez conserver votre définition de fonction dans un fichier d'en-tête qui est #include D par multiple unités de traduction (notez qu'aucun problème ne se posera surviennent si votre en-tête est #include d juste par un unité de traduction), vous devez utiliser le mot clé inline.

Sinon, vous devez conserver uniquement la déclaration de votre fonction dans header.h, En mettant sa définition (corps) dans un séparé .cpp Uniquement (c'est l'approche classique).

Le mot clé inline représente une demande non contraignante adressée au compilateur pour aligner le corps de la fonction directement sur le site d'appel, plutôt que de configurer un cadre de pile pour un appel de fonction normal. Bien que le compilateur n'ait pas à répondre à votre demande, le mot clé inline réussit à dire à l'éditeur de liens de tolérer plusieurs définitions de symboles. Selon l'article 3.2/5 de la norme C++ 11:

Il peut y avoir plusieurs définitions de a type de classe (article 9), type d'énumération (7.2), fonction en ligne avec liaison externe (7.1.2), modèle de classe (article 14), modèle de fonction non statique (14.5.6), membre de données statique d'un modèle de classe (14.5.1.3), fonction membre de un modèle de classe (14.5.1.1) ou une spécialisation de modèle pour laquelle certains paramètres de modèle ne sont pas spécifiés (14.7, 14.5.5) dans un programme à condition que chaque définition apparaisse dans une unité de traduction différente et à condition que les définitions satisfassent aux exigences suivantes [ ...]

Le paragraphe ci-dessus répertorie essentiellement toutes les définitions qui sont généralement placées dans les fichiers d'en-tête , car elles peuvent être incluses en toute sécurité dans plusieurs unités de traduction. En revanche, toutes les autres définitions avec liaison externe appartiennent aux fichiers source.

L'utilisation du mot clé static au lieu du mot clé inline entraîne également la suppression des erreurs de l'éditeur de liens en donnant à votre fonction liaison interne , ce qui fait que chaque unité de traduction détient un - copie privé de cette fonction (et de ses variables statiques locales). Cependant, cela aboutit finalement à un plus grand exécutable, et l'utilisation de inline devrait être préférée en général.

Une autre façon d'obtenir le même résultat qu'avec le mot clé static consiste à placer la fonction f() dans un espace de noms sans nom. Conformément au paragraphe 3.5/4 de la norme C++ 11:

Un espace de noms sans nom ou un espace de noms déclaré directement ou indirectement dans un espace de noms sans nom a une liaison interne. Tous les autres espaces de noms ont une liaison externe. Un nom ayant une étendue d'espace de noms qui n'a pas reçu de lien interne ci-dessus a le même lien que l'espace de noms englobant s'il s'agit du nom de:

- une variable; ou

- une fonction ; ou

- une classe nommée (article 9) ou une classe non nommée définie dans une déclaration typedef dans laquelle la classe a le nom typedef à des fins de liaison (7.1.3); ou

- une énumération nommée (7.2) ou une énumération non nommée définie dans une déclaration typedef dans laquelle l'énumération a le nom typedef à des fins de liaison (7.1.3); ou

- un énumérateur appartenant à une énumération avec liaison; ou

- un modèle.

Pour la même raison mentionnée ci-dessus, le mot clé inline doit être préféré.

128
Andy Prowl