Question

C++ Template Definitions in .cpp Files: Explicit Instantiation Explained

cpptemplates

Question

In C++, I have template code that I would prefer to place in a .cpp file instead of defining inline in the header. I understand that this can work if I know in advance which template types will be used.

For example, suppose I have:

// foo.h
#include <string>

class foo
{
public:
    template <typename T>
    void doSomething(const T& t);
};
// foo.cpp
#include "foo.h"

template <typename T>
void foo::doSomething(const T& t)
{
    // Do something with t
}

template void foo::doSomething<int>(const int&);
template void foo::doSomething<std::string>(const std::string&);

The last two lines explicitly instantiate the template for int and std::string, so the program links as long as those are the only types used.

Is this considered a bad workaround, or is it a standard and portable technique that works across other compilers and linkers? I am currently using Visual Studio 2008, but I want the code to be portable to other environments later.

Short Answer

By the end of this page, you will understand why C++ templates are usually defined in headers, when it is valid to move template definitions into a .cpp file, and how explicit instantiation makes that possible. You will also see why this is a standard C++ technique rather than a compiler-specific hack, along with its trade-offs and common pitfalls.

Concept

In C++, templates are not normal functions or classes. A template is more like a blueprint that the compiler uses to generate actual code when a specific type is needed.

For example, if you write:

template <typename T>
void f(T value) { }

then f<int> and f<std::string> are only generated when the compiler sees they are needed.

Why templates are usually put in headers

The compiler must usually see the full template definition at the point where it tries to instantiate it. That is why template code is commonly placed in header files.

If only the declaration is visible:

template <typename T>
void f(T value);

and the definition is hidden away in a .cpp file, another translation unit may try to use f<double> without having enough information to generate it.

The exception: explicit instantiation

If you already know exactly which template types will be used, you can place the definition in a .cpp file and explicitly tell the compiler to generate those versions there:

template void foo::doSomething<int>(const int&);
template void foo::doSomething<std::string>(const std::string&);

This is called explicit instantiation.

It is a standard C++ feature, not a hack. It is commonly used when:

  • you want to reduce compile times
  • you want to hide implementation details
  • you only support a fixed set of template arguments
  • you are building a library with controlled template usage

Why this matters

Understanding this helps you design APIs correctly:

  • If users should be able to use any type, keep the template definition in the header.
  • If only a known set of types is supported, explicit instantiation in a .cpp file is a clean option.

So the real issue is not portability, but whether your design allows unknown template arguments or only a fixed list.

Mental Model

Think of a template like a cookie cutter design.

  • The header gives other files the shape of the cutter.
  • The definition gives the full cutter design.
  • Instantiation is when you actually stamp out a cookie of a specific shape and size.

If another .cpp file wants an int cookie, the compiler must either:

  • have the full cutter design available right there, or
  • be told that someone else already made that exact cookie

Putting template definitions in headers means every file can stamp out whatever cookie it needs.

Putting the definition in a .cpp file with explicit instantiations means:

  • only specific cookies are manufactured
  • everyone else can only request those exact ones

So this is not a trick. It is more like switching from made-to-order to pre-manufactured inventory.

Take Quiz

Syntax and Examples

The core syntax looks like this:

// header
class Foo {
public:
    template <typename T>
    void doSomething(const T& value);
};
// source file
#include "Foo.h"
#include <string>
#include <iostream>

template <typename T>
void Foo::doSomething(const T& value) {
    std::cout << value << '\n';
}

// Explicit instantiations
template void Foo::doSomething<int>(const int&);
template void Foo::doSomething<std::string>(const std::string&);

Now this usage works:

Foo f;
f.doSomething(42);
f.doSomething(std::string("hello"));

But this will fail if no explicit instantiation exists for double:

Foo f;
f.doSomething(3.14); // likely linker error if double is not instantiated

What this means

  • int and std::string work because code was explicitly generated for them.
  • double fails because only the declaration is visible in the header, not the definition.
  • The compiler in another source file cannot generate Foo::doSomething<double> on its own.

A more complete library-style pattern

Sometimes developers also add extern template declarations in the header to avoid duplicate instantiations:

// Foo.h
#include <string>

class Foo {
public:
    template <typename T>
    void doSomething(const T& value);
};

extern template void Foo::doSomething<int>(const int&);
extern template void Foo::doSomething<std::string>(const std::string&);
// Foo.cpp
#include "Foo.h"
#include <iostream>

template <typename T>
void Foo::doSomething(const T& value) {
    std::cout << value << '\n';
}

