8.10 – Различение перегруженных функций
В предыдущем уроке (8.9 – Введение в перегрузку функций) мы представили концепцию перегрузки функций, которая позволяет нам создавать несколько функций с одинаковыми именами, если все эти функции имеют разные параметры (или эти функции могут быть различены как-то иначе).
В этом уроке мы поговорим о том, как различаются перегруженные функции. Перегруженные функции, которые не различаются должным образом, приведут к тому, что компилятор выдаст ошибку компиляции.
Как различаются перегруженные функции
Самый простой способ дифференцировать перегрузку функций – убедиться, что каждая перегруженная функция имеет отличающийся набор (количество и/или тип) параметров.
Свойство функции | Используется для перегрузки | Примечания |
---|---|---|
Количество параметров | Да | |
Тип параметров | Да | Не включает себя использование typedef , псевдонимы типов и квалификатор const для значений параметров. Включает в себя многоточия. |
Тип возвращаемого значения | Нет |
Обратите внимание, что тип возвращаемого значения функции не используется для различения перегруженных функций. Мы поговорим об этом чуть позже.
Для продвинутых читателей
Для функций-членов также рассматриваются дополнительные квалификаторы уровня функции:
Квалификатор уровня функции | Используется для перегрузки |
---|---|
const или volatile | Да |
ref-квалификаторы | Да |
Например, константную функция-член может отличить от идентичной в остальном неконстантной функции-члена (даже если они имеют один и тот же набор параметров).
Перегрузка по количеству параметров
Перегруженные функции различаются до тех пор, пока каждая перегруженная функция имеет разное количество параметров. Например:
int add(int x, int y)
{
return x + y;
}
int add(int x, int y, int z)
{
return x + y + z;
}
Компилятор может легко сказать, что вызов функции с двумя параметрами int
должен идти на add(int, int)
, а вызов функции с тремя параметрами int
должен идти на add(int, int, int)
.
Перегрузка по типу параметров
Функции также можно различать, если различаются наборы типов параметров каждой перегруженной функции. Например, различаются все следующие перегрузки:
int add(int x, int y); // целочисленная версия
double add(double x, double y); // версия с плавающей запятой
double add(int x, double y); // смешанная версия
double add(double x, int y); // смешанная версия
Поскольку псевдонимы типов (или определения typedef
) не являются отдельными типами, перегруженные функции, использующие псевдонимы типов, не отличаются от перегрузок, использующих исходные типы. Например, все следующие перегрузки не различаются (и приведут к ошибке компиляции):
typedef int height_t; // typedef
using age_t = int; // псевдоним типа
void print(int value);
void print(age_t value); // не отличается от print(int)
void print(height_t value); // не отличается от print(int)
Для параметров, передаваемых по значению, квалификатор const
также не учитывается. Поэтому следующие функции не считаются разными:
void print(int);
void print(const int); // не отличается от print(int)
Для продвинутых читателей
Мы еще не рассмотрели многоточие, но параметры многоточия считаются уникальным типом параметров:
void foo(int x, int y);
void foo(int x, ...); // отличается от foo(int, int)
Тип возвращаемого значения функции не учитывается в уникальности
Тип возвращаемого значения функции не учитывается при различении перегруженных функций.
Рассмотрим случай, когда вы хотите написать функцию, возвращающую случайное число, но вам нужна одна версия, которая вернет int
, и другая версия, которая вернет double
. У вас может возникнуть соблазн сделать так:
int getRandomValue();
double getRandomValue();
В Visual Studio 2019 это приводит к следующей ошибке компилятора:
error C2556: 'double getRandomValue(void)': overloaded function differs only by return type from 'int getRandomValue(void)'
Эту ошибку можно понять. Если бы вы были компилятором и увидели следующую инструкцию:
getRandomValue();
Какую из двух перегруженных функций вы бы вызвали? Не понятно.
В качестве отступления...
Это был преднамеренный выбор, поскольку он обеспечивает возможность определения поведения вызова функции независимо от остальной части выражения, что значительно упрощает понимание сложных выражений. Другими словами, мы всегда можем определить, какая версия функции будет вызвана исключительно на основе аргументов в вызове функции. Если бы для различения использовались возвращаемые значения, у нас не было бы простого синтаксического способа определить, какая перегруженная функция вызывается – нам также нужно было бы понять, как используется возвращаемое значение, что требует гораздо большего анализа.
Лучший способ решить эту проблему – дать функциям разные имена:
int getRandomInt();
double getRandomDouble();
Альтернативный способ – сделать так, чтобы функции возвращали void
, а возвращаемое значение передавалось обратно вызывающему в качестве выходного параметра (что такое выходной параметр, рассматривается в уроке «11.3 – Передача аргументов по ссылке»).
void getRandomValue(int &out);
void getRandomValue(double &out);
Поскольку эти функции имеют разные параметры, они считаются уникальными. Однако у этого есть свои недостатки. Во-первых, синтаксис неудобен, и вы не можете направить вывод этой функции непосредственно на ввод другой. Рассмотрим:
// метод 1: getRandomInt() возвращает int
printValue(getRandomInt()); // легко
// метод 2: getRandomValue() имеет выходной параметр int
int temp{}; // теперь нам нужна временная переменная
getRandomValue(temp); // чтобы вызвать здесь getRandomValue(int)
printValue(temp); // это должна быть отдельная строка, поскольку
// getRandomValue() возвращает void
Кроме того, тип переданного аргумента должен точно соответствовать типу параметра. По этим причинам мы не рекомендуем этот метод.
Изменение имени
В качестве отступления...
Когда компилятор компилирует функцию, он выполняет изменение имени, что означает, что скомпилированное имя функции изменяется на основе различных критериев, таких как количество и тип параметров, поэтому компоновщик получает для работы уникальные имена.
Например, функция с прототипом int fcn()
может компилироваться с именем __fcn_v,
тогда как int fcn(int)
может компилироваться с именем __fcn_i
. Таким образом, хотя в исходном коде две перегруженные функции имеют одинаковые имена, в скомпилированном коде имена на самом деле уникальны.
Стандарта того, как следует изменять имена, не существует; поэтому разные компиляторы будут создавать разные искаженные имена.