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.
Template Type Deduction Rules
When you call a function template without explicit type arguments, the compiler deduces each template parameter by matching the types of the call arguments against the function parameter declarations. The three key rules are: (1) top-level const and references are stripped from the argument type when deducing a plain T; (2) if the parameter is T&, the reference is kept and const is preserved; (3) universal references (T&&) follow their own collapsing rules.
template <typename T> void by_value(T x); // deduces T, strips ref & const
template <typename T> void by_ref(T& x); // deduces T, preserves const
template <typename T> void by_fwd(T&& x); // universal reference
int i = 42;
const int ci = 42;
by_value(ci); // T = int (top-level const stripped)
by_ref(ci); // T = const int (const preserved)
by_fwd(i); // T = int& (lvalue β T deduced as int&)
by_fwd(42); // T = int (rvalue β T deduced as int)Deduction fails when arguments conflict β calling max_of(3, 3.14) on a template with a single T tries to deduce T = int and T = double simultaneously and produces an error. The fix is either an explicit template argument (max_of<double>(3, 3.14)) or a second type parameter. Use auto in C++14 lambda parameters or C++20 abbreviated function templates (auto max_of(auto a, auto b)) to let each parameter deduce independently.
Non-Type Template Parameters
Template parameters can be compile-time values β integers, enums, pointers to objects with external linkage, or (since C++20) floating-point values and structural types β not just types. Non-type parameters are resolved at compile time and baked into the generated code, enabling array sizes, flags, and identifiers to influence the template's behaviour without runtime overhead.
#include <array>
#include <bitset>
#include <iostream>
// Integer non-type parameter: N is a compile-time constant
template <typename T, std::size_t N>
class RingBuffer {
public:
void push(T val) { data_[head_++ % N] = val; }
T at(std::size_t i) const { return data_[i % N]; }
private:
std::array<T, N> data_{};
std::size_t head_{0};
};
// Enum non-type parameter
enum class Endian { Big, Little };
template <Endian E>
uint32_t read_u32(const uint8_t* p) {
if constexpr (E == Endian::Big)
return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
else
return (p[3] << 24) | (p[2] << 16) | (p[1] << 8) | p[0];
}
int main() {
RingBuffer<int, 8> rb; // N=8 baked into type β zero heap allocation
rb.push(1);
rb.push(2);
}C++20 extended non-type parameters to include floating-point literals, nullptr, and any "structural type" (a class with no private members and no user-defined comparison operators). This enables template <double Epsilon> for numerical code and template <std::string_view Name> for compile-time string identifiers.
Template Template Parameters
A template template parameter lets you pass a class template (not an instantiation of one) as a template argument, so the outer template can instantiate its own version of the inner one. This is useful when you want to parameterize a container's container type without fixing the element type ahead of time.
#include <deque>
#include <stack>
#include <vector>
// Template template parameter: Container is a class template, not a type
template <typename T,
template <typename, typename> class Container = std::vector>
class Bag {
Container<T, std::allocator<T>> data_;
public:
void add(T val) { data_.push_back(std::move(val)); }
std::size_t size() const { return data_.size(); }
};
int main() {
Bag<int> v_bag; // uses vector<int, allocator<int>>
Bag<int, std::deque> d_bag; // uses deque<int, allocator<int>>
}The template template parameter signature must match the template being passed β std::vector takes <typename T, typename Allocator> so the parameter is declared template <typename, typename> class Container. C++17 introduced variadic template template parameters (template <typename...> class) that match templates of any arity, removing this fragility in simple cases.
typename vs class in Template Parameters
typename and class are completely interchangeable when declaring a type template parameter, but typename is universally preferred in modern C++ because class misleadingly suggests the argument must be a class type. Built-in types like int and double are valid template arguments for both spellings.
// These two declarations are identical
template <typename T> T square(T x) { return x * x; }
template <class T> T square(T x) { return x * x; }
// `class` is required in one specific place: template template parameters
// (before C++17 variadic improvement)
template <template <class> class Container> // `class` required here in C++11/14
void process(Container<int>& c) { /* ... */ }The typename keyword has a second, unrelated role: disambiguating dependent types inside templates (see Two-Phase Lookup below). class cannot play that role. Because of this dual purpose of typename, consistently using typename for type parameters makes code easier to read β every typename in a template context either introduces a type parameter or says "this is a type."
Two-Phase Lookup
When a template is compiled, the compiler makes two passes: the first checks syntax and resolves non-dependent names immediately; the second resolves dependent names at instantiation time with the actual template arguments. A dependent name is one that depends on a template parameter. Failing to qualify dependent names correctly is a common source of errors when porting code between compilers.
template <typename T>
class Base {
public:
int value = 42;
void show() { /* ... */ }
};
template <typename T>
class Derived : public Base<T> {
public:
void print() {
// ERROR in standard C++: 'value' is a dependent name but looks independent
// std::cout << value; // fails on GCC/Clang
// CORRECT: qualify with this-> to make it dependent
std::cout << this->value << "\n";
// Or use a using declaration
// using Base<T>::value;
}
};The compiler cannot see into Base<T> during the first phase because T is unknown. Without this->, value looks like a free name, and the compiler reports it as undeclared. The same issue arises with member function calls: write this->show() or Base<T>::show(). This is not a compiler quirk β it is standard behaviour mandated by the two-phase lookup rules.
Variadic Templates
Variadic templates accept an arbitrary number of type (or value) parameters through a parameter pack, enabling generic functions like std::make_unique and std::tuple that work for any number and combination of types. The ... operator both declares a pack and expands it.
#include <iostream>
// Base case: no arguments β prints nothing
void print_all() {}
// Recursive variadic template
template <typename First, typename... Rest>
void print_all(First first, Rest... rest) {
std::cout << first << " ";
print_all(rest...); // expand pack: recurse with one fewer argument
}
int main() {
print_all(1, "hello", 3.14, 'x'); // 1 hello 3.14 x
}sizeof...(Rest) gives the number of types remaining in the pack at compile time. C++17 fold expressions eliminate the recursive base-case pattern for simple operations:
// C++17 fold expression β sum any number of arguments
template <typename... Ts>
auto sum(Ts... args) {
return (args + ...); // unary right fold: a + (b + (c + d))
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
std::cout << sum(1.5, 2.5) << "\n"; // 4.0
}The (args + ...) fold expands to arg0 + (arg1 + (arg2 + ...)). Use (... + args) for a left fold. Fold expressions support all binary operators: &&, ||, ,, <<, and more.
Explicit Template Instantiation
Explicit instantiation forces the compiler to generate the code for a specific template specialization in one translation unit, and extern template suppresses re-generation in all others β cutting compile times and binary size when a template is used across many translation units. Without it, every .cpp file that uses vector<Widget> generates its own copy of the member functions, and the linker deduplicates them at the cost of extra work.
// widget.h
#include <vector>
#include <string>
template <typename T>
class Serializer {
public:
std::string serialize(const T& value);
T deserialize(const std::string& data);
};
// Tell every .cpp that includes this header: do NOT instantiate these here
extern template class Serializer<int>;
extern template class Serializer<double>;
// serializer.cpp β instantiate exactly once
#include "widget.h"
template <typename T>
std::string Serializer<T>::serialize(const T& value) {
return std::to_string(value); // simplified
}
template <typename T>
T Serializer<T>::deserialize(const std::string& data) {
// ...
return T{};
}
// Explicit instantiation definitions β code generated here and only here
template class Serializer<int>;
template class Serializer<double>;This pattern is how the C++ standard library itself is compiled on most platforms: std::basic_string<char> and std::vector<int> are explicitly instantiated in the library, and headers use extern template to prevent you from regenerating them.
Template Best Practices
Choose the right constraint mechanism for the problem: use C++20 concepts when you want readable error messages and self-documenting signatures; use SFINAE only when targeting C++17 or when you need fine-grained overload selection that concepts do not yet express. Explicit instantiation should be a deliberate optimization, not the default. Keep template definitions in headers unless you use the explicit instantiation pattern above.
#include <concepts>
// C++20 concepts β constraints are part of the signature, errors are clear
template <std::totally_ordered T>
T clamped(T value, T lo, T hi) {
return value < lo ? lo : (value > hi ? hi : value);
}
// C++17 SFINAE β same constraint, much harder to read
template <typename T,
typename = std::enable_if_t<std::is_arithmetic_v<T>>>
T clamped_old(T value, T lo, T hi) {
return value < lo ? lo : (value > hi ? hi : value);
}
// Prefer concepts β error messages name the violated concept directly:
// clamped("hello", "a", "z") β T must satisfy 'totally_ordered'
// vs. SFINAE: a wall of substitution failure text
// When to use explicit template arguments vs deduction
clamped(3, 1, 5); // deduction: T = int β prefer this
clamped<double>(3, 1.0, 5.0); // explicit when deduction would conflict
// Separate interface from implementation: use .tpp or include in header
// template<typename T> class Foo { ... };
// #include "foo.tpp" β definitions, included at end of headerA practical rule of thumb: if a template works for only one or two types, prefer overloading. If it works for a well-defined category (all arithmetic types, all iterators, all ranges), express that category with a concept and write one template. Reach for SFINAE only when you need it for backward compatibility or when concepts are unavailable.