Примечание. Эта статья не для новичков в 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 заключается в том, что он работает с sizeofoperator, поэтому мы удостоверяемся, что 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›