Wednesday, May 18, 2016

Succinct and helpful C++ template compilation errors

We've all experienced them, the long and unhelpful compilation errors from templates, usually referring to some internal header you didn't even know existed. Finding the source of the error can be painful, and not unusually the clue, if there is any, is some where in the middle of the long list of messages.

Yes yes, concepts are coming to C++. GCC 6 has them, and they are in a TS. Concepts can help get the error message to the source of the error, and some times to give a good idea of why the error occurred.

This post will show one technique available from C++11 and later. It, and some variants of it, are used extensively in the Trompeloeil C++14 mocking frame work to provide short compilation error messages that explains to the user what went wrong.

In all fairness, this technique is not my invention, and unfortunately I've forgotten where I first learned of it. If you know who pioneered it, please let me know so I can attribute it properly.
[edit: Shafik Yaghmour directed me to discussions on reddit pointing to Eric Niebler being the likely originator of this technique, first used in the Boost/Proto library.]
[edit again: Eric Niebler himself isn't entirely sure he pioneered the technique, but this youtube video makes it likely.]

Problem introduction


Here's a simple toy example. Imagine that everything except the main() function is a template library.

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <string>
#include <iostream>
#include <utility>

void internal_func(const std::string& s)
{
  std::cout << s << '\n';
}

template <typename ... T>
void do_with_string(T&& ... t)
{
  internal_func({std::forward<T>(t)...});
}

int main()
{
  do_with_string("foobar", 3U);
  do_with_string(std::string("bar"));
}

The function do_with_string() accepts any parameters that a std::string can be constructed from. As long as it's used correctly, it works nicely.

But when a programmer makes a mistake, things turn unpleasant:

1
2
3
4
5
6
int main()
{
  do_with_string("foobar", 3U);
  do_with_string(std::string("bar"));
  do_with_string(3.1); // Error, can't make a string from a double
}

There's what clang++-3.8 says:


t.cpp:13:18: error: type 'double' cannot be narrowed to 'char' in initializer list [-Wc++11-narrowing]
  internal_func({std::forward<T>(t)...});
                 ^~~~~~~~~~~~~~~~~~
t.cpp:20:3: note: in instantiation of function template specialization 'do_with_string<double>' requested here
  do_with_string(3.1);
  ^
t.cpp:13:18: note: insert an explicit cast to silence this issue
  internal_func({std::forward<T>(t)...});
                 ^~~~~~~~~~~~~~~~~~
                 static_cast<char>()
1 error generated.
   ^

There are thee huge problems here.
  1. The highlited problems refer to internal functions that, in a real world library, would likely be something that the user of the library never even knew existed. 
  2. The highlited information doesn't say what failed. The complaint that 'double' cannot be narrowed to 'char' is true but nonsensical. The actual problem is that the entire parameter pack expansion cannot be used to construct a std::string, and that information is completely missing.
  3. Even though the root location of the error is shown, the do_with_string(3.1) call, this information is in the middle. Again, in a real world template library this could be in the middle of hundreds of lines of disinformation.
g++-5.2 does ever so slightly better in this case, in that the failing call site is the first line of the error list, but the rest of the problems are there for g++-5.2 as well.


t.cpp: In instantiation of ‘void do_with_string(T&& ...) [with T = {double}]’:
t.cpp:20:21:   required from here
t.cpp:13:16: warning: narrowing conversion of ‘std::forward<double>((* & t#0))’ from ‘double’ to ‘char’ inside { } [-Wnarrowing]
   internal_func({std::forward<T>(t)...});
                ^
t.cpp:13:16: warning: narrowing conversion of ‘std::forward<double>((* & t#0))’ from ‘double’ to ‘char’ inside { } [-Wnarrowing]
   ^


static_assert()


A popular method to give more information is to use static_assert(). With it you can provide a helpful error message that gives the user an explanation.

1
2
3
4
5
6
7
template <typename ... T>
void do_with_string(T&& ... t)
{
  constexpr auto legal = std::is_constructible<std::string, T&&...>{};
  static_assert(legal, "it must be possible to form a std::string from the args");
  internal_func({std::forward<T>(t)...});
}

On line 4, the constant legal becomes either std::true_type or std::false_type depending on whether std::is_constructible<> concludes that std::string can be constructed from T&&... or not.

The static_assert() on line 5 causes a compilation error with the explicit error message string if legal is false.

The difficulty is figuring out methods to check whether the function can succeed or not. In this case it was relatively easy.

Here's what clang++-3.8 says when the faulty main() is compiled:


t.cpp:14:3: error: static_assert failed "it must be possible to form a std::string from the args"
  static_assert(legal, "it must be possible to form a std::string from the args");
  ^             ~~~~~
t.cpp:22:3: note: in instantiation of function template specialization 'do_with_string<double>' requested here
  do_with_string(3.1); // Error, can't make a string from a double
  ^
t.cpp:15:18: error: type 'double' cannot be narrowed to 'char' in initializer list [-Wc++11-narrowing]
  internal_func({std::forward<T>(t)...});
                 ^~~~~~~~~~~~~~~~~~
t.cpp:22:3: note: in instantiation of function template specialization 'do_with_string<double>' requested here
  do_with_string(3.1); // Error, can't make a string from a double
  ^
t.cpp:15:18: note: insert an explicit cast to silence this issue
  internal_func({std::forward<T>(t)...});
                 ^~~~~~~~~~~~~~~~~~
                 static_cast<char>()
2 errors generated.

It is an improvement, but there is a lot of noise and disinformation. There is also the repetition of blaming the call to do_with_string(3.1) twice for different offenses.

g++-5.2 is slightly better in that it doesn't try to give conflicting reasons for the failure, but the extra warnings are misleading at best.


t.cpp: In instantiation of ‘void do_with_string(T&& ...) [with T = {double}]’:
t.cpp:22:21:   required from here
t.cpp:14:3: error: static assertion failed: it must be possible to form a std::string from the args
   static_assert(legal, "it must be possible to form a std::string from the args");
   ^
t.cpp:15:16: warning: narrowing conversion of ‘std::forward<double>((* & t#0))’ from ‘double’ to ‘char’ inside { } [-Wnarrowing]
   internal_func({std::forward<T>(t)...});
                ^
t.cpp:15:16: warning: narrowing conversion of ‘std::forward<double>((* & t#0))’ from ‘double’ to ‘char’ inside { } [-Wnarrowing]

Both compilers give the necessary helpful message, but there's a lot of unhelpful cruft. In a real world example, the helpful message might be difficult to find among all the uninteresting ones.

The problem is that even though a static_assert() is triggered, the compiler continues to try to make sense out of the function, and it fails to do so, and this causes the unhelpful extra messages.

The solution, as so often, is another level of indirection.

Tag dispatch


The trick is to not call internal_func() directly from do_with_string(), but to use an indirection via a function that takes an extra parameter saying whether the call can succeed or not. Since the flag legal is already of either std::true_type or std::false_type, the indirection functions can be selected on those types. Note that it is really the type that differs, not just different boolean values.

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <typename ... T>
void do_with_string_(std::false_type, T&& ...);

template <typename ... T>
void do_with_string_(std::true_type, T&& ... t)
{
  internal_func({std::forward<T>(t)...});
}

template <typename ... T>
void do_with_string(T&& ... t)
{
  constexpr auto legal = std::is_constructible<std::string, T&&...>{};
  static_assert(legal, "it must be possible to form a std::string from the args");
  do_with_string_(legal, std::forward<T>(t)...);
}

The new indirection functions do_with_string_() on lines 1-2 and 4-8 are selected on the first parameter type, which is provided from the call at line 15. Note that the failure function doesn't have to be implemented, just declared.

This removes the cruft.

clang++-3.8 says:


t.cpp:23:3: error: static_assert failed "it must be possible to form a std::string from the args"
  static_assert(legal, "it must be possible to form a std::string from the args");
  ^             ~~~~~
t.cpp:31:3: note: in instantiation of function template specialization 'do_with_string<double>' requested here
  do_with_string(3.1); // Error, can't make a string from a double
  ^
1 error generated.

Excellent!

g++-5.2 is equally helpful:


t.cpp: In instantiation of ‘void do_with_string(T&& ...) [with T = {double}]’:
t.cpp:31:21:   required from here
t.cpp:23:3: error: static assertion failed: it must be possible to form a std::string from the args
   static_assert(legal, "it must be possible to form a std::string from the args");
   ^

Both give the helpful message, and the source of the failing call, and not much else, and specifically no misleading messages from internal functions that the user doesn't even know about.

Unfortunately I do not feel comfortable enough with concepts to show examples in a future C++, but perhaps someone else can complement this post and write an alternative article from the viewpoint of a concepts world?