Шаблоны — это фрагменты обобщённого кода, в котором некоторые типы или константы вынесены в параметры. Шаблонными могут быть функции, структуры (классы) и даже переменные. Компилятор превращает использование шаблона в конкретный код, подставляя в него нужные параметры на этапе компиляции. Шаблоны позволяют писать общий код, пригодный для использования с разными типами данных.
Стандартная библиотека 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>
.
В следующих параграфах мы будем подробно рассматривать шаблонные классы, в которых могут быть шаблонные функции-члены.