412 votes

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

Je me retrouve souvent dans une situation où je suis confronté à de multiples erreurs de compilation/liaison 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++ dans différents fichiers d'en-tête. (peut également se produire dans le même fichier) . Mais heureusement ( ?) cela ne se produit pas assez souvent pour que je me souvienne de la solution à ce problème la prochaine fois que cela se reproduira.

Ainsi, pour faciliter le rappel à l'avenir, je vais poster un problème représentatif et sa solution. Les 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;
    }

23 votes

Lorsque l'on travaille avec Visual Studio, le /showIncludes aide beaucoup à déboguer ce genre de problèmes.

338voto

Roosh Points 726

La façon de voir les choses est de "penser comme un compilateur".

Imaginez que vous êtes en train d'écrire 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 .cc (rappelez-vous que le fichier .cc et non le .h est l'unité de compilation), vous devez allouer de l'espace pour l'objet A . Alors, combien d'espace ? Assez pour stocker B ! Quelle est la taille de B alors ? Assez pour stocker A ! Oups.

Il s'agit clairement d'une référence circulaire que vous devez briser.

Vous pouvez le casser en permettant au compilateur de réserver à la place autant d'espace qu'il le sait en amont - les pointeurs et les références, par exemple, seront toujours de 32 ou 64 bits (selon l'architecture) et donc si vous remplacez (l'un ou l'autre) par un pointeur ou une référence, les choses iraient bien. 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. Un peu. main() dit encore :

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

#include qui, à toutes fins utiles (si vous retirez le préprocesseur), se contente de copier le fichier dans le répertoire de l'utilisateur. .cc . Donc, en réalité, 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 que les B est - il n'a jamais vu le symbole avant.

Alors disons au compilateur B . C'est ce qu'on appelle un déclaration prospective 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;
}

Ce site travaux . Il n'est pas grand . Mais à ce stade, vous devriez avoir une idée du problème de la référence circulaire et de ce que nous avons fait pour le "corriger", même si la correction est mauvaise.

La raison pour laquelle ce correctif est mauvais est que la prochaine personne à #include "A.h" devra déclarer B avant de pouvoir l'utiliser et obtiendront une terrible #include erreur. 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 juste #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.

23 votes

"Le fait d'informer le compilateur au sujet de B est connu sous le nom de déclaration directe de B.

12 votes

Omg ! J'ai complètement oublié que les références sont connues en termes d'espace occupé. Enfin, maintenant je peux concevoir correctement !

60 votes

Mais vous ne pouvez toujours pas utiliser une fonction sur B (comme dans la question _b->Printt()).

112voto

Sandeep Datta Points 7344

Vous pouvez éviter les erreurs de compilation en supprimant les définitions de méthodes des fichiers d'en-tête et en laissant les classes contenir uniquement les déclarations de méthodes et les déclarations/définitions de variables. Les définitions de méthodes doivent être placées dans un fichier .cpp (comme l'indique le guide des meilleures pratiques).

L'inconvénient de la solution suivante (en supposant que vous ayez placé les méthodes dans le fichier d'en-tête pour les mettre en ligne) est que les méthodes ne sont plus mises en ligne par le compilateur et qu'essayer d'utiliser le mot-clé inline produit des erreurs de liaison.

//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;
}

0 votes

Merci. Cela a résolu le problème facilement. J'ai simplement déplacé les includes circulaires vers les fichiers .cpp.

4 votes

Que se passe-t-il si vous avez une méthode modèle ? Dans ce cas, vous ne pouvez pas vraiment la déplacer dans un fichier CPP, sauf si vous instanciez les modèles manuellement.

0 votes

Vous incluez toujours "A.h" et "B.h" ensemble. Pourquoi ne pas inclure "A.h" dans "B.h" et ensuite inclure seulement "B.h" dans "A.cpp" et "B.cpp" ?

23voto

dirkgently Points 56879

Des choses à retenir :

  • Cela ne fonctionnera pas si class A a un objet de class B en tant que membre ou vice versa.
  • La déclaration préalable 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.

Lisez la FAQ :

1 votes

Les liens que vous avez fournis ne fonctionnent plus, connaissez-vous les nouveaux liens à utiliser ?

15voto

epatel Points 32451

J'ai une fois résolu ce genre de problème en déplaçant toutes les inlines après la définition de la classe et en mettant 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+inlines sont définies avant que les inlines soient analysées.

En procédant ainsi, il est possible de conserver un grand nombre d'inlines dans les deux (ou plusieurs) fichiers d'en-tête. Mais il est nécessaire d'avoir comprennent des gardes .

Comme ceci

// 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 en B.h

0 votes

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 d'inlines, on n'aurait pas dû écrire le code comme il a été écrit depuis le début...

0 votes

Que se passe-t-il si un utilisateur inclut B.h d'abord ?

3 votes

Notez que votre garde d'en-tête utilise un identifiant réservé, tout ce qui comporte un double trait de soulignement adjacent est réservé.

7voto

Eduard Wirch Points 4102

J'ai déjà écrit un billet à ce sujet : Résoudre les 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;
}

4 votes

Veuillez noter que l'utilisation des interfaces et virtual a des répercussions sur les performances d'exécution.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X