(Difficult-to-grok metaprogramming below. Not for the faint of heart.)
At the recent Urbana-Champaign meeting of the C++ Standardization Committee, Bill Seymour presented his paper N4115: Searching for Types in Parameter Packs which, as its name suggests, describes a library facility for, uh, searching for a type in a parameter pack, among other things. It suggests a template called packer
to hold a parameter pack:
// A class template that just holds a parameter pack: template <class... T> struct packer { };
Many of you are probably already familiar with such a facility, but under a different name:
// A class template that is just a list of types: template <class... T> struct typelist { };
It became clear in the discussion about N4115 that C++ needs a standard typelist
template and some utilities for manipulating them. But what utilities, exactly?
Metaprogramming in the Wild
When it comes to metaprogramming in C++, there is no shortage of prior art. Andrei Alexandrescu started the craze with his Loki library. Boost got in on the act with Boost.MPL, Boost.Fusion, and (currently under development) Hana. All of these libraries are feature-rich and elaborate with their own philosophy, especially Boost.MPL, which takes inspiration from the STL’s containers, iterators, and algorithms.
It wasn’t until recently that I came to doubt MPL’s slavish aping of the STL’s design. The abstractions of the STL were condensed from real algorithms processing real data structures on real computer hardware. But metaprograms don’t run on hardware; they run on compilers. The algorithms and data structures for our metaprograms should be tailored to their peculiar problem domain and execution environment. If we did that exercise, who is to say what abstractions would fall out? Compile-time iterators? Or something else entirely?
Dumb Typelists
If we were to standardize some metaprogramming facilities, what should they look like? It’s an interesting question. N4115 gets one thing right: parameter packs are the compile-time data structure of choice. As of C++11, C++ has language support for lists of types. We would be foolish to work with anything else. IMO, if a standard metaprogramming facility did nothing but manipulate parameter packs — dumb typelists — it would cover 95% of the problem space.
But parameter packs themselves are not first-class citizens of the language. You can’t pass a parameter pack to a function without expanding it, for instance. Wrapping the parameter pack in a variadic typelist
template is a no-brainer.
So, like N4115 suggests, this is a sensible starting point:
// A class template that just a list of types: template <class... T> struct typelist { };
It’s a rather inauspicious start, though; clearly we need more. But what? In order to answer that, we need to look at examples of real-world metaprogramming. With concrete examples, we can answer the question, What he heck is this stuff good for, anyway? And for examples, we have to look no farther than the standard library itself.
Tuple_cat
Stephan T. Lavavej drew my attention to the tuple_cat
function in the standard library, a function that takes N tuple
s and glues them together into one. It sounds easy, but it’s tricky to code efficiently, and it turns out to be a great motivating example for metaprogramming facilities. Let’s code it up, and posit a few typelist algorithms to make our job easier. (All the code described here can be found in my range-v3 library on GitHub.)
First, I’m going to present the final solution so you have an idea of what we’re working toward. Hopefully, by the time you make it to the end of this post, this will make some sort of sense.
namespace detail { template<typename Ret, typename...Is, typename ...Ks, typename Tuples> Ret tuple_cat_(typelist<Is...>, typelist<Ks...>, Tuples tpls) { return Ret{std::get<Ks::value>( std::get<Is::value>(tpls))...}; } } template<typename...Tuples, typename Res = typelist_apply_t< meta_quote<std::tuple>, typelist_cat_t<typelist<as_typelist_t<Tuples>...> > > > Res tuple_cat(Tuples &&... tpls) { static constexpr std::size_t N = sizeof...(Tuples); // E.g. [0,0,0,2,2,2,3,3] using inner = typelist_cat_t< typelist_transform_t< typelist<as_typelist_t<Tuples>...>, typelist_transform_t< as_typelist_t<make_index_sequence<N> >, meta_quote<meta_always> >, meta_quote<typelist_transform_t> > >; // E.g. [0,1,2,0,1,2,0,1] using outer = typelist_cat_t< typelist_transform_t< typelist<as_typelist_t<Tuples>...>, meta_compose< meta_quote<as_typelist_t>, meta_quote_i<std::size_t, make_index_sequence>, meta_quote<typelist_size_t> > > >; return detail::tuple_cat_<Res>( inner{}, outer{}, std::forward_as_tuple(std::forward<Tuples>(tpls)...)); }
That’s only 43 lines of code. The implementation in stdlib++ is 3x longer, no easier to understand (IMHO), and less efficient. There’s real value in this stuff. Really.
Let’s look first at the return type:
// What return type??? template< typename ...Tuples > ???? tuple_cat( Tuples &&... tpls );
You can think of a tuple as a list of types and a list of values. To compute the return type, we only need the list of types. So a template that turns a tuple into a typelist would be useful. Let’s call it as_typelist
. It takes a tuple and does the obvious. (Another possibility would be to make tuples usable as typelists, but let’s go with this for now.)
If we convert all the tuples into typelists, we end up with a list of typelists. Now, we want to concatenate them. Ah! We need an algorithm for that. Let’s call it typelist_cat
in honor of tuple_cat
. (Functional programmers: typelist_cat is join in the List Monad. Shhh!! Pass it on.) Here’s what we have so far:
// Concatenate a typelist of typelists template< typename ...Tuples > typelist_cat_t< typelist< as_typelist_t< Tuples >... > > tuple_cat( Tuples &&... tpls );
Here, I’m following the convention in C++14 that some_trait_t<X>
is a template alias for typename some_trait<X>::type
.
The above signature isn’t right yet — tuple_cat
needs to return a tuple
, not a typelist
. We need a way to convert a typelist back to a tuple. It turns out that expanding a typelist into a variadic template is a useful operation, so let’s create an algorithm for it. What should it be called? Expanding a typelist into a template is a lot like expanding a tuple into a function call. There’s a tuple algorithm for that in the Library Fundamentals TS called apply
. So let’s call our metafunction typelist_apply
. It’s implementation is short and interesting, so I’ll show it here:
template<template<typename...> class C, typename List> struct typelist_apply; template<template<typename...> class C, typename...List> struct typelist_apply<C, typelist<List...>> { using type = C<List...>; };
The first parameter is a rarely-seen template template parameter. We’ll tweak this interface before we’re done, but this is good enough for now.
We can now write the signature of tuple_cat
as:
template<typename...Tuples> typelist_apply_t< std::tuple, typelist_cat_t<typelist<as_typelist_t<Tuples>...> > > tuple_cat(Tuples &&... tpls);
Not bad, and we have discovered three typelist algorithms already.
Tuple_cat Implementation
It’s time to implement tuple_cat
, and here’s where things get weird. It’s possible to implement it by peeling off the first tuple and exploding it into the tail of a recursive call. Once you’ve recursed over all the tuples in the argument list, you have exploded all the tuple elements into function arguments. From there, you bundle them into a final tuple and you’re done.
That’s a lot of parameter passing.
Stephan T. Lavavej tipped me off to a better way: Take all the tuples and bundle them up into a tuple-of-tuples with std::forward_as_tuple
. Since tuples are random-access, a tuple of tuples is like a jagged 2-dimensional array of elements. We can index into this 2-dimensional array with (i,j) coordinates, and if we have the right list of (i,j) pairs, then we can fetch each element in turn and build the resulting tuple in one shot, without all the explosions.
To make this more concrete, imaging the following call to tuple_cat
:
std::tuple<int, short, long> t1; std::tuple<> t2; std::tuple<float, double, long double> t3; std::tuple<void*, char*> t4; auto res = tuple_cat(t1,t2,t3,t4);
We want the result to be a monster tuple of type:
std::tuple<int, short, long, float, double, long double, void*, char*>
This call to tuple_cat
corresponds to the following list of (i,j) coordinates:
[(0,0),(0,1),(0,2),(2,0),(2,1),(2,2),(3,0),(3,1)]
Below is a tuple_cat_
helper function that takes the i‘s, j‘s, and tuple of tuples, and builds the resulting tuple:
template<typename Ret, typename...Is, typename ...Js, typename Tuples> Ret tuple_cat_(typelist<Is...>, typelist<Js...>, Tuples tpls) { return Ret{std::get<Js::value>( std::get<Is::value>(tpls))...}; }
Here, the Is
and Js
are instances of std::integral_constant
. Is
contains the sequence [0,0,0,2,2,2,3,3] and Js
contains [0,1,2,0,1,2,0,1].
Well and good, but how to compute Is
and Js
? Hang on tight, because Kansas is going bye bye.
Higher-Order Metaprogramming, Take 1
Let’s first consider the sequence of Js
since that’s a little easier. Our job is to turn a list of typelists [[int,short,long],[],[float,double,long double],[void*,char*]] into a list of integers [0,1,2,0,1,2,0,1]. We can do it in four stages:
- Transform the lists of typelist into a list of typelist sizes: [3,0,3,2],
- Transform that to a list of index sequences [[0,1,2],[],[0,1,2],[0,1]] using
std::make_index_sequence
, - Transform the
std::index_sequence
into a typelist ofstd::integral_constant
s withas_typelist
, and - Flatten that into the final list using
typelist_cat
.
By now it’s obvious that we’ve discovered our fourth typelist algorithm: typelist_transform
. Like std::transform
, typelist_transform
takes a sequence and a function, and returns a new sequence where each element has been transformed by the function. (Functional programmers: it’s fmap in the List Functor). Here’s one possible implementation:
template<typename List, template<class> class Fun> struct typelist_transform; template<typename ...List, template<class> class Fun> struct typelist_transform<typelist<List...>, Fun> { using type = typelist<Fun<List>...>; };
Simple enough.
Metafunction Composition
Above, we suggested three consecutive passes with typelist_transform
. We can do this all in one pass if we compose the three metafunctions into one. Metafunction composition seems like a very important utility, and it’s not specific to typelist manipulation. So far, we’ve been using template template parameters to pass metafunctions to other metafunctions. What does metafunction composition look like in that world? Below is a higher-order metafunction called meta_compose
that composes two other metafunctions:
template<template<class> class F0, template<class> class F1> struct meta_compose { template<class T> using apply = F0<F1<T>>; };
Composing two metafunction has to result in a new metafunction. We have to use an idiom to “return” a template by defining a nested template alias apply
which does the composition.
Seems simple enough, but in practice, this quickly becomes unwieldy. If you want to compose three metafunctions, the code looks like:
meta_compose<F0, meta_compose<F1, F2>::template apply> ::template apply
Gross. What’s worse, it’s not very general. We want to compose std::make_index_sequence
, and that metafunction doesn’t take a type; it takes an integer. We can’t pass it to to a meta_compose
. Let’s back up.
Higher-Order Metaprogramming, Take 2
What if, instead of passing meta_compose<X,Y>::template apply
to a higher-order function like typelist_transform
, we just passed meta_compose<X,Y>
and let typelist_transform
call the nested apply
? Now, higher-order functions like typelist_transform
take ordinary types instead of template template parameters. typelist_transform
would now look like:
template<typename ...List, typename Fun> struct typelist_transform<typelist<List...>, Fun> { using type = typelist<typename Fun::template apply<List>...>; };
That complicates the implementation of typelist_transform
, but makes the interface much nicer to deal with. The concept of a class type that behaves like a metafunction comes from Boost.MPL, which calls it a Metafunction Class.
We can make Metafunction Classes easier to deal with with a little helper that applies the nested metafunction to a set of arguments:
template<typename F, typename...As> using meta_apply = typename F::template apply<As...>;
With meta_apply
, we can rewrite typelist_transform
as:
template<typename ...List, typename Fun> struct typelist_transform<typelist<List...>, Fun> { using type = typelist<meta_apply<Fun, List>...>; };
That’s not bad at all. Now we can change meta_compose
to also operate on Metafunction Classes:
template<typename F1, typename F2> struct meta_compose { template<class T> using apply = meta_apply<F1, meta_apply<F2, T>>; };
With a little more work, we could even make it accept an arbitrary number of Metafunction Classes and compose them all. It’s a fun exercise; give it a shot.
Lastly, now that we have Metafunction Classes, we should change typelist_apply
to take a Metafunction Class instead of a template template parameter:
template<typename C, typename...List> struct typelist_apply<C, typelist<List...> > { using type = meta_apply<C, List...>; };
Metafunctions to Metafunction Classes
Recall the four steps we’re trying to evaluate:
- Transform the lists of typelist into a list of typelist sizes: [3,0,3,2],
- Transform that to a list of index sequences [[0,1,2],[],[0,1,2],[0,1]] using
std::make_index_sequence
, - Transform the
std::index_sequence
into a typelist ofstd::integral_constant
s withas_typelist
, and - Flatten that into the final list using
typelist_cat
.
In step (1) we get the typelist sizes, so we need another typelist algorithm called typelist_size
that fetches the size of type typelist:
template<typename...List> struct typelist_size<typelist<List...> > : std::integral_constant<std::size_t, sizeof...(List)> {};
We’re going to want to pass this to meta_compose
, but typelist_size
is a template, and meta_compose
is expecting a Metafunction Class. We can write a wrapper:
struct typelist_size_wrapper { template<typename List> using apply = typelist_size<List>; };
Writing these wrappers is quickly going to get tedious. But we don’t have to. Below is a simple utility for turning a boring old metafunction into a Metafunction Class:
template<template<class...> class F> struct meta_quote { template<typename...Ts> using apply = F<Ts...>; };
The name quote
comes from LISP via Boost.MPL. With meta_quote
we can turn the typelist_size
template into a Metafunction Class with meta_quote<typelist_size>
. Now we can pass it to either meta_compose
or typelist_transform
.
Our steps call for composing three metafunctions. It will look something like this:
meta_compose< meta_quote<as_typelist_t>, // Step 3 meta_quote<std::make_index_sequence>, // Step 2 meta_quote<typelist_size_t> > // Step 1
As I already mentioned, std::make_index_sequence
takes an integer not a type, so it can’t be passed to meta_quote
. This is a bummer. We can work around the problem with a variant of meta_quote
that handles those kinds of templates. Let’s call it meta_quote_i
:
template<typename Int, template<Int...> class F> struct meta_quote_i { template<typename...Ts> using apply = F<Ts::value...>; };
With meta_quote_i
, we can compose the three functions with:
meta_compose< meta_quote<as_typelist_t>, // Step 3 meta_quote_i<std::size_t, std::make_index_sequence>, // Step 2 meta_quote<typelist_size_t> > // Step 1
Now we can pass the composed function to typelist_transform
:
typelist_transform_t< typelist<as_typelist_t<Tuples>...>, meta_compose< meta_quote<as_typelist_t>, meta_quote_i<std::size_t, make_index_sequence>, meta_quote<typelist_size_t> > > >;
Voila! We have turned our lists of tuples into the list of lists: [[0,1,2],[],[0,1,2],[1,2]]. To get the final result, we smoosh this into one list using typelist_cat
:
// E.g. [0,1,2,0,1,2,0,1] typelist_cat_t< typelist_transform_t< typelist<as_typelist_t<Tuples>...>, meta_compose< meta_quote<as_typelist_t>, meta_quote_i<std::size_t, make_index_sequence>, meta_quote<typelist_size_t> > > >;
The result is the K
indices that we pass to the tuple_cat_
helper. And to repeat from above, the I
indices are computed with:
// E.g. [0,0,0,2,2,2,3,3] typelist_cat_t< typelist_transform_t< typelist<as_typelist_t<Tuples>...>, typelist_transform_t< as_typelist_t<make_index_sequence<N> >, meta_quote<meta_always> >, meta_quote<typelist_transform_t> > >;
I won’t step through it, but I’ll draw your attention to two things: on line (7) we make use of a strange type called meta_always
(described below), and on line (8) we pass typelist_transform
as the function argument to another call of typelist_transform
. Talk about composability!
So what is meta_always
? Simply, it’s a Metafunction Class that always evaluates to the same type. Its implementation couldn’t be siimpler:
template<typename T> struct meta_always { template<typename...> using apply = T; };
I’ll leave you guys to puzzle out why the above code works.
Summary
I set out trying to find a minimal useful set of primitives for manipulating lists of types that would be fit for standardization. I’m happy with the result. What I’ve found is that in addition to the typelist
template, we need a small set of algorithms like the ones needed to implement tuple_cat
:
typelist_apply
typelist_size
typelist_transform
typelist_cat
as_typelist
Some other typelist algorithms come up in other metaprogramming tasks:
make_typelist
(from a count and type)typelist_push_front
typelist_push_back
typelist_element
(indexing into a typelist)typelist_find
andtypelist_find_if
typelist_foldl
(aka, accumulate) andtypelist_foldr
- etc.
In addition, for the sake of higher-order metafunctions like typelist_transform
and typelist_find_if
, it’s helpful to have a notion of a Metafunction Class: an ordinary class type that can be used as a metafunction. A small set of utilities for creating and manipulating Metafunction Classes is essential for the typelist algorithms to be usable:
meta_apply
meta_quote
meta_quote_i
meta_compose
meta_always
For other problems, the ability to partially apply (aka bind) Metafunction Classes comes in very handy:
meta_bind_front
meta_bind_back
And that’s it, really. In my opinion, those utilities would meet the needs of 95% of all metaprograms. They are simple, orthogonal, and compose in powerful ways. Since we restricted ourselves to the typelist
data structure, we ended up with a design that is vastly simpler than Boost.MPL. No iterators needed here, which makes sense since iterators are a pretty stateful, iterative abstraction, and metaprogramming is purely functional.
One Last Thing…
Below is one more metafunction to tickle your noodle. It’s an N-way variant of transform
: it takes a list of typelists and a Metafunction Class, and builds a new typelist by mapping over all of them. I’m not suggesting this is important or useful enough to be in the standard. I’m only showing it because it demonstrates how well these primitive operations compose to build richer functionality.
// ([[a,b,c],[x,y,z]], F) -> [F(a,x),F(b,y),F(c,z)] template<typename ListOfLists, typename Fun> struct typelist_transform_nary : typelist_transform< typelist_foldl_t< ListOfLists, make_typelist< typelist_front_t<ListOfLists>::size(), Fun>, meta_bind_back< meta_quote<typelist_transform_t>, meta_quote<meta_bind_front> > >, meta_quote<meta_apply> > {};
Enjoy!
Update: This comment by tkamin helped me realize that the above typelist_transform_nary
is really just the zipWith
algorithm from the functional programming world. I’ve renamed it in my latest code, and provided a typelist_zip
metafunction that dispatches to typelist_zip_with
with meta_quote<typelist>
as the function argument. Very nice!