Résoudre les erreurs de construction dues à la dépendance circulaire entre les classes

353

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;
    }
    
Autocrate
la source
23
Lorsque vous travaillez avec Visual Studio, l' indicateur / showIncludes aide beaucoup à déboguer ce type de problèmes.
essuyez

Réponses:

288

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.

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

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 ranger B! Quelle est la taille Balors? Assez pour ranger A! 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:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

Maintenant, les choses vont mieux. Quelque peu. main()dit toujours:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include, à toutes fins utiles (si vous retirez le préprocesseur) copie simplement le fichier dans le .cc . Donc vraiment, le .cc ressemble à:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

Vous pouvez voir pourquoi le compilateur ne peut pas gérer cela - il n'a aucune idée de ce qui Best - 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 .

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

Ç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éclarer Bavant de pouvoir l'utiliser et obtiendra une terrible #includeerreur. Déplaçons donc la déclaration dans Ah lui-même.

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

Et à Bh , à ce stade, vous pouvez simplement #include "A.h"directement.

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

HTH.

Roosh
la source
20
"Raconter le compilateur à propos de B" est connu comme une déclaration avancée de B.
Peter Ajtai
8
OMG! totalement ignoré le fait que les références sont connues en termes d'espace occupé. Enfin, maintenant je peux concevoir correctement!
kellogs
47
Mais vous ne pouvez toujours pas utiliser de fonction sur B (comme dans la question _b-> Printt ())
rank1
3
C'est le problème que j'ai. Comment introduire les fonctions avec une déclaration directe sans réécrire complètement le fichier d'en-tête?
sydan
101

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.

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

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;
}
Autocrate
la source
Merci. Cela a résolu le problème facilement. J'ai simplement déplacé les inclusions circulaires dans les fichiers .cpp.
Lenar Hoyt
3
Et si vous avez une méthode de modèle? Ensuite, vous ne pouvez pas vraiment le déplacer dans un fichier CPP sauf si vous instanciez manuellement les modèles.
Malcolm
Vous incluez toujours "Ah" et "Bh" ensemble. Pourquoi n'incluez-vous pas "Ah" dans "Bh" et n'incluez ensuite que "Bh" dans "A.cpp" et "B.cpp"?
Gusev Slava
28

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:

#pragma once
class A;

ah:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

bh:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

Les responsables des bibliothèques Aet Bdevraient 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:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

bh:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

