Generics в C++: зачем нужны и как использовать для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Generics в C++: зачем нужны и как использовать

Каждый программист рано или поздно сталкивается с ситуацией, когда нужно написать функцию, которая работает с разными типами данных. Например, функция поиска максимального элемента — она должна одинаково хорошо работать и с int, и с double, и с string. Без специальных средств пришлось бы писать отдельную реализацию для каждого типа. Именно здесь на помощь приходят generics (обобщённые типы) или, как их называют в C++, шаблоны.

В C++ механизм шаблонов — это основа обобщённого программирования. Он позволяет писать код, который работает с любым типом данных, не жертвуя производительностью. В этой статье мы разберём, что такое generics в C++, зачем они нужны и как их использовать на практике. Материал рассчитан на начинающих, поэтому мы будем двигаться от простого к сложному.

Что такое шаблоны (generics) в C++?

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

Представьте себе форму для выпечки печенья. Форма одна, но печенье можно делать из разного теста: песочного, шоколадного, овсяного. Шаблон — это форма, а тип (int, double, string) — это тесто. Вы один раз описываете алгоритм, а компилятор автоматически создаёт версии для нужных типов.

Шаблоны бывают двух видов:

  • Шаблоны функций — для создания обобщённых функций.
  • Шаблоны классов — для создания обобщённых классов (например, контейнеров).

Шаблоны функций: первый шаг к обобщённому коду

Начнём с самого простого — шаблонной функции. Допустим, нам нужна функция, которая возвращает максимум из двух значений. Вот как она выглядит без шаблонов:

int max_int(int a, int b) {
    return (a > b) ? a : b;
}

double max_double(double a, double b) {
    return (a > b) ? a : b;
}

Код дублируется. С шаблоном это решается элегантно:

#include <iostream>
#include <string>

template <typename T>
T max_value(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << max_value(10, 20) << std::endl;           // 20
    std::cout << max_value(3.14, 2.71) << std::endl;       // 3.14
    std::cout << max_value(std::string("apple"), 
                             std::string("banana")) << std::endl; // banana
    return 0;
}

Ключевое слово template говорит компилятору, что это шаблон. <typename T> — это список параметров шаблона. T — условное имя типа, который будет подставлен позже. Вместо typename можно использовать class — разницы нет:

template <class T>
T max_value(T a, T b) {
    return (a > b) ? a : b;
}

Когда вы вызываете max_value(10, 20), компилятор видит, что аргументы — int, и автоматически создаёт версию функции для int. Этот процесс называется выводом типов. Если нужно, можно явно указать тип:

std::cout << max_value<double>(5, 7.5) << std::endl; // 7.5

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

Шаблоны классов: создаём универсальные контейнеры

Шаблоны классов — ещё более мощный инструмент. Самый яркий пример — это контейнеры из стандартной библиотеки (STL): std::vector<int>, std::map<std::string, int> и так далее. Все они реализованы как шаблонные классы.

Давайте напишем простой шаблонный класс «Пара», который хранит два значения любого типа:

#include <iostream>
#include <string>

template <typename T1, typename T2>
class Pair {
private:
    T1 first;
    T2 second;
public:
    Pair(T1 f, T2 s) : first(f), second(s) {}

    T1 getFirst() const { return first; }
    T2 getSecond() const { return second; }

    void print() const {
        std::cout << "(" << first << ", " << second << ")" << std::endl;
    }
};

int main() {
    Pair<int, double> p1(10, 3.14);
    p1.print(); // (10, 3.14)

    Pair<std::string, int> p2("Age", 25);
    p2.print(); // (Age, 25)

    return 0;
}

Мы использовали два параметра типа: T1 и T2. Это позволяет хранить пару из разных типов. Обратите внимание: при создании объекта мы явно указываем типы в угловых скобках: Pair<int, double>. Для классов вывод типов (как для функций) не работает — типы нужно указывать всегда.

Шаблоны классов могут иметь не только параметры-типы, но и параметры-значения. Например, можно создать массив фиксированного размера:

template <typename T, int Size>
class FixedArray {
private:
    T data[Size];
public:
    int getSize() const { return Size; }
    T& operator[](int index) { return data[index]; }
};

int main() {
    FixedArray<double, 5> arr;
    arr[0] = 1.5;
    std::cout << arr[0] << std::endl; // 1.5
    std::cout << arr.getSize();        // 5
    return 0;
}

Блоги

Рекомендаций по книгам