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.