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:

  1. 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.
  2. 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.
  3. 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:

  1. The compiler left template instantiations undefined in initial object files.
  2. A database tracked which templates were needed and where they were defined.
  3. 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.