Deciphering C++ Template Error Messages

Deciphering C++ template error messages

You can try the following tool to make things more sane:

http://www.bdsoft.com/tools/stlfilt.html

Tools to generate higher-quality error messages for template-based code?

Let's have a stab at an answer (I marked this community wiki so we get a good response together)...

I'm working since a long time with templates and error messages have generally improved in some way or another:

  • Writing a stack of errors creates a lot more text but also typically includes the level the user is looking at and this generally includes a hint at what the actual problem is. Given that the compiler only sees a translation unit tossed at it, there isn't a lot which can be done determining which error in the stack is the one most suitable for the user.
  • Using concept checkers, i.e. classes or functions which exercise all the required members of template arguments and possibly generating errors messages using static_assert() give the template author a way to tell users about assumptions which apparently don't hold.
  • Telling the user about types he writes rather than expanding all typedefs as the compiler like to see at the lowest level also helps. clang is rather good at this and actually gives you error messages e.g. talking about std::string rather than expanding things type to whatever it ends up to be.

A combination of the technique actually causes e.g. clang to create quite decent error message (even if it doesn't implement C++2011, yet; however, no compiler does and as far as I can tell gcc and clang are leading the pack). I know other compiler developers actively work on improving the template error messages as lots of programmers have discovered that templates actually are a huge leap forward even though the error messages are something which takes a bit of getting used to.

One problem tools like stlfilt face is that C++ compilers and libraries are under active development. This results in error messages shifting all the time, causing the tool to receive different outputs. While it is good that compiler writers work on improving error messages, it certainly makes life harder for people who try to work from the error messages they got. There is another side to this as well: once a certain error pattern is detected to be common and is picked up e.g. by stlfilt (well, it isn't actively maintained as far as I know) compiler writers are probably keen to report the errors following these patterns directly, possibly also providing additional information available to the compiler but not emitted before. Put differently, I would expect that compiler writers are quite receptive to reports from users describing common error situations and how they are best reported. The compiler writers may not encounter the errors themselves because the code they are working on is actually C (e.g. gcc is implemented in C) or because they are so used to certain template techniques that they avoid certain errors (e.g. omission of typename for dependent types).

Finally, to address the question about concrete tools: the main "tool" I'm using when I get kind of stuck with a compiler complaining about some template instantiation is to use different compilers! Although it isn't always the case but often one compiler reports an entirely incomprehensible error messages which only makes sense after seeing the fairly concise report from another compiler (in case you are interested, I regularly use recent version of gcc, clang, and EDG for this). I'm not aware of a readily packaged too like stlfilt, however.

C++ compiler template error information - tool to decode the error information

If your platform can't support clang for some reason try STLFilt to get sensible errors.

How to improve compiler error messages when using C++ std::visit?

My first attempt at solving this problem can be found here. After some some googling and lots of trial and error, I've come up with a much better solution, which I've posted here. I'll copy-paste the solution, below, for convenience.


Here is a proof of concept.

#include <iostream>
#include <variant>

template <typename> class Test { };

using Foo = std::variant<
Test<struct A>,
Test<struct B>,
Test<struct C>,
Test<struct D>
>;

using Bar = std::variant<
Test<struct E>,
Test<struct F>,
Test<struct G>,
Test<struct H>,
Test<struct I>,
Test<struct J>,
Test<struct K>,
Test<struct L>
>;

template <typename T>
struct DefineVirtualFunctor
{
virtual int operator()(T const&) const = 0;
};

template <template <typename> typename Modifier, typename... Rest>
struct ForEach { };
template <template <typename> typename Modifier, typename T, typename... Rest>
struct ForEach<Modifier, T, Rest...> : Modifier<T>, ForEach<Modifier, Rest...> { };

template <typename Variant>
struct Visitor;
template <typename... Alts>
struct Visitor<std::variant<Alts...>> : ForEach<DefineVirtualFunctor, Alts...> { };

struct FooVisitor final : Visitor<Foo>
{
int operator()(Test<A> const&) const override { return 0; }
int operator()(Test<B> const&) const override { return 1; }
int operator()(Test<C> const&) const override { return 2; }
int operator()(Test<D> const&) const override { return 3; }
};

struct BarVisitor final : Visitor<Bar>
{
int operator()(Test<E> const&) const override { return 4; }
int operator()(Test<F> const&) const override { return 5; }
int operator()(Test<G> const&) const override { return 6; }
int operator()(Test<H> const&) const override { return 7; }
int operator()(Test<I> const&) const override { return 8; }
int operator()(Test<J> const&) const override { return 9; }
int operator()(Test<K> const&) const override { return 10; }
int operator()(Test<L> const&) const override { return 11; }
};

int main(int argc, char const* argv[])
{
Foo foo;
Bar bar;

switch (argc) {
case 0: foo = Foo{ std::in_place_index<0> }; break;
case 1: foo = Foo{ std::in_place_index<1> }; break;
case 2: foo = Foo{ std::in_place_index<2> }; break;
default: foo = Foo{ std::in_place_index<3> }; break;
}
switch (argc) {
case 0: bar = Bar{ std::in_place_index<0> }; break;
case 1: bar = Bar{ std::in_place_index<1> }; break;
case 2: bar = Bar{ std::in_place_index<2> }; break;
case 3: bar = Bar{ std::in_place_index<3> }; break;
case 4: bar = Bar{ std::in_place_index<4> }; break;
case 5: bar = Bar{ std::in_place_index<5> }; break;
case 6: bar = Bar{ std::in_place_index<6> }; break;
default: bar = Bar{ std::in_place_index<7> }; break;
}

std::cout << std::visit(FooVisitor{ }, foo) << "\n";
std::cout << std::visit(BarVisitor{ }, bar) << "\n";

return 0;
}

As you can see, the Visitor class template accepts a std::variant type as a template parameter, from which it will define an interface that must be implemented in any child classes that inherit from the template class instantiation. If, in a child class, you happen to forget to override one of the pure virtual methods, you will get an error like the following.

$ g++ -std=c++17 -o example example.cc
example.cc: In function ‘int main(int, const char**)’:
example.cc:87:41: error: invalid cast to abstract class type ‘BarVisitor’
87 | std::cout << std::visit(BarVisitor{ }, bar) << "\n";
| ^
example.cc:51:8: note: because the following virtual functions are pure within ‘BarVisitor’:
51 | struct BarVisitor final : Visitor<Bar>
| ^~~~~~~~~~
example.cc:29:17: note: ‘int DefineVirtualFunctor<T>::operator()(const T&) const [with T = Test<J>]’
29 | virtual int operator()(T const&) const = 0;
| ^~~~~~~~

This is much easier to understand than the error messages that the compiler usually generates when using std::visit().

Confusing Template error

ISO C++03 14.2/4:

When the name of a member template specialization appears after . or -> in a postfix-expression, or after nested-name-specifier in a qualified-id, and the postfix-expression or qualified-id explicitly depends on a template-parameter (14.6.2), the member template name must be prefixed by the keyword template. Otherwise the name is assumed to name a non-template.

In t->f0<U>(); f0<U> is a member template specialization which appears after -> and which explicitly depends on template parameter U, so the member template specialization must be prefixed by template keyword.

So change t->f0<U>() to t->template f0<U>().

C++ vs. D , Ada and Eiffel (horrible error messages with templates)

The problem, at heart, is that error recovery is difficult, whatever the context.

And when you factor in C and C++ horrid grammars, you can only wonder that error messages are not worse than that! I am afraid that the C grammar has been designed by people who didn't have a clue about the essential properties of a grammar, one of them being that the less reliance on the context the better and the other being that you should strive to make it as unambiguous as possible.

Let us illustrate a common error: forgetting a semi-colon.

struct CType {
int a;
char b;
}
foo
bar() { /**/ }

Okay so this is wrong, where should the missing semi-colon go ? Well unfortunately it's ambiguous, it can go either before or after foo because:

  • C considers it normal to declare a variable in stride after defining a struct
  • C considers it normal not to specify a return type for a function (in which case it defaults to int)

If we reason about, we could see that:

  • if foo names a type, then it belongs to the function declaration
  • if not, it probably denotes a variable... unless of course we made a typo and it was meant to be written fool, which happens to be a type :/

As you can see, error recovery is downright difficult, because we need to infer what the writer meant, and the grammar is far from being receptive. It is not impossible though, and most errors can indeed be diagnosed more or less correctly, and even recovered from... it just takes considerable effort.

It seems that people working on gcc are more interested in producing fast code (and I mean fast, search for the latest benchmarks on gcc 4.6) and adding interesting features (gcc already implement most - if not all - of C++0x) than producing easy to read error messages. Can you blame them ? I can't.

Fortunately there are people who think that accurate error reporting and good error recovery are a very worthy goal, and some of those have been working on CLang for quite a bit, and they are continuing to do so.

Some nice features, off the top of my head:

  • Terse but complete error messages, which include the source ranges to expose exactly where the error emanated from
  • Fix-It notes when it's obvious what was meant
  • In which case the compiler parses the rest of the file as if the fix had been there already, instead of spewing lines upon lines of gibberish
  • (recent) avoid including the include stack for notes, to cut out on the cruft
  • (recent) trying only to expose the template parameter types that the developper actually wrote, and preserving typedefs (thus talking about std::vector<Name> instead of std::vector<std::basic_string<char, std::allocator<char>>, std::allocator<std::basic_string<char, std::allocator<char>> > which makes all the difference)
  • (recent) recovering correctly in case of a missing template in case it's missing in a call to a template method from within another template method

But each of those has required several hours to days of work.

They certainly didn't come for free.

Now, concepts should have (normally) made our lives easier. But they were mostly untested and so it was deemed preferable to remove them from the draft. I must say I am glad for this. Given C++ relative inertia, it's better not to include features that haven't been thoroughly revised, and the concept maps didn't really thrilled me. Neither did they thrilled Bjarne or Herb it seems, as they said that they would be rethinking Concepts from scratch for the next standard.

How to deal with way too long STL template error report?

STLFilt: An STL Error Message Decryptor for C++ is a popular tool to filter these verbose error messages and turn them into something more legible.

From their website:

STLFilt was initially conceived as a teaching aid, to allow students
taking C++ and/or STL-specific workshops to make sense of typically
overbloated STL error messages. Today, however, even some C++ experts
have adopted STLFilt for use in everyday development. The results may
not always be perfect, but most of the time the information lost
during Decryption is not critical to the application being debugged.
The rest of the time, Decryption is easy enough to bypass.

The distribution for each platform (compiler/library set) is
self-contained and tuned to the idiosyncrasies of that platform. Each
Perl script performs basic regex substitutions for all the standard
(and extended, if present in the library) STL components, while
certain versions of the script go further with respect to message
ordering, line wrapping, library header error treatment, etc., as I
unilaterally deemed appropriate for that platform.

Here's a demo run that shows how it can be useful:

The source program:

#include <map>
#include <algorithm>
#include <cmath>

const int values[] = { 1,2,3,4,5 };
const int NVALS = sizeof values / sizeof (int);

int main()
{
using namespace std;

typedef map<int, double> valmap;

valmap m;

for (int i = 0; i < NVALS; i++)
m.insert(make_pair(values[i], pow(values[i], .5)));

valmap::iterator it = 100; // error
valmap::iterator it2(100); // error
m.insert(1,2); // error

return 0;
}

First, an unfiltered run using the MinGW gcc 3.2 compiler:

d:\src\cl\demo>c++2 rtmap.cpp
rtmap.cpp: In function `int main()':
rtmap.cpp:19: invalid conversion from `int' to `
std::_Rb_tree_node<std::pair<const int, double> >*'
rtmap.cpp:19: initializing argument 1 of `std::_Rb_tree_iterator<_Val, _Ref,
_Ptr>::_Rb_tree_iterator(std::_Rb_tree_node<_Val>*) [with _Val =
std::pair<const int, double>, _Ref = std::pair<const int, double>&, _Ptr =
std::pair<const int, double>*]'
rtmap.cpp:20: invalid conversion from `int' to `
std::_Rb_tree_node<std::pair<const int, double> >*'
rtmap.cpp:20: initializing argument 1 of `std::_Rb_tree_iterator<_Val, _Ref,
_Ptr>::_Rb_tree_iterator(std::_Rb_tree_node<_Val>*) [with _Val =
std::pair<const int, double>, _Ref = std::pair<const int, double>&, _Ptr =
std::pair<const int, double>*]'
E:/GCC3/include/c++/3.2/bits/stl_tree.h: In member function `void
std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::insert_unique(_II,

_II) [with _InputIterator = int, _Key = int, _Val = std::pair<const int,
double>, _KeyOfValue = std::_Select1st<std::pair<const int, double> >,
_Compare = std::less<int>, _Alloc = std::allocator<std::pair<const int,
double> >]':
E:/GCC3/include/c++/3.2/bits/stl_map.h:272: instantiated from `void std::map<_
Key, _Tp, _Compare, _Alloc>::insert(_InputIterator, _InputIterator) [with _Input
Iterator = int, _Key = int, _Tp = double, _Compare = std::less<int>, _Alloc = st
d::allocator<std::pair<const int, double> >]'
rtmap.cpp:21: instantiated from here
E:/GCC3/include/c++/3.2/bits/stl_tree.h:1161: invalid type argument of `unary *
'

And a filtered run using the gcc-specific Proxy c++:

d:\src\cl\demo>c++ rtmap.cpp
*** {BD Software Proxy c++ for gcc v3.01} STL Message Decryption is ON! ***
rtmap.cpp: In function `int main()':
rtmap.cpp:19: invalid conversion from `int' to `iter'
rtmap.cpp:19: initializing argument 1 of `iter(iter)'
rtmap.cpp:20: invalid conversion from `int' to `iter'
rtmap.cpp:20: initializing argument 1 of `iter(iter)'
stl_tree.h: In member function `void map<int,double>::insert_unique(_II, _II)':
[STL Decryptor: Suppressed 1 more STL standard header message]
rtmap.cpp:21: instantiated from here
stl_tree.h:1161: invalid type argument of `unary *'

STL Decryptor reminder:
Use the /hdr:L option to see all suppressed standard lib headers

[Note: demo runs were performed in an 80-column console window with
STLFilt's intelligent line wrapping enabled, and with internal
switches set to produce messages as terse as possible. More detail is
available by tailoring the Decryptor's options.]

The only downside I can see is that it mislabels the C++ Standard Library. :(

Here's a relevant journal article by STLFilt's author.



Related Topics



Leave a reply



Submit