Шаблон это рецепт, по которому компилятор генерирует код для конкретных типов. std::vector<int> и std::vector<std::string> два разных класса, созданных из одного шаблона. Ошибки типов ловятся при компиляции, а сгенерированный код так же быстр, как написанный руками.
Инстанцирование и заголовки
Компилятор генерирует код в момент использования (инстанцирования), поэтому ему нужно видеть определение шаблона, а не только объявление. Отсюда правило: шаблоны определяют в заголовочных файлах. Каждая единица трансляции инстанцирует свои копии, а линковщик выкидывает дубликаты. Если инстанцирований много, растут время сборки и размер бинарника (code bloat). Помогают extern template и явное инстанцирование в одном .cpp.
Вывод типов
template <class T> void f(T x);
const int& r = 42;
f(r); // T = int: ссылка и верхний const отбрасываются
template <class T> void g(T& x);
g(r); // T = const int
Тот же механизм работает для auto. С C++17 вывод работает и для шаблонов классов (CTAD): std::vector v{1, 2, 3} даёт std::vector<int>, std::pair p(1, 2.5) даёт std::pair<int, double>.
Специализация
template <class T> struct Hash { std::size_t operator()(const T&) const; };
template <> struct Hash<bool> { /* полная специализация */ };
template <class T> struct Hash<T*> { /* частичная специализация для указателей */ };
Классы можно специализировать полностью и частично, функции только полностью, а вместо частичной специализации функций используют перегрузку. При разрешении перегрузки нешаблонная функция с точным совпадением предпочтительнее шаблона.
Зависимые имена
Внутри шаблона имя, зависящее от параметра, компилятор не может разобрать до инстанцирования (двухфазный поиск имён):
typename T::value_type x;: безtypenameкомпилятор считаетT::value_typeзначением, а не типом;- члены зависимой базы видны только через
this->илиBase<T>::; - шаблонный член зависимого типа вызывают с
template:obj.template get<0>().
Variadic templates и fold-выражения
template <class... Ts>
auto sum(Ts... xs) { return (xs + ... + 0); } // C++17 fold
sizeof...(Ts); // число аргументов
SFINAE и концепты
SFINAE (substitution failure is not an error): если подстановка типов в сигнатуру шаблона не удалась, кандидат молча выбрасывается из перегрузки. На этом строились std::enable_if и трюки с decltype, но код получался нечитаемым, а ошибки огромными.
C++20 дал концепты: именованные требования к типам.
template <class T>
concept Number = std::integral<T> || std::floating_point<T>;
template <Number T>
T twice(T x) { return x * 2; }
template <class T>
requires requires(T a) { a.size(); }
auto length(const T& c) { return c.size(); }
Плюсы: требования видны в сигнатуре, ошибки короткие («не выполнен концепт Number»), перегрузка по концептам выбирает более строгий вариант. В стандартной библиотеке десятки готовых концептов в <concepts> и <iterator>: std::same_as, std::convertible_to, std::random_access_iterator, std::ranges::range.
if constexpr
if constexpr (cond) вычисляет условие при компиляции, и в шаблоне ветка с ложным условием не инстанцируется. Это заменяет многие перегрузки и специализации:
template <class T>
std::string describe(const T& x) {
if constexpr (std::is_arithmetic_v<T>) return std::to_string(x);
else return std::string(x);
}
Частые ошибки
- Определение шаблона в
.cppи ошибка линковки «undefined reference». - Забытый
typenameперед зависимым типом. - Обращение к члену зависимой базы без
this->. - Попытка частично специализировать функцию.
- Слишком общий шаблон с forwarding-ссылкой, перехватывающий вызовы, предназначенные копирующему конструктору.
Что спросят на собесе
- Почему шаблоны пишут в заголовках.
- Специализация полная и частичная, перегрузка шаблонов.
- SFINAE и чем концепты лучше.
- Variadic templates и fold-выражения.
typename,template,this->в зависимом контексте.