Для чего нужно шаблонное метапрограммирование
Шаблонное метапрограммирование (TMP) позволяет писать код, параметризованный по типам — и ограничивать, какие типы допустимы. Цель — ловить ошибки типов на этапе компиляции, а не во время выполнения, и генерировать специализированный код на тип без его ручного написания.
Практические применения:
- Принять
std::span<uint8_t>иstd::vector<uint8_t>в одной функции - Ограничить контейнер тривиально копируемыми типами (безопасны для DMA-передачи)
- Включить разные ветви кода в зависимости от того, является ли тип знаковым или вещественным
- Абстракции с нулевой стоимостью через
if constexpr
Трейты типов — запрос типов на этапе компиляции
<type_traits> предоставляет предикаты о типах:
1#include <type_traits>
2
3// T — целое число?
4std::is_integral<int>::value // true
5std::is_integral<float>::value // false
6std::is_integral_v<int> // сокращение C++17 — то же самое
7
8// T — тривиально копируемый? (безопасен для memcpy)
9std::is_trivially_copyable_v<float> // true
10std::is_trivially_copyable_v<std::vector<int>> // false — имеет деструктор
11
12// T — указатель?
13std::is_pointer_v<int*> // true
14std::is_pointer_v<int> // false
Используйте для защиты DMA-безопасных передач:
1template <typename T>
2void dmaTransfer(std::span<T> data, DMA_HandleTypeDef* hdma) {
3 static_assert(std::is_trivially_copyable_v<T>,
4 "DMA-передача требует тривиально копируемого типа");
5 HAL_DMA_Start(hdma, reinterpret_cast<uint32_t>(data.data()),
6 dmaDestination, data.size() * sizeof(T));
7}
Ошибка срабатывает на этапе компиляции, а не когда прошивка уже запущена.
SFINAE — Substitution Failure Is Not An Error
До C++20 способом условного включения шаблона функции было SFINAE:
1// Включить эту перегрузку только если T — целое число
2template <typename T,
3 typename = std::enable_if_t<std::is_integral_v<T>>>
4T clamp(T value, T lo, T hi) {
5 return value < lo ? lo : value > hi ? hi : value;
6}
7
8// Включить эту перегрузку только если T — вещественное число
9template <typename T,
10 typename = std::enable_if_t<std::is_floating_point_v<T>>>
11T clamp(T value, T lo, T hi) {
12 if (std::isnan(value)) return lo; // обработка NaN для вещественных
13 return value < lo ? lo : value > hi ? hi : value;
14}
Сообщения об ошибках SFINAE печально известны своей нечитаемостью:
error: no matching function for call to 'clamp(std::string, ...)'
note: template argument deduction/substitution failed
...15 строк трассировки инстанциирования шаблона...
Концепты C++20 заменяют SFINAE читаемыми ограничениями и читаемыми ошибками.
Концепты C++20 — читаемые ограничения
Концепт — именованный булев предикат на типах:
1#include <concepts>
2
3// Встроенные концепты
4std::integral<int> // true
5std::floating_point<float> // true
6std::copyable<std::vector<int>> // true
7std::trivially_copyable<float> // true
8
9// Определить собственные
10template <typename T>
11concept Numeric = std::integral<T> || std::floating_point<T>;
12
13template <typename T>
14concept DmaSafe = std::is_trivially_copyable_v<T>;
Ограничение шаблона с помощью концепта:
1// Три эквивалентных синтаксиса
2
3// Предложение requires
4template <typename T>
5 requires Numeric<T>
6T clamp(T value, T lo, T hi);
7
8// Концепт в списке параметров шаблона
9template <Numeric T>
10T clamp(T value, T lo, T hi);
11
12// Сокращённый шаблон функции (C++20)
13auto clamp(Numeric auto value, Numeric auto lo, Numeric auto hi);
Сообщение об ошибке с концептами:
error: no matching function for call to 'clamp(std::string, ...)'
note: constraints not satisfied
note: 'std::string' does not satisfy 'Numeric'
Читаемо, понятно что исправить.
Ограничение интерфейса датчика
1// Определить, что должен поддерживать "Sensor"
2template <typename T>
3concept Sensor = requires(T s) {
4 { s.read() } -> std::convertible_to<float>;
5 { s.isReady() } -> std::convertible_to<bool>;
6 { s.name() } -> std::convertible_to<std::string_view>;
7};
8
9// Компилируется только если T удовлетворяет Sensor
10template <Sensor S>
11class SamplingPipeline {
12 S& sensor_;
13 std::vector<float> history_;
14public:
15 explicit SamplingPipeline(S& s) : sensor_(s) {}
16
17 void sample() {
18 if (sensor_.isReady())
19 history_.push_back(sensor_.read());
20 }
21
22 std::span<const float> history() const { return history_; }
23};
24
25struct Bmp280 {
26 float read() { return readTemp(); }
27 bool isReady() { return (readStatus() & 0x08) == 0; }
28 std::string_view name() { return "BMP280"; }
29};
30
31SamplingPipeline<Bmp280> pipeline(bmp); // компилируется — Bmp280 удовлетворяет Sensor
Если в Bmp280 отсутствует метод, ошибка называет отсутствующее требование:
error: 'Bmp280' does not satisfy 'Sensor'
note: 'read()' is required but missing
if constexpr — ветвление на этапе компиляции
if constexpr выбирает ветку кода на этапе компиляции. Невыбранная ветка
не компилируется — нет накладных расходов, нет ошибок компоновщика для веток,
которые не работают с заданным типом:
1template <typename T>
2void serialize(T value, std::span<uint8_t> out) {
3 if constexpr (std::is_same_v<T, float>) {
4 uint32_t bits;
5 std::memcpy(&bits, &value, 4);
6 out[0] = bits >> 24; out[1] = bits >> 16;
7 out[2] = bits >> 8; out[3] = bits;
8 } else if constexpr (std::is_integral_v<T>) {
9 for (size_t i = sizeof(T); i > 0; --i)
10 out[i - 1] = value & 0xFF, value >>= 8;
11 } else {
12 static_assert(sizeof(T) == 0, "неподдерживаемый тип для serialize");
13 }
14}
static_assert(false) в ветке else — ошибка компиляции, срабатывающая только
когда используется неподдерживаемый тип, а не при каждом инстанциировании.
Написание кастомного трейта типа
Определяйте трейты для собственных типов домена:
1// Пометить типы как безопасные для DMA-передачи
2template <typename T>
3struct is_dma_safe : std::is_trivially_copyable<T> {};
4
5// Специализация для типа, не являющегося тривиально копируемым, но безопасного
6// (например, тип с нетривиальным деструктором, не влияющим на данные)
7template <>
8struct is_dma_safe<MySpecialBuffer> : std::true_type {};
9
10template <typename T>
11inline constexpr bool is_dma_safe_v = is_dma_safe<T>::value;
12
13// Использовать как концепт
14template <typename T>
15concept DmaSafe = is_dma_safe_v<T>;
16
17template <DmaSafe T>
18void startDma(std::span<T> buf, DMA_HandleTypeDef* hdma) {
19 HAL_DMA_Start(hdma, (uint32_t)buf.data(), dmaDest, buf.size() * sizeof(T));
20}
Вычисления на этапе компиляции
TMP может вычислять значения на этапе компиляции — вычисление происходит при компиляции, результат — константа:
1// Проверка степени двойки на этапе компиляции (C++14)
2template <size_t N>
3constexpr bool isPowerOfTwo = N > 0 && (N & (N - 1)) == 0;
4
5static_assert(isPowerOfTwo<64>, "буфер должен быть степенью двойки");
6static_assert(!isPowerOfTwo<63>, "проверка вменяемости");
7
8// Логарифм по основанию 2 на этапе компиляции
9constexpr size_t log2(size_t n) {
10 return n <= 1 ? 0 : 1 + log2(n / 2);
11}
12
13constexpr size_t mask = (1 << log2(64)) - 1; // = 63
Свёрточные выражения C++17
Свёрточные выражения разворачивают пакеты параметров без рекурсивных шаблонов:
1// Сумма любого числа аргументов
2template <typename... Ts>
3auto sum(Ts... args) {
4 return (args + ...); // свёрточное выражение: a1 + a2 + a3 + ...
5}
6
7float total = sum(1.0f, 2.5f, 3.7f); // 7.2
8
9// Условие "все"
10template <typename... Ts>
11bool allPositive(Ts... args) {
12 return ((args > 0) && ...);
13}
14
15bool ok = allPositive(1, 2, 3); // true
16bool no = allPositive(1, -1, 3); // false
Итоги
<type_traits>+static_assert: ловить злоупотребление типами на этапе компиляции — DMA-безопасные типы, ограничения размера- SFINAE: условное включение шаблонов в C++11/14/17 — читаемо только с практикой, некрасивые ошибки
- Концепты C++20: именованные ограничения, читаемые ошибки, три варианта синтаксиса — предпочитайте
template <MyConcept T> if constexpr: ветвление по типу на этапе компиляции — сериализация по типу, выбор формата- Свёрточные выражения: развёртка пакетов без рекурсии — сумма, все, любой из пакетов аргументов
- Кастомные трейты:
is_dma_safe<T>,is_trivially_copyable<T>— проверки конкретного домена на этапе компиляции
Что дальше
- CRTP — статический полиморфизм и примеси — полиморфизм с нулевой стоимостью на этапе компиляции
- Стирание типов без virtual — гибкость во время выполнения с концептами, охраняющими интерфейс
- std::variant и std::optional — типобезопасные дискриминированные объединения