View Categories

Шаблоны

Шаблоны (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);
}

Полезные страницы #

Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх