Ответ 1
Судя по формулировке вашего вопроса (вы использовали слово "скрыть"), вы уже знаете, что здесь происходит. Это явление называется "сокрытие имени". По какой-то причине каждый раз, когда кто-то задает вопрос о том, почему происходит скрытие имени, люди, которые отвечают, говорят, что это называется "скрытие имени" и объясняют, как это работает (что вы, вероятно, уже знаете), или объясните, как его переопределить (что вы никогда не спрашивали), но никто, кажется, не заботится об актуальном вопросе "почему".
Решение, обоснование скрытия имени, то есть, почему оно фактически было сконструировано в 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)
вызовы 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 между классами. Это правда, но, по-видимому, тогда было решено, что в конечном итоге сокрытие имен окажется меньшим злом.