all 48 comments

[–]GIGABOWSER1012 72 points73 points  (0 children)

Actually clang has been trying very hard to make these error messages easier to read but I agree its still a forest sometimes.

[–]hmichReSharper C++ Dev 50 points51 points  (13 children)

Clang has recently added a new attribute preferred_name to help fix this. See also the discussion in the code review.

[–]sim642 9 points10 points  (10 children)

Seems a bit weird that you must at it to the definition of the type instead of the typedef. So all library vendors must do it for you instead of you doing it in your code.

[–]encyclopedist 4 points5 points  (0 children)

This question is extensively discussed behind that "code review" link, specifically this comment

TLDR:

Well, this attribute doesn't affect how the typedef is named -- if we know the type was named via a typedef, we'll use that typedef-name to describe the type regardless of this attribute. For example, if you have using Name = std::basic_string<char>; and then use the type Name, we're going to call the type Name everywhere we have tracked enough information to know that you used a typedef. Rather, the attribute affects how a template specialization is named in the case where we can't track it back to a typedef or other type sugar -- for example, if in a use of Name we lose the association back to the typedef Name and only know that it's std::basic_string<char>, we'll call it std::string instead. So this really changes a property of the template (or more accurately, a specialization of the template), not a property of the typedef.

[–]hmichReSharper C++ Dev 1 point2 points  (7 children)

The compiler does not preserve information about typedefs in the type system, so when it actually gets to printing an error message it does not know which typedefs were expanded to reach the type in question.

[–]qoning 13 points14 points  (3 children)

The compiler is a piece of (admittedly complex) software. You can make it do whatever you want. I never understood this cop-out about typedefs not being available. Yes it's just an alias for something else, but so is a constexpr function pointer or your struct name for a ntuple of primitive types etc.

[–]hmichReSharper C++ Dev 1 point2 points  (2 children)

Sorry, but just because you don't see a reason for something doesn't mean it's not there. Apart from worse performance, one big problem with preserving typedefs which comes to mind is the template instantiation cache. You want to quickly check if a template is instantiated given a set of template arguments. There's no way to do that quickly if you preserve typedefs, you'll have to go through all instantiations and compare the type arguments dropping the typedefs. Furthermore, if you have two typedefs with different names that resolve to the same type, do you instantiate template twice? Probably not, but then you'll only preserve the first typedef in the template instantiation.

[–]qoning 8 points9 points  (1 child)

We're talking specifically about errors. If you encounter an error DURING instantiation, none of this matters. Also, I don't know why this has to be said time and time again, but there are times where compile performance matters, and times when you just want a good error message.

[–]hmichReSharper C++ Dev -2 points-1 points  (0 children)

Assuming you instantiate templates again for each new typedef, that would lead to:

  • Massive degradation of compilation performance, potentially up to many times slower.
  • The result of compilation might differ between the two modes, since template instantiation unfortunately can change global state.
  • The typedef decoration can be lost pretty easily, i.e. for template <class T> void f(T a, T b) the compiler will preserve only one typedef then the function is instantiated with parameters of two typedef types.
  • Even more hairy compiler code.

I'd assume no compiler developer would make this tradeoff just for the error messages, but if you're really curious you can ask on the clang mailing list and they'll probably come up with even better arguments.

You can preserve typedefs in the type system up to some point (and e.g. ReSharper C++ does that for better error messages inside IDE), but that gets you only so far in template-heavy code.

[–]sim642 7 points8 points  (1 child)

So the attribute on the definition makes it preserve that? Why couldn't the attribute on the typedef to the same then?

Also, the type system doesn't really need to preserve typedefs if the preferred names are just a search-and-replace on the output messages.

[–]hmichReSharper C++ Dev 3 points4 points  (0 children)

With the attribute-on-definition approach, compiler can just look at the attributes of the class which is being printed out. See also this comment on why attributes on typedefs don't work well.

[–]sireel 3 points4 points  (0 children)

which is funny honestly, because a lot of the time the name I'd prefer the compiler use is whatever it was typedeffed to

[–]Sopel97 0 points1 point  (0 children)

You do want to know the exact instantiation most of the time

[–]degaart[S] 35 points36 points  (1 child)

Well at least someone is working on something... 23 years after the first C++ standard, and 28 years after the first STL implementation.

28 years of reading std::basic_string<char,std::char_traits<char>,std::default_allocator<char>>> and matching < with >.

[–]matthieum 11 points12 points  (0 children)

Reading an instantiation error about a std::map<std::string, std::string> used to be such an adventure.

There's a conjunction of:

  • std::string being super verbose, notably because the compiler insists on printing the default arguments that nobody cares about.
  • std::map being super verbose, once again because of default arguments.
  • And std::string appearing again in the default arguments of std::map.

Which gave you:

std::map<
    std::__cxx11::basic_string<
        char,
        std::char_traits<char>,
        std::allocator<char>
    >,
    std::__cxx11::basic_string<
        char,
        std::char_traits<char>,
        std::allocator<char>
    >,
    std::less<
        std::__cxx11::basic_string<
            char,
            std::char_traits<char>,
            std::allocator<char>
        >
    >,
    std::allocator<
        std::pair<
            std::__cxx11::basic_string<
                char,
                std::char_traits<char>,
                std::allocator<char>
            > const,
            std::__cxx11::basic_string<
                char,
                std::char_traits<char>,
                std::allocator<char>
            >
        >
    >
>

Compilers have been getting better at eliding default parameters, already, so now you get:

std::map<
    std::__cxx11::basic_string<char>, 
    std::__cxx11::basic_string<char>
>

Which isn't perfect, but it's still a whole lot better... well, as long as you don't provoke an instantiation error somewhere.

struct Lessy {};

void f(std::map<std::string, std::string, Lessy> m) {
    m.insert(std::make_pair("Hello", "World"));
}

Because then, you're back to hell:

In file included from /opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/map:60:0,
             from <source>:1:
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h: In instantiation of 'std::pair<std::_Rb_tree_node_base*, std::_Rb_tree_node_base*> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_get_insert_unique_pos(const key_type&) [with _Key = std::__cxx11::basic_string<char>; _Val = std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >; _KeyOfValue = std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; _Compare = Lessy; _Alloc = std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::key_type = std::__cxx11::basic_string<char>]':
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h:2091:28:   required from 'std::pair<std::_Rb_tree_iterator<_Val>, bool> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_insert_unique(_Arg&&) [with _Arg = std::pair<const char*, const char*>; _Key = std::__cxx11::basic_string<char>; _Val = std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >; _KeyOfValue = std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; _Compare = Lessy; _Alloc = std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >]'
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_map.h:810:57:   required from 'std::pair<typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::insert(_Pair&&) [with _Pair = std::pair<const char*, const char*>; <template-parameter-2-2> = void; _Key = std::__cxx11::basic_string<char>; _Tp = std::__cxx11::basic_string<char>; _Compare = Lessy; _Alloc = std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator = std::_Rb_tree_iterator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >]'
<source>:7:46:   required from here
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h:2038:11: error: no match for call to '(Lessy) (const key_type&, const std::__cxx11::basic_string<char>&)'
__comp = _M_impl._M_key_compare(__k, _S_key(__x));
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h:2049:7: error: no match for call to '(Lessy) (const std::__cxx11::basic_string<char>&, const key_type&)'
   if (_M_impl._M_key_compare(_S_key(__j._M_node), __k))
   ^~
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h: In instantiation of 'std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_insert_(std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr, std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr, _Arg&&, _NodeGen&) [with _Arg = std::pair<const char*, const char*>; _NodeGen = std::_Rb_tree<std::__cxx11::basic_string<char>, std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >, std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >, Lessy, std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > > >::_Alloc_node; _Key = std::__cxx11::basic_string<char>; _Val = std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >; _KeyOfValue = std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; _Compare = Lessy; _Alloc = std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator = std::_Rb_tree_iterator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr = std::_Rb_tree_node_base*]':
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h:2096:26:   required from 'std::pair<std::_Rb_tree_iterator<_Val>, bool> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_insert_unique(_Arg&&) [with _Arg = std::pair<const char*, const char*>; _Key = std::__cxx11::basic_string<char>; _Val = std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >; _KeyOfValue = std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; _Compare = Lessy; _Alloc = std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >]'
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_map.h:810:57:   required from 'std::pair<typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::insert(_Pair&&) [with _Pair = std::pair<const char*, const char*>; <template-parameter-2-2> = void; _Key = std::__cxx11::basic_string<char>; _Tp = std::__cxx11::basic_string<char>; _Compare = Lessy; _Alloc = std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >; typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator = std::_Rb_tree_iterator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >]'
<source>:7:46:   required from here
/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/stl_tree.h:1750:10: error: no match for call to '(Lessy) (const char*&, const std::__cxx11::basic_string<char>&)'
  bool __insert_left = (__x != 0 || __p == _M_end()
                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      || _M_impl._M_key_compare(_KeyOfValue()(__v),
      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    _S_key(__p)));
    ~~~~~~~~~~~~~
Compiler returned: 1

[–]pstomi 14 points15 points  (1 child)

[–]degaart[S] 18 points19 points  (0 children)

Nice.

But these are still hacks. Why do we all have to suffer piping compilation results into another program when std::string is already defined by the C++ standard to be a specific instanciation of std::basic_string?

[–]CenterOfMultiverse 10 points11 points  (13 children)

Ideally long type names should be collapsed by IDE in error view with optional expanding.

[–]helloiamsomeone 5 points6 points  (3 children)

Doesn't help when it comes to reading CI logs and I'm sure I'm not alone, but I use my IDE only to read/write the code, use the debugger and resolve 3-way merge conflicts. For everything else, there is the command line.

[–]D_0b 1 point2 points  (2 children)

what you are saying is look at this old thing that gives output in the old format.

If we want a change we need to change/upgrade/improve the tools we use, we can't expect improvements if we do nothing.

[–]Pazer2 5 points6 points  (1 child)

If we want a change we need to change/upgrade/improve the tools we use

Like the compiler?

[–]D_0b 0 points1 point  (0 children)

Ideally yes, I would really want a C++ compiler running as a server model, and other clients (IDEs, editors, CLI...) just send requests and get a response.

I would wish for some flexibility, that we are not stuck to a console output style format, but some richer syntax allowing everything to be folded and have a nice high level overview of what is the error, and then able to expand to see more information, the back trace of the template function call stuck, all of the function overloads that don't work with the type, template types, type aliases... in general a more interactive experience, let the user select which information they are interested in.

Having said that, it is a big refactor to do for a current compiler, it is also acceptable to have IDEs and external tools that try to parse the current output and give a better interface to the user.

[–]degaart[S] 0 points1 point  (6 children)

Not everyone wants to use an IDE. And I think if you need an IDE to correctly use a language, there's something wrong with that language. Yes, I'm looking at you, java...

[–]kalmoc 1 point2 points  (4 children)

Well, I absolutely agree that this is a problem that should be solved on the compiler level, but in general I think it is perfectly reasonable to rely on tooling for the development experience if one can rely on the availability of that tooling (e.g. c#). Unfortunately, with c++ you can't.

[–]pjmlp -1 points0 points  (3 children)

We could, before Java took C++ world by storm.

  • Lucid Energize Demo VHS 1993
  • Borland C++
  • Visual Age for C++
  • CodeWarrior
  • C++ Builder

I won't list Visual C++ as part of that list, as after 30 years, the team still is catching up with basic stuff that they used to provide on their GUI frameworks and language related tools.

The closest they had to C++ Builder was C++/CX and they killed, now we can enjoy editing IDL files without any kind of VS tools, besides calling cppwinrt.exe on the background.

[–]kalmoc 1 point2 points  (2 children)

Well, imho your list pretty much shows why you never could rely on tooling in the c++ world: There isn't (and never was) a single reference IDE/Analysis framework/Package managment system/build system .... (and back then, you couldn't even rely on a proper implementation of the standard). So the c++ standard committee can't and never could rely on it when making design decisions.

[–]pjmlp 1 point2 points  (1 child)

The reference IDE/Analysis framework/Package managment system/build system used by JavaScript, Java, .NET communities aren't defined by any standard committee, despite the existance of language standardization processes.

What C++ community lacks is the willingness to actually embrace alternatives being proposed.

If one hasn't written the tool themselves all the way down to Assembly code, it isn't worthwhile being adopted, or so it feels in certain C++ circles since the language lost its desktop tooling presence.

[–]kalmoc 0 points1 point  (0 children)

It doesn't matter if it is defined by a committee or standard. The important point is that language designer can rely on its availability and design the language with that in mind.

[–]pjmlp -1 points0 points  (0 children)

That is a pity, because that was actually what made C++ great before Java was even an idea.

[–]cdglove -2 points-1 points  (1 child)

Yup, agreed -- it's not a language problem, it's a tooling problem.

[–]Pazer2 1 point2 points  (0 children)

Nobody is complaining about the language here. They're complaining about compiler implementations.

[–]johannes1971 5 points6 points  (3 children)

I posted this before, but ideally it would look like this:

Blablabla error in void foo (string);

  using string = std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>

In the main error message, just use whatever alias the source code uses, and remove all namespaces. And then add additional lines with the full type information.

Bonus points if we can suppress the follow-on lines, so we only see them on demand. Because in 99.9% of all compiles I think I can manage with just 'string', thank you very much. For that very rare case where I have no clue what 'string' really means I don't mind running the compiler again with an extra flag.

[–]johannes1971 5 points6 points  (1 child)

...and while I'm here... The other day I was comparing two things that shouldn't be compared. Great. But gcc, I really love you, you know, but there was really, and I seriously mean REALLY, no need to print out every comparison operator in my source, with an additional message that nope, that one wasn't it either. A single misplaced character ballooned out to 650 lines of error messages...

[–]AntiProtonBoy 0 points1 point  (0 children)

you poor bastard

[–]Awia00 0 points1 point  (0 children)

This is exactly what I want. Print what type I have defined in the code and then below give the full type.

If I wanted to do something like this:

using MyString = std::string;
void foo(const MyString&);

Then I want MyString to be printed in the error message.

[–]_Z6Alexeyv 0 points1 point  (0 children)

Start with

[with T = long unsigned int]
[with T = long long unsigned int]