Por que uma function substituída na class derivada oculta outras sobrecargas da class base?

Considere o código:

#include  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 esse erro:

 teste g ++ -pedantic -Os test.cpp -o
 test.cpp: na function `int main () ':
 test.cpp: 31: erro: nenhuma function correspondente para chamar o `Derived :: gogo (int) '
 test.cpp: 21: note: os candidatos são: void virtual Derived :: gogo (int *) 
 test.cpp: 33: 2: aviso: não há nova linha no final do arquivo
 > Código de saída: 1

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

A julgar pelo texto da sua pergunta (você usou a palavra “esconder”), você já sabe o que está acontecendo aqui. O fenômeno é chamado de “esconderijo de nome”. Por alguma razão, toda vez que alguém faz uma pergunta sobre por que o nome se esconde, as pessoas que respondem dizem que isso significa “esconder nomes” e explicam como funciona (o que você provavelmente já sabe), ou explicam como substituí-lo nunca perguntou sobre), mas ninguém parece se importar em abordar a questão “por que”.

A decisão, a lógica por trás do nome escondido, ou seja, porque ele foi projetado em C ++, é para evitar certos comportamentos inesperados, imprevistos e potencialmente perigosos que podem ocorrer se o conjunto herdado de funções sobrecarregadas puderem ser combinadas com o conjunto atual de nomes. sobrecargas na class dada. Você provavelmente sabe que em resolução de sobrecarga C ++ funciona escolhendo a melhor function do conjunto de candidatos. Isso é feito combinando os tipos de argumentos com os tipos de parâmetros. As regras de correspondência podem ser complicadas às vezes, e muitas vezes levam a resultados que podem ser percebidos como ilógicos por um usuário despreparado. Adicionar novas funções a um conjunto de funções anteriormente existentes pode resultar em uma mudança bastante drástica nos resultados da resolução de sobrecarga.

Por exemplo, digamos que a class base B possui uma function-membro foo que usa um parâmetro do tipo void * , e todas as chamadas para foo(NULL) são resolvidas para B::foo(void *) . Digamos que não exista nenhum nome escondido e que esse B::foo(void *) seja visível em muitas classs diferentes, descendentes de B No entanto, digamos que em algum descendente D [indireto, remoto] da class B uma function foo(int) é definida. Agora, sem o nome esconder D tem ambos foo(void *) e foo(int) visíveis e participando da resolução de sobrecarga. Qual function as chamadas para foo(NULL) resolverão se forem feitas através de um object do tipo D ? Eles serão resolvidos para D::foo(int) , já que int é uma correspondência melhor para zero integral (isto é, NULL ) do que qualquer tipo de ponteiro. Assim, em toda a hierarquia, as chamadas para foo(NULL) resolvem para uma function, enquanto que em D (e sob) elas repentinamente resolvem para outra.

Outro exemplo é dado em O design e evolução do 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 “name hiding”, significando que cada class começa com uma “folha limpa” com relação a cada nome de método que declara. Para replace esse comportamento, uma ação explícita é exigida do usuário: originalmente uma redeclaração de método (s) herdado (atualmente preterido), agora um uso explícito de declaração de uso.

Como você observou corretamente em seu post original (estou me referindo à observação “Não polimórfico”), esse comportamento pode ser visto como uma violação da relação IS-A entre as classs. Isso é verdade, mas, aparentemente, naquela época, foi decidido que, no final do nome, esconder-se seria um mal menor.

As regras de resolução de nomes dizem que a pesquisa de nomes é interrompida no primeiro escopo no qual 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 class 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 usando uma declaração de uso na class Derived:

 using Base::gogo; 

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

Isso é “Por Design”. Na resolução de sobrecarga C ++ para este tipo de método funciona como o seguinte.

  • Começando pelo tipo de referência e indo para o tipo base, encontre o primeiro tipo que tenha um método chamado “gogo”
  • Considerando apenas os methods chamados “gogo” nesse tipo, encontre uma sobrecarga correspondente

Como o Derived não tem uma function correspondente chamada “gogo”, a resolução de sobrecarga falha.

A ocultação de 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 por Derived::func(double) em Derived, chamaríamos a function de class 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/