Partage
  • Partager sur Facebook
  • Partager sur Twitter

Liste d'objets polymorphes et distinction de type

    22 septembre 2017 à 9:05:35

    Hello :)

    Je me retrouve avec une liste d'objets polymorphes, instances d'une hiérarchie de classes que j'ai créée. Pour distinguer certaines sous hiérarchies, j'utilise des prédicats.

    class Element
    {
    public:
    
       virtual std::string getText() const = 0;
    
       virtual bool isValue() const { return false; }
    };
    
    class ValueElement : public Element
    {
    public:
    
       virtual bool isValue() const override { return true; }
    };
    
    class IntValue : public ValueElement
    {
    public:
    
       virtual std::string getText() const override
       {
          return "int";
       }
    };
    
    class OtherElement : public Element
    {
    public:
    
       virtual std::string getText() const override
       {
          return "foo";
       }
    };

    Ici l'utilisation de isValue me permet de détecter tous les ValueElement, et d'y appliquer les méthodes qui y sont propres.

    std::list<Element*> elements;
    
    for (auto element : elements)
    {
       if (element->isValue())
       {
          ElementValue *ev = static_cast<ElementValue*>(element);
          // ...
       }
    }

    Je pense que l'utilisation de ce type de méthode booléennes est assez répandu dans certaines conceptions objet, je ne sais pas si c'est le meilleur choix. Ça permet notamment de caster certains objets en un "super-type plus précis".


    Maintenant je voulais savoir, si toutes ces classes font partie d'une lib et que l'on veut y ajouter une nouvelle sous-hiérarchie avec une nouvelle classe abstraite héritant de Element, comment distinguer ces sous-types sans pouvoir ajouter un prédicat dans la classe Element (qui serait redéfini dans la nouvelle classe abstraite) ?

    J'ai pensé au dynamic_cast mais étant donné qu'il peut s'agir de plusieurs types dynamiques différents je pense que ça ne fonctionnera pas.

    J'espère avoir été clair. ^^

    Merci. ;)

    -
    Edité par Maluna34 22 septembre 2017 à 9:08:47

    • Partager sur Facebook
    • Partager sur Twitter
      22 septembre 2017 à 9:29:49

      Lu'!

      Maluna34 a écrit:

      Pour distinguer certaines sous hiérarchies, j'utilise des prédicats. Je pense que l'utilisation de ce type de méthode booléennes est assez répandu dans certaines conceptions objet, je ne sais pas si c'est le meilleur choix. Ça permet notamment de caster certains objets en un "super-type plus précis".

      C'est super crade.

      Maluna34 a écrit:

      J'ai pensé au dynamic_cast mais étant donné qu'il peut s'agir de plusieurs types dynamiques différents je pense que ça ne fonctionnera pas.

      C'est encore plus crade.

      Pour les programmes de ce genre, un type algébrique est plus adapté qu'une hiérarchie de classes. En C++, ce n'est pas pratique du tout de faire de tels types. Cela dit il y a std::variant qui rend ça plus simple même si ce n'est pas la panacée.

      Pour le multiple-dispatch en C++ voir ce post de cette semaine : https://openclassrooms.com/forum/sujet/poo-simple-mais-complique-a-votre-avis .

      • Partager sur Facebook
      • Partager sur Twitter

      Posez vos questions ou discutez informatique, sur le Discord NaN | Tuto : Preuve de programmes C

        22 septembre 2017 à 14:02:41

        Salut,

        Maluna34 a écrit

        Je pense que l'utilisation de ce type de méthode booléennes est assez répandu dans certaines conceptions objet, je ne sais pas si c'est le meilleur choix. Ça permet notamment de caster certains objets en un "super-type plus précis".

        Et source d'innombrables bugs sur le long terme.  Car la technique aura "tellement bien marché" la première fois que tu en arriveras très vite à décider de l'utiliser à un autre endroit du code.  Puis à un autre, et  encore à un autre...

        Seulement, pour que la technique fonctionne, tu dois impérativement veiller à tester toutes les possibilités, à n'oublier aucune des classes dérivées de ta hiérarchie (ou, à tout le moins, à prendre systématiquement en compte toutes les classes les plus dérivées).  Car, si tu en oublies une, tu auras un cas d'utilisation qui ne sera pas couvert, une classe qui "ne réagira pas comme elle devrait le faire".

        Or, il arrivera forcément un jour où tu voudras une classe à ta hiérarchie, afin de prendre en charge la nouvelle "super fonctionnalité" à laquelle tu viens de penser.  Tu penseras donc -- peut-être -- à rajouter ce "cas particulier" à un ou deux endroits dans ton code.  Mais la loi de Finagle nous dit aussi que tu en oublieras forcément et que la seule question qu'il faudra alors se poser sera "combien d'endroits dans le code vas tu oublier?".

        Partant de là, chaque endroit du code que tu auras oublié de modifier provoquera forcément un bug, et la seule question qu'il faudra alors se poser sera "combien de temps te faudra-t-il pour t'en apercevoir?" Car le temps est le pire ennemi du "chasseur de bug": plus il passe, plus les bugs sont difficiles à corriger.

        Par chance, toutes les données savent toujours en permanence précisément de quel type elle sont, et ce, même si tu as été "assez idiot" pour l'oublier à un moment donné (bien que tu n'aies souvent pas d'autre choix).

        Rajoutes à ce fait les fonctions virtuelles et les fonctions surchargées (en gardant en tête le fait que toute fonction correspond à un service particulier que tu es en droit de t'attendre à être en mesure de faire appel), et tu as "tout ce qu'il faut" pour ne jamais (ou très peu s'en faut) avoir recours au RTTI et au transtypage dans ton code.

        Le principe qui te permet d'éviter d'y avoir recours s'appelle le double dispatch.  L'une de ses mises en oeuvres les plus connues étant le patron de conception visiteur.  Mais ce n'est pas la seule!.

        Cette pratique fera que le compilateur deviendra ton meilleur allié, car, si tu oublies de surcharger une fonction pour le type réel de ton objet, il te le signalera systématiquement; ce qui t'obligera à rajouter cette surcharge bien avant d'en arriver ne serait-ce qu'en période de tests.

        • Partager sur Facebook
        • Partager sur Twitter
        Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
          22 septembre 2017 à 16:08:14

          En complément de ce qu'ont dit mes VDD, il faut peut être se poser la question en amont, c'est à dire envisager le fait que stocker des éléments trop disparates dans une seule collection n'est peut être pas une si bonne idée que ça...

          -
          Edité par int21h 22 septembre 2017 à 16:08:45

          • Partager sur Facebook
          • Partager sur Twitter
          Mettre à jour le MinGW Gcc sur Code::Blocks. Du code qui n'existe pas ne contient pas de bug
            22 septembre 2017 à 23:12:23

            int21h a écrit:

            En complément de ce qu'ont dit mes VDD, il faut peut être se poser la question en amont, c'est à dire envisager le fait que stocker des éléments trop disparates dans une seule collection n'est peut être pas une si bonne idée que ça...

            -
            Edité par int21h il y a environ 6 heures


            Ben oui.

            Par exemple on a des trucs qui sont Dessinables (ils ont une fonction membre draw()) , et d'autres qui sont Animables (fonction animate).

            Pour quelle raisons aurait-on un type qui recouvre les deux ? pour avoir un conteneur commun ? Et après faut parcourir la liste de tous les trucs mélangés et faire du transtypage dynamique pour animer ceux qui sont animables, et dessiner ceux qui sont dessinables ?  C'est du temps perdu.

             Si on tient vraiment à unifier, il faut faire remonter les services à une classe parente commune qui aura à la fois animate() et draw(). On suppose que tout le monde est animable et dessinable, c'est juste qu'il  y en a pour qui ça ne fait rien.

            Ca coute moins de travail de lancer une fonction qui ne fait rien, que d'appeler une fonction de "typage" qui nous dira si on peut appeler l'autre.

            -
            Edité par michelbillaud 22 septembre 2017 à 23:18:07

            • Partager sur Facebook
            • Partager sur Twitter
              23 septembre 2017 à 0:27:23

              michelbillaud a écrit:

              int21h a écrit:

              En complément de ce qu'ont dit mes VDD, il faut peut être se poser la question en amont, c'est à dire envisager le fait que stocker des éléments trop disparates dans une seule collection n'est peut être pas une si bonne idée que ça...

              -
              Edité par int21h il y a environ 6 heures


              Ben oui.

              Par exemple on a des trucs qui sont Dessinables (ils ont une fonction membre draw()) , et d'autres qui sont Animables (fonction animate).

              Pour quelle raisons aurait-on un type qui recouvre les deux ? pour avoir un conteneur commun ? Et après faut parcourir la liste de tous les trucs mélangés et faire du transtypage dynamique pour animer ceux qui sont animables, et dessiner ceux qui sont dessinables ?  C'est du temps perdu.

               Si on tient vraiment à unifier, il faut faire remonter les services à une classe parente commune qui aura à la fois animate() et draw(). On suppose que tout le monde est animable et dessinable, c'est juste qu'il  y en a pour qui ça ne fait rien.

              Ca coute moins de travail de lancer une fonction qui ne fait rien, que d'appeler une fonction de "typage" qui nous dira si on peut appeler l'autre.

              -
              Edité par michelbillaud il y a 14 minutes

              Comme bien souvent entre nous, je te comprends, mais je crois que tu laisses l'arbre te cacher la foret.

              En effet, tu le dis toi-même: un objet peut être traçable (mais pas animable), animable (mais pas tracable) ou les deux.

              Si tu pars sur cette idée (tout à fait correcte, au demeurant), tu auras sans doute créé des interfaces pour traçable et pour animable.  Et tu auras sans doute aussi créé une classe de base "globale" te permettant de représenter "n'importe quel objet implémentant n'importe laquelle de tes interface", car, comme tu le fais si bien remarquer, tu pourrais vouloir, à certains moments (au moins) disposer de tous les objets en même temps.

              Le seul truc que tu semble perdre de vue, c'est que ce n'es pas parce que sohaites pouvoir disposer, à certains moments clés de tous les objets de tous les types que tu es obligé de te les coltiner en permanence!

              Or, même java et C# acceptent parfaitement l'idée qu'une interface puisse intervenir exactement comme une classe dans le processus cher à l'orienté objet qu'est la substituabilité.

              A vrai dire, même ces langages prennent en compte le fait que la seule restriction claire que l'on doit faire planer sur les interfaces, c'est qu'elle ne peuvent être pas instanciées (ni détruites, mais c'est son pendant "miroir") par elle-même; qu'elles ne peuvent l'être qu'au travers de classes (concrètes) qui les implémentent. 

              Ils ont utilisé le moyen le plus simple qui est (était?) mis à leur disposition : les fonctions virtuelles pures font que le compilateur refusera d'instancier une interface, vu qu'il n'aime pas le vide.

              Mais une fois que tu as posé cette restriction, il n'y a rien qui t'empêche de placer une (référence sur) une interface dans une collection quelconque!

              Avant C++11 et l'arrivée de std::reference_wrapper ou des pointeurs intelligents, nous ne disposions de manière standard que de la notion de pointeurs, car une référence ne peut pas être placée dans une collection.  Et je reconnais très honnêtement que cela posait parfois problème ;)

              Mais, depuis C++11, tu disposes de tellement de solutions offertes par la bibliothèque standard qu'il n'y a absolument aucune raison pour ne pas faire en sorte de disposer d'un coté, d'une collection qui ne contiendrait que les éléments traçables et de l'autre, d'une collection qui ne contiendrait que des éléments animables!

              A la lecture de cette phrase, tu voudras sans doute t'écrier que c'est trop compliqué, à cause de la nécessité de synchronisation de ces collections (et de la complexité que cela induit) ou à cause de la redondance que cela peut impliquer.  Mais, regardons y d'un peu plus près.

              Je vois, sans même y avoir réfléchi plus de deux minutes, au moins deux possibilités pour réduire la complexité.

              D'un coté, il est facile de mettre un système de signaux et de slots en place.  Sans aller chercher ceux de boost ou de Qt (qui sont de véritables usines à gaz), il est possible d'en mettre en place en une grosse centaine de lignes de code avec C++11.

              Un peu de double dispatch, un peu de template, et l'ajout ou la suppression d'un élément dans ta collection "globale" peut se répercuter très facilement sur celles qui seront intéressées par l'ajout ou par l'élément de l'élément.

              Mais l'ajout d'un élément est sans doute le cadet de nos soucis.  Ce qui importe surtout, c'est que l'on puisse ne faire appels qu'aux éléments qui existent (encore) depuis la collection d'élément tracables (ou animables).

              Nous pourrions donc tout aussi bien (même si je trouve cela "moins bon" que l'idée d'avoir un système de signaux et de slots) faire en sorte que ces collections contiennent... des std::weak_ptr pour lesquels le shared_ptr associé se trouverait dans la collection "globale".

              On peut trouver tout un tas de raisons pour lesquelles la deuxième solution ne serait pas adaptée.  C'est pour cette raison que je préférerais de loin une solution à base de signaux et de slots (ou à base d'événements) ;).  Mais bon, chacun voit midi à sa porte, hein?

              Le deuxième point sur lequel tu te seras sans doute récrié étant la redondance des informations, je vais aussi en parler un peu, et juste te poser une seule question à ce sujet: quelle redondance?

              Car, si tu y regardes un peu sérieusement, tu te rendras compte que d'un coté, nous n'avons que des éléments qui sont connus comme étant des objets de base, au départ desquels nous ne pouvons appeler que des fonctions exposées par le type de base et que nous avons "ailleurs" des éléments dont tout ce que nous savons, c'est ... qu'il sont tracable (ou animable).  Et à partir desquels nous ne pouvons appeler que les fonctions exposées par ces interface.

              Dés lors, nous nous trouvons dans une situation dans laquelle:

              toutes les données n'existent jamais qu'une seule et unique fois en mémoire (au niveau de la collection "globale") et où

              nous ne pouvons, au travers des "copies supposées" de toutes manières accéder qu'à un ensemble très restreint de de fonctionnalités, correspondant à chaque fois à une des interfaces proposées.

              La redondance supposée est donc nulle, aussi bien en termes de données qu'en termes de fonctionnalités.

              Après toi, les puristes viendront sans doute s'écrier que "ce n'est pas très cache friendly".  Et ce sera sans doute la seule remarque qu'il me sera impossible de contredire.  Du moins, sans faire entrer en jeu des notions comme les MemoryPool et autres joyeusetés du genre.

              Je leur dirais cependant -- dans l'espoir d'atténuer leur douleur -- que c'est malheureusement inhérent à l'allocation dynamique de la mémoire sous toutes ses formes: que ce soit à l'aide de new, de std::make_shared ou de std::make_unique n'y changera rien ;)

              • Partager sur Facebook
              • Partager sur Twitter
              Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                23 septembre 2017 à 0:39:40

                Mais d'un autre côté si on pense en terme de service, doter une classe d'une fonction (donc d'un service) qui ne fait rien, c'est plutôt moyen et il y a par ailleurs un risque, celui de laisser entendre  que la classe dérivée rend effectivement un service, alors que ce dernier est totalement fictif. D'un point de vue conceptuel, je trouve ça assez perturbant et je pense que ce type de raisonnement mène tout droit vers des God Objects, ce qui à mon sens, constitue une erreur de conception majeure. Je suis un feignant patenté, fervent adepte du principe de parcimonie de surcroît, donc prévoir du code qui n'a pas d'utilité fonctionnelle réelle, c'est un peu contre ma religion ^^ Si je me casse le c.. à coder une fonction, c'est pour que ça serve, et puis comme je le dis dans ma signature, du code qui n'existe pas ne contient pas de bug. On a bien assez à faire avec du code qui fait des trucs pour en plus aller s’embêter avec du code qui ne fait rien, mais qui par sa seule existence peut engendrer des bugs.

                Edit pour Koala01 : De toute façon cache et polymorphisme d'inclusion ne font jamais bon ménage ^^ Si on veut des grosses perfs, il faut penser en data driven. Là on n'a plus que des collections de structures (0 polymorphisme) sur lesquelles on déroule des traitements, qui pour le coup peuvent être hyper polymorphes, mais comme ils ne contiennent que peu ou pas de data, les histoires de cache, on s'en balance ^^ La collection de structures sera cache friendly, le traitement, on s'en fout qu'il soit cache friendly puisqu'il n'y aura quasiment que du code.

                -
                Edité par int21h 23 septembre 2017 à 1:22:56

                • Partager sur Facebook
                • Partager sur Twitter
                Mettre à jour le MinGW Gcc sur Code::Blocks. Du code qui n'existe pas ne contient pas de bug
                  23 septembre 2017 à 1:13:21

                  int21h a écrit:

                  Mais d'un autre côté si on pense en terme de service, doter une classe d'une fonction (donc d'un service) qui ne fait rien, c'est plutôt moyen et il y a par ailleurs un risque, celui de laisser entendre  que la classe dérivée rend effectivement un service, alors que ce dernier est totalement fictif. D'un point de vue conceptuel, je trouve ça assez perturbant et je pense que ce type de raisonnement mène tout droit vers des God Objects, ce qui à mon sens, constitue une erreur de conception majeure. Je suis un feignant patenté, fervent adepte du principe de parcimonie de surcroît, donc prévoir du code qui n'a pas d'utilité fonctionnelle réelle, c'est un peu contre ma religion ^^ Si je me casse le c.. à coder une fonction, c'est pour que ça serve, et puis comme je le dis dans ma signature, du code qui n'existe pas ne contient pas de bug. On a bien assez à faire avec du code qui fait des trucs pour en plus aller s’embêter avec du code qui ne fait rien, mais qui par sa seule existence peut engendrer des bugs.

                  Oui, effectivement...

                  J'ai hésité à parler des god objects et j'espérais plus ou moins secrètement que quelqu'un pose le doigt dessus ;)

                  Car la problématique d'un god object se pose bien plus régulièrement que l'on ne pourrait croire.  Par exemple, dés que l'on a affaire à une hiérarchie de classe plus "profondes" que "larges" (s'il faut, j'expliquerai ces termes ;) )

                  Quoi qu'il en soit, que l'on tombe ou non dans le piège des god objects, le raisonnement reste sensiblement le même: en faisant en sorte de disposer de collections spécifiques à la présence d'un (ensemble de) comportement(s) qui n'est pas garanti par la collection "générale" (c'est peut-être mieux que le terme globale :D ), il devient beaucoup plus facile d'utiliser ce(t ensemble de) comportement(s) spécifique sur des élément dont on sait qu'il en sont munis ;)

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                    23 septembre 2017 à 7:04:54

                    Ce que j'en dis, c'est qu'on introduit une classe parente si elle a un sens, et que ce sens n' est pas

                    << une classe qui regroupe tout et n'importe quoi, comme ca je pourrais avoir une liste polymorphe avec tout dedans >>

                    Donc que les opérations qu'on applique sur les éléments de la liste doivent être communes, applicables à tous les sous types. Qu'on ne demande pas à l'objet qui il est pour savoir si on peut lui demander de faire quelque chose.

                    Éventuellement, ca peut pousser à reconsidérer les services qu'on attribuait aux classes en première analyse.

                    Un exemple c'est la représentation d'un arbre (xml) sous forme d'éléments qui sont soit des textes, soit des noeuds contenant des éléments.

                    Y a un moment  ( pattern composite) où on se dit que tout élément, textes compris, a une liste de descendants, et c'est juste que pour un texte, la liste est toujours vide. On est parti d'un service qui était naturel pour les noeuds, et on l'a généralisé aux textes.

                    God objects vous-mêmes !

                    -
                    Edité par michelbillaud 23 septembre 2017 à 7:16:30

                    • Partager sur Facebook
                    • Partager sur Twitter

                    Liste d'objets polymorphes et distinction de type

                    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                    • Editeur
                    • Markdown