Шаблоны (template) - это механизм языка C++, который позволяет писать обобщённый универсальный код, добавляя функциям и классам набор дополнительных параметров. Для каждого уникального набора параметров компилятор сгенерирует отдельную версию функции/класса, где эти параметры станут константами и типами данных. Шаблон - буквально конструктор кода программы, при помощи которого можно получить функции, классы или методы с нужными свойствами на основе одного общего шаблона.
Шаблоны - основной инструмент метапрограммирования, т.е. программирования, в котором программа пишет "сама себя". В отличие от макросов на #define
шаблоны являются более безопасным и удобным инструментом, который лучше оптимизируется компилятором.
Создание и вызов #
Шаблонная сущность создаётся следующим образом:
template <параметры...>
// далее код как обычно
Шаблонная функция #
Создание:
template <параметры...>
void foo(...) {
}
Шаблонная функция должна быть одновременно объявлена и определена, то есть должна находиться только в .h или только в .cpp файле - разделять нельзя
Вызов:
foo<аргументы...>(...);
Шаблонный класс #
Объявление:
template <параметры...>
class Foo {
};
Создание объекта:
Foo<аргументы...> foo;
Если класс нужно разделить на объявление и определение, например чтобы разбить на файлы, то синтаксис будет такой:
// .h
template <параметры...>
class Foo {
public:
void bar();
};
// .cpp
template <параметры...>
void Foo<аргументы...>::bar() {
}
Более понятно будет на конкретном примере:
// .h
template <int a, int b>
class Foo {
public:
void bar();
};
// .cpp
template <int a, int b>
void Foo<a, b>::bar() {
}
Компилятор создаёт отдельный класс для каждого уникального набора параметров шаблона, причём имя класса теперь неразрывно связано с шаблоном и не существует самостоятельно, т.е. с точки зрения компилятора например Foo<1, 1>
и Foo<1, 2>
это разные классы.
Параметры шаблона #
Параметром шаблона может являться переменная любого типа, тип данных или даже класс.
Переменная #
Переменная, как параметр шаблона, может иметь любой тип. На деле она является не переменной, а константой, причём известной на этапе компиляции: для компилятора это такая же "жёсткая" константа, как constexpr
или #define
, что позволяет сильно оптимизировать код.
На практике часто используется для оптимизации условий и свитчей, создания массивов указанной длины и прочих моментов, где нужна константа:
template <int size, bool mode>
class Foo {
public:
void test() {
if (mode) {
// код 1
} else {
// код 2
}
}
int buffer[size];
};
В отличие от внешних констант, шаблон позволяет создать несколько сущностей с разными "настройками" в рамках одной программы:
Foo<10, true> foo1;
Foo<10, true> foo2;
Foo<5, false> foo3;
Шаблонный класс является шаблоном, по которому компилятор сделает новый класс, по которому дальше создаст объект. В примере выше у нас в программе будет существовать два класса, примерно вот так они будут выглядеть после раскрытия шаблона и оптимизации:
// Foo<10, true>
template <int size, bool mode>
class Foo {
public:
void test() {
// код 1
}
int buffer[10];
};
// Foo<5, false>
class Foo {
public:
void test() {
// код 2
}
int buffer[5];
};
Если мы захотим сделать указатель или ссылку на объект шаблонного класса, то нужно не забывать, что класс как тип данных теперь должен включать в себя параметры шаблона:
Foo<10, true> foo;
Foo<10, true>* fooP = &foo;
Foo<10, true>& fooRef = foo;
Тип данных/класс #
Параметром шаблона может быть тип данных, делается это через ключевое слово typename
. Указанный далее "синоним" типа данных (обычно используют букву T
) можно использовать как тип данных далее в коде - компилятор буквально подставит вместо него указанный тип. Вот например функция, которая перемножает два числа и возвращает результат:
template <typename T>
T mult(T a, T b) {
return a * b;
}
Вернёмся чуть назад. В уроке про функции мы рассматривали перегруженные функции: для того, чтобы функция умножения двух чисел корректно работала для разных типов данных, надо вручную написать нужные варианты этой функции. Например, нам нужна такая функция отдельно для целых и для float
чисел:
int mult(int a, int b) { // #1
return a * b;
}
float mult(float a, float b) { // #2
return a * b;
}
А вот версия с шаблоном является универсальной перегруженной функцией для любого тип данных!
Например, можно вызвать mult<int>(3, 4)
- и компилятор сделает нам функцию, такую же как #1. Или взывать mult<float>(4, 5)
- компилятор сделает функцию как #2, то есть результат мы получим как float
.
Шаблон - это полноценный конструктор кода, то есть мы можем сделать например вот так:
template<typename Tres, typename T>
Tres square(T a) {
return (Tres)a * a;
}
Функция возводит число в квадрат, но мы можем указать возвращаемый тип и тип аргумента. Например square<long, int>(1234)
.

