256 votes

Pourquoi une fonction remplacée dans la classe dérivée cache-t-elle les autres surcharges de la classe de base ?

Considérez le code :

#include <stdio.h>

class Base {
public: 
    virtual void gogo(int a){
        printf(" Base :: gogo (int) \n");
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*) \n");
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*) \n");
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

Erreur obtenue :

\>g++ -pedantic -Os test.cpp -o test
test.cpp: In function \`int main()':
test.cpp:31: error: no matching function for call to \`Derived::gogo(int)'
test.cpp:21: note: candidates are: virtual void Derived::gogo(int\*) 
test.cpp:33:2: warning: no newline at end of file
>Code de sortie: 1

Ici, la fonction de la classe dérivée éclipse toutes les fonctions du même nom (pas de la même signature) dans la classe de base. En quelque sorte, ce comportement en C++ ne semble pas correct. Non polymorphe.

1 votes

10 votes

Brillante question, je viens tout juste de découvrir cela récemment aussi

11 votes

Je pense que Bjarne (du lien posté par Mac) l'a mieux exprimé en une phrase : "En C++, il n'y a pas de surcharge entre les espaces de noms - les espaces de noms de classes dérivées ne font pas exception à cette règle générale."

444voto

AndreyT Points 139512

À en juger par la formulation de votre question (vous avez utilisé le mot "cacher"), vous savez déjà ce qui se passe ici. Le phénomène s'appelle "masquage de nom". Pour une raison quelconque, chaque fois que quelqu'un pose une question sur pourquoi le masquage de nom se produit, les personnes qui répondent disent soit que cela s'appelle "masquage de nom" et expliquent comment cela fonctionne (que vous connaissez probablement déjà), soit expliquent comment le remplacer (ce qui n'était pas votre question), mais personne ne semble se soucier de répondre à la question réelle "pourquoi".

La décision, la justification derrière le masquage de nom, c'est-à-dire pourquoi il a réellement été conçu en C++, est d'éviter certains comportements contre-intuitifs, imprévus et potentiellement dangereux qui pourraient se produire si l'ensemble hérité de fonctions surchargées était autorisé à se mélanger avec l'ensemble actuel de surcharges dans la classe donnée. Vous savez probablement que en C++, la résolution de surcharge fonctionne en choisissant la meilleure fonction parmi l'ensemble de candidats. Cela se fait en faisant correspondre les types d'arguments aux types de paramètres. Les règles de correspondance peuvent être compliquées parfois, et conduisent souvent à des résultats qui pourraient être perçus comme illogiques par un utilisateur non préparé. Ajouter de nouvelles fonctions à un ensemble de fonctions déjà existantes pourrait entraîner un changement assez drastique dans les résultats de la résolution de surcharge.

Par exemple, disons que la classe de base B a une fonction membre foo qui prend un paramètre de type void *, et que tous les appels à foo(NULL) sont résolus en B::foo(void *). Supposons qu'il n'y a pas de masquage de nom et que ce B::foo(void *) est visible dans de nombreuses classes descendantes de B. Cependant, disons que dans une [descendance indirecte, distante] D de la classe B, une fonction foo(int) est définie. Maintenant, sans masquage de nom, D a à la fois foo(void *) et foo(int) visibles et participant à la résolution de surcharge. Vers quelle fonction les appels à foo(NULL) se résoudront-ils, s'ils sont faits via un objet de type D? Ils seront résolus en D::foo(int), puisque int est un meilleur correspondant pour zéro entier (c'est-à-dire NULL) que tout type de pointeur. Ainsi, dans toute la hiérarchie, les appels à foo(NULL) se résolvent en une fonction, tandis que dans D (et en dessous), ils se résolvent soudainement en une autre.

Un autre exemple est donné dans The Design and Evolution of C++, page 77:

class Base {
    int x;
public:
    virtual void copy(Base* p) { x = p-> x; }
};

class Derived : public Base{
    int xx;
public:
    virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copie la partie Base de b
    b.copy(&a); // erreur: copie(Base*) est masquée par copy(Derived*)
}

Sans cette règle, l'état de b serait partiellement mis à jour, entraînant une coupe.

Ce comportement a été jugé indésirable lors de la conception du langage. En tant qu'approche meilleure, il a été décidé de suivre la spécification du "masquage de nom", signifiant que chaque classe commence avec une "feuille blanche" par rapport à chaque nom de méthode qu'elle déclare. Pour annuler ce comportement, une action explicite de l'utilisateur est requise : initialement une redéclaration de méthode(s) héritée(s) (actuellement déconseillée), maintenant une utilisation explicite de la déclaration using.

Comme vous l'avez correctement observé dans votre publication originale (je fais référence à la remarque "Non polymorphe"), ce comportement pourrait être considéré comme une violation de la relation IS-A entre les classes. C'est vrai, mais apparemment à l'époque il a été décidé qu'en fin de compte, le masquage de nom s'avérerait être un moindre mal.

25 votes

Oui, c'est une vraie réponse à la question. Merci. J'étais curieux aussi.

4 votes

Super réponse! En outre, en termes pratiques, la compilation serait probablement beaucoup plus lente si la recherche de nom devait remonter jusqu'en haut à chaque fois.

0 votes

J'ai cherché cette réponse depuis longtemps.

54voto

Drew Hall Points 15917

Les règles de résolution de nom indiquent que la recherche de nom s'arrête dans la première portée où un nom correspondant est trouvé. À ce stade, les règles de résolution de surcharge entrent en jeu pour trouver la meilleure correspondance des fonctions disponibles.

Dans ce cas, gogo(int*) est trouvé (seul) dans la portée de la classe dérivée, et comme il n'y a pas de conversion standard de int à int*, la recherche échoue.

La solution est d'apporter les déclarations de la classe de base via une déclaration d'utilisation dans la classe dérivée:

using Base::gogo;

...permettrait aux règles de recherche de nom de trouver tous les candidats et ainsi la résolution de la surcharge se déroulerait comme vous le souhaitez.

11 votes

OP: "Pourquoi une fonction surchargée dans la classe dérivée cache-t-elle d'autres surcharges de la classe de base?" Cette réponse : "Parce qu'elle le fait".

13voto

JaredPar Points 333733

Cela se nomme "Par conception". En C++, la résolution de surcharge pour ce type de méthode fonctionne comme suit.

  • En partant du type de la référence et en remontant vers le type de base, trouvez le premier type qui a une méthode appelée "gogo"
  • En ne considérant que les méthodes nommées "gogo" sur ce type, trouvez une surcharge correspondante

Étant donné que Derived n'a pas de fonction correspondante nommée "gogo", la résolution de surcharge échoue.

2voto

Sandeep Singh Points 1058

Le masquage de nom a du sens car il empêche les ambiguïtés dans la résolution de nom.

Considérez ce code:

class Base
{
public:
    void func (float x) { ... }
}

class Derived: public Base
{
public:
    void func (double x) { ... }
}

Derived dobj;

Si Base::func(float) n'était pas masqué par Derived::func(double) dans Derived, nous appellerions la fonction de la classe de base lors de l'appel de dobj.func(0.f), même si un float peut être promu en double.

Référence: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X