Chapter 3. C++ Metaprogramming and Application Design

Chapter 2 gave you a taste of how powerful type manipulation can be and how it can make code more compact, generic, and elegant, and less error prone.

As in all automation systems, metaprogramming requires some tools to avoid rewriting the same code over and over. This chapter will go over the basic components of such a toolbox. We also will try to extract axioms (small, self-contained elements of knowledge) to apply in our everyday jobs as metaprogram developers.

Compile-Time Versus Runtime Paradigms

C++ runtime code is based on the fact that users can define functions that will operate on values represented by some data type (either native or user defined) to produce new values or side effects. A C++ program is then the orchestration of said function calls to advance the application’s goal at runtime.

If those notions of functions, values, and types defined by runtime behavior are pretty straightforward, things become blurry when speaking about their compile-time equivalents.

Values at Compile Time

Compile-time computations need to operate on values defined as valid at compile time. Here’s what this notion of compile-time values covers:

  • Types, which are entities defined at compile time
  • Integral constants, defined by using the constexpr keyword or passed to the template class as template parameters.

Unfortunately, types and integral constants are two completely different beasts in C++. Moreover, there is currently no way in C++11/14/17 to specify a template parameter to be either a type or an integral constant.1 To be able to work with both kinds of values (types and integral constants), we need a way to make both kinds of values homogeneous.

The standard way to do so, which stems from Boost.MPL, is to wrap the value in a type. We can do this by using std::integral_constant, which we can implement roughly as follows:

template<typename T, T Value>
struct integral_constant
{
  using type = T;
  static constexpr T value = Value;
};

This structure is a simple box containing a value. This box’s type is dependent on both the value and the value type, making it unambiguous.2 We can later retrieve either the value type through the ::type internal type definition or the numerical value through the ::value constant.

Because integral constants can be turned easily into types, we can consider that the only required flavor of compile-time values is type. This is such a strong proposition that we will define this as one of our basic axioms in metaprogramming.

Meta-Axiom #1

Types are first-class values inside compile-time programs.

Functions at Compile Time

We can view runtime functions as mappings between values of some types and results of a given type (which might be void). In a perfect world, such functions would behave the same way as their mathematical, pure equivalents, but in some cases we might need to trigger side effects (like I/O or memory access).

Functions at compile time live in a world where no side effects can occur. They are, by their very nature, pure functions living in a small functional language embedded in C++. As per our first axiom, the only interesting values in metaprogramming are types. Thus, compile-time functions are components that map types to other types.

This statement looks familiar. It is basically the same definition as that for runtime functions, with “values” replaced by “types.” The question now is, how can we specify such a component when it’s clear C++ syntax won’t let us write return float somewhere?

Again, we take advantage of the pioneering work of Boost.MPL by reusing its notion of the metafunction. Quoting from the documentation, a metafunction is “a class or a class template that represents a function invocable at compile-time.” Such classes or class templates follow a simple protocol. The inputs of the metafunctions are passed as template parameters and the returned type is provided as an internal type definition.

A simple metafunction can be written as follows:

template<class T>
struct as_constref
{
  using type = T const&;
};

As its name implies, this metafunction turns a type into a reference to a constant value of said type.3 Invoking a metafunction is just a matter of accessing its internal ::type, as demonstrated here:

using cref = as_constref<float>::type;

This principle has already leaked from MPL into the standard. The type_traits header provides a large number of such metafunctions, supporting for analyzing, creating, or modifying types based on their properties.

Pro Tip

Most of the basic needs for type manipulation are provided by type_traits. We strongly advise any metaprogrammer-in-training to become highly familiar with this standard component.

Type Containers

C++ runtime development relies on the notion of containers to express complex data manipulations. Such containers can be defined as data structures holding a variable number of values and following a given schema of storage (contiguous cells, linked cells, and so on). We can then apply operations and algorithms to containers to modify, query, remove, or insert values. The STL provides pre-made containers, like list, set, and vector.

How can we end up with a similar concept at compile time? Obviously, we cannot request memory to be allocated to store our values. Moreover, our “values” actually being types, such storage makes little sense. The logical leap we need to make is to understand that containers are also values, which happen to contain zero or more other values; if we apply our systematic “values are types” motto, this means that compile-time containers must be types that contain zero or more other types. But how can a type contain another type? There are multiple solutions to this issue.

