Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Chapter 16: Metaprogramming

In C++ Metaprogramming refers to the use of macros or templates to generate code at compile-time.

In general, macros are frowned upon in this role and templates are preferred, although they are not as generic.

Template metaprogramming often makes use of compile-time computations, whether via templates or constexpr functions, to achieve its goals of generating code, however compile-time computations are not metaprogramming per se.

Section 16.1: Calculating Factorials

Factorials can be computed at compile-time using template metaprogramming techniques.

#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 is a struct, but in template metaprogramming it is treated as a template metafunction. By convention, template metafunctions are evaluated by checking a particular member, either ::type for metafunctions that result in types, or ::value for metafunctions that generate values.

In the above code, we evaluate the factorial metafunction by instantiating the template with the parameters we want to pass, and using ::value to get the result of the evaluation.

The metafunction itself relies on recursively instantiating the same metafunction with smaller values. The factorial<0> specialization represents the terminating condition. Template metaprogramming has most of the restrictions of a functional programming language, so recursion is the primary "looping" construct.

Since template metafunctions execute at compile time, their results can be used in contexts that require compiletime values. For example:

int my_array[factorial<5>::value];

Automatic arrays must have a compile-time defined size. And the result of a metafunction is a compile-time constant, so it can be used here.

Limitation: Most of the compilers won't allow recursion depth beyond a limit. For example, g++ compiler by default

GoalKicker.com – C++ Notes for Professionals

86

limits recursion depeth to 256 levels. In case of g++, programmer can set recursion depth using -ftemplate-depth- X option.

Version ≥ C++11

Since C++11, the std::integral_constant template can be used for this kind of template computation:

#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"

}

 

Additionally, constexpr functions become a cleaner alternative.

#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';

}

The body of factorial() is written as a single statement because in C++11 constexpr functions can only use a

quite limited subset of the language.

Version ≥ C++14

Since C++14, many restrictions for constexpr functions have been dropped and they can now be written much more conveniently:

constexpr long long factorial(long long n)

{

if (n == 0) return 1;

else

return n * factorial(n - 1);

}

Or even:

constexpr long long factorial(int n)

{

long long result = 1;

GoalKicker.com – C++ Notes for Professionals

87

for (int i = 1; i <= n; ++i) { result *= i;

}

return result;

}

Version ≥ C++17

Since c++17 one can use fold expression to calculate 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;

}

Section 16.2: Iterating over a parameter pack

Often, we need to perform an operation over every element in a variadic template parameter pack. There are many ways to do this, and the solutions get easier to read and write with C++17. Suppose we simply want to print every element in a pack. The simplest solution is to recurse:

Version ≥ C++11

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...);

}

We could instead use the expander trick, to perform all the streaming in a single function. This has the advantage of not needing a second overload, but has the disadvantage of less than stellar readability:

Version ≥ C++11

template <class... Ts>

void print_all(std::ostream& os, Ts const&... args) { using expander = int[];

(void)expander{0,

(void(os << args), 0)...

};

}

For an explanation of how this works, see T.C's excellent answer.

Version ≥ C++17

With C++17, we get two powerful new tools in our arsenal for solving this problem. The first is a fold-expression:

GoalKicker.com – C++ Notes for Professionals

88

template <class... Ts>

void print_all(std::ostream& os, Ts const&... args) { ((os << args), ...);

}

And the second is if constexpr, which allows us to write our original recursive solution in a single function:

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...);

}

}

Section 16.3: Iterating with std::integer_sequence

Since C++14, the standard provides the class template

template <class T, T... Ints> class integer_sequence;

template <std::size_t... Ints>

using index_sequence = std::integer_sequence<std::size_t, Ints...>;

and a generating metafunction for it:

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>;

While this comes standard in C++14, this can be implemented using C++11 tools.

We can use this tool to call a function with a std::tuple of arguments (standardized in C++17 as 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);

GoalKicker.com – C++ Notes for Professionals

89