Skip to content
Laviyx Learn

Try

C++ course menu

C++ Templates: Function Templates, Class Templates and Concepts

Learn C++ templates: writing generic function templates, type deduction, class templates like a generic Stack, multiple and non-type template parameters, template specialization, C++20 concepts with requires, and why templates live in headers.

Advanced12 min readUpdated
On this page

You have used templates since the first vector: std::vector<int> and std::vector<std::string> are the same code, generated for different types. Templates let you write that kind of generic code yourself: one function or class that works for many types, with no run-time cost.

Function templates#

Without templates, a largest function needs a copy for every type. A template writes it once:

C++
#include <iostream>
#include <string>

template <typename T>
T largest(const T& a, const T& b) {
    return a > b ? a : b;
}

int main() {
    std::cout << largest(3, 9) << '\n';                                    // T = int
    std::cout << largest(2.5, 1.5) << '\n';                                // T = double
    std::cout << largest(std::string("pear"), std::string("apple")) << '\n';  // T = std::string
}
Output
9
2.5
pear
  • template <typename T> introduces a type parameter named T. (class instead of typename means the same thing here.)
  • When you call largest(3, 9), the compiler deduces T = int and generates an int version.
  • The code works for any type that supports what the template uses, here > and copying.

Each type gets its own compiled version, so templates are as fast as hand-written code.

When deduction fails#

Both arguments must deduce the same T:

C++
template <typename T>
T largest(const T& a, const T& b) { return a > b ? a : b; }

int main() {
    largest(3, 2.5);   // error: T is int from 3 but double from 2.5
}

Fix it by naming the type explicitly, largest<double>(3, 2.5), or by converting an argument.

Templates that work on containers#

A template can accept any container, as long as it supports a range-based for:

C++
#include <iostream>
#include <list>
#include <string>
#include <vector>

template <typename Container>
void print_all(const Container& items) {
    for (const auto& item : items) {
        std::cout << item << ' ';
    }
    std::cout << '\n';
}

int main() {
    print_all(std::vector<int>{1, 2, 3});
    print_all(std::list<std::string>{"a", "b"});
    print_all(std::string("xyz"));
}
Output
1 2 3
a b
x y z

Class templates#

A class template is a blueprint for classes. Here is a small generic stack built on a vector:

C++
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

template <typename T>
class Stack {
public:
    void push(const T& value) { items_.push_back(value); }

    T pop() {
        if (items_.empty()) throw std::out_of_range("pop from empty stack");
        T top = items_.back();
        items_.pop_back();
        return top;
    }

    bool empty() const { return items_.empty(); }
    std::size_t size() const { return items_.size(); }

private:
    std::vector<T> items_;
};

int main() {
    Stack<int> numbers;
    numbers.push(1);
    numbers.push(2);
    std::cout << numbers.pop() << ' ' << numbers.size() << '\n';

    Stack<std::string> words;
    words.push("hello");
    std::cout << words.pop() << '\n';
}
Output
2 1
hello

For class templates you usually write the type in angle brackets: Stack<int>. Since C++17 the compiler can sometimes deduce it from the constructor (std::vector v = {1, 2, 3}; deduces std::vector<int>).

Several parameters, and non-type parameters#

Templates can take several type parameters, and also values known at compile time:

C++
#include <iostream>
#include <string>

template <typename K, typename V>
struct Pair {
    K key;
    V value;
};

template <typename T, int N>
struct FixedBuffer {
    T data[N] = {};
    int capacity() const { return N; }
};

int main() {
    Pair<std::string, int> p{"age", 21};
    FixedBuffer<double, 8> buf;
    std::cout << p.key << '=' << p.value << ", capacity " << buf.capacity() << '\n';
}
Output
age=21, capacity 8

std::array<int, 5> uses exactly this: a type parameter and a size parameter.

Specialization#

Sometimes one type needs different behaviour. A specialization provides it:

C++
#include <iostream>
#include <string>

template <typename T>
std::string describe(const T&) {
    return "some value";
}

template <>
std::string describe<bool>(const bool& b) {
    return b ? "yes" : "no";
}

int main() {
    std::cout << describe(42) << ", " << describe(true) << '\n';
}
Output
some value, yes

For functions, a plain overload (std::string describe(bool b)) is usually simpler than a specialization.

C++20 concepts: constraining templates#

When a template is used with an unsuitable type, older compilers produce pages of confusing errors. Concepts state the requirements up front, which gives clear errors and documents the intent:

C++
#include <concepts>
#include <iostream>

template <typename T>
    requires std::integral<T> || std::floating_point<T>
T average(T a, T b) {
    return (a + b) / 2;
}

template <std::integral T>          // shorter syntax
bool is_even(T n) {
    return n % 2 == 0;
}

int main() {
    std::cout << average(3.0, 4.0) << ' ' << average(7, 9) << '\n';
    std::cout << std::boolalpha << is_even(10) << '\n';
    // is_even(2.5);   // error: the constraint std::integral<double> is not satisfied
}
Output
3.5 8
true

The standard library provides many concepts in <concepts>, such as std::integral, std::floating_point, std::totally_ordered and std::invocable, and you can define your own with concept Name = ...;.

Templates live in headers#

The compiler generates template code when it sees a use with a specific type, so it needs the full template definition, not just a declaration. That is why templates are written entirely in header files, unlike ordinary functions, whose definitions go in .cpp files.

Common mistakes#

Practice#

Practice exercise

Write a function template count_matches(const std::vector<T>& items, const T& target) that returns how many elements equal target. Use it with a vector of ints and a vector of strings.

Show solution
C++
#include <iostream>
#include <string>
#include <vector>

template <typename T>
int count_matches(const std::vector<T>& items, const T& target) {
    int count = 0;
    for (const T& item : items) {
        if (item == target) ++count;
    }
    return count;
}

int main() {
    std::vector<int> nums = {3, 7, 3, 3, 9};
    std::vector<std::string> words = {"to", "be", "or", "not", "to", "be"};

    std::cout << count_matches(nums, 3) << ' '
              << count_matches(words, std::string("be")) << '\n';
}
Output
3 2

std::string("be") is needed so that T is deduced as std::string from both arguments. std::count in <algorithm> already does this job in real code.

Key takeaway#

Finished this lesson?