Write Generic Code with C++ Templates
Learn to write function and class templates that work across types, eliminating repetition without sacrificing performance.
By the end of this page, you will be able to write function templates and class templates, understand how the compiler instantiates them, recognize the most common beginner mistakes, and know when templates are the right tool for the job.
What and Why
Imagine you want a function that returns the larger of two values. You write one for int, then realize you need the same logic for double, then for std::string. Three functions, identical bodies, different types β that is the problem templates solve.
A template is a blueprint the compiler uses to stamp out type-specific code on demand. You write the logic once; the compiler generates the concrete versions it needs. The result is zero runtime overhead compared to hand-written per-type functions, because the generated code is identical to what you would have written manually.
This is fundamentally different from runtime polymorphism (virtual functions), where the type is resolved at runtime through a vtable. Templates resolve types at compile time, which means the compiler can inline, optimize, and eliminate dead code freely.
C++ standard: everything in this tutorial requires C++11 or later. The
typenamekeyword for template parameters and the uniform initialization syntax used below are both C++11 features.
Step by Step
Step 1 β Your first function template
Start with the simplest possible template: a function that returns the maximum of two values.
#include <iostream>
template <typename T>
T max_of(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max_of(3, 7) << "\n"; // T = int
std::cout << max_of(3.14, 2.72) << "\n"; // T = double
std::cout << max_of('a', 'z') << "\n"; // T = char
}template <typename T> introduces a template parameter named T. Think of T as a placeholder that the compiler replaces with a real type each time you call max_of. You can also write class instead of typename β they are interchangeable in this position; typename is preferred because it is less confusing than class when T can be a built-in type.
When you write max_of(3, 7), the compiler deduces T = int from the arguments and generates a concrete int max_of(int, int) function. This process is called template argument deduction.
Step 2 β Multiple template parameters
Templates can take more than one type parameter.
#include <iostream>
template <typename From, typename To>
To convert(From value) {
return static_cast<To>(value);
}
int main() {
double d = convert<int, double>(42); // explicit template arguments
std::cout << d << "\n"; // 42.0
}Here deduction cannot fill in To (the return type is not part of the argument list), so we supply the template arguments explicitly with <int, double>.
Step 3 β Class templates
The same idea applies to classes. A class template lets you parameterize a data structure over the type it stores.
#include <iostream>
#include <stdexcept>
template <typename T>
class Stack {
public:
void push(T value) {
data_[top_++] = value;
}
T pop() {
if (top_ == 0) throw std::underflow_error{"stack is empty"};
return data_[--top_];
}
bool empty() const { return top_ == 0; }
private:
T data_[64]{};
int top_{0};
};
int main() {
Stack<int> ints;
ints.push(10);
ints.push(20);
std::cout << ints.pop() << "\n"; // 20
Stack<std::string> words;
words.push("hello");
std::cout << words.pop() << "\n"; // hello
}Stack<int> and Stack<std::string> are two completely separate types generated from one blueprint. Each member function is also a template β it is only compiled when actually called (lazy instantiation).
Common Patterns
Pattern 1 β Non-type template parameters
Template parameters do not have to be types; they can be compile-time values.
#include <array>
#include <iostream>
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
T total{};
for (const auto& v : arr) total += v;
return total;
}
int main() {
std::array<int, 4> nums{1, 2, 3, 4};
std::cout << sum(nums) << "\n"; // 10
}N is a non-type parameter β a compile-time integer baked into the template. This is how std::array itself works: std::array<int, 4> and std::array<int, 8> are different types with no heap allocation.
Pattern 2 β Default template arguments
Just like function parameters, template parameters can have defaults.
#include <vector>
#include <deque>
#include <iostream>
template <typename T, typename Container = std::vector<T>>
class Queue {
public:
void enqueue(T value) { data_.push_back(std::move(value)); }
T dequeue() {
T v = std::move(data_.front());
data_.erase(data_.begin());
return v;
}
private:
Container data_;
};
int main() {
Queue<int> q1; // uses vector<int>
Queue<int, std::deque<int>> q2; // uses deque<int>
q1.enqueue(1);
q1.enqueue(2);
std::cout << q1.dequeue() << "\n"; // 1
}Pattern 3 β Function template overloading
Templates participate in overload resolution. You can provide a specialized (more specific) overload that the compiler prefers when the type matches.
#include <iostream>
#include <cstring>
template <typename T>
bool equal(T a, T b) {
return a == b;
}
// More specific: called when both arguments are const char*
bool equal(const char* a, const char* b) {
return std::strcmp(a, b) == 0;
}
int main() {
std::cout << equal(1, 1) << "\n"; // true (template)
std::cout << equal("hi", "hi") << "\n"; // true (overload)
}The non-template overload is preferred over the template for const char* because the compiler favors exact non-template matches.
What Can Go Wrong
Mistake 1 β Mismatched template argument types
// BROKEN
template <typename T>
T max_of(T a, T b) { return (a > b) ? a : b; }
int main() {
max_of(3, 3.14); // error: deduced conflicting types int and double for T
}The compiler deduces T = int from 3 and T = double from 3.14. It cannot pick one, so it fails. Fix by casting explicitly, using two type parameters, or supplying the template argument: max_of<double>(3, 3.14).
Mistake 2 β Putting template definitions in a .cpp file
Templates must be defined in headers (or in the same translation unit where they are used). If you put the definition in a .cpp file and only declare it in a header, you will get linker errors.
// BROKEN layout
// stack.h
template <typename T>
class Stack { void push(T value); }; // declaration only
// stack.cpp
template <typename T>
void Stack<T>::push(T value) { ... } // definition hidden hereThe compiler cannot instantiate Stack<int>::push in another .cpp that only sees the header. Move definitions into the header, or use the export mechanism β but in practice, headers are the universal solution.
Mistake 3 β Assuming a template works for all types
A template compiles successfully only if T actually supports the operations you use. Using > inside max_of means T must define operator>. If you call max_of with a type that lacks it, you get a (sometimes cryptic) error.
struct Point { int x, y; };
int main() {
Point a{1, 2}, b{3, 4};
// max_of(a, b); // error: no match for operator> for Point
}C++20 concepts let you state these requirements explicitly and produce clear error messages β see /reference/language/templates-advanced.
Quick Reference
| Concept | Syntax |
|---|---|
| Function template | template <typename T> T fn(T a) |
| Class template | template <typename T> class Foo { ... }; |
| Multiple parameters | template <typename A, typename B> |
| Non-type parameter | template <typename T, std::size_t N> |
| Default parameter | template <typename T, typename C = std::vector<T>> |
| Explicit instantiation | fn<double>(value) |
| Member definition outside class | template <typename T> void Foo<T>::method() { ... } |
| Where to put definitions | Headers (.h / .hpp), not .cpp |
Key rules to remember:
- Template code is compiled lazily β only the member functions you actually call are instantiated.
- Each unique combination of template arguments produces a separate compiled entity.
- Templates resolve types at compile time; virtual functions resolve at runtime.
- Definitions must be visible at the point of use β keep them in headers.
What's Next
You have seen the fundamentals. The next steps deepen your mastery:
- Template specialization and advanced techniques β partial specialization, explicit specialization, and SFINAE.
- Variable templates β parameterize constants, not just functions and classes (C++14).
- Expression templates β an advanced idiom that uses templates to eliminate temporary objects in expression evaluation.
- ADL and templates β how argument-dependent lookup interacts with template instantiation.
Once you are comfortable here, explore C++20 concepts, which let you write constraints on template parameters that produce readable error messages and express intent directly in the signature.