web-dev-qa-db-fra.com

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

Je me trouve souvent dans une situation où je suis confronté à plusieurs erreurs de compilation/linker dans un projet C++ en raison de mauvaises décisions de conception (prises par quelqu'un d'autre :)) qui entraînent des dépendances circulaires entre les classes C++ de différents fichiers d'en-tête (can arriver aussi 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.

Par conséquent, pour faciliter les rappels à l'avenir, je vais publier un problème représentatif ainsi qu'une solution. 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;
    }
    
297
Autodidact

La façon de penser à cela est de "penser comme un compilateur".

Imaginez que vous écrivez un compilateur. Et vous voyez un 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 (rappelez-vous que .cc et non .h est l'unité de compilation), vous devez allouer de l'espace à l'objet A. Alors, combien d'espace alors? Assez pour stocker B! Quelle est la taille de B alors? Assez pour stocker A! Oops.

Clairement une référence circulaire que vous devez casser.

Vous pouvez le casser en autorisant le compilateur à réserver à la place autant d’espace qu’il en sait. Les pointeurs et les références, par exemple, auront toujours 32 ou 64 bits (selon l’architecture). un pointeur ou une référence, les choses seraient géniales. 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, pour tous les domaines et toutes les utilisations (si vous retirez le préprocesseur), copie simplement le fichier dans le fichier .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 qu'est B - il n'a même jamais vu le symbole auparavant.

Alors disons au compilateur à propos de B. Ceci est connu sous le nom de déclaration en aval , et est discuté plus en détail dans cette réponse .

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

Ceci fonctionne. Ce n'est pas génial. Mais à ce stade, vous devriez comprendre le problème des références circulaires et ce que nous avons fait pour le "résoudre", bien que le correctif soit mauvais.

La raison pour laquelle ce correctif est incorrect est que la prochaine personne à #include "A.h" devra déclarer B avant de pouvoir l'utiliser et obtiendra une terrible erreur #include. Déplaçons donc la déclaration dans A.h lui-même.

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

Et dans B.h, à 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.

237
Roosh

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 variables. Les définitions de méthodes doivent être placées dans un fichier .cpp (comme le stipule un guide de bonnes pratiques). 

L'inconvénient de la solution suivante est (en supposant que vous ayez placé les méthodes dans le fichier d'en-tête pour les aligner) que les méthodes ne sont plus en ligne 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;
}
93
Autodidact

Choses à retenir:

  • Cela ne fonctionnera pas si class A a un objet de class B en tant que membre ou inversement. 
  • La déclaration anticipée est la voie à suivre.
  • L’ordre de déclaration est important (c’est pourquoi vous déplacez les définitions) .
    • Si les deux classes appellent des fonctions de l'autre, vous devez déplacer les définitions.

Lire la FAQ:

17
dirkgently

Je suis en retard pour répondre à cette question, mais il n'y a pas une seule réponse raisonnable à ce jour, bien qu'il s'agisse d'une question populaire avec des réponses très votées ....

Meilleure pratique: en-têtes de déclaration

Comme illustré dans l'en-tête <iosfwd> de la bibliothèque standard, le moyen approprié de fournir des déclarations en aval à d'autres consiste à avoir un en-tête de déclarationforward. Par exemple:

a.fwd.h:

#pragma once
class A;

a.h:

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

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

b.fwd.h:

#pragma once
class B;

b.h:

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

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

Les responsables des bibliothèques A et B devraient chacun être responsables de la synchronisation de leurs en-têtes de déclaration forward avec leurs en-têtes et leurs fichiers de mise en œuvre. Ainsi, par exemple, si le mainteneur de "B" arrive et réécrit le code pour qu'il soit ...

b.fwd.h:

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

b.h:

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 au b.fwd.h inclus et devrait se terminer proprement.


Pratique médiocre mais courante: faire une déclaration dans d'autres bibliothèques

Dites - au lieu d'utiliser un en-tête de déclaration forward comme expliqué ci-dessus - code dans a.h ou a.cc à la place de la déclaration directe class B; lui-même:

  • si a.h ou a.cc a inclus b.h plus tard:
    • la compilation de A se terminera par une erreur une fois que la déclaration/définition en conflit de B sera en conflit (c’est-à-dire que la modification ci-dessus apportée à B a cassé A et que tout autre client a abusé des déclarations, au lieu de fonctionner de manière transparente).
  • sinon (si A n'a finalement pas inclus b.h - possible si A stocke/fait simplement passer Bs par pointeur et/ou référence)
    • les outils de compilation reposant sur l'analyse #include et les horodatages de fichier modifiés ne reconstruiront pas A (et son code dépendant supplémentaire) après la modification en B, générant des erreurs au moment de la liaison ou de l'exécution. Si B est distribué en tant que DLL chargée à l'exécution, le code dans "A" risque de ne pas trouver les symboles différemment déformés au moment de l'exécution, ce qui peut être ou ne pas être assez bien géré pour déclencher un arrêt ordonné ou une fonctionnalité réduite de manière acceptable.

Si le code de A contient des spécialisations/"traits" de modèles pour l'ancienne B, ils ne prendront effet.

14
Tony Delroy

Une fois, j'ai résolu ce genre de problème en déplaçant tout inlines après la définition de classe et en plaçant le #include pour les autres classes juste avant le inlines dans le fichier d'en-tête. De cette façon, on s'assure que toutes les définitions + les lignes sont définies avant que les lignes ne soient analysées.

En procédant ainsi, il est toujours possible d’avoir beaucoup d’inlines dans les deux (ou plusieurs) fichiers d’en-tête. Mais il faut que include gardes.

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

11
epatel

J'ai écrit un billet à ce sujet une fois: Résolution de 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;
}
6
Eduard Wirch

Voici la solution pour les modèles: Comment gérer les dépendances circulaires avec des modèles

La solution à ce problème consiste à déclarer les deux classes avant de fournir les définitions (implémentations). Il n’est pas possible de scinder la déclaration et la définition dans des fichiers séparés, mais vous pouvez les structurer comme s’ils se trouvaient dans des fichiers séparés.

3
Tatyana

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;
}
2
madx

Malheureusement, je ne peux pas commenter la réponse de Geza.

Il ne dit pas simplement "mettre les déclarations en avant dans un en-tête séparé". Il dit qu'il faut 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 permettre 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 (champs/paramètres de pointeur).

Pour le comprendre, imaginez mieux que la classe A possède un champ de type B et non pas 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"

Cela donnerait 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 nécessite un type complet de A défini plus tard.

Pour être sûr qu'il compile le code source, il 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 doit définir des fonctions inline. 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 terminer tous les inclusions en attente.

0
Bernd Baumanns

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 manière, vous éviterez de mettre des définitions dans les fichiers .cc, par exemple si vous souhaitez implémenter une bibliothèque avec 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();
}

0
Jonas

Malheureusement, il manque quelques détails dans toutes les réponses précédentes. La solution correcte est un peu lourde, mais c'est la seule façon de le faire correctement. Et il évolue facilement, gère également des dépendances plus complexes.

Voici comment procéder, en conservant exactement tous les détails et la convivialité:

  • la solution est exactement la même que celle initialement prévue
  • fonctions inline toujours inline
  • les utilisateurs de A et B peuvent inclure A.h et B.h dans n’importe quel ordre

Créez deux fichiers, A_def.h et B_def.h. Celles-ci ne contiendront que les définitions de A et B:

// 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 ensuite, A.h et B.h 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", les utilisateurs de A et B ne doivent pas les utiliser. L'en-tête public est A.h et B.h.

0
geza