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

Langage C++ Discussion :

Downcaster, le mal ?


Sujet :

Langage C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut Downcaster, le mal ?
    Bonjour,

    Je suis face à un problème d'ordre général à propos du downcasting. Généralement, je pars du principe que downcaster est synonyme d'un problème de conception, or dans mon cas, je ne trouve que peu d'alternatives et pas spécialement attrayantes.

    J'implémente un compilateur, on doit donc représenter les types du langage source : tableau, structure, … On peut donc faire hériter les types de la classe Type.

    Or à un moment ou à un autre il faudra comparer ces types, par surcharge de l'opérateur ==.
    On peut dès lors avoir deux solutions :

    Sans variant :
    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
    class Type
    {
    public:
      virtual bool operator==(const shared_ptr<Type>& type) =0;
    };
     
    class Structure : Type
    {
    public:
       virtual bool operator==(const shared_ptr<Type>& type)
       {
          shared_ptr<Structure> toCompare = dynamic_pointer_cast<Structure>(type);
          if(toCompare)
          {
             // Ok comparaison un par un des champs de la classe.
          }
          return false;
       }
    };
     
    // Dans le code
     
    if(!(type1 == type2))
    {
      message_error_type(type1, type2);
    }
    Avec variant :
    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
    28
    29
    30
    31
    32
     
    class Type
    {
    public:
      boost::variant<Structure, ...> value;
    };
     
    class Structure
    {
    public:
       bool operator==(const shared_ptr<Structure>& type)
       {
          // Ok comparaison un par un des champs de la classe.
       }
    };
     
    class BinaryVisitorType : public boost::static_visitor<>
    {
     public:
       template <typename T>
       void operator()(const T& type1, const T& type2)
       {
          if(!(type1 == type2))
            message_error_type(type1, type2);
       }
     
      template <typename T1, typename T2>
      void operator()(const T1& type1, const T2& type2)
      {
        message_error_type(type1, type2);
      }
    };
    De ces deux solutions, je préfère encore la variant, j'évite ainsi le dynamic cast, et il me semble que c'est plus propre. Notons tout de même que les types peuvent se contenir entre eux (array de structure, tableau multidimension), donc l'usage du recursive_wrapper des variants semblent inévitables.

    Mais je ne suis guère satisfait car j'aurais aimé implémenter une vraie relation EST-UN tout en gardant les avantages de la variante.

    Comment faire ça proprement ?

    Merci d'avance.

  2. #2
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Salut,

    Déjà, on peut estimer que tout type possède un identifiant unique qui correspond à son nom, et que tu peux donc "simplement" considérer que si type1->name() == type2->name(), c'est que tu as affaire au même type...

    la fonction publique "name" fait donc partie intégrante de ta classe de base
    Bien sur, il est peut etre intéressant de disposer d'une valeur numérique qui sera elle aussi unique et corrélée avec le nom, parce que la comparaison de chaines de caractères est loin d'être ce qu'il y a de plus efficace, mais bon, ce n'est, finalement, qu'un détail

    Ensuite, vient le problème qui fait que tu as des types qui sont soit des types primitifs (char, short, int long et tous les autres), soit des structures, soit des énumérations, soit des classes soit, enfin, de unions...

    Classiquement, tu géreras les différents types de manière différentes, et donc, l'idée est peut etre, tout simplement, de travailler sur un pattern proche du DP visiteur, pour implémenter le double dispatch:

    Tu commences par créer des fonctions virtuelles pures proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    virtual void accept(Visitor const & v) =0;
    virtual void accept(ConstVisitor  const & v) const = 0;
    que tu implémentes dans chaque type particulier sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    virtual void accept(Visitor const & v)
    {
        v.visit(*this);
    }
    virtual void accept(ConstVisitor const & v) const
    {
        v.visit(*this);
    }
    Tout ce qui doit pouvoir visiter tes types concrets hérite de Visitor (s'ils doivent modifier le type concret) ou de ConstVisitor (s'ils ne doivent pas le modifier), et entre dans une hiérarchie proche de
    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
    /* Tous les visiteurs non constants héritent de Visitor */
    class Visitor
    {
        public:
            virtual void visit(PrimitiveType & ) const =0;
            virtual void visit(EnumType & ) const =0;
            virtual void visit(UnionType & ) const =0;
            virtual void visit(StructureType & ) const =0;
            virtual void visit(ClassType & ) const =0;
    };
    /* et tous les visiteurs constant hérite de ConstVisitor */
    class Visitor
    {
        public:
            virtual void visit(PrimitiveType const & ) const =0;
            virtual void visit(EnumType const & ) const =0;
            virtual void visit(UnionType const & ) const =0;
            virtual void visit(StructureType const & ) const =0;
            virtual void visit(ClassType const & ) const =0;
    };
    A partir de là, il t'est meme tout à fait possible d'envisager de créer des classes qui manipuleront un type particulier (par exemple, un PrimitiveType) pour obtenir un résultat bien particulier, de sorte à ce que ce soit ces classes qui fassent effectivement le boulot et non les classes dérivées de (Const)Visitor
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut
    Merci pour cette réponse que j'attendais

    Je n'avais pas du tout pensé à un système comme ça.

    Merci encore et bonne soirée.

    EDIT:
    À ceux qui passerait par là. Afin de ne pas avoir à ré-implémenter (ni même à déclarer) visit dans toutes les sous-classes, vous pouvez tirer parti du CRTP en définissant une classe intermédiaire

  4. #4
    Expert confirmé

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Déjà, on peut estimer que tout type possède un identifiant unique qui correspond à son nom, et que tu peux donc "simplement" considérer que si type1->name() == type2->name(), c'est que tu as affaire au même type...
    Ça dépend du langage et suivant le langage, c'est une erreur de faire autrement ou c'est une erreur de faire comme cela. On n'est pas dans la conception, on est dans la sémantique.

  5. #5
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Citation Envoyé par Jean-Marc.Bourguet Voir le message
    Ça dépend du langage et suivant le langage, c'est une erreur de faire autrement ou c'est une erreur de faire comme cela. On n'est pas dans la conception, on est dans la sémantique.
    Effectivement, je me suis basé sur l'assertion non vérifiée que le langage envisagé utilisait des types nommés

    Mais, de manière générale, on peut estimer qu'il est "logique" d'avoir un critère discriminant quelconque permettant d'identifier chaque type de donnée de manière unique et non ambigüe, vu que l'on est dans un contexte de classes ayant sémantique d'entité.

    Je me suis basé sur le nom, mais cela aurait tout aussi bien put être un "unique_id" quelconque, et l'idée serait restée la même: la classe de base fournit la possibilité d'obtenir le critère discriminant dans son interface publique, et, si la comparaison de ce critère pour deux objets distincts fait apparaitre qu'ils a la même valeur, c'est que les deux objets sont identiques
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  6. #6
    Expert confirmé

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Je me suis basé sur le nom, mais cela aurait tout aussi bien put être un "unique_id" quelconque, et l'idée serait restée la même: la classe de base fournit la possibilité d'obtenir le critère discriminant dans son interface publique, et, si la comparaison de ce critère pour deux objets distincts fait apparaitre qu'ils a la même valeur, c'est que les deux objets sont identiques
    Il faut le bâtir cette unique_id. En fait, je crois plus simple d'avoir une solution non encore envisagée, une factory qui renvoie un pointeur tel que p1==p2 est garanti pour les types égaux (du moins quand on a une notion d’égalité suffisamment stricte et dominante)

  7. #7
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Citation Envoyé par Jean-Marc.Bourguet Voir le message
    Il faut le bâtir cette unique_id. En fait, je crois plus simple d'avoir une solution non encore envisagée, une factory qui renvoie un pointeur tel que p1==p2 est garanti pour les types égaux (du moins quand on a une notion d’égalité suffisamment stricte et dominante)
    Je présume que tu sous entend "avec une prédictibilité suffisante pour que l'identifiant unique soit susceptible d'être unit-testable et un algorithme de comparaison dont la complexité reste autant que faire se peut proche du temps constant"

    Et je suis bien d'accord avec toi...

    Mais, dans le sens où il faudra de toutes manières avoir un facteur discriminant unique dés le moment où l'on travaille sur une hiérarchie de classe ayant sémantique d'entité, il est encore assez facile de considérer le fait d'avoir un "numéro d'ordre de création" qui, en lisant les même fichiers (non modifié) dans le même ordre, fourniront des identifiants uniques identiques, et que cela apporte une prédictibilité suffisante

    ...Ou, du moins, elle serait suffisante pour permettre les tests unitaires
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  8. #8
    Expert confirmé

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Par défaut
    Citation Envoyé par Trademark Voir le message
    Or à un moment ou à un autre il faudra comparer ces types, par surcharge de l'opérateur ==.
    D'experience, on a plusieurs comparaisons possibles a utiliser suivant les contextes, en favoriser une pour l'operateur== ne me semble pas pertinent (meme si ton langage est suffisamment simple pour que ce ne soit pas le cas pour le moment, ca peut poser un probleme quand il evolue).

    Ensuite, avoir un operateur== sur le type veut dire que tu compares des valeurs, hors tu le veux virtuels. Soit tu manipuleras tout le temps des references, qui ne sont pas re-bindables ce qui me semble genant, soit tu vas avoir partout des *t1 == *t2 et pas des t1 == t2.

    On se retrouve en fait dans un cas du probleme des expressions (en fait presque le cas qui a donne son nom au probleme). Il est difficile (difficile comme "je n'ai pas encore vu de proposition convaincante, meme de la part des gens publiant sur les sujets a base de langages experimentaux") de batir une structure de code qui permet d'ajouter facilement des types (l'OO le fait bien) et des operations (une structure a base de variant le permet facilement).

    Le double dispatch (souvent confondu avec le pattern visiteur parce que les implementations de ce pattern en ont besoin et donc pas mal de gens apprennent les deux en meme temps) est une sorte de compromis OO mais qui reste lourd. Ca peut etre le meilleur choix, mais ici quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    if (t1->kind() != t2->kind()) {
       return false;
    } else {
       switch (t1->kind()) {
       case structureType:
          ...
          break;
       case arrayType:
          ...
          break;
    }
    ne me générait pas. Ça reste la manière la plus légère d'ajouter des opérations sur les types et dans un compilateur, faire de toutes les opérations nécessaires des membres virtuels ou des classes "visiteuses" n'est pas sans inconvénients. Ajouter des types serait plus facile avec les alternatives, mais ce sera loin d’être aussi courant qu'ajouter des opérations, autant prendre la structure la plus adéquate et la plus légère pour ce qui sera le plus commun.

  9. #9
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    +1 pour ce que tu dis de l'opérateur ==

    Par contre, s'il est vrai que le patter "visiteur" est une structure assez lourde, tu le dis toi-même : il est difficile (et je suis tout à fait d'accord avec le sens que tu donnes au terme en l'occurrence )

    Mon avis est que tu arriveras très rapidement à un nombre finalement clairement défini de types différents, alors que tu observera plus ou moins rapidement une explosion du nombre des manipulations que tu peux envisager dessus.

    Le pattern visiteur n'est donc effectivement pas exempt de défaut, mais il garde l'énorme avantage de ne pas bloquer l'évolutivité du programme

    Il est cependant tout aussi vrai que certaines manipulations ne sont envisageable que pour certains type très particuliers (rajouter une valeur énumérée ne pouvant se faire que pour... les énumérations, par exemple ) et qu'il peut donc s'avérer utile de pouvoir disposer, dans certaines circonstances, de collections de références (au sens global du terme, donc sans doute plutot des pointeurs ) sur objet d'un type particulier (par exemple, tous les "type primitifs" ou toutes les classes ou encore toute les unions).

    Si tu ne devais faire que cinq visiteurs, ce serait, peut etre, des visiteurs qui auraient pour résultat de remplir ces collections d'objets de types précis, de manière à savoir par la suite que tu travailles, quoi qu'il en soit, sur un type bien particulier

    Cela ne me gênerait pas outre mesure si le parsing du code source venait à remplir une collection "globale" (dans le sens : tous les types sans distinction ) d'objets et si l'étape suivante était de créer des collections pour chaque type particulier, avant de gérer chaque ensemble de types particulier par lot
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

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

Discussions similaires

  1. [MFC]dialogues qui s'initialisent mal
    Par Tsunamis dans le forum MFC
    Réponses: 4
    Dernier message: 25/03/2004, 12h57
  2. Le kernel version 2.6.3-mdk mal reconnu
    Par christophe D dans le forum Administration système
    Réponses: 5
    Dernier message: 23/03/2004, 10h03
  3. Pb de pointeur mal détruit
    Par olive_le_malin dans le forum MFC
    Réponses: 20
    Dernier message: 15/01/2004, 21h20
  4. Réponses: 3
    Dernier message: 12/05/2003, 12h11

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