В производном классе добавили перегруженную функцию с тем же именем, что в базовом. Почему обычный вызов базовой перегрузки перестаёт находиться?
Объявление функции в производном классе скрывает все одноимённые объявления базового класса при обычном поиске имени, даже если их параметры различаются. Поэтому компилятор рассматривает сначала область видимости производного класса и не добавляет базовые перегрузки автоматически.
Чтобы открыть базовые перегрузки, в производном классе используют using-декларацию вида using Base::name;. Это влияет на поиск имени, но не превращает функции в переопределения и не меняет правила виртуального вызова.
Правило основано на общей модели областей видимости C++: объявление имени во внутренней области скрывает одноимённые объявления из внешней области. Производный класс образует отдельную область имён своих членов, поэтому его объявление не сливается автоматически с одноимёнными членами базового класса.
Такой подход делает поиск имён предсказуемым и позволяет производному классу намеренно заменить или скрыть часть интерфейса базового. Если требуется сохранить доступ к базовым перегрузкам, это явно указывается через using.
Пусть базовый класс предоставляет несколько перегруженных функций, а производный добавляет функцию с тем же именем. Вызов через объект производного класса может перестать находить подходящую базовую перегрузку, хотя она существует и по типам параметров подходит.
Последствие — ошибка компиляции или выбор неожиданной функции, если производственная перегрузка всё же подходит после стандартных преобразований. Особенно опасна такая проблема при расширении интерфейса библиотеки: добавление метода в производный класс может сломать ранее корректные вызовы.
Поиск неквалифицированного имени выполняется до выбора перегрузки. Сначала компилятор ищет имя в области производного класса; если оно найдено, поиск в базовых классах для этого имени обычно прекращается. Только затем среди найденных объявлений выполняется разрешение перегрузки.
Минимальный пример:
Вызов value.process(1) не рассматривает Base::process(int), потому что имя process уже найдено в Derived. Наличие функции Derived::process(std::string) не означает, что компилятор объединит все одноимённые функции в общий набор.
Исправление — явно внести базовое имя в область производного класса:
using Base::process открывает базовые объявления для поиска перегрузки. Это не копирование функций и не создание новых обёрток; при вызове сохраняется принадлежность функции своему классу.
Есть и альтернативы. Явная квалификация вроде обращения к Base::process однозначна, но привязывает вызывающий код к базовому классу и обычно неудобна для общего интерфейса. Переименование функции устраняет скрытие, однако может быть невозможно при сохранении совместимого API.
Скрытие имени также не следует путать с переопределением виртуальной функции. Для переопределения нужны виртуальная функция в базовом классе и совместимая сигнатура; само совпадение имени ещё не означает переопределение. Ключевое слово override помогает обнаружить ошибку в сигнатуре, но не отменяет правил поиска имени для перегруженных функций.
В базовом классе обработчика есть перегрузки handle(Request&) и handle(Error&). Производный обработчик добавляет handle(const Config&) для специальной конфигурации. После этого код, работающий с объектом производного типа, перестаёт находить обработку Request&.
Рассматривались три варианта:
using BaseHandler::handle — сохраняет все существующие перегрузки и позволяет участвовать новой функции в общем наборе.Выбран третий вариант, потому что производный класс действительно расширяет, а не заменяет интерфейс обработчика. В результате старые вызовы снова разрешаются, а вызов с Config направляется в новую функцию; при этом виртуальная диспетчеризация, если она используется, продолжает работать по своим отдельным правилам.
Скрывает ли производная функция только перегрузку с такой же сигнатурой?
Нет. При обычном поиске скрывается имя целиком: производственное объявление process скрывает базовые process с любыми параметрами. Поэтому сравнение сигнатур происходит слишком поздно — базовые объявления уже не попали в набор кандидатов.
Исключение на практике создаётся явным восстановлением имени через using или обращением с квалификацией. Простое совпадение типов параметров не восстанавливает базовую функцию автоматически.
Что изменится, если базовая функция виртуальная, а производная имеет другую сигнатуру?
Другая сигнатура сама по себе не создаёт переопределение. Производная функция может скрыть базовое имя для статического поиска, но при вызове через указатель или ссылку на базовый класс будет применяться интерфейс базового класса и его правила виртуального вызова.
Для намеренного переопределения следует использовать override: компилятор проверит совместимость сигнатуры. Если функция должна одновременно расширять набор перегрузок, using может понадобиться дополнительно для вызовов через объект производного типа.
Работает ли using только для публичных базовых функций?
using подчиняется правилам доступа и видимости. Он может сделать имя доступным в области производного класса, но не позволяет обойти private-доступ базового класса; попытка использовать недоступный член приводит к ошибке.
Кроме того, открытие имени не меняет уровень доступа самого базового объявления произвольным образом во всех контекстах. На практике нужно отдельно проверять доступность конкретной функции и учитывать, что у базового класса могут быть одноимённые private, protected и public члены.