web-dev-qa-db-fra.com

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

Considérons 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: aucune correspondance fonction pour l'appel à `Derived :: gogo (int) '
 test.cpp: 21: remarque: les candidats sont les suivants: 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 du même nom (pas de signature) de la classe de base. D'une manière ou d'une autre, ce comportement de C++ ne semble pas correct. Non polymorphe.

211
Aman Aggarwal

À 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 s'appelle "cacher son nom". Pour une raison quelconque, chaque fois que quelqu'un pose une question à propos de pourquoi le masquage de nom se produit, les personnes qui répondent répondent soit à l'appelant "masquage de nom" et expliquent comment cela fonctionne (que vous connaissez probablement déjà), ou expliquez comment la remplacer (ce que vous n'avez jamais demandé), mais personne ne semble se soucier de répondre à la question du "pourquoi".

La décision, la raison derrière la dissimulation du nom, 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é mélanger avec le jeu actuel de surcharges dans la classe donnée. Vous savez probablement que la résolution de surcharge C++ fonctionne en choisissant la meilleure fonction parmi l'ensemble des candidats. Cela se fait en faisant correspondre les types d'arguments aux types de paramètres. Les règles d'appariement peuvent parfois être compliquées et conduire souvent à des résultats qui pourraient être perçus comme illogiques par un utilisateur non préparé. L'ajout de nouvelles fonctions à un ensemble de fonctions existantes pourrait entraîner un changement radical dans les résultats de résolution de surcharge.

Par exemple, supposons que la classe de base B ait une fonction membre foo qui prend un paramètre de type void *, Et tous les appels à foo(NULL) sont résolus en B::foo(void *). Supposons qu'il n'y ait pas de nom caché et que B::foo(void *) soit visible dans de nombreuses classes différentes descendant de B. Cependant, supposons que dans certains descendants [indirects, distants] D de la classe B, une fonction foo(int) soit définie. Maintenant, sans masquer le nom, D a les deux foo(void *) et foo(int) visibles et participant à la résolution de la surcharge. Quelle fonction seront résolus les appels à foo(NULL), s'ils sont effectués via un objet de type D? Ils seront résolus en D::foo(int), étant donné que int correspond mieux au zéro intégral (c.-à-d. NULL) qu'à tout type de pointeur. Ainsi, tout au long de la hiérarchie, les appels à foo(NULL) se résolvent en une fonction, tandis que dans D (et sous), ils se résolvent soudainement en une autre.

Un autre exemple est donné dans Conception et é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, ce qui entraînerait un découpage.

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

Comme vous l'avez correctement observé dans votre message d'origine (je fais référence à la remarque "Non polymorphique"), ce comportement peut être considéré comme une violation des relations IS-A entre les classes. C’est vrai, mais apparemment à l’époque, il a été décidé qu’en fin de compte, cacher son nom serait un moindre mal.

392
AnT

Les règles de résolution de noms 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 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 d'int + en *, la recherche échoue.

La solution consiste à importer les déclarations de base via une déclaration using dans la classe dérivée:

using Base::gogo;

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

43
Drew Hall

C'est "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, puis du type de base, recherchez le premier type qui a une méthode nommée "gogo"
  • Considérant que les méthodes nommées "gogo" sur ce type trouvent une surcharge correspondante

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

13
JaredPar

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

Considérons 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 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/

2
Sandeep Singh