Добавить в избранное

Шаблоны в C++: функции, классы и основы обобщённого программирования

Чтение ≈ 5 мин

Шаблоны C++ с нуля: шаблонные функции и классы, вывод типов, специализация, variadic templates, concepts из C++20 и почему вся STL построена на шаблонах.

Зачем нужны шаблоны

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

Шаблоны — для кода, который реально работает с разными типами. Если типов один — шаблон лишний.

Поделиться
Понравился материал?

Комментарии

Добавить комментарий