Template Instantiation

One template becomes one concrete function per set of arguments used, in every translation unit that used it — and then the linker throws nearly all of those copies away. The bill arrives as compile time, object-file size and link work, in that order.

The question

Where does the compile time and the binary size actually go in a template-heavy project?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

While one translation unit is being compiled, the instantiations live in a table keyed by (template, argument list). Each entry is a fully substituted declaration and, if the specialisation was used in a way that demands a body, a fully substituted definition that then walks the ordinary pipeline — optimization, code generation — as if you had written it out by hand. The object file that comes out is therefore not "the code for this unit": it is a set of *candidate* definitions, each tagged with the mangled name that identifies it and packaged so the linker may keep one and discard the rest. The question the representation exists to answer is which concrete functions this unit needs, and which of them it is obliged to supply.

What this phase may assume or do

A specialisation must be implicitly instantiated exactly where it is odr-used, and not before: naming std::vector<Widget> requires the class, but only the member functions you actually call are instantiated, and an unused member is never even checked. Across units, C++ grants templates an explicit exception to the One Definition Rule — the same specialisation may be defined in many translation units, and the linker may keep any one, *provided* every definition consists of the same token sequence and every name in it means the same entity in every unit. Compile two units with different -D flags, or with a struct that has an extra field in one of them, and the exception does not apply: the program is ill-formed with no diagnostic required, the linker still silently keeps one definition, and the other unit calls it with the wrong layout.

Key points

  • A specialisation is instantiated once per translation unit that uses it; nothing coordinates the units, so the compile-time cost multiplies by the include fan-out.
  • The linker recovers the space with link-once symbols, but never the time — the work was already done in every unit.
  • Member functions of a class template are instantiated only when used, which saves code and means unused members were never type-checked.
  • Section-group folding is required and safe; identical code folding is optional, opt-in, and makes distinct functions share an address.
  • The ODR exception for templates holds only if every definition is token-identical and every name means the same thing — differing macros or layouts break it with no diagnostic.
  • extern template converts a specialisation back into an ordinary separately compiled function, trading inlining for build time.

The demand is per translation unit, and nothing coordinates it

targetThe W class and the section-group mechanism behind it are ELF, as produced by GCC and Clang on Linux. Mach-O spells the same idea weak external with weak_def_can_be_hidden, and PE/COFF uses COMDAT sections with an explicit selection kind chosen by the compiler. The guarantee is equivalent on all three; the tooling output and the failure messages are not, so a script that greps for W works on exactly one platform.

The compiler has no idea what any other translation unit is doing. When unit A calls twice(21) it instantiates int twice(int) and emits a definition; when unit B does the same it instantiates and emits its own. Neither can skip the work on the grounds that someone else did it, because compiling units independently is the entire point of the model — see [[cpp-pipeline]].

So the same specialisation is parsed, substituted, checked, optimized and encoded once per unit that needs it. In a project where a header-only library is included by four hundred files, the compiler produces four hundred copies of every specialisation those files touch. This is the term that dominates template-heavy build times, and it is not visible in any one file: no single compilation looks slow, the total is enormous, and the profile is flat.

What saves the binary is not the compiler but the linker. Each definition is emitted with *vague linkage* — a weak, link-once symbol in its own section group — so the linker can discard all but one. The compile-time work is still paid four hundred times; only the space is recovered.

Two units, the same specialisations, weak symbols in both
1// twice.h
2template <typename T> T twice(T x) { return x + x; }
3
4// a.cpp: int f() { return twice(21); }
5// b.cpp: long g() { return twice(21L); }
6
7$ g++ -c a.cpp b.cpp
8$ nm -C a.o | grep twice
90000000000000000 W int twice<int>(int)
10$ nm -C b.o | grep twice
110000000000000000 W long twice<long>(long)
12
13$ nm a.o | grep twice
140000000000000000 W _Z5twiceIiET_S0_

W is a weak symbol: this unit offers a definition and consents to another one winning. That single letter is the whole cross-unit contract for templates — and _Z5twiceIiET_S0_ is why the linker can tell twice<int> from twice<long> without understanding a word of C++.

Only what is used, and that rule cuts both ways

Implicit instantiation is lazy at member granularity. Instantiating std::vector<Widget> instantiates the class — enough to know its size, layout and member declarations — but a member function body is instantiated only when that member is odr-used. A container used only for push_back and iteration never generates code for the two dozen members you did not call.

The saving is real and it is also a trap, because an uninstantiated member is an unchecked member. A class template can contain a member whose body is ill-formed for Widget and compile perfectly for years, until someone calls it. That is the same instantiation-time checking described in [[templates]], seen from the build side: the set of things the compiler has verified is exactly the set of things somebody happened to use.

