Заметки по языку с и с++

Шаблоны (templates) в C++ для начинающих: от функции до класса

1 просмотров

Введение

Шаблоны — это механизм обобщённого программирования в C++, позволяющий писать код, работающий с разными типами без дублирования. Вместо того чтобы писать отдельную функцию maxInt, maxDouble, maxString, можно написать одну шаблонную функцию max, и компилятор сгенерирует нужную версию для каждого использованного типа на этапе компиляции.

Шаблонные функции

#include <iostream>
#include <string>

// Шаблонная функция: T — параметр шаблона, подставляется компилятором
template <typename T>
T myMax(T a, T b)
{
    return (a > b) ? a : b;
}

int main()
{
    std::cout << myMax(3, 7) << std::endl;          // T = int
    std::cout << myMax(2.5, 1.1) << std::endl;       // T = double
    std::cout << myMax(std::string("abc"),
                        std::string("abd")) << std::endl; // T = std::string
    return 0;
}

Компилятор не компилирует «универсальную» функцию для всех типов сразу — для каждого набора типов, с которыми шаблон реально используется в коде, генерируется отдельная конкретная функция (этот процесс называется инстанцированием шаблона, instantiation). Если тип не поддерживает оператор >, при попытке вызвать myMax для этого типа компилятор выдаст ошибку именно в момент инстанцирования.

Шаблонные классы

#include <iostream>
#include <stdexcept>
#include <vector>

template <typename T>
class FixedStack
{
public:
    explicit FixedStack(size_t capacity) : capacity_(capacity)
    {
        data_.reserve(capacity_);
    }

    void push(const T& value)
    {
        if (data_.size() >= capacity_) {
            throw std::overflow_error("Стек заполнен");
        }
        data_.push_back(value);
    }

    T pop()
    {
        if (data_.empty()) {
            throw std::underflow_error("Стек пуст");
        }
        T value = data_.back();
        data_.pop_back();
        return value;
    }

    bool empty() const { return data_.empty(); }
    size_t size() const { return data_.size(); }

private:
    std::vector<T> data_;
    size_t capacity_;
};

int main()
{
    FixedStack<int> intStack(3);
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    std::cout << "Размер: " << intStack.size() << std::endl;
    while (!intStack.empty()) {
        std::cout << intStack.pop() << ' ';
    }
    std::cout << std::endl;

    FixedStack<std::string> stringStack(2);
    stringStack.push("hello");
    stringStack.push("world");
    std::cout << stringStack.pop() << ' ' << stringStack.pop() << std::endl;

    return 0;
}

Разбор примера

FixedStack<T> — обобщённый стек с ограниченной ёмкостью. Когда мы пишем FixedStack<int>, компилятор генерирует конкретный класс, где T заменён на int: поле data_ становится std::vector<int>, методы push/pop принимают и возвращают int. Точно так же FixedStack<std::string> создаёт отдельную полноценную реализацию для строк. Это и есть суть обобщённого программирования: один исходный код, множество независимых конкретных реализаций, сгенерированных компилятором.

Шаблонные параметры могут быть не только типами. Например, можно зафиксировать ёмкость стека как параметр шаблона (а не аргумент конструктора), используя нетиповой шаблонный параметр:

template <typename T, size_t Capacity>
class StaticStack
{
public:
    void push(const T& value)
    {
        if (size_ >= Capacity) {
            throw std::overflow_error("Стек заполнен");
        }
        data_[size_++] = value;
    }

    T pop()
    {
        if (size_ == 0) {
            throw std::underflow_error("Стек пуст");
        }
        return data_[--size_];
    }

private:
    std::array<T, Capacity> data_{};
    size_t size_ = 0;
};

// Использование: StaticStack<int, 10> stack;

Здесь Capacity — это значение, известное во время компиляции, что позволяет использовать std::array фиксированного размера вместо динамически выделяемого std::vector — выигрыш в производительности без потери выразительности кода.

Подводные камни и советы

  • Ошибки шаблонов выглядят пугающе. Если шаблон используется с неподходящим типом, сообщение об ошибке может растянуться на десятки строк, указывая всю цепочку инстанцирования. Читайте сообщение снизу вверх (или сверху вниз, в зависимости от компилятора) и ищите самую конкретную строку, где впервые что-то пошло не так.
  • Каждая используемая комбинация типов увеличивает размер бинарного файла (code bloat) — компилятор генерирует отдельный код для каждой инстанциации. Для больших шаблонных классов с тяжёлой логикой иногда выносят общую часть в нешаблонный базовый класс.
  • Используйте typename против class в объявлении шаблона — это синонимы, выбор обычно дело стиля команды.
  • Не путайте определение и объявление шаблона в разных файлах — в отличие от обычных функций, реализация шаблонной функции/метода обычно должна быть видна в каждой единице трансляции, где шаблон инстанцируется, поэтому шаблонный код чаще всего пишут целиком в заголовочных файлах (.h/.hpp), а не разделяют на .h/.cpp как обычные классы.
  • Концепты (C++20) позволяют ограничивать допустимые типы для шаблонного параметра и дают куда более читаемые сообщения об ошибках — это естественное продолжение темы шаблонов, см. отдельную статью про концепты.

Шаблоны — фундамент стандартной библиотеки C++ (std::vector, std::map, алгоритмы из <algorithm>), и понимание базового синтаксиса шаблонных функций и классов открывает дорогу к по-настоящему обобщённому, переиспользуемому коду.

================================================================================