The first idea could be to have a compile-time container be a type with a variable number of internal using statements, as in the following example:

struct list_of_ints
{
  static constexpr std::size_t size = 4;
  using element0 = char;
  using element1 = short;
  using element2 = int;
  using element3 = long;
};

There are a few issues with this solution, though. First, there is no way to add or remove types without having to construct a new type. Then, accessing a given type is complex because it requires us to be able to map an integral constant to a type name.

Another idea is to use variadic templates to store types as the parameter pack of a variadic type. Our list_of_ints then becomes the following:

template<typename... Values> struct meta_list {};
using list_of_ints = meta_list<chr,short,int,long>;

This solution has neither of the aforementioned drawbacks. Operations on this meta_list can be carried out by using the intrinsic properties of the parameter pack, because no name mapping is required. Insertion and removal of elements is intuitive; we just need to play with the contents of the parameter pack.

Those properties of variadic templates define our second axiom of metaprogramming: the fact that any variadic template structure is de facto a compile-time container.

Meta-Axiom #2

Any template class accepting a variable number of type parameters can be considered a type container.

Compile-Time Operations

We now have defined type containers as arbitrary template classes with at least a template parameter pack parameter. Operations on such containers are defined by using the intrinsic C++ support for template parameter packs.

We can do all of the following:

  • Retrieve information about the pack.
  • Expand or shrink the pack’s contents.
  • Rewrap the parameter pack.
  • Apply operations on the parameter pack’s contents.

Using Pack-Intrinsic Information

Let’s try to make a simple metafunction that operates on a type container by writing a way to access a container’s size:

template<class List> struct size;
 
template<template<class...> class List, class... Elements> 
struct size<List<Elements...>> 
     : std::integral_constant<std::size_t, sizeof...(Elements)> 
{};

Let’s go over this snippet. First, we declare a size structure that takes exactly one template parameter. At this point, the nature of this parameter is unknown; thus, we can’t give size a proper definition. Then, we partially specialize size for all of the types of the form List<Elements...>. The syntax is a bit daunting, so let’s decompose it. The template parameters of this specialization comprise the following:

List

A template template parameter awaiting a template parameter pack as an argument

Elements
A template parameter pack

From those two parameters, we specialize size<Elements...>. We can write this as Elements..., which will trigger an expansion of every type in the pack, which is exactly what List requires in its own parameters. This technique of describing the variadic structure of a type container so that an algorithm can be specified will be our main tool from now on.

Take a look at how we can use this compile-time algorithm and how the compiler interprets this call. Consider the following as we try to evaluate the size of std::tuple<int,float,void>:

constexpr auto s = size<std::tuple<int,float,void>>::value;

By the definition of std::tuple, this call will match the size<List<Elements...>> specialization. Rather trivially, List will be substituted by std::tuple and Elements will be substituted by the parameter pack {int, float, void}. When it is there, the sizeof... operator will be called and will return 3. size will then inherit publicly from std::integral_constant<std::size_t,3> and forward its internal value constant. We could have used any kind of variadic structure instead of a tuple, and the process would have been similar.

Adding and Removing Pack Elements

The next natural step is to try to modify the elements inside a type container. We can do this by using the structural description of a parameter pack. As an example, let’s try to write push_back<List,Element>, which inserts a new element at the end of a given type container.

The implementation starts in a now-familiar way:

template<class List, class New> struct push_back;

As for size, we declare a push_back structure with the desired type interface but no definition. The next step is to specialize this type so that it can match type containers and proceed:

template<template<class...> class List, 
class... Elements, class New>

struct push_back<List<Elements...>, New> 
{
  using type = List<Elements...,New>;
};

As compile-time metaprogramming has no concept of values, our only way to add an element to an existing type container is to rebuild a new one. The algorithm is pretty simple: expand the existing parameter pack inside the container and add one more element at the end. By the definitions of List and ..., this builds a new valid type where the New element has been inserted at the end.

Exercise

Can you infer the implementation for push_front?

Removal of existing elements in a type container follows a similar reasoning but relies on the recursive structure of the parameter pack. Fear not! As we said earlier, recursion in template metaprogramming is usually ill advised, but here we will only exploit the structure of the parameter pack and we won’t do any loops. Let’s begin with the bare-bones code for a hypothetical remove_front algorithm:

