Введение
До этого момента каждая функция в примерах цикла была написана для конкретного, фиксированного типа параметров — шаблоны позволяют написать одну функцию, работающую с произвольным типом, определяемым компилятором автоматически при каждом конкретном вызове. В этой статье разберём шаблоны функций — основу обобщённого программирования в C++.
Концепция
Шаблон функции объявляется с одним или несколькими параметрами шаблона (обычно типами, обозначаемыми условно как T), и компилятор генерирует конкретную, специализированную версию функции для каждого реально использованного в коде набора типов — этот процесс называется инстанцированием шаблона и происходит на этапе компиляции, а не во время выполнения программы, что отличает шаблоны C++ от обобщённого программирования через динамическую типизацию некоторых других языков. Вывод типа шаблона (template argument deduction) обычно позволяет компилятору самостоятельно определить нужный тип T из переданных аргументов, без необходимости явно указывать его при каждом вызове, хотя явное указание (function<int>(...)) всегда возможно и иногда необходимо, когда вывод типа не однозначен.
Пример кода
#include <iostream>
template<typename T> // объявление параметра шаблона T — может быть ЛЮБЫМ типом
T maxValue(T a, T b)
{
return (a > b) ? a : b;
}
int main()
{
std::cout << maxValue(3, 7) << std::endl; // T выводится как int — компилятор САМ определяет это
std::cout << maxValue(3.5, 2.1) << std::endl; // T выводится как double — ОТДЕЛЬНАЯ инстанциация
std::cout << maxValue<float>(1.0f, 2.0f) << std::endl; // ЯВНОЕ указание типа — иногда необходимо
// maxValue(3, 3.5); // ОШИБКА — неоднозначный вывод типа: int или double? Компилятор не угадывает
return 0;
}
// Шаблон с несколькими параметрами разных типов
#include <iostream>
template<typename T, typename U>
void printPair(T first, U second)
{
std::cout << "(" << first << ", " << second << ")" << std::endl;
}
int main()
{
printPair(42, "строка"); // T=int, U=const char*
printPair(3.14, true); // T=double, U=bool
return 0;
}
// Шаблон с ограничением через простое условие (до появления concepts, статья 549)
#include <type_traits>
template<typename T>
T add(T a, T b)
{
static_assert(std::is_arithmetic<T>::value, "T должен быть числовым типом"); // проверка НА ЭТАПЕ КОМПИЛЯЦИИ
return a + b;
}
Пояснения к коду
maxValue<T>(T a, T b) показывает простейший, классический шаблон функции — параметр T подставляется компилятором конкретным типом при каждом вызове, и для разных вызовов (maxValue(3, 7) с int, maxValue(3.5, 2.1) с double) компилятор генерирует ДВЕ отдельные, специализированные версии функции, хотя в исходном коде написана только одна, обобщённая версия. Закомментированный вызов maxValue(3, 3.5) показывает важное ограничение автоматического вывода типа — оба параметра шаблона T a, T b должны соответствовать ОДНОМУ И ТОМУ ЖЕ типу T, и попытка передать аргументы разных типов (int и double) делает вывод неоднозначным, требуя либо явного приведения одного из аргументов, либо явного указания типа шаблона. static_assert(std::is_arithmetic<T>::value, ...) показывает раннюю, до появления полноценных concepts (статья 549), технику ограничения допустимых типов шаблона — попытка инстанцировать add<T> с неподходящим типом (например, std::string) даст понятную ошибку компиляции с явно указанным сообщением, а не менее понятную, глубоко вложенную ошибку, возникающую где-то внутри реализации шаблона при попытке выполнить операцию +, не поддерживаемую этим типом.
Подводные камни
- Ожидание автоматического неявного преобразования типов между разными параметрами одного и того же параметра шаблона — как показано с закомментированным вызовом
maxValue(3, 3.5), компилятор не пытается автоматически найти «общий» тип для нескольких аргументов, соответствующих одному параметру шаблона, и явное указание типа или приведение одного из аргументов необходимо для разрешения такой неоднозначности. - Раздувание размера итогового исполняемого файла из-за инстанцирования шаблона для большого количества разных типов (так называемый «code bloat») — поскольку компилятор генерирует отдельную, полную версию кода функции для каждого реально использованного набора типов параметров шаблона, шаблонная функция, вызываемая с десятками разных типов в большом проекте, может существенно увеличить итоговый размер скомпилированного кода по сравнению с единственной, нешаблонной версией функции, и для случаев, где это становится значимой проблемой, стоит рассмотреть техники уменьшения этого эффекта (вынесение общей, не зависящей от конкретного типа логики в отдельную, нешаблонную функцию).
- Сложные, труднопонятные сообщения об ошибках компиляции при неверном использовании шаблона (до появления
static_assert/concepts) — без явного ограничения допустимых типов (какstatic_assertв примере) ошибка при попытке использовать шаблон с неподходящим типом обычно проявляется глубоко внутри реализации шаблона, в точке, где конкретная операция (как+для типа, не поддерживающего сложение) реально не компилируется, и итоговое сообщение об ошибке часто длинное, малопонятное и далёкое от реальной, корневой причины проблемы с точки зрения пользователя шаблона — явные ограничения (черезstatic_assertили, лучше, через concepts) существенно улучшают качество диагностики для пользователей шаблона. - Путаница между шаблонами и перегрузкой функций (function overloading) — хотя оба механизма позволяют использовать одно и то же имя функции для разных типов параметров, перегрузка требует явного, отдельного написания каждой конкретной версии функции для каждого набора типов, тогда как шаблон автоматически генерирует все нужные версии из единственного, обобщённого описания, и понимание этого фундаментального различия важно для выбора подходящего инструмента — перегрузка для случаев с действительно разной логикой для разных типов, шаблоны для случаев с одинаковой, обобщённой логикой, применимой к произвольному (или ограниченному определённым набором требований) типу.