Skip to content

Module 12 — Debugging

The technique this module adds

Reduction: read the error from the end, then rebuild the failure in five lines.

Every earlier module's technique worked on the code near the error. Here the error is usually not in your file — it is in a library header you have never read, quoting types you did not write, in a message four hundred lines long. In generic code, reduction is not a debugging step, it is the method, and it has three moves:

1. Read the last line first. That is the actual complaint, and it is almost always about your type. Everything above it is the trail the compiler took to get there.

scratch/m12d-errors.cpp: In instantiation of 'T smallest(const std::vector<T>&) [with T = Reading]':
scratch/m12d-errors.cpp:31:20:   required from here
scratch/m12d-errors.cpp:17:19: error: no match for 'operator<' (operand types are 'const Reading' and 'Reading')

Read bottom-up: Reading cannot be ordered. The whole answer is in the last line; the two lines above it tell you which instantiation produced it, which is what you need when the same template is used in ten places.

2. Find the [with T = …]. That names the exact instantiation. A component used with several types will produce several errors; the one you want is the one naming the type you just added.

3. Reproduce it in five lines. Take your type and the one operation, drop the library, and compile:

struct Reading { std::string sensor; double value = 0.0; };

int main() {
    Reading a{"a", 1.0};
    Reading b{"b", 2.0};
    bool ordered = a < b;          // the whole requirement, in one line
}

This is worth doing even when you are sure. If the five-line version compiles, the requirement was met and the problem is elsewhere; if it fails, you have the requirement in one message instead of a page, and you can decide deliberately whether to give the type the operation or to constrain the component.

Two flags worth knowing, and one warning. GCC and Clang accept -ftemplate-backtrace-limit=1, which truncates the instantiation trail to its first entry — useful when the trail is longer than the error. The warning: trimming output can hide the frame you need, which is the one naming your type. Try the full message first, with the last line in your head.


A worked diagnosis

Symptom. A new function, summaryOf, works for Repository<Book> and produces all of this for Repository<Member>:

In file included from …/include/c++/14.2.0/vector:67,
                 from repository.h:4,
                 from main.cpp:1:
…/include/c++/14.2.0/bits/stl_uninitialized.h: In instantiation of
'constexpr bool std::__check_constructible() [with _ValueType = Member; _Tp = const Member&]':
…
…/include/c++/14.2.0/vector:606:31:   required from
'std::vector<_Tp, _Alloc>::vector(const std::vector<_Tp, _Alloc>&) [with _Tp = Member; …]'
repository.h:88:30:   required from 'std::vector<T> Repository<T>::summaryOf() const [with T = Member]'
main.cpp:42:18:   required from here
…/include/c++/14.2.0/bits/stl_uninitialized.h:90:56: error: static assertion failed:
         result type must be constructible from input type

Library paths in this output are abbreviated to …/include/c++/…. The frames that matter are the two that name your own files, repository.h and main.cpp; every other line is a library header you will never edit.

First hypothesis. "Something is wrong with std::vector." It is the file mentioned most often, and the error is inside it.

Test. Read the last line — result type must be constructible from input type — then find the one frame that names a file of ours: required from 'std::vector<T> Repository<T>::summaryOf() const [with T = Member]', at repository.h:88. Then reduce:

Member m{7, "Ayesha", 2};
std::vector<Member> copy = std::vector<Member>{m};   // does copying a Member work at all?

Result. That compiles. So the element is copyable, and the failing instantiation must involve something else — and reading repository.h:88 again shows what:

std::vector<T> summaryOf() const {
    return std::vector<std::unique_ptr<T>>(items_);   // ← a vector of unique_ptr, copied
}

Actual cause. The function returns vector<unique_ptr<T>> and tries to construct it from items_, which is a vector<T>. The error is genuine but the first hypothesis was wrong: the library was reporting that unique_ptr<Member> cannot be constructed from Member, which is true and is not what the function wanted to do at all. The real defect is the function's return type.

Fix. Return what the function actually produces, and move the ownership problem out of it:

std::vector<T> summaryOf() const { return items_; }        // values: copyable, and correct

Lesson. The library's error was a statement about the type I passed to it, and reading the required from frame that named my own file found the mismatch. Four hundred lines of diagnostics reduced to one wrong return type in under a minute — because the last line says what is wrong and the frame with your filename in it says where.


The escalation ladder

  1. Read the first error, including any note: line — for ordinary code. In templates, invert this rule: read the last line of the first error, then the frames above it.
  2. Locate the frame that names your own file. required from here marks the call site you can change; everything else is the compiler showing its work.
  3. Reduce. Five lines, your type, one operation, no library. In generic code this is not preparation for debugging — it is the debugging.
  4. Check the common-mistakes table in Misconceptions and mistakes, then the glossary.
  5. Ask the machine. static_assert questions about your type — std::is_same<decltype(T{}.id), int>, std::is_copy_constructible<T>, std::is_nothrow_move_constructible<T> — answer "does this type meet the requirement?" at compile time, in one line, instead of in an error storm.
  6. Restate the problem in words, without code. "This algorithm needs an ordering and this type has none." Most template errors are one sentence long, once you have read them.
  7. Then consult cppreference for the exact requirement of the standard component you are passing your type to — it is stated precisely there, and guessing it is how the loop in step 3 repeats.

Tools introduced here

Tool What it shows How
The last line first The actual complaint, in one sentence Read a diagnostic bottom-up
[with T = …] Which instantiation failed Search the message for [with
required from here The line you can change Find the frame naming your own file
Reduction to five lines The requirement, isolated from the library Your type plus one operation, compiled alone
static_assert(std::is_…) Whether a type meets a requirement, at compile time Put it in the template, or in a throwaway test file
nm -C file.o Which object file defines a symbol the linker says is missing nm -C main.o \| grep ' U ', then the same for the object you expected to define it
-ftemplate-backtrace-limit=N A shorter instantiation trail (GCC/Clang) g++ -ftemplate-backtrace-limit=1 …, with the caveat that it may hide your frame
static_assert in the template body A requirement stated so a failure is one readable line The pattern in section 3 of Concepts
Explicit instantiation Proof that a type can be instantiated template class Repository<int>; — it compiles the whole class or tells you why not

Three of those deserve to become habits rather than excursions.

A throwaway test file per generic component. Five lines, one type, one operation. It documents the requirement, proves the type meets it, and takes seconds.

static_assert in the template, for every requirement you can express. A one-line message at the top of the component replaces a page of diagnostics for the next person to use it — including you, in a month.

Explicit instantiation as a check. template class Repository<Book>; in a test file forces the compiler to instantiate every member for that type, which finds the errors that "compiled only when used" was hiding until run time. It is the cheap equivalent of testing a template with a second type.


A note on what a template error is not

The messages in this module are long, unhelpful-looking and full of type names you did not write, which makes it tempting to conclude that templates are unreliable or hostile. Two facts to keep alongside the technique:

  • The error is almost always accurate and about your type. The compiler's complaint (no match for 'operator<', no member named 'id', not constructible from input type) is a precise statement of a requirement your type did not meet. It is verbose about where it was instantiated from — because that is the information a person debugging a generic library needs.
  • Every message here has an exact fix, and most of them are one line: a typename, an operator<, an explicit template argument, a definition moved into a header. Lab 12 asks you to record one such reduction, precisely so that the next storm has somewhere to be filed.

Next: Exercises — ten tasks, from writing a template used with three types to building a generic component with documented requirements.