Mopping up template barf with static_assert

Templates are a mixed blessing for C++ developers. On the one hand, templates avoid code duplication as most impressively demonstrated by the standard template library. The template mechanism, in contrast to preprocessor macros, is aware of the C++ language and part of it. Compilers can produce helpful error messages – and some compilers even do so.

Some compilers, however, fail in providing clear template compilation error messages. If you make a mistake, you are greeted by a bucket load of irritating notes on what went wrong in detail. Since it is down to you to identify the relevant chunks, this is often referred to as template barf.

A simple template function

Consider the following template function which calculates the hypothenuse given the two other sides of a right-angled triangle:

template <typename Floating_Point>
Floating_Point hypothenuse(Floating_Point side_a, Floating_Point side_b)
{
    Floating_Point a_squared = side_a * side_a;
    Floating_Point b_squared = side_b * side_b;
    return sqrt(a_squared + b_squared);
}

From the declaration of the function it should be clear that it is designed to work with floating point numbers (using the common T instead of Floating_Point would destroy the interface’s clarity).

The compiler, our cryptic friend

Let us forget a moment that we are infallible programmers and consider we use our function in a slightly less-than-correct way:

std::string const a("3.0");
std::string const b("4.0");
auto c = hypothenuse(a, b);

What will happen if we compile this? Compilation will fail. Exactly how it will fail depends on your compiler. This is the output of Visual Studio 2012:

1>templatebarf.cpp(26): error C2676: binary '*' : 'std::string' does not define this operator or a conversion to a type acceptable to the predefined operator
1>templatebarf.cpp(38) : see reference to function template instantiation 
1>          'Floating_Point hypothenuse<std::string>(Floating_Point,Floating_Point)' being compiled
1>          with
1>          [
1>              Floating_Point=std::string
1>          ]
1>templatebarf.cpp(27): error C2676: binary '*' : 'std::string' does not define this operator or a conversion to a type acceptable to the predefined operator
1>templatebarf.cpp(28): error C2665: 'sqrt' : none of the 3 overloads could convert all the argument types
1>          c:\program files (x86)\microsoft visual studio 11.0\vc\include\math.h(127): could be 'double sqrt(double)'
1>          c:\program files (x86)\microsoft visual studio 11.0\vc\include\math.h(540): or       'float sqrt(float)'
1>          c:\program files (x86)\microsoft visual studio 11.0\vc\include\math.h(588): or       'long double sqrt(long double)'
1>          while trying to match the argument list '(std::basic_string<_Elem,_Traits,_Alloc>)'
1>          with
1>          [
1>              _Elem=char,
1>              _Traits=std::char_traits<char>,
1>              _Alloc=std::allocator<char>
1>          ]

This is actually one of the better error messages we encountered. You must be above 18 to see the ones generated by other compilers for the same code. Even though we made a simple, not-so-honest mistake, the compiler merely reports on symptoms (* operator not found, sqrt not defined for std::string). The root of the problem is left for the user to find.

Custom error messages with static_assert

The new C++11 standard introduced the static_assert keyword which helps deal with template barf. The syntax is like this:

bool const boolean_fixed_at_compile_time = false;
static_assert(boolean_fixed_at_compile_time, "Precise error message");

static_assert checks a boolean at compile time, which is also the main difference to the assert macro provided by the C part of C++. If the boolean is true, compilation continues without further ado; run time performance is not affected. If the boolean is false, however, the compiler will mark the compilation as failed and print the error message you provided. Depending on your compiler, compilation might continue for a while, though. Thus, static_assert does not remove template barf completely. Think of it more like an introductory “I had a Cheeseburger with bacon for lunch” before handing you the mop.

Type checking with is_floating_point

To be actually helpful, you need sources for booleans which are fixed at compile time, but yield values which depend on other circumstances fixed at compile time. Normal functions won’t do, because a function is evaluated when it is called at run time. What we need are template metafunctions. Template metafunctions are templated types, usually structs, designed to provide a static constant member value and the typedef type. Depending on the provided template argument, type and value will be set accordingly.

Template metafunctions will prominently feature in other articles. For now, we will look at template metafunctions provided by the C++11’s very own standard template library:

#include <type_traits>
#include <cmath>

template <typename Floating_Point>
Floating_Point hypothenuse(Floating_Point side_a, Floating_Point side_b)
{
	bool const is_floating_point = std::is_floating_point<Floating_Point>::value;
	static_assert( is_floating_point, "Provided type is not a floating point type");
	Floating_Point a_squared = side_a * side_a;
	Floating_Point b_squared = side_b * side_b;
	return sqrt(a_squared + b_squared);
}

std::is_floating_point<>  is a metafunction which determines whether the passed type is a floating point type or not. It defines the value member as true for types such as float and double, and false for non-floating point types such as int or std::string. In our wrong usage scenario, the error message changes to this:

1>templatebarf.cpp(28): error C2338: Provided type is not a floating point type
1>          templatebarf.cpp(42) : see reference to function template instantiation 
1>          'Floating_Point hypothenuse<std::string>(Floating_Point,Floating_Point)' being compiled
1>          with
1>          [
1>              Floating_Point=std::string
1>          ]
1> ... more messages, basically the same as shown above ...

The error message we got earlier is still there and still valid. However, the message we put in the static_assert is printed. Our custom message clearly transports the root cause of the compilation error: We should have used a floating point type to call hypothenuse, but we used std::string instead.

Conclusion

static_assert is not the best imaginable way to deal with concepts a type must satisfy in order to be used in the template. More sophisticated language features are on the way and might be included in the next C++ standard or technical report. Until then, static_assert provides a somewhat cumbersome, yet powerful way to make our template functions and classes easier to use correctly.

2 Replies to “Mopping up template barf with static_assert”

  1. Nice blog! *bookmark*

    The compiler messages are hard to read because they exceed the number of printed letters in the box in one line and one has to scroll a lot. Furthermore the first line is hard to read, because of the mouseover.

    There is a typo in the italic typeset of the word “how”.

    Can you additionally provide a list of useful STL template metafunctions or link a reference?

  2. Hey Zebro!

    Thanks a lot for your suggestions, I have tried to accommodate them in the article.

Leave a Reply

Your email address will not be published. Required fields are marked *