Templates
C++ templates are compile-time generic programming by code generation: one template, one concrete function or class per type used. Checking happens at instantiation, which is why an error in your call site is reported inside the library.
Why does a one-line mistake with a template produce four hundred lines of errors from inside a header I never opened?
A template is not code. It is a pattern in the compiler's AST, parameterised by types and values, from which code is generated on demand. Until a specialisation is requested there is no function and no class — only a tree with unresolved dependent names, kept alongside a table of which specialisations have already been produced. The question this representation exists to answer is "what would this code be for type T", and it can only be answered once a T is supplied.
A template definition may be parsed even though most of its meaning is undecidable: names that depend on a template parameter are deferred, and a compiler may only diagnose errors in the non-dependent parts before instantiation. At instantiation, substitution of the arguments must produce a well-formed declaration; if it does not, and the failure is in the immediate context of the declaration, the specialisation is removed from the overload set rather than being an error — the SFINAE rule. A failure in the body, by contrast, is a hard error, which is exactly the distinction that decides whether a mistake is a graceful non-match or a wall of text.
Key points
- A template is a code generator: one concrete specialisation is produced per distinct set of arguments used.
- That is monomorphization, the opposite choice from type erasure, and it trades compile time and code size for zero-indirection specialised code.
- Before concepts, requirements on
Tlived only in the body, so violations were reported deep inside the instantiated code rather than at the call site. - Concepts move the requirement into the declaration, which is what allows the error to be reported against the argument at the call.
- Two-phase lookup resolves non-dependent names at definition and dependent names at instantiation, which is where
typenameandthis->come from. - The costs are compile time, object size, link work and diagnostic quality — never runtime speed, which is what the technique buys.
A template is a code generator with a type-level interface
The mental model that survives contact with real code is this: template <typename T> T add(T a, T b) does not declare a function. It declares a rule for producing functions. Write add(1, 2) and the compiler produces int add(int, int); write add(1.0, 2.0) and it produces a second, entirely separate function. This is [[monomorphization]], and it is one of the two answers a language can give to generic programming.
The other answer is [[type-erasure]]: compile the generic code once, represent every value uniformly, and dispatch at runtime. Java's generics and Go's interfaces do this; so, in a different way, does TypeScript, which erases the parameters entirely. Neither answer is better. Monomorphization produces specialised code with no indirection and no boxing, and pays in compile time and binary size. Erasure produces one copy and pays with indirection, boxing, and the inability to specialise on the type at all.
C++ chose monomorphization and then leaned on it much harder than a generics feature would require, because a template can do things a generic cannot: specialise on values as well as types, select different implementations per type, compute at compile time, and inspect the type it was given. That power is why templates became a metaprogramming facility rather than only a container mechanism — and why their error messages are what they are.
| Property | C++ templates (monomorphization) | Java/TS generics (erasure) | Rust generics (monomorphization + bounds) |
|---|---|---|---|
| Code generated | One copy per distinct instantiation | One copy, shared | One copy per distinct instantiation |
| When checked | At instantiation, against the body (pre-concepts) | At definition, against the declared bounds | At definition, against the trait bounds |
| Error location | Deep in the instantiated body | At the call site | At the call site |
| Runtime cost | None: fully specialised, inlinable | Indirection, boxing of primitives | None: fully specialised |
| Binary size | Grows with instantiations | Constant | Grows with instantiations |
| Can specialise per type | Yes — explicit and partial specialisation | No | Yes, via traits and specialised impls |
Checked at instantiation, which is why the error is in the wrong place
Before C++20 concepts, a template body was checked only when instantiated. A template said nothing about what it required of T; the requirements were implicit in what the body happened to do. Pass a type that lacks one of them and the error is reported at the line inside the template that used it — frequently several levels deep in a standard-library header — with the instantiation stack printed as a backtrace.
This is not a quality-of-implementation failing. The compiler genuinely has no earlier point at which it could know: nothing in the declaration template <typename T> void sort(T first, T last) states that T must be a random-access iterator whose value type is less-than comparable. Those requirements exist only in the body, so the body is where the violation is found.
Concepts, added in C++20, move the requirement into the declaration: template <std::random_access_iterator It> void sort(It, It). Now the constraint is checked at the call site, against the argument, before instantiation is attempted, and the diagnostic can say "std::list<int>::iterator does not satisfy random_access_iterator because it lacks operator+". Rust and Haskell had this property from the start — the bound is part of the signature, so the error is always at the call — which is the single largest ergonomic difference between the three systems.
1// Unconstrained: the requirement lives in the body2template <typename T>3T largest(const std::vector<T>& v) {4 T best = v[0];5 for (const T& x : v) if (best < x) best = x; // requires operator<6 return best;7}8 9struct Point { int x, y; };10largest(points);11// error: no match for 'operator<' (operand types are 'const Point' and 'const Point')12// ... in instantiation of 'T largest(const std::vector<T>&) [with T = Point]'13// ... required from here14 15// Constrained: the requirement lives in the declaration16template <std::totally_ordered T>17T largest(const std::vector<T>& v);18// error: constraints not satisfied: 'Point' does not model 'std::totally_ordered'Both compilers found the same problem. The second one found it before instantiating anything, so it can name the type, the concept and the missing operation instead of quoting a line from inside a header.
Two-phase lookup, and why templates surprise people who write them
A template definition is parsed when it is written, not when it is used, which means the compiler must decide at parse time which names it can resolve. Names that do not depend on a template parameter are looked up immediately, in the context of the definition. Names that do depend on a parameter are deferred to instantiation, when the argument is known.
This split — two-phase lookup — is why typename and template appear as disambiguating keywords in template code. Writing T::value_type x; requires typename T::value_type x; because at parse time the compiler cannot know whether T::value_type is a type or a static member, and it must decide in order to parse. It is also why a base-class member must be qualified as this->member in a derived class template: unqualified lookup at phase one does not search a dependent base.
The practical consequence is that a template can contain an error in a dependent branch that only fires for one particular argument type, and the same template can compile fine in one compiler and fail in another if one of them implements two-phase lookup less strictly than the other. That last difference is a genuine portability trap between MSVC's historical behaviour and the conforming one.
The costs, stated plainly
Every distinct instantiation is code the compiler generates, optimizes, and emits. A container instantiated for forty types produces forty copies of every member function that is used, in every translation unit that uses it, and the linker deduplicates them afterwards. That is compile time, memory during compilation, object-file size and link time, and it is the dominant term in the build time of template-heavy C++.
It also produces code the debugger and the profiler must un-name for you, since every symbol is a mangled instantiation, and it makes error messages proportional to the instantiation depth rather than to the mistake. Heavy template metaprogramming can turn a syntactically small mistake into a diagnostic that no human reads, which is a real engineering cost even though it produces no runtime cost at all.
The compensation is that the generated code is as good as hand-written code for that type: no indirection, no boxing, everything inlinable, and the optimizer sees a concrete function with concrete types. That is why the technique survives its ergonomics. [[template-instantiation]] takes the cost side apart in detail.
How it works
The steps, in the order the compiler takes them.
- A template definition is parsed into an AST with dependent names left unresolved, and non-dependent names bound immediately in the definition context.
- A use of the template with concrete arguments triggers argument deduction, then constraint checking if the template is constrained.
- If a candidate's declaration is ill-formed after substitution, and the failure is in the immediate context, the candidate is silently removed from the overload set (SFINAE) rather than producing an error.
- For the selected specialisation, the compiler substitutes the arguments through the body, resolving the deferred names, and produces a concrete declaration and definition.
- That specialisation is added to a table so a later identical use reuses it within the translation unit, and it is emitted with vague (COMDAT) linkage so the linker can fold copies from other units.
- The specialisation then proceeds through the ordinary pipeline — optimization, code generation — as if it had been written by hand.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A missing
operator<on a user type produces hundreds of lines of diagnostics quoting standard-library internals, with the actual cause on the last line of the instantiation stack. - Build time grows superlinearly as a header-only template library spreads through a project, and no single file is slow enough to look guilty.
- A template compiles under MSVC and fails under Clang because a dependent name was not marked with
typenameorthis->and one compiler resolved it too eagerly. - A subtle behaviour change appears when a new overload becomes viable for one instantiation, silently selecting a different specialisation with no diagnostic.
- Binary size doubles after a refactor that instantiated a widely used template for a handful of extra types, and the profiler shows instruction-cache pressure rather than any single slow function.
- A template that compiled for years fails the first time it is instantiated with a type that exercises a previously unused branch of the body, because that branch was never checked before.
When it helps
- Containers and algorithms that must work for arbitrary types with no runtime cost — the original motivation, and still the strongest case.
- Zero-overhead abstraction where indirection is unacceptable: numeric code, embedded systems, anything where an indirect call defeats the point.
- Compile-time selection of an implementation per type, which erasure-based generics cannot express at all.
- Compile-time computation via
constexprand templates, moving work out of the running program — see[[compile-time-evaluation]].
When it hurts
- Large, widely instantiated templates in headers, where each instantiation multiplies build time and object size across the whole project.
- Interfaces meant to be stable across a binary boundary: a template cannot be exported the way a virtual interface can, because the code does not exist until a consumer instantiates it — see
[[abi-stability]]. - Deep metaprogramming, where the diagnostic cost and the compile-time cost outgrow the abstraction being bought, and a runtime-polymorphic design would be both faster to build and easier to read.
What it costs
Every one of these is paid by something.
- Monomorphization buys fully specialised, inlinable, indirection-free code, and pays with compile time, memory during compilation, object-file size and link-time deduplication work proportional to the number of instantiations.
- Instantiation-time checking buys extreme expressiveness — the template can use anything the argument type happens to support without declaring it — and pays with error messages that name the library instead of the mistake.
- Concepts buy back the diagnostic quality and pay with the effort of writing and maintaining the constraint, plus a real learning cost: the constraint is now part of the interface, and getting it wrong rejects valid code.
- Putting templates in headers buys the availability the model requires (a definition must be visible where it is instantiated) and pays by exporting all that parse and instantiation work to every consumer, forever.
What else you could do
What a different compiler or language does instead, and when that is better.
- Runtime polymorphism through a virtual interface compiles once, keeps binary size constant, and permits a stable ABI — paying an indirect call per operation and preventing inlining. See
[[type-erasure]]. - Java-style erasure generics check at the declaration and share one implementation, which gives call-site errors and constant code size, at the cost of boxing and of being unable to specialise per type.
- Rust generics monomorphize like C++ but check against declared trait bounds at definition time, so the error is at the call site *and* the code is specialised — the combination C++ reached only with concepts.
- Code generation from an external tool produces ordinary functions the compiler checks normally, trading build machinery for readable code and comprehensible errors.
- A
void*-based container with hand-written casts is the C answer: one copy, no type safety, and the cast errors are yours to make.
See it for yourself
The flag, dump or tool that shows you this directly.
clang -Xclang -ast-dumpafter semantic analysis shows every instantiation the translation unit forced, with the substituted argument types.clang -ftime-traceproduces a Chrome-tracing JSON attributing compile time per template instantiation — the single best tool for finding what is actually slow in a template-heavy build.-ftemplate-backtrace-limit=0(GCC and Clang) prints the full instantiation stack instead of eliding it;-fconcepts-diagnostics-depth=controls concept explanation depth in GCC.nm -C --size-sorton an object file shows which instantiations are large;bloatyattributes binary size to symbols and demangles them.templightand-ftemplate-depthfor pathological recursion; Compiler Explorer to see the generated code for one instantiation in isolation.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Templates are C++'s generics." They are a code generation mechanism that can express generics, and also compile-time computation, per-type specialisation and value parameters. Generics in Java or TypeScript can express none of the last three.
- "The compiler generates code for every possible type." It generates code for every type actually used. An unused member function of a class template is not even instantiated.
- "Template errors are bad because compilers are bad at diagnostics." They were bad because the requirement was not written down anywhere the compiler could check before instantiating. Concepts fixed the cause, not the presentation.
- "Templates make the program slower because there is more code." They make the *build* slower and the binary larger. The generated code is specialised and typically faster than an indirect-call design — at some point instruction-cache pressure can invert that, which is a measurement question.
- "I can put a template in a
.cppfile like any other function." Only if you explicitly instantiate it there for every type used. Otherwise the definition must be visible at the point of instantiation, which is why templates live in headers.
Misconceptions
The claim, and what is actually true.
enable_if.enable_if achieves partial overlap by exploiting SFINAE, and produces far worse diagnostics.Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
A C++ template is a recipe, not a function. When you use it with a particular type, the compiler writes out a real function for that type and compiles it. Use it with five types and you get five functions. That is why templated code runs fast — each version is specialised — and why it builds slowly and produces long error messages.
practical
Constrain your templates with concepts so mistakes are reported at the call site. Keep large template bodies out of widely included headers, and use explicit instantiation in a .cpp when a template is only ever used with a known set of types. When a build is slow, run clang -ftime-trace and look at the instantiation list rather than guessing. When an error message is enormous, read it from the bottom: the last frame of the instantiation stack is your code.
advanced
The design tension is between expressiveness and checkability, and C++ sat at one extreme for thirty years. Unconstrained templates are structurally typed at instantiation: the requirement is whatever the body happens to use, so a template automatically works with any type that supports the right operations, including types written afterwards by people who never heard of it. That is enormously powerful and completely uncheckable in advance. Concepts move C++ toward the Rust and Haskell position — a declared bound, checked at the call — while keeping the monomorphized code generation. The residual difference is that a C++ concept is a *structural* predicate over a type, while a Rust trait bound is *nominal*: the type must have declared an impl. That distinction decides whether a library can retroactively work with a type it never anticipated, and it is the same trade as [[structural-vs-nominal]].
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
/permissive- turns it off.-ftime-trace on the same source with two toolchains can attribute the cost to different headers entirely. Any build-time claim needs both named.[[benchmarking]].If you were asked this in an interview
- What does the compiler actually do when it sees
std::vector<MyType>for the first time? - Why were pre-C++20 template error messages so bad, and what exactly did concepts change?
- Compare monomorphization and erasure on binary size, runtime cost and where errors are reported.
Connections
- Software Design — Choosing between compile-time and runtime polymorphism in an APIThe decision to template an interface or to make it virtual is a design decision with consequences this lesson enumerates — binary size, ABI stability, error quality — but the design criteria themselves, including who is allowed to extend the interface later, belong to that domain.