Что такое вывод аргументов шаблона

Ранее мы использовали такой шаблон функции:

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, массивами и ссылочными типами.