Зачем нужны шаблоны функций

Если несколько функций отличаются только типами параметров, их общую реализацию можно вынести в шаблон.

Например, следующая функция возвращает большее из двух значений типа int:

int max_value(int a, int b)
{
    return b < a ? a : b;
}

Для значений типа double пришлось бы написать ещё одну перегрузку:

double max_value(double a, double b)
{
    return b < a ? a : b;
}

Затем могут понадобиться версии для float, long, std::string и других типов. Код при этом останется практически одинаковым.

Шаблон функции позволяет записать общую реализацию один раз:

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

Теперь одну реализацию можно использовать с разными типами:

max_value(10, 20);
max_value(3.5, 7.2);

В первом вызове T будет выведен как int, а во втором — как double.

Параметр типа

Конструкция

template<typename T>

объявляет параметр шаблона с именем T.

Далее T можно использовать там, где обычно указывается тип:

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

В этом шаблоне T используется:

  • как тип возвращаемого значения;
  • как тип параметра a;
  • как тип параметра b.

Имя T выбрано по соглашению и не имеет специального значения для языка. Параметру можно дать более содержательное имя:

template<typename ValueType>
ValueType max_value(ValueType a, ValueType b)
{
    return b < a ? a : b;
}

Обе записи эквивалентны.

Для коротких шаблонов обычно используют имя T. Если параметров несколько, применяют имена T1, T2 или более содержательные названия.

Параметры шаблона и параметры функции

В объявлении

template<typename T>
T max_value(T a, T b);

T, a и b относятся к разным уровням.

  • T — параметр шаблона, который задаёт тип;
  • a и b — параметры функции, принимающие значения во время вызова.

Например, при вызове

max_value(10, 20);

компилятор выводит:

T = int

А во время выполнения функции её параметры получают конкретные значения:

a = 10
b = 20

Параметры шаблона могут представлять не только типы. Например:

template<typename T, std::size_t N>

Здесь T является параметром типа, а N — значением времени компиляции, то есть нетиповым параметром шаблона.

Инстанцирование шаблона

Определение шаблона

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

задаёт общую форму функции.

Когда компилятор встречает вызов

max_value(10, 20);

он выводит:

T = int

и при необходимости инстанцирует функцию для типа int.

Упрощённо получившуюся функцию можно представить так:

int max_value(int a, int b)
{
    return b < a ? a : b;
}

Для другого вызова

max_value(3.5, 7.2);

понадобится версия с T = double:

double max_value(double a, double b)
{
    return b < a ? a : b;
}

В исходном коде эти функции вручную не объявляются. Они получаются из шаблона при его использовании.

Несколько вызовов с одним типом используют одну и ту же специализацию:

max_value(1, 2);
max_value(10, 20);
max_value(100, 200);

Во всех случаях используется:

max_value<int>

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

В большинстве случаев тип указывать не требуется:

max_value(10, 20);

Компилятор определяет T по аргументам функции.

Тип также можно указать явно:

max_value<int>(10, 20);

Параметр и аргумент шаблона

В объявлении

template<typename T>

T называется параметром шаблона.

В вызове

max_value<int>(10, 20);

int является аргументом шаблона. Таким образом, явно задаётся:

T = int

При обычном вызове

max_value(10, 20);

тот же аргумент шаблона выводится автоматически.

Подробные правила этого процесса называются template argument deduction — выводом аргументов шаблона.

typename и class

Параметр типа можно объявить двумя способами:

template<typename T>

или:

template<class T>

Для параметра типа эти варианты равнозначны:

template<class T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

Например, вызов

max_value(10, 20);

даёт:

T = int

А вызов

max_value(1.5, 2.5);

даёт:

T = double

Исторически class использовался для объявления параметров типов раньше typename. Оба варианта сохранились в языке.

В современном коде часто используется typename, поскольку по смыслу параметр представляет произвольный тип.

Требования к типу

Наличие typename T само по себе не означает, что шаблон можно применить к любому типу.

Рассмотрим функцию:

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

В её теле используется выражение:

b < a

Следовательно, для выбранного типа T должна существовать подходящая операция <.

С типом int всё корректно:

max_value(10, 20);

С типом double шаблон также работает:

max_value(1.5, 2.5);

Для std::string определено лексикографическое сравнение:

std::string a = "alpha";
std::string b = "beta";

auto result = max_value(a, b);

Теперь рассмотрим пользовательский тип:

struct Point
{
    int x;
    int y;
};

Попытка вызвать max_value для двух объектов этого типа выглядит так:

Point a{1, 2};
Point b{3, 4};

auto p = max_value(a, b);

Во время инстанцирования компилятору потребуется выражение:

b < a

Однако operator< для Point не определён. Поэтому такая специализация не скомпилируется.

Как операции определяют требования

Требования к типу следуют из операций, используемых в шаблоне.

Например:

template<typename T>
T add(T a, T b)
{
    return a + b;
}

Этот шаблон требует, чтобы для типа T было допустимо выражение:

a + b

Следующая функция требует поддержки операции инкремента:

template<typename T>
void increment(T& value)
{
    ++value;
}

Для шаблона вывода значения

template<typename T>
void print(const T& value)
{
    std::cout << value;
}

должен существовать подходящий operator<<.

Начиная с C++20 эти требования можно описывать явно с помощью concept и requires. К ним имеет смысл перейти после изучения базовой механики шаблонов.

Шаблоны работают на этапе компиляции

Рассмотрим простой шаблон:

template<typename T>
void foo(T value)
{
}

При вызове

foo(42);

компилятор уже знает:

T = int

и работает с конкретной специализацией:

foo<int>

Для вызова

foo(3.14);

используется:

foo<double>

Выбор типа происходит во время компиляции.

Это отличает шаблоны от классического полиморфизма времени выполнения с виртуальными функциями:

struct Shape
{
    virtual void draw() = 0;
};

При виртуальном вызове конкретная реализация может определяться во время выполнения программы. В шаблонном коде конкретные типы известны компилятору заранее.

Благодаря этому шаблонный код обычно хорошо поддаётся оптимизации: после инстанцирования компилятор видит обычную функцию с конкретными типами.

Полный пример

Один шаблон для чисел и строк

#include <iostream>
#include <string>

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(4.2, 7.5) << '\n';

    std::string s1 = "alpha";
    std::string s2 = "beta";

    std::cout << max_value(s1, s2) << '\n';
}

В программе используются три варианта max_value:

max_value<int>
max_value<double>
max_value<std::string>

Исходная реализация при этом остаётся одна:

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

Что нужно запомнить

  • Шаблон функции позволяет записать общую реализацию один раз и применять её к разным типам.
  • T в конструкции template<typename T> является параметром типа.
  • Параметр шаблона задаёт тип, а параметры функции принимают значения.
  • Создание конкретной функции из шаблона называется инстанцированием.
  • Аргумент шаблона может быть указан явно или выведен компилятором.
  • typename и class равнозначны при объявлении параметра типа.
  • Шаблон работает только с типами, которые поддерживают необходимые операции.
  • Выбор типа и инстанцирование шаблона происходят во время компиляции.