Введение
Ранее разбирали шаблоны функций — шаблоны классов применяют ту же идею обобщённого программирования к целым классам, что и лежит в основе всех стандартных контейнеров библиотеки (std::vector<T>, std::map<K, V>, уже неоднократно использованных в предыдущих циклах статей без подробного объяснения их шаблонной природы).
Концепция
Шаблон класса объявляется аналогично шаблону функции, с параметрами шаблона, используемыми внутри определения класса для типов полей, параметров методов, и возвращаемых значений — каждое конкретное инстанцирование (MyContainer<int>, MyContainer<std::string>) фактически создаёт отдельный, независимый тип, хотя оба происходят из одного и того же шаблонного описания. В отличие от шаблонов функций, для шаблонов классов параметр шаблона обычно необходимо указывать явно при объявлении переменной (хотя начиная с C++17 вывод типа шаблона класса из аргументов конструктора во многих случаях стал возможен автоматически, аналогично выводу для функций).
Пример кода
#include <iostream>
template<typename T>
class Box {
public:
Box(T value) : m_value(value) {}
T getValue() const { return m_value; }
void setValue(T value) { m_value = value; }
private:
T m_value;
};
int main()
{
Box<int> intBox(42); // явное указание типа шаблона — ОБЯЗАТЕЛЬНО в более старых стандартах
Box<std::string> stringBox("Привет");
std::cout << intBox.getValue() << std::endl;
std::cout << stringBox.getValue() << std::endl;
Box deducedBox(3.14); // C++17 — вывод типа шаблона класса из аргумента конструктора, без явного <double>
return 0;
}
// Простой обобщённый контейнер — концептуальная демонстрация принципа std::vector
#include <iostream>
#include <stdexcept>
template<typename T>
class SimpleDynamicArray {
public:
SimpleDynamicArray(size_t capacity) : m_capacity(capacity), m_size(0)
{
m_data = new T[capacity];
}
~SimpleDynamicArray() { delete[] m_data; }
void add(T value)
{
if (m_size >= m_capacity) {
throw std::overflow_error("Контейнер заполнен"); // статья 540 — исключения
}
m_data[m_size++] = value;
}
T &operator[](size_t index) // статья 535 — перегрузка оператора [] для шаблонного класса
{
if (index >= m_size) {
throw std::out_of_range("Индекс вне диапазона");
}
return m_data[index];
}
size_t size() const { return m_size; }
private:
T *m_data;
size_t m_capacity;
size_t m_size;
};
int main()
{
SimpleDynamicArray<int> numbers(10);
numbers.add(1);
numbers.add(2);
numbers.add(3);
for (size_t i = 0; i < numbers.size(); ++i) {
std::cout << numbers[i] << " "; // 1 2 3
}
return 0;
}
Пояснения к коду
Box<T> показывает минимальный, базовый шаблон класса — параметр T используется и для типа поля m_value, и для типов параметров/возвращаемых значений методов, и каждое конкретное использование (Box<int>, Box<std::string>) компилятор обрабатывает как создание отдельного, специализированного типа со всеми методами, реализованными конкретно для этого типа. Box deducedBox(3.14) без явного <double> показывает современную возможность C++17 (CTAD, class template argument deduction) — компилятор способен сам вывести параметр шаблона класса из аргумента конструктора, аналогично давно работающему выводу типа для шаблонов функций, что упрощает синтаксис для многих типичных случаев использования. SimpleDynamicArray<T> показывает упрощённую, концептуальную демонстрацию принципа, лежащего в основе std::vector — обобщённый, типобезопасный контейнер, способный хранить элементы произвольного типа T, с методами добавления и индексированного доступа, написанными один раз и работающими корректно для любого конкретного типа, для которого этот шаблон будет инстанцирован.
Подводные камни
- Попытка разделить объявление и определение методов шаблона класса на заголовочный (
.h) и файл реализации (.cpp) тем же способом, что и для обычных, нешаблонных классов — поскольку компилятор должен видеть ПОЛНОЕ определение шаблона (включая тела всех методов) в каждой единице трансляции, где этот шаблон инстанцируется конкретным типом, обычное разделение на.h/.cpp(с объявлениями в заголовке и определениями в отдельном файле реализации) не работает напрямую для большинства шаблонов — реализация шаблонных методов обычно либо размещается непосредственно в заголовочном файле (наиболее распространённый подход), либо требует более сложных техник явного инстанцирования для специфичных случаев. - Ожидание автоматического полиморфного отношения между разными инстанциациями одного и того же шаблона (например, ожидание, что
Box<int>можно неявно использовать там, где ожидаетсяBox<double>, аналогично отношению между производным и базовым классом) —Box<int>иBox<double>являются полностью отдельными, независимыми типами без какого-либо автоматического отношения наследования между ними, несмотря на происхождение из общего шаблона, и попытка передать объект одного инстанцирования туда, где ожидается другое, является ошибкой типов, требующей явного, осознанного решения (если такое преобразование действительно нужно и осмысленно для конкретного случая). - Создание избыточно сложных шаблонов классов с большим числом параметров шаблона без действительно ясной, обоснованной необходимости в каждом из них — каждый дополнительный параметр шаблона увеличивает сложность инстанцирования и понимания кода, использующего этот шаблон, и хорошая практика — минимизировать количество параметров шаблона до действительно необходимого минимума, рассматривая параметры со значениями по умолчанию (
template<typename T, typename Allocator = std::allocator<T>>, как у реальных стандартных контейнеров) для менее часто настраиваемых аспектов. - Игнорирование того, что каждая отдельная инстанциация шаблона класса — это полностью независимая компиляция всего кода методов для конкретного типа, что означает, что ошибки, специфичные для конкретного типа (например, попытка использовать оператор, не поддерживаемый этим конкретным типом, в одном из методов, который реально вызывается только для этого конкретного инстанцирования), обнаруживаются только при реальной попытке использовать шаблон с этим конкретным типом, а не при первоначальном написании самого шаблона — это отличается от обычных, нешаблонных классов, где все ошибки обнаруживаются единократно, при компиляции самого класса, независимо от того, как он будет в дальнейшем использоваться.