Module 12 — Misconceptions and Mistakes¶
Two different problems, fixed two different ways.
A misconception is a wrong mental model, and only evidence removes it, so each one below is paired with a program that contradicts it. A mistake is a concrete error, and information fixes it, so each one below comes with the message the compiler actually printed and the corrected code.
Every diagnostic quoted here was produced by compiling the snippets shown, with the course's flags, on the toolchain described in Setting up.
Part 1 — Misconceptions¶
"Templates are for building libraries, not applications."¶
Why this feels true. The famous templates are library ones: std::vector, std::sort,
std::map. The examples in textbooks are containers and algorithms, written by people who do that
for a living.
Where it breaks. Lab 12's Repository<T> is an application component — a named collection
with add, find and iteration — written once and used for two record kinds, because the library
system now has three kinds of record and a fourth coming:
Part A - one class template, two record kinds
Repository<Book> holds 3 book(s); Repository<Member> holds 2 member(s)
The correct model. A template is a tool for removing duplication when the only thing that differs is a type. That happens in applications constantly: three containers, two serialisers, a retry helper, a cache keyed by something different. The standard library is the most-tested example of the technique, not the definition of its scope.
The re-test. Name one thing in your own Lab 11 program that is duplicated with only the type differing. (Most answers are a container, an accessor pair, or a load/save function.)
"A template is a class that works with anything."¶
Why this feels true. The syntax carries no restrictions. template <typename T> reads as though
T can be absolutely anything.
Where it breaks. Ask the template to do something the type cannot do:
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')
The correct model. Every template has requirements, whether or not anybody wrote them down:
the operations its body performs. smallest requires an ordering; Repository::sortedById requires
an int id member; a container requires its element type to be movable or copyable. Requirements are
part of the component's interface, and the ones the compiler can check belong in a static_assert
with a message a human can read.
The re-test. What does std::sort require of its element type, and what happens if you break
it? (Strict weak ordering from operator< — or a comparator — and if you break it the result is not
an error but silent nonsense, because the algorithm's contract is not checkable.)
"Templates and virtual functions are two ways to do the same thing."¶
Why this feels true. Both let one piece of code serve many types, and both are taught as "the polymorphism" of their respective kinds.
Where it breaks. Build the same extension point both ways and the difference is not stylistic:
template <typename T> class Repository { std::vector<T> items_; /* ... */ }; // compiled per T
class RecordSink { // chosen at run time
public:
virtual ~RecordSink() = default;
virtual void store(const Record& record) = 0;
};
The correct model. A template is decided by the compiler, costs nothing at run time, works for
int, and cannot grow after it is compiled — a new type recompiles its users. A virtual
interface is decided while the program runs, costs an indirection per call, needs a base class, and
can be extended by code written years later — a plugin, a configured backend, a new kind of shape
read from a file. The right question is not "which is better" but "is this type a fact about the
design, or something the program discovers?"
The re-test. A format converter must support every format found in a data directory, discovered at run time. Which mechanism, and why? (A virtual interface — the set is not known when the program is written.)
"Templates are always faster, so prefer them."¶
Why this feels true. Compile-time decisions can be inlined away, and the instantiations are as fast as hand-written code. That part is true, and it is the reason templates exist.
Where it breaks. The costs are real and appear elsewhere: compile time, code size, and the
difficulty of reading errors. Repository<Book> and Repository<Member> are two complete classes,
each with its own member functions instantiated per use; a template used with fifteen types in three
translation units generates a lot of code, and changing its header recompiles every user.
The correct model. Templates move work from run time to compile time. That is a good trade when the work is a function call per element and a bad one when it is an hour of build time for a program that runs for two seconds. Measure the build as well as the program — Lab 12 asks you to note what the compiler generated for each instantiation for exactly this reason.
The re-test. A template is used with one type only. What did generalising cost, and what did it buy? (Compile time, readability and error complexity; nothing.)
"Template code can go in a .cpp like anything else."¶
Why this feels true. It looks exactly like a class. Every other class in this course has a header and a source file, and Modules 9 and 11 spent effort keeping them tidy.
Where it breaks. The program compiles, and then fails to link — with a linker that names nothing at all, which is why the object files have to be inspected:
$ nm -C main.o | grep ' U ' | grep Repository
U Repository<int>::add(int const&)
U Repository<int>::size() const
$ nm -C repository.o | grep -c Repository
0
The correct model. The compiler must see the definition at every point of use, because that is
where it generates the concrete version. So the definition lives in the header — or lives in a .cpp
with explicit instantiations (template class Repository<int>;) for the types you intend to support,
which is a deliberate trade of flexibility for build time.
The re-test. Where does std::vector's definition live, and what would happen if it did not?
(In <vector>, which every user includes — otherwise no compiler could generate vector<Book>.)
"The compiler did not check my template."¶
Why this feels true. A template that nobody instantiates produces no errors at all, even if its body is nonsense for every type. That looks like an absence of checking.
Where it breaks. Syntax and everything independent of T is checked when the template is
defined. Watch a dependent name with an obvious spelling mistake:
error: need 'typename' before 'T::value_type' because 'T' is a dependent scope
error: dependent-name 'T::value_type' is parsed as a non-type, but instantiation yields a type
note: say 'typename T::value_type' if a type is meant
The parser complained the moment it read the definition, because the shape of the expression is
decidable even though T is not. What waits for instantiation is everything that depends on T.
The correct model. Templates are checked in two phases: at definition, and at instantiation. Phase two is where the surprises live — which is why "it compiles" for a template means very little until it is instantiated for the types you actually intend to use.
The re-test. Why does a class template's invalid member function not break its instantiation? (Member functions of a class template are compiled when they are used, not when the class is instantiated.)
"If it compiles for one type, it works for all."¶
Why this feels true. Because the code is generic, it feels verified by construction. It compiled, it ran, the output was right.
Where it breaks. The second type arrives — and it fails, on a requirement nobody wrote down:
error: no matching function for call to 'Repository<Book>::add(Member&)'
note: candidate: 'void Repository<T>::add(const T&) [with T = Book]'
note: no known conversion for argument 1 from 'Member' to 'const Book&'
or worse, it compiles and behaves differently, because the type satisfies the syntax of the
requirement while violating its meaning — a operator< that is not a strict weak ordering is the
classic case.
The correct model. A template is verified by using it with the second type, which is why the course's rule is to generalise when a second genuine use appears. Testing with one type tests one instantiation.
The re-test. Which types should you instantiate a generic component with, before calling it done? (At least two unrelated ones, and at least one that tests the boundary: an empty container, a type with no default constructor, a type whose copy throws.)
"I should make it a template in case I need it later."¶
Why this feels true. Generality sounds like good design, and the cost is invisible today: the code compiles, and the flexibility is free if nobody uses it.
Where it breaks. The cost is not free. The interface must be documented as requirements, the errors get harder to read, every user recompiles when the header changes, and — the part that actually bites — a template used with one type hides the requirement that mattered, because that one type satisfied it incidentally. The single-use template is the case where "make it general" turns into "make it harder to change".
The correct model. Generalise when the second use appears, and design the parameterisation at that moment, when you have two real cases to generalise from. Lab 12 generalises after three record kinds exist in Labs 9 to 11, not before.
The re-test. What is the evidence that a generalisation was warranted? (Two callers existing now, not one imagined later.)
Part 2 — Common programming mistakes¶
| Symptom | Cause | Fix |
|---|---|---|
undefined reference to 'Repository<int>::add(int const&)', or a linker that names nothing at all |
The template's definition is not visible where it is used | Put the definition in the header, or add explicit instantiations |
error: need 'typename' before 'T::value_type' because 'T' is a dependent scope |
A dependent type name used without typename |
Write typename T::value_type |
error: no match for 'operator<' (operand types are …) from deep inside an algorithm |
The element type has no ordering, or none that is a strict weak ordering | Add a coherent operator<, or pass a comparator |
error: no matching function for call to 'larger(int, double)' with deduced conflicting types |
One type parameter, two argument types | Make the types agree, or add a second parameter |
error: class template argument deduction failed / couldn't deduce template parameter 'T' |
A class template used without arguments where nothing can be deduced | Write the arguments: Stack<int> |
error: wrong number of template arguments (2, should be 1) |
An argument added that the template does not accept | Use a template that takes it, or add the parameter |
| A hundred lines of error, none of them in your own file | The instantiation failed inside library code | Read the last line, then reduce to a five-line case |
error: static assertion failed: … requires … |
A requirement was not met | Meet it, or choose a different type — the message names the requirement |
| A template used with many types builds very slowly | Many instantiations in many places | Accept it, or reduce instantiations; measure rather than guess |
Code that compiles for int and misbehaves for a second type |
The type satisfies the syntax of a requirement but not its meaning | Document the requirement precisely; test with an unrelated type |
Mistake 1 — the template that compiles and does not link [L3 Modify]¶
Reproduce it. A declaration in the header, a definition in the .cpp, a use in main:
// repository.h
template <typename T>
class Repository { public: void add(const T& record); };
// repository.cpp
template <typename T>
void Repository<T>::add(const T& record) { items_.push_back(record); } // never instantiated
What you see. No compile error. At link time, on this toolchain, the entire message is:
which names no symbol at all — so inspect the objects:
$ nm -C main.o | grep ' U ' | grep Repository
U Repository<int>::add(int const&)
U Repository<int>::size() const
$ nm -C repository.o | grep -c Repository
0
Why. The compiler needs the definition at the point of use, because that is where it generates
Repository<int>::add. repository.cpp contains a template nobody instantiated, so its object file
defines nothing. The caller's object asks for a symbol that no one ever produced.
Fix it. Either move the definition into the header, or say which instantiations to generate:
Prevent it. Remember the sentence: the definition must be visible where the type is used. If
your linker complains about a symbol you are certain you wrote, look for a template definition in a
source file — or run nm -C on the object files and see which one is empty.
Mistake 2 — a dependent name without typename [L2 Apply]¶
Reproduce it.
template <typename T>
T firstElement(const T& container) {
T::value_type first = *container.begin();
return first;
}
What you see.
error: need 'typename' before 'T::value_type' because 'T' is a dependent scope
error: dependent-name 'T::value_type' is parsed as a non-type, but instantiation yields a type
note: say 'typename T::value_type' if a type is meant
Why. T::value_type is a dependent name: until T is known, the compiler cannot tell
whether value_type names a type or a static value, so the grammar has to be disambiguated. The
second message is the mature version of the same complaint, and the note tells you the fix in
words.
Fix it.
Prevent it. Inside a template, any name of the form Something::name where Something depends
on a template parameter is a type only if you say typename. Two exceptions you will meet: a base
class in a class header (class Derived : public Base<T>) and a member initialiser list. Modern
compilers point at the right line, which was not always the case.
Mistake 3 — a requirement the compiler discovers for you [L3 Modify]¶
Reproduce it.
struct Reading { std::string sensor; double value = 0.0; };
template <typename T>
T smallest(const std::vector<T>& values) {
T best = values.front();
for (const T& value : values) { if (value < best) { best = value; } }
return best;
}
std::vector<Reading> readings{ {"a", 1.0}, {"b", 2.0} };
smallest(readings);
What you see.
preceded by the instantiation trail:
Why. smallest has a requirement — an ordering — that was never written down. Reading has no
operator<, so the requirement surfaces as an error about the operation rather than about the
requirement.
Fix it. Two valid answers, and choosing between them is the design decision:
// (a) give the type the operation, as Module 5 taught — with a coherent ordering
bool operator<(const Reading& left, const Reading& right) {
if (left.sensor != right.sensor) { return left.sensor < right.sensor; }
return left.value < right.value;
}
// (b) state the requirement where it can be checked
template <typename T>
T smallest(const std::vector<T>& values) {
static_assert(std::is_copy_constructible<T>::value, "smallest<T> copies its element type");
…
}
Prevent it. Write the requirement list in the header next to the template, and enforce what can be enforced. A requirement that exists only in the author's head is discovered by the second user, at the worst possible moment.
Mistake 4 — an argument the template cannot accept [L2 Apply]¶
Reproduce it.
What you see.
error: class template argument deduction failed:
note: couldn't deduce template parameter 'T'
error: wrong number of template arguments (2, should be 1)
note: provided for 'template<class T> class Stack'
Why. A class template is not a type; it becomes one when its arguments are known. Before C++17
the arguments were always written explicitly; C++17 can deduce them from a constructor's arguments,
and when there are none — a default constructor tells the compiler nothing about T — deduction has
nothing to work from.
Fix it.
Stack<int> typed; // explicit: always valid
Stack values{7}; // C++17 deduction from the constructor argument: T is int
Prevent it. Write the arguments unless the constructor makes them obvious. std::pair p{1, 'a'}
is a readability win; Stack untyped; is a bug the compiler happened to catch.
Mistake 5 — the template that works for one type and lies for another [L4 Analyze]¶
Reproduce it.
struct Score {
int points = 0;
};
bool operator<(const Score& left, const Score& right) { return left.points <= right.points; } // ← not strict
What you see. Nothing, ever, from the compiler. Sorted containers and algorithms silently
misbehave: std::sort may drop elements, std::map may report that a key exists when it does not,
and binary search may miss a value that is present. Undefined behaviour, with no diagnostic and a
plausible-looking result.
Why. <= is not a strict weak ordering: a < a must be false. Every standard algorithm that
orders or searches assumes a strict weak ordering, and none of them can check that you provided one —
checking it would cost the performance the requirement exists to enable.
Fix it.
Prevent it. Write orderings from the canonical shape: compare the first key, then the second, and
end with a strict comparison. Test !comp(a, a) for the element type you hand to a standard
container — that one assertion catches most violations — and remember Module 5's rule that <= is
never the body of operator<.
Next: Debugging — the three-line method for a diagnostic storm, and a diagnosis that began inside the standard library.