template void Foo::doSomething<int>(const int&);
template void Foo::doSomething<std::string>(const std::string&);

This pattern is standard, though extern template is from C++11 onward. The basic explicit instantiation approach itself is older and standard.

Step by Step Execution

Consider this code:

// Foo.h
class Foo {
public:
    template <typename T>
    void print(const T& value);
};
// Foo.cpp
#include "Foo.h"
#include <iostream>
#include <string>

template <typename T>
void Foo::print(const T& value) {
    std::cout << value << '\n';
}

template void Foo::print<int>(const int&);
template void Foo::print<std::string>(const std::string&);
// main.cpp
#include "Foo.h"
#include <string>

int main() {
    Foo f;
    f.print(10);
    f.print(std::string("hi"));
}

What happens step by step

  1. main.cpp includes Foo.h.
  2. The compiler sees that Foo has a member template print.
  3. main.cpp calls f.print(10) and f.print(std::string("hi")).
  4. main.cpp does not see the template definition, only the declaration.
  5. That is acceptable because the linker may still find compiled versions elsewhere.
  6. Foo.cpp contains the full template definition.
  7. Foo.cpp also explicitly instantiates:
    • Foo::print<int>
    • Foo::print<std::string>
  8. The compiler generates object code for those two versions in Foo.cpp.
  9. During linking, main.cpp's calls are matched to those generated definitions.
  10. The program links successfully.

If you add this line in main.cpp

f.print(2.5);

then:

  1. main.cpp still only sees the declaration.
  2. It needs Foo::print<double>.
  3. No explicit instantiation for double exists in Foo.cpp.
  4. The linker cannot find a definition.
  5. You get an unresolved external or similar linker error.

Real World Use Cases

Explicit template instantiation is useful in several practical situations.

1. Library code with a fixed supported type set

A math or utility library may support only a few types:

  • int
  • float
  • double

Instead of exposing template definitions in headers, the library can instantiate only those supported types.

2. Reducing compile times

Header-only templates can slow builds because every translation unit may instantiate the same templates repeatedly. Moving definitions into a .cpp file and explicitly instantiating common types can reduce compile overhead.

3. Hiding implementation details

A library author may want users to see only the public API, not the full template implementation. Explicit instantiation allows some template code to stay in source files.

4. Controlling binary size

If only a few types are valid, generating code only for those types can keep the binary more predictable.

5. Enforcing allowed types

Sometimes a template is written in a generic style internally, but the public API should support only selected types. Explicit instantiation can act as a practical boundary.

For example, a serializer might support only:

  • int
  • std::string
  • std::vector<int>

and reject everything else by simply not providing instantiations.

Take Quiz

Real Codebase Usage

In real codebases, developers usually choose one of two patterns.

Pattern 1: Header-only templates

This is the most common pattern when a template should work for arbitrary types.

// all in header
template <typename T>
T add(T a, T b) {
    return a + b;
}

Use this when:

  • the template is meant to be truly generic
  • users may instantiate it with unknown types
  • ease of use matters more than hiding implementation

Pattern 2: .cpp definition plus explicit instantiation

This is common in libraries that support a fixed set of types.

// implementation in .cpp
// explicit instantiations for supported types only

Use this when:

  • the supported types are known in advance
  • compile-time cost matters
  • implementation hiding is useful

Related real-world patterns

Validation by limiting supported types

Instead of allowing every possible type, a codebase may intentionally instantiate only approved ones.

Error handling through linker failures or static assertions

If unsupported types should fail clearly, explicit instantiation can be combined with constraints or static checks.

API boundary control

A public header may declare a template member, but the library only ships compiled instantiations for documented types.

Reducing repeated work

Large codebases often avoid unnecessary template instantiations because they increase build time and object file size.

Practical advice

If other developers are expected to use your template with arbitrary custom types, do not hide the definition in a .cpp file.

If your template is only an implementation mechanism and you control all valid types, explicit instantiation is a solid approach.

Common Mistakes

1. Assuming declarations are enough for any type

Beginners often think this is sufficient:

// Foo.h
class Foo {
public:
    template <typename T>
    void print(const T& value);
};
// Foo.cpp
template <typename T>
void Foo::print(const T& value) {
}

This compiles, but usage from another source file may fail at link time because no instantiation was generated.

2. Forgetting to explicitly instantiate needed types

Broken example:

// Only this exists
template void Foo::print<int>(const int&);

But elsewhere:

Foo f;
f.print(std::string("hello"));

This will likely fail to link.

3. Treating explicit instantiation as if it supports all future types

