Meta-Sequences in C++

Dec 8, 2015

With the introduction of variadic templates to C++, meta-sequences became a central idiom in meta-programming. The standard implementation is not always the best choice.

The relation between C++ and C is somewhat of an anomaly; usually abstraction
hurts performance, yet C++ code often compiles to a more efficient executable
than its C equivalent. Templates deserve a lot of the credit for this, as they
allow the compiler to resolve at compile-time much of the abstraction required
by modular and maintainable code design. That’s what meta-programming is all
about.

In that respect, variadic
templates, introduced
in C++11, were a major improvement of the language. And with them came a new and
extremely useful idiom: meta-sequences. Below I’ll give a demonstration of a
problem - initializing an array at compile-time - for which meta-sequences
provide a good solution.

Since they are the key idea behind many elegant solutions to various problems,
it is not surprising that utilities for using them were included in the
standard library (since C++14). It
is surprising, though, that the implementation that comes with the STL is sometimes
problematic and may require using homemade alternatives (this is the case, for
example, in gcc 5.1.0). So I’ll discuss implementation details as well.

Compile-Time Array Initialization

As a simple scenario, consider the problem of initializing a
std::array<T,N> when T is a non-trivially constructible type:

All kinds of runtime hacks (e.g. switching to an array of pointers and
constructing its elements in a loop) are obviously evil. For example, they
introduce an extra level of indirection that obstructs the optimizer, and they
present an unattractive choice between heap allocations or custom
allocators.

Luckily, this problem has an easy and decent solution:

autopool=MakeArray<3>([](size_tindex){returnDummyType(index);});

where the function MakeArray will be defined shortly. This code is
evaluated, in compile time, to this:

autopool=std::array<int,N>{DummyType(0),DummyType(1),DummyType(2)};

So the compiler is in a position to eliminate copies, moves and function calls,
and generate in-place consturctions wherever possible. As a matter of fact, with
this main function -

the entire computation was done by the compiler (using gcc 5.1.0), and the
generated assembly code was essentially:

mov eax, 6
ret

The Function “MakeArray”

So how could a function like MakeArray be implemented? Using meta-
sequences of course. In C++14 std::integer_sequence became a standard part
of the language, as part of the header <utility>. Two very useful helpers
are the templates std::index_sequence and std::make_index_sequence.

Let’s look at the first template. The first argument is a callback that
returns elements to be placed in the array (here, it takes an
index as a parameter), and the second argument has no name. This means the
function never uses its value. Only its type matters. The method returns an
array whose elements’ type is the same type returned by the callable given as
the first argument, and whose length is equal to the length of the variadic
integer sequence S.

The value of S is deduced from the second, nameless, argument. In fact,
that’s the raison d’etre of that second argument. It has no other purpose. The
template index_sequence<> is an empty class, which is nothing more than a
placeholder for the sequence S. In essence, this is it:

The function make_index_sequence is a mechanism for constructing a
sequence such as index_sequence<0,1,2,3,4,5,6,7,8,9,10,11...,N-1> from the
integer N. It is used by the second template, whose job is to allow the
user to do the same by simply writing:

autoarr=MakeArray<12>(ctor);

And this is it.

That was just one scenario in which meta-sequences shine. There are many others.
But I think it’s best not to make this post into a long list of random examples,
and it’s likely that later posts will provide real-life scenarios anyway. So
instead, let’s go on to discuss some implementation details.

The third line is merely syntactic sugar. The first line defines a template
class that encodes a postfix of the final sequence (S...) and a parameter
N that counts the missing elements. This class is defined recursively, so that
shorter postfixs are derived from longer postfixs. The second line is a
specialized version of this class for the base case, of a fully spanned sequence

and it defines the type type that encodes the entire sequence, and is
accessible (due to inheritance) from all the chain of derived classes.

So the type of make_sequence_imp<4>::type is index_sequence<0, 1, 2, 3>.

Now the problem with this implementation might be clearer: generation of a
sequence of $N$ elements requires an inheritance tree of depth $N$. Furthermore,
sequences of different lengths can’t share intermediate instantiations. This
could easily make compile time unreasonably long even for moderate $N$s.

Using it has several benefits: it doesn’t require a recursive inheritance, it
compiles in $O(\ln n)$ time instead of $O(n)$ and it uses reusable building-
blocks for the sequences shareable between different instantiations. Here’s an
implementation for concatenation-based sequences via ranges: