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łania foo(NULL)
są rozpoznawane B::foo(void *)
. Powiedzmy, że nie kryje się żadna nazwa i B::foo(void *)
jest to widoczne w wielu różnych klasach zstępujących B
. Powiedzmy jednak, że u jakiegoś [pośredniego, zdalnego] potomka D
klasy zdefiniowana jest B
funkcja foo(int)
. Teraz bez nazwy ukryciu D
ma zarówno foo(void *)
i foo(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 typu D
? 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 do foo(NULL)
rozwiązania jednej funkcji, podczas gdy w D
(i poniżej) nagle przechodzą do innej funkcji.
Inny przykład podano w The Design and Evolution of C ++ , strona 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*)
}
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.