Зачем нужны шаблоны функций
Если несколько функций отличаются только типами параметров, их общую реализацию можно вынести в шаблон.
Например, следующая функция возвращает большее из двух значений типа 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равнозначны при объявлении параметра типа.- Шаблон работает только с типами, которые поддерживают необходимые операции.
- Выбор типа и инстанцирование шаблона происходят во время компиляции.