IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Affichage des résultats du sondage: Quelle solution vous parais la mieux ?

Votants
11. Vous ne pouvez pas participer à ce sondage.
  • Solution 1

    3 27,27%
  • Solution 2

    8 72,73%
Langage C++ Discussion :

Inclusions circulaires, Comment BIEN faire ?


Sujet :

Langage C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre averti
    Inscrit en
    Avril 2009
    Messages
    15
    Détails du profil
    Informations personnelles :
    Âge : 37

    Informations forums :
    Inscription : Avril 2009
    Messages : 15
    Par défaut Inclusions circulaires, Comment BIEN faire ?
    Bonjour à tous,

    Ma question est un peu particulière, et ressemble a si méprendre a "Bonnet blanc ou Blanc bonnet ?"

    En effet lors de la création d'header, il arrive de faire des référances circulaires. On les bloquent par des #define et #ifndef et on ajoute des class ; pour les définitions. La question n'est pas là.

    Imaginons cette exemple :

    J'ai une classe A et une classe B, les deux se connaissent (j'ai donc besoin d'un include de A dans B et vise versa)

    Deux solutions s'oppose:

    Solution 1 : tout dans les Headers

    Code A.h : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    #ifndef __A_H
    #define __A_H
     
    #include "B.h"
     
    Class B;
     
    Class A {
       private :
          B * __b;
    };
     
    #endif  // __A_H
    Code A.cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    #include "A.h"
     
    /*code vide pour la demo*/
    Code B.h : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    #ifndef __B_H
    #define __B_H
     
    #include "A.h"
     
    Class A;
     
    Class B {
       private :
          A * __a;
    };
     
    #endif  // __B_H
    Code B.cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    #include "B.h"
     
    /*code vide pour la demo*/

    Avantages :
    • Pas de pollution de .h dans les .ccp (seullement le .h reliant le .cpp)
    • Déclaration des Class X et des includes (.h) dans le même fichier


    Solution 2 : un peu partout

    Code A.h : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    #ifndef __A_H
    #define __A_H
     
    Class B;
     
    Class A {
       private :
          B * __b;
    };
     
    #endif  // __A_H
    Code A.cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    #include "A.h"
    #include "B.h"
     
    /*code vide pour la demo*/
    Code B.h : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    #ifndef __B_H
    #define __B_H
     
    Class A;
     
    Class B {
       private :
          A * __a;
    };
     
    #endif  // __B_H
    Code B.cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    #include "B.h"
    #include "A.h"
     
    /*code vide pour la demo*/

    Avantages :
    • J'en cherche toujours ...



    Bien évidement dans les deux solutions si il y a des référancements non-circulaire à faire ils sont mis dans le .h et non dans le .cpp.


    Conclusion :

    Personnelement je trouve la solution 1 bien meilleur, mais on m'a dit "non, c'est pas bien faut coder la solution 2", et à la question "Pourquoi ?" je n'obtiens qu'un "Parce que c'est comme ca !" (pédagogie quand tu nous tiens ... )
    Je cherche donc déséperement à connaitre le fin mot de l'histoire et savoir si ma facon de faire (solution 1) est mauvaise et pourquoi ?

    Me doutant que c'est une question "Bonnet blanc ou Blanc bonnet ?" je veux savoir si l'un est plus efficace que l'autre, si l'un pose problème (norme ISO a tout hasard etc ...)


    Merci d'avance

    Marge

  2. #2
    Membre Expert

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Par défaut
    Citation Envoyé par Marge Voir le message
    Personnelement je trouve la solution 1 bien meilleur, mais on m'a dit "non, c'est pas bien faut coder la solution 2", et à la question "Pourquoi ?" je n'obtiens qu'un "Parce que c'est comme ca !" (pédagogie quand tu nous tiens ... )
    Bonjour,
    La vrai raison, c'est que ta solution n°1 est MAL sans compter d'être VRAIMENT PAS BIEN.


    Ok, Ok, je rigole.

    EDIT : (j'ai oublié un bout ici )
    La différence entre ta solution n°1 et la solution n°2 c'est que dans l'une les includes sont dans les .h et dans l'autre dans les .cpp. La deuxième est meilleure, car elle permet de réduire les temps de compilations en évitant de polluer les .h avec du superflu.

    Bon... un exemple sera surement plus parlant.
    (Dans tout le code qui vient, je n'ai pas mis les #ifndef...#endif, et j'ai utilisé des struct pour gagner en place.)

    On a trois classes, A, B et C.

    A possède une fonction membre qui prend une référence constante sur B. (ça pourrait être un pointeur)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
     
    // A.h
    struct B; // déclaration anticipée
    struct A
    {
       void fooA(const B& b) const;
    };
     
    //A.cpp
    #include "A.h"
    #include "B.h"
    #include <iostream>
     
    void A::fooA(const B& b) const
    {
       std::cout << "A\n";
       b.fooB();
    }
    B possède une donnée membre qui est un pointeur sur C. (ça pourrait être une référence)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     
    // B.h
    struct C; // déclaration anticipée
    struct B
    {
       B(C* c);
       void fooB() const;
       C* c_;
    };
     
    //B.cpp
    #include "B.h"
    #include "C.h"
    #include <iostream>
     
    B::B(C* c):c_(c)
    {
    }
     
    void B::fooB() const
    {
       std::cout << "B\n";
       c_->fooC();
    }
    C n'a aucune relation particulière avec les deux autres classes.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    // C.h
    struct C
    {
       void fooC() const;
    };
     
    //C.cpp
    #include "C.h"
    #include <iostream>
    void C::fooC() const
    {
    	std::cout << "C\n";
    }
    Enfin le main
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    #include "A.h"
    #include "B.h"
    #include "C.h"
    int main()
    {
       C* c = new C;
       B b(c);
       A a;
       a.fooA(b);
       return 0;
    }
    Qui affiche "A, B, C".

    En résumé, la classe A a besoin de B et B a besoin de C, par contre A et C n'ont pas connaissance l'une de l'autre.

    EDIT : Ok, tu as corrigé entre temps en rajoutant des pointeurs, le paragraphe qui suit tombe à l'eau.
    Le point central, c'est que le header de A déclare une fonction prenant une référence sur B, et le header de B déclare un pointeur membre sur C. Donc, le compilateur n'a pas besoin de connaitre la déclaration complète de C dans le header de B et n'a pas besoin non plus de la définition complète de B dans le header de A. Il lui suffit d'une déclaration anticipée pour savoir qu'il existe un type nommé "C" et un type nommé "B".

    Maintenant, imagine que l'on modifie C.h, par exemple pour rajouter une fonction ou un membre.

    C.h modifié => il faut recompiler C.cpp
    C.h modifié => B.cpp modifié => il faut recompiler B.cpp

    Par contre, imagine maintenant qu'on applique la solution "tout dans les header". On aurait A.h qui inclut B.h et B.h qui inclut C.h. Résultat :

    C.h modifié => il faut recompiler C.cpp
    C.h modifié => B.h modifié => il faut recompiler B.cpp
    B.h modifié => A.h modifié => il faut recompiler A.cpp

    Au final, une modification dans le header de C entrainerait une recompilation de A, qui n'a pourtant rien à voir.

    Voilà pourquoi l'on préconise d'utiliser les déclarations anticipées dès que possible et de n'inclure que le minimum indispensable dans les headers. Si l'on applique la solution "tout dans les header" partout dans son code, alors le moindre changement va se répercuter en cascade et c'est le monde entier qui recompile.


    Une anecdote pour finir !

    Lors de mon premier stage en entreprise, j'ai eu affaire pour la première fois à un programme C++ vraiment immense. Plusieurs millions de lignes de code, des milliers de fichiers .h et .cpp, des dizaines de programmeurs ayant travaillé dessus depuis environ 5 ans. Et personne n'avait vraiment fait attention à ce problème de déclaration anticipée, en pensant surement que c'était "bonnet blanc et blanc bonnet".

    Le résultat, c'est que les temps de compilation étaient vraiment devenues insoutenables. Pourtant, il y avait un système pour faire de la compilation distribuée en parallèle sur une dizaine de machine. (Et j'avoue avoir eu un choc en voyant la première fois le prog de compilation parallèle m'annoncer que ma compilation se faisait à 42 Ghz !)

    Las, la moindre modification entrainait des recompilations héroïques - un bon quart d'heure d'attente à chaque fois ! On devenait fou. Il a fallu qu'un des programmeurs de l'équipe stoppe toute activité pendant quelques jours, fasse un checkout monstrueux de l'intégralité du code source, puis épluche un par un tous les headers pour rajouter des déclarations anticipées partout où c'était possible.

    Une fois toute ses modifications validées, le temps de compilation moyen est brutalement tombé sous les 5 minutes.

  3. #3
    Inactif  
    Avatar de Mac LAK
    Profil pro
    Inscrit en
    Octobre 2004
    Messages
    3 893
    Détails du profil
    Informations personnelles :
    Âge : 51
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations forums :
    Inscription : Octobre 2004
    Messages : 3 893
    Par défaut
    Solution 1 pour ma part : quand j'inclus un .H, j'entends qu'il soit "complet". J'ai horreur des .H partiels qui réclament N autres .H pour pouvoir fonctionner, et où l'ordre d'inclusion va jouer fortement. Sans compter que, parfois, tu n'as l'erreur qu'au moment du link !
    Au moins, si c'est dans ton .H à toi, l'ordre d'inclusion est déjà correct, et tu peux alors aussi avoir des inclusions automatiques de librairies statiques via les .H si ton compilateur le permet (Visual possède un #pragma à cet effet, par exemple). C'est très pratique pour celui qui utilise tes modules.

    Si le temps de compilation est trop long, il faut :
    • Vérifier qu'il n'y a pas de multiples inclusions inutiles (un graphe de dépendances des fichiers te le montrera, Doxygen en génère de très utiles par exemple).
    • Vérifier que les .H ne recensent bien que le strict minimum en inclusions : juste ce qu'il faut pour compiler un programme minimaliste incluant juste une instance de la classe principale déclarée. Un .H qui incluerait "stdio.h", par exemple, n'est sûrement pas "minimal" côté inclusions... En ce sens, je suis totalement d'accord avec Arzar.
    • Vérifier qu'il n'y a pas trop de choses dans les .H : inutile par exemple d'y mettre les constantes privées, elles doivent être dans un .H séparé utilisé exclusivement par le .CPP correspondant.
    • Vérifier l'arbre d'inclusion des entêtes, et chercher s'il n'y a pas un endroit intéressant pour faire des entêtes précompilés.
    • Séparer au maximum les entêtes "privés" des entêtes "publics", de façon à limiter l'impact d'une modification interne sur le reste du projet. Une modification "privée" d'un module ne devrait JAMAIS provoquer la recompilation en chaîne de l'intégralité du projet !
    • Au besoin, passer par des classes abstraites, ou du masquage d'implémentation : un module réellement modulaire est rarement limité à un seul fichier CPP et un seul entête, je dirais que c'est le plus souvent un fichier .CPP, un .H "public" et un .H "privé" pour assurer un maximum d'indépendance. Je fais en général inclure l'entête public par l'entête privé, pour ma part, le .CPP n'incluant alors que l'entête privé.


    En espérant t'avoir été utile.
    Mac LAK.
    ___________________________________________________
    Ne prenez pas la vie trop au sérieux, de toutes façons, vous n'en sortirez pas vivant.

    Sources et composants Delphi sur mon site, L'antre du Lak.
    Pas de question technique par MP : posez-la dans un nouveau sujet, sur le forum adéquat.

    Rejoignez-nous sur : Serveur de fichiers [NAS] Le Tableau de bord projets Le groupe de travail ICMO

  4. #4
    gl
    gl est déconnecté
    Rédacteur

    Homme Profil pro
    Inscrit en
    Juin 2002
    Messages
    2 165
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 46
    Localisation : France, Isère (Rhône Alpes)

    Informations forums :
    Inscription : Juin 2002
    Messages : 2 165
    Par défaut
    Autant je te rejoins totalement sur le fait qu'un header doit être indépendant et inclure tout ce dont il a besoin.

    Autant je ne suis pas d'accord avec le choix systématique de la solution 1. Surtout que c'est contradictoire avec un de tes points suivants, à savoir :

    Citation Envoyé par Mac LAK Voir le message
    Vérifier que les .H ne recensent bien que le strict minimum en inclusions : juste ce qu'il faut pour compiler un programme minimaliste incluant juste une instance de la classe principale déclarée. Un .H qui incluerait "stdio.h", par exemple, n'est sûrement pas "minimal" côté inclusions... En ce sens, je suis totalement d'accord avec Arzar.
    En effet, dans le cas présenté ici, seule la déclaration anticipé de la seconde classe (class B est nécessaire dans le header, pas l'inclusion du header complet (#include "B.h").

    Pour revenir sur le découpage des modules entre header publics et header privés, il est parfois possible et utile d'aller plus loin en créant parmi les header publics des header ne prenant en charge que les déclarations anticipées et justement destinés typiquement à être inclus dans d'autres header comme ici.

  5. #5
    Inactif  
    Avatar de Mac LAK
    Profil pro
    Inscrit en
    Octobre 2004
    Messages
    3 893
    Détails du profil
    Informations personnelles :
    Âge : 51
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations forums :
    Inscription : Octobre 2004
    Messages : 3 893
    Par défaut
    Citation Envoyé par gl Voir le message
    En effet, dans le cas présenté ici, seule la déclaration anticipé de la seconde classe (class B est nécessaire dans le header, pas l'inclusion du header complet (#include "B.h").
    Pas forcément contradictoire, cela dépend en fait de la classe "maître" et de l'existence d'une telle classe... Je n'ai par contre peut-être pas assez bien expliqué ma notion de "privé" et "public".

    1. Si l'utilisateur ne va utiliser QUE la classe A, par exemple, il faut au maximum tenter de masquer la classe B, y compris en utilisant une classe mère commune (vide et/ou abstraite au besoin) déclarée dans "A.h" afin de masquer son implémentation (et vive le "dynamic_cast<B*>" par la suite...).
    2. Si l'utilisateur a besoin des deux classes tout le temps, inclure un seul des entêtes règlera le problème.
    3. S'il n'a besoin que de la classe B, reprendre le premier cas et inverser les lettres...


    Citation Envoyé par gl Voir le message
    Pour revenir sur le découpage des modules entre header publics et header privés, il est parfois possible et utile d'aller plus loin en créant parmi les header publics des header ne prenant en charge que les déclarations anticipées et justement destinés typiquement à être inclus dans d'autres header comme ici.
    Sauf que s'il y a déclaration anticipée, à un moment où à un autre, il va bien falloir l'implémenter, cette fameuse classe "B"... Et donc inclure son entête, ce qui serait "mal" si l'utilisateur doit le faire manuellement dans son code... Surtout s'il n'a pas, à priori, besoin de la classe B !

    Pour détailler un peu plus :
    • Si les deux classes sont "publiques", la solution 1 est très bien.
    • Si l'une des classes est "privée", alors le fichier d'entête de la classe "publique" doit absolument masquer l'existence de la classe "esclave".


    Dans les deux cas, j'ai expliqué ce qu'il faudrait faire, même si ce n'est pas forcément toujours simple à faire bien entendu. L'utilisation d'une classe mère commune, vide et/ou abstraite est la plus simple, même si ce n'est pas forcément la plus "propre"...
    Mac LAK.
    ___________________________________________________
    Ne prenez pas la vie trop au sérieux, de toutes façons, vous n'en sortirez pas vivant.

    Sources et composants Delphi sur mon site, L'antre du Lak.
    Pas de question technique par MP : posez-la dans un nouveau sujet, sur le forum adéquat.

    Rejoignez-nous sur : Serveur de fichiers [NAS] Le Tableau de bord projets Le groupe de travail ICMO

  6. #6
    gl
    gl est déconnecté
    Rédacteur

    Homme Profil pro
    Inscrit en
    Juin 2002
    Messages
    2 165
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 46
    Localisation : France, Isère (Rhône Alpes)

    Informations forums :
    Inscription : Juin 2002
    Messages : 2 165
    Par défaut
    Citation Envoyé par Mac LAK Voir le message
    Si l'utilisateur ne va utiliser QUE la classe A, par exemple, il faut au maximum tenter de masquer la classe B, y compris en utilisant une classe mère commune (vide et/ou abstraite au besoin) déclarée dans "A.h" afin de masquer son implémentation (et vive le "dynamic_cast<B*>" par la suite...).
    L'utilisation de la déclaration anticipée de la classe B donne certes l'information de l'existence de cette classe mais ne donne aucune information sur l'implémentation de cette classe.
    A priori, il n'est pas donc nécessaire de mettre en place une classe commune vide ou d'autre mécanisme.

    Citation Envoyé par Mac LAK Voir le message
    Si l'utilisateur a besoin des deux classes tout le temps, inclure un seul des entêtes règlera le problème.
    Si l'utilisateur a besoin des deux classes, je ne vois pas où se situe le problème de devoir inclure dans le .cpp les deux en-têtes.
    Lorsque tu inclues <iostream> tu ne t'attends pas à ce que <string> soit inclus automatiquement même lorsque tu en as besoin, pourquoi en serait-il différemment ici ?

    Le problème de dépendance inclus par de tels constructions viennent:
    • De devoir respecter un ordre d'inclusion.
    • De devoir inclure dans le .cpp un header dont tu ne te sers pas dans le code mais qui est nécessaire pour un autre header.


    Ces deux problèmes sont réglés par l'utilisation d'une déclaration anticipée dans le header.

    Citation Envoyé par Mac LAK Voir le message
    Sauf que s'il y a déclaration anticipée, à un moment où à un autre, il va bien falloir l'implémenter, cette fameuse classe "B"... Et donc inclure son entête, ce qui serait "mal" si l'utilisateur doit le faire manuellement dans son code... Surtout s'il n'a pas, à priori, besoin de la classe B !
    Oui la classe B doit être implémenter, elle le sera dans les fichiers .cpp correspondant, certainement pas dans le code de l'utilisateur.

    De même, le header de la classe B sera probablement inclus dans les fichiers sources de la classe A mais là encore ça ne concerne pas l'utilisateur.

    Si l'utilisateur a besoin de manipuler la classe B, je ne vois pas de problème à ce qu'il inclus les header de la classe B.

    S'il manipule uniquement la classe A, dans ce cas effectivement, il n'a pas à devoir inclure inclure les headers de B, mais la déclaration anticipée dans les headers de A suffit pour l'exemple fourni.

    Le but est que le header de A soit autonome, pas qu'il inclut des headers qu'il ne lui sont pas utiles mais qui pourrait éventuellement servir dans le code utilisateur.
    Inclure le header de B dans celui de A alors que la déclaration anticipée de B est suffisante, apporte un couplage trop important entre A et B qui risque de provoquer plus de problème que d'en résoudre.

  7. #7
    Membre Expert

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Par défaut
    Citation Envoyé par Mac LAK Voir le message
    Solution 1 pour ma part : quand j'inclus un .H, j'entends qu'il soit "complet". J'ai horreur des .H partiels qui réclament N autres .H pour pouvoir fonctionner, et où l'ordre d'inclusion va jouer fortement. Sans compter que, parfois, tu n'as l'erreur qu'au moment du link !
    Tu pourrais préciser ? Je ne comprends pas trop ce que tu veux dire par .h partiels

    Pourquoi la solution n°2 entrainerait des problèmes d'ordre d'inclusion ou des inclusions redondantes pour l'utilisateur ?

  8. #8
    Rédacteur
    Avatar de 3DArchi
    Profil pro
    Inscrit en
    Juin 2008
    Messages
    7 634
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juin 2008
    Messages : 7 634
    Par défaut
    Bonsoir,
    Je suis de ceux qui préfère la solution 2. Outre le problème de temps de compilation présenté par Arzar, il y a à mon avis un moindre couplage entre les différentes classes avec cette solution. En effet, imagine la classe suivante :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    class AImpl;
    class A
    {
    public:
    protected:
    // jamais d'utilisation de AImpl dans l'interface publique ou protégée
    private:
    AImpl *p_impl;
    };
    AImpl est un détail d'implémentation de A et n'a pas à être connue des autres classes utilisant A. Or la première solution, en imposant d'inclure un 'AImpl.h' oblige les autres classes à connaître des choses dont elles n'ont que faire. En effet, AImpl n'intervenant que dans la partie privée de A, elles n'y ont aucun accès. Mais la solution 1 leur impose de connaître ce bidule qui ne leur sert à rien.

Discussions similaires

  1. Comment "bien" faire ses CSS
    Par sliderman dans le forum Mise en page CSS
    Réponses: 11
    Dernier message: 30/06/2008, 20h38
  2. [Debutant] comment bien faire une variable ?
    Par nighthammer dans le forum iReport
    Réponses: 2
    Dernier message: 27/05/2008, 11h56
  3. comment bien faire organiser ses header
    Par DEVfan dans le forum C++
    Réponses: 43
    Dernier message: 29/04/2008, 11h58
  4. Class de mesh, comment bien faire ?
    Par Tosh dans le forum Développement 2D, 3D et Jeux
    Réponses: 8
    Dernier message: 24/04/2007, 12h24
  5. [MS/SQL 2K][Securité][Restauration]Comment bien faire ?
    Par jbat dans le forum MS SQL Server
    Réponses: 3
    Dernier message: 18/04/2007, 11h18

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo