8.11 – Разрешение перегрузки функций и неоднозначные совпадения
В предыдущем уроке («8.10 – Различение перегруженных функций») мы обсудили, какие атрибуты функции используются для различения перегруженных функций друг от друга. Если перегруженная функция не отличается должным образом от других перегрузок с тем же именем, компилятор выдаст ошибку компиляции.
Однако наличие набора различающихся перегруженных функций – это только половина картины. Когда выполняется какой-либо вызов функции, компилятор также должен гарантировать, что можно найти соответствующее объявление функции.
В случае неперегруженных функций (функций с уникальными именами) существует только одна функция, которая потенциально может соответствовать вызову функции. Эта функция либо соответствует (или может быть выполнено соответствие после применения преобразования типов), либо нет (и возникает ошибка компиляции). С перегруженными функциями может быть много функций, которые потенциально могут соответствовать вызову функции. Поскольку вызов функции может разрешить только одну из них, компилятор должен определить, какая перегруженная функция лучше всего подходит. Процесс сопоставления вызовов функций с конкретной перегруженной функцией называется разрешением перегрузки.
В простых случаях, когда тип аргументов функции и тип параметров функции точно совпадают, это (обычно) просто:
#include <iostream>
void print(int x)
{
std::cout << x;
}
void print(double d)
{
std::cout << d;
}
int main()
{
print(5); // 5 - это int, поэтому это соответствует print(int)
print(6.7); // 6.7 - это double, поэтому это соответствует print(double)
return 0;
}
Но что происходит в случаях, когда типы аргументов в вызове функции не совсем соответствуют типам параметров ни в одной из перегруженных функций? Например:
#include <iostream>
void print(int x)
{
std::cout << x;
}
void print(double d)
{
std::cout << d;
}
int main()
{
print('a'); // char не совпадает с int или double
print(5l); // long не совпадает с int или double
return 0;
}
Тот факт, что здесь нет точного совпадения, не означает, что соответствие не может быть найдено – в конце концов, тип char
или long
можно неявно преобразовать в int
или double
. Но какое преобразование лучше всего выполнить в каждом конкретном случае?
В этом уроке мы исследуем, как компилятор сопоставляет заданный вызов функции с конкретной перегруженной функцией.
Разрешение вызовов перегруженных функций
Когда выполняется вызов перегруженной функции, компилятор выполняет последовательность правил, чтобы определить, какая из перегруженных функций (если она есть) лучше всего подходит.
На каждом шаге в вызове функции компилятор применяет к аргументу(ам) кучу различных преобразований типов. Для каждого примененного преобразования компилятор проверяет, соответствует ли какая-либо из перегруженных функций. После того, как все преобразования различных типов были применены и проверены на совпадения, этап завершен. Результатом будет один из трех возможных исходов:
- Подходящих функций не найдено. Компилятор переходит к следующему шагу в последовательности.
- Обнаружена единственная соответствующая функция. Эта функция считается наиболее подходящей. Теперь процесс сопоставления завершен, и последующие шаги не выполняются.
- Найдено более одной соответствующей функции. Компилятор выдаст ошибку компиляции о неоднозначном совпадении. Об этом случае поговорим чуть позже.
Если компилятор достигает конца всей последовательности, не найдя совпадения, он сгенерирует ошибку компиляции, что для вызова функции не может быть найдена соответствующая перегруженная функция.
Последовательность сопоставления аргументов
Шаг 1) Компилятор пытается найти точное совпадение. Это происходит в два этапа. Во-первых, компилятор смотрит, существует ли перегруженная функция, в которой тип аргументов в вызове функции точно соответствует типу параметров в перегруженных функциях. Например:
void print(int)
{
}
void print(double)
{
}
int main()
{
print(0); // точное совпадение с print(int)
print(3.4); // точное совпадение с print(double)
return 0;
}
Поскольку 0 в вызове функции print(0)
является int
, компилятор будет проверять, была ли объявлена перегрузка print(int)
. Поскольку это так, компилятор определяет, что print(int)
является точным совпадением.
Во-вторых, компилятор применит ряд тривиальных преобразований к аргументам в вызове функции. Тривиальные преобразования – это набор определенных правил преобразования, которые изменяют типы (без изменения значения) с целью поиска совпадения. Например, неконстантный тип можно тривиально преобразовать в константный тип:
void print(const int)
{
}
void print(double)
{
}
int main()
{
int x { 0 };
print(x); // x тривиально конвертируется в const int
return 0;
}
В приведенном выше примере мы вызвали print(x)
, где x
– это число int
. Компилятор тривиально преобразует x
из int
в const int
, который затем соответствует print(const int)
.
Для продвинутых читателей
Преобразование не ссылочного типа в ссылочный тип (или наоборот) также является тривиальным преобразованием).
Совпадения, полученные с помощью тривиальных преобразований, считаются точными совпадениями.
Шаг 2) Если точное совпадение не найдено, компилятор пытается найти совпадение, применяя к аргументу(ам) числовое продвижение. В уроке «8.2 – Продвижение целочисленных типов и типов с плавающей запятой» мы рассмотрели, как некоторые узкие целочисленные типы и типы с плавающей запятой могут автоматически преобразовываться в более широкие типы, такие как int
или double
. Если после числового продвижения совпадение найдено, вызов функции разрешен.
Например:
void print(int)
{
}
void print(double)
{
}
int main()
{
print('a'); // расширяющее преобразование для соответствия print(int)
print(true); // расширяющее преобразование для соответствия print(int)
print(4.5f); // расширяющее преобразование для соответствия print(double)
return 0;
}
Для print('a')
, поскольку точное совпадение для print(char)
не может быть найдено на предыдущем шаге, компилятор преобразует char
'a' в int
и ищет совпадение. Это соответствует print(int)
, поэтому вызов функции разрешается как print(int)
.
Шаг 3) Если совпадение не найдено с помощью числового продвижения, компилятор пытается найти совпадение, применяя к аргументам числовые преобразования (8.3 – Числовые преобразования).
Например:
#include <string> // для std::string
void print(double)
{
}
void print(std::string)
{
}
int main()
{
print('a'); // 'a' преобразовано для соответствия с print(double)
return 0;
}
В этом случае, поскольку нет print(char)
(точное совпадение) и print(int)
(совпадение при числовом продвижении), 'a' численно преобразуется в double
и сопоставляется с print(double)
.
Ключевые выводы
Совпадения, полученные с помощью числовых продвижений (расширяющих преобразований), имеют приоритет над любыми совпадениями, полученными с помощью числовых преобразований.
Шаг 4) Если совпадение не найдено с помощью числового преобразования, компилятор пытается найти совпадение с помощью любых пользовательских преобразований. Хотя мы еще не рассмотрели пользовательские преобразования, некоторые типы (например, классы) могут определять преобразования в другие типы, которые могут быть вызваны неявно. Вот пример, чтобы проиллюстрировать суть:
// Мы еще не рассмотрели классы, поэтому не волнуйтесь, если это вам пока непонятно
class X
{
public:
operator int() { return 0; } // пользовательское преобразование из X в int
};
void print(int)
{
}
void print(double)
{
}
int main()
{
X x;
print(x); //X преобразуется в int
return 0;
}
В этом примере компилятор сначала проверит, существует ли точное совпадение с print(X)
. Мы не определили такой функции. Затем компилятор проверит, можно ли применить к x
числовое продвижение, чего он не может. Затем компилятор проверит, можно ли применить к x
числовое преобразование, чего он также не может. Наконец, компилятор будет искать любые пользовательские преобразования. Поскольку мы определили пользовательское преобразование из X
в int
, компилятор преобразует X
в int
, чтобы соответствовать print(int)
.
Связанный контент
Мы обсудим, как создавать пользовательские преобразования для типов классов (путем перегрузки операторов преобразования типов) в уроке «13.11 – Перегрузка операторов преобразования типов данных».
Шаг 5) Если совпадение не найдено с помощью пользовательского преобразования, компилятор будет искать соответствующую функцию, которая использует многоточие.
Связанный контент
Мы рассмотрим многоточия в уроке «11.12 – Многоточия (и почему их следует избегать)».
Шаг 6) Если к этому моменту совпадений не найдено, компилятор сдается и выдает ошибку компиляции о невозможности найти подходящую функцию.
Неоднозначные совпадения
С неперегруженными функциями каждый вызов функции либо будет преобразован в функцию, либо совпадение не будет найдено, и компилятор выдаст ошибку компиляции:
void foo()
{
}
int main()
{
foo(); // ok: совпадение найдено
goo(); // ошибка компиляции: совпадений не найдено
return 0;
}
С перегруженными функциями есть третий возможный результат: может быть найдено неоднозначное совпадение. Неоднозначное совпадение возникает, когда компилятор находит две или более функции, которые могут быть сопоставлены на одном шаге. Когда это произойдет, компилятор прекратит сопоставление и выдаст ошибку компиляции, заявив, что он обнаружил неоднозначный вызов функции.
Поскольку каждая перегруженная функция для компиляции должна быть различаться, вам может быть интересно, как это возможно, что вызов функции может привести к более чем одному совпадению. Давайте посмотрим на пример, который это иллюстрирует:
void print(int x)
{
}
void print(double d)
{
}
int main()
{
print(5l); // 5l имеет тип long
return 0;
}
Поскольку литерал 5l имеет тип long
, компилятор сначала проверяет, может ли он найти точное совпадение для print(long)
, но не найдет его. Затем компилятор попробует выполнить числовое продвижение, но значения типа long
не могут быть расширены, поэтому здесь тоже нет совпадений.
После этого компилятор попытается найти совпадение, применив числовые преобразования к аргументу long
. В процессе проверки всех правил преобразования чисел компилятор найдет два возможных совпадения. Если аргумент long
численно преобразован в int
, тогда вызов функции будет соответствовать print(int)
. Если вместо этого аргумент long
преобразуется в double
, тогда он будет соответствовать print(double)
. Поскольку посредством числового преобразования были обнаружены два возможных совпадения, вызов функции считается неоднозначным.
В Visual Studio 2019 это приводит к следующему сообщению об ошибке:
error C2668: 'print': ambiguous call to overloaded function
message : could be 'void print(double)'
message : or 'void print(int)'
message : while trying to match the argument list '(long)'
Ключевые выводы
Если компилятор находит несколько совпадений на данном шаге, результатом будет неоднозначный вызов функции. Это означает, что ни одно совпадение на данном шаге не считается лучшим, чем любое другое совпадение на том же шаге.
Вот еще один пример, который дает неоднозначные совпадения:
void print(unsigned int x)
{
}
void print(float y)
{
}
int main()
{
print(0); // int можно численно преобразовать в unsigned int или float
print(3.14159); // double можно численно преобразовать в unsigned int или float
return 0;
}
Хотя вы можете ожидать, что 0 приведет к print(unsigned int)
, и 3.14159 приведет к print(float)
, оба этих вызова приводят к неоднозначному совпадению. Значение 0 типа int
может быть численно преобразовано в unsigned int
или float
, поэтому обе перегрузки соответствуют одинаково хорошо, и результатом является неоднозначный вызов функции.
То же самое относится к преобразованию числа типа double
в тип float
или unsigned int
. Оба являются числовыми преобразованиями, поэтому обе перегрузки соответствуют одинаково, и результат снова неоднозначен.
Разрешение неоднозначных совпадений
Поскольку неоднозначные совпадения являются ошибкой времени компиляции, их необходимо устранить, прежде чем ваша программа будет компилироваться. Есть несколько способов разрешить неоднозначные совпадения:
- Часто лучший способ – просто определить новую перегруженную функцию, которая принимает параметры именно того типа, с которым вы пытаетесь ее вызвать. Тогда C++ сможет найти точное совпадение для вызова функции.
- В качестве альтернативы явно приведите неоднозначные аргументы к типу функции, которую хотите вызвать. Например, чтобы
print(0)
вызывалprint(unsigned int)
, вы должны сделать это:int x{ 0 }; print(static_cast<unsigned int>(x)); // вызовет print(unsigned int)
- Если ваш аргумент является литералом, вы можете использовать суффикс литерала, чтобы убедиться, что ваш литерал интерпретируется как правильный тип:
print(0u); // вызовет print(unsigned int), поскольку суффикс 'u' = unsigned int
Список наиболее часто используемых суффиксов можно найти в уроке «4.13 – Литералы».
Сопоставление функций с несколькими аргументами
Если аргументов несколько, компилятор по очереди применяет правила сопоставления к каждому аргументу. Выбирается так функция, для которой каждый аргумент соответствует по крайней мере так же хорошо, как и у всех других функций, причем по крайней мере один аргумент соответствует лучше, чем у всех других функций. Другими словами, выбранная функция должна обеспечивать лучшее соответствие, чем все другие функции-кандидаты, по крайней мере, по одному параметру и не хуже по всем остальным параметрам.
В случае, если такая функция будет найдена, это явный и однозначно лучший выбор. Если такая функция не найдена, вызов будет считаться неоднозначным (или без совпадений).
Например:
#include <iostream>
void print(char c, int x)
{
std::cout << 'a';
}
void print(char c, double x)
{
std::cout << 'b';
}
void print(char c, float x)
{
std::cout << 'c';
}
int main()
{
print('x', 'a');
}
В приведенной выше программе все функции точно соответствуют по первому аргументу. Однако верхняя функция соответствует по второму параметру через числовое продвижение, тогда как другие функции требуют числового преобразования. Таким образом, print(char, int)
однозначно является лучшим совпадением.