Что такое вывод аргументов шаблона
Ранее мы использовали такой шаблон функции:
template<typename T>
T max_value(T a, T b)
{
return b < a ? a : b;
}
Его можно вызвать без явного указания T:
max_value(10, 20);
Компилятор самостоятельно определит тип:
T = int
Этот процесс называется template argument deduction - выводом аргументов шаблона.
Как выводится T
Возьмём более простой шаблон:
template<typename T>
void foo(T value)
{
}
При вызове
foo(42);
тип аргумента - int, поэтому компилятор получает:
T = int
Для других аргументов результат будет другим:
foo(42); // T = int
foo(3.14); // T = double
foo(3.14f); // T = float
foo('A'); // T = char
Компилятор сопоставляет тип параметра функции
T
с типом переданного аргумента и на основании этого определяет значение параметра шаблона.
После вызова
foo(42);
можно считать, что дальнейшая работа идёт со специализацией:
foo<int>
А после вызова
foo(3.14);
- со специализацией:
foo<double>
Один параметр шаблона может встречаться несколько раз
В max_value один и тот же T используется для обоих параметров:
template<typename T>
T max_value(T a, T b);
При вызове
max_value(10, 20);
вывод выполняется для каждого аргумента:
10 → T = int
20 → T = int
Оба результата совпадают. Поэтому компилятор определяет:
T = int
и может использовать специализацию:
max_value<int>
То же происходит здесь:
max_value(1.5, 2.7);
Оба аргумента имеют тип double:
1.5 → T = double
2.7 → T = double
Результат:
max_value<double>
Проблема возникает, когда аргументы дают разные результаты при выводе одного и того же параметра шаблона.
Конфликт типов при вызове max_value(4, 7.2)
Рассмотрим вызов:
max_value(4, 7.2);
Первый аргумент имеет тип int:
4 → T = int
Второй аргумент имеет тип double:
7.2 → T = double
Но в шаблоне используется только один параметр T:
template<typename T>
T max_value(T a, T b);
Параметр T должен обозначать один тип, тогда как аргументы предлагают для него два разных варианта: int и double.
При вызове max_value(4, 7.2) первый аргумент даёт T = int, второй - T = double. Поскольку результаты вывода должны совпадать, возникает ошибка.
max_value(4, 7.2); // ошибка
Почему обычная функция в этом случае работает
Предположим, существует обычная функция:
double max_value(double a, double b)
{
return b < a ? a : b;
}
Такой вызов корректен:
max_value(4, 7.2);
Тип параметров функции уже известен:
double max_value(double, double);
Поэтому первый аргумент преобразуется из int в double:
4 → 4.0
После этого вызывается:
max_value(4.0, 7.2);
В случае шаблона
template<typename T>
T max_value(T a, T b);
сначала нужно определить сам T. При вызове
max_value(4, 7.2);
этот этап завершается конфликтом:
T = int
T = double
Ошибка возникает на этапе вывода T, поэтому выбор конкретной специализации и неявные преобразования аргументов уже не выполняются.
Явное указание аргумента шаблона
Параметр T можно задать вручную:
max_value<double>(4, 7.2);
Тип T уже задан явно:
T = double
После подстановки получаются параметры функции:
double max_value(double a, double b);
Для первого аргумента применяется обычное преобразование:
int → double
Поэтому вызов становится корректным.
Можно выполнить преобразование и в другую сторону:
max_value<int>(4, 7.2);
Теперь параметры функции имеют тип int, поэтому значение 7.2 преобразуется в int:
7.2 → 7
Вызов скомпилируется, но дробная часть будет потеряна.
Вывод типа и аргументы функции по умолчанию
Вывод T выполняется по переданным аргументам функции, а значение по умолчанию применяется позже. Поэтому оно не участвует в template argument deduction.
Например:
template<typename T>
void foo(T value = {})
{
}
Такой вызов работает:
foo(42); // T = int
Тип T выводится из переданного аргумента 42.
Вызов foo() не передаёт аргументов, поэтому определить T компилятору не из чего:
foo(); // ошибка: невозможно вывести T
Значение по умолчанию для value применяется только после определения параметров шаблона. Поэтому сначала компилятор должен узнать T, сформировать конкретную функцию и лишь затем использовать значение её аргумента по умолчанию.
Тип можно указать явно:
foo<int>(); // OK
Теперь известно:
T = int
и функция фактически имеет параметр:
int value = {};
Другой вариант - задать значение по умолчанию уже для параметра шаблона:
#include <string>
template<typename T = std::string>
void foo(T value = "")
{
}
Тогда допустимы оба вызова:
foo(42); // T = int
foo(); // T = std::string
В первом случае T выводится из аргумента функции и значение шаблона по умолчанию не используется. Во втором аргументов функции нет, поэтому берётся заданный для T тип std::string.
Здесь важно различать два независимых механизма:
template<typename T = std::string> // значение по умолчанию для параметра шаблона
void foo(T value = ""); // значение по умолчанию для параметра функции
Тип можно привести до вызова
Другой вариант - самостоятельно привести аргументы к одному типу до вызова шаблона:
max_value(static_cast<double>(4), 7.2);
Теперь механизм вывода видит два аргумента типа double:
double
double
Поэтому получает:
T = double
Можно сделать и наоборот:
max_value(4, static_cast<int>(7.2));
Тогда:
T = int
Первый вариант обычно разумнее для чисел с дробной частью, поскольку сохраняет её.
Deduction требует согласованного типа
При вызове
max_value(4, 7.2);
может показаться логичным следующее рассуждение:
int и double
→ double может представить оба значения
→ выберем double
Однако механизм deduction действует иначе:
первый аргумент → T = int
второй аргумент → T = double
↓
конфликт
Каждый аргумент участвует в выводе отдельно, а полученные результаты должны совпасть. Компилятор не выбирает «лучший» или наиболее широкий тип вместо них.
Если функции действительно требуется принимать разные типы, это нужно отразить в самом шаблоне:
template<typename T1, typename T2>
auto max_value(T1 a, T2 b)
{
return b < a ? a : b;
}
Здесь первый и второй аргументы уже связаны с разными параметрами шаблона:
T1 = int
T2 = double
Такой вариант требует отдельного разбора: важно определить тип результата и правила сравнения значений разных типов.
Явный и автоматический вывод
Разницу между автоматическим выводом и явным указанием типа удобно увидеть на простом шаблоне:
template<typename T>
void foo(T value)
{
}
Автоматический вывод
foo(42);
Компилятор определяет:
T = int
Явное указание типа
foo<double>(42);
Тип задан заранее:
T = double
После этого значение 42 преобразуется к double.
Таким образом, вызовы используют разные специализации:
foo(42); // foo<int>
foo<double>(42); // foo<double>
Исходное значение аргумента одинаково, но полученные специализации различаются.
Вывод выполняется по типам аргументов
Вывод типа обычно зависит от типа аргумента, а не от его значения:
foo(1);
foo(100);
foo(-500);
Во всех случаях:
T = int
А здесь получаются разные специализации:
foo(1); // T = int
foo(1L); // T = long
foo(1.0); // T = double
foo(1.0f); // T = float
Суффиксы литералов важны именно потому, что меняют их тип.
Например, литерал
1.0
имеет тип double, а литерал
1.0f
имеет тип float. Поэтому:
foo(1.0); // foo<double>
foo(1.0f); // foo<float>
Что происходит при вызове шаблонной функции
Для вызова
max_value(10, 20);
процесс в упрощённом виде выглядит так:
max_value(10, 20)
10 → int
20 → int
T = int
max_value<int>
Для вызова
max_value(4, 7.2);
получается конфликт:
4 → int
7.2 → double
T = int
T = double
вывод не удался
В вызове с явно указанным типом
max_value<double>(4, 7.2);
T уже задан, поэтому компилятор сразу получает конкретные параметры функции:
T = double
параметры функции:
double, double
4 → 4.0
max_value<double>(4.0, 7.2)
Полный пример
Автоматический и явный выбор типа
#include <iostream>
template<typename T>
T max_value(T a, T b)
{
return b < a ? a : b;
}
int main()
{
std::cout << max_value(10, 20) << '\n';
std::cout << max_value(1.5, 2.7) << '\n';
// Ошибка:
// std::cout << max_value(4, 7.2) << '\n';
std::cout << max_value<double>(4, 7.2) << '\n';
std::cout << max_value(
static_cast<double>(4),
7.2
) << '\n';
}
В первых двух вызовах аргумент шаблона выводится автоматически:
max_value<int>
max_value<double>
В третьем вызове double задаётся явно. В последнем оба аргумента заранее приведены к одному типу, поэтому T = double снова можно вывести автоматически.
Что дальше
Пока рассматривались параметры, передаваемые по значению:
template<typename T>
void foo(T value);
Для ссылок правила вывода меняются:
template<typename T>
void foo(T& value);
template<typename T>
void foo(const T& value);
Особенно заметны различия при работе с const, массивами и ссылочными типами.