Как компилятор превращает шаблон в код
Шаблон — это не функция и не класс. Это «шаблон-заготовка», из которой компилятор при использовании создаёт конкретные версии. Этот процесс называется инстанцированием.
template<class T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int i = maximum(3, 7); // T = int
double d = maximum(2.5, 1.8); // T = double
char c = maximum('a', 'z'); // T = char
}
maximum(3, 7) — из типов
аргументов определяет T = int — и генерирует отдельную функцию
int maximum(int, int). Для double — свою. Получается три
независимые функции, как если бы вы написали их вручную. Код шаблона при этом — один.
Интерактивно: что создаёт компилятор
Выберите типы, и увидите, какие конкретные функции сгенерирует компилятор.
Специализация: особое поведение для особых типов
Иногда для конкретного типа нужно не общее решение из шаблона, а специальное.
Например, для типа bool функция maximum должна возвращать
логическое ИЛИ, а не «большее из двух» (у которого нет смысла для bool).
// Общий шаблон
template<class T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
// Явная специализация для типа bool
template<>
bool maximum<bool>(bool a, bool b) {
return a || b; // «максимум» для bool — это «или»
}
std::vector<bool>: он реализован как специализация, которая хранит
биты, а не байты.
Частичная специализация
Для шаблонов классов можно специализировать не конкретный тип, а целую категорию типов.
template<class T>
class Storage {
public:
void describe() { cout << "обычный объект"; }
};
// Частичная специализация для указателей
template<class T>
class Storage<T*> {
public:
void describe() { cout << "указатель на объект"; }
};
<int> побеждает частичную
<T*>, а та — общий шаблон <T>.
Вариативные шаблоны: любое количество аргументов
Иногда заранее неизвестно, сколько аргументов будет у функции. Variadic templates позволяют принимать их любое количество — и разных типов.
// Базовый случай — один аргумент
template<class T>
T sum(T value) {
return value;
}
// Рекурсивный случай: отделяем первый аргумент
// и складываем его с суммой остальных
template<class T, class... Rest>
T sum(T first, Rest... rest) {
return first + sum(rest...);
}
int main() {
cout << sum(1, 2, 3, 4, 5) << endl; // 15
cout << sum(1.5, 2.5) << endl; // 4.0
cout << sum(1, 2.5, 3) << endl; // выведет ошибку — типы разные
}
class... Rest — «пакет типов» (может быть
пустым). Rest... rest — «пакет аргументов». Выражение rest...
«раскрывает» пакет: подставляет через запятую все аргументы.
Интерактивно: раскрытие пакета
Нажимайте «Шаг», чтобы увидеть, как рекурсивно раскрывается пакет аргументов.
template<class... Args>
auto sum(Args... args) {
return (args + ...); // раскрывается в (a + (b + (c + ...)))
}
SFINAE: подстановка не является ошибкой
Расшифровывается как Substitution Failure Is Not An Error. Это ключевой принцип, позволяющий выбирать нужную перегрузку функции, отбрасывая неподходящие варианты без ошибки компиляции.
Классический пример: разные ветки для чисел и строк
#include <type_traits>
// Версия для чисел: T — целое или вещественное
template<class T>
typename enable_if<is_arithmetic<T>::value>::type
process(T value) {
cout << "обработка числа: " << value * 2 << endl;
}
// Версия для строк: T — строковый тип
template<class T>
typename enable_if<is_same<T, string>::value>::type
process(T value) {
cout << "обработка строки: " << value.size() << " символов" << endl;
}
int main() {
process(42); // вызовет первую версию
process(3.14); // тоже первую
process(string("hi")); // вторую
}
Интерактивно: как компилятор выбирает перегрузку
Выберите тип аргумента — увидите, какие кандидаты отбрасываются по SFINAE, а какой выбирается в итоге.
Концепты (C++20): SFINAE по-человечески
SFINAE работает, но читать его тяжело. В C++20 появились концепты — способ записать то же самое в удобном виде.
SFINAE (C++11)
template<class T>
typename enable_if<
is_arithmetic<T>::value,
T
>::type
double_it(T x) {
return x * 2;
}
Concepts (C++20)
template<arithmetic T>
T double_it(T x) {
return x * 2;
}
std::string не удовлетворяет концепту
arithmetic».
Что выбрать: шпаргалка
| Задача | Инструмент |
|---|---|
| Обобщённый алгоритм для разных типов | Обычный шаблон template<class T> |
| Особое поведение для конкретного типа | Явная специализация template<> |
| Особое поведение для категории типов | Частичная специализация template<class T> class X<T*> |
| Произвольное число аргументов | Variadic template template<class... Args> |
| Условная перегрузка (C++11) | std::enable_if + SFINAE |
| Условная перегрузка (C++20) | Концепты template<concept> |
Задание
- Напишите шаблонную функцию
print(const T& value). Убедитесь, что она работает дляint,double,string. - Добавьте явную специализацию
printдляbool, которая выводит «true» / «false» вместо 1 / 0. - Напишите рекурсивный variadic-шаблон
count_args(...), который возвращает количество аргументов. - Напишите variadic-шаблон
print_all(...), выводящий каждый аргумент на новой строке. - Реализуйте SFINAE-функцию
length, которая для строк возвращаетsize(), а для чисел — количество цифр. - Перепишите предыдущую задачу через концепты C++20.
- Что произойдёт при инстанцировании
sum(1, 2.5, 3)? Почему?