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

Resuelto Aman Aggarwal asked hace 15 años • 4 respuestas

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

Recibí este error:

>g++ -pedante -Os test.cpp -o prueba
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 Derivado::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 eclipsa todas las funciones del mismo nombre (no firma) en la clase base. De alguna manera, este comportamiento de C++ no parece correcto. No polimórfico.

Aman Aggarwal avatar Oct 27 '09 11:10 Aman Aggarwal
Aceptado

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

La decisión, la razón detrás de ocultar el nombre, es decir, por qué en realidad fue diseñado en C++, es evitar ciertos comportamientos contrarios a la intuición, imprevistos y potencialmente peligrosos que podrían tener lugar si se permitiera que el conjunto heredado de funciones sobrecargadas se mezclara con el actual. conjunto 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 hace haciendo coincidir los tipos de argumentos con los tipos de parámetros. Las reglas de coincidencia pueden ser complicadas en ocasiones y, a menudo, conducen a resultados que un usuario no preparado podría percibir como ilógicos. Agregar nuevas funciones a un conjunto de funciones previamente existentes podría resultar en un cambio bastante drástico en los resultados de la resolución de sobrecarga.

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

Otro ejemplo se da en El diseño y evolución de C++ , página 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: 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 provocaría un corte.

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

Como observó correctamente en su publicación original (me refiero a la observación "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 ocultar el nombre sería un mal menor.

AnT stands with Russia avatar Oct 27 '2009 06:10 AnT stands with Russia

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 se activan 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 existe una conversión estándar de int a int*, la búsqueda falla.

La solución es incorporar las declaraciones Base mediante una declaración de uso en la clase Derivada:

using Base::gogo;

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

Drew Hall avatar Oct 27 '2009 04:10 Drew Hall