Зачем нужны шаблоны
Шаблон — это генератор кода: вы пишете функцию или класс один раз для произвольного типа, а компилятор создаёт конкретные версии под каждый использованный тип. Так std::vector<int>, std::vector<std::string> и std::vector<Point> — три разных класса, сгенерированных из одного описания, но полностью типобезопасных и эффективных.
Это и есть обобщённое программирование: алгоритм sort одинаково работает и для чисел, и для строк, и для любых ваших типов — лишь бы тип поддерживал нужные операции.
Шаблон функции
template<typename T>
T maxValue(T a, T b) {
return a > b ? a : b;
}
maxValue(3, 7); // T = int
maxValue(2.5, 1.0); // T = double
maxValue<>(3, 7); // явно через <> — тоже ок
maxValue<double>(3, 7); // принудительно double
Вывод типа работает по аргументам. Если типы аргументов разные — maxValue(1, 2.5) не скомпилируется: компилятор не знает, что выбрать. Лечится явным параметром maxValue<double>(1, 2.5) или двумя шаблонными параметрами.
Несколько параметров и не тип-параметры
template<typename T, typename U>
auto add(T a, U b) { return a + b; }
template<typename T, size_t N> // не тип, а значение — compile-time константа
T sumArray(const T (&arr)[N]) {
T s{};
for (size_t i = 0; i < N; ++i) s += arr[i];
return s;
}
int a[] = {1, 2, 3};
sumArray(a); // N выведено = 3 — без ручной передачи размера
Шаблон класса
template<typename T>
class Stack {
std::vector<T> data_;
public:
void push(T v) { data_.push_back(std::move(v)); }
T pop() { T v = std::move(data_.back()); data_.pop_back(); return v; }
bool empty() const { return data_.empty(); }
size_t size() const { return data_.size(); }
};
Stack<int> si;
si.push(42);
Stack<std::string> ss; // другой класс — та же реализация
Ключевой момент: каждая инстанциация — это отдельный класс. Stack<int> и Stack<string> не связаны друг с другом, для них нет общего базового. Код для каждого типа генерируется заново — поэтому шаблоны должны жить в заголовках, а не в .cpp: компилятору нужно видеть всё определение при инстанцировании.
Специализация: особый случай для конкретного типа
Общий шаблон можно переопределить для отдельных типов:
template<typename T>
struct TypeName { static const char* get() { return "неизвестно"; } };
template<> // полная специализация
struct TypeName<int> { static const char* get() { return "int"; } };
template<> // полная специализация
struct TypeName<std::string> { static const char* get() { return "string"; } };
TypeName<double>::get(); // "неизвестно"
TypeName<int>::get(); // "int"
Для классов есть и частичная специализация (template<typename T> struct Vec<T*> — версия для указателей), для функций — только полная; вместо частичной для функций используют перегрузки или if constexpr.
Variadic templates: переменное число аргументов
Шаблоны с typename... Args принимают любое число аргументов:
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
if constexpr (sizeof...(rest) > 0)
print(rest...); // рекурсия по пакету
}
print(1, "x", 3.14, 'c'); // все аргументы разных типов
На этом построены std::tuple, std::make_unique, emplace-методы и std::format.
Concepts: контракты на типы (C++20)
Раньше ошибка в шаблоне давала стену шаблонных сообщений. Concepts позволяют описать требования к типу прямо в сигнатуре:
#include <concepts>
template<std::integral T>
T gcd(T a, T b) {
while (b) { T t = b; b = a % b; a = t; }
return a;
}
gcd(12, 18); // ок — int интегральный
// gcd(1.5, 2.0); // ошибка компиляции с понятным сообщением: double не integral
// свой концепт
template<typename T>
concept Addable = requires(T a, T b) { a + b; };
template<Addable T>
T twice(T v) { return v + v; }
Стандартные концепты уже есть: std::integral, std::floating_point, std::same_as, std::convertible_to, std::ranges — и принимаемый вами тип можно проверить статически.
Если C++ старше 20 — SFINAE и enable_if
До concepts требования реализовывали через std::enable_if — отключение шаблона по условию:
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T> // возвращаемый тип = T только для целых
isDivisible(T a, T b) { return a % b == 0; }
Работает, но читается плохо — concepts для новых проектов однозначно чище.
Типичные ошибки
- Определение шаблона в .cpp — линкер не найдёт инстанциации. Шаблоны живут в .h/.hpp.
typenameиtemplateв зависимых именах —T::typeтребуетtypename T::type.- Неявное копирование в шаблонах: параметр
Tпо значению копирует — беритеconst T&. - Не надо городить шаблон там, где хватит функции с
auto(C++14) или перегрузки. - Время компиляции: тяжёлые шаблоны раздувают сборку; в больших проектах применяют extern template.
Итог
Шаблоны — сердце STL и вся современная C++-библиотека. Начальный набор: шаблоны функций с выводом типов, шаблонные классы в заголовках, if constexpr вместо SFINAE, concepts в C++20 для понятных ошибок. Не нужно сразу писать метапрограммирование — сначала научитесь принимать шаблоны как «функцию, работающую с любым типом, который умеет нужные операции».
Специализация переменных и type_traits
Шаблонные константы тоже специализируются:
template<typename T> constexpr bool isPOD = std::is_trivially_copyable_v<T>;
template<> constexpr bool isPOD<MyPackedStruct> = true;
Вся <type_traits> — это шаблоны-вычисления: is_integral, is_same, is_constructible, enable_if — сотни готовых предикатов для концептов и if constexpr.
Вывод типов: auto и decltype
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
decltype(expr) — «тип этого выражения»; auto в C++14 умеет выводить и без него. Для сложных типов хвостовой возврат помогает читабельности.
Fold expressions: упаковка аргументов (C++17)
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // разворачивается в a1 + a2 + ...
}
template<typename... Args>
void printAll(Args... args) {
(std::cout << ... << args) << '\n'; // fold с <<
}
Без рекурсии — одна строка обрабатывает весь пакет.
Мини-проект: type-safe println
Напишите void println(fmt, args...) через variadic template + fold expression: заменяет {} в строке на аргументы по порядку. Это мини-версия std::format и отличное упражнение на пакеты и вывод типов.
Где шаблоны НЕ нужны
- Один используемый тип — обычная функция/класс, без шаблона.
- Два-три типа — перегрузки проще шаблона.
- Полиморфизм в рантайме — виртуальные функции/variant, не шаблоны.
- Шаблон ради шаблона — усложняет читаемость и время сборки.
Шаблоны — для кода, который реально работает с разными типами. Если типов один — шаблон лишний.