Explicit instantiation (template class Foo<int>;) forces every member, which is how library authors both pin down the code generation and get the whole class checked in one place. It is also the standard way to test a template properly: instantiate it explicitly for the types you claim to support, in a translation unit whose only job is to fail loudly.

The linker deduplicates, and one of its tools is dangerous

implementationICF behaviour is a linker property, not a compiler one, and the linkers disagree: gold and lld both offer --icf=all and --icf=safe, mold implements folding with its own heuristics, and MSVC's /OPT:ICF is on by default in release builds — which means a Windows release build may already be folding functions that a Linux release build of the same source keeps distinct. The saving is also entirely program-dependent; treat any percentage as a measurement of one binary.

Two separate mechanisms are at work at link time and they are constantly confused. The first is section-group deduplication: definitions that share a mangled name are grouped, and the linker keeps one group and discards the identical ones. This is required for the model to work at all, it is on by default everywhere, and it is safe by construction because the discarded definitions were required to be identical in the first place.

The second is identical code folding — ICF. Here the linker compares the *machine code* of functions with different names and merges those whose bodies happen to be byte-identical after relocation. Templates generate a great deal of this: twice<int> and twice<long> may compile to the same instructions on a 64-bit target, and a std::vector<Foo*> and a std::vector<Bar*> share almost everything.

ICF saves real space and it changes an observable property: two functions that were distinct now have the same address. Code that compares function pointers for identity, or that uses a function address as a map key or a type tag, silently starts finding two things equal that the language says are different. This is why ICF is opt-in and why linkers offer a "safe" mode that folds only functions whose address is never taken.

Two deduplication mechanisms that are not the same thingtypical
PropertySection-group / COMDAT foldingIdentical code folding (ICF)
What it comparesThe mangled symbol nameThe bytes of the function body, after relocations
Why it is allowedThe language required the definitions to be identicalNothing in the language allows it; it is a deliberate deviation
DefaultimplementationOn, always — the model does not work without itOff; --icf=all or --icf=safe in lld and gold, /OPT:ICF in MSVC
Observable effectNoneDistinct functions can compare equal by address
Typical savingtypicalEverything above one copy per specialisationSingle-digit to low-double-digit percent of .text in template-heavy code
How you checkreadelf -g lists the section groups in an object file-Wl,--print-icf-sections lists what was folded

Paying it down: extern template

If a specialisation is used in hundreds of units and its definition is large, the compile-time cost of instantiating it hundreds of times is pure waste — the linker was always going to keep one. extern template says so explicitly: it suppresses implicit instantiation in every unit that sees the declaration, and one designated unit supplies the definition with an explicit instantiation.

The effect is to convert a template back into an ordinary separately compiled function for the argument lists you name. That buys build time and costs the things separate compilation always costs: the definition is no longer visible at the call site, so the optimizer cannot inline it or specialise it against the caller, and adding a new argument list means editing the designated unit rather than just using it.

This is a targeted tool, not a policy. Applied to a small function it makes the program slower for no build-time gain, because a one-line body that used to inline is now a call. Measure first, with -ftime-trace, and apply it to the handful of large specialisations that actually dominate.

Moving one specialisation out of every translation unit
Before
// bigthing.h — included by 400 files
template <typename T> class BigThing { /* large members */ };
// every one of the 400 units instantiates BigThing<int> and emits it
After
// bigthing.h
template <typename T> class BigThing { /* large members */ };
extern template class BigThing<int>;   // do not instantiate here

// bigthing.cpp — exactly one unit
template class BigThing<int>;          // instantiate and emit here
Legal only when

Only if exactly one translation unit in the final link provides the explicit instantiation definition for that exact argument list, that unit is actually part of the link, and every unit sees the extern template declaration before any use that would otherwise instantiate. The specialisation must not be one the language forbids suppressing — an inline function whose address is required, or a member the standard says is implicitly instantiated regardless.

Illegal when

If the designated unit is in a static library that the linker discards for having no referenced symbols, or if a unit uses BigThing<short> — an argument list nobody explicitly instantiated — and the extern template declaration for int gave a false sense that all of them were covered. The first produces "undefined reference" at link time; the second quietly instantiates as normal, so the build-time gain you measured evaporates as the argument set grows.

How it works

