Por que uma função substituída na classe derivada oculta outras sobrecargas da classe base?

219

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

Tem este erro:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: Na função `int main () ':
test.cpp: 31: error: nenhuma função correspondente à chamada para `Derived :: gogo (int) '
test.cpp: 21: note: os candidatos são: virtual void Derivado :: gogo (int *) 
test.cpp: 33: 2: aviso: nenhuma nova linha no final do arquivo
> Código de saída: 1

Aqui, a função da classe Derived está eclipsando todas as funções do mesmo nome (não assinatura) na classe base. De alguma forma, esse comportamento do C ++ não parece bom. Não polimórfico.

Aman Aggarwal
fonte
8
brilhante pergunta, eu só descobriu isso recentemente também
Matt Joiner
11
Acho que Bjarne (do link que o Mac postou) colocou melhor em uma frase: "No C ++, não há sobrecarga nos escopos - os escopos de classe derivados não são uma exceção a essa regra geral".
114410 Sivabudh
7
@ Ashish Esse link está quebrado. Aqui está o correto (a partir de agora) - stroustrup.com/bs_faq2.html#overloadderived
nsane
3
Além disso, queria salientar que obj.Base::gogo(7);ainda funciona chamando a função oculta.
fórumulator

Respostas:

406

A julgar pelo teor da sua pergunta (você usou a palavra "ocultar"), você já sabe o que está acontecendo aqui. O fenômeno é chamado de "ocultação de nome". Por alguma razão, sempre que alguém faz uma pergunta sobre por que a ocultação de nomes acontece, as pessoas que respondem dizem que isso se chama "ocultação de nomes" e explicam como funciona (o que você provavelmente já conhece) ou explicam como substituí-lo (que você nunca perguntei sobre), mas ninguém parece se importar em abordar a questão real "por que".

A decisão, a lógica por trás do nome oculto, ou seja, por que ele realmente foi projetado em C ++, é evitar certos comportamentos contra-intuitivos, imprevistos e potencialmente perigosos que possam ocorrer se o conjunto herdado de funções sobrecarregadas puder se misturar ao conjunto atual de sobrecargas na classe especificada. Você provavelmente sabe que na resolução de sobrecarga do C ++ funciona escolhendo a melhor função do conjunto de candidatos. Isso é feito combinando os tipos de argumentos aos tipos de parâmetros. As regras de correspondência podem ser complicadas às vezes e geralmente levam a resultados que podem ser vistos como ilógicos por um usuário despreparado. Adicionar novas funções a um conjunto de funções existentes anteriormente pode resultar em uma mudança bastante drástica nos resultados da resolução de sobrecarga.

Por exemplo, digamos que a classe base Btenha uma função de membro fooque aceita um parâmetro do tipo void *e todas as chamadas para foo(NULL)sejam resolvidas B::foo(void *). Digamos que não haja ocultação de nomes e isso B::foo(void *)é visível em várias classes diferentes, descendentes de B. No entanto, digamos que em algum descendente [indireto, remoto] Dda classe Buma função foo(int)seja definida. Agora, sem nome esconderijo Dtem tanto foo(void *)e foo(int)visível e participando de resolução de sobrecarga. Para qual função as chamadas serão foo(NULL)resolvidas, se feitas por meio de um objeto do tipo D? Eles resolverão D::foo(int), já que inté uma melhor correspondência para o zero integral (ou seja,NULL) que qualquer tipo de ponteiro. Assim, em toda a hierarquia, as chamadas são foo(NULL)resolvidas para uma função, enquanto em D(e abaixo) elas repentinamente são resolvidas para outra.

Outro exemplo é dado em 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*)
}

Sem essa regra, o estado de b seria parcialmente atualizado, levando ao fatiamento.

Esse comportamento foi considerado indesejável quando o idioma foi projetado. Como uma abordagem melhor, foi decidido seguir a especificação "ocultação de nome", o que significa que cada classe começa com uma "planilha" em relação a cada nome de método que declara. Para substituir esse comportamento, é necessária uma ação explícita do usuário: originalmente uma redeclaração de métodos herdados (atualmente obsoletos), agora um uso explícito de declaração de uso.

Como você observou corretamente em sua postagem original (estou me referindo à observação "Não polimórfica"), esse comportamento pode ser visto como uma violação do relacionamento IS-A entre as classes. Isso é verdade, mas aparentemente naquela época foi decidido que, no final, esconder-se provaria ser um mal menor.

Formiga
fonte
22
Sim, esta é uma resposta real para a pergunta. Obrigado. Eu também estava curioso.
Onipotente 27/10/09
4
Ótima resposta! Além disso, por uma questão prática, a compilação provavelmente ficaria muito mais lenta se a pesquisa de nomes tivesse que chegar ao topo todas as vezes.
Drew Hall
6
(Resposta antiga, eu sei.) Agora nullptr, objetarei ao seu exemplo dizendo "se você quiser chamar a void*versão, use um tipo de ponteiro". Existe um exemplo diferente em que isso pode ser ruim?
GManNickG 17/05
3
O nome escondido não é realmente mau. O relacionamento "is-a" ainda existe e está disponível na interface base. Então, talvez d->foo()você não obtenha o "Is-a Base", mas static_cast<Base*>(d)->foo() sim , incluindo o envio dinâmico.
11114 Kerrek SB
12
Essa resposta é inútil porque o exemplo dado se comporta da mesma forma com ou sem ocultar: D :: foo (int) será chamado porque é uma correspondência melhor ou porque ocultou B: foo (int).
Richard Wolf
46

As regras de resolução de nomes dizem que a pesquisa de nomes para no primeiro escopo em que um nome correspondente é encontrado. Nesse ponto, as regras de resolução de sobrecarga entram em ação para encontrar a melhor correspondência de funções disponíveis.

Nesse caso, gogo(int*)é encontrado (sozinho) no escopo da classe Derived e, como não há conversão padrão de int para int *, a pesquisa falha.

A solução é trazer as declarações Base por meio de uma declaração using na classe Derived:

using Base::gogo;

... permitiria que as regras de pesquisa de nome encontrassem todos os candidatos e, portanto, a resolução de sobrecarga continuaria conforme o esperado.

Drew Hall
fonte
10
OP: "Por que uma função substituída na classe derivada oculta outras sobrecargas da classe base?" Esta resposta: "Porque faz".
Richard Wolf
12

Isso é "por design". Em C ++, a resolução de sobrecarga para esse tipo de método funciona da seguinte maneira.

  • Começando pelo tipo de referência e depois indo para o tipo base, encontre o primeiro tipo que possui um método chamado "gogo"
  • Considerando apenas os métodos denominados "gogo" nesse tipo, encontre uma sobrecarga correspondente

Como o Derivado não possui uma função correspondente denominada "gogo", a resolução de sobrecarga falha.

JaredPar
fonte
2

Ocultar nomes faz sentido porque evita ambiguidades na resolução de nomes.

Considere este código:

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

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

Derived dobj;

Se Base::func(float)não estivesse oculto Derived::func(double)em Derived, chamaríamos a função de classe base ao chamar dobj.func(0.f), mesmo que um float possa ser promovido para um double.

Referência: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

Sandeep Singh
fonte