template<class List> struct remove_front;

template<template<class...> class List, class... Elements>
struct remove_front<List<Elements...>>
{
  using type = List</* what goes here??? */>;
};

As you can see, we haven’t diverged much from what we’ve seen so far. Now, let’s think about how we can remove the first type of an arbitrary parameter pack so that we can complete our implementation. Let’s enumerate the cases:

List<Elements...>
Contains at least one element (the head) and a potentially empty pack of other types (the tail). In this case, we can write it as List<Head, Tail...>
List<Elements...>
This is empty. In this case, it can be written as List<>.

If we know that a head type exists, we can remove it. If the list is empty, the job is already done. The code then reflects this process:

template<class List> struct remove_front;

template<template<class...> class List
        , class Head, class... Elements>
struct remove_front<List<Head,Elements...>>
{
  using type = List<Elements...>;
};

template<template<class...> class List>
struct remove_front<List<>>
{
  using type = List<>;
};

This introspection of the recursive nature of the parameter pack is another tool in our belt. It has some limitations, given that decomposing a pack into a list of heads and a single tail type is more complex, but it helps us build basic blocks that we can reuse in more complex contexts.

Pack Rewrapping

So far, we’ve dealt mostly with accessing and mutating the parameter pack. Other algorithms might need to work with the enclosing type container.

As an example, let’s write a metafunction that turns an arbitrary type container into a std::tuple. How can we do that? Because the difference between std::tuple<T...> and List<T...> is the enclosing template type, we can just change it, as shown here:

template<class List> struct as_tuple;

template<template<class..> class List, class... Elements> 
struct as_tuple<List<Elements...>>
{
  using type = std::tuple<Elements...>;
};

But wait: there’s more! Changing the type container to tuple or variant or anything else can actually be generalized by passing the new container type as a parameter. Let’s generalize as_tuple into rename:

struct rename;

template<template<class..> class Container
        , template<class..> class List
        , class... Elements
        > 
struct rename<Container, List<Elements...>>
{
  using type = Container<Elements...>;
};

The code is rather similar. We use the fact that a template template parameter can be passed naturally to provide rename with its actual target. A sample call can then be as follows:

using my_variant = rename<boost::variant
                         , std::tuple<int,short>
                         >;
Note

This technique was explained by Peter Dimov in his blog in 2015 and instigated a lot of discussion around similar techniques.

Container Transformations

These tools—rewrapping, iteration, and type introspection for type containers—lead us to the final and most interesting metaprograms: container transformations. Such transformations, directly inspired by the STL algorithms, will help introduce the concept of structured metaprogramming.

Concatenating containers

A first example of transformation is the concatenation of two existing type containers. Considering any two lists L1<T1...> and L2<T2...>, we wish to obtain a new list equivalent to L1<T1...,T2...>.

The first intuition we might have coming from our runtime experience is to find a way to “loop” over types as we repeatedly call push_back. Even if it’s a correct implementation, we need to fight this compulsion of thinking with loops. Loops over types will require a linear number of intermediate types to be computed, leading to unsustainable compilation times. The correct way of handling this use case is to find a natural way to exploit the variadic nature of our containers.

In fact, we can look at append as a kind of rewrapping in which we push into a given variadic structure more types than it contained before. A sample implementation can then be as follows:

template<typename L1, typename L2> struct append;

tempate< template<class...> class L1, typename... T1
       , template<class...> class L2, typename... T2
       >
struct append< L1<T1...>, L2<T2...> >
{
  using type = L1<T1...,T2...>;
};

After the usual declaration, we define append as awaiting two different variadic structures filled with two distinct parameter packs. Note that, as with regular specialization on nonvariadic templates, we can use multiple parameter packs as long as they are wrapped properly in the specialization. We now have access to all of the elements required. The result is computed as the first variadic type instantiated with both parameter packs expanded.

Pro Tip

Dealing with compile-time containers requires no loops. Try to express your algorithm as much as possible as a direct manipulation of parameter packs.

Toward a compile-time transform

