Je me retrouve souvent dans une situation où je suis confronté à plusieurs erreurs de compilation / éditeur de liens dans un projet C ++ en raison de mauvaises décisions de conception (prises par quelqu'un d'autre :)) qui conduisent à des dépendances circulaires entre les classes C ++ dans différents fichiers d'en-tête (peut également se produire dans le même fichier) . Mais heureusement (?) Cela n'arrive pas assez souvent pour que je me souvienne de la solution à ce problème pour la prochaine fois que cela se reproduira.
Donc, à des fins de rappel facile à l'avenir, je vais publier un problème représentatif et une solution avec lui. De meilleures solutions sont bien sûr les bienvenues.
A.h
class B; class A { int _val; B *_b; public: A(int val) :_val(val) { } void SetB(B *b) { _b = b; _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B' } void Print() { cout<<"Type:A val="<<_val<<endl; } };
B.h
#include "A.h" class B { double _val; A* _a; public: B(double val) :_val(val) { } void SetA(A *a) { _a = a; _a->Print(); } void Print() { cout<<"Type:B val="<<_val<<endl; } };
main.cpp
#include "B.h" #include <iostream> int main(int argc, char* argv[]) { A a(10); B b(3.14); a.Print(); a.SetB(&b); b.Print(); b.SetA(&a); return 0; }
c++
compiler-errors
circular-dependency
c++-faq
Autocrate
la source
la source
Réponses:
La façon d'y penser est de "penser comme un compilateur".
Imaginez que vous écrivez un compilateur. Et vous voyez du code comme celui-ci.
Lorsque vous compilez le fichier .cc (n'oubliez pas que le .cc et non le .h est l'unité de compilation), vous devez allouer de l'espace pour l'objet
A
. Alors, combien d'espace alors? Assez pour rangerB
! Quelle est la tailleB
alors? Assez pour rangerA
! Oops.Clairement une référence circulaire que vous devez briser.
Vous pouvez le casser en autorisant le compilateur à réserver à la place autant d'espace qu'il en sait sur les éléments initiaux - les pointeurs et les références, par exemple, seront toujours de 32 ou 64 bits (selon l'architecture) et donc si vous les avez remplacés (l'un ou l'autre) par un pointeur ou une référence, les choses seraient super. Disons que nous remplaçons dans
A
:Maintenant, les choses vont mieux. Quelque peu.
main()
dit toujours:#include
, à toutes fins utiles (si vous retirez le préprocesseur) copie simplement le fichier dans le .cc . Donc vraiment, le .cc ressemble à:Vous pouvez voir pourquoi le compilateur ne peut pas gérer cela - il n'a aucune idée de ce qui
B
est - il n'a même jamais vu le symbole auparavant.Parlons donc du compilateur
B
. Ceci est connu comme une déclaration à terme , et est discuté plus loin dans cette réponse .Ça marche . Ce n'est pas génial . Mais à ce stade, vous devriez avoir une compréhension du problème des références circulaires et de ce que nous avons fait pour le "corriger", bien que la correction soit mauvaise.
La raison pour laquelle ce correctif est mauvais est que la prochaine personne
#include "A.h"
devra déclarerB
avant de pouvoir l'utiliser et obtiendra une terrible#include
erreur. Déplaçons donc la déclaration dans Ah lui-même.Et à Bh , à ce stade, vous pouvez simplement
#include "A.h"
directement.HTH.
la source
Vous pouvez éviter les erreurs de compilation si vous supprimez les définitions de méthode des fichiers d'en-tête et laissez les classes contenir uniquement les déclarations de méthode et les déclarations / définitions de variable. Les définitions de méthode doivent être placées dans un fichier .cpp (comme le dit une directive de bonnes pratiques).
L'inconvénient de la solution suivante est (en supposant que vous avez placé les méthodes dans le fichier d'en-tête pour les aligner) que les méthodes ne sont plus alignées par le compilateur et que l'utilisation du mot clé inline génère des erreurs de l'éditeur de liens.
la source
Je réponds tard, mais il n'y a pas de réponse raisonnable à ce jour, bien qu'il s'agisse d'une question populaire avec des réponses très appréciées ...
Meilleure pratique: en-têtes de déclaration avant
Comme illustré par l'en-
<iosfwd>
tête de la bibliothèque Standard , la bonne façon de fournir des déclarations avancées pour les autres est d'avoir un en-tête de déclaration directe . Par exemple:a.fwd.h:
ah:
b.fwd.h:
bh:
Les responsables des bibliothèques
A
etB
devraient chacun être responsables de la synchronisation de leurs en-têtes de déclaration directe avec leurs en-têtes et fichiers d'implémentation, donc - par exemple - si le responsable de "B" arrive et réécrit le code pour être ...b.fwd.h:
bh:
... alors la recompilation du code pour "A" sera déclenchée par les modifications apportées à l'inclus
b.fwd.h
et devrait se terminer proprement.Pauvre mais pratique courante: déclarer des choses dans d'autres bibliothèques
Dites - au lieu d'utiliser un en-tête de déclaration avancée comme expliqué ci-dessus - codez
a.h
oua.cc
au lieu de cela se déclareclass B;
lui - même:a.h
oua.cc
a inclusb.h
plus tard:B
(c'est-à-dire que le changement ci-dessus pour B a cassé A et tout autre client abusant des déclarations avancées, au lieu de travailler de manière transparente).b.h
- possible si A stocke / passe simplement Bs par pointeur et / ou référence)#include
analyse et les horodatages des fichiers modifiés ne seront pas reconstruitsA
(et son code dépendant plus loin) après le passage à B, provoquant des erreurs au moment de la liaison ou de l'exécution. Si B est distribué en tant que DLL chargée lors de l'exécution, le code dans «A» peut ne pas trouver les symboles modifiés différemment lors de l'exécution, qui peuvent ou non être traités suffisamment bien pour déclencher un arrêt ordonné ou une fonctionnalité acceptablement réduite.Si le code de A a des spécialisations / "traits" de modèle pour les anciens
B
, ils ne prendront pas effet.la source
a.fwd.h
dansa.h
, pour assurer qu'ils restent synchronisés. L'exemple de code manque lorsque ces classes sont utilisées.a.h
etb.h
devront tous deux être inclus car ils ne fonctionneront pas isolément: `` `` //main.cpp #include "ah" #include "bh" int main () {...} `` Ou l'un d'eux doit être pleinement inclus dans l'autre comme dans la question d'ouverture. Oùb.h
comprenda.h
etmain.cpp
comprendb.h
main.cpp
, mais c'est bien que vous ayez documenté ce qu'il devrait contenir dans votre commentaire. Santé<iosfwd>
est un exemple classique: il peut y avoir quelques objets de flux référencés depuis de nombreux endroits, et il<iostream>
y a beaucoup à inclure.#include
s).Choses à retenir:
class A
a un objet enclass B
tant que membre ou vice versa.Lisez la FAQ:
la source
Une fois, j'ai résolu ce type de problème en déplaçant toutes les lignes après la définition de la classe et en plaçant
#include
les autres classes juste avant les lignes dans le fichier d'en-tête. De cette façon, on s'assure que toutes les définitions + inlines sont définies avant que les inlines soient analysées.Faire cela permet d'avoir toujours un tas de lignes dans les deux (ou plusieurs) fichiers d'en-tête. Mais il faut avoir des gardes inclus .
Comme ça
... et faire de même dans
B.h
la source
B.h
abord?J'ai écrit un article à ce sujet une fois: Résolution des dépendances circulaires en c ++
La technique de base consiste à découpler les classes à l'aide d'interfaces. Donc dans votre cas:
la source
virtual
a des impacts sur les performances d'exécution.Voici la solution pour les modèles: Comment gérer les dépendances circulaires avec les modèles
La clé pour résoudre ce problème est de déclarer les deux classes avant de fournir les définitions (implémentations). Il n'est pas possible de diviser la déclaration et la définition en fichiers séparés, mais vous pouvez les structurer comme si elles se trouvaient dans des fichiers séparés.
la source
L'exemple simple présenté sur Wikipedia a fonctionné pour moi. (vous pouvez lire la description complète sur http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )
Fichier '' 'a.h' '':
Fichier '' 'b.h' '':
Fichier '' 'main.cpp' '':
la source
Malheureusement, toutes les réponses précédentes manquent de détails. La bonne solution est un peu lourde, mais c'est la seule façon de le faire correctement. Et il évolue facilement, gère également les dépendances plus complexes.
Voici comment vous pouvez le faire, en conservant exactement tous les détails et la convivialité:
A
etB
peuvent inclure Ah et Bh dans n'importe quel ordreCréez deux fichiers, A_def.h, B_def.h. Ceux-ci ne contiendront que
A
laB
définition de ' et ':Et puis, Ah et Bh contiendront ceci:
Notez que A_def.h et B_def.h sont des en-têtes "privés", utilisateurs
A
etB
ne doivent pas les utiliser. L'en-tête public est Ah et Bhla source
A
laB
définition de ' et ' est disponible, la déclaration directe ne suffit pas).Dans certains cas, il est possible de définir une méthode ou un constructeur de classe B dans le fichier d'en-tête de classe A pour résoudre les dépendances circulaires impliquant des définitions. De cette façon, vous pouvez éviter d'avoir à mettre des définitions dans des
.cc
fichiers, par exemple si vous souhaitez implémenter une bibliothèque d'en-tête uniquement.la source
Malheureusement, je ne peux pas commenter la réponse de geza.
Il ne dit pas simplement "mettre des déclarations dans un en-tête séparé". Il dit que vous devez renverser les en-têtes de définition de classe et les définitions de fonction en ligne dans différents fichiers d'en-tête pour autoriser les «dépendances différées».
Mais son illustration n'est pas vraiment bonne. Parce que les deux classes (A et B) n'ont besoin que d'un type incomplet l'une de l'autre (champs / paramètres de pointeur).
Pour mieux le comprendre, imaginez que la classe A possède un champ de type B et non B *. De plus, les classes A et B veulent définir une fonction en ligne avec des paramètres de l'autre type:
Ce code simple ne fonctionnerait pas:
Il en résulterait le code suivant:
Ce code ne se compile pas car B :: Do a besoin d'un type complet de A qui sera défini plus tard.
Pour vous assurer qu'il compile le code source devrait ressembler à ceci:
C'est exactement possible avec ces deux fichiers d'en-tête pour chaque classe qui a besoin de définir des fonctions en ligne. Le seul problème est que les classes circulaires ne peuvent pas simplement inclure "l'en-tête public".
Pour résoudre ce problème, je voudrais suggérer une extension de préprocesseur:
#pragma process_pending_includes
Cette directive doit différer le traitement du fichier actuel et compléter toutes les inclusions en attente.
la source