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

C++ Discussion :

Peut-on faire des classes qui dépendent l'une de l'autre ?


Sujet :

C++

  1. #1
    Futur Membre du Club
    Profil pro
    Inscrit en
    Décembre 2007
    Messages
    20
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2007
    Messages : 20
    Points : 8
    Points
    8
    Par défaut Peut-on faire des classes qui dépendent l'une de l'autre ?
    Bonjour,

    Je fais un programme avec une classe abstraite A qui contient la méthode virtuelle pure M, et deux classes dérivées de A, B et C, pour les quelles j'aurais envie d'utiliser l'une ou l'autre dans la définition des méthodes virtuelles M.

    En pratique ça donne un truc du genre :

    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
    25
     
    class A{
    protected:
    ...
    public:
    virtual A *M() =0;
    };
     
    class B : public A{
    public:
    A *M(){
    C *tmp=new C;
    *tmp=C(this,untruc);
    A *res=new A;
    *res=tmp->M();
    return res;
    }
     
    class C : public A{
    public:
    A *M(){
    B *res=new B;
    *res=B(this,untruc);
    return res;
    }
    Voila donc si on rentre comme ça, bien entendu ça ne marche pas, car B::M() a besoin de C::M(), mais C::M() a besoin que B soit définie.

    J'avais pensé déclarer la méthode B::M() en dehors de B, après avoir défini C (et C::M()), mais ça ne marche pas non plus car si je ne donne pas à B une méthode M, B est une classe abstraite et je n'ai donc pas le droit de faire dans C::M() mon *res=B(this,untruc);.

    Actuellement je m'en sort avec une troisième classe qui est en fait un cas particulier de B, mais que je me garde bien de faire hériter de cette dernière.
    Conceptuellement je n'aime pas trop cette solution, mais je ne vois pas comment faire autrement.

    Quelqu'un a-t-il une meilleure idée ?

    Merci

    --
    Z.

  2. #2
    Expert confirmé

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2007
    Messages
    1 895
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 48
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Septembre 2007
    Messages : 1 895
    Points : 4 551
    Points
    4 551
    Par défaut
    Citation Envoyé par Zenigata Voir le message
    Bonjour,

    Je fais un programme avec une classe abstraite A qui contient la méthode virtuelle pure M, et deux classes dérivées de A, B et C, pour les quelles j'aurais envie d'utiliser l'une ou l'autre dans la définition des méthodes virtuelles M.

    Voila donc si on rentre comme ça, bien entendu ça ne marche pas, car B::M() a besoin de C::M(), mais C::M() a besoin que B soit définie.

    J'avais pensé déclarer la méthode B::M() en dehors de B, après avoir défini C (et C::M()), mais ça ne marche pas non plus car si je ne donne pas à B une méthode M, B est une classe abstraite et je n'ai donc pas le droit de faire dans C::M() mon *res=B(this,untruc);.

    Actuellement je m'en sort avec une troisième classe qui est en fait un cas particulier de B, mais que je me garde bien de faire hériter de cette dernière.
    Conceptuellement je n'aime pas trop cette solution, mais je ne vois pas comment faire autrement.

    Quelqu'un a-t-il une meilleure idée ?

    Merci

    --
    Z.
    1) Ne pas faire.
    Les références circulaires ne sont pas une solution acceptable dans le cadre d'un design orienté objet. Si tu as des références circulaire, tu introduit de la viscosité et de la fragilité dans ton architecture (fragilité: une modification peut avoir un effet non désiré autre part dans le code source; viscosité: au final, il devient plus aisé de rajouter hack sur hack plutôt que de bien faire les choses; les deux ensembles: mort assurée à plus ou moins court terme).

    2) Ton code est une étrange étrangeté
    Cf l'initialisation de tmp dans B::M(). Sait tu que tu as le droit d'écrire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class B : public A{
    public:
    A *M(){
    C tmp;
    return tmp.M();
    } };
    Ou même encore plus court:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    class B : public A{
    public:
    A *M() { return C().M(); }
    };
    Mais ce n'est pas le débat. Sans vouloir enfoncer le clou ou paraître condescedant, je te conseillerais de revoir quelques bases du langage: il me semble que tu sais mal la notion de pointeur et d'allocation mémoire.

    3) Mais, que fais tu de la pile ?
    B::M() appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M(). Si je continue aussi longtemps que le code que tu proposes, il est fort probable que je me fasse assasiner par les modérateurs. Ton programme, lui, va se faire assassiner par une exception système du type "stack overflow". Mais encore une fois, ce n'est pas le débat.

    4) J'ai dit, ne pas faire.
    Mais puisque tu pose la question, je réponds quand même: le modèle de compilation du C++ impose que l'utlisation d'un symbole soit précédé par sa déclaration ou sa définition (qui fait aussi office de déclaration). La forme suivante:
    (avec les accolades) est une définition de classe. Puisque tu n'as que des définitions de classes, tu te retrouves bloqué (parce que la définition de B nécessite C qui nécessite B qui... OK, j'arrête). Comment régler le problème ? Et bien il suffit de non pas définir B (ou C) mais seulement de déclarer cette classe. Et comment fais-je ? Deux solutions:
    a) "forward" declaration: en déclarant une classe en avance (ce qui en fait avertit que la classe en question existe, sans définir son contenu), on permet à une unité de compilation d'utiliser ce symbole. Le cas est similaire à la déclaration d'une fonction. La déclaration d'une classe est créée comme suit:
    ou NomClasse est la nom de la classe.
    Problème: en agissant ainsi, on crée un type incomplet, qui ne peut pas être utilisé tant que la définition de la classe n'a pas été trouvée parce que le compilateur ne connait pas la taille de ce type (entre autre) et ne sait en outre pas quels membres le compose. De fait:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class C;
    class B  {
      A* M() {
        return C().M(); // ERREUR DE COMPILATION: C est un type incomplet
      }
    };
    En pratique, les forward declarations ne sont utiles que dans la définition d'une interface, tant que le compilateur n'a pas besoin d'avair d'informations sur la définition de la classe:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    class C;
    class B {
      void f(C* c) // ok: un pointeur sur C a une taille connue, indépendant de C
      { ... }
    };
    b) Séparation des unités de compilations en fichiers header et fichier source: le modèle de compilation du C++ autorise de découper une unité de compilation en N fichiers différents. Généralement, on place les déclarations de fonctions ET les définitions de classe dans des fichiers header et les définitions des fonctions et fonctions membres de classes dans les fichiers C++.
    Dans notre cas, on obtient:
    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
    25
    26
    27
    // fichier B.h
    class B: public A {
      virtual A* M();
    };
     
    // fichier C.h
    class C: public A {
      virtual A* M();
    };
     
    // fichier B.cpp
    #include "B.h"
    #include "C.h"
     
    A* B::M()
    {
      return C().M();
    }
     
    // fichier C.cpp
    #include "C.h"
    #include "B.h"
     
    A* C::M()
    {
      return B(param1, param2).M();
    }
    Et hop, ni vu ni connu je t'embrouille.

    5) Je sais, je me répète
    Les références circulaires sont vraiment une mauvaise idée

  3. #3
    Futur Membre du Club
    Profil pro
    Inscrit en
    Décembre 2007
    Messages
    20
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2007
    Messages : 20
    Points : 8
    Points
    8
    Par défaut
    Citation Envoyé par Emmanuel Deloget Voir le message
    1) Ne pas faire.
    Les références circulaires ne sont pas une solution acceptable dans le cadre d'un design orienté objet. Si tu as des références circulaire, tu introduit de la viscosité et de la fragilité dans ton architecture (fragilité: une modification peut avoir un effet non désiré autre part dans le code source; viscosité: au final, il devient plus aisé de rajouter hack sur hack plutôt que de bien faire les choses; les deux ensembles: mort assurée à plus ou moins court terme).
    Oui, c'est vrai qu'en songeant à la référence circulaire on se dit que ça peut mal se passer, mais en l'occurrence je pense que ça correspond à un esprit orienté objet car je ne vois pas comment représenter autrement les objets que ces classes représentent et ce n'est pas une boucle complète car l'un a besoin d'un objet de la classe de l'autre, l'autre d'une méthode.

    En gros M est la dérivation, B est une fonction puissance (qui pointe vers deux autres fonctions pour faire F1^F2) et C est une fonction log (qui pointe vers une autre fonction et vers NULL). A est la classe ancêtre de tous les noeuds qui peuvent constituer un graphe d'une représentation symbolique d'une fonction.

    Et donc pour dériver f^g, je dois donc écrire f^g * ( g' log(f) + f'/f g) et (f'/f) est f' * f^(-1).

    Pour éviter de boucler les solutions sont de rajouter une classe diviser ou une classe puissances entières, mais je cherche à minimiser le nombre de classes binaires non commutatives.

    2) Ton code est une étrange étrangeté
    Cf l'initialisation de tmp dans B::M(). Sait tu que tu as le droit d'écrire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class B : public A{
    public:
    A *M(){
    C tmp;
    return tmp.M();
    } };
    Ou même encore plus court:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    class B : public A{
    public:
    A *M() { return C().M(); }
    };
    Mais ce n'est pas le débat. Sans vouloir enfoncer le clou ou paraître condescedant, je te conseillerais de revoir quelques bases du langage: il me semble que tu sais mal la notion de pointeur et d'allocation mémoire.
    Tu peux tout à fait dire cela, c'est sans doute vrai, je ne suis pas vraiment programmeur. Après au début j'avais bien mis un truc ressemblant à ta version encore plus courte, mais suite à un comportement étrange on m'a dit de mettre des "new", j'ai donc mis des new.

    3) Mais, que fais tu de la pile ?
    B::M() appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M() qui appelle B::M() qui appelle C::M(). Si je continue aussi longtemps que le code que tu proposes, il est fort probable que je me fasse assasiner par les modérateurs. Ton programme, lui, va se faire assassiner par une exception système du type "stack overflow". Mais encore une fois, ce n'est pas le débat.
    Non non non, là c'est B::M() qui appelle C::M() qui crée un B (et pas un B::M()), donc la dépendance n'est pas si tordue, car il "suffit" d'avoir un C::M() qui a besoin d'un B dont le B::M() à besoin du C::M().

    4) J'ai dit, ne pas faire.
    Oui mais ça colle si bien aux maths de faire ainsi.

    a) forward
    Problème: en agissant ainsi, on crée un type incomplet, qui ne peut pas être utilisé tant que la définition de la classe n'a pas été trouvée parce que le compilateur ne connait pas la taille de ce type (entre autre) et ne sait en outre pas quels membres le compose. De fait:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class C;
    class B  {
      A* M() {
        return C().M(); // ERREUR DE COMPILATION: C est un type incomplet
      }
    };
    Oui, en ayant défini une partie de B seulement, j'ai eu un peu le même problème, B restait une classe abstraite, et je ne pouvais pas l'utiliser dans C

    b) Séparation des unités de compilations en fichiers header et fichier source:
    Ah oui en effet c'est une solution qui devrait marcher !

    Merci.

    5) Je sais, je me répète
    Les références circulaires sont vraiment une mauvaise idée
    Oui, après je suis assez entêté, et quand le concept me parait beau, je m'y accroche. Je trouve ça bête de créer une classe qui ne sert conceptuellement à rien car on peut déjà faire des objets qui ont toutes les mêmes caractéristiques avec les classes existantes, d'autant plus qu'idéologiquement la classe à rajouter devrait hériter de B.

    Merci beaucoup pour ces explications.

    --
    Z.

  4. #4
    Expert confirmé

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2007
    Messages
    1 895
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 48
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Septembre 2007
    Messages : 1 895
    Points : 4 551
    Points
    4 551
    Par défaut
    Citation Envoyé par Zenigata Voir le message
    Oui mais ça colle si bien aux maths de faire ainsi.
    Ce n'est pas tout à fait une bonne raison. La relation de sous-typage (et donc de généralisation, en suivant le lien à l'envers) en programmation orienté objet est très différente de la notion de généralisation utilisée en mathématique. Pour un exemple simple: un carré est un rectangle. Si tu te bases sur cette relation pour créer une classe Carre qui dérive de Rectangle, tu te rends vite compte que bien que les concepts soit liés mathématiquement, les propriétés de l'un et leur comportement ne sont pas compatible. Que doivent faire Carre::setWidth() et Carre::setHeight() écrits en séquence ? Ces fonctions ont du sens si elles sont appliquées à une instance de Rectangle, beacoup moins sur un Carre.

  5. #5
    Futur Membre du Club
    Profil pro
    Inscrit en
    Décembre 2007
    Messages
    20
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2007
    Messages : 20
    Points : 8
    Points
    8
    Par défaut
    Citation Envoyé par Emmanuel Deloget Voir le message
    Que doivent faire Carre::setWidth() et Carre::setHeight() écrits en séquence ? Ces fonctions ont du sens si elles sont appliquées à une instance de Rectangle, beacoup moins sur un Carre.
    Ben Carre::setHeight() fait la même chose que Carre::setWidth() (qui peut même faire pareil que Carre::setSideSize(). A priori ça ne me choque aucunement que par spécialisation certaines fonctions se trouvent changer, c'est d'ailleurs à cela que sert la virtualisation, et il se trouve que pour le carré, on changer la largeur à un sens, même si cela s'accompagne d'un changement de hauteur.

    Ensuite dans le cas précis de mon exemple, où j'ai des classe d'expression mathématiques (somme [d'expressions], produit[d'expressions], log, exponentielle, puissance, etc...) chacune dérivant d'une classe ancêtre abstraite, si je fait une classe puissance entière, c'est une spécialisation de la classe puissance quelconque (même si ça permet de se dériver plus simplement) et pour le coup tout ce qui a un sens sur les puissances pas entières en a un sur celles entières.

    Enfin bon autant je pense pouvoir faire plein de bêtises en C++, autant dans ce cas précis je pense que c'est plutôt très logique de chercher à ne pas rajouter une classe inutile juste pour éviter le petit jeu de référence pseudo circulaire pour deux raisons :

    * l'ajout de cette classe romprait avec la logique d'héritage en place car elle serait nécessairement uen spécialisation d'une classe existante (aussi bien au sens des maths qu'au sens informatique)
    * l'ajout de cette classe rajoute du code alors qu'il n'y en a pas besoin (bon d'accord, avec mes new truc je suis pas vraiment à ça près)...

    Allez, merci bien pour ces éclaircissements.

    --
    Z.

  6. #6
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Points : 16 213
    Points
    16 213
    Par défaut
    Citation Envoyé par Zenigata Voir le message
    Ben Carre::setHeight() fait la même chose que Carre::setWidth() (qui peut même faire pareil que Carre::setSideSize(). A priori ça ne me choque aucunement que par spécialisation certaines fonctions se trouvent changer, c'est d'ailleurs à cela que sert la virtualisation, et il se trouve que pour le carré, on changer la largeur à un sens, même si cela s'accompagne d'un changement de hauteur.
    Donc, j'ai un code client, écrit avant que la classe carré n'existe :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    void f(Rectangle &r)
    {
      double oldSurface = r.surface();
      r.setHeigth(2*r.heigth());
      assert(r.surface == 2*oldSurface);
    }
    Ce code marche très bien, il a été testé et tout.... Et un jour, arrive la classe carré telle que tu l'as définie :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    Carre c;
    f(c); // Boom !
    Je te conseille de rechercher lsp (liskov substitution principle) ou programmation par contrat (On a le droit dans la surcharge d'une fonction virtuelle d'alléger les pré-conditions et de renforcer les post-conditions, rien d'autre).

    Pour ton problème, je l'ai parcouru de très loin, et je ne suis forcément choqué, c'est juste une application classique du design pattern composite.

  7. #7
    Expert éminent sénior
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 379
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 379
    Points : 41 573
    Points
    41 573
    Par défaut
    Cela dépend: Si le contrat de Rectangle ne garantit pas que ça marche, alors le carré ne transgresse pas le contrat.

    Bref, il faut avoir le contrat sous les yeux pour savoir ce que l'on est autorisé à faire ou non...

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. [XL-2007] Trois listes déroulantes qui dépendent l'une de l'autre.
    Par hsh63 dans le forum Excel
    Réponses: 3
    Dernier message: 17/12/2013, 21h09
  2. 3 ComboBox qui dépendent l'une de l'autre
    Par XMMMX dans le forum Composants
    Réponses: 3
    Dernier message: 25/04/2012, 18h05
  3. Sélectionner des colonnes qui dépendent d'une requête
    Par socomajor dans le forum Langage SQL
    Réponses: 6
    Dernier message: 14/10/2011, 13h05
  4. Réponses: 6
    Dernier message: 06/10/2008, 12h06
  5. Des classes qui discutent...
    Par comme de bien entendu dans le forum Langage
    Réponses: 4
    Dernier message: 27/03/2006, 17h45

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