Метапрограммирование шаблонов (TMP) – это технология C++, позволяющая писать код, управляющий типами и генерирующий код во время компиляции. Он предоставляет способ реализации мощного универсального программирования, когда один и тот же код может применяться к нескольким типам, включая типы, определяемые пользователем.
Вот различные методы шаблонного метапрограммирования (TMP) в C++. Каждый из этих методов имеет свою уникальную цель и вариант использования.
- Признаки типов. Предоставляет возможность запрашивать свойства типов во время компиляции.
- SFINAE(Отказ замены не является ошибкой): позволяет создавать специализации шаблона на основе определенных условий.
- Шаблоны Variadic: предоставляет способ обработки переменного количества параметров шаблона.
- Целые последовательности: позволяет создавать последовательности целых чисел во время компиляции.
- Списки типов: позволяет управлять списками типов во время компиляции.
- Function Traits: позволяет запрашивать свойства функций во время компиляции.
- Рекурсивные шаблоны: позволяет создавать шаблоны, которые можно использовать рекурсивно.
- Лямбда-выражения и замыкания. Предоставляет возможность создавать анонимные функции во время компиляции.
- Определяемые пользователем литералы: позволяет создавать настраиваемые операторы-литералы, которые позволяют инициализировать переменные со специальными значениями.
Каждый из этих методов предоставляет уникальный и мощный способ манипулирования типами и генерации кода во время компиляции, и их можно использовать вместе различными способами для достижения мощного универсального программирования.
1. Черты типа
Признаки типа — это набор классов, которые можно использовать для запроса информации о типах, например, является ли тип целочисленным или с плавающей запятой, является ли он указателем и т. д. Они позволяют выполнять анализ типов во время компиляции, что может быть полезно в различных ситуациях, например при написании универсального кода.
Вот пример использования признаков типа в C++:
#include <type_traits> #include <iostream> template <typename T> void print_value(T value) { if constexpr (std::is_integral<T>::value) { std::cout << "Integral value: " << value << std::endl; } else if constexpr (std::is_floating_point<T>::value) { std::cout << "Floating-point value: " << value << std::endl; } else { std::cout << "Other value: " << value << std::endl; } } int main() { print_value(42); print_value(3.14); print_value("Hello, world!"); }
Код определяет шаблон функции print_value
, который принимает один аргумент любого типа. Функция использует трейты типа is_integral
и is_floating_point
для определения типа аргумента и соответствующего вывода сообщения. Если аргумент не является ни целочисленным типом, ни типом с плавающей запятой, выводится сообщение по умолчанию.
Признаки типов очень полезны при написании универсального и эффективного кода на C++, поскольку они позволяют вам принимать решения о типах во время компиляции, а не во время выполнения. Это может привести к повышению производительности и повышению ремонтопригодности кода.
2. СФИНАЭ
SFINAE (Sзамена Fнеудача Is Not An Ошибка) — это функция C++, позволяющая предоставлять несколько перегруженных шаблонов функций или классов и выбирать правильный в зависимости от типа аргументов.
Вот пример использования SFINAE в C++:
#include <iostream> template <typename T> typename T::value_type sum(const T& container) { typename T::value_type result = typename T::value_type(); for (const auto& element : container) { result += element; } return result; } template <typename T> void print_sum(const T& container) { std::cout << "Sum: " << sum<T>(container) << std::endl; } int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; print_sum(numbers); }
Код определяет два шаблона функций: sum
и print_sum
. Шаблон функции sum
принимает контейнер любого типа T
и возвращает сумму его элементов. Шаблон функции print_sum
берет контейнер любого типа T
и выводит его сумму.
Шаблон функции sum
использует SFINAE, чтобы гарантировать, что тип контейнера T
имеет вложенные функции-члены value_type
, begin
и end
. Если тип T
не имеет этих свойств, шаблон функции sum
не будет считаться допустимой перегрузкой, и компилятор продолжит поиск подходящей перегрузки.
3. Вариативные шаблоны
Шаблоны Variadic позволяют определить шаблон функции или класса, который принимает произвольное количество аргументов любого типа. Это позволяет писать общий код, который может обрабатывать широкий диапазон входных данных, без необходимости писать отдельный код для каждого конкретного случая.
Вот пример использования вариативных шаблонов в C++:
#include <iostream> template <typename T> T add(T a, T b) { return a + b; } template <typename T, typename... Args> T add(T a, T b, Args... args) { return add(add(a, b), args...); } int main() { std::cout << add(1, 2, 3, 4, 5) << std::endl; }
В коде определены две перегрузки шаблона функции add
: одна принимает два аргумента, а другая принимает произвольное количество аргументов. Вторая перегрузка использует синтаксис ...
для определения вариативного шаблона, что означает, что он может принимать произвольное количество аргументов любого типа.
Вторая перегрузка шаблона функции add
рекурсивно вызывает себя с add(a, b)
до тех пор, пока не останется аргументов, после чего она возвращает результат суммы.
Шаблоны Variadic часто используются для написания функций, которые могут обрабатывать переменное количество аргументов, таких как printf
или std::tuple_cat
.
4. Целочисленные последовательности
Целочисленные последовательности — это способ представления последовательности целых чисел как типа. Их можно использовать для создания массивов времени компиляции, для выполнения операций над последовательностью целых чисел и для индексирования других конструкций времени компиляции.
Например:
template <int... Is> struct integer_sequence {}; template <int N, int... Is> struct make_integer_sequence : make_integer_sequence<N-1, N-1, Is...> {}; template <int... Is> struct make_integer_sequence<0, Is...> : integer_sequence<Is...> {}; template <int N> using make_index_sequence = make_integer_sequence<N>; template <typename... Ts> void print(Ts... args) { (std::cout << ... << args) << '\n'; } template <int... Is, typename... Ts> void print_indexed(integer_sequence<Is...>, Ts... args) { (void)(std::initializer_list<int>{(std::cout << Is << ": " << args << '\n', 0)...}); } int main() { print("hello", "world"); print_indexed(make_index_sequence<2>{}, "hello", "world"); }
5. Списки типов
Списки типов — это способ представления списка типов как типа. Их можно использовать для выполнения операций со списком типов, таких как сортировка, фильтрация и сопоставление.
Например:
template <typename... Ts> struct type_list {}; template <typename T, typename... Ts> struct contains : std::true_type {}; template <typename T> struct contains<T> : std::false_type {}; template <typename T, typename... Ts> struct contains<T, T, Ts...> : std::true_type {}; template <typename T, typename U, typename... Ts> struct contains<T, U, Ts...> : contains<T, Ts...> {}; int main() { std::cout << contains<int, int, double, char>::value << std::endl; // outputs 1 std::cout << contains<int, double, char>::value << std::endl; // outputs 0 }
6. Функциональные черты
Функциональные признаки — это способ запроса информации о типах функций, таких как тип возвращаемого значения, количество аргументов и типы аргументов.
Например:
#include <type_traits> #include <iostream> template <typename T> struct function_traits : public function_traits<decltype(&T::operator())> {}; template <typename ClassType, typename ReturnType, typename... Args> struct function_traits<ReturnType(ClassType::*)(Args...) const> { static constexpr std::size_t arity = sizeof...(Args); using result_type = ReturnType; template <std::size_t i> struct argument { using type = typename std::tuple_element<i, std::tuple<Args...>>::type; }; }; template <typename Func> using result_of = typename function_traits<Func>::result_type; template <typename Func, std::size_t i> using argument_of = typename function_traits<Func>::template argument<i>::type; double add(double a, double b) { return a + b; } int main() { std::cout << "arity: " << function_traits<decltype(add)>::arity << std::endl; std::cout << "result_type: " << typeid(result_of<decltype(add)>).name() << std::endl; std::cout << "argument_of<0>: " << typeid(argument_of<decltype(add), 0>).name() << std::endl; std::cout << "argument_of<1>: " << typeid(argument_of<decltype(add), 1>).name() << std::endl; }
Код определяет шаблон класса function_traits
, который использует частичную специализацию для извлечения информации о типе функции, такой как ее арность (количество аргументов), тип результата и типы аргументов. В этом примере функция add
используется для демонстрации того, как можно использовать шаблон function_traits
для извлечения информации о его типе.
7. Рекурсивные шаблоны
Рекурсивные шаблоны — это мощный способ написания сложных алгоритмов, работающих с типами, путем определения шаблонов, которые вызывают сами себя. Например, вы можете определить список типов, где каждый список типов является либо пустым списком типов, либо списком типов с головой и хвостом.
Например:
template <typename... T> struct type_list {}; template <typename T, typename... Rest> struct type_list<T, Rest...> { using head = T; using tail = type_list<Rest...>; }; template <typename List> struct size; template <typename... T> struct size<type_list<T...>> { static constexpr std::size_t value = sizeof...(T); }; int main() { using list = type_list<int, double, char>; std::cout << size<list>::value << std::endl; // outputs 3 }
8. Лямбда-выражения и замыкания
Лямбда-выражения и замыкания — это мощная функция C++, позволяющая определять анонимные функции (функции без имени) на лету. Они часто используются для передачи функций в качестве аргументов другим функциям или для захвата переменных из окружающей области.
В сочетании с шаблонным метапрограммированием (TMP) лямбда-выражения и замыкания могут использоваться для генерации кода и управления им во время компиляции, что позволяет писать высокоэффективный общий код.
Вот пример использования лямбда-выражений и замыканий в сочетании с TMP в C++:
#include <iostream> #include <functional> template <typename T, typename F> void apply(T& t, F f) { f(t); } int main() { int x = 42; apply(x, [](int& n) { n *= 2; }); std::cout << x << std::endl; }
Код определяет шаблонную функцию apply
, которая принимает ссылку на объект типа T
и объект функции типа F
. Функциональный объект f
— это лямбда-выражение, которое берет ссылку на int
и умножает ее на 2.
Функция main
определяет int
x
со значением 42 и вызывает функцию apply
со значением x
и лямбда-выражением. Функция apply
вызывает лямбда-выражение для x
, умножая его на 2 и изменяя его значение на 84.
Лямбда-выражения и замыкания — это мощная функция C++, позволяющая определять анонимные функции и передавать их в качестве аргументов другим функциям. В сочетании с TMP их можно использовать для генерации кода и управления им во время компиляции, что позволяет писать высокоэффективный универсальный код.
9. Пользовательские литералы
C++11 представил возможность определять определяемые пользователем литералы, которые позволяют вам определять новый синтаксис для литералов в вашей программе. Это может быть полезно в сочетании с TMP для написания специализированного кода для определенных литеральных типов.
Вот пример использования определяемых пользователем литералов в C++:
#include <iostream> constexpr long operator"" _kb(unsigned long long x) { return x * 1024; } constexpr long operator"" _mb(unsigned long long x) { return x * 1024 * 1024; } int main() { std::cout << 42_kb << std::endl; std::cout << 42_mb << std::endl; }
В этом примере определены два определяемых пользователем литерала: _kb
и _mb
. Эти литералы позволяют указывать значения в килобайтах и мегабайтах, соответственно, в более удобочитаемой форме.
Синтаксис operator""
используется для определения пользовательских литералов. Первый аргумент функции operator""
— строковый литерал, а второй аргумент — тип литерала. В этом примере это тип unsigned long long
.
Определяемые пользователем литералы — это мощная функция C++, позволяющая создавать новые формы литералов для ваших типов данных, делая ваш код более выразительным и читабельным. Они часто используются для определения новых единиц измерения, таких как килобайты и мегабайты в приведенном выше примере.
Это всего лишь несколько примеров TMP в C++. Изучив TMP, вы сможете писать более гибкий и эффективный код, способный обрабатывать широкий спектр ситуаций и типов.
Лучшие :)