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.

The question

Why does a one-line mistake with a template produce four hundred lines of errors from inside a header I never opened?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

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.

What this phase may assume or do

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 T lived 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 typename and this-> 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

specThat instantiation produces a distinct specialisation per set of template arguments is required by the C++ standard, but the standard does not require a distinct *body* in the output: identical instantiations across translation units are folded by the linker, and an implementation may share code between instantiations that differ only in ways it can prove irrelevant. Java's erasure is likewise a specification-level choice, made for backwards compatibility with pre-generics bytecode, not an implementation detail.

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.

Three answers to "how do I write code that works for many types"typical
PropertyC++ templates (monomorphization)Java/TS generics (erasure)Rust generics (monomorphization + bounds)
Code generatedOne copy per distinct instantiationOne copy, sharedOne copy per distinct instantiation
When checkedAt instantiation, against the body (pre-concepts)At definition, against the declared boundsAt definition, against the trait bounds
Error locationDeep in the instantiated bodyAt the call siteAt the call site
Runtime costNone: fully specialised, inlinableIndirection, boxing of primitivesNone: fully specialised
Binary sizeGrows with instantiationsConstantGrows with instantiations
Can specialise per typeYes — explicit and partial specialisationNoYes, via traits and specialised impls

Checked at instantiation, which is why the error is in the wrong place

implementationThe quality of concept diagnostics varies considerably between GCC, Clang and MSVC and has improved substantially since C++20 shipped — early implementations often printed the unsatisfied atomic constraint without explaining which requirement failed. Standard-library implementations also differ in how thoroughly they constrain their own templates, so the improvement you see depends on both the compiler and the library.

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.

The same mistake, before and after concepts
1// Unconstrained: the requirement lives in the body
2template <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 here
14
15// Constrained: the requirement lives in the declaration
16template <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 typename or this-> 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 constexpr and 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-dump after semantic analysis shows every instantiation the translation unit forced, with the substituted argument types.
  • clang -ftime-trace produces 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-sort on an object file shows which instantiations are large; bloaty attributes binary size to symbols and demangles them.
  • templight and -ftemplate-depth for 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 .cpp file 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.

A template is compiled once and reused for every type.
That describes erasure. A C++ template is compiled once *per set of arguments used*, which is why the binary grows and the build slows.
Concepts are just nicer syntax for enable_if.
They are checked before instantiation, participate in overload ordering by subsumption, and can be named and reused. enable_if achieves partial overlap by exploiting SFINAE, and produces far worse diagnostics.
Template code has a runtime cost because of all the generated code.
Each instantiation is ordinary code with no dispatch overhead. The costs are build time and binary size; the runtime effect, if any, comes through instruction-cache pressure and must be measured.
If it compiles for one type, the template is correct.
Only the instantiated paths were checked. An unexercised branch of the body may be ill-formed for a type that has not been tried yet, which is what concepts and explicit instantiation tests exist to surface.

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.

specInstantiation semantics, two-phase lookup and SFINAE are specified by the C++ standard, so the behaviour is portable in principle. In practice MSVC accepted non-conforming template code for many years under its historical lookup model, so code written against it can fail on GCC and Clang — the divergence is a compatibility mode, not a standards disagreement, and /permissive- turns it off.
implementationHow much build time templates cost depends heavily on the compiler and the standard-library implementation: libstdc++, libc++ and the MSVC STL constrain and structure their templates differently, and -ftime-trace on the same source with two toolchains can attribute the cost to different headers entirely. Any build-time claim needs both named.
typicalThe statement that monomorphized code is faster than a virtual-call design is typical rather than universal: it holds when the specialised code inlines and the working set stays in cache, and can invert when many instantiations of a large function evict each other from the instruction cache. This is a measurement question per program, not a rule — see [[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

Computer Architectureinstruction-cache
Performancebenchmarking
Domains that do not exist yet
  • Software Design — Choosing between compile-time and runtime polymorphism in an API
    The 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.