Expressive C++ Template Metaprogramming
There is a part of C++ developers that appreciate template metaprogramming.
And there are all the other C++ developers.
While I consider myself falling rather in the camp of the aficionados, I’ve met a lot more people that don’t have a strong interest for it, or that even find it downright disgusting, than TMP enthusiasts. Which camp do you fall into?
One of the reasons why TMP is off putting for many people in my opinion is that it is often obscure. To the point that sometimes it looks like dark magic, reserved for a very peculiar sub-species of developers that can understand its dialect. Of course, we sometimes come across the occasional understandable piece of TMP, but on average, I find it harder to understand than regular code.
And the point I want to make is that TMP doesn’t have to be that way.
I’m going to show you how to make TMP code much more expressive. And it’s not rocket science.
TMP is often described as a language within the C++ language. So to make TMP more expressive, we just need to apply the same rules as in regular code. To illustrate, we’re going to take a piece of code that only the bravest of us can understand, and apply on it the following two guidelines for expressiveness:
- choosing good names,
- and separating out levels of abstractions.
I told you, it’s not rocket science.
Just before we start, I want to thank my colleague Jeremy for helping me with his impressive agility with TMP, and Vincent who’s always so great for resonating ideas with. You guys rock.
The purpose of the code
We will write an API that checks whether an expression is valid for a given type.
For example given a type T, we would like to know whether T is incrementable, that is to say that, for an object t of type T, whether or not the expression:
++t
is valid. If T is int
, then the expression is valid, and if T is std::string
then the expression is not valid.
Here is a typical piece of TMP that implements it:
template< typename, typename = void > struct is_incrementable : std::false_type { }; template< typename T > struct is_incrementable<T, std::void_t<decltype( ++std::declval<T&>() )> > : std::true_type { };
I don’t know how much time you need to parse this code, but it took me a significant amount of time to work it all out. Let’s see how to rework this code to make it more quickly understandable.
In all fairness, I must say that to understand TMP there are constructs that you need to know. A bit like one needs to know “if”, “for” and function overloading to understand C++, TMP has some prerequisites like “std::true_type” and SFINAE. But don’t worry if you don’t know them, I’ll explain everything all along.
The basics
If you’re already familiar with TMP you can skip over to the next section.
Our goal is to be able to query a type this way:
is_incrementable<T>::value
is_incrementable<T>
is a type, that has one public boolean member, value
, which is either true if T is incrementable (e.g. T is int
) or false if it isn’t (e.g. T is std::string
).
We will use std::true_type
. It is a type that only has a public boolean member value
equal to true. We will make is_incrementable<T>
inherit from it in the case that T can be incremented. And, as you’d have guessed, inherit from std::false_type
if T can’t be incremented.
To allow for having two possible definitions we use template specialization. One specialization inherits from std::true_type
and the other from std::false_type
. So our solution will look roughly like this:
template<typename T> struct is_incrementable : std::false_type{}; template<typename T> struct is_incrementable<something that says that T is incrementable> : std::true_type{};
The specialization will be based on SFINAE. Put simply, we’re going to write some code that tries to increment T in the specialization. If T is indeed incrementable, this code will be valid and the specialization will be instantiated (because it always has priority over the primary template). This is the one inheriting from std::true_type
.
On the other hand if T isn’t incrementable, then the specialization won’t be valid. In this case SFINAE says that an invalid instantiation doesn’t halt compilation. It is just completely discarded, which leaves as the only remaining option the primary template, the one inheriting from std::false_type
.
Choosing good names
The code at the top of the post used std::void_t
. This construct appears in the standard in C++17, but can be instantly replicated in C++11:
template<typename...> using void_t = void;
EDIT: as u/Drainedsoul pointed out on Reddit, this implementation is guaranteed to work in C++14 but not in C++11, where unused template parameters of an alias declaration don’t necessarily trigger SFINAE. The C++11 implementation uses an intermediate type an is available on cppreference.com.
void_t
is just instantiating the template types it is passed, and never uses them. It’s like a surrogate mother for templates, if you would.
And to make the code work, we write the specialization this way:
template<typename T> struct is_incrementable<T, void_t<decltype(++std::declval<T&>())>> : std::true_type{};
Ok, to understand TMP you also need to understand decltype
and declval
: decltype
returns the type of its argument, and declval<T>()
does as if an object of type T was instantiated in the decltype
expression (it’s useful because we don’t necessarily know what the constructors of T look like). So decltype(++std::declval<T&>())
is the return type of operator++
called on T.
And as said above void_t
is just a helper to instantiate this return type. It doesn’t carry any data or behaviour, it’s just a sort of launchpad to instantiate the type returned by decltype
.
If the increment expression is not valid then this intantiation made by void_t
fails, SFINAE kicks in and is_incrementable
resolves to the primary template inheriting from std::false_type
.
It’s a great mechanism, but I’m cross with the name. In my opinion it’s absolutely at the wrong level of abstraction: it’s implemented as void, but what it means to do is trying to instantiate a type. By working this piece of information into the code, the TMP expression immediately clears up:
template<typename...> using try_to_instantiate = void; template<typename T> struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};
Given that the specialization that uses two template parameters, the primary template has to have two parameters too. And to avoid the user to pass it, we provide a default type, say void
. The question now is how to name this technical parameter?
One way to go about it is to not name it at all, (the code at the top took this option):
template<typename T, typename = void> struct is_incrementable : std::false_type{};
It’s a way of saying “don’t look at this, it’s irrelevant and it’s there only for technical reasons” which I find reasonable. Another option is to give it a name that says what it means. The second parameter is the attempt to instantiate the expression in the specialization, so we could work this piece of information into the name, which gives for the complete solution so far:
template<typename...> using try_to_instantiate = void; template<typename T, typename Attempt = void> struct is_incrementable : std::false_type{}; template<typename T> struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};
Separating out levels of abstraction
We could stop here. But the code in is_incrementable
is still arguably too technical, and could be pushed down to a lower layer of abstraction. Besides, it is conceivable that we’ll need the same technique for checking other expressions at some point, and it would be nice to factor out the checking mechanism in order to avoid code duplication.
We will end up with something resembling the is_detected
experimental feature.
The part that can vary most in the above code is clearly the decltype
expression. So let’s take it in input, as a template parameter. But again, let’s pick the name carefully: this parameter represents the type of an expression.
This expression itself depends on a template parameter. For this reason we don’t simply use a typename
as a parameter, but rather a template (hence the template<typename> class
):
template<typename T, template<typename> class Expression, typename Attempt = void> struct is_detected : std::false_type{}; template<typename T, template<typename> class Expression> struct is_detected<T, Expression, try_to_instantiate<Expression<T>>> : std::true_type{};
is_incrementable
then becomes:
template<typename T> using increment_expression = decltype(++std::declval<T&>()); template<typename T> using is_incrementable = is_detected<T, increment_expression>;
Allowing for several types in the expression
So far we’ve used an expression involving only one type, but it would be nice to be able to pass several types to expressions. Like for testing if two types are assignable to one another, for example.
To achieve this, we need to use variadic templates to represent the types coming into the expression. We’d like to throw in some dots like in the following code, but it’s not going to work:
template<typename... Ts, template<typename...> class Expression, typename Attempt = void> struct is_detected : std::false_type{}; template<typename... Ts, template<typename...> class Expression> struct is_detected<Ts..., Expression, try_to_instantiate<Expression<Ts...>>> : std::true_type{};
It’s not going to work because the variadic pack typename... Ts
is going to eat up all the template parameters, so it needs to be put at the end (if you want to better understand variadic templates I suggest you watch this part of Arthur O’Dwyer’s excellent talk Template Normal Programming). But the default template parameter Attempt
also needs to be at the end. So we have a problem.
Let’s start by moving the pack to the end of the template parameters list, and also remove the default type for Attempt
:
template<template<typename...> class Expression, typename Attempt, typename... Ts> struct is_detected : std::false_type{}; template<template<typename...> class Expression, typename... Ts> struct is_detected<Expression, try_to_instantiate<Expression<Ts...>>, Ts...> : std::true_type{};
But what type to pass to Attempt
?
A first impulse could be to pass void
, since the successful trial of try_to_instantiate
resolves to void
so we need to pass it to let the specialization be instantiated.
But I think that doing this would make the callers scratch their head: what does it mean to pass void
? Contrary to the return type of a function, void
doens’t mean “nothing” in TMP, because void
is a type.
So let’s give it a name that better carries our intent. Some call this sort of thing “dummy”, but I like to be even more explicit about it:
using disregard_this = void;
But I guess the exact name is a matter of personal taste.
And then the check for assignment can be written this way:
template<typename T, typename U> using assign_expression = decltype(std::declval<T&>() = std::declval<U&>()); template<typename T, typename U> using are_assignable = is_detected<assign_expression, disregard_this, T, U>
Of course, even if disregard_this
reassures the reader by saying that we don’t need to worry about it, it is still in the way.
One solution is to hide it behind a level of indirection: is_detected_impl
. “impl_” often mean “level of indirection” in TMP (and in other places too). While I don’t find this word natural, I can’t think of a better name for it and it is useful to know it because a lot of TMP code uses it.
We’ll also take advantage of this level of indirection to get the ::value
attribute, relieving all the elements further up from calling it each time they use it.
The final code is then:
template<typename...> using try_to_instantiate = void; using disregard_this = void; template<template<typename...> class Expression, typename Attempt, typename... Ts> struct is_detected_impl : std::false_type{}; template<template<typename...> class Expression, typename... Ts> struct is_detected_impl<Expression, try_to_instantiate<Expression<Ts...>>, Ts...> : std::true_type{}; template<template<typename...> class Expression, typename... Ts> constexpr bool is_detected = is_detected_impl<Expression, disregard_this, Ts...>::value;
And here is how to use it:
template<typename T, typename U> using assign_expression = decltype(std::declval<T&>() = std::declval<U&>()); template<typename T, typename U> constexpr bool is_assignable = is_detected<assign_expression, T, U>;
The generated values can be used at compile-time or at run-time. The following program:
// compile-time usage static_assert(is_assignable<int, double>, ""); static_assert(!is_assignable<int, std::string>, ""); // run-time usage std::cout << std::boolalpha; std::cout << is_assignable<int, double> << '\n'; std::cout << is_assignable<int, std::string> << '\n';
compiles successfully, and outputs:
true false
TMP doesn’t have to be that complex
Sure, there are a few prerequisites to understand TMP, like SFINAE and such. But apart from those, there is no need to make the code using TMP look more complex than necessary.
Consider what is now a good practice for unit tests: it’s not because it’s not production code that we should lower our standards of quality. Well, it is even more true for TMP: it is production code. For this reason, let’s treat it like the rest of the code and do our best to make it as expressive as possible. Chances are, more people would then be attracted to it. And the richer the community, the richer the ideas.
Related articles:
Don't want to miss out ? Follow:   
Share this post!