Explicit instantiation is not automatic generality. It only supports the types you list.

4. Hiding a template that should really be generic

If users need this:

MyType x;
f.print(x);

then keeping the definition only in a .cpp file is usually the wrong design.

5. Confusing compile errors with linker errors

With missing template definitions in headers, you often do not get a syntax error. Instead, the program compiles and then fails to link.

6. Forgetting required includes for explicitly instantiated types

If you explicitly instantiate std::string, include <string> in the source file where the instantiation appears.

7. Using reserved or confusing names

In your original example, a member function named do may be legal, but naming like doSomething or print is often clearer for teaching and maintenance.

How to avoid these mistakes

  • Put template definitions in headers unless you have a good reason not to.
  • Use explicit instantiation only for known supported types.
  • Document which types are supported.
  • Add tests that compile and link all supported type combinations.

Comparisons

ApproachWhere definition livesSupports unknown types?Compile-time costHides implementation?Typical use
Header-only templateHeaderYesHigherNoGeneral-purpose templates
.cpp + explicit instantiationSource fileNoLower for usersYesFixed supported type set
Non-template overloadsHeader/sourceNoUsually lowerYesSmall fixed APIs

Explicit instantiation vs header-only templates

  • Header-only is flexible.
  • Explicit instantiation is controlled.
  • Header-only is usually easier for library users.
  • Explicit instantiation is often better when the library owns the allowed types.

Explicit instantiation vs ordinary overloads

If you only need a tiny fixed set of types, overloads may be simpler:

class Foo {
public:
    void print(int value);
    void print(const std::string& value);
};

This avoids template complexity entirely.

Explicit instantiation vs specialization

  • Explicit instantiation says: generate code for this type.
  • Specialization says: use a different implementation for this type.

Example of specialization:

template <typename T>
void printValue(const T& value) {
    std::cout << value << '\n';
}

template <>
void printValue<std::string>(const std::string& value) {
    std::cout << "string: " << value << '\n';
}

These concepts are related but not the same.

Take Quiz

Cheat Sheet

// Header
class Foo {
public:
    template <typename T>
    void func(const T& value);
};
// Source
template <typename T>
void Foo::func(const T& value) {
    // implementation
}

template void Foo::func<int>(const int&);
template void Foo::func<std::string>(const std::string&);

Rules to remember

  • Templates are usually defined in headers.
  • A template definition in a .cpp file works only if needed instantiations are provided.
  • Explicit instantiation is standard C++ and portable.
  • Unsupported types usually cause linker errors, not compile errors.
  • Use this approach when the supported type set is fixed.
  • If the template should work for arbitrary user-defined types, keep the definition in the header.

Quick decision guide

  • Need any type? → put template definition in the header.
  • Need only known types? → .cpp + explicit instantiation is fine.
  • Need only 2 or 3 types? → consider plain overloads instead.

Key terms

  • Template definition: the full implementation
  • Instantiation: generated code for a specific type
  • Explicit instantiation: manually requesting code generation for chosen types
  • Specialization: custom implementation for a specific type

FAQ

Is putting a C++ template definition in a .cpp file portable?

Yes. If you use explicit instantiation correctly, this is standard C++ and works across conforming compilers.

Why are templates usually defined in header files?

Because the compiler normally needs the full template definition wherever it instantiates the template for a new type.

Is explicit instantiation a hack?

No. It is an official language feature and a common technique when only specific types are supported.

What happens if I use a type that was not explicitly instantiated?

You will usually get a linker error because no compiled definition exists for that type.

Can I use this technique for template classes too?

Yes. Explicit instantiation also works for class templates.

Should I always move templates into .cpp files?

No. Most templates are better kept in headers unless you control the full list of supported types.

When should I use overloads instead of templates?

If you only support a very small fixed set of types, overloads can be simpler and clearer.

Does this help reduce compile times?

It can, especially in larger projects, because fewer translation units need to instantiate the same templates repeatedly.

Take Quiz

Mini Project

Description

Build a small C++ logging utility that supports printing only selected types using explicit template instantiation. This demonstrates how to keep a template definition in a .cpp file while still exposing a clean interface in a header.

Goal

Create a logger class whose template member function works for int and std::string, but not for unsupported types unless you explicitly add them.

Requirements

  • Create a class with a templated member function declared in a header file.
  • Define the templated member function in a .cpp file.
  • Explicitly instantiate the function for int and std::string.
  • Show successful calls with supported types in main().
  • Keep the implementation separate from the declaration.
Take Quiz

Keep learning

Related questions

View more