C++ Template Instantiation Caching Across Files
In C++, template instantiation across separate source files relies primarily on the "vague linkage" model resolved at link time rather than a shared runtime cache during compilation. Because C++ treats each translation unit independently, identical template instantiations are generated multiple times across distinct object files. This article explains how modern compilers and linkers handle these redundant instantiations using COMDAT sections, the historical repository approach, and modern language features like explicit template declarations and C++20 modules to optimize binary size and build performance.
The Separate Compilation Challenge
C++ compiles source files through independent translation units. When
a header file containing a template definition is included in multiple
.cpp files, the compiler cannot automatically verify
whether another file has already generated the assembly code for a
specific specialization (such as std::vector<int>).
Without a coordination mechanism, the compiler instantiates the same
specialization in every object file where it is referenced, leading to
severe code bloat and potential violations of the One Definition Rule
(ODR).
The Dominant Solution: Duplicate Emission and COMDAT Folding
Modern C++ toolchains (including GCC, Clang, and MSVC) solve this issue using a strategy commonly referred to as the Borland model or duplicate emission with link-time deduplication:
- Weak Symbols and COMDAT Sections: When compiling a template specialization, the compiler places the resulting machine code and data into a specialized container within the object file called a COMDAT (Common Data) section, marking the symbol with weak linkage.
- Linker Deduplication: During the link step, the linker inspects all incoming object files. When it identifies multiple COMDAT sections bearing the exact same symbol name and signature, it retains one arbitrarily chosen copy and discards the rest.
- Relocation Patching: The linker updates all call sites across all translation units to jump to the single retained definition.
This approach acts as a post-compilation deduplication cache. While it successfully eliminates duplicate machine code in the final executable, it does not prevent the compiler from redundantly parsing and generating the machine code in each translation unit during the compilation phase.
Historical Alternative: The Instantiation Repository
Earlier implementations, such as the AT&T cfront
compiler and early versions of GCC (via the -frepo flag),
used a centralized database to implement actual compile-time caching.
Under this model:
- The compiler left template instantiations undefined in initial object files.
- A database tracked which templates were needed and where they were defined.
- At link time, a specialized pre-linker checked the database, identified missing instantiations, triggered recompilation of only the necessary templates into a centralized repository, and finalized the link.
Although this approach eliminated redundant compilation, it
introduced severe coordination bottlenecks, broke parallel build tools
like make and ninja, and was eventually
abandoned by mainstream compilers in favor of COMDAT folding.
Programmer-Controlled Caching: Extern Templates
To eliminate redundant compilation time without relying on linker
deduplication, C++11 introduced explicit template instantiation
declarations using the extern template syntax.
By declaring
extern template class std::vector<int>; in a shared
header, developers instruct the compiler not to instantiate that
specialization in translation units that include the header. A single
translation unit then explicitly defines the specialization using
template class std::vector<int>;. This manually
caches the template to one object file, reducing both compile times and
linker overhead.
C++20 Modules and Compiler Caches
The introduction of C++20 Modules fundamentally changes template sharing. When a template is exported from a module interface unit, the compiler generates a serialized binary representation of the Abstract Syntax Tree (AST) called a Built Module Interface (BMI). Dependent translation units import this pre-analyzed metadata rather than reparsing raw text headers.
Complementary to this, external build tools such as
ccache and sccache operate at the process
level, hashing compiler inputs to retrieve previously compiled object
files—along with their template COMDATs—from an on-disk cache to prevent
re-instantiation across subsequent builds.