Templates & Concepts

C++ · Lesson 19 of 32 6 min read Updated

Suppose you need a function that returns the larger of two values. Without templates you would write one version for int, another for double, another for std::string, all with identical bodies. A template lets you write the code once with a placeholder type, and the compiler creates a version for each type you actually use. The whole standard library, including std::vector and std::max, is built from templates.

In this lesson you will write function and class templates, use non-type template parameters, and then make templates safer and clearer with concepts, one of the most important features of C++20.

Function templates

Start with template <typename T>, then write the function using T wherever a type is needed. When you call it, the compiler deduces T from the arguments.

C++
#include <print>
#include <string>

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

int main() {
    std::println("{}", largest(12, 45));                 // T is int
    std::println("{}", largest(3.5, 2.25));              // T is double
    std::println("{}", largest(std::string("Jaipur"), std::string("Pune")));
    std::println("{}", largest<double>(3, 4.5));         // T chosen by you
    // largest(3, 4.5);   // error: T cannot be both int and double
}
Output
45
3.5
Pune
4.5

The compiler generates real functions, such as largest<int> and largest<double>, only for the types you use. This is called instantiation, and it means templates cost nothing at run time: the generated code is as fast as a hand-written version. In the commented line the two arguments suggest different types for T, so deduction fails; writing largest<double> picks the type explicitly.

Since C++20 there is also a short form. A parameter declared as auto makes the function a template automatically, and each auto parameter can be a different type:

C++
auto add(auto a, auto b) {
    return a + b;
}
// add(2, 3) is 5; add(2, 0.5) is 2.5

Class templates

A class can be a template too. Here is a simple stack, a container where the last item pushed is the first popped, like a pile of plates.

C++
#include <print>
#include <string>
#include <vector>

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

    T pop() {
        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<std::string> plates;
    plates.push("Red");
    plates.push("Blue");
    plates.push("Green");
    std::println("Top plate: {}", plates.pop());
    std::println("Plates left: {}", plates.size());

    Stack<int> undo;
    undo.push(7);
    std::println("Undo step: {}", undo.pop());
}
Output
Top plate: Green
Plates left: 2
Undo step: 7

For a class template you normally write the type in angle brackets, as in Stack<int>. Since C++17, class template argument deduction (CTAD) lets the compiler work it out from constructor arguments when it can, which is why std::vector v{1, 2, 3}; creates a std::vector<int>.

Non-type template parameters

A template parameter can also be a value, such as a size. std::array<int, 7> uses one. Here a function works with an array of any type and any length:

C++
#include <array>
#include <print>

template <typename T, std::size_t N>
T sum(const std::array<T, N>& values) {
    T total{};
    for (const auto& v : values) {
        total += v;
    }
    return total;
}

int main() {
    std::array<int, 4> overs = {6, 11, 4, 9};
    std::array<double, 3> weights = {1.5, 2.25, 0.75};
    std::println("{} {}", sum(overs), sum(weights));
}
Output
30 4.5

T total{} starts at zero for numbers (and empty for strings). Both T and N are deduced from the argument.

Concepts: saying what a template needs

A plain template accepts any type, even one that makes no sense. If you call a gcd function with double values, the error appears deep inside the template, often in many confusing lines. A concept (C++20) is a named requirement on a type. The standard library provides many in <concepts>, such as std::integral, std::floating_point and std::totally_ordered.

C++
#include <concepts>
#include <print>

template <std::integral T>
T gcd_of(T a, T b) {
    while (b != 0) {
        T remainder = a % b;
        a = b;
        b = remainder;
    }
    return a;
}

double average(std::integral auto a, std::integral auto b) {
    return (a + b) / 2.0;
}

int main() {
    std::println("gcd(84, 36) = {}", gcd_of(84, 36));
    std::println("average = {}", average(7, 10));
    // gcd_of(8.5, 2.5);   // error: constraints not satisfied (double is not integral)
}
Output
gcd(84, 36) = 12
average = 8.5

template <std::integral T> means "T must be an integer type". The short form std::integral auto does the same for a single parameter. With a wrong type, the compiler now gives a short message pointing at the call and saying which concept was not satisfied.

You can also write your own concepts with a requires expression, which lists operations the type must support:

C++
#include <concepts>
#include <print>
#include <string>

template <typename T>
concept HasArea = requires(const T& shape) {
    { shape.area() } -> std::convertible_to<double>;
};

struct Square {
    double side;
    double area() const { return side * side; }
};

struct Label {
    std::string text;
};

void print_area(const HasArea auto& shape) {
    std::println("Area: {}", shape.area());
}

int main() {
    print_area(Square{4});
    // print_area(Label{"Hi"});   // error: Label does not satisfy HasArea
}
Output
Area: 16

HasArea accepts any type with a const member function area() whose result converts to double. Unlike virtual functions, this works at compile time and needs no common base class.

Common mistakes

  • Mixing types in deduction. largest(3, 4.5) fails; convert one argument or write largest<double>(...).
  • Putting template code in a .cpp file. Other files cannot instantiate it, and linking fails.
  • Forgetting the angle brackets. Stack s; fails when nothing can be deduced; write Stack<int> s;.
  • Assuming operations exist. A template that uses < fails for types without <; express the need with a concept such as std::totally_ordered.

Key points

  • A function template (template <typename T>) is a recipe the compiler instantiates for each type used; auto parameters (C++20) are a short form.
  • Class templates, such as Stack<T> and std::vector<T>, take types in angle brackets; CTAD can deduce them.
  • Templates can take values too, such as the N in std::array<T, N>.
  • Concepts (C++20) state what a template needs, using standard ones like std::integral or your own requires expressions.
  • Template definitions belong in headers.

Exercise

Write a function template count_above that takes a const std::vector<T>& and a limit of type T, and returns how many items are greater than the limit. Constrain T with std::totally_ordered. Test it with marks {45, 78, 92, 60, 88} and limit 75, and with the city names {"Agra", "Pune", "Delhi", "Surat"} and limit "Mumbai".

Show solution
C++
#include <concepts>
#include <print>
#include <string>
#include <vector>

template <std::totally_ordered T>
std::size_t count_above(const std::vector<T>& items, const T& limit) {
    std::size_t count = 0;
    for (const auto& item : items) {
        if (item > limit) {
            ++count;
        }
    }
    return count;
}

int main() {
    std::vector<int> marks = {45, 78, 92, 60, 88};
    std::vector<std::string> cities = {"Agra", "Pune", "Delhi", "Surat"};

    std::println("Marks above 75: {}", count_above(marks, 75));
    std::println("Cities after Mumbai: {}", count_above(cities, std::string("Mumbai")));
}
Output
Marks above 75: 3
Cities after Mumbai: 2

78, 92 and 88 are above 75. Alphabetically, "Pune" and "Surat" come after "Mumbai". The second call wraps the limit in std::string so that both arguments agree on T.

Quick Quiz

1. What does the compiler do with template <typename T> T largest(T a, T b) when you call largest(2, 9)?

2. Why do template definitions usually go in header files?

3. What does template <std::integral T> mean?

4. Which C++ version introduced concepts?

Learn C++ with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call