EXPLORATION 70

image

Metaprogramming

Metaprogramming is the act of writing programs that run during the compilation of an ordinary program. Most metaprograms use templates to operate on types, but you can also write constexpr functions to compute values at compile time. Metaprogramming requires a different style than ordinary programming, and you will find yourself calling upon almost all the programming techniques you have learned so far.

Use constexpr for Compile-Time Values

In C++ 03, the only way to work with numeric values at compile time was to use templates and metaprogramming. In C++ 11, a better way is to use constexpr functions. To compare these two styles of programming, consult Listing 51-6, which presents the power10 function to compute powers to 10 at compile time. The equivalent function can also be implemented with templates, as shown in Listing 70-1, which was how the first edition of this book computed a power of 10 at compile time.

Listing 70-1.  Computing a Power of 10 at Compile Time with Templates

template<int N>
struct power10; // forward declaration
 
template<int N>
struct square
{
  // A metaprogramming function to square a value.
  enum t { value = N * N };
};
 
template<int N, bool Even>
struct power10_aux
{
  // Primary template is for odd N (Even is false)
  enum t { value = 10 * power10<N - 1>::value };
};
 
template<int N>
struct power10_aux<N, true>
{
  // Specialization when N is even: square the value
  enum t { value = square<power10<N / 2>::value>::value };
};
 
template<int N>
struct power10
{
  enum t { value = power10_aux<N, N % 2 == 0>::value };
};
 
template<>
struct power10<0>
{
  enum t { value = 1 };
};

The metaprogram, like almost all template metaprograms, uses template specialization and partial specialization. This metaprogram uses only techniques that you have learned over the course of this book, but it is daunting to read and hard to follow. Metaprograms that compute values can often be written much more easily with constexpr functions, as you saw in Listing 51-6.

Metaprogramming with constexpr functions is slightly harder than writing ordinary functions but much easier than writing template metaprograms. Because the only executable statement you can place in a constexpr function is a return statement, you must learn to think in terms functional programming, using helper functions and recursion. You are limited to built-in types and custom types that can be constructed with constexpr constructors. But in spite of these limitations, you can achieve a lot, such as precomputing constants, or extending the language with user-defined literals.

User-defined literals were introduced in Exploration 25. They combine nicely with constexpr to extend the language at compile time. For example, if you often work with bit patterns, you probably write constants in hexadecimal, and you mentally map base 2 to and from base 16. It would be simpler if you could write literals directly in base 2. For example, suppose you want to write the following:

enum { answer = 00101010_binary }; // answer == 0x2a == 42

An enumerated literal must be a compile-time constant, so the _binary literal operator must be a constexpr function. The function interprets the series of digits as a binary number, as shown in Listing 70-2.

Listing 70-2.  Implementing the _binary User-Defined Literal Operator

/// Compute one bit of a binary integer.
/// Compute a new result by taking the right-most decimal digit from @p digits,
/// and if it is 1, shifting the 1 left by @p shift places and bitwise ORing
/// the value with @p result. Ignore digits other than 0 or 1. Recurse
/// to compute the remaining result.
/// @param digits The user-supplied decimal digits, should use only 0 and 1
/// @param result The numeric result so far
/// @param shift The number of places to shift the right-most digit in @p digits
/// @return if @p digits is zero, return @p result; otherwise return the result
/// of converting @p digits to binary
constexpr unsigned long long binary_helper(unsigned long long digits,
    unsigned long long result, int shift)
{
    return digits == 0 ?
        result :
        binary_helper(digits / 10,
            result | ((digits % 10 == 1) << shift),
            digits % 10 < 2 ? shift + 1 : shift);
}
 
constexpr unsigned long long operator"" _binary(unsigned long long digits)
{
   return binary_helper(digits, 0ULL, 0);
}

Because the body of a constexpr function must contain only a return statement, conditionals require the use of the conditional operator, which is harder to read than an if statement but not too difficult. The digits string is treated as though it were in base 2, ignoring any digits other than 1 or 0. (Error-handling is one difficulty with constexpr functions. There is no standard way for a constexpr function such as this _binary operator to report a compile-time error to the user. Later, you will learn a different way to write this operator that will allow some error-checking.) This particular implementation of the _binary literal is limited to binary numbers that look like unsigned long long decimal values, so you cannot express the full range of unsigned long long base 2 values. The next section presents a technique that can eliminate this restriction.

Variable-Length Template Argument Lists

You can define a template that takes any number of template arguments (called a variadic template). This ability gives rise to a number of programming tricks. Most of the tricks are used by library authors, but that doesn’t mean others can’t join in. To declare a template parameter that can accept any number of arguments, use an ellipsis after the class or typename keyword for a type template parameter, or after the type in a value template parameter. Such a parameter is called a parameter pack. Following are some simple examples:

template<class... Ts> struct List {};
template<int... Ns> struct Numbers {};

Instantiate the template with any number of template arguments:

typedef List<int> Int_type;
typedef List<char, unsigned char, signed char> Char_types;
typedef Numbers<1, 2, 3> One_two_three;

You can also declare a function parameter pack, so the function can take any number of arguments of any type, such as the following:

template<class... Types>
void list(Types... args);

When you call the function, the parameter pack contains the type of each function argument. In the following example, the compiler implicitly determines that the Types template argument is <int, char, std::string>.

list(1, 'x', std::string("yz"));

The sizeof... operator returns the number of elements in the parameter pack. You can, for example, define a Size template to compute the number of arguments in a parameter pack, as shown in the following:

template<class... Ts>
struct Size { constexpr static std::size_t value = sizeof...(Ts); };
static_assert(Size<int, char, long>::value == 3, "oops");

The static_assertdeclaration checks a compile-time Boolean expression and causes a compiler error if the condition is false. The string literal argument is the text of the message, and you will usually want something more informative than “oops.” Using static_assert is a great way to verify that a metaprogram does what you intend. It is useful in ordinary programs too. The more problems you can detect at compile time, the better.

To use a parameter pack, you typically expand it with a pattern followed by an ellipsis. The pattern can be the parameter name, a type that uses the parameter, an expression that uses the function parameter pack, etc.

Listing 70-3 shows a print() function that takes a stream followed by any number of arguments of any type. It prints each value by expanding the parameter pack. The std::forward() function forwards a value to a function without altering or copying it (called “perfect forwarding”). The compiler expands the pack expression std::forward<Types>(rest)... into std::forward(r) for each argument r in rest. By passing rvalue references everywhere and using std::forward(), the print() function can pass references to its arguments with a minimum of overhead. Notice that nowhere is there a test for the size of the parameter pack. The pack is expanded at compile time, and an overloaded function ends the expansion when the pack is empty.

Listing 70-3.  Using a Function Parameter Pack to Print Arbitrary Values

#include <iostream>
#include <utility>
 
// Forward declaration.
template<class... Types>
void print(std::ostream& stream, Types&&...);
 
// Print the first value in the list, then recursively
// call print() to print the rest of the list.
template<class T, class... Types>
void print_split(std::ostream& stream, T&& head, Types&& ... rest)
{
   stream << head << ' ';
   print(stream, std::forward<Types>(rest)...);
}
 
// End recursion when there are no more values to print.
void print_split(std::ostream&)
{}
 
// Print an arbitrary list of values to a stream.
template<class... Types>
void print(std::ostream& stream, Types&&... args)
{
   print_split(stream, std::forward<Types>(args)...);
}
 
int main()
{
   print(std::cout, 42, 'x', "hello", 3.14159, 0, ' '),
}

You can use a function parameter pack with a user-defined literal too. Instead of taking an unsigned long long argument, you can implement the operator as a template function. The template arguments are the characters that make up the literal. Thus if the source code contains 00101010_binary, the template arguments are <'0', '0', '1', '0', '1', '0', '1', '0'>. Because you don’t know beforehand how many template arguments to expect, you have to use a parameter pack, as shown in Listing 70-4.

Listing 70-4.  Using a Function Parameter Pack to Implement the _binary Operator

/// Extract one bit from a bit string and then recurse.
template<char Head, char... Rest>
struct binary_helper
{
   constexpr unsigned long long operator()(unsigned long long result) const;
};
 
/// Teminate recursion when interpreting a bit string.
template<char Head>
struct binary_helper<Head>
{
   constexpr unsigned long long operator()(unsigned long long result) const;
};
    
template<char Head, char... Rest>
constexpr unsigned long long
binary_helper<Head, Rest...>::operator()(unsigned long long result)
const
{
   static_assert(Head == '0' or Head == '1', "_binary contains only 0 or 1");
   return binary_helper<Rest...>{}(result << 1 | (Head - '0'));
}
 
template<char Head>
constexpr unsigned long long
binary_helper<Head>::operator()(unsigned long long result)
const
{
   static_assert(Head == '0' or Head == '1', "_binary contains only 0 or 1");
   return result << 1 | (Head - '0'),
}
 
template<char... Digits>
constexpr unsigned long long operator"" _binary()
{
   return binary_helper<Digits...>{}(0ULL);
}

By now, you should recognize the familiar pattern. A helper function strips the first template argument and recursively calls itself with the remaining parameter pack. Partial specialization terminates the recursion. Functions cannot be partially specialized, so Listing 70-3 partially specializes the binary_helper functor.

As a side benefit, using a template gives you the opportunity for additional error-checking with static_assert. Because static_assert does not produce any executable code, you can have a static_assert in a constexpr function.

What does your compiler do when you try the following declaration after defining_binary?

constexpr int two = 2_binary;

You should get a message about the static_assert failing.

Types as Values

Metaprogramming with values is made easier with constexpr functions, but much metaprogramming involves types, which requires an entirely different viewpoint. When metaprogramming with types, a type takes on the role of a value. There is no way to define a variable, only template arguments, so you devise templates that declare the template parameters you need to store type information. A “function” in a metaprogram (sometimes called a metafunction) is just another template, so its arguments are template arguments.

For example, the standard library contains the metafunction is_same (defined in <type_traits>). This template takes two template arguments and yields a type as its result. The metafunctions in the standard library return a result with class members. If the result is a type, the member typedef is called type. The type member for a predicate such as is_same is a metaprogramming Boolean value. If the two argument types are the same type, the result is std::true_type (also defined in <type_traits>). If the arguments types are different, the result is std::false_type.

Because true_type and false_type are themselves metaprogramming types, they also have type member typedefs. The value of true_type::type is true_type; ditto for false_type. Sometimes a metaprogram has to treat a metaprogramming value as an actual value. Thus, metaprogramming types that represent values have a static data member named value. As you may expect, true_type::value is true and false_type::value is false.

How would you write is_same? You have to declare the member typedef typeto std::true_type or std::false_type, depending on the template arguments. An easy way to do this, and to obtain the convenience value static data member at the same time, is to derive is_same from true_type or false_type, depending on the template arguments. This is a straightforward implementation of partial specialization, as you can see in Listing 70-5.

Listing 70-5.  Implementing the is_same Metafunction

template<class T, class U>
struct is_same : std::false_type {};
 
template<class T>
struct is_same<T, T> : std::true_type {};

Let’s write another metafunction, one that is not in the standard library. This one is called promote. It takes a single template argument and yields int if the template argument is bool, short, char, or variations and yields the argument itself otherwise. In other words, it implements a simplified subset of the C++ rules for integer promotion. How would you write promote? This time, the result is pure type, so there is no value member. The simplest way is the most direct. Listing 70-6 shows one possibility.

Listing 70-6.  One Implementation of the promote Metafunction

template<class T> struct promote          { typedef T type; };
template<> struct promote<bool>           { typedef int type; };
template<> struct promote<char>           { typedef int type; };
template<> struct promote<signed char>    { typedef int type; };
template<> struct promote<unsigned char>  { typedef int type; };
template<> struct promote<short>          { typedef int type; };
template<> struct promote<unsigned short> { typedef int type; };

