Примечание. Эта статья не для новичков в C++, но вы все равно можете попробовать!
Как программисты C++, мы знаем, что шаблоны — это мощный инструмент для создания универсального кода. Они позволяют нам писать функции и классы, которые могут работать с любым типом, если этот тип соответствует определенным требованиям.
Но шаблоны — это не просто инструмент для создания универсального кода. Они также являются мощным инструментом для создания оптимизированного кода, который вычисляет результаты во время компиляции и может быть адаптирован к конкретным случаям использования. В этой статье мы рассмотрим возможности шаблонов в современном C++ (C++17), используя базовое TMP (метапрограммирование шаблонов), и посмотрим, как их можно использовать для создания высокопроизводительного и гибкого кода.
В этой статье будут использоваться средства std::type_traits , привязка структуры, вариативные шаблоны, constexpr и std::static_assert, чтобы показать полный вариант использования операций в очень простой реализации списка типов, т.е. на самом деле просто тупой заполнитель, который содержит список арифметических типов с соответствующими им значениями, и что мы будем использовать его элементы для вычисления суммы всех содержащихся в нем значений и подсчета количества четных чисел в нем.
Основы шаблонов
Прежде чем мы углубимся в более сложные способы использования шаблонов, давайте рассмотрим основы. В C++ шаблон — это механизм для создания кода, который работает с несколькими типами. Например, рассмотрим следующую функцию шаблона:
Эта функция принимает один аргумент типа T
и возвращает квадрат этого значения. Ключевое слово typename
указывает, что T
является параметром типа, а constexpr
говорит, что функция должна оцениваться во время компиляции. Обратите внимание, что constexpr
здесь не является обязательным, но улучшает читаемость кода, а в некоторых случаях увеличивает нагрузку на компилятор, так что это хорошая привычка.
Мы можем использовать эту функцию с любым типом, который поддерживает оператор *
. Например, мы можем вызвать square(3)
, чтобы получить квадрат целого числа, или square(3.14)
, чтобы получить квадрат значения с плавающей запятой.
Теперь в духе TMP, как указано в абзаце выше, вы должны ограничить свой тип, чтобы мы могли использовать стандартное средство метапрограммирования шаблонов std::type_traits, представленное в заголовке <type_traits>
, чтобы проверить, соответствует ли тип является арифметическим, т. е. целым или с плавающей запятой. Вот улучшенная версия:
Обратите внимание на использование здесь static_assert
как одного из ваших лучших друзей при использовании TMP (концепции C++20 идут дальше). При компиляции он обеспечивает, чтобы тип логически соответствовал ожиданиям.
Если бы мы попытались передать строку функции square
, это вызвало бы что-то вроде static_assert failed из-за требования 'std::is_arithmetic_v‹const char *›' "T должен быть арифметическим типом"
static_assert(std ::is_arithmetic_v‹T›, «T должен быть арифметическим типом»);
Вот полный список примера программы Square:
Хорошо, теперь мы перейдем к варианту использования, давайте объявим список типов, который использует вариативные аргументы шаблона:
type_list
— это просто тупой заполнитель наших типов и связанных значений. Мы будем использовать тип std::integral_constant из заголовка ‹type_traits› в type_list
вместо обычных целых чисел или чисел с плавающей запятой, поскольку мы рассчитываем на типы, которые имеют « атрибут value», связанный с ним. std::integral_constant
заключает в себя статическую константу указанного типа, что-то вроде листинга ниже:
Вот наш type_list с нашими значениями:
Мы присвоили ему псевдоним my_list
, чтобы его было проще использовать в нашем примере кода.
Вскоре мы увидим базовую реализацию без проверки типов функции sum
, которая будет суммировать все аргументы в списке типов, но до этого мы увидим пример, используемый на cppreference.com, в котором мы можем визуализировать расширение пакета параметров, используя folding (функция C++17) с простой функцией, которая выполняет логическую логическую оценку своих типов и возвращает true, если все значения равны true, false в противном случае:
template<typename... Args> bool all(Args... args) { return (... && args); } bool b = all(true, true, true, false); // within all(), the unary left fold expands as // return ((true && true) && true) && false; // b is false
В этом примере используется унарное сворачивание влево, но также поддерживается унарное сворачивание вправо, бинарное сворачивание влево и вправо. Пожалуйста, смотрите ссылку выше для правильного объяснения.
Теперь мы можем попытаться визуализировать, как будет выглядеть базовая функция суммирования, которая суммирует свои аргументы:
Теперь мы можем увидеть реализацию нашей суммы для нашего type_list, которая очень похожа:
При этом используется двоичное свертывание пакета параметров, в котором компилятор выполняет типизацию за нас. Еще одна замечательная особенность folding заключается в том, что он работает с sizeof
operator, поэтому мы удостоверяемся, что type_list не пуст, хотя в нашем случае код отлично работал бы с пустым списком. и сумма вернет 0.
Правильно, реализация функции count
немного сложнее, так как мы просто хотим подсчитать четные числа:
У нас есть функция is_even_v
, которая возвращает значение, если значение ее типа четное, и внутри нашей функции count_even
мы вызываем ее для каждого аргумента, используя преимущество свертывания.
Нам также понадобится функция, которая будет возвращать аргументы списка типов в виде кортежа, который представляет собой набор разнородных значений фиксированного размера для последующего извлечения значений.
Мы реализуем последние две функции, которые будут работать с нашим type_list, одна будет возвращать переднюю часть списка типов, а другая — заднюю!
Функция front
получит только первый тип T
. Хитрость заключается в том, чтобы воспользоваться тем, как работают шаблоны с переменным числом аргументов, чтобы у нас было 2 параметра typename: сначала «обычный» T
с получит первый тип type_list, а затем мы используем пакет параметров …Args
, который будет использоваться для «упаковки» остальных типов type_list. Функцияback
использует те же два имени типов, а затем работает рекурсивно до тех пор, пока в пакете параметров Args не будет отсутствовать тип, поэтому у нас будет только T
{}соответствует первому аргументу шаблона T
. Пошаговый анализ вызова back
см. ниже.
В TMP много рекурсии, поэтому, пожалуйста, не торопитесь, чтобы понять, как это работает, как в ссылке выше или здесь.
Теперь нам просто нужен полный список того, что мы обсуждали, единственное, что нужно добавить, это то, что мы используем преимущества привязки структуры и std::tuples в примере кода. для извлечения типов из нашего type_list, который является функцией C++17, в которой мы можем присваивать значения нескольким переменным одновременно, извлекая значения массивов, членов данных, кортежей и т. д. См. ссылку, чтобы увидеть, что разрешено или нет. Примечание: необязательных значений нет, все значения должны быть привязаны, и они также должны быть уникальными идентификаторами.
// Use structure binding to extract the first, second, third, and last elements of the type list auto [first, second, third, last] = unzip(my_list{});
Это вызовет нашу функцию unzip
, которая вернет кортеж с типами нашего списка типов, который, в свою очередь, привяжется к соответствующим переменным.
Чтобы лучше понять это, вот дословная копия примера связывания структуры с сайта cppreference.com с кортежем и их объяснением.
float x{}; char y{}; int z{}; std::tuple<float&, char&&, int> tpl(x, std::move(y), z); const auto& [a, b, c] = tpl; // using Tpl = const std::tuple<float&, char&&, int>; // a names a structured binding that refers to x (initialized from get<0>(tpl)) // decltype(a) is std::tuple_element<0, Tpl>::type, i.e. float& // b names a structured binding that refers to y (initialized from get<1>(tpl)) // decltype(b) is std::tuple_element<1, Tpl>::type, i.e. char&& // c names a structured binding that refers to the third component of tpl, get<2>(tpl) // decltype(c) is std::tuple_element<2, Tpl>::type, i.e. const int
Полный листинг рабочего исходного кода мы обсуждали в статье
Надеюсь, вам понравилась статья! Приветствуется любая критика, хорошая или плохая! Я должен опубликовать несколько учебников по низкоуровневому параллелизму и концепциям C++ 20 в качестве своих следующих статей! Следите за обновлениями!
Примечание. Для части 2 отметьте https://medium.com/@joao_vaz/template-metaprograming-or-constexpr-a-primer-and-comparison-in-c-17-beeaf2d7f0af
Примечание 2. Для полного понимания списков типов см. книгу Александреску Современный дизайн C++ или просмотрите его старую библиотеку Loki https://github.com/snaewe/loki-lib
Логотип C++ из ‹a href="https://www.flaticon.com/free-icons/programmer' title="programmer icons"›Значки программистов, созданные Freepik — Flaticon‹/a›