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 :

Pattern NVI : préconisé dans tous les cas ?


Sujet :

C++

  1. #1
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut Pattern NVI : préconisé dans tous les cas ?
    Bonjour,
    La question est dans le titre.
    Mais je suppose que la raison pour laquelle je (me) pose cette question permettra d'affiner les réponses...

    Je pense que j'ai compris l'intérêt du pattern « Non Virtual Interface », mais il y a quelques cas pour lesquels je me demande s'il est vraiment utile/nécessaire.
    À chaque fois, il n'y a ni précondition, ni postcondition.


    Un bout de code est souvent plus parlant qu'un peu de blabla...
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    class base
    {
     
      public:
        virtual
        ~base()
        {}
     
        virtual
        bool is_cat1() const
        { return false; }
     
        virtual
        bool is_cat2() const
        { return false; }
     
        (...)
     
        virtual
        type1 get_value1() const
        { throw std::runtime_error("Invalid operation."); }
     
        virtual
        type2 get_value2() const
        { throw std::runtime_error("Invalid operation."); }
     
        (...)
     
    }; // class base
     
     
    class deriv1 : public base
    {
     
      private:
        type1 m_value;
     
      public:
        virtual
        bool is_cat1() const
        { return true; }
     
        virtual
        type1 get_value1() const
        { return m_value; }
     
    }; // class deriv1
     
     
    class deriv2 : public base
    {
     
      private:
        type2 m_value;
     
      public:
        virtual
        bool is_cat1() const
        { return true; }
     
        virtual
        type2 get_value2() const
        { return m_value; }
     
    }; // class deriv2
     
    (...)
    Bon, je suppose que ce bout de code est suffisamment parlant que pour que voyiez là où je veux en venir.
    Je précise tout de même que pour les autres services proposés par certaines classes filles, mais pas toutes, j'utiliserai le pattern NVI.
    Mais pour ceux présentés ici, je ne suis convaincu de son utilité.


    Pour le second cas, disons que l'on dispose d'une hiérarchie de foncteurs.
    Quoi ? Cela vous paraît étrange ?
    Hum...

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    struct fonct1
    {
     
        virtual
        return_type operator () (arguments_type... args)
        {
            (...)
            f1(...);
            (...)
            f2(...);
            (...)
        }
     
      protected:
        void f1(...);
     
        void f2(...);
     
    }; // struct fonct1
     
     
    struct fonct2 : public fonct1
    {
     
        virtual
        return_type operator () (arguments_type... args)
        {
            (...)
            f1(...);
            (...)
            f2(...);
            (...)
        }
     
    }; // struct fonct1
     
     
    //******************//
     
    struct fonct3
    {
     
        virtual
        return_type operator () (type arg0, arguments_type... args);
     
    }; // struct fonct3
     
     
    struct fonct4 : public fonct3
    {
     
        virtual
        return_type operator () (type arg0, arguments_type... args)
        {
            switch (arg0) {
                case ...:
                    (...)
                    break;
     
                case ...:
                    (...)
                    break;
     
                (...)
     
                default:
                    return fonct3::operator()(arg0, args...);
            }
            (...)
        }
     
    }; // struct fonct4
    Dans le premier exemple, où la seconde classe utilise des éléments (données ou fonctions) de la première, on aurait pu en faire des classes sœurs, certes.
    Et dans ce cas, la classe mère aurait été attribuée d'un opérateur () virtuel pur.
    Mais du coup, la question se pose aussi.
    NVI ou pas NVI ?

    Pour le second exemple, la seconde classe ajoute des comportements spécifiques pour des valeurs particulières à la première.

    Bon, je me doute bien que dans un contexte autre que celui des foncteurs, on utiliserait sans hésiter le pattern NVI.
    Alors pourquoi faire une exception ?
    Eh bien, la principale raison d'être d'un foncteur est l'opérateur ().
    À moins que l'opération intrinsèque soit vraiment complexe, on n'écrit pas grand chose à côté.
    Bien sûr, une classe fille hérite de l'opérateur de la classe mère, s'il n'est pas redéfini.
    Mais je me demandais si, par convention, on ne se contentait pas de rendre l'opérateur virtuel si l'on pouvait avoir besoin de le redéfinir dans des classes filles.
    Bien entendu, comme précisé en début de message, on reste dans le cadre où il n'y a ni précondition, ni postcondition.


    S'il y a des cas connus où il n'y a pas besoin du pattern NVI, je suis tout ouïe...


    [Edit]
    Citation Envoyé par Steph_ng8 Voir le message
    Il ne me reste plus qu'à modifier ma question.
    À quoi faut-il réfléchir quand on veut savoir si le pattern NVI se justifie ?
    [/Edit]

  2. #2
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Une petite question en plus, pas vraiment liée au pattern NVI, mais à la virtualité...

    Lorsqu'une fonction membre est déclarée virtual, alors sa redéfinition dans les classes filles l'est forcément.
    Employé ce mot-clé pour les classes filles est donc a priori inutile, sauf que ça apporte une information pour le programmeur (qu'il n'a pas besoin d'aller chercher à la base de l'héritage).

    C'est également vrai pour le destructeur.
    Mais on peut se retrouver dans un cas où l'on n'a pas besoin d'écrire le destructeur (constructeur par défaut inchangé, constructeur par copie et opérateur d'assignation non définie dans la classe mère, corps du destructeur vide).
    Sans héritage, il serait conseillé de ne pas écrire le destructeur.
    Mais dans ce cas précis, a-t-on intérêt à l'écrire, pour bien indiquer la présence de l'héritage (sachant qu'il peut y avoir des fonctions membres virtuelles dans cette classe) ?

  3. #3
    Membre éprouvé
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 766
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 766
    Par défaut
    Dans l'absolu, si ta classe peut être héritée, tu dois mettre ton destructeur en virtuel, même s'il ne fait rien, et même si tu n'as pas de fonctions virtuelles.

    Car si un client crée une classe dérivée, y colle des données membres, manipule des instances de cette nouvelle classe comme des pointeurs vers la classe de base, et les supprime, tu te retrouves avec des fuites de mémoire.

  4. #4
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Désolé, visiblement je n'ai pas été clair.
    Le destructeur dont je parle est celui d'une classe dont la classe mère possède un destructeur virtuel.

  5. #5
    Membre éprouvé
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 766
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 766
    Par défaut
    Dans ce cas là, le destructeur n'a rien de particulier : tu rajoutes virtual si tu l'estimes nécessaire quant à la compréhension du code. C'est aussi une question de préférences personnelles...

  6. #6
    Inactif  


    Homme Profil pro
    Inscrit en
    Novembre 2008
    Messages
    5 288
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Secteur : Santé

    Informations forums :
    Inscription : Novembre 2008
    Messages : 5 288
    Par défaut
    Citation Envoyé par Steph_ng8 Voir le message
    Désolé, visiblement je n'ai pas été clair.
    Le destructeur dont je parle est celui d'une classe dont la classe mère possède un destructeur virtuel.
    Non, sauf si tu veux pouvoir dériver également ta classe fille.

    Quand tu as :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    class A {};
    class B : public A {};
    le problème se présente quand tu écris :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    A* a = new B();
    delete a;
    delete appelle ~A et pas ~B. Donc il faut un destucteur virtuel pour A pour que delete appelle le bon destructeur. Par contre :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    B* b = new B();
    delete b;
    delete appelle bien ~B, qui appellera lui même ~A donc pas de problème, même sans mettre le destructeur de B en virtuel.

  7. #7
    Membre Expert
    Homme Profil pro
    Inscrit en
    Décembre 2010
    Messages
    734
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Décembre 2010
    Messages : 734
    Par défaut
    Citation Envoyé par gbdivers Voir le message
    Non, sauf si tu veux pouvoir dériver également ta classe fille.
    Tu parles pour des raisons de documentation de la classe fille? Il me semblait que virtual se propageait vers les filles?

  8. #8
    screetch
    Invité(e)
    Par défaut
    j'ai l'impression parfois que personne n'a lu... :-/

    1) il n'est pas necessaire de mettre un destructeur dans une classe fille si il ne fait rien (et si la classe mère a un destructeur virtuel, ce qui devrait etre le cas)
    je ne pense pas que ca ajoute quoi que ce soit a la documentation mais je mets demaniere systématique les destructeurs (meme les vides) pour une raison con: le BREAKPOINT!!!

    2) il n'est pas necessaire de mettre virtual sur les methodes mais c'est effectivement une bonne documentation.
    pour la "vraie" documentation, j'ai aussi ajouté le "mot clé" override; override existe sous MSVC (au prix d'un warning que tu devrais mettre en silencieux) comme vrai mot clé et assure que la fonction que tu donnes est bien un override d'une fonction de classe de base (et que donc tu n'as pas une typo a la con); ca ne compiel pas si ta fonction marquée override n'est pas un vrai override.

    Sous GCC, je remplace grace au preprocesseur par un espace; ainsi, sous visual studio il va verifier tous les override, sous GCC il ne se plaindra pas.

    Et ca c'est la documentation =)


    3) demander a une classe si elle est bien du type que tu veux, c'est un peu du RTTI de pauvre ou un dynamic_cast caché, ce pattern est un peu un code smell; il pourrait montrer que tu as un problème de conception si tu dois demander a un type ce qu'il est en réalité;


    4) je ne conseille pas le NVI sans reflexion; je trouve que ce principe est très restrictif pour parfois pas grand chose. Mais c'est un avis personnel plus qu'autre chose. Mon avis (attention psychologie inside) c'est que c'est aussi ton avis et que tu as posté sur ce forum pour tester la temperature, pour voir si d'autre gens renforcaient ton opinion, et je m'en veux un peu de répondre a cette demande

    une versio plus objective serait de demander ce qui dans ton code pourrait justifier le NVI.

  9. #9
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Citation Envoyé par gbdivers Voir le message
    Non, sauf si tu veux pouvoir dériver également ta classe fille.
    Euh... oui...
    Disons que ce serait pour dire à l'utilisateur de la classe : « Personnellement, je ne fais pas hériter cette classe ; mais comme elle fait déjà partie d'une hiérarchie, si tu veux la dériver, fais-toi plaisir ! »


    Citation Envoyé par therwald Voir le message
    Il me semblait que virtual se propageait vers les filles?
    C'est ce qu'il me semble également.
    C'est d'ailleurs pour cette raison que je posais la question...


    Citation Envoyé par screetch Voir le message
    1) il n'est pas necessaire de mettre un destructeur dans une classe fille si il ne fait rien (et si la classe mère a un destructeur virtuel, ce qui devrait etre le cas)
    je ne pense pas que ca ajoute quoi que ce soit a la documentation mais je mets demaniere systématique les destructeurs (meme les vides) pour une raison con: le BREAKPOINT!!!
    Le breakpoint


    Citation Envoyé par screetch Voir le message
    3) demander a une classe si elle est bien du type que tu veux, c'est un peu du RTTI de pauvre ou un dynamic_cast caché, ce pattern est un peu un code smell; il pourrait montrer que tu as un problème de conception si tu dois demander a un type ce qu'il est en réalité;
    Oups.
    À trop simplifier, j'ai créé une ambiguïté.
    J'aurais plutôt dû appeler les fonctions comme ceci : is_category1...
    Il peut y avoir moins de « catégories » que de sous-classes, et plusieurs sous-classes peuvent appartenir à la même « catégorie » (pas forcément mère-fille).
    (J'ai modifié le code dans le message.)


    Citation Envoyé par screetch Voir le message
    4) je ne conseille pas le NVI sans reflexion; je trouve que ce principe est très restrictif pour parfois pas grand chose. Mais c'est un avis personnel plus qu'autre chose. Mon avis (attention psychologie inside) c'est que c'est aussi ton avis et que tu as posté sur ce forum pour tester la temperature, pour voir si d'autre gens renforcaient ton opinion, et je m'en veux un peu de répondre a cette demande

    une versio plus objective serait de demander ce qui dans ton code pourrait justifier le NVI.
    Bonne analyse.
    Mais du coup, ça me donne l'impression que je n'ai pas vraiment compris ce pattern.
    Je pensais que c'était une bonne pratique de l'utiliser dans le cas général.
    Mais visiblement, ce n'est pas le cas...


    Il ne me reste plus qu'à modifier ma question.
    À quoi faut-il réfléchir quand on veut savoir si le pattern NVI se justifie ?

  10. #10
    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,

    Je crois que tous les patterns, comme toutes les solutions "toutes faites" que l'on peut préconiser en POO doivent systématiquement être évaluées en fonctions des circonstances (sauf, selon moi, certaines règles telles OCP, LSP, demeter, et autres joyeusetés du genre, mais ce sont là des principes, des règles ou des lois et il est donc "normal" de ne pas y déroger )

    Il y a en effet des cas où il est indéniable que le NVI est, pour ainsi dire, indispensable : s'il y a des pré / post conditions à vérifier avant ou après un comportement adapté, par exemple, ou pour adapter le comportement des opérateurs (qui ne peuvent pas être virtuels).

    Il y a des cas où il nous permettra juste de forcer celui veut redéfinir le comportement à réfléchir à l'utilité de ce qu'il veut faire : Si tu as une hiérarchie de classe importante (en terme de niveaux), tu peux, pourquoi pas, décider de ne pas redéfinir le comportement en question pour certaines classe dérivées, parce que la classe parente modifie déjà le comportement de manière satisfaisante.

    L'utilisateur qui ne se serait alors intéressé qu'à une classe fille trop spécialisée pourrait, tout simplement, ne pas avoir conscience du fait qu'il peut redéfinir ce comportement

    Mais il y a, effectivement, des cas où... bah, ma foi, utiliser le NVI ne revient qu'à se faire du mal inutilement

    Dans ces cas particuliers, le mieux est peut etre encore de se contenter d'une fonction virtuelle publique

    NOTA: je suis d'accord avec screetch concernant les fonctions virtuelles isTypeXXX... Ce n'est pas vraiment à faire

    La raison est bien simple : si tu ajoute un type dérivé, tu dois rajouter une fonction, et :
    1. cela va à l'encontre de l'OCP
    2. cela donne une responsabilité à la classe mere qu'elle ne devrait pas avoir (permettre de savoir le type de la classe fille )
    3. cela va te forcer à écrire des tests if... else if .... en pagaille (qu'il faudra adapter à chaque fois que tu rajoutera un type ) et t'interdira l'utilisation de switch... case
    4. A choisir, il serait sans doute plus cohérent d'avoir une fonction non virtuelle type() renvoyant une valeur énumérée... à charge pour la classe dérivée de la faire passer jusqu'à la classe mère pour que ce soit elle (la classe mère) qui se charge de garder cette valeur "bien au chaud"
    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

  11. #11
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Je crois que tous les patterns, comme toutes les solutions "toutes faites" que l'on peut préconiser en POO doivent systématiquement être évaluées en fonctions des circonstances
    Oui, c'est parce que j'en ai pris conscience que j'ai modifié ma question.


    Citation Envoyé par koala01 Voir le message
    le comportement des opérateurs (qui ne peuvent pas être virtuels).
    Ah bon ?
    GCC fait n'importe quoi alors ?
    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
    33
    34
    35
    #include <iostream>
     
     
    #define PRINT std::cout << __PRETTY_FUNCTION__ << std::endl;
     
     
    struct Base
    {
     
        virtual
        ~Base()
        {}
     
        virtual
        void operator () () const
        { PRINT; }
     
    }; // struct Base
     
     
    struct Derived : public Base
    {
     
        virtual
        void operator () () const
        { PRINT; }
     
    }; // struct Derived
     
     
    int main()
    {
        Derived()();
        return 0;
    }
    Code Sortie console : Sélectionner tout - Visualiser dans une fenêtre à part
    virtual void Derived::operator()() const


    Citation Envoyé par koala01 Voir le message
    Il y a des cas où il nous permettra juste de forcer celui quiveut redéfinir le comportement à réfléchir à l'utilité de ce qu'il veut faire : Si tu as une hiérarchie de classe importante (en terme de niveaux), tu peux, pourquoi pas, décider de ne pas redéfinir le comportement en question pour certaines classe dérivées, parce que la classe parente modifie déjà le comportement de manière satisfaisante.

    L'utilisateur qui ne se serait alors intéressé qu'à une classe fille trop spécialisée pourrait, tout simplement, ne pas avoir conscience du fait qu'il peut redéfinir ce comportement
    Tu peux développer, s'il-te-plaît ?
    Je ne vois pas très bien où tu veux en venir (pour la deuxième partie).


    Citation Envoyé par koala01 Voir le message
    Mais il y a, effectivement, des cas où... bah, ma foi, utiliser le NVI ne revient qu'à se faire du mal inutilement

    Dans ces cas particuliers, le mieux est peut etre encore de se contenter d'une fonction virtuelle publique
    Ah, ça me rassure...
    Ça correspond à ce que je pensais.
    Mais ça illustre encore que je n'ai pas bien saisi les cas d'utilisation de ce pattern...


    Citation Envoyé par koala01 Voir le message
    NOTA: je suis d'accord avec screetch concernant les fonctions virtuelles isTypeXXX... Ce n'est pas vraiment à faire
    (...)
    Oui, j'ai bien compris...
    Mais comme je l'ai dit après coup, ce ne sont pas des fonctions « isTypeXXX() » que je voulais écrire.

    En passant, comment feriez-vous si vous aviez des services proposés par certaines classes filles et pas par les autres, et que les objets étaient manipulés via des pointeurs sur la classe de base ?

  12. #12
    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 Steph_ng8 Voir le message
    Ah bon ?
    GCC fait n'importe quoi alors ?
    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
    33
    34
    35
    #include <iostream>
     
     
    #define PRINT std::cout << __PRETTY_FUNCTION__ << std::endl;
     
     
    struct Base
    {
     
        virtual
        ~Base()
        {}
     
        virtual
        void operator () () const
        { PRINT; }
     
    }; // struct Base
     
     
    struct Derived : public Base
    {
     
        virtual
        void operator () () const
        { PRINT; }
     
    }; // struct Derived
     
     
    int main()
    {
        Derived()();
        return 0;
    }
    Code Sortie console : Sélectionner tout - Visualiser dans une fenêtre à part
    virtual void Derived::operator()() const
    Non, c'est moi qui ai pris un raccourci un peu trop osé...

    Certains opérateurs ne peuvent pas être membres de classe (les opérateur << et >> vers des flux sortants / entrants, par exemple) et ne peuvent donc pas etre virtuels... cf l'entrée de la FAQ qui en parle


    Tu peux développer, s'il-te-plaît ?
    Je ne vois pas très bien où tu veux en venir (pour la deuxième partie).
    Imagine un code basé sur quelque chose comme (oui, je sais, ce genre de hiérarchie n'est pas vraiment idéale )
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    #include <iostream>
    class Base
    {
    public:
        void Nvi()
        {
            doNvi();
        }
        virtual ~Base(){}
    private:
        virtual void doNvi()
        {
            std::cout<<"Base->doNvi()"<<std::endl;
        }
    };
    class Derived : public Base
    {
    public:
        void Nvi()
        {
            doNvi();
        }
    private:
        virtual void doNvi()
        {
            std::cout<<"Derived->doNvi()"<<std::endl;
        }
    };
    class SubDerived : public Derived
    {
    public:
    private:
        /* doNvi not redefined */
    };
    class SubSubDerived : public SubDerived
    {
    public:
    private:
        virtual void doNvi()
        {
            std::cout<<"SubSubDerived->doNvi()"<<std::endl;
        }
    };
    int main()
    {
        Base * b = new Base;
        Base * d = new Derived;
        Base * sd = new SubDerived;
        Base * ssd = new SubSubDerived;
        b->Nvi(); // Base->doNvi()
        d->Nvi(); // Derived->doNvi()
        sd->Nvi(); // Derived->doNvi()
        ssd->Nvi(); // SubSubDerived->doNvi()
        delete b;
        delete d;
        delete sd;
        delete ssd;
        return 0;
    }
    Comme l'indiquent les commentaires, l'objet dont le type réel est SubDerived va réagir comme la classe parent la plus proche dans la hiérarchie (Derived, dans le cas présent ) dans laquelle doNvi a été redéfini.

    N'oublie pas que, en théorie, Base, Derived, SubDerived et SubSubDerived seront définies dans autant de fichiers différents.

    L'existence de la fonction virtuelle doNvi est donc totalement cachée à l'utilisateur décidant d'utiliser SubDerived comme classe de base d'une de ses classes perso, d'autant plus qu'il ne peut pas y faire appel, meme depuis une fonction membre de subDerived.

    Pour qu'un utilisateur sache seulement qu'il est possible de redéfinir cette fonction, il faudra qu'il "maitrise" relativement la hiérarchie de classes...

    Cela permet, d'une certaine manière, d'éviter qu'elle ne soit redéfinie en dehors du bon sens
    Oui, j'ai bien compris...
    Mais comme je l'ai dit après coup, ce ne sont pas des fonctions « isTypeXXX() » que je voulais écrire.

    En passant, comment feriez-vous si vous aviez des services proposés par certaines classes filles et pas par les autres, et que les objets étaient manipulés via des pointeurs sur la classe de base ?
    Je ferais sans doute appel à des patterns tels que visiteur ou chaine de responsabilité, ou je m'arrangerais pour disposer de collections de pointeurs regroupant uniquement les objets offrant un service particulier:

    Rien ne t'empêche d'avoir, pour tous les cas où les services offerts par la classe de base suffisent, un std::vector<Base*> et de maintenir, par ailleurs (là où les services offerts par la classe dérivée sont nécessaires ) un std::vector<Derived*>

    Et sinon, il reste toujours la possibilité de redéfinir les fonctions virtuelles de la classe de base dans la classe dérivée de sorte à ce que celles qui en ont besoins profitent des services offerts par la classe dérivée
    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

  13. #13
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Certains opérateurs ne peuvent pas être membres de classe (les opérateur << et >> vers des flux sortants / entrants, par exemple) et ne peuvent donc pas etre virtuels... cf l'entrée de la FAQ qui en parle
    Dans ce cas, ok.

    Citation Envoyé par koala01 Voir le message
    L'existence de la fonction virtuelle doNvi est donc totalement cachée à l'utilisateur décidant d'utiliser SubDerived comme classe de base d'une de ses classes perso, d'autant plus qu'il ne peut pas y faire appel, meme depuis une fonction membre de subDerived.

    Pour qu'un utilisateur sache seulement qu'il est possible de redéfinir cette fonction, il faudra qu'il "maitrise" relativement la hiérarchie de classes...

    Cela permet, d'une certaine manière, d'éviter qu'elle ne soit redéfinie en dehors du bon sens
    Ah d'accord.
    Je comprends maintenant.

    Citation Envoyé par koala01 Voir le message
    Rien ne t'empêche d'avoir, pour tous les cas où les services offerts par la classe de base suffisent, un std::vector<Base*> et de maintenir, par ailleurs (là où les services offerts par la classe dérivée sont nécessaires ) un std::vector<Derived*>
    Non, ce n'est pas possible.
    Ce sont des données membres d'autres classes, sous forme de pointeurs sur la classe de base.
    Si je dois séparer les « Base » des « Derived », je perds l'avantage du polymorphisme.

    Ceci dit, c'est dans du code que je n'ai pas écrit.
    Et je ne me souviens plus exactement comment sont appelés les services qui ne sont pas présents partout.
    Je vais vérifier dès que possible.

    Citation Envoyé par koala01 Voir le message
    Et sinon, il reste toujours la possibilité de redéfinir les fonctions virtuelles de la classe de base dans la classe dérivée de sorte à ce que celles qui en ont besoins profitent des services offerts par la classe dérivée
    Ouais, ça c'est ce que j'ai déjà présenté...


    J'ai déjà entendu parler des chaînes de responsabilité, mais en listant rapidement une description cela ne m'avait pas semblé correspondre à des besoins que j'aurais pu avoir.
    Je vais regarder plus en détails.

  14. #14
    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
    Salut,
    Le pattern NVI a un intérêt sémantique ET syntaxique. Ce dernier point fait que beaucoup d'expression sont plus 'naturelles' avec le pattern NVI et plus surprenantes sinon. Il n'a (potentiellement) aucun coût, donc je ne vois pas pourquoi s'en passer.

  15. #15
    Membre émérite
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Mars 2009
    Messages
    552
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Mars 2009
    Messages : 552
    Par défaut
    Bonjour,

    Je vois surtout un cas où il est utile que je ne lis pas ici : Quand on risque d'avoir des masquages de méthode. En gros, quand la fonction virtuelle servant d'interface va être utilisée par la classe mère pour fournir une méthode utilitaire.

    Cas bidon : On veut mettre en place une interface XML pour du SAX et du DOM

    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
     
     
    class XmlReader {
    public:
    	virtual void read( XmlSaxHandler & handler ) 
    	{
    		_read( handler );
    	}
    	virtual void read( XmlDocument & document ) 
    	{
    		XmlDomBuilder builder(document); // --> XmlSaxHandler
    		_read( builder );
    	}
    private:
    	// NVI utile, sans lui masquage
    	virtual void _read( XmlSaxHandler & handler ) = 0 ;
    };

  16. #16
    Membre Expert
    Homme Profil pro
    Inscrit en
    Décembre 2010
    Messages
    734
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Décembre 2010
    Messages : 734
    Par défaut
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    public:
            virtual void read( XmlSaxHandler & handler )=0; //1
     
    	virtual void read( XmlDocument & document) //2
    	{
    		XmlDomBuilder builder(document); // --> XmlSaxHandler
    		read( builder );
    	}
    Je ne suis pas sûr de voir ce qui masque dans ce cas? Par surcharge si on appelle avec un XmlSaxHanlder on appelle //1 (ou plutôt son implémentation dans la classe concrète) directement => c'est ce qu'on veut? Et si on appelle avec un XmlDocument, on appelle //2, qui en interne appelle //1 (sauf si la classe concrète redéfinit ce comportement) En quoi y-a-t'il masquage, et en quoi ce comportement est-il non voulu?

    Sauf si tu ne nous dit pas tout, et que ceci est pour passer une barrière DLL, auquel cas on ne peut pas utiliser de surcharge pour des raisons de compatibilité binaire?

  17. #17
    screetch
    Invité(e)
    Par défaut
    Ca marche techniquement mais c'est franchement confus; on ne sait pas quelle méthode va être appelée et on multiplie par 2 les chances de bugs.
    Deux méthodes qui font la même chose (ou presque) et l'une appelant l'autre, et l'un étant peut-être redefinie (ou pas) et pas l'autre, c'est un sac de noeuds ou un plat de spaghetti

  18. #18
    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 Steph_ng8 Voir le message
    Non, ce n'est pas possible.
    Ce sont des données membres d'autres classes, sous forme de pointeurs sur la classe de base.
    Si je dois séparer les « Base » des « Derived », je perds l'avantage du polymorphisme.
    Oui, mais le polymorphisme n'est que l'adaptation d'un comportement au type réel de l'objet, et tu ne peux, en tout état de cause, utiliser les comportements fournis par la classe de base (sauf à passer par un visiteur, une chaine de responsabilité ou autre tests de dynamic_cast).

    En outre, une classe est malgré tout sensée connaitre le type de ses membres!

    Le seul cas où le polymorphisme s'avère éventuellement applicable pour les membre est l'idiome Pimpl:
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    /* la classe de base qui nous sert de lecteur
     *
     * tout ce que l'on peut faire au départ d'un lecteur (sans précision)
     * est de demander de lire un fichier dont le nom est donné
     */
    class Reader
    {
        public:
           virtual ~reader();
           virtual void read(std::string const & filename) = 0;
           /* 
            voir, avec le pattern NVI
            void read(std::string const & filename)
            {
                doRead(filename);
            }
        private:
            virtual void doRead(std::string filename);
           */
    };
    /* les classes dérivées de lecteur : XmlReader, CsvReader et BinaryReader
     * permettent de lire les fichiers de type respectifs
     *
     * La classe qui dispose d'un pointeur sur la classe de base Reader ne pourra
     * appeler que read(filename)...
     */
    class XmlReader : public Reader
    {
        public:
            virtual XmlReader();
            virtual void Read();
        /* voir, avec le pattern NVI
        private:
            virtual void doRead();
        */
            /* on peut rajouter des fonctions membres publiques, mais seules 
             * les classes qui disposeront explicitement d'un XmlReader
             * pourront en profiter
             */
        private:
            /* ce qui est utile et à usage interne uniquement pour un XmlReader
    };
    /* Meme principe pour CsvReader et BynaryReader :D
     */
     
    class ReaderUser
    {
        public:
            /* on sait juste que la classe va manipuler un Reader, mais on 
             * ne sait pas exactement quel type lui sera fourni 
             * (la décision sera prise par ailleurs dans le programme ; ) )
             */
            ReaderUser(Reader * reader): reader_(reader){}
            void load(std::string const & filename)
            {
                /* la seule fonction à laquelle on ait acces, c'est read */
                reader->read(filename);
             }
        private:
            Reader * reader_;
    };
    /* par contre, si une classe (qui n'a rien à voir avec ReaderUser) utilise
     * explicitement un objet de type XmlReader, il en va tout autrement:
     */
    class OtherClass
    {
        public:
            void doSomething()
            {
                /* elle n'a meme pas besoin d'un pointeur sur l'objet: 
                 * elle peut le créer sans recourir à l'allocation dynamique
                 * à la demande
                 */
                XmlReader reader;
                reader.read("MyFile.xml");
                /* appel de n'importe quelle autre fonction publique de XmlReader 
                 */
            }
            void doSomethingElse()
            {
                /* voir, si elle dispose d'un membre de type XmlReader
                 * toutes les possibilités sont ouvertes
                 */
                ReaderUser ru(& xmlReader_);
                ru.load("MyFile.xml");
                xmlReader_.doTruc();
            }
    };
    Ceci dit, c'est dans du code que je n'ai pas écrit.
    Et je ne me souviens plus exactement comment sont appelés les services qui ne sont pas présents partout.
    Je vais vérifier dès que possible.
    Soit, tu pars sur les patterns déjà évoqués, soit tu pars du principe qu'une classe dérivée donnée ( XmlReaderUser, qui hériterait de ReaderUser, dans l'exemple précédent) dois obligatoirement d'un objet dont le type réel de l'objet pointé est donné (XmlReader pour suivre l'exemple) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class XmlReaderUser : public ReaderUser
    {
        public:
            XmlReaderUser( Reader * reader):ReaderUser(reader)
            {
                assert(dynamic_cast<XmlReader*>(reader)!=NULL);
            }
            void doSomething()
            {
                 XmlReader & xmlR =dynamic_cast<XmlReader&>(getReader());
                 /* manipulation de xmlR en tant que XmlReader */
            }
     
    }
    J'ai déjà entendu parler des chaînes de responsabilité, mais en listant rapidement une description cela ne m'avait pas semblé correspondre à des besoins que j'aurais pu avoir.
    Je vais regarder plus en détails.[/QUOTE]Je ne dis pas non plus qu'elles seront d'office utiles dans ton cas particulier, je rappelle juste qu'il est peut etre utile d'y penser
    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

  19. #19
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 295
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 295
    Billets dans le blog
    2
    Par défaut
    Après il y a peut-être une histoire de goût.
    Par exemple, dans certains cas, j'aime bien avoir des classes abstraites (classe qui ne contient que des fonctions virtuelles pures). Comme les interfaces en java. Cela permet de renforcer la sémantique et la logique du programme.

    Par exemple, parfois tu met en place une hiérarchie de classe et tu ne souhaites plus y toucher, du moins pas les interfaces des classe (ici j'utilise interface dans le sens "ensemble des fonctions membres publiques d'une classe"). Du coup, en relisant ton code, tu vois que ta classe hérite de Drawable, Movable, et de Animated, ça te donne déjà une bonne idée de ce qu'est cette classe.

  20. #20
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Citation Envoyé par koala01 Voir le message
    En outre, une classe est malgré tout sensée connaitre le type de ses membres!

    Le seul cas où le polymorphisme s'avère éventuellement applicable pour les membre est l'idiome Pimpl:
    Je te renvoie à l'exemple que j'ai donné ici
    La value d'une NamedConstant est soit une NumericalConstant soit une SymbolicConstant.
    Les bounds d'un RangeTerm sont soit des constantes, soit des Variable.
    Et je rajoute que Variable possède un attribut membre ground de type Term* (qui sert lorsque l'on donne une valeur à la variable), qui peut être tout sauf une ArithmeticExpression (oui, même une autre Variable).

    Même si ce n'est pas spécialement ce qu'on a voulu faire, ça ressemble à du PIMPL.
    ...N'est-ce pas ?

    Pour info, la plupart de ces objets sont créés via des Factory ou des Builder (j'ai du mal à voir la différence...)

Discussions similaires

  1. $db->query ne marche pas dans tous les cas
    Par bigorre1000 dans le forum Zend_Db
    Réponses: 8
    Dernier message: 22/07/2008, 19h50
  2. Réponses: 1
    Dernier message: 17/03/2008, 20h29
  3. [CloseWindow] Quitte dans tous les cas
    Par michaeljeru dans le forum AWT/Swing
    Réponses: 2
    Dernier message: 05/05/2007, 14h45
  4. Priorité aux familles dans tous les cas
    Par aline921 dans le forum Congés
    Réponses: 6
    Dernier message: 06/03/2007, 16h53
  5. Réponses: 23
    Dernier message: 11/04/2006, 17h33

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