Шаблон (template) в C++ — это заготовка функции или класса, в которой тип (а иногда и значение) вынесен в параметр. По этой заготовке компилятор сам создает конкретный код для каждого набора аргументов, например maxOf<int> и maxOf<double>. Ниже разберем шаблоны функций и классов, специализацию, концепты C++20 и типовые ошибки, а в конце коротко сравним с generics в C#.
Содержание
- Минимальный рабочий пример
- Как это работает: разбор примера
- Типовая ошибка: вывод типа не сошелся
- Специализация: особая версия для особых типов
- Концепты C++20: ограничения на тип
- Где хранить шаблоны и что делать, если не собирается
- Врезка: шаблоны C++ и generics C
- Выводы
- Где применяется / связь с практикой
- FAQ
Сначала о границах «Си-семейства». Шаблоны есть только в C++. В языке C их нет: ближайшее средство — макросы и _Generic из C11, который выбирает выражение по типу аргумента, но новый код по типу не генерирует. В C# есть generics — похожий по цели, но другой механизм. А «шаблоны» в C#-документации про is и switch — это сопоставление с образцом (pattern matching), к обобщенному программированию оно отношения не имеет.
Мини-словарь, чтобы не путаться дальше:
| Термин | Что это | Пример |
|---|---|---|
| Параметр шаблона | «Дырка» в заготовке: тип или значение | typename T, int N |
| Аргумент шаблона | Что подставили вместо параметра | int, 3 |
| Инстанцирование | Генерация конкретного кода по шаблону при компиляции | maxOf<int> |
| Специализация | Отдельная версия шаблона для особых аргументов | версия для const char* |
Минимальный рабочий пример
Один файл, который компилируется и запускается как есть (проверено в Apple clang 21, режим по умолчанию C++14; в GCC и MSVC работает так же):
#include <iostream>
#include <string>
// Шаблон функции: T выводится из аргументов
template <typename T>
T maxOf(T a, T b) {
return (a < b) ? b : a;
}
// Шаблон класса: T - тип, N - нетиповой параметр (размер)
template <typename T, int N>
class FixedStack {
T data[N];
int size = 0;
public:
bool push(const T& v) {
if (size == N) return false; // переполнение - не пишем за границу
data[size++] = v;
return true;
}
T top() const { return data[size - 1]; }
int count() const { return size; }
};
int main() {
std::cout << maxOf(3, 7) << "\n"; // T = int
std::cout << maxOf(2.5, 1.5) << "\n"; // T = double
std::cout << maxOf<std::string>("kiwi", "apple") << "\n";
FixedStack<std::string, 2> s;
s.push("a");
s.push("b");
bool ok = s.push("c"); // места нет
std::cout << s.top() << " " << s.count() << " " << ok << "\n";
}
В консоли появится:
7
2.5
kiwi
b 2 0
Как это работает: разбор примера
Шаблон функции. Строка template <typename T> объявляет параметр типа T. Когда вы пишете maxOf(3, 7), компилятор выводит T = int из аргументов и инстанцирует функцию int maxOf(int, int). Для maxOf(2.5, 1.5) появится вторая, независимая функция для double. Ключевые слова typename и class в списке параметров взаимозаменяемы.
Явный аргумент. В третьей строке тип указан вручную: maxOf<std::string>. Без этого T вывелся бы как const char*, и a < b сравнивал бы адреса строк в памяти, а не текст. Шаблон подставляет тип буквально.
Шаблон класса. У FixedStack два параметра: тип элементов T и нетиповой параметр N — целое, известное на этапе компиляции. FixedStack<std::string, 2> и FixedStack<std::string, 3> — два разных типа. Для шаблона класса до C++17 аргументы указывают всегда; с C++17 компилятор умеет выводить их из конструктора (CTAD), например std::pair p(1, 2.0).
Важная граница модели: шаблон сам по себе не код, а заготовка. Пока его никто не использовал с конкретными аргументами, машинного кода нет, а многие ошибки в теле шаблона всплывут только при инстанцировании.
Типовая ошибка: вывод типа не сошелся
Неверно — аргументы разных типов при одном параметре T:
template <typename T>
T maxOf(T a, T b) { return (a < b) ? b : a; }
int main() { return maxOf(1, 2.5) > 0 ? 0 : 1; }
Результат — ошибка компиляции. Clang 21 пишет no matching function for call to 'maxOf' и поясняет deduced conflicting types for parameter 'T' ('int' vs. 'double'). У GCC и MSVC формулировка другая, но суть та же: из 1 вышел int, из 2.5 — double.
Исправление — одно из трех, по задаче:
— указать тип явно: maxOf<double>(1, 2.5);
— привести аргумент: maxOf(1.0, 2.5);
— ввести два параметра template <typename A, typename B> и вернуть общий тип через std::common_type_t<A, B> — если смешанные типы действительно нужны.
Специализация: особая версия для особых типов
Иногда общий алгоритм для конкретного типа неверен. Классика — сравнение C-строк: a == b для const char* сравнивает адреса. Тогда пишут полную специализацию — отдельную версию для конкретного аргумента. Для шаблонов классов есть еще частичная специализация — версия для целого семейства типов, например для всех указателей.
#include <iostream>
#include <cstring>
template <typename T>
bool isEqual(T a, T b) { return a == b; }
// Полная специализация для C-строк: сравниваем содержимое, а не адреса
template <>
bool isEqual<const char*>(const char* a, const char* b) {
return std::strcmp(a, b) == 0;
}
// Частичная специализация (только для шаблонов классов): все указатели
template <typename T>
struct TypeName { static const char* get() { return "value"; } };
template <typename T>
struct TypeName<T*> { static const char* get() { return "pointer"; } };
int main() {
char buf[] = "otus";
const char* p = buf;
std::cout << isEqual(p, "otus") << "\n"; // 1
std::cout << TypeName<int>::get() << " "
<< TypeName<int*>::get() << "\n"; // value pointer
}
Программа выведет 1 и value pointer. Без специализации первая строка дала бы 0: buf и литерал "otus" лежат по разным адресам. Шаблоны функций частично специализировать нельзя — для них вместо этого пишут обычную перегрузку.
Концепты C++20: ограничения на тип
Без ограничений шаблон принимает любой тип, а ошибка всплывает где-то в глубине тела. Концепт — именованное требование к типу, которое проверяется до инстанцирования. Раньше то же делали громоздкими SFINAE и static_assert. Пример требует -std=c++20 (GCC 10+, Clang 10+, MSVC 2019 16.3+ с /std:c++20):
#include <concepts>
#include <iostream>
// Свой концепт: тип поддерживает сложение и результат того же типа
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <Addable T>
T sum3(T a, T b, T c) { return a + b + c; }
// Стандартный концепт из <concepts>
template <std::integral T>
T half(T x) { return x / 2; }
int main() {
std::cout << sum3(1, 2, 3) << "\n"; // 6
std::cout << sum3(0.5, 0.25, 0.25) << "\n"; // 1
std::cout << half(7) << "\n"; // 3
// half(7.0); // не скомпилируется: double не удовлетворяет std::integral
}
Вывод: 6, 1, 3. Если раскомментировать half(7.0), Clang сообщит прямо в точке вызова: because 'double' does not satisfy 'integral'. Это главный практический выигрыш концептов — понятная ошибка и явный контракт в сигнатуре.
Где хранить шаблоны и что делать, если не собирается
Определение шаблона обычно целиком кладут в заголовочный файл: компилятору нужно видеть тело в каждой единице трансляции, где шаблон инстанцируется. Типовые симптомы:
| Симптом | Вероятная причина | Что делать |
|---|---|---|
undefined reference / undefined symbol при линковке |
Тело шаблона в .cpp, а используется в другом файле |
Перенести тело в заголовок или сделать явное инстанцирование template int maxOf<int>(int, int); |
deduced conflicting types |
Аргументы разных типов при одном T |
Явный аргумент, приведение или два параметра |
| Длинная ошибка из глубины тела шаблона | Тип не поддерживает нужную операцию | Добавить концепт (C++20) или static_assert |
concept не распознается |
Компилятор в режиме ниже C++20 | Флаг -std=c++20 или /std:c++20 |
Врезка: шаблоны C++ и generics C
Цель у них общая — один код для многих типов, но устроены они по-разному:
| Свойство | Шаблоны C++ | Generics C# |
|---|---|---|
| Когда создается конкретный код | При компиляции, отдельно для каждого набора аргументов | Обобщенный тип хранится в сборке; конкретизация во время выполнения (JIT/AOT) |
| Параметры-значения | Есть (int N) |
Нет, только типы |
| Специализация | Полная и частичная | Нет |
| Ограничения | Концепты C++20 (раньше SFINAE) | where T : ... |
| Проверка тела | Во многом при инстанцировании | Сразу, по ограничениям |
Аналог maxOf на C#: static T MaxOf<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) < 0 ? b : a;. Без ограничения where компилятор C# не даст вызвать CompareTo, потому что проверяет тело заранее, а не при подстановке типа.
Выводы
- Шаблон C++ — заготовка функции или класса с параметрами-типами и параметрами-значениями; конкретный код компилятор создает при инстанцировании.
- Для шаблонов функций типы обычно выводятся из аргументов; при конфликте типов нужен явный аргумент или второй параметр.
- Специализация нужна, когда общий алгоритм неверен для конкретного типа; частичная доступна только шаблонам классов.
- Концепты C++20 задают требования к типу в сигнатуре и дают ошибку в точке вызова.
- Generics C# решают похожую задачу, но без параметров-значений и специализации; pattern matching в C# — не шаблоны.
Где применяется / связь с практикой
Освойте тему на практике
На шаблонах построена стандартная библиотека C++: std::vector, std::map, std::sort, умные указатели. Уверенно читать и писать шаблонный код нужно почти в любом C++-проекте, от игровых движков до высоконагруженных сервисов. Системно пройти язык от основ до шаблонов, STL и современных стандартов можно на курсе «C++ Developer. Basic». Попробовать формат и задать вопросы преподавателям — на бесплатных открытых уроках.
FAQ
Замедляют ли шаблоны программу?
Обычно нет: код генерируется при компиляции и оптимизируется как обычный. Цена — дольше сборка и больше размер бинарника при многих инстанцированиях.
Чем шаблон отличается от макроса #define?
Макрос — текстовая подстановка препроцессора без проверки типов. Шаблон обрабатывает компилятор с учетом типов, областей видимости и перегрузки.
Можно ли передать в шаблон строку как значение?
Строковый литерал напрямую — нет. С C++20 можно передать объект структурного типа, например свою обертку над массивом символов, но это продвинутый прием.



