Шаблоны — это фрагменты обобщённого кода, в котором некоторые типы или константы вынесены в параметры. Шаблонными могут быть функции, структуры (классы) и даже переменные. Компилятор превращает использование шаблона в конкретный код, подставляя в него нужные параметры на этапе компиляции. Шаблоны позволяют писать общий код, пригодный для использования с разными типами данных.

Стандартная библиотека C++ построена на шаблонах. Раньше её даже называли Standard Template Library (STL, стандартная библиотека шаблонов). Её контейнеры и итераторы являются шаблонными классами, а алгоритмы — шаблонными функциями. Примеры шаблонных конструкций из стандартной библиотеки нам уже встречались: это, например, контейнер std::vector и функция std::sort. В следующем параграфе мы рассмотрим контейнер std::array, размер которого задаётся шаблонной константой времени компиляции. В этом параграфе мы рассмотрим шаблоны функций и структур, параметры которых являются типами. Но прежде чем говорить про шаблоны, рассмотрим перегрузку функций.

Перегрузка функций

Количество и типы аргументов функции должны быть известны заранее, на этапе компиляции. Но в языке C++ можно создавать функции с одним и тем же именем, но разным набором или типами аргументов и с разными телами. Такие функции называются перегруженными. Рассмотрим, например, семейство перегруженных функций для печати переменной на экран:

1#include <iostream>
2#include <string>
3
4void Print(int value) {
5    std::cout << value << "\n";
6}
7
8void Print(const std::string& name, int value) {
9    std::cout << name << ": " << value << "\n";  // печатаем название и саму величину
10}
11
12void Print(const std::string& str) {
13    std::cout << str << "\n";
14}
15
16int main() {
17    Print(42);  // версия 1
18    Print("x", 42);  // версия 2
19    Print("good bye");  // версия 3
20}

Компилятор, сравнивая разные версии функции друг с другом, смотрит на их имена и набор типов аргументов. При этом имена аргументов ни на что не влияют. Также нельзя перегружать функции по типу возвращаемого значения. Действительно, возвращаемое значение может просто игнорироваться в месте вызова, и компилятор не сможет определить, какая версия функции имеется в виду.

1int f(int x) {
2    return x;
3}
4
5int f(int y) {  // ошибка компиляции: функция с таким именем и типом параметра уже была
6    return 2 * y;
7}
8
9double f(int x) {  // ошибка компиляции: перегружать по возвращаемому значению нельзя
10    return 3 * x;
11}

Шаблонные функции

Рассмотрим классический пример. Предположим, у нас есть функция, вычисляющая максимум целых чисел:

1int Max(int x, int y) {
2    if (x > y) {
3        return x;
4    } else {
5        return y;
6    }
7}

Она определена для аргументов типа int. Однако, если применить её к аргументам типа double, результат получится неожиданным. А её применение к строкам или векторам вообще не скомпилируется:

1#include <iostream>
2#include <string>
3
4int main() {
5    std::cout << Max(1, 2) << "\n";  // 2
6    std::cout << Max(3.14159, 2.71828) << "\n";  // внезапно 3
7
8    std::string word1 = "hello", word2 = "world";
9    std::cout << Max(word1, word2);  // ошибка компиляции
10}

В вызове Max(3.14159, 2.71828) аргументы будут преобразованы к типу int, то есть получится Max(3, 2). Вызов Max(word1, word2) не скомпилируется, так как строки нельзя привести к типу int. Чтобы эти вызовы корректно заработали, надо определить перегруженные версии функции Max:

1#include <iostream>
2#include <string>
3
4int Max(int x, int y) {
5    if (x > y) {
6        return x;
7    } else {
8        return y;
9    }
10}
11
12double Max(double x, double y) {
13    if (x > y) {
14        return x;
15    } else {
16        return y;
17    }
18}
19
20std::string Max(const std::string& x, const std::string& y) {
21    if (x > y) {
22        return x;
23    } else {
24        return y;
25    }
26}
27
28int main() {
29    std::cout << Max(1, 2) << "\n";  // 2
30    std::cout << Max(3.14159, 2.71828) << "\n";  // 3.14159
31
32    std::string word1 = "hello", word2 = "world";
33    std::cout << Max(word1, word2);  // world
34}

Выписывать похожие друг на друга версии функций утомительно. Кроме того, такие функции не смогут работать с новыми, неизвестными нам заранее типами. Шаблоны позволяют описать такую функцию один раз, вынеся тип в параметры:

1template <typename T>
2T Max(const T& x, const T& y) {
3    if (x > y) {
4        return x;
5    } else {
6        return y;
7    }
8}

Шаблон начинается с шапки template. Далее в угловых скобках перечисляются формальные имена параметров. В нашем случае параметр один — это тип T (от слова type). Вместо ключевого слова typename в этом месте допускается использовать слово class (вы можете встретить такие описания шаблонов на cppreference.com). А вместо имени T можно было бы использовать любой другой идентификатор.

