This lesson is not translated yet – the Russian original is below.
Перегрузка знакома по модулю о функциях: несколько функций с одним именем и разными параметрами. В классах она встречается почти в каждом типе, и правила выбора стоит знать точно – в чужом коде неочевидный выбор перегрузки объясняет добрую половину «почему оно так себя ведёт».
Что входит в сигнатуру
Для перегрузки различают:
- число параметров;
- их типы;
- константность (
const) самого метода; - ссылочность (
&/&&у метода, C++11).
И не различают:
- возвращаемый тип;
- имена параметров;
- значения по умолчанию;
constу параметра, переданного по значению.
int GetValue();
double GetValue(); // ошибка: сигнатуры не различаются
void Foo(int x);
void Foo(const int x); // ошибка: то же самое, const у копии ни на что не влияет
void Bar(int& x);
void Bar(const int& x); // а вот это разные функции
Как выбирается перегрузка
Процесс из трёх шагов. Компилятор:
- Ищет имя. Просматривает области видимости снизу вверх и останавливается на первой, где имя нашлось.
- Собирает кандидатов. Все найденные перегрузки, подходящие по числу аргументов.
- Ранжирует. Для каждого аргумента оценивает, насколько «дорого» преобразование.
Порядок предпочтений от лучшего к худшему:
| Ранг | Что это | Пример |
|---|---|---|
| 1 | Точное совпадение | int → int |
| 2 | Продвижение типа | char → int, float → double |
| 3 | Стандартное преобразование | int → double, Derived* → Base* |
| 4 | Пользовательское преобразование | конструктор или operator T() |
| 5 | Многоточие ... |
последний вариант |
Побеждает кандидат, который не хуже всех остальных по каждому аргументу и лучше хотя бы по одному. Если такого нет – неоднозначность и ошибка компиляции.
void Print(int value);
void Print(double value);
Print(42); // точное совпадение с int
Print(3.14); // точное совпадение с double
Print('a'); // продвижение char → int, побеждает Print(int)
Print(5L); // ошибка: long одинаково далёк от обоих
Шаг 1 важнее, чем кажется: поиск останавливается на первой области, где имя встретилось. Метод наследника скрывает все одноимённые методы базы, а функция в области видимости класса – одноимённые свободные функции. «Почему не выбралась очевидно подходящая перегрузка» чаще всего означает, что до неё просто не дошли: её не было среди кандидатов.
Перегрузка по константности
Самый частый случай в классах: два варианта одного метода, различающиеся только const.
class String {
public:
char& At(size_t idx) { return str_[idx]; }
char At(size_t idx) const { return str_[idx]; }
private:
char* str_;
};
String s("Hello");
char c1 = s.At(0); // неконстантная версия: вернула ссылку, её прочитали
const String cs("Hello");
char c2 = cs.At(0); // константная версия
// cs.At(0) = 'x'; // ошибка: возвращена копия, присваивать некуда
Выбор делает компилятор по константности объекта. Так устроены operator[], at(), front(),
back(), begin() у всех стандартных контейнеров.
Чтобы не дублировать тело, константную версию иногда пишут первой, а неконстантную выражают через неё: return const_cast<char&>(static_cast<const String&>(*this).At(idx));. Для двух строк это лекарство хуже болезни, но в методе на тридцать строк оно окупается. В C++23 для этого появилось «дедуцирующее this», позволяющее написать метод один раз.
Перегрузка и наследование
Как обсуждалось в предыдущем уроке, метод наследника скрывает все одноимённые методы базы:
class Base {
public:
void Handle(int value);
void Handle(const std::string& text);
};
class Derived : public Base {
public:
void Handle(double value);
};
Derived d;
d.Handle("text"); // ошибка: Base::Handle(const string&) скрыт
d.Handle(42); // вызовется Handle(double): int преобразуется в double
Второй вызов особенно неприятен – он компилируется и делает не то. Лечится using:
class Derived : public Base {
public:
using Base::Handle;
void Handle(double value);
};
Перегрузка и преобразования указателей
Приведение Derived* к Base* – стандартное преобразование, и оно участвует в ранжировании:
void Process(Base* b);
void Process(Derived* d);
Derived* d = new Derived;
Process(d); // Process(Derived*): точное совпадение лучше преобразования
Если бы перегрузки для Derived* не было, вызвалась бы версия с Base*. Но выбор происходит на
этапе компиляции по статическому типу выражения:
Base* b = new Derived;
Process(b); // Process(Base*) – хотя объект на самом деле Derived
Перегрузка ничего не знает о том, что лежит по указателю во время работы. Выбор по фактическому типу объекта – совсем другой механизм, и он называется виртуальными методами. О нём – следующий урок.
Отсюда полезное разграничение. Перегрузка – это статический полиморфизм: решение принимается компилятором, стоит ноль во время выполнения, но опирается только на объявленные типы. Виртуальные методы – динамический полиморфизм: решение принимается во время работы по фактическому типу объекта, ценой одного косвенного вызова.