Skip to content
C++
Language
since C++11
Basic

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 typename keyword 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.

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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

cpp
// 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.

cpp
// 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 here

The 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.

cpp
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

ConceptSyntax
Function templatetemplate <typename T> T fn(T a)
Class templatetemplate <typename T> class Foo { ... };
Multiple parameterstemplate <typename A, typename B>
Non-type parametertemplate <typename T, std::size_t N>
Default parametertemplate <typename T, typename C = std::vector<T>>
Explicit instantiationfn<double>(value)
Member definition outside classtemplate <typename T> void Foo<T>::method() { ... }
Where to put definitionsHeaders (.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:

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.