Another way to implement promote is to use template parameter packs. Suppose you have a metafunction, is_member, which tests its first argument to determine whether it appears in the parameter pack formed by its remaining arguments. That is, is_member<int, char> is false_type, and is_member<int, short, int, long> yields true_type. Given is_member, how would you implement promote? Listing 70-7 shows one way, using partial specialization on the result of is_member.

Listing 70-7.  Another Implementation of the promote Metafunction

// Primary template when IsMember=std::true_type.
template<class IsMember, class T>
struct get_member {
   typedef T type;
};
 
template<class T>
struct get_member<std::false_type, T>
{
   typedef int type;
};
 
template<class T>
struct promote {
    typedef typename get_member<typename is_member<T,
        bool, unsigned char, signed char, char, unsigned short, short>::type, T>::type type;
};

Remember that typename is required when naming a type that depends on a template parameter. The type member of a metafunction certainly qualifies as a dependent type name. This implementation uses partial specialization to determine the result from is_member. Using is_member to implement promote might seem to be more complicated, but if the list of types is long, or is likely to grow as an application evolves, the is_member approach seems more inviting. Although using is_member is easy, writing it is not so easy. Remember how Listing 70-4 splits off the head of the function pack? Use the same technique to split the parameter pack, that is, write a helper class that has a Head template parameter and a Rest template parameter pack. Listing 70-8 shows one way to implement is_member.

Listing 70-8.  Implementing the is_member Metafunction

template<class Check, class... Args> struct is_member;
 
// Helper metafunction to separate Args into Head, Rest
template<class Check, class Head, class... Rest>
struct is_member_helper :
    std::conditional<std::is_same<Check, Head>::value,
        std::true_type,
        is_member<Check, Rest...>>::type
{};
 
/// Test whether Check is the same type as a type in Args.
template<class Check, class... Args>
struct is_member : is_member_helper<Check, Args...> {};
 
// Partial specialization for empty Args
template<class Check>
struct is_member<Check> : std::false_type {};

Instead of writing a custom metafunction that specializes on std::false_type, Listing 70-8 uses a standard metafunction, std::conditional. It is usually better to use the standard library whenever possible, and you can rewrite Listing 70-7 to use std::conditional. To help you understand this important metafunction, the next section discusses std::conditional in depth.

Conditional Types

One key aspect of metaprogramming is making decisions at compile time. To do that, you need a conditional operator. The standard library offers two styles of conditionals in the <type_traits> header.

To test a condition, use std::conditional<Condition, IfTrue, IfFalse>::type. The Condition is a bool value, and IfType and IfFalse are types. The type member is a typedef for IfTrue if Condition is true and is a typedef for IfFalse if Condition is false.

Try writing your own implementation of std::conditional. Your standard library may be different but won’t be to terribly different from my solution in Listing 70-9.

Listing 70-9.  One Way to Implement std::conditional

template<bool Condition, class IfTrue, class IfFalse>
struct conditional
{
    typedef IfFalse type;
};
 
template<class IfTrue, class IfFalse>
struct conditional<true, IfTrue, IfFalse>
{
   typedef IfTrue type;
};

Another way to look at std::conditional is to consider it an array of two types, indexed by a bool value. What about an array of types indexed by an integer? The standard library doesn’t have such a template, but you can write one. Use a template parameter pack and an integer selector. If the selector is invalid, do not define the type member typedef. For example, choice<2, int, long, char, float, double>::type would be char, and choice<2, int, long> would not declare a type member. Try writing choice. Again, you will probably want two mutually  recursive classes. One class strips the first template parameter from the parameter pack and decrements the index. Template specialization terminates the recursion. Compare your solution with mine in Listing 70-10.

Listing 70-10.  Implementing an Integer-Keyed Type Choice

#include <cstddef>
#include <type_traits>
 
// forward declaration
template<std::size_t, class...>
struct choice;
 
