8.15 – Шаблоны функций с несколькими шаблонными типами
В уроке «8.13 – Шаблоны функций» мы написали шаблон функции для вычисления максимального из двух значений:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x > y) ? x : y;
}
int main()
{
std::cout << max(1, 2) << '\n'; // создаст экземпляр max(int, int)
std::cout << max(1.5, 2.5) << '\n'; // создаст экземпляр max(double, double)
return 0;
}
Теперь рассмотрим следующую аналогичную программу:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x > y) ? x : y;
}
int main()
{
std::cout << max(2, 3.5) << '\n'; // ошибка компиляции
return 0;
}
Вы можете удивиться, обнаружив, что эта программа не компилируется. Вместо этого компилятор выдаст кучу (возможно, сумасшедших) сообщений об ошибках. В Visual Studio автор получил следующее:
Project3.cpp(11,18): error C2672: 'max': no matching overloaded function found
Project3.cpp(11,28): error C2782: 'T max(T,T)': template parameter 'T' is ambiguous
Project3.cpp(4): message : see declaration of 'max'
Project3.cpp(11,28): message : could be 'double'
Project3.cpp(11,28): message : or 'int'
Project3.cpp(11,28): error C2784: 'T max(T,T)': could not deduce template argument for 'T' from 'double'
Project3.cpp(4): message : see declaration of 'max'
В нашем вызове функции max(2, 3.5)
мы передаем аргументы двух разных типов: int
и double
. Поскольку мы вызываем функцию без использования угловых скобок для указания фактического типа, компилятор сначала проверяет, есть ли совпадение не с шаблоном для max(int, double)
. И не находит.
Затем компилятор проверит, сможет ли он найти совпадение с шаблоном функции (используя вывод аргументов шаблона, который мы рассмотрели в уроке «8.14 – Создание экземпляра шаблона функции»). Однако это также не удастся по простой причине: T
может представлять только один тип. Для T
не существует типа, который позволил бы компилятору создать экземпляр шаблона функции max<T>(T, T)
для функции с двумя разными типами параметров. Другими словами, поскольку оба параметра в шаблоне функции относятся к типу T
, они должны соответствовать одному и тому же фактическому типу.
Поскольку ни нешаблонных, ни шаблонных совпадений не найдено, вызов функции не удается разрешить, и мы получаем ошибку компиляции.
Вы можете задаться вопросом, почему компилятор не сгенерировал функцию max<double>(double, double)
и не использовал затем числовое преобразование для приведения типа аргумента int
в double
. Ответ прост: преобразование типов выполняется только при разрешении перегрузок функций, а не при выводе аргументов шаблона.
Это отсутствие преобразования типов преднамеренное, по крайней мере, по двум причинам. Во-первых, это помогает упростить задачу: мы либо находим точное соответствие между аргументами вызова функции и параметрами типа шаблона, либо нет. Во-вторых, это позволяет нам создавать шаблоны функций для случаев, когда мы хотим гарантировать, что два или более параметров имеют один и тот же тип (как в примере выше).
Придется найти другое решение. К счастью, мы можем решить эту проблему (по крайней мере) тремя способами.
Использование static_cast
для преобразования аргументов в соответствующие типы
Первое решение – возложить бремя преобразования аргументов в соответствующие типы на вызывающего. Например:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x > y) ? x : y;
}
int main()
{
// преобразовываем наш int в double, чтобы мы могли вызвать max(double, double)
std::cout << max(static_cast<double>(2), 3.5) << '\n';
return 0;
}
Теперь, когда оба аргумента имеют тип double
, компилятор сможет создать экземпляр max(double, double)
, который удовлетворит этот вызов функции.
Однако это решение неудобно и трудночитаемо.
Предоставление фактического типа
Если бы мы написали функцию max(double, double)
, не являющуюся шаблоном, то мы могли бы вызвать max(int, double)
и позволить правилам неявного преобразования типа преобразовать наш аргумент int
в double
, чтобы можно было разрешить вызов функции:
#include <iostream>
double max(double x, double y)
{
return (x > y) ? x : y;
}
int main()
{
std::cout << max(2, 3.5) << '\n'; // аргумент int будет преобразован в double
return 0;
}
Однако когда компилятор выполняет вывод аргументов шаблона, он не выполняет никаких преобразований типов. К счастью, нам не нужно использовать вывод аргументов шаблона, если мы сразу укажем фактический тип:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x > y) ? x : y;
}
int main()
{
// мы предоставили фактический тип double, поэтому
// компилятор не будет использовать вывод аргументов шаблона
std::cout << max<double>(2, 3.5) << '\n';
return 0;
}
В приведенном выше примере мы вызываем max<double>(2, 3.5)
. Поскольку мы явно указали, что T
следует заменить на double
, компилятор не будет использовать вывод аргументов шаблона. Вместо этого он просто создаст экземпляр функции max<double>(double, double)
, а затем выполнит преобразование типов для любых несовпадающих аргументов. Наш параметр int
будет неявно преобразован в double
.
Хотя это более читабельно, чем использование static_cast
, было бы еще лучше, если бы нам вообще не приходилось думать о типах при вызове функции max
.
Шаблоны функций с несколькими параметрами шаблонных типов
Корень нашей проблемы в том, что для нашего шаблона функции мы определили только один шаблонный тип (T
), а затем указали, что оба параметра должны быть одного и того же типа.
Лучший способ решить эту проблему – переписать наш шаблон функции таким образом, чтобы наши параметры могли вычисляться в разные типы. Вместо того, чтобы использовать один параметр шаблонного типа T
, теперь мы будем использовать два (T
и U
):
#include <iostream>
// Мы используем два параметра шаблонных типов с именами T и U
template <typename T, typename U>
// x может определиться в тип T, а y может определиться в тип U
T max(T x, U y)
{
return (x > y) ? x : y; // упс, здесь у нас проблема сужающегося преобразования
}
int main()
{
std::cout << max(2, 3.5) << '\n';
return 0;
}
Поскольку мы определили x
с шаблонным типом T
, а y
с шаблонным типом U
, x
и y
теперь могут определять свои типы независимо. Когда мы вызываем max(2, 3.5)
, T
может быть int
, а U
может быть double
. Компилятор с радостью создаст для нас экземпляр max<int, double>(int, double)
.
Однако в приведенном выше коде всё еще есть проблема: используя обычные арифметические правила (8.4 – Преобразования при вычислении арифметических выражений), double
имеет приоритет над int
, поэтому наш условный оператор вернет double
. Но наша функция определена как возвращающая T
– в тех случаях, когда T
определяется в int
, наше возвращаемое значение double
подвергнется сужающему преобразованию в int
, что приведет к предупреждению (и возможной потере данных).
Указание возвращаемого типа U
не решает проблемы, поскольку мы всегда можем поменять порядок операндов в вызове функции, чтобы поменять местами типы T
и U
.
Как решить эту проблему? Это хорошее применение для автоматического типа возвращаемого значения – мы позволим компилятору определить из инструкции return
, каким должен быть возвращаемый тип:
#include <iostream>
template <typename T, typename U>
auto max(T x, U y)
{
return (x > y) ? x : y;
}
int main()
{
std::cout << max(2, 3.5) << '\n';
return 0;
}
Эта версия max
теперь отлично работает с операндами разных типов.
Сокращенные шаблоны функций
C++20 вводит новое использование ключевого слова auto
: когда ключевое слово auto
используется в качестве типа параметра в обычной функции, компилятор автоматически преобразует эту функцию в шаблон функции, при этом каждый параметр auto
становится независимым параметром шаблонного типа. Этот метод создания шаблона функции называется сокращенным шаблоном функции.
Например:
auto max(auto x, auto y)
{
return (x > y) ? x : y;
}
является сокращением в C++20 для следующего шаблона:
template <typename T, typename U>
auto max(T x, U y)
{
return (x > y) ? x : y;
}
который совпадает с шаблоном функции max
, который мы написали выше.
В случаях, когда вы хотите, чтобы каждый параметр шаблонного типа был независимым типом, эта форма предпочтительна, поскольку удаление строки объявления параметров шаблона делает ваш код более кратким и читабельным.
Лучшая практика
Не стесняйтесь использовать сокращенные шаблоны функций, если каждый параметр auto
должен быть независимым шаблонным типом (а стандарт языка установлен у вас на C++20 или новее).