Menu

Перегрузка функций в C++: одно имя, разные параметры

Перегрузка функций в C++ позволяет нескольким функциям иметь одно имя, пока их списки параметров различаются. Узнайте, как разрешение перегрузки выбирает подходящий вариант, почему один лишь тип возвращаемого значения не считается и каких ловушек неоднозначности и аргументов по умолчанию следует избегать.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Одно имя, много версий

Часто нужна одна и та же операция для разных видов данных: вывести 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 в точке вызова).

Как разрешение перегрузки выбирает победителя

Когда вы делаете вызов, компилятор ранжирует все подходящие перегрузки и выбирает наилучшее совпадение. Грубо говоря, он предпочитает в таком порядке:

  1. Точное совпадение (преобразование не нужно).
  2. Продвижение (например, char или short -> int, float -> double).
  3. Стандартное преобразование (например, 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), где обе требуют преобразования одного ранга. Исправить можно, добавив перегрузку с точным совпадением или приведя аргумент к нужному типу.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