Авто-тип (инстанцирование) #
Ещё один момент - при указании typename
для функций компилятор сам может определить тип данных и его необязательно указывать в угловых скобках, как и сами угловые скобки, то есть mult(3, 4)
равносильно вызову mult<int>(3, 4)
- компилятор и так понял, что мы передали int
. Или mult(3.0f, 4.0f)
- компилятор автоматически сделает и вызовет функцию для float
. Это называется инстанцированием.
Но если на примере этой же функции мы вызовем с разными типами данных mult(3, 3.5)
- будет ошибка компиляции, т.к. компилятор не сможет выбрать, какой из типов использовать. Нужно указать вручную, например mult<float>(3, 3.5)
.
Массивы #
Тип данных в шаблоне очень удобно использовать для работы с массивами, как было показано в уроке про массивы:
template <typename T>
void foo(T& arr) {
// здесь корректно считается sizeof(arr)
}
int arr1[5];
float arr2[10];
foo(arr1);
foo(arr2);
То есть мы написали одну функцию, которая может принимать массивы любого размера и типа, а дальше на основе этого шаблона компилятор сам сделает версию функции под переданный массив.
Разбивка на байты #
При разработке инструментов для связи и обмена данными часто нужна удобная функция, которая примет любой тип данных и дальше сможет "передать" его побайтно. Это можно сделать через указатель на void
+ размер:
void send(void* data, size_t size) {
uint8_t* p = (uint8_t*)data;
// набор байт p размера size
}
AnyType var;
send(&var, sizeof(var));
На шаблонах можно сделать более лаконичный API:
template <typename T>
void send(T& data) {
uint8_t* p = (uint8_t*)&data;
// набор байт p размера sizeof(T)
}
AnyType var;
send(var);
Контейнеры #
Ещё один частый сценарий использования шаблонов - создание класса-контейнера для данных указанного типа. Например:
template <typename T, size_t size>
class Container {
T buffer[size];
};
Container<int, 10> cont1;
Container<float, 20> cont2;
Получился универсальный класс, который создаёт в себе статический массив указанного типа и размера. Массив может быть и динамическим - через new
и хранение указателя типа T
, именно так и работют инструменты из STL - стандартной шаблонной библиотеки C++, например std::vector
.
Шаблонный полиморфизм #
Шаблонный тип данных может использоваться везде, где подразумевается тип данных. Например, можно наследовать указанный класс в шаблонный класс:
template <typename Base>
class Foo : public Base {
};
Здесь можно использовать слово class
вместо typename
- по сути одно и то же, но программисту более понятно, что здесь подразумевается именно пользовательский класс:
template <class Base>
class Foo : public Base {
};
Это добавляет ещё один уровень полиморфизма, то есть можно наследовать несвязанные друг с другом классы, например разные реализации для разных платформ. Например - мы делаем обёртку для стандартного Arduino-серво API и хотим, чтобы она работала с любой библиотекой для серво, которая его поддерживает. Например стандартная Servo
, какая нибудь нестандартная SoftServo
и ESP32Servo
для ESP32. Эти классы никак не связаны между собой, но имеют одинаковый набор методов. Используем шаблонный полиморфизм:
template <typename ServoClass>
class ServoWrapper : public ServoClass {
public:
void test() {
write(10); // есть в любом классе (Servo API)
}
}
ServoWrapper<Servo> servo1;
ServoWrapper<SoftServo> servo2;
ServoWrapper<ESP32Servo> servo3;
servo1.test();
servo2.test();
У нас получился класс, который может управлять сервоприводом на основе указанного класса сервопривода, при условии что он соблюдает Servo API. Такой подход часто можно встретить в Arduino-библиотеках.
Или более простой пример - функция, которая принимает объект сервопривода любого класса и поворачивает на угол:
template <typename T>
void turnServo(T& servo, int deg) {
servo.write(deg);
}
Теперь в эту функцию можно отправлять объект серво, созданный из любой библиотеки для серво:
Servo servo;
turnServo(servo, 100);
Перегрузка шаблона #
Шаблонные сущности точно так же можно перегружать, например:
template <typename T>
T mult(T a, T b) {
return a * b;
}
// +1 параметр шаблона
template <typename T1, typename T2>
T1 mult(T1 a, T2 b) {
return a * b;
}
// +1 параметр функции
template <typename T>
T mult(T a, T b, T c) {
return a * b * c;
}
Специализация шаблона #
Если мы сделали универсальную шаблонную "штуку", то есть возможность указать отдельную реализацию для конкретного параметра шаблона, это называется специализацией шаблона. Например:
template <typename T>
T mult(T a, T b) {
return a * b;
}
// отдельная версия кода для float
template <>
float mult(float a, float b) {
return round(a) * round(b);
}
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками
