Generics и типизация в C++: почему это не «просто шаблоны»?
Когда разработчик переходит с Python или Java на C++, его часто пугает слово «шаблон» (template). В голове возникают ассоциации с бесконечными угловыми скобками, непонятными ошибками компиляции и магией std::vector. На самом деле generics в C++ (или обобщённое программирование) — это мощнейший инструмент, который позволяет писать код один раз, а использовать его с любыми типами данных. И да, это совсем не то же самое, что дженерики в Java или C#.
В этой статье мы разберём, как работает типизация в C++ через шаблоны, чем отличаются шаблоны классов от шаблонов функций, и зачем в C++20 добавили концепты. Будет много примеров — от простых до тех, которые реально пригодятся в продакшене. Поехали!
Что такое generics (шаблоны) в C++?
Если совсем просто: шаблоны в C++ — это способ написать код, который работает с любым типом данных, не теряя при этом производительности. В отличие от динамической типизации Python, где тип проверяется в рантайме, C++ генерирует отдельную версию функции или класса для каждого используемого типа на этапе компиляции. Это называется статической типизацией с параметризацией.
Синтаксически шаблон объявляется с помощью ключевого слова template и угловых скобок с параметрами:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
Здесь typename T — это параметр шаблона. Вы можете использовать class вместо typename — разницы нет. Когда вы пишете max(3, 7), компилятор автоматически подставляет int вместо T и создаёт функцию int max(int, int). Если напишете max(3.14, 2.71) — получите версию для double. Всё просто и эффективно.
Шаблоны функций: первый шаг к generics
Начнём с малого — шаблоны функций в C++. Это идеальный способ избежать дублирования кода, когда логика одинакова, а типы разные.
Пример: функция, которая печатает элемент в консоль (полезно для отладки):
#include <iostream>
#include <string>
template <typename T>
void print(const T& value) {
std::cout << value << std::endl;
}
int main() {
print(42); // int
print(3.1415); // double
print("Hello"); // const char*
print(std::string("Generics!")); // std::string
return 0;
}
Обратите внимание: мы используем const T& — это типизация в C++ с передачей по константной ссылке, чтобы избежать копирования больших объектов. Для встроенных типов (int, double) это не критично, но привычка хорошая.
А что, если нужно передать два аргумента разных типов? Легко:
template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
return a + b;
}
// В C++14 и новее можно просто:
template <typename T1, typename T2>
auto add(T1 a, T2 b) {
return a + b;
}
Здесь auto выводит тип возвращаемого значения автоматически. Это и есть generics в действии — один код, бесконечное количество комбинаций типов.
Шаблоны классов: строим универсальные структуры
Теперь перейдём к шаблонам классов в C++. Это основа стандартной библиотеки (STL): std::vector<T>, std::map<Key, Value>, std::optional<T> — все они реализованы через шаблоны.
Давайте напишем простой, но показательный пример — шаблонный класс «Пара» (аналог std::pair):
template <typename First, typename Second>
class Pair {
private:
First first_;
Second second_;
public:
Pair(const First& f, const Second& s) : first_(f), second_(s) {}
First getFirst() const { return first_; }
Second getSecond() const { return second_; }
void print() const {
std::cout << "(" << first_ << ", " << second_ << ")" << std::endl;
}
};
int main() {
Pair<int, std::string> student(1, "Alice");
student.print(); // (1, Alice)
Pair<double, double> coordinates(48.8566, 2.3522);
coordinates.print(); // (48.8566, 2.3522)
return 0;
}
Обратите внимание: типизация в C++ здесь строгая — вы не сможете передать int туда, где ожидается std::string, на этапе компиляции. Это огромный плюс по сравнению с динамическими языками.
Специализация шаблонов: когда общее решение не подходит
Иногда нужно, чтобы для конкретного типа шаблон работал иначе. Это называется специализация шаблонов. Бывает полная (для одного типа) и частичная (для группы типов).
Пример: мы хотим, чтобы для типа bool наша пара хранила значения более эффективно (упаковывала биты). Полная специализация:
// Основной шаблон
template <typename T>
class Storage {
public:
void store(const T& value) { /*...*/ }
};
// Полная специализация для bool
template <>
class Storage<bool> {
private:
unsigned char bits_;
public:
void store(bool value) {
bits_ = value ? 1 : 0;
std::cout << "Stored bool efficiently!" << std::endl;
}
};
Это мощный приём, но злоупотреблять им не стоит — усложняет чтение кода. В современных C++ (17/20) часто можно обойтись if constexpr.
Концепты (C++20): типизация на стероидах
Долгое время главной проблемой generics в C++ были чудовищные ошибки компиляции. Если вы передали в шаблон тип, у которого нет нужного оператора, компилятор выдавал километровые сообщения, в которых трудно было найти суть. Решение пришло в C++20 — концепты.
Концепты в C++20 позволяют явно указать, какими свойствами должен обладать тип-параметр. Это делает код самодокументированным, а ошибки — понятными.
Синтаксис:
#include <concepts