Так как мы не знаем, будет ли тип T встроенным или сложным, то на всякий случай передаём аргументы в функцию по константной ссылке, чтобы избежать лишнего копирования.

В нашей шаблонной функции Max используется оператор >. Он определён для обычных чисел, строк и векторов (если, конечно, для элементов вектора тоже определён этот оператор). Но если попробовать применить наш шаблон к типу, не поддерживающему оператор >, то произойдёт ошибка компиляции:

1struct Point {
2    double x = 0.0;
3    double y = 0.0;
4    double z = 0.0;
5};
6
7int main() {
8    Point p1, p2;
9    Point p = Max(p1, p2);  // ошибка компиляции
10}

Вывод шаблонных параметров

Конкретные версии шаблонной функции Max для нужных типов получаются подстановкой шаблонных аргументов в угловые скобки. Так, Max<int> — это версия нашей функции для типа int, а Max<std::string> — версия для строк. Важно понимать, что, несмотря на общий шаблон, это разные функции, которые просто порождаются компилятором по образцу.

Вызвать шаблонную функцию можно было бы так:

1Max<double>(3.14159, 2.71828);  // 3.14159
2Max<int>(3.14159, 2.71828);  // вызывается int-версия, вернётся 3

Однако параметры шаблона в угловых скобках можно не писать: компилятор попытается сам угадать эти параметры по типу аргументов:

1int main() {
2    std::cout << Max(1, 2) << "\n";  // 2, вызывается Max<int>
3    std::cout << Max(3.14159, 2.71828) << "\n";  // 3.14159, вызывается Max<double>
4
5    std::string word1 = "hello", word2 = "world";
6    std::cout << Max(word1, word2);  // world, вызывается Max<std::string>
7}

В случае неоднозначностей, например в вызове Max(3.14159, 2), компилятор не сможет автоматически вывести параметр, и ему придётся подсказать тип: Max<double>(3.14159, 2).

Перегрузка шаблонных функций

Шаблонные функции тоже можно перегружать. Пусть, например, мы хотим вычислять максимум двух векторов, но при этом сравнивать векторы сначала по размеру, а затем уже лексикографически. Стандартное сравнение векторов через оператор > не будет учитывать размер. Поэтому напишем отдельную перегрузку для векторов:

1#include <iostream>
2#include <vector>
3
4// общая версия
5template <typename T>
6T Max(const T& x, const T& y) {
7    if (x > y) {
8        return x;
9    } else {
10        return y;
11    }
12}
13
14// перегрузка для векторов
15template <typename T>
16const std::vector<T>& Max(const std::vector<T>& v1, const std::vector<T>& v2) {
17    if (v1.size() > v2.size()) {
18        return v1;
19    } else if (v1.size() < v2.size()) {
20        return v2;
21    } else if (v1 > v2) {
22        return v1;
23    } else {
24        return v2;
25    }
26}
27
28int main() {
29    std::cout << Max(1, 2) << "\n";  // вызов общей версии
30
31    std::vector<int> v1 = {1, 2, 3};
32    std::vector<int> v2 = {4, 5};
33    for (int x : Max(v1, v2)) {  // вызов перегруженной версии
34        std::cout << x << " ";  // 1 2 3
35    }
36    std::cout << "\n";
37}

Разрешение неоднозначностей

Когда компилятор видит вызов функции, ему нужно правильно определить, в каком пространстве имён её искать, какую из перегруженных версий выбрать, а в случае шаблонной функции — как вывести параметры шаблона. Для шаблонных функций после выбора перегруженной версии возможен ещё выбор из вариантов полной специализации шаблона. Общие правила поиска нужной функции достаточно сложны, и мы не будем их здесь приводить полностью. Однако в случаях, которые мы будем рассматривать в этом учебнике, выбор нужной функции будет интуитивно понятен.

Шаблонные структуры

Структуры и классы также могут быть описаны в общем виде и параметризованы типами или константами времени компиляции. Типичный пример шаблонной структуры — std::pair. Определим по аналогии свою структуру Triple с тремя шаблонными типами:

1#include <string>
2
3template <typename T1, typename T2, typename T3>
4struct Triple {
5    T1 first;
6    T2 second;
7    T3 third;
8};
9
10int main() {
11    Triple<int, int, int> point = {-1, 3, 2};
12    Triple<std::string, std::string, int> wordPairsFreq = {"hello", "world", 42};
13}

Здесь так же, как и в случае функций, компилятор генерирует по образцу две никак не связанные друг с другом структуры Triple<int, int, int> и Triple<std::string, std::string, int>.

В следующих параграфах мы будем подробно рассматривать шаблонные классы, в которых могут быть шаблонные функции-члены.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.7. Функции

Функции позволяют отделить часто используемый код и переиспользовать его с разными значениями аргументов.

Следующий параграф2.9. Разбор задач к главе «Базовые конструкции C++»

В этом параграфе мы разберём задачи к главе «Базовые конструкции C++».