Шаблоны — это фрагменты обобщённого кода, в котором некоторые типы или константы вынесены в параметры. Шаблонными могут быть функции, структуры (классы) и даже переменные. Компилятор превращает использование шаблона в конкретный код, подставляя в него нужные параметры на этапе компиляции. Шаблоны позволяют писать общий код, пригодный для использования с разными типами данных.
Стандартная библиотека C++ построена на шаблонах. Раньше её даже называли Standard Template Library (STL, стандартная библиотека шаблонов). Её контейнеры и итераторы являются шаблонными классами, а алгоритмы — шаблонными функциями. Примеры шаблонных конструкций из стандартной библиотеки нам уже встречались: это, например, контейнер std::vector
и функция std::sort
. В следующем параграфе мы рассмотрим контейнер std::array
, размер которого задаётся шаблонной константой времени компиляции. В этом параграфе мы рассмотрим шаблоны функций и структур, параметры которых являются типами. Но прежде чем говорить про шаблоны, рассмотрим перегрузку функций.
Перегрузка функций
Количество и типы аргументов функции должны быть известны заранее, на этапе компиляции. Но в языке C++ можно создавать функции с одним и тем же именем, но разным набором или типами аргументов и с разными телами. Такие функции называются перегруженными. Рассмотрим, например, семейство перегруженных функций для печати переменной на экран:
#include <iostream>
#include <string>
void Print(int value) {
std::cout << value << "\n";
}
void Print(const std::string& name, int value) {
std::cout << name << ": " << value << "\n"; // печатаем название и саму величину
}
void Print(const std::string& str) {
std::cout << str << "\n";
}
int main() {
Print(42); // версия 1
Print("x", 42); // версия 2
Print("good bye"); // версия 3
}
Компилятор, сравнивая разные версии функции друг с другом, смотрит на их имена и набор типов аргументов. При этом имена аргументов ни на что не влияют. Также нельзя перегружать функции по типу возвращаемого значения. Действительно, возвращаемое значение может просто игнорироваться в месте вызова, и компилятор не сможет определить, какая версия функции имеется в виду.
int f(int x) {
return x;
}
int f(int y) { // ошибка компиляции: функция с таким именем и типом параметра уже была
return 2 * y;
}
double f(int x) { // ошибка компиляции: перегружать по возвращаемому значению нельзя
return 3 * x;
}
Шаблонные функции
Рассмотрим классический пример. Предположим, у нас есть функция, вычисляющая максимум целых чисел:
int Max(int x, int y) {
if (x > y) {
return x;
} else {
return y;
}
}
Она определена для аргументов типа int
. Однако, если применить её к аргументам типа double
, результат получится неожиданным. А её применение к строкам или векторам вообще не скомпилируется:
#include <iostream>
#include <string>
int main() {
std::cout << Max(1, 2) << "\n"; // 2
std::cout << Max(3.14159, 2.71828) << "\n"; // внезапно 3
std::string word1 = "hello", word2 = "world";
std::cout << Max(word1, word2); // ошибка компиляции
}
В вызове Max(3.14159, 2.71828)
аргументы будут преобразованы к типу int
, то есть получится Max(3, 2)
. Вызов Max(word1, word2)
не скомпилируется, так как строки нельзя привести к типу int
. Чтобы эти вызовы корректно заработали, надо определить перегруженные версии функции Max
:
#include <iostream>
#include <string>
int Max(int x, int y) {
if (x > y) {
return x;
} else {
return y;
}
}
double Max(double x, double y) {
if (x > y) {
return x;
} else {
return y;
}
}
std::string Max(const std::string& x, const std::string& y) {
if (x > y) {
return x;
} else {
return y;
}
}
int main() {
std::cout << Max(1, 2) << "\n"; // 2
std::cout << Max(3.14159, 2.71828) << "\n"; // 3.14159
std::string word1 = "hello", word2 = "world";
std::cout << Max(word1, word2); // world
}
Выписывать похожие друг на друга версии функций утомительно. Кроме того, такие функции не смогут работать с новыми, неизвестными нам заранее типами. Шаблоны позволяют описать такую функцию один раз, вынеся тип в параметры:
template <typename T>
T Max(const T& x, const T& y) {
if (x > y) {
return x;
} else {
return y;
}
}
Шаблон начинается с шапки template
. Далее в угловых скобках перечисляются формальные имена параметров. В нашем случае параметр один — это тип T
(от слова type). Вместо ключевого слова typename
в этом месте допускается использовать слово class
(вы можете встретить такие описания шаблонов на cppreference.com). А вместо имени T
можно было бы использовать любой другой идентификатор.
Так как мы не знаем, будет ли тип T
встроенным или сложным, то на всякий случай передаём аргументы в функцию по константной ссылке, чтобы избежать лишнего копирования.
В нашей шаблонной функции Max
используется оператор >
. Он определён для обычных чисел, строк и векторов (если, конечно, для элементов вектора тоже определён этот оператор). Но если попробовать применить наш шаблон к типу, не поддерживающему оператор >
, то произойдёт ошибка компиляции:
struct Point {
double x = 0.0;
double y = 0.0;
double z = 0.0;
};
int main() {
Point p1, p2;
Point p = Max(p1, p2); // ошибка компиляции
}
Вывод шаблонных параметров
Конкретные версии шаблонной функции Max
для нужных типов получаются подстановкой шаблонных аргументов в угловые скобки. Так, Max<int>
— это версия нашей функции для типа int
, а Max<std::string>
— версия для строк. Важно понимать, что, несмотря на общий шаблон, это разные функции, которые просто порождаются компилятором по образцу.
Вызвать шаблонную функцию можно было бы так:
Max<double>(3.14159, 2.71828); // 3.14159
Max<int>(3.14159, 2.71828); // вызывается int-версия, вернётся 3
Однако параметры шаблона в угловых скобках можно не писать: компилятор попытается сам угадать эти параметры по типу аргументов:
int main() {
std::cout << Max(1, 2) << "\n"; // 2, вызывается Max<int>
std::cout << Max(3.14159, 2.71828) << "\n"; // 3.14159, вызывается Max<double>
std::string word1 = "hello", word2 = "world";
std::cout << Max(word1, word2); // world, вызывается Max<std::string>
}
В случае неоднозначностей, например в вызове Max(3.14159, 2)
, компилятор не сможет автоматически вывести параметр, и ему придётся подсказать тип: Max<double>(3.14159, 2)
.
Перегрузка шаблонных функций
Шаблонные функции тоже можно перегружать. Пусть, например, мы хотим вычислять максимум двух векторов, но при этом сравнивать векторы сначала по размеру, а затем уже лексикографически. Стандартное сравнение векторов через оператор >
не будет учитывать размер. Поэтому напишем отдельную перегрузку для векторов:
#include <iostream>
#include <vector>
// общая версия
template <typename T>
T Max(const T& x, const T& y) {
if (x > y) {
return x;
} else {
return y;
}
}
// перегрузка для векторов
template <typename T>
const std::vector<T>& Max(const std::vector<T>& v1, const std::vector<T>& v2) {
if (v1.size() > v2.size()) {
return v1;
} else if (v1.size() < v2.size()) {
return v2;
} else if (v1 > v2) {
return v1;
} else {
return v2;
}
}
int main() {
std::cout << Max(1, 2) << "\n"; // вызов общей версии
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {4, 5};
for (int x : Max(v1, v2)) { // вызов перегруженной версии
std::cout << x << " "; // 1 2 3
}
std::cout << "\n";
}
Разрешение неоднозначностей
Когда компилятор видит вызов функции, ему нужно правильно определить, в каком пространстве имён её искать, какую из перегруженных версий выбрать, а в случае шаблонной функции — как вывести параметры шаблона. Для шаблонных функций после выбора перегруженной версии возможен ещё выбор из вариантов полной специализации шаблона. Общие правила поиска нужной функции достаточно сложны, и мы не будем их здесь приводить полностью. Однако в случаях, которые мы будем рассматривать в этом учебнике, выбор нужной функции будет интуитивно понятен.
Шаблонные структуры
Структуры и классы также могут быть описаны в общем виде и параметризованы типами или константами времени компиляции. Типичный пример шаблонной структуры — std::pair
. Определим по аналогии свою структуру Triple
с тремя шаблонными типами:
#include <string>
template <typename T1, typename T2, typename T3>
struct Triple {
T1 first;
T2 second;
T3 third;
};
int main() {
Triple<int, int, int> point = {-1, 3, 2};
Triple<std::string, std::string, int> wordPairsFreq = {"hello", "world", 42};
}
Здесь так же, как и в случае функций, компилятор генерирует по образцу две никак не связанные друг с другом структуры Triple<int, int, int>
и Triple<std::string, std::string, int>
.
В следующих параграфах мы будем подробно рассматривать шаблонные классы, в которых могут быть шаблонные функции-члены.