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

219

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);
}

Vous avez cette erreur:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: Dans la fonction `int main () ':
test.cpp: 31: erreur: pas de fonction correspondante pour l'appel à `Derived :: gogo (int) '
test.cpp: 21: note: les candidats sont: virtual void Derived :: gogo (int *) 
test.cpp: 33: 2: avertissement: pas de nouvelle ligne à la fin du fichier
> Code de sortie: 1

Ici, la fonction de la classe dérivée éclipse toutes les fonctions de même nom (pas de signature) dans la classe de base. D'une manière ou d'une autre, ce comportement de C ++ ne semble pas correct. Pas polymorphe.

Aman Aggarwal
la source
8
question brillante, je ne l'ai découvert que récemment aussi
Matt Joiner
11
Je pense que Bjarne (à partir du lien publié par Mac) l'a exprimé en une phrase: "En C ++, il n'y a pas de surcharge entre les étendues - les étendues de classe dérivées ne font pas exception à cette règle générale."
sivabudh
7
@Ashish Ce lien est rompu. Voici la bonne (à partir de maintenant) - stroustrup.com/bs_faq2.html#overloadderived
nsane
3
Aussi, je voulais souligner que cela obj.Base::gogo(7);fonctionne toujours en appelant la fonction cachée.
forumulator

Réponses:

406

À en juger par le libellé de votre question (vous avez utilisé le mot «cacher»), vous savez déjà ce qui se passe ici. Le phénomène est appelé "dissimulation de nom". Pour une raison quelconque, chaque fois que quelqu'un pose une question sur la raison pour laquelle le masquage de nom se produit, les personnes qui répondent disent que cela s'appelle "masquage de nom" et expliquent comment cela fonctionne (que vous connaissez probablement déjà), ou expliquent comment le remplacer (ce que vous n'a jamais posé de questions), mais personne ne semble se soucier de répondre à la véritable question du «pourquoi».

La décision, la raison d'être du nom qui se cache, c'est-à-dire pourquoi il a é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 la résolution de surcharge en C ++ fonctionne en choisissant la meilleure fonction dans l'ensemble des candidats. Cela se fait en faisant correspondre les types d'arguments aux types de paramètres. Les règles de correspondance peuvent parfois être compliquées et conduire souvent à des résultats qui peuvent être perçus comme illogiques par un utilisateur non préparé. L'ajout de nouvelles fonctions à un ensemble de fonctions existantes peut entraîner un changement assez radical des résultats de résolution de surcharge.

Par exemple, supposons que la classe de base Bpossède une fonction membre fooqui accepte un paramètre de type void *, et tous les appels à foo(NULL)sont résolus en B::foo(void *). Disons qu'il n'y a pas de nom caché et cela B::foo(void *)est visible dans de nombreuses classes différentes qui descendent B. Cependant, disons que dans un descendant Dde classe [indirect, distant], Bune fonction foo(int)est définie. Désormais, sans nom, le masquage Da à la fois foo(void *)et foo(int)visible et participe à la résolution de surcharge. À quelle fonction les appels seront-ils foo(NULL)résolus s'ils sont effectués via un objet de type D? Ils se résoudront à D::foo(int), puisqu'il ints'agit d'une meilleure correspondance pour le zéro intégral (c.-à-d.NULL) que tout type de pointeur. Ainsi, tout au long de la hiérarchie, les appels à se foo(NULL)résoudre à une fonction, tandis que dans D(et sous) ils se résolvent soudainement à une autre.

Un autre exemple est donné dans La conception et l'évolution de C ++ , page 77:

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

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

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copy Base part of b
    b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}

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

Ce comportement a été jugé indésirable lors de la conception du langage. Comme meilleure approche, il a été décidé de suivre la spécification "masquage de nom", ce qui signifie que chaque classe commence par une "feuille blanche" en ce qui concerne chaque nom de méthode qu'elle déclare. Afin de remplacer ce comportement, une action explicite est requise de la part de l'utilisateur: à l'origine une redéclaration de méthode (s) héritée (actuellement déconseillée), maintenant une utilisation explicite de using-declaration.

Comme vous l'avez correctement observé dans votre message d'origine (je fais référence à la remarque "non polymorphe"), ce comportement peut ê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'à la fin, la dissimulation du nom se révélerait être un moindre mal.

Fourmi
la source
22
Oui, c'est une vraie réponse à la question. Je vous remercie. J'étais aussi curieux.
Omnifarious
4
Très bonne réponse! En outre, en pratique, la compilation serait probablement beaucoup plus lente si la recherche de nom devait aller jusqu'au sommet à chaque fois.
Drew Hall
6
(Ancienne réponse, je sais.) nullptrJe vais maintenant m'opposer à votre exemple en disant "si vous voulez appeler la void*version, vous devez utiliser un type de pointeur". Y a-t-il un exemple différent où cela peut être mauvais?
GManNickG
3
Le nom qui se cache n'est pas vraiment mauvais. La relation "est-un" est toujours là et est disponible via l'interface de base. Alors peut d->foo()- être ne vous obtiendrez pas le "Is-a Base", mais le static_cast<Base*>(d)->foo() fera , y compris l'envoi dynamique.
Kerrek SB
12
Cette réponse est inutile car l'exemple donné se comporte de la même manière avec ou sans masquage: D :: foo (int) sera appelé soit parce qu'il correspond mieux, soit parce qu'il a masqué B: foo (int).
Richard Wolf
46

Les règles de résolution de nom indiquent que la recherche de nom s'arrête dans la première étendue dans laquelle un nom correspondant est trouvé. À ce stade, les règles de résolution de surcharge interviennent pour trouver la meilleure correspondance des fonctions disponibles.

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

La solution consiste à introduire les déclarations de base via une déclaration using dans la classe Derived:

using Base::gogo;

... permettrait aux règles de recherche de nom de trouver tous les candidats et donc la résolution de surcharge se déroulerait comme prévu.

Drew Hall
la source
10
OP: "Pourquoi une fonction remplacée dans la classe dérivée masque-t-elle d'autres surcharges de la classe de base?" Cette réponse: "Parce que c'est le cas".
Richard Wolf
12

C'est "By Design". En C ++, la résolution de surcharge pour ce type de méthode fonctionne comme suit.

  • En commençant par le type de la référence et en allant ensuite au type de base, trouvez le premier type qui a une méthode nommée "gogo"
  • En considérant uniquement les méthodes nommées "gogo" sur ce type, trouver une surcharge correspondante

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

JaredPar
la source
2

Le masquage de nom est logique 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 caché par Derived::func(double)dans Derived, nous appellerions la fonction de classe de base lors de l'appel dobj.func(0.f), même si un flottant peut être promu en double.

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

Sandeep Singh
la source