Question

Je me trouve souvent dans une situation où je suis confronté à plusieurs erreurs de compilation / de l'éditeur de liens 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 < em> (peut aussi arriver 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 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;
    }
    
Était-ce utile?

La solution

Pour penser à cela, il faut & "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 le .h est l'unité de compilation), vous devez allouer de l'espace pour l'objet A. Alors, combien d'espace alors? Assez pour stocker B! Quelle est la taille de main() alors? Assez pour stocker #include! Oops.

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

Vous pouvez le casser en permettant au compilateur de réserver autant d’espace qu’il en sait sur upfront - les pointeurs et les références, par exemple, auront toujours 32 ou 64 bits (selon l’architecture). Ainsi, si vous avez remplacé (soit un) par un pointeur ou une référence, les choses seraient géniales. Disons que nous remplaçons dans #include "A.h":

// 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. <=> dit toujours:

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

<=>, pour tous les domaines et objectifs (si vous retirez le préprocesseur), il suffit de copier le fichier dans le fichier .cc . Alors 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 que <=> est - il n'a même jamais vu le symbole auparavant.

Alors, parlons au compilateur de <=>. C’est ce qu’on appelle une déclaration anticipée , qui est décrite 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 avoir une compréhension du problème de référence circulaire et de ce que nous avons fait pour & "Résoudre &"; mais la solution est mauvaise.

Si ce correctif est incorrect, c'est parce que la prochaine personne devant <=> devra déclarer <=> avant de pouvoir l'utiliser et obtiendra une terrible <=> 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 , vous pouvez simplement <=> 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.

Autres conseils

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).

Le problème 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;
}

Points à 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 des déclarations 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 vers l'extérieur.

Lisez la FAQ:

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 ....

Bonne pratique: faire suivre les en-têtes de la déclaration

Comme le montre l'en-tête <iosfwd> de la bibliothèque Standard, le bon moyen de fournir des déclarations en aval aux autres consiste à créer un en-tête de déclaration en aval . 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 être responsables de la synchronisation de leurs en-têtes de déclaration d'envoi avec leurs en-têtes et leurs fichiers d'implémentation, par exemple si le responsable de " B < !> quot; arrive et réécrit le code pour être ...

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;

... puis recompilation du code pour " A " sera déclenché par les modifications apportées à l'élément inclus b.fwd.h et devrait se terminer correctement.

Pratique médiocre mais courante: déclarer des éléments dans d'autres bibliothèques

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

  • si b.h ou #include incluait <=> plus tard:
    • la compilation de A se terminera par une erreur une fois que la déclaration / définition en conflit de <=> aura été contournée (c.-à-d. le changement ci-dessus en B cassé A et tout autre client utilisant des déclarations en aval au lieu de fonctionner de manière transparente).
  • sinon (si A n'incluait pas finalement <=> - possible si A stocke / passe autour de Bs par pointeur et / ou référence)
    • Les outils de construction qui reposent sur <=> l'analyse et les horodatages des fichiers modifiés ne reconstruiront pas <=> (et son code dépendant par la suite) après la modification en B, générant des erreurs lors de la liaison ou de l'exécution. Si B est distribué en tant que DLL chargée à l'exécution, codez dans & Quot; A & Quot; peut ne pas trouver les symboles mutilés différemment au moment de l’exécution, qui peuvent ne pas être gérés suffisamment bien pour déclencher un arrêt ordonné ou une fonctionnalité réduite de manière acceptable.

Si le code de A comporte des spécialisations de modèle / " traits " pour l'ancien <=>, ils ne prendront pas effet.

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

Cette façon de procéder permet d’avoir encore un tas d’inlines dans les deux (ou plusieurs) fichiers d’en-tête. Mais il est nécessaire que inclue 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 dans B.h

J'ai déjà écrit un billet à ce sujet: Résolution des dépendances circulaires dans 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;
}

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). & # 8217; impossible de scinder la déclaration et la définition en fichiers séparés, mais vous pouvez les structurer comme s'ils étaient dans des fichiers séparés.

L'exemple simple présenté sur Wikipedia a fonctionné pour moi. (vous pouvez lire la description complète à l'adresse http://fr.wikipedia.org/ /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;
}

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 les dépendances plus complexes.

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

  • la solution est exactement la même que celle initialement prévue
  • les fonctions en ligne toujours en ligne
  • 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. Ceux-ci ne contiendront que les définitions de <=> et de <=>:

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

Dans certains cas, il est possible de définir une méthode ou un constructeur de la classe B dans le fichier d'en-tête de la classe A afin de résoudre les dépendances circulaires impliquant des définitions. De cette manière, vous éviterez de placer des définitions dans les .cc fichiers, par exemple si vous souhaitez implémenter une bibliothèque contenant uniquement un en-tête.

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

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 autoriser & "; 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 mieux comprendre, imaginez 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 inline 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 compile pas car B :: Do nécessite un type complet de A défini plus tard.

Pour vous assurer que le code source est compilé, 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 le & "; Entête publique &";

.

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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top