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 :

Overload, override et hiding ?


Sujet :

C++

  1. #1
    Membre averti
    Homme Profil pro
    Ingénieur à ses heures perdues
    Inscrit en
    Août 2011
    Messages
    36
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur à ses heures perdues

    Informations forums :
    Inscription : Août 2011
    Messages : 36
    Par défaut Overload, override et hiding ?
    Bonjour à tous,

    Je me heurte à un problème pour comprendre quels sont les liens étroits qui existent entre l'overload, l'override et l'hiding ?

    Apparemment, l'overload consiste à surcharger une fonction mais que cette surcharge n'est valable que dans la portée courante. Je me pose plusieurs questions. Supposons qu'on ait une classe B qui hérite d'un classe A. La classe A définit plusieurs versions surchargées d'une fonctions f.

    Selon que les fonction f soit virtuelles ou non, que l'on manipule ou non des pointeurs ou références d'un objet qui hérite de A, comment les 3 concepts "Overload", "override" et "hiding" interagisent entre eux?

    Merci

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

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 288
    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 288
    Billets dans le blog
    2
    Par défaut
    Bonjour,

    l'overloading, ou surchage en français, consiste à déclarer une fonction du même nom mais d'un type différent (le type d'une fonction, ou sa signature, dépend de ses paramètres), et dans le même scope (portée). Dans le code suivant, on a une surcharge de la fonction f() dans la portée de la classe A:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    class A
    {
       void f() { /* code */ }
       void f( int a ) { /* code */ }
    };
    L'overloading est un mécanisme fonctionnel, qui ne concerne donc que les fonctions et leur scope; je veux dire par là que ça n'a rien à voir avec le paradigme objet. Ci-dessus j'ai pris l'exemple d'une fonction membre, mais ça marche aussi avec des fonctions libres.

    l'overriding (la redéfinition en français) consiste à redéclarer la même fonction, avec donc le même type, dans une classe qui hérite de la classe de base. Dans le code suivant, on override la fonction virtuelle A::f(int) par la fonction B::f(int):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A
    {
       virtual void f( int a ) { /* code */ }
    };
     
    class B : public A
    {
       void f( int a ) { /* code */ };
    };
    L'overriding n'a de sens que dans le paradigme objet, puisqu'il ne concerne que les fonctions membres dans une hiérarchie de classe.

    Le name hiding (masquage en français), c'est comme la redéfinition (overriding), sauf que la fonction de base n'est pas déclarée virtuelle:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A
    {
       void f( int a ) { /* code */ }
    };
     
    class B : public A
    {
       void f( int a ) { /* code */ };
    };
    Dans le cas ci-dessus, la fonction membre A::f(int) est masquée, car elle sera impossible à appeler à partir d'une instance de B.


    Il y a un cas où le name hiding peut poser des soucis, c'est le cas suivant:
    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 A
    {
    public: 
       virtual void f(int a){ /* code */ };
       virtual void f(int* a){ /* code */ }; // surcharge de f()
    };
     
    class B: public A
    {
    public:
       virtual void f(int* a) { /* code */ }; // override de A::f(int*)
    };
     
    int main(){
        B b;
        b.f(10); // error
    }
    La ligne b.f(10) renvoie une erreur grâce au name hiding. On pourrait se dire que, normalement, puisque B ne déclare pas de fonction f() de type f(int), il devrait aller chercher dans la classe mère. Mais il n'en est rien. L'idée c'est que, lorsque le compilateur essaie de déterminer quelle version de f() il faut appeler à cet endroit (le name lookup), il commence par regarder les fonctions qui s'appellent f() dans la classe instanciée, c'est à dire B. Il voit qu'il y en a au moins une, alors il ne remonte pas la hiérarchie.
    La raison de ce choix, c'est qu'il est considéré comme une mauvaise pratique de ne pas overrider toutes les surcharges d'une fonction. Cela va à l'encontre du LSP il me semble. Koala tu confirmes?

  3. #3
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 292
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 292
    Par défaut
    Non r0d. Il y a une erreur dans ce que tu dis.
    La redéfinition (ou overriding) exige que la fonction soit virtuelle dans la classe mère pour permettre à la supplantation(*) d'opérer. Sans cela, il y a masquage (qui n'est rien d'autre qu'une dégénérescence de la surcharge)

    (*) il y a longtemps il y a eu une discussion sur fclc++ sur une bonne traduction pour to override. supplanter fut proposé, mais ce n'est pas le terme usité par tous. Même si son sens est plus proche que celui de redéfinir


    Sinon, avec un using le scénario que tu décris est corrigé. Dans le scénario de masquage classique, i.e. celui que tu as montré comme étant une redéfinition (ce qu'il n'est pas comme j'expliquais donc), using ne peut pas nous sauver.
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

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

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 288
    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 288
    Billets dans le blog
    2
    Par défaut
    Merci pour ces précisions.
    Citation Envoyé par Luc Hermitte Voir le message
    Non r0d. Il y a une erreur dans ce que tu dis.
    La redéfinition (ou overriding) exige que la fonction soit virtuelle dans la classe mère pour permettre à la supplantation(*) d'opérer.
    Soit virtuelle pure tu veux dire?

  5. #5
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 292
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 292
    Par défaut
    Non. Juste virtuelle. Directement, ou indirectement (via une classe grand-parente qui déclarait déjà la fonction comme virtuelle)
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

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

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 288
    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 288
    Billets dans le blog
    2
    Par défaut
    Okay, j'ai édité mon message pour corriger ça.
    Merci!

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

    Informations professionnelles :
    Activité : aucun

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

    LSP ou non, il n'y a aucune raison d'obliger une classe dérivée à obliger la redéfinition de tous les comportements de la classe de base qui sont susceptibles de l'être.

    Il n'y a même pas obligation de le faire pour les fonctions virtuelles pures de la classe de base, si tant est qu'il y ait du sens à ce que ta classe dérivée soit elle aussi abstraite

    Pour ce qui est du masquage de fonction, s'il y a très certainement un problème conceptuel, il ne faut pas forcément aller le chercher du coté de Liskov.

    D'une certaine manière, il y a masquage de fonction avec un code 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
    class Base{
    public:
        void foo(){std::cout<<"base"<<std::endl;}
    };
    class Derivee : public Base{
    public:
        void foo(){std::cout<<"derivee"<<std::endl;}
    };
     
    int main(){
        Base b;
        Derivee d;
        Base * ptrb=&d;
        b.foo();
        d.foo();
        ptrb->foo();
        return 0;
    }
    Ce code démontre que la substitution ne se fait pas correctement et on se rend compte qu'il y a "un cheveu dans la soupe" par le fait que ptrb->foo() affiche "base" au lieu de "derivee".

    Cependant, aux termes de Liskov, il n'y a strictement aucun problème : La classe Base dispose d'une propriété prouvable "expose a fonction foo ne prenant aucun argument" que l'on retrouve au niveau de Derivee. LSP est donc parfaitement respecté.

    S'il faut chercher l'origine de ce problème, il faut se tourner sans doute vers un problème de communication entre le développeur de Base et celui de Derivee, car soit, c'est le développeur de Base n'ai pas pris en compte le fait que le comportement puisse être redéfini, soit c'est le développeur de Derivee qui fait une connerie en essayant de le redéfinir.

    On obtient également un masquage de fonction avec le code
    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
    class Base{
    public:
        virtual void foo(int){std::cout<<"base"<<std::endl;}
    };
    class Derivee : public Base{
    public:
        virtual void foo(int *){std::cout<<"derivee"<<std::endl;}
    };
     
    int main(){
        int i;
        int * ptri = &i;
        Base b;
        Derivee d;
        Base * ptrb=&d;
        b.foo(i);
        // d.foo(i); erreur de compilation : attend un int *
        // b.foo(ptri); erreur de compilation : attend un int
        d.foo(ptri);
        ptrb->foo(i); // substitution ratée : affiche "base"
        // ptrb->foo(ptri); erreur de compilation : attend un int
        return 0;
    }
    (NOTA: que foo soit virtuelle ou non dans Derivee ne changera strictement rien )
    Dans ce cas, Liskov n'est effectivement pas respecté, parce que la classe Base dispose d'une propriété "expose une fonction foo qui prend un entier en paramètre" qui est absente de la classe Derivee.

    Mais je crois que le problème est plus du au mode de fonctionnement du compilateur qu'au développeur lui-même : Le fait est que le compilateur fait une (première) recherche "naïve", basée uniquement sur le nom de la fonction appelée lorsqu'il rencontre un appel de fonction, et qu'il s'arrête "au premier nom qui correspond", c'est à dire en priorité celles qui sont explicitement déclarées dans la classe dérivée.

    Je crois que ce comportement est clairement défini dans la norme. L'origine du problème est donc la méconnaissance (ou la non prise en compte) d'une arcane particulière du langage.

    Enfin, il y a le cas encore plus tordu, mais très similaire, d'un code 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
    21
    22
    23
    24
    class Base{
    public:
        virtual void foo(int){std::cout<<"value base"<<std::endl;}
        virtual void foo(int *){std::cout<<"pointer base"<<std::endl;}
    };
    class Derivee : public Base{
    public:
        virtual void foo(int *){std::cout<<"pointer derivee"<<std::endl;}
    };
     
    int main(){
        int i;
        int * ptr1 = &i;
        Base b;
        Derivee d;
        Base * ptrb=&d;
        b.foo(i);
        // d.foo(i); erreur de compilation : attend un int *
        b.foo(ptr1);
        d.foo(ptr1);
        ptrb->foo(i); // substitution inutile : compportement inchangé
        ptrb->foo(ptr1); // substitution OK
        return 0;
    }
    Ce code démontre, si besoin en est, que les mêmes causes ont les mêmes effets : LSP n'est pas respecté, autrement nous n'aurions pas l'erreur de compilation en ligne 18, mais c'est uniquement du à la méconnaissance des arcanes du langage.

    Tout cela me fait dire que, d'une certaine manière, je me demande si une évolution tendant à faire en sorte que le compilateur aille aussi chercher dans les noms de fonctions de la classe de base ne serait pas une bonne chose (si tant est que cela n'occasionne pas d'autres problèmes ). Même si je me rend compte que l'implémentation de cette manière de fonctionner au niveau du compilateur risque d'être assez difficile et qu'elle se ferait au dépend des temps de compilation qui risqueraient d'exploser
    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é
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 292
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 292
    Par défaut
    Ce qui me fait penser que je vois un intérêt à la surcharge masquante : à relaxer les préconditions dans une fonction sans sortir des fonctions virtuelles et autres templates pour vérifier les contrats.
    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
    struct A : noncopyable {
        void f(int i) {
            assert(i>42);
            do_f(i);
        }
        virtual ~A() {}
    private:
        void do_f(int i) = 0;
    };
     
    struct B : A {
        void f(int i) {
            assert(i > 0);
            do_f(i);
        }
    private:
        void do_f(int i) ;
    };
    Ainsi, Selon que l'on manipule un A ou un B, on passe par un contrat adapté.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void f(A & a) {
        a.f(500); // Accepté selon le contrat de A, comme de B
    }
    void g(B & b) {
        b.f(10); // accepté selon le contrat de B
    }
    ...
    B b;
    f(b); // b vu comme un A
    g(b); // b vu comme un B, avec un précondition relaxée sur f
    Bien sûr, il ne faut pas rajouter des choses plus contraignantes dans B::f
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  9. #9
    Membre averti
    Homme Profil pro
    Ingénieur à ses heures perdues
    Inscrit en
    Août 2011
    Messages
    36
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur à ses heures perdues

    Informations forums :
    Inscription : Août 2011
    Messages : 36
    Par défaut
    Merci pour ces échanges très instructifs!

    @Koala

    Le premier exemple ne me choque pas, c'est généralement ce qui est pris comme exemple pour illustrer l'intérêt du polymorphisme.

    Dans ton second exemple :

    // d.foo(i); erreur de compilation : attend un int * --> pour un objet qui n'est ni un pointeur ni une référence cela m'a étonné. Cependant en disant que :

    Mais je crois que le problème est plus du au mode de fonctionnement du compilateur qu'au développeur lui-même : Le fait est que le compilateur fait une (première) recherche "naïve", basée uniquement sur le nom de la fonction appelée lorsqu'il rencontre un appel de fonction, et qu'il s'arrête "au premier nom qui correspond", c'est à dire en priorité celles qui sont explicitement déclarées dans la classe dérivée.
    j'ai davantage compris ce que j'observais à l'exécution. Cependant, on peut toujours se demander si en imputant cela au comportement du compilateur, est-ce qu'il s'agit aussi d'une règle défini dans le standard ou cela dépend du compilateur utilisé ?

    Par contre:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
        ptrb->foo(i); // substitution ratée : affiche "base"
        // ptrb->foo(ptri); erreur de compilation : attend un int
    Je ne comprend pas pourquoi la 1ère ligne fonctione et la deuxième non. La première fonction que le compilateur va trouver dans la classe dérivée est foo(int *) et dans ce cas le premier appel à foo dans l'extrait de code ci-dessus ne devrait même pas compiler et le deuxième si...

    Pour le 3ème exemple:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
       ptrb->foo(i); // substitution inutile : compportement inchangé
        ptrb->foo(ptr1); // substitution OK
    Idem, je ne comprend pas pourquoi la 1ère ligne compile ? Du fait de la rédefinition de foo(int *) dans la classe B, cela devrait masquer toutes les version de la fonction foo dans la classe A....

  10. #10
    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 godskitchen Voir le message
    Merci pour ces échanges très instructifs!

    @Koala

    Le premier exemple ne me choque pas, c'est généralement ce qui est pris comme exemple pour illustrer l'intérêt du polymorphisme.

    Dans ton second exemple :

    // d.foo(i); erreur de compilation : attend un int * --> pour un objet qui n'est ni un pointeur ni une référence cela m'a étonné. Cependant en disant que :



    j'ai davantage compris ce que j'observais à l'exécution. Cependant, on peut toujours se demander si en imputant cela au comportement du compilateur, est-ce qu'il s'agit aussi d'une règle défini dans le standard ou cela dépend du compilateur utilisé ?
    C'est defini par la norme. Le fond est qu'une classe est une portee et qu'une classe derivee est consideree comme imbriquee dans la portee. La meme chose peut se passer des qu'on a des portees imbriquees (namespace ou bloc). Dans une conversation precedente, je completais (le contexte peut aussi etre interessant, je donnai un exemple avec des namespace p.e.)

    Citation Envoyé par Jean-Marc.Bourguet Voir le message
    Le principe est qu'on fait une recherche et qu'on arrête celle-ci une fois le nom trouvé. Puis on cherche la meilleure fonction avec ce nom en appliquant une série de règles relativement complexes à cause des conversions implicites, arguments par défaut et de la possibilité de templates de fonction à instancier.

    A noter qu'il n'y a pas d'arguments très fort pour dire que dans le cas des fonctions on ne pourrait pas collecter toutes les surcharges avant de choisir. De mémoire dans D&E (j'ai pas le bouquin ici) Stroustrup dit qu'outre le fait que c'est plus homogène (on traite tous les identificateurs de la même façon -- en passant au moment où on cherche les définitions, on n'a aucune idée si c'est une définition de fonction, de variable, de template, de type... qu'on va trouver) c'est pour éviter des problèmes que d'autres modifications introduites depuis éviteraient maintenant. Je suppose que quand on s'en est rendu compte, changer cet aspect de la collecte des fonctions à surcharger aurait causé trop de problèmes.

Discussions similaires

  1. Qu'est ce que overload & override ?
    Par savoir dans le forum Débuter
    Réponses: 5
    Dernier message: 12/08/2007, 11h28
  2. Réponses: 5
    Dernier message: 17/05/2006, 11h33
  3. [VB.NET]Comment correctement utiliser Overrides/Overloads?
    Par NicolasJolet dans le forum VB.NET
    Réponses: 5
    Dernier message: 21/03/2006, 10h39
  4. [OO] override overload
    Par eponette dans le forum Langage
    Réponses: 9
    Dernier message: 10/02/2006, 15h21
  5. [Traduction] Overload et Override en français ?
    Par vbrabant dans le forum Langage
    Réponses: 3
    Dernier message: 11/08/2005, 18h44

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