The append algorithm was rather straightforward. Let’s now hop to a more complex example: a compile-time equivalent to std::transform. Let’s first state what the interface of such a metaprogram could be. In the runtime world, std::transform calls a callable object over each and every value of the target container and fills another container with the results. Again, this must be transposed to a metafunction that will iterate over types inside a parameter pack, apply an arbitrary metafunction, and generate a new parameter pack to be returned.

Even if “iterating over the contents of a parameter pack using ...” is a well-known exercise, we need to find a way to pass an arbitrary metafunction to our compile-time transform variant. A runtime callable object is an object providing an overload for the so-called function call operator—usually denoted operator(). Usually those objects are regular functions, but they can also be anonymous functions (aka lambda functions) or full-fledged user-defined classes providing such an interface.

Generalizing metafunctions

In the compile-time world, we can pass metafunctions directly by having our transform metaprogram await a template template parameter. This is a valid solution, but as for runtime functions, we might want to bind arbitrary parameters of existing metafunctions to maximize code reuse.

Let’s introduce the Boost.MPL notion of the metafunction class. A metafunction class is a structure, which might or might not be a template, that contains an internal template structure named apply. This internal metafunction will deal with actually computing our new type. In a way, this apply is the equivalent of the generalized operator() of callable objects. As an example, let’s turn std::remove_ptr into a metafunction class:

struct remove_ptr
{
  template<typename T> struct apply
  {
    using type = typename std::remove_ptr<T>::type;
  };
};

How can we use this so-called metafunction class? It’s a bit different than with metafunctions:

using no_ptr = remove_ptr::apply<int*>::type;

Note the requirement of accessing the internal apply template structure. Wrapping this so that the end user is shielded from complexity is tricky.

Note how the metafunction class is no longer a template but relies on its internal apply to do its bidding. If you’re an astute reader, you will see that we can generalize this to convert any metafunction into a metafunction class. Let’s introduce the lambda metafunction:

template<template<class...> class MetaFunction>
struct lambda
{
  struct type
  {
    template<typename Args...> struct apply
    {
      using type = typename MetaFunction<Args...>::type;
    };
  };
};

This lambda structure is indeed a metafunction because it contains an internal type to be retrieved. This type structure is using pack expansion to adapt the template template parameter of lambda so that its usage is correct. Notice also that, like runtime lambda functions, this internal type is actually anonymous.

Implementing transform

We now have a proper protocol to pass metafunctions to our compile-time transform. Let’s write a unary transform that works on type containers:

template<typename List, typename F> struct transform;

template<template<class...> class List,
struct transform<List<Elems...>,F>
{
  using call = typename F::template apply<T>::type;
  using type = List< call<Elems>... >;
};

This code is both similar to what we wrote earlier and a bit more complex. It begins as usual by declaring and defining transform as acting on container types using a parameter pack. The actual code performs iterations over elements of the container using the classic ... approach. The addition we need to make is to call the metafunction class F over each type. We do this by taking advantage of the fact that ... will unpack and apply the code fragment on its left for every type in the pack. For clarity, we use an intermediate template using a statement to hold the actual metafunction class application to a single type.

Now, as an example, let’s call std::remove_ptr on a list of types:

using no_pointers = transform< meta_list<int*,float**, double>
                             , lambda<std::remove_ptr>
                             >::type;

Note the abstraction power of algorithms being transposed to the compile-time world. Here we used a high-level metafunction to apply a well-known pattern of computation on a container of types. Observe also how the lambda construct can help us make the use and reuse of existing metafunctions easier.

Pro Tip

Metafunctions follow similar rules to those for functions: they can be composed, bound, or turned into various similar yet different interfaces. The transition between metafunctions and metafunction classes is only the tip of the iceberg.

Advanced Uses of Metaprogramming

With a bit of imagination and knowledge, you can do things much more advanced than performing compile-time checks with template metaprogramming. The purpose of this section is just to give you an idea of what is possible.

A Revisited Command Pattern

The command pattern is a behavioral design pattern in which you encapsulate all the information required to execute a command into an object or structure. It’s a great pattern, which in C++ is often written with runtime polymorphism.

Putting aside the tendency of runtime polymorphism to induce the “factory of factories” antipattern, there can be a nonnegligible performance cost induced by vtables because they prevent the compiler from aggressively optimizing and inlining code.

The “Factory of Factories” Antipattern

This antipattern can happen in object-oriented programming when you spend more time writing code to manage abstractions than you do writing code to solve problems.

