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
.cppfile 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.
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
intandstd::stringwork because code was explicitly generated for them.doublefails 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
main.cppincludesFoo.h.- The compiler sees that
Foohas a member templateprint. main.cppcallsf.print(10)andf.print(std::string("hi")).main.cppdoes not see the template definition, only the declaration.- That is acceptable because the linker may still find compiled versions elsewhere.
Foo.cppcontains the full template definition.Foo.cppalso explicitly instantiates:Foo::print<int>Foo::print<std::string>
- The compiler generates object code for those two versions in
Foo.cpp. - During linking,
main.cpp's calls are matched to those generated definitions. - The program links successfully.
If you add this line in main.cpp
f.print(2.5);
then:
main.cppstill only sees the declaration.- It needs
Foo::print<double>. - No explicit instantiation for
doubleexists inFoo.cpp. - The linker cannot find a definition.
- 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:
intfloatdouble
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:
intstd::stringstd::vector<int>
and reject everything else by simply not providing instantiations.
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
| Approach | Where definition lives | Supports unknown types? | Compile-time cost | Hides implementation? | Typical use |
|---|---|---|---|---|---|
| Header-only template | Header | Yes | Higher | No | General-purpose templates |
.cpp + explicit instantiation | Source file | No | Lower for users | Yes | Fixed supported type set |
| Non-template overloads | Header/source | No | Usually lower | Yes | Small 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.
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
.cppfile 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.
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
.cppfile. - Explicitly instantiate the function for
intandstd::string. - Show successful calls with supported types in
main(). - Keep the implementation separate from the declaration.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.