Судячи з формулювання вашого запитання (ви вживали слово "сховатися"), ви вже знаєте, що тут відбувається. Явище називається «приховування імені». Чомусь кожен раз, коли хтось задає питання про те, чому трапляється приховування імені, люди, які відповідають або говорять, що це називається "приховування імені", і пояснюють, як воно працює (що ви, мабуть, вже знаєте), або пояснюють, як його переосмислити (що ви ніколи про це не питали), але, здається, ніхто не піклується про вирішення актуального питання "чому".
Рішення, обґрунтування приховування імені, тобто чому воно насправді було розроблено на C ++, полягає в тому, щоб уникнути певних контрінтуїтивних, непередбачуваних та потенційно небезпечних поведінок, які можуть мати місце, якщо спадкоємному набору перевантажених функцій було дозволено змішуватися з поточним набором перевантаження в даному класі. Ви, мабуть, знаєте, що в C ++ роздільна здатність перевантаження працює, вибираючи найкращу функцію з набору кандидатів. Це робиться шляхом підбору типів аргументів до типів параметрів. Правила відповідності часом можуть бути складними і часто призводять до результатів, які непідготовлений користувач може сприймати як нелогічний. Додавання нових функцій до набору вже існуючих може призвести до досить різкого зрушення результатів роздільної здатності перевантаження.
Наприклад, скажімо, базовий клас B
має функцію-член, foo
який приймає параметр типу void *
, і всі виклики, для foo(NULL)
яких вирішено B::foo(void *)
. Скажімо, немає імені, яке ховається, і це B::foo(void *)
видно в багатьох різних класах, що походять від B
. Однак, скажімо, в якомусь [непрямому, віддаленому] нащадку D
класу B
визначена функція foo(int)
. Тепер без приховування імен D
є foo(void *)
і foo(int)
видиме, і участь у вирішенні перевантажень. До якої функції будуть foo(NULL)
вирішуватися дзвінки , якщо вони здійснюються через тип об'єкта D
? Вони вирішуватимуться D::foo(int)
, оскільки int
це краща відповідність інтегральному нулю (тобтоNULL
), ніж будь-який тип вказівника. Отже, протягом всієї ієрархії заклики foo(NULL)
вирішувати одну функцію, тоді як в D
(і під) вони раптово вирішуються на іншу.
Інший приклад наведено в «Дизайн та еволюція C ++» , стор. 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*)
}
Без цього правила стан b було б частково оновлене, що призвело б до скорочення.
Така поведінка вважалася небажаною, коли мова була розроблена. В якості кращого підходу було вирішено дотримуватися специфікації "приховування імені", тобто кожен клас починається з "чистого аркуша" стосовно кожного названого методу. Для того, щоб змінити таку поведінку, від користувача потрібна чітка дія: спочатку передекларація успадкованих методів (методів) (наразі застаріла), тепер явне використання використання-декларації.
Як ви правильно помітили у своєму початковому дописі (я маю на увазі зауваження "Не поліморфне"), ця поведінка може розглядатися як порушення відносин IS-A між класами. Це правда, але, мабуть, тоді було вирішено, що врешті-решт приховування імені виявиться меншим злом.