From a strictly software design point of view, it also forces you to relate objects together just because they will go through the same function at some point in time.

If generic programming has taught us anything, it’s that you don’t need to create a relation between objects just to make them use a function.

All you need to do is make the objects share common properties:

struct first_command
{
    std::string operator()(int) { /* something */ }
};

struct second_command
{
    std::string operator()(int) { /* something */ }
};

And have a function that accepts a command:

template <typename Command>
void execute_command(const Command & c, int param)
{
    c(param);
}

To which you will retort, “How do I transmit those commands through a structure, given that I know only at runtime which command to run?”

There are two ways to do it: manually, by using an unrestricted union, or by using a variant such as Boost.Variant. Template metaprogramming comes to the rescue because you can safely list the types of the commands in a type list and build the variant (or the union) from that list.

Not only will the code be more concise and more efficient, but it will also be less error prone: at compile time you will get an error if you forgot to implement a function, and the “pure virtual function call” is therefore impossible.4

Compile-Time Serialization

What do we mean by compile-time serialization? When you want to serialize an object, there are a lot of things you already know at compile time—and remember, everything you do at compile time doesn’t need to be done any more at runtime.

That means much faster serialization and more efficient memory usage.

For example, when you want to serialize a std::uint64_t, you know exactly how much memory you need, whereas when you serialize a std::vector<std::uint64_t>, you must read the size of the vector at runtime to know how much memory you need to allocate.

Recursively, it means that if you serialize a structure that is made up strictly of integers, you are able, at compile time, to know exactly how much memory you need, which means you can allocate the required intermediate buffers at compile time.

With template metaprogramming, you can branch, at compile time, the right code for serialization. This means that for every structure for which you are able to exactly compute the memory requirements, you will avoid dynamic memory allocation altogether, yielding great performance improvements and reduced memory usage.

Helper Functions and Libraries

Must you reinvent the wheel and write all your own basic functions, like we’ve seen in this chapter? Fortunately, no. Since C++11, a great number of helper functions have been included in the standard, and we strongly encourage you to use them whenever possible.

The standard isn’t yet fully featured when it comes to metaprogramming; for example, it lacks an official “list of types” type, algorithms, and more advanced metafunctions.

Fortunately, there are libraries that prevent you from needing to reinvent the wheel and that will work with all major compilers. This will save you the sweat of working around compilers’ idiosyncrasies and enable you to focus on writing metaprograms.

Boost comes with two libraries to help you with template metaprogramming:

MPL, written by Aleksey Gurtovoy and David Abrahams
A complete C++03 template metaprogramming toolbox that comes with containers, algorithms, and iterators. Unless you are stuck with a C++03 compiler, we would recommend against using this library.
Hana, written by Louis Dionne
A new metaprogramming paradigm, which makes heavy use of lambdas. Hana is notoriously demanding of the compiler.

The authors of this report are also the authors of Brigand, a C++11/14 metaprogramming library that nicely fills the gap between Boost.MPL and Boost.Hana.

We strongly encourage you to use existing libraries because they will help you structure your code and give you ideas of what you can do with metaprogramming.

Summary

In this chapter, we took a journey into the land of types within C++ and we saw that they can be manipulated like runtime values.

We defined the notion of type values and saw how such a notion can lead to the definition of type containers; that is, types containing other types. We saw how the expressiveness of parameter packs can lead to a no-recursion way of designing metaprograms. The small yet functional subset of classic container operators we defined showed the variety of techniques usable to design such metaprograms in a systemic way.

We hope we reached our goal of giving you a taste for metaprogramming and proving that it isn’t just some arcane technique that should not be used outside of research institutes. Whether you want to check out the libraries discussed herein, write your own first metaprogram, or revisit some code you wrote recently, we have only one hope: that by reading this report you learned something that will make you a better programmer.

1 If you think it could be useful, feel free to write such a proposal.

2 If you’re an astute reader with a more theoretical background, you might have recognized a crude implementation of Church numerals.

3 Let’s ignore the potential issue of adding a reference to a reference type.

4 To be fair, C++11 introduced a series of enhancements that allow the programmer to ensure at compile time that she hasn’t forgotten to implement a virtual function. That doesn’t eliminate the risk of invalid dynamic casts, though.

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

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