The steps, in the order the compiler takes them.

  • A use of a template with concrete arguments triggers deduction, constraint checking, and a lookup in the unit's table of already-produced specialisations.
  • On a miss, the compiler substitutes the arguments through the declaration; if the use requires a definition, it substitutes through the body as well and adds the result to the table.
  • The substituted definition is type-checked, lowered, optimized and encoded exactly like a hand-written function, and emitted into its own section group under the mangled name of the specialisation.
  • Members of a class template are handled the same way individually, so only the ones odr-used produce code.
  • At link time the linker keeps one group per mangled name and discards the rest, applying relocations to point every reference at the survivor.
  • If identical code folding is enabled, it then compares the remaining function bodies byte for byte and merges matches across different names, updating relocations again.
  • An extern template declaration suppresses step two in every unit that sees it, leaving one explicit instantiation as the sole supplier.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A full build takes twenty minutes and no individual file takes more than four seconds; -ftime-trace shows the time is instantiation, spread evenly across everything.
  • The binary grows by megabytes after a refactor that introduced a handful of new template arguments, and bloaty attributes it to symbols nobody wrote.
  • A member function of a widely used class template turns out to be ill-formed for one of the types it is documented to support, discovered years later by the first person to call it.
  • A function-pointer comparison that always worked starts returning true for two different functions after a release build enables identical code folding — usually on one platform only.
  • Two libraries are built with different -D flags, both define the same specialisation, the linker keeps one, and a struct is read with the wrong layout at run time with no diagnostic from any stage.
  • extern template is added, build time improves, and a hot small function that used to inline becomes a call — a measurable slowdown with nothing in the source to point at.

When it helps

  • Diagnosing build time in a C++ project, where the answer is almost always instantiation count rather than lines of code.
  • Understanding why a header-only library is convenient to consume and expensive to consume at scale.
  • Deciding what belongs in a header versus behind an explicit instantiation, which is the physical-design decision that sets build time for every consumer.
  • Reading a linker map or a size report, where most large symbols in template-heavy code are instantiations rather than functions anyone typed.

When it hurts

  • Applying extern template broadly as a policy: it removes inlining opportunities across the board to fix a cost that a few specialisations were responsible for.
  • Reasoning about binary size from source size. The relationship runs through the number of distinct argument lists, which is not visible in any file.
  • Assuming deduplication makes duplicate instantiation free. It makes it free in space; the compile time is spent regardless, and it is the term that hurts.

What it costs

Every one of these is paid by something.

  • Instantiating per translation unit buys independent, parallel compilation with no coordination between units, and pays with compile time proportional to uses times include fan-out — the dominant term in template-heavy builds.
  • Link-once vague linkage buys a binary that contains one copy of each specialisation, and pays with an unchecked global obligation: the definitions must be identical, and nothing verifies it.
  • Lazy member instantiation buys smaller output and faster builds, and pays with members that are never type-checked until somebody calls them.
  • Identical code folding buys real space in template-heavy binaries and pays by breaking function-pointer identity, which is why it is off by default on most toolchains.
  • extern template buys build time and pays with the inlining and cross-function optimization that visibility of the definition made possible.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Type erasure behind a virtual interface or a void* implementation core compiles once, keeps size constant and permits a stable ABI, paying an indirect call — see [[type-erasure]] and [[abi-stability]].
  • The "thin template" idiom does both: a non-template implementation carries the bulk of the code and a tiny inline template adds type safety on top, so only a few instructions are duplicated per argument list.
  • Rust monomorphizes the same way but generates code in the crate that uses the generic, so deduplication is a compiler-internal concern instead of the linker's — see [[rust-pipeline]].
  • Go's generics avoid full duplication entirely by sharing one instantiation per memory shape and passing a dictionary — smaller output, some indirection. See [[go-pipeline]].
  • C++20 modules remove the reparse-per-unit half of the cost by compiling an interface once, though the instantiation itself still happens where it is used — see [[modules]].

See it for yourself

The flag, dump or tool that shows you this directly.

  • clang -ftime-trace writes a Chrome-tracing JSON per translation unit with a span per instantiation; ClangBuildAnalyzer aggregates them across a whole build and names the worst templates.
  • nm -C --size-sort object.o shows which instantiations are large; the W (or weak external) class marks the link-once ones.
  • readelf -g object.o lists the section groups — one per link-once definition — which is the mechanism itself rather than its effect.
  • bloaty --domain=vm -d symbols binary attributes binary size to demangled symbols and makes instantiation bloat obvious in one screen.
  • ld.lld -Wl,--print-icf-sections reports exactly which functions were folded, and --icf=safe restricts folding to functions whose address is never taken.
  • -Wl,-Map=out.map produces a linker map showing which object file each surviving definition came from.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "The linker deduplicates the instantiations, so duplicate instantiation is free." It is free in the binary. It is paid in full, once per translation unit, in the compiler — and that is the cost people actually feel.
  • "Instantiating std::vector<Widget> generates all of std::vector." It generates the class and only the members you use. The rest is neither emitted nor checked.
  • "Two identical functions folded to one address, so the linker has a bug." That is identical code folding doing what it was asked to do. It is off by default on most toolchains precisely because it is observable.
  • "extern template makes the program faster." It makes the *build* faster and can make the program slower, by removing the definition the inliner needed.
  • "If both libraries built successfully, their instantiations must agree." Nothing checked. The ODR exception for templates is an obligation on you, and a violation links cleanly.

