Rozważ kod:
#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);
}
Mam ten błąd:
> g ++ -pedantic -Os test.cpp -o test test.cpp: W funkcji `int main () ': test.cpp: 31: błąd: brak pasującej funkcji dla wywołania `Derived :: gogo (int) ' test.cpp: 21: uwaga: kandydatami są: virtual void Derived :: gogo (int *) test.cpp: 33: 2: ostrzeżenie: brak nowej linii na końcu pliku > Kod wyjścia: 1
Tutaj funkcja klasy pochodnej przyćmiewa wszystkie funkcje o tej samej nazwie (nie sygnaturze) w klasie bazowej. Jakoś to zachowanie C ++ nie wygląda OK. Nie polimorficzny.
c++
polymorphism
overriding
Aman Aggarwal
źródło
źródło
obj.Base::gogo(7);
nadal działa, wywołując funkcję ukrytą.Odpowiedzi:
Sądząc po sformułowaniu pytania (użyłeś słowa „ukryj”), już wiesz, co się tutaj dzieje. Zjawisko to nazywa się „ukrywaniem nazwy”. Z jakiegoś powodu za każdym razem, gdy ktoś zadaje pytanie o to, dlaczego dzieje się ukrywanie imienia, ludzie, którzy odpowiadają, albo mówią, że to się nazywa „ukrywanie imienia” i wyjaśniają, jak to działa (co prawdopodobnie już wiesz), lub wyjaśniają, jak to zmienić (co ty nigdy o to nie pytano), ale wydaje się, że nikomu nie zależy na odpowiedzi na pytanie „dlaczego”.
Decyzja, uzasadnienie ukrywania nazwy, tj. Dlaczego tak naprawdę została zaprojektowana w C ++, polega na uniknięciu pewnych sprzecznych z intuicją, nieprzewidzianych i potencjalnie niebezpiecznych zachowań, które mogłyby mieć miejsce, gdyby odziedziczony zestaw przeciążonych funkcji mógł mieszać się z bieżącym zestawem przeciążenia w danej klasie. Prawdopodobnie wiesz, że w C ++ rozwiązywanie przeciążeń działa poprzez wybranie najlepszej funkcji z zestawu kandydatów. Odbywa się to poprzez dopasowanie typów argumentów do typów parametrów. Reguły dopasowania mogą być czasem skomplikowane i często prowadzą do wyników, które mogą być postrzegane jako nielogiczne przez nieprzygotowanego użytkownika. Dodanie nowych funkcji do zestawu wcześniej istniejących może spowodować dość drastyczne przesunięcie wyników rozwiązywania przeciążenia.
Załóżmy na przykład, że klasa podstawowa
B
ma funkcjęfoo
składowąvoid *
, która przyjmuje parametr typu , a wszystkie wywołaniafoo(NULL)
są rozpoznawaneB::foo(void *)
. Powiedzmy, że nie kryje się żadna nazwa iB::foo(void *)
jest to widoczne w wielu różnych klasach zstępującychB
. Powiedzmy jednak, że u jakiegoś [pośredniego, zdalnego] potomkaD
klasy zdefiniowana jestB
funkcjafoo(int)
. Teraz bez nazwy ukryciuD
ma zarównofoo(void *)
ifoo(int)
widoczne i uczestnicząc w rozdzielczości przeciążenia. Jaką funkcję będąfoo(NULL)
rozstrzygać wywołania , jeśli zostaną wykonane za pomocą obiektu typuD
? Rozwiążą sięD::foo(int)
, ponieważint
jest to lepsze dopasowanie dla całki zerowej (tjNULL
) niż jakikolwiek typ wskaźnika. Tak więc w całej hierarchii wezwania dofoo(NULL)
rozwiązania jednej funkcji, podczas gdy wD
(i poniżej) nagle przechodzą do innej funkcji.Inny przykład podano w The Design and Evolution of C ++ , strona 77:
Bez tej reguły stan b zostałby częściowo zaktualizowany, co doprowadziłoby do krojenia.
To zachowanie zostało uznane za niepożądane, gdy język został zaprojektowany. Jako lepsze podejście, postanowiono zastosować się do specyfikacji „ukrywania nazw”, co oznacza, że każda klasa zaczyna się od „czystego arkusza” w odniesieniu do każdej deklarowanej nazwy metody. Aby zastąpić to zachowanie, wymagane jest wyraźne działanie od użytkownika: pierwotnie ponowne zadeklarowanie odziedziczonych metod (obecnie nieaktualne), teraz jawne użycie deklaracji użycia.
Jak prawidłowo zauważyłeś w swoim oryginalnym poście (odnoszę się do uwagi „Nie polimorficzny”), takie zachowanie może być postrzegane jako naruszenie relacji IS-A między klasami. To prawda, ale najwyraźniej wtedy zdecydowano, że ukrywanie nazwy okaże się mniejszym złem.
źródło
nullptr
się twojemu przykładowi, mówiąc „jeśli chcesz wywołaćvoid*
wersję, powinieneś użyć typu wskaźnika”. Czy jest inny przykład, w którym może to być złe?d->foo()
nie dostaniesz „Is-aBase
”, alestatic_cast<Base*>(d)->foo()
będzie , włączając dynamiczną wysyłkę.Reguły rozpoznawania nazw mówią, że wyszukiwanie nazw kończy się w pierwszym zakresie, w którym znaleziono pasującą nazwę. W tym momencie uruchamiane są reguły rozwiązywania problemu przeciążenia, aby znaleźć najlepsze dopasowanie dostępnych funkcji.
W tym przypadku
gogo(int*)
znajduje się (sam) w zakresie klasy Derived, a ponieważ nie ma standardowej konwersji z int na int *, wyszukiwanie kończy się niepowodzeniem.Rozwiązaniem jest wprowadzenie deklaracji bazowych za pomocą deklaracji using w klasie Derived:
... pozwoliłby regułom wyszukiwania nazw znaleźć wszystkich kandydatów, a zatem rozwiązanie problemu przeciążenia przebiegałoby zgodnie z oczekiwaniami.
źródło
To jest „Z założenia”. W C ++ rozdzielczość przeciążenia dla tego typu metody działa jak poniżej.
Ponieważ Derived nie ma pasującej funkcji o nazwie „gogo”, rozwiązywanie problemów z przeciążeniem kończy się niepowodzeniem.
źródło
Ukrywanie nazw ma sens, ponieważ zapobiega dwuznacznościom w rozpoznawaniu nazw.
Rozważ ten kod:
Gdyby
Base::func(float)
nie było to ukryteDerived::func(double)
w Derived, wywołalibyśmy funkcję klasy bazowej podczas wywoływaniadobj.func(0.f)
, nawet jeśli liczba zmiennoprzecinkowa może zostać podwojona.Odniesienie: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/
źródło