C++
Метапрограммирование
Поиск…
Вступление
В C ++ Metaprogramming ссылается на использование макросов или шаблонов для генерации кода во время компиляции.
В общем, макросы не одобряются в этой роли, и предпочтительны шаблоны, хотя они не являются общими.
Метапрограммирование шаблонов часто использует вычисления времени компиляции, будь то через шаблоны или функции constexpr
, для достижения целей генерации кода, однако вычисления времени компиляции не являются метапрограммированием как таковым.
замечания
Метапрограммирование (или, более конкретно, Template Metaprogramming) - это практика использования шаблонов для создания констант, функций или структур данных во время компиляции. Это позволяет выполнять вычисления один раз во время компиляции, а не в каждое время выполнения.
Вычисление факториалов
Факториалы могут быть вычислены во время компиляции с использованием методов метапрограммирования шаблонов.
#include <iostream>
template<unsigned int n>
struct factorial
{
enum
{
value = n * factorial<n - 1>::value
};
};
template<>
struct factorial<0>
{
enum { value = 1 };
};
int main()
{
std::cout << factorial<7>::value << std::endl; // prints "5040"
}
factorial
- это структура, но в метапрограммировании шаблона он рассматривается как шаблонный метафунг. По соглашению метафайлы шаблонов оцениваются путем проверки определенного члена: ::type
для метафайлов, которые приводят к типам, или ::value
для метафайлов, которые генерируют значения.
В приведенном выше коде мы оцениваем factorial
метафункт, создавая экземпляр шаблона с параметрами, которые мы хотим передать, и используя ::value
чтобы получить результат оценки.
Сама метафония полагается на рекурсивно создающую тот же метафунгмент с меньшими значениями. factorial<0>
представляет собой конечное условие. Метапрограммирование шаблонов имеет большинство ограничений функционального языка программирования , поэтому рекурсия является основной конструкцией «looping».
Поскольку метафайлы шаблонов выполняются во время компиляции, их результаты могут использоваться в контекстах, требующих значений времени компиляции. Например:
int my_array[factorial<5>::value];
Автоматические массивы должны иметь определенный размер времени компиляции. И результат metafunction - константа времени компиляции, поэтому ее можно использовать здесь.
Ограничение . Большинство компиляторов не позволят глубже рекурсии выйти за пределы. Например, компилятор g++
по умолчанию рекурсия ограничивает 256 уровней. В случае g++
программист может установить глубину рекурсии с помощью параметра -ftemplate-depth-X
.
С C ++ 11 шаблон std::integral_constant
может использоваться для такого расчета шаблонов:
#include <iostream>
#include <type_traits>
template<long long n>
struct factorial :
std::integral_constant<long long, n * factorial<n - 1>::value> {};
template<>
struct factorial<0> :
std::integral_constant<long long, 1> {};
int main()
{
std::cout << factorial<7>::value << std::endl; // prints "5040"
}
Кроме того, функции constexpr
становятся более чистой альтернативой.
#include <iostream>
constexpr long long factorial(long long n)
{
return (n == 0) ? 1 : n * factorial(n - 1);
}
int main()
{
char test[factorial(3)];
std::cout << factorial(7) << '\n';
}
Тело factorial()
записывается как один оператор, потому что в C ++ 11 функции constexpr
могут использовать только ограниченное подмножество языка.
Начиная с C ++ 14, многие ограничения для функций constexpr
были отброшены, и теперь их можно записать гораздо удобнее:
constexpr long long factorial(long long n)
{
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
Или даже:
constexpr long long factorial(int n)
{
long long result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
Поскольку c ++ 17 можно использовать выражение fold для вычисления factorial:
#include <iostream>
#include <utility>
template <class T, T N, class I = std::make_integer_sequence<T, N>>
struct factorial;
template <class T, T N, T... Is>
struct factorial<T,N,std::index_sequence<T, Is...>> {
static constexpr T value = (static_cast<T>(1) * ... * (Is + 1));
};
int main() {
std::cout << factorial<int, 5>::value << std::endl;
}
Итерация по пакету параметров
Часто нам нужно выполнить операцию над каждым элементом в пакете параметров вариационного шаблона. Есть много способов сделать это, и решения легче читать и писать с C ++ 17. Предположим, мы просто хотим напечатать каждый элемент в пакете. Самое простое решение - рекурсия:
void print_all(std::ostream& os) {
// base case
}
template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
os << first;
print_all(os, rest...);
}
Вместо этого мы могли бы использовать трюк экспандера, чтобы выполнить все потоки в одной функции. Это имеет то преимущество, что не требуется вторая перегрузка, но имеет недостаток, превышающий читабельность звезд:
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
using expander = int[];
(void)expander{0,
(void(os << args), 0)...
};
}
Для объяснения того, как это работает, см . Отличный ответ TC .
С C ++ 17 мы получаем два мощных новых инструмента в нашем арсенале для решения этой проблемы. Первый - это сгиб-выражение:
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
((os << args), ...);
}
И второе - if constexpr
, что позволяет нам написать наше оригинальное рекурсивное решение в одной функции:
template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
os << first;
if constexpr (sizeof...(rest) > 0) {
// this line will only be instantiated if there are further
// arguments. if rest... is empty, there will be no call to
// print_all(os).
print_all(os, rest...);
}
}
Итерация с помощью std :: integer_sequence
Поскольку C ++ 14, стандарт предоставляет шаблон класса
template <class T, T... Ints>
class integer_sequence;
template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;
и генерирующий метафунд для него:
template <class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;
Хотя это стандартно в C ++ 14, это можно реализовать с помощью инструментов C ++ 11.
Мы можем использовать этот инструмент для вызова функции с помощью std::tuple
аргументов (стандартизован в C ++ 17 как std::apply
):
namespace detail {
template <class F, class Tuple, std::size_t... Is>
decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) {
return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...);
}
}
template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tpl) {
return detail::apply_impl(std::forward<F>(f),
std::forward<Tuple>(tpl),
std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}
// this will print 3
int f(int, char, double);
auto some_args = std::make_tuple(42, 'x', 3.14);
int r = apply(f, some_args); // calls f(42, 'x', 3.14)
Отправка тегов
Простым способом выбора функций во время компиляции является отправка функции в перегруженную пару функций, которые принимают тег как один (обычно последний) аргумент. Например, для реализации std::advance()
мы можем отправить в категорию итератора:
namespace details {
template <class RAIter, class Distance>
void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {
it += n;
}
template <class BidirIter, class Distance>
void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {
if (n > 0) {
while (n--) ++it;
}
else {
while (n++) --it;
}
}
template <class InputIter, class Distance>
void advance(InputIter& it, Distance n, std::input_iterator_tag) {
while (n--) {
++it;
}
}
}
template <class Iter, class Distance>
void advance(Iter& it, Distance n) {
details::advance(it, n,
typename std::iterator_traits<Iter>::iterator_category{} );
}
Параметры std::XY_iterator_tag
перегруженных details::advance
- неиспользуемые параметры функции. Фактическая реализация не имеет значения (на самом деле она полностью пуста). Их единственная цель - разрешить компилятору выбирать перегрузку, на основе которой вызывается класс класса details::advance
.
В этом примере advance
использует iterator_traits<T>::iterator_category
который возвращает один из классов iterator_tag
, в зависимости от реального типа Iter
. Построенный по умолчанию объект типа iterator_category<Iter>::type
затем позволяет компилятору выбрать одну из разных перегрузок details::advance
. (Этот параметр функции, скорее всего, будет полностью оптимизирован, так как это построенный по умолчанию объект пустой struct
и никогда не используется).
Отправка тегов может дать вам код, который намного легче читать, чем эквиваленты, используя SFINAE и enable_if
.
Примечание: в то время как C ++ 17, if constexpr
может упростить реализацию advance
в частности, он не подходит для открытых реализаций, в отличие от диспетчеризации тегов.
Определить, действительно ли выражение
Можно определить, можно ли вызвать оператор или функцию для типа. Чтобы проверить, имеет ли класс перегрузку std::hash
, можно сделать следующее:
#include <functional> // for std::hash
#include <type_traits> // for std::false_type and std::true_type
#include <utility> // for std::declval
template<class, class = void>
struct has_hash
: std::false_type
{};
template<class T>
struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())>
: std::true_type
{};
Поскольку C ++ 17, std::void_t
можно использовать для упрощения этого типа конструкции
#include <functional> // for std::hash
#include <type_traits> // for std::false_type, std::true_type, std::void_t
#include <utility> // for std::declval
template<class, class = std::void_t<> >
struct has_hash
: std::false_type
{};
template<class T>
struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > >
: std::true_type
{};
где std::void_t
определяется как:
template< class... > using void_t = void;
Для определения того, является ли оператор, например operator<
, синтаксисом, почти то же самое:
template<class, class = void>
struct has_less_than
: std::false_type
{};
template<class T>
struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())>
: std::true_type
{};
Они могут использоваться для использования std::unordered_map<T>
если T
имеет перегрузку для std::hash
, но в противном случае попытается использовать std::map<T>
:
template <class K, class V>
using hash_invariant_map = std::conditional_t<
has_hash<K>::value,
std::unordered_map<K, V>,
std::map<K,V>>;
Вычисление мощности с C ++ 11 (и выше)
С C ++ 11 и выше вычисления во время компиляции могут быть намного проще. Например, вычисление мощности заданного числа во время компиляции будет следующим:
template <typename T>
constexpr T calculatePower(T value, unsigned power) {
return power == 0 ? 1 : value * calculatePower(value, power-1);
}
Ключевое слово constexpr
отвечает за вычисление функции во время компиляции, тогда и только тогда, когда все требования для этого будут выполнены (см. Больше в ссылке на constexpr), например, все аргументы должны быть известны во время компиляции.
Примечание. В C ++ 11 функция constexpr
должна составлять только один оператор return.
Преимущества: сравнивая это со стандартным способом вычисления времени компиляции, этот метод также полезен для расчетов времени выполнения. Это означает, что если аргументы функции не известны во время компиляции (например, значение и мощность задаются как входные данные через пользователя), то функция запускается во время компиляции, поэтому нет необходимости дублировать код (поскольку мы был бы вынужден в более старых стандартах C ++).
Например
void useExample() {
constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,
// as both arguments are known at compilation time
// and used for a constant expression.
int value;
std::cin >> value;
int runtimeCalculated = calculatePower(value, 3); // runtime calculated,
// because value is known only at runtime.
}
Другой способ вычисления мощности во время компиляции может использовать выражение fold:
#include <iostream>
#include <utility>
template <class T, T V, T N, class I = std::make_integer_sequence<T, N>>
struct power;
template <class T, T V, T N, T... Is>
struct power<T, V, N, std::integer_sequence<T, Is...>> {
static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));
};
int main() {
std::cout << power<int, 4, 2>::value << std::endl;
}
Ручное различие типов при задании любого типа T
При реализации SFINAE с использованием std::enable_if
часто бывает полезно иметь доступ к вспомогательным шаблонам, который определяет, соответствует ли данный тип T
набору критериев.
Чтобы помочь нам в этом, стандарт уже предоставляет два типа аналоговых std::true_type
true
и false
которые являются std::true_type
и std::false_type
.
В следующем примере показано, как определить, является ли тип T
указателем или нет, шаблон is_pointer
имитирует поведение стандартного помощника std::is_pointer
:
template <typename T>
struct is_pointer_: std::false_type {};
template <typename T>
struct is_pointer_<T*>: std::true_type {};
template <typename T>
struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }
В приведенном выше коде есть три шага (иногда вам нужно только два):
Первое объявление
is_pointer_
является случаем по умолчанию и наследуется отstd::false_type
. Случай по умолчанию должен всегда наследоваться отstd::false_type
поскольку он аналогичен «false
состоянию».Второе объявление специализируется на шаблоне
is_pointer_
для указателяT*
не заботясь о том, что такоеT
Эта версия наследуется отstd::true_type
.Третья декларация (реальная) просто удаляет ненужную информацию из
T
(в этом случае мы удаляемconst
иvolatile
квалификаторы), а затем отступаем к одной из двух предыдущих деклараций.
Поскольку is_pointer<T>
является классом, для доступа к его значению необходимо:
- Use
::value
, напримерis_pointer<int>::value
-value
- статический член класса типаbool
унаследованный отstd::true_type
илиstd::false_type
; - Построить объект этого типа, например
is_pointer<int>{}
- Это работает, потому чтоstd::is_pointer
наследует свой конструктор по умолчанию изstd::true_type
илиstd::false_type
(которые имеют конструкторыconstexpr
) и обаstd::true_type
иstd::false_type
имеютconstexpr
преобразованияconstexpr
дляbool
.
Это хорошая привычка предоставлять «вспомогательные шаблоны помощников», которые позволяют вам напрямую получать доступ к значению:
template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
В C ++ 17 и выше большинство вспомогательных шаблонов уже предоставляют версию _v
, например:
template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;
Если-то-иначе
Тип std::conditional
в заголовке стандартной библиотеки <type_traits>
может выбрать один или другой тип на основе логического значения времени компиляции:
template<typename T>
struct ValueOrPointer
{
typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};
Эта структура содержит указатель на T
если T
больше размера указателя или самого T
, если он меньше или равен размеру указателя. Поэтому sizeof(ValueOrPointer)
всегда будет <= sizeof(void*)
.
Generic Min / Max с переменным аргументом count
Можно написать обобщенную функцию (например, min
), которая принимает различные числовые типы и произвольное количество аргументов по метапрограмме шаблона. Эта функция объявляет min
для двух аргументов и рекурсивно для большего.
template <typename T1, typename T2>
auto min(const T1 &a, const T2 &b)
-> typename std::common_type<const T1&, const T2&>::type
{
return a < b ? a : b;
}
template <typename T1, typename T2, typename ... Args>
auto min(const T1 &a, const T2 &b, const Args& ... args)
-> typename std::common_type<const T1&, const T2&, const Args& ...>::type
{
return min(min(a, b), args...);
}
auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);