Misconceptions

The claim, and what is actually true.

A template is compiled once and the linker copies it where it is needed.
It is compiled once per translation unit that uses it, and the linker deletes all but one copy. The direction is deletion, not copying.
Binary size grows because templates generate verbose code.
Each instantiation is ordinary, well-optimized code. Size grows because there are many of them — one per distinct argument list, times the members used.
Explicit instantiation is a micro-optimization for experts.
It is the standard way to test a class template completely, because it forces every member to be instantiated and therefore checked.
The One Definition Rule does not apply to templates.
A specific exception permits multiple *identical* definitions. Definitions that differ — different macros, different layouts — violate the ODR and link without complaint.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

Using a template with a type makes the compiler write out a real function for that type. It does this separately in every file that uses it, because files are compiled independently and cannot know about each other. At the end, the linker notices the copies are identical and keeps one. The binary is fine; the build time is not, because the work happened in every file.

practical

When a C++ build is slow, run clang -ftime-trace and aggregate it — the answer is usually a handful of templates instantiated everywhere. The fixes, in order of how often they work: reduce the include fan-out so fewer units see the template at all; move large template bodies out of widely included headers; use extern template for a small number of large, heavily used specialisations. When a binary is unexpectedly large, run bloaty -d symbols and look for instantiations of things you did not write.

advanced

The interesting structural point is that C++ pushed a compiler problem into the object format. Because instantiation must happen where the template is used, and because units cannot coordinate, the language needed a linkage kind meaning "here is a definition; another unit may offer the same one; keep any". That requirement shaped ELF section groups and COMDAT alike, and it is why inline functions, virtual tables, static data members of class templates and default argument thunks all travel the same way. Rust reaches the same destination differently: monomorphization happens inside the compiler for the whole crate graph, so deduplication is a compiler-internal decision about codegen units rather than an obligation on the linker — which is why a Rust build has no ODR to violate, and why its equivalent cost shows up as LLVM time on a mountain of monomorphized functions instead.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

specThe ODR exception permitting multiple identical definitions of a template specialisation, and the rule that implicit instantiation of a member happens only on odr-use, are both required by the C++ standard. What is not specified is the mechanism: nothing in the standard mentions COMDAT, section groups or weak symbols, and a conforming implementation could deduplicate some other way — which is exactly why the object-file details below differ per platform.
targetLink-once linkage is spelled differently in every object format: ELF section groups with W symbols under GCC and Clang, weak external definitions in Mach-O, COMDAT sections with a selection kind in PE/COFF. The behaviour is equivalent; the tool output, the flag names and the diagnostics are not, so instructions written for one platform mislead on the others.
implementationIdentical code folding is a linker feature with per-linker defaults: gold and lld expose --icf=all and --icf=safe and default to off, mold folds with its own heuristics, and MSVC enables /OPT:ICF in release builds by default. The same source can therefore fold on Windows and not on Linux, which makes any function-pointer-identity bug platform-specific.
typicalThe claim that instantiation dominates template-heavy build time is what -ftime-trace reports on mainstream projects using libstdc++ or libc++, not a law. A project whose headers are thin and whose bodies are large can be dominated by optimization instead, and a build with heavy constexpr metaprogramming can be dominated by constant evaluation — measure before acting.

If you were asked this in an interview

  • A C++ build takes twenty minutes and no single file is slow. Where would you look, and with what tool?
  • Why can the same template specialisation be defined in fifty object files without a "multiple definition" error?
  • What does identical code folding change about a program's observable behaviour, and why is it off by default?

Connections

Computer Architectureinstruction-cache
Performancebenchmarking
OS & Networkingprogram-vs-process
Domains that do not exist yet
  • DevOps / Production Engineering — Build caching, distributed compilation and remote execution
    Duplicate instantiation across units is exactly the work a compile cache or a distributed build farm can avoid re-doing, so the cost model here is the reason those tools pay for themselves on C++ codebases and rarely on others. The cache infrastructure itself belongs there.