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

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

Стандартная библиотека 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>.

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

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

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

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

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

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

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