... alors la recompilation du code pour "A" sera déclenchée par les modifications apportées à l'inclus b.fwd.het 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.hou a.ccau lieu de cela se déclare class B;lui - même:

  • si a.hou a.cca inclus b.hplus tard:
    • la compilation de A se terminera avec une erreur une fois qu'elle arrivera à la déclaration / définition conflictuelle 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).
  • sinon (si A n'a finalement pas inclus b.h- possible si A stocke / passe simplement Bs par pointeur et / ou référence)
    • les outils de génération reposant sur l' #includeanalyse et les horodatages des fichiers modifiés ne seront pas reconstruits A(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.

Tony Delroy
la source
2
C'est une façon vraiment propre de gérer les déclarations avancées. Le seul "inconvénient" serait dans les fichiers supplémentaires. Je suppose que vous incluez toujours a.fwd.hdans a.h, pour assurer qu'ils restent synchronisés. L'exemple de code manque lorsque ces classes sont utilisées. a.het b.hdevront 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.hcomprend a.het main.cppcomprendb.h
Farway
2
@ Farway Right sur tous les plans. Je n'ai pas pris la peine de le montrer main.cpp, mais c'est bien que vous ayez documenté ce qu'il devrait contenir dans votre commentaire. Santé
Tony Delroy
1
L'une des meilleures réponses avec une belle explication détaillée de pourquoi avec les choses à faire et à ne pas faire en raison des avantages et des inconvénients ...
Francis Cugler
1
@RezaHajianpour: il est logique d'avoir un en-tête de déclaration avant pour toutes les classes dont vous voulez des déclarations avant, circulaires ou non. Cela dit, vous ne les voudrez que lorsque: 1) l'inclusion de la déclaration réelle est (ou pourrait devenir plus tard) coûteuse (par exemple, elle comprend de nombreux en-têtes dont votre unité de traduction n'aurait pas besoin autrement), et 2) le code client est susceptibles de pouvoir utiliser des pointeurs ou des références aux objets. <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.
Tony Delroy
1
@RezaHajianpour: Je pense que vous avez la bonne idée, mais il y a un problème terminologique avec votre déclaration: "nous avons juste besoin que le type soit déclaré " serait juste. Le type déclaré signifie que la déclaration avant a été vue; il est défini une fois que la définition complète a été analysée (et pour cela, vous aurez peut-être besoin de plus de #includes).
Tony Delroy
20

Choses à retenir:

  • Cela ne fonctionnera pas si class Aa un objet en class Btant que membre ou vice versa.
  • La déclaration à venir est la voie à suivre.
  • L'ordre de déclaration est important (c'est pourquoi vous retirez les définitions).
    • Si les deux classes appellent des fonctions de l'autre, vous devez déplacer les définitions.

Lisez la FAQ:

dirkgently
la source
1
les liens que vous avez fournis ne fonctionnent plus, connaissez-vous les nouveaux auxquels vous référer?
Ramya Rao
11

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 #includeles 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

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

... et faire de même dans B.h

epatel
la source
Pourquoi? Je pense que c'est une solution élégante à un problème délicat ... quand on veut des inlines. Si on ne veut pas de ligne, on n'aurait pas dû écrire le code comme il a été écrit depuis le début ...
epatel
Que se passe-t-il si un utilisateur inclut d' B.habord?
M. Fooz du
3
Notez que votre protège-tête utilise un identifiant réservé, tout ce qui a des soulignements doubles adjacents est réservé.
Lars Viklund
6

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:

//Printer.h
class Printer {
public:
    virtual Print() = 0;
}

//A.h
#include "Printer.h"
class A: public Printer
{
    int _val;
    Printer *_b;
public:

    A(int val)
        :_val(val)
    {
    }

    void SetB(Printer *b)
    {
        _b = b;
        _b->Print();
    }

    void Print()
    {
        cout<<"Type:A val="<<_val<<endl;
    }
};

//B.h
#include "Printer.h"
class B: public Printer
{
    double _val;
    Printer* _a;
public:

    B(double val)
        :_val(val)
    {
    }

    void SetA(Printer *a)
    {
        _a = a;
        _a->Print();
    }

    void Print()
    {
        cout<<"Type:B val="<<_val<<endl;
    }
};

//main.cpp
#include <iostream>
#include "A.h"
#include "B.h"

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;
}
Eduard Wirch
la source
2
Veuillez noter que l'utilisation des interfaces et virtuala des impacts sur les performances d'exécution.
cemper93
4

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.

Tatyana
la source
2

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' '':

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

Fichier '' 'b.h' '':

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

Fichier '' 'main.cpp' '':

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}
madx
la source
1

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é:

  • la solution est exactement la même que celle initialement prévue
  • fonctions en ligne toujours en ligne
  • les utilisateurs de Aet Bpeuvent inclure Ah et Bh dans n'importe quel ordre

Créez deux fichiers, A_def.h, B_def.h. Ceux-ci ne contiendront que Ala Bdéfinition de ' et ':

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

public:
    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

public:
    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

Et puis, Ah et Bh contiendront ceci:

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.h"

inline A::A(int val) :_val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

inline B::B(double val) :_val(val)
{
}

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

inline void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

#endif

Notez que A_def.h et B_def.h sont des en-têtes "privés", utilisateurs Aet Bne doivent pas les utiliser. L'en-tête public est Ah et Bh

geza
la source
1
Est-ce que cela présente des avantages par rapport à la solution de Tony Delroy ? Les deux sont basés sur des en-têtes "helper", mais ceux de Tony sont plus petits (ils contiennent juste la déclaration directe) et ils semblent fonctionner de la même manière (au moins à première vue).
Fabio dit Réintégrer Monica
1
Cette réponse ne résout pas le problème d'origine. Il dit simplement "mettre les déclarations en avant dans un en-tête séparé". Rien sur la résolution de la dépendance circulaire (la question a besoin d'une solution où Ala Bdéfinition de ' et ' est disponible, la déclaration directe ne suffit pas).
geza
0

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 .ccfichiers, par exemple si vous souhaitez implémenter une bibliothèque d'en-tête uniquement.

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

// file: main.cc
#include "a.h"
int main(...) {
  B b;
  A a = b.get();
}
jkoendev
la source
0

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:

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

Il en résulterait le code suivant:

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

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:

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

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.

Bernd Baumanns
la source