¿Por qué una función anulada en la clase derivada oculta otras sobrecargas de la clase base?

219

Considera el código:

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

Tengo este error:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: en la función `int main () ':
test.cpp: 31: error: no hay función coincidente para la llamada a `Derived :: gogo (int) '
test.cpp: 21: nota: los candidatos son: virtual void Derived :: gogo (int *) 
test.cpp: 33: 2: advertencia: no hay nueva línea al final del archivo
> Código de salida: 1

Aquí, la función de la clase Derivada está eclipsando todas las funciones del mismo nombre (no firma) en la clase base. De alguna manera, este comportamiento de C ++ no se ve bien. No polimórfico

Aman Aggarwal
fuente
8
pregunta brillante, también descubrí esto recientemente
Matt Joiner
11
Creo que Bjarne (del enlace publicado por Mac) lo expresó mejor en una oración: "En C ++, no hay sobrecarga en los ámbitos: los ámbitos de clase derivados no son una excepción a esta regla general".
sivabudh
77
@Ashish Ese enlace está roto. Aquí está el correcto (a partir de ahora) - stroustrup.com/bs_faq2.html#overloadderived
nsane
3
Además, quería señalar que obj.Base::gogo(7);todavía funciona llamando a la función oculta.
forumulator

Respuestas:

406

A juzgar por la redacción de su pregunta (usó la palabra "ocultar"), ya sabe lo que está sucediendo aquí. El fenómeno se llama "ocultación de nombres". Por alguna razón, cada vez que alguien hace una pregunta sobre por qué ocurre la ocultación del nombre, las personas que responden dicen que esto se llama "ocultación del nombre" y explican cómo funciona (lo que probablemente ya sepas), o explican cómo anularlo (lo cual nunca se le preguntó), pero a nadie parece importarle abordar la pregunta real del "por qué".

La decisión, la razón detrás de la ocultación del nombre, es decir, por qué en realidad se diseñó en C ++, es evitar ciertos comportamientos contraintuitivos, imprevistos y potencialmente peligrosos que podrían tener lugar si se permitiera que el conjunto heredado de funciones sobrecargadas se mezclara con el conjunto actual de sobrecargas en la clase dada. Probablemente sepa que en C ++ la resolución de sobrecarga funciona eligiendo la mejor función del conjunto de candidatos. Esto se realiza haciendo coincidir los tipos de argumentos con los tipos de parámetros. Las reglas de coincidencia pueden ser complicadas a veces, y a menudo conducen a resultados que pueden ser percibidos como ilógicos por un usuario no preparado. Agregar nuevas funciones a un conjunto de funciones previamente existentes podría resultar en un cambio bastante drástico en los resultados de resolución de sobrecarga.

Por ejemplo, supongamos que la clase base Btiene una función miembro fooque toma un parámetro de tipo void *y foo(NULL)se resuelven todas las llamadas a B::foo(void *). Digamos que no hay nombre oculto y esto B::foo(void *)es visible en muchas clases diferentes que descienden B. Sin embargo, supongamos que en algún descendiente [indirecto, remoto] Dde la clase se define Buna función foo(int). Ahora, sin ocultar el nombre Dtiene tanto foo(void *)y foo(int)visible y participando en la resolución de sobrecarga. ¿A qué función se foo(NULL)resolverán las llamadas , si se realizan a través de un objeto de tipo D? Resolverán D::foo(int), desdeint es una mejor coincidencia para el cero integral (es decirNULL) que cualquier tipo de puntero. Entonces, a lo largo de la jerarquía, las llamadas sefoo(NULL)resolver a una función, mientras que en D(y debajo) de repente se resuelven a otra.

Otro ejemplo se da en The Design and Evolution of C ++ , página 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*)
}

Sin esta regla, el estado de b se actualizaría parcialmente, lo que llevaría a la división.

Este comportamiento se consideró indeseable cuando se diseñó el lenguaje. Como un mejor enfoque, se decidió seguir la especificación de "ocultación del nombre", lo que significa que cada clase comienza con una "hoja limpia" con respecto a cada nombre de método que declara. Para anular este comportamiento, se requiere una acción explícita del usuario: originalmente una redeclaración de los métodos heredados (actualmente en desuso), ahora un uso explícito del uso de la declaración.

Como observó correctamente en su publicación original (me refiero al comentario "No polimórfico"), este comportamiento podría verse como una violación de la relación IS-A entre las clases. Esto es cierto, pero aparentemente en aquel entonces se decidió que, al final, esconderse resultaría ser un mal menor.

Hormiga
fuente
22
Sí, esta es una respuesta real a la pregunta. Gracias. Yo también tenía curiosidad.
Omnifarious
44
¡Gran respuesta! Además, como cuestión práctica, la compilación probablemente se volvería mucho más lenta si la búsqueda de nombres tuviera que ir al principio cada vez.
Drew Hall
66
(Respuesta anterior, lo sé). Ahora nullptrme opondría a su ejemplo diciendo "si desea llamar a la void*versión, debe usar un tipo de puntero". ¿Hay un ejemplo diferente donde esto puede ser malo?
GManNickG
3
El nombre escondido no es realmente malo. La relación "es-a" todavía está ahí y está disponible a través de la interfaz base. Entonces, tal d->foo()vez no le dé el "Is-a Base", pero lo static_cast<Base*>(d)->foo() hará , incluido el despacho dinámico.
Kerrek SB
12
Esta respuesta no es útil porque el ejemplo dado se comporta igual con o sin ocultación: se llamará a D :: foo (int) porque es una mejor coincidencia o porque ha ocultado B: foo (int).
Richard Wolf
46

Las reglas de resolución de nombres dicen que la búsqueda de nombres se detiene en el primer ámbito en el que se encuentra un nombre coincidente. En ese punto, las reglas de resolución de sobrecarga entran en acción para encontrar la mejor combinación de funciones disponibles.

En este caso, gogo(int*)se encuentra (solo) en el ámbito de la clase Derivada, y como no hay una conversión estándar de int a int *, la búsqueda falla.

La solución es traer las declaraciones de Base a través de una declaración de uso en la clase Derivada:

using Base::gogo;

... permitiría que las reglas de búsqueda de nombres encuentren todos los candidatos y, por lo tanto, la resolución de sobrecarga continuaría como esperaba.

Drew Hall
fuente
10
OP: "¿Por qué una función anulada en la clase derivada oculta otras sobrecargas de la clase base?" Esta respuesta: "Porque lo hace".
Richard Wolf
12

Esto es "Por diseño". En C ++, la resolución de sobrecarga para este tipo de método funciona de la siguiente manera.

  • Comenzando en el tipo de referencia y luego yendo al tipo base, encuentre el primer tipo que tiene un método llamado "gogo"
  • Considerando solo los métodos llamados "gogo" en ese tipo, encuentre una sobrecarga coincidente

Dado que Derived no tiene una función de coincidencia llamada "gogo", la resolución de sobrecarga falla.

JaredPar
fuente
2

La ocultación de nombres tiene sentido porque evita ambigüedades en la resolución de nombres.

Considera este código:

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

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

Derived dobj;

Si Base::func(float)no estuviera oculto Derived::func(double)en Derivado, llamaríamos a la función de la clase base al llamar dobj.func(0.f), aunque un float pueda promoverse a un doble.

Referencia: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

Sandeep Singh
fuente