// Default: subtract one, drop the head of the list, and recurse.
template<std::size_t N, class T, class... Types>
struct choice_split {
    typedef typename choice<N-1, Types...>::type type;
};
 
// Index 0: pick the first type in the list.
template<class T, class... Ts>
struct choice_split<0, T, Ts...> {
    typedef T type;
};
 
// Define type member as the N-th type in Types.
template<std::size_t N, class... Types>
struct choice {
    typedef typename choice_split<N, Types...>::type type;
};
 
// N is out of bounds
template<std::size_t N>
struct choice<N> {};
 
// Tests
 
static_assert(std::is_same<int,
  typename choice<0, int, long, char>::type>::value, "error in choice<0>");
static_assert(std::is_same<long,
  typename choice<1, int, long, char>::type>::value, "error in choice<1>");
static_assert(std::is_same<char,
  typename choice<2, int, long, char>::type>::value, "error in choice<2>");

Use the new choice template to choose one option from among many. On one project, I defined three styles of iterators for different trade-offs of safety and performance. The fast iterator worked as fast as possible, with no safety checks. The safe iterator would check just enough to avoid undefined behavior. The pedantic iterator was used for debugging and checked everything possible, with no regard for speed. I could pick which iterator style I wanted by defining ITERATOR_TYPE as 0, 1, or 2, for example:

typedef typename choice<ITERATOR_TYPE,
    pedantic_iterator, safe_iterator, fast_iterator>::type iterator;

Checking Traits

A variation on std::conditional is std::enable_if. Like conditional, enable_if uses a bool template argument to choose a type that it uses to declare its type member typedef. The difference is that if the condition is false, enable_if does not define any type member at all (like choice when the index is out of bounds). Use enable_if to enable or disable function signatures or entire classes.

A silly, but simple, example is to define a minus function template. It takes a signed numeric argument and returns the arithmetic negation. But what if the argument is not signed? By using enable_if, you can ensure that the function template is defined only for signed types. Use std::numeric_limits (refer to Exploration 25, if you need a reminder about numeric_limits) to determine whether a type is signed. Use enable_if to enable the function only when is_signed is true, as shown in Listing 70-11.

Listing 70-11.  Implementing an Integer-Keyed Type Choice

#include <limits>
#include <type_traits>
 
template<class T>
typename std::enable_if<std::numeric_limits<T>::is_signed, T>::type
minus(T const& x)
{
   return -x;
}

The use of enable_if is intimidating, so let’s take it one step at a time. The minus() function template can take any argument type, T. The numeric_limits traits are tested to determine whether T is signed. If so, enable_if’s type member will be a typedef for T, which is exactly what we want the return type to be. But if is_signed is false, enable_if does not declare any type member, and the compiler will issue an error, thereby preventing the user from calling minus() incorrectly.

You can also use enable_if to enable or disable entire classes. For example, suppose you want to restrict rational’s template argument to integral types. Add a template argument and supply a default argument that uses enable_if to test whether the first template argument is an integer. If the enable_if fails, the compiler will reject the entire template. Listing 70-12 shows a skeleton of the rational class, using enable_if.

Listing 70-12.  Specializing the rational Class Using enable_if

#include <limits>
#include <type_traits>
 
template<class T, class Enable = typename std::enable_if<std::numeric_limits<T>::is_integer, T>::type>
class rational {
public:
   ... normal class definition here ...
};
 
rational<long> okay{1, 2};
rational<float> problem{1, 2};

Another enhancement to rational is to limit the floating-point conversion functions to types that are truly floating-point. Use enable_if the same way you did minus() to write the convert function (Exploration 48) or an explicit type-conversion operator (Exploration 68) as a member of rational, as follows:

template<class T>
operator typename std::enable_if<std::is_floating_point<T>::value, T>::type()
{
    return static_cast<T>(numerator()) / static_cast<T>(denominator());
}

The code is hard to read, so creating an intermediate type is helpful.

template<class T>
using EnableIfFloat =
    typename std::enable_if<std::is_floating_point<T>::value, T>::type;
 
template<class T>
operator typename EnableIfFloat<T>::type ()
{
    return static_cast<T>(numerator()) / static_cast<T>(denominator());
}

The enable_if class template is a powerful tool to enforce compile-time rules. It is even more powerful if you don’t use it to stop compilation but to enable the compiler to continue. The following section discusses this programming technique.

Substitution Failure Is Not An Error (SFINAE)

A programming technique introduced by Daveed Vandevoorde goes by the unwieldy name of SFINAE (pronounced ess-finn-ee), for Substitution Failure Is Not An Error. Briefly, if the compiler attempts to instantiate an invalid template function, the compiler does not consider it an error but merely discards that instantiation from consideration when resolving overloads. Such a simple concept turns out to be extremely useful.

Not all uses of enable_if are to cause compile-time errors. You can also use enable_if with SFINAE to affect overload resolution. For example, suppose you are writing data in some data encoding, such as ASN.1 BER, XDR, JSON, etc. The details of the encoding are unimportant for this exercise. What matters is that we want to treat all integers the same, and all floating-point numbers the same, but treat integers differently from floating-point numbers. That is, we want to use templates to reduce the amount of repetitive coding, but we want distinct implementations for certain types. We cannot partially specialize functions, so we must use overloading.

The problem is how to declare three template functions, all with the name encode, such that one is a template function for any integer, another for any floating-point type, and another for strings.

One approach is to declare overloads for the largest integer type and largest floating-point type. The compiler will convert the actual types to the larger types. This is simple to implement but incurs a runtime cost, which can be significant in some environments. We need a better solution.

Using enable_if, you can declare overloaded encode functions but enable one function only when is_integral is true, another for floating-point types, and so on. The goal is not to disable the encode() function but to guide the compiler’s resolution of overloading.

The <type_traits> header has several introspection traits. Every type is categorized as class, enumeration, integer, floating-point, and so on. Just to show how they work, I will use std::is_integral instead of numeric_limits to test for integral types. This book is not about binary data encoding, so the guts of this example will write text to a stream, but it will serve to illustrate how to use enable_if.

The normal rules of overloading still apply. That is, different functions must have different arguments. So using enable_if for the return type doesn’t help. This time, enable_if will be used as another argument to encode, but with a default value that hides it from the caller. (Note that using for the primary argument to the function doesn’t work, because it breaks the compiler’s ability to deduce the template type from the function’s argument type.) Specifically, the enable_if argument is made into a pointer type, with nullptr as the default value, to ensure that there is no extra code constructing or passing this additional argument. With inlining, the compiler can even optimize away the extra argument, so there is no runtime penalty. Listing 70-13 shows one way to solve this problem.

Listing 70-13.  Using enable_if to Direct Overload Resolution

#include <iostream>
#include <type_traits>
 
template<class T>
void encode(std::ostream& stream, T const& int_value,
   typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr)
{
   // All integer types end up here.
   stream << "int: " << int_value << ' ';
}
 
template<class T>
void encode(std::ostream& stream, T const& enum_value,
   typename std::enable_if<std::is_enum<T>::value>::type* = nullptr)
{
   // All enumerated types end up here.
   // Record the underlying integer value.
   stream << "enum: " <<
      static_cast<typename std::underlying_type<T>::type>(enum_value) << ' ';
}
 
template<class T>
void encode(std::ostream& stream, T const& float_value,
   typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr)
{
   // All floating-point types end up here.
   stream << "float: " << float_value << ' ';
}
 
// enable_if forms cooperate with normal overloading
void encode(std::ostream& stream, std::string const& string_value)
{
   stream << "str: " << string_value << ' ';
}
 
int main()
{
   encode(std::cout, 1);
   enum class color { red, green, blue };
   encode(std::cout, color::blue);
   encode(std::cout, 3.0);
   encode(std::cout, std::string("string"));
}

That concludes your exploration of C++ 11. The next and final Exploration is a capstone project to incorporate everything you have learned. I hope that you have enjoyed your journey and that you will plan many more excursions to complete your understanding and mastery of this language.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset