Переопределение перегруженной функции базового класса в C++
Я столкнулся с проблемой, когда после того, как мой класс переопределил функцию базового класса, все перегруженные версии этой функции оказались скрыты. Это предусмотрено языком или я просто делаю что-то неправильно?
Например:
class foo
{
public:
foo(void);
~foo(void);
virtual void a(int);
virtual void a(double);
};
class bar : public foo
{
public:
bar(void);
~bar(void);
void a(int);
};
Попытка вызвать функцию a(double)
из класса bar
приводит к ошибке компиляции, указывая на то, что функция a(double)
не найдена:
int main()
{
double i = 0.0;
bar b;
b.a(i); // Ошибка компиляции
}
Как я могу решить эту проблему?
3 ответ(ов)
В классе bar
добавьте
using foo::a;
Это распространенная "цепочка" в C++. Когда в области видимости класса находится совпадение имени, компилятор не ищет дальше по дереву наследования для перегрузок. Указывая директиву using
, вы переносите все перегрузки a
из foo
в область видимости bar
. После этого все работает должным образом.
Учтите, что если в существующем коде используется класс foo
, его поведение может измениться из-за добавленных перегрузок. Либо новые перегрузки могут привести к неоднозначности, и код не скомпилируется. Об этом упомянуто в ответе Джеймса Хопкинса.
Это то, как язык работал ранее. До введения ключевого слова using, если вы переопределяли одну из перегруженных функций, вам приходилось перегружать все:
class bar : public foo
{
public:
bar(void);
~bar(void);
void a(int);
void a(double d) { foo::a(d); } // добавьте это
}
Это вызывало достаточное раздражение у разработчиков, и комитет языка добавил возможность using, но некоторые старые привычки трудно искоренить; и завсегдатаи† имеют довольно убедительные аргументы.
Как указывает Джеймс Хопкинс, добавляя using, программист выражает намерение, что производный класс будет без предупреждений добавлять любые будущие переопределения foo::a() в свой список допустимых сигнатур.
Вот пример того, о чем он говорит:
#include <iostream>
class Base {
public:
virtual void f(double){ std::cout << "Base::Double!" << std::endl; }
// virtual void f(int) { std::cout << "Base::Int!" << std::endl; } // (1)
virtual ~Base() {}
};
class Derived : public Base {
public:
// using Base::f; // (2)
void f(double) { std::cout << "Derived::Double!" << std::endl; }
};
int main(int, char **) {
Derived d;
d.f(21);
return 0;
}
Вывод будет "Derived::Double!", потому что компилятор преобразует целочисленный аргумент в double. Компилятор g++ 4.0.1 с ключом -Wall не выдаст предупреждения о том, что произошло это преобразование.
Рассмотрим (1) — раскомментируйте строку, чтобы смоделировать будущее изменение в Base, добавляя метод Basef(int). Код скомпилируется снова без предупреждений, даже с включенным -Wall, и вывод останется "DerivedDouble!".
Теперь раскомментируйте (2), чтобы смоделировать решение программиста Derived включить все сигнатуры Basef. Код компилируется (без предупреждений), но вывод теперь "BaseInt!".
—
† Я не могу найти подходящее английское слово для «тех, кто имеет привычку», а «зависимый» слишком сильно.
Это сделано намеренно. Разрешение перегрузки ограничивается единственным контекстом. Это предотвращает некоторые неприятные ситуации, когда валидный код меняет свое значение при добавлении новых функций в базовый класс или в область видимости пространства имен.
Когда использовать виртуальные деструкторы?
Какова разница между публичным, приватным и защищённым наследованием?
Является ли List<Собака> подклассом List<Животное>? Почему дженерики в Java не являются неявно полиморфными?
Как вызвать функцию родительского класса из функции производного класса?
Работает ли оператор delete с указателями на базовый класс?