Одно имя, много версий
Часто нужна одна и та же операция для разных видов данных: вывести int, вывести string, вывести double. В некоторых языках вы бы придумали printInt, printString, printDouble. C++ позволяет дать всем им одно имя и различает их по параметрам. Это и есть перегрузка функций.
Правило простое: несколько функций могут иметь общее имя, пока их списки параметров различаются — по числу параметров, их типам или порядку. Компилятор смотрит на аргументы в каждой точке вызова и сам подбирает подходящую версию.
Три функции, одно имя. Каждый вызов попадает в ту версию, тип параметра которой подходит к аргументу. Именно поэтому std::cout << x одинаково работает для int, double и string — operator<< перегружен множество раз.
Что считается отдельной перегрузкой
Перегрузка различается только по списку параметров. Вы можете менять:
int area(int side); // 1 параметр
int area(int width, int height); // 2 параметра -> другая
double area(double r); // другой тип -> другая
void log(string msg, int level); // порядок важен...
void log(int level, string msg); // ...поэтому это тоже другая
Каждая из них — допустимая, отдельная перегрузка. Компилятор собирает множество кандидатов из всех функций с именем area, а затем сопоставляет по числу и типу аргументов.
Одного типа возвращаемого значения недостаточно
Вот ловушка, на которой спотыкаются почти все: нельзя перегружать по типу возвращаемого значения. Возвращаемое значение не участвует в выборе перегрузки, потому что компилятор решает, какую функцию вызвать, по аргументам — задолго до того, как взглянет на то, что возвращается.
int convert(double x); // OK
double convert(double x); // ОШИБКА: переопределение - различается только тип возврата
Это не скомпилируется. Если списки параметров одинаковы, два объявления — это одна и та же функция с точки зрения перегрузки, и вы получаете ошибку переопределения. Чтобы ветвиться по типу результата, измените параметр (или используйте шаблоны / приведение типов с static_cast в точке вызова).
Как разрешение перегрузки выбирает победителя
Когда вы делаете вызов, компилятор ранжирует все подходящие перегрузки и выбирает наилучшее совпадение. Грубо говоря, он предпочитает в таком порядке:
- Точное совпадение (преобразование не нужно).
- Продвижение (например,
charилиshort->int,float->double). - Стандартное преобразование (например,
int->double,double->int, указатель на базовый класс).
Если ровно одна перегрузка строго лучше всех остальных, побеждает она. Посмотрите, как точное совпадение обходит преобразование:
'A' — это char, но продвижение к int ранжируется выше преобразования к double, поэтому вызывается перегрузка int. Эти правила ранжирования — причина того, что разрешение перегрузки обычно «делает то, что нужно» — и время от времени вас удивляет.
Ловушка неоднозначности
Если две перегрузки одинаково хороши — ни одна не строго лучше — компилятор отказывается гадать и сообщает о неоднозначном вызове. Хрестоматийный случай — две перегрузки, каждой из которых нужно преобразование одного ранга:
void f(int x);
void f(double x);
f(0L); // ОШИБКА: неоднозначно - long -> int и long -> double — преобразования одного ранга
Ни int, ни double не являются точным совпадением для long, и оба преобразования стоят на одном ранге, поэтому вызов неоднозначен. У вас есть два чистых решения:
Связанный сюрприз: передача строкового литерала. void g(const string&) и void g(bool) обе будут претендовать на g("hi"), и bool может победить, потому что const char* преобразуется в bool (не null -> true) за меньшее число шагов, чем построение std::string. Если вы когда-нибудь увидите, что строковый литерал загадочно вызывает вашу перегрузку bool, вот причина — добавьте перегрузку const char* или const string&, чтобы она забрала точное совпадение.
Перегрузка и аргументы по умолчанию плохо уживаются
Аргументы по умолчанию не заменяют перегрузку, а сочетание этих двух создаёт неоднозначность. Каждый из них может ответить на один и тот же вызов, поэтому компилятор не может выбрать:
void connect(string host, int port = 8080); // можно вызвать с 1 аргументом
void connect(string host); // тоже вызывается с 1 аргументом
connect("localhost"); // ОШИБКА: неоднозначно - обе подходят к одному аргументу
Выбирайте один подход на каждую форму вызова. Используйте аргументы по умолчанию, когда поведение одинаково и вам просто нужны необязательные параметры; используйте перегрузку, когда разные списки аргументов должны выполнять действительно разный код. Смешивать их так, чтобы две сигнатуры сталкивались при одном и том же числе аргументов, — это гарантированная ошибка неоднозначности.
Ещё одно различие, которое стоит чётко зафиксировать: перегрузка — это не переопределение. Перегрузка разрешается во время компиляции среди функций в одной области видимости с одним именем, но разными параметрами. Переопределение заменяет virtual-функцию в производном классе во время выполнения и требует той же сигнатуры — тема для виртуальных функций позже.
Далее: лямбды
Перегрузка даёт одному имени несколько типизированных реализаций, выбираемых во время компиляции. Однако иногда вам вовсе не нужна именованная функция — нужна крошечная одноразовая функция, определённая прямо там, где вы её используете, часто чтобы передать в алгоритм вроде sort. Именно это и есть лямбды: анонимные функции, которые можно написать встроенно, захватить ими окружающие переменные и передать в одном выражении. Далее мы посмотрим, как их писать и когда они выигрывают у полноценной именованной функции.
Часто задаваемые вопросы
Что такое перегрузка функций в C++?
Перегрузка функций позволяет определять несколько функций с одним именем, пока их списки параметров различаются (по числу, типу или порядку). Компилятор выбирает, какую вызвать, исходя из переданных аргументов, поэтому print(42) и print("hi") могут вызывать две разные функции print.
Могут ли две функции C++ различаться только типом возвращаемого значения?
Нет. Перегрузки должны различаться в своём списке параметров. int f(int) и double f(int) — это ошибка компиляции: тип возвращаемого значения не входит в сигнатуру, используемую при разрешении перегрузки, потому что компилятор выбирает перегрузку по аргументам в точке вызова, ещё до того, как возвращаемое значение вообще используется.
Из-за чего возникает ошибка «неоднозначный вызов» с перегруженными функциями?
Она возникает, когда две перегрузки одинаково хорошо подходят и компилятор не может предпочесть одну. Классический случай — f(int) и f(double), вызванные как f(0L) (тип long), где обе требуют преобразования одного ранга. Исправить можно, добавив перегрузку с точным совпадением или приведя аргумент к нужному типу.