There are only a few things more fun in this world than doing template meta-programming (TMP) and reading all those long poems that the compiler writes out when we make even the smallest mistake.
While we don’t usually welcome these messages, there are ways to make them useful.
One of the main causes of errors in TMP code are unexpected types – types that the compiler is deducing instead of the types that we expect it to deduce.
This results in error messages occurring in seemingly random places in our code.
printf debugging
Just like it is sometimes useful to printf values in our program while debugging a problem, it can be useful to print types while debugging the problems in TMP code.
For this, here is one of my favourite meta-functions for C++:
template <typename... Ts>
struct print_types;
It is a meta-function that takes several arguments, but has no
implementation. Which means that every time we try to use it, the
compiler will report an error along with some additional information
like which Ts...
we passed to the meta-function.
For example, if we wanted to know what are the exact types of
std::vector<bool>::reference
and friends, we could do
this:
print_types<
std::vector<bool>::reference,
std::vector<bool>::value_type,
std::vector<bool>::iterator
>{};
And the compiler will print out an error message along these lines:
invalid use of incomplete type 'class print_types<
std::_Bit_reference,
bool,
std::_Bit_iterator
>'
The compiler printed out all three types that we requested it to print.
This can be used in conjunction with some compiler output
post-processing – shortening some type names like
std::basic_string<char>
to string
etc,
replacing <
and >
with (
and )
and putting the output through
clang-format
to get a pretty readable and nice formatted
output for complex types.
For example, this is the output generated for one expression template in my codebase:
expression(
expression(
void,
expression(PRODUCER,
expression(PRODUCER, ping_process,
transform("(λ tests_multiprocess.cpp:91:26)")),
transform("(λ tests_multiprocess.cpp:82:38)"))),
expression(
TRAFO,
expression(TRAFO,
expression(TRAFO,
expression(TRAFO, identity_fn,
transform("(λ tests_multiprocess.cpp:99:26)")),
…
Printing types several times
The problem with the previous approach is that you can call
print_types
only once – as soon as the compiler encounters
it, it will report an error and stop.
Instead of triggering an error, we might want to try something else – something that the compiler reports without stopping the compilation – we could trigger a warning instead.
One of the easiest warnings to trigger is the deprecation warning –
we just need to mark the print_types
class as
[[deprecated]]
.
template <typename... Ts>
struct [[deprecated]] print_types {};
This way, we can use it multiple times and the compiler will generate a warning for each of the usages. Now, this works well with clang because it reports the whole type that triggered the warning. Other compilers might not be that detailed when reporting warnings so your mileage might vary.
Assertions on types
When you write TMP code, it is useful to assert that the types you’re getting have some desired property.
For example, it is a sane default (just like const
is a
sane default for everything) to expect all template parameters to be
value types. For this, you could create an assertion macro and sprinkle
it all over your codebase:
#define assert_value_type(T) \
static_assert( \
std::is_same_v<T, std::remove_cvref_t<T>>, \
"Not a value type")
Having the habit of adding assertions like these can make your life much easier down the road.
Tests on types
Just like the original print_types
we defined stops the
compilation when called, static_assert
stops compilation as
soon as the first one fails.
What if we wanted just to get notified that a test failed, without it stopping the compilation.
We can use the same trick we used to allow print_types
to be called several times. We can use the [[deprecated]]
annotation to get a warning instead of getting an error with
static_assert
.
This can be implemented in several ways. The main gist of all the
solutions is to create a class template which is then specialized for
false
or std::false_type
and that
specialization is marked as deprecated.
Here is one of the more fun implementations:
template <typename T>
struct static_test {
static_test(std::true_type) {}
};
template <>
struct [[deprecated]] static_test<std::false_type> {
static_test(std::false_type) {}
};
template <typename T>
static_test(T x) -> static_test<T>;
And the usage is quite simple:
static_test int_is_int { std::is_same<int, int>::type{} };
Whenever a test returns std::false_type
, we are going to
get a deprecation warning.
Conclusion
Working with templates allows us to do awesome things but it can be quite tedious. Small tricks like these can make working with templates a breeze.
template <typename T>
static_test(T x) -> static_test<T>;