Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 8 of 8

Lesson 11.5 taught reading a stack trace. This is the same skill for compile time, and it is the one that decides whether templates are usable for you or not.

The wall

// #include <iostream>
// #include <map>
// #include <string>
// #include <vector>
//
// struct Widget { int id; };
//
// int main() {
//     std::map<std::string, std::vector<Widget>> registry;
//     registry["a"].push_back(Widget{1});
//     std::cout << registry << "\n";        // maps do not have operator<<
//     return 0;
// }

GCC 13 produces 338 lines for that, containing 122 note: lines. Nobody reads all of it. Nobody should.

Here is the first error, which is the entire diagnosis:

main.cpp: In function 'int main()':
main.cpp:11:15: error: no match for 'operator<<' (operand types are 'std::ostream'
    {aka 'std::basic_ostream<char>'} and 'std::map<std::__cxx11::basic_string<char>,
    std::vector<Widget> >')
   11 |     std::cout << registry << "\n";
      |     ~~~~~~~~~ ^~ ~~~~~~~~
      |          |       |
      |          |       std::map<std::basic_string<char>, std::vector<Widget> >
      |          std::ostream

Your file, your line, the operation, and both operand types. The remaining 330 lines are the compiler listing every operator<< in the standard library and explaining why each one did not match — useful to the compiler author, not to you.

The technique

  1. Read the first error only. Everything after it is usually a consequence.
  2. Find the line in your own file. The same rule as frame #0 in a stack trace: skip the library, find your name.
  3. Read the types, ignoring the noise. Mentally rewrite std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> as std::string.
  4. Read the required from chain. It is the instantiation path, newest last. The bottom entry is your call.
  5. Fix, recompile. One error at a time. Fixing the first often removes fifty.

Two substitutions make most standard library types readable:

| What you see | What it is | |---|---| | std::__cxx11::basic_string<char, ...> | std::string | | std::basic_ostream<char, ...> | std::ostream | | __gnu_cxx::__normal_iterator<T*, std::vector<T>> | std::vector<T>::iterator | | std::_Rb_tree_iterator<...> | a map or set iterator |

Making the compiler help

g++ -std=c++20 -fno-diagnostics-show-caret main.cpp

On the 338-line example above:

| Flags | Lines | |---|---| | default | 338 | | -fmax-errors=1 | 328 | | -fno-diagnostics-show-caret | 137 |

-fmax-errors=1 barely helps here, because this is one error with 122 notes — worth knowing, since the flag is the usual first suggestion. Suppressing the caret diagrams cuts the output by more than half, because each note carries three lines of source art.

Other flags worth knowing:

-fconcepts-diagnostics-depth=2     # GCC: explain why a concept was not satisfied
-ftemplate-backtrace-limit=0       # show the full instantiation chain, not a summary
-fdiagnostics-color=always         # colour, when piping through less -R

And a genuinely different option: Clang's template diagnostics are usually shorter and better organised than GCC's. If you have both, compiling a confusing error with the other compiler is often faster than deciphering the first one.

The four errors that produce most of the pain

1. A missing operation on T

error: no match for 'operator<' (operand types are 'const Widget' and 'const Widget')

Means: the template needs an operation your type does not provide.

Fix: give the type the operator, pass a comparator, or constrain the template with a concept so the error moves to the call (lesson 12.7).

The instantiation chain is what tells you which template wanted it. Read required from upward until you reach your own call.

2. A template split across a header and a .cpp

This one is a linker error, which is why it confuses people — the code compiles fine:

/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x28): undefined reference to `Box<int>::Box(int)'
/usr/bin/ld: main.cpp:(.text+0x34): undefined reference to `Box<int>::get() const'
collect2: error: ld returned 1 exit status

Means: you declared the template in box.h and defined it in box.cpp. Compiling box.cpp instantiated nothing, because nothing there uses Box<int>. Compiling main.cpp saw a declaration with no body, so it emitted a call to a function that does not exist.

Fix: put the definitions in the header (lesson 12.1). If you truly need them in a .cpp, add an explicit instantiation there:

// At the bottom of box.cpp — generate these versions here, and only these:
// template class Box<int>;
// template class Box<std::string>;

That works, and it keeps compile times down for a template used with a handful of known types. It fails the moment someone wants Box<double>.

3. typename missing before a dependent type

// template <typename T>
// void process(const std::vector<T>& v) {
//     std::vector<T>::const_iterator it = v.begin();     // needs typename
// }
error: need 'typename' before 'std::vector<T>::const_iterator' because
       'std::vector<T>' is a dependent scope
error: expected ';' before 'it'

Means: while parsing the template, the compiler does not yet know what T is, so it cannot know whether std::vector<T>::const_iterator is a type or a static member. It assumes not a type, and the line stops parsing.

Fix: typename std::vector<T>::const_iterator it = v.begin();

Note the second error — expected ';' — is a consequence of the first, which is rule 1 of the technique in action.

In practice you write auto it = v.begin(); and the question never comes up. But you will read older code full of typename, and now you know why it is there.

C++20 removed the requirement in many contexts, though not this one — GCC 13 reports it identically under -std=c++17 and -std=c++20.

4. A member of a dependent base class is invisible

// template <typename T>
// class Base {
// protected:
//     T value = T{};
// };
//
// template <typename T>
// class Derived : public Base<T> {
// public:
//     void show() { std::cout << value << "\n"; }     // 'value' not found
// };
error: 'value' was not declared in this scope
   13 |         std::cout << value << "\n";
      |                      ^~~~~

Means: this is the one that looks like a compiler bug and is not. Base<T> depends on T, and a specialization of Base might not have a value at all — so the compiler refuses to look in the base class when parsing the template.

Fix: name it explicitly, either way:

// std::cout << this->value << "\n";       // most common
// std::cout << Base<T>::value << "\n";    // also fine
// using Base<T>::value;                   // once, in the class body

If you have ever wondered why template-heavy code is full of this-> on members that are obviously in scope, this is the reason.

Worked example

main.cpp: In instantiation of 'void print_sorted(std::vector<T>&) [with T = Widget]':
main.cpp:24:18:   required from here
/usr/include/c++/13/bits/stl_algo.h:1819:14:   required from
    'constexpr void std::__insertion_sort(...)'
/usr/include/c++/13/bits/predefined_ops.h:45:23: error: no match for 'operator<'
    (operand types are 'Widget' and 'Widget')

Work it in the technique's order:

  • Line 1 — a template being instantiated with T = Widget. Your function, your type.
  • Line 2 — main.cpp:24 is where you called it. That is the line to look at.
  • Line 3 — inside std::sort. Library. Skip.
  • Line 4 — Widget has no operator<.

Four lines, and the whole thing is: line 24 sorts Widgets, and Widget cannot be compared. Give Widget an operator< — or, in C++20, auto operator<=>(const Widget&) const = default;, which generates all six comparisons at once.

Prevention

The errors get shorter when you make them impossible to reach:

  • Constrain with concepts (lesson 12.7) so the failure is at the call, not inside the body.
  • static_assert early in the template body, with a message you wrote:
// template <typename T>
// void process(const T& value) {
//     static_assert(std::is_arithmetic_v<T>, "process() requires a numeric type");
//     ...
// }

The assert fires before anything else does, and prints your sentence rather than the compiler's.

  • Test each instantiation. A template with no Box<std::string> anywhere in the codebase has never been compiled for std::string. Instantiate it in a test, and the error arrives while you are writing it instead of a month later.

Try it

Write a template that fails for a type lacking the operation it needs, and read the error. Then add a static_assert with your own message and read it again. Then constrain it with a concept and read it a third time. Count the lines each version produces.

Checkpoint

  • Read the first error; the rest is usually consequence and library noise.
  • Find the line in your own file — the same rule as frame #0 in a stack trace.
  • Mentally substitute std::string for std::__cxx11::basic_string<char, ...>.
  • The required from chain is the instantiation path; your call is the bottom entry.
  • -fno-diagnostics-show-caret cut a 338-line error to 137; -fmax-errors=1 barely helped.
  • Clang's template errors are often shorter than GCC's — try the other compiler.
  • Undefined reference to a template member means it was defined in a .cpp, not the header.
  • need 'typename' means a dependent type; write typename or use auto.
  • 'value' was not declared in a derived template means you need this->value.
  • Prevent the wall: constrain with concepts, static_assert early, and test each instantiation.

Questions about this lesson

Log in to ask a question.