2 Templates

In Chapter 1 you saw how virtual functions let you write code that works with a family of related types at run time. Templates solve a different problem: writing code that works with any type at compile time.

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, and so on — identical logic repeated for every type. Templates let you write the logic once and let the compiler generate the type-specific versions for you.

In this chapter you will learn function templates, class templates, specialization, class template argument deduction (CTAD), variadic templates, and concepts.

2.1 Function Templates

A function template is a blueprint for a function. The compiler generates a concrete function for each type you use it with:

template<typename T>
T max_of(T a, T b) {
    return (a > b) ? a : b;
}
#include <iostream>
#include <string>

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

int main() {
    std::cout << max_of(3, 7) << "\n";                               // int
    std::cout << max_of(3.14, 2.72) << "\n";                         // double
    std::cout << max_of<std::string>("Crazy", "Beautiful") << "\n";  // std::string

    return 0;
}
7
3.14
Crazy

The compiler deduces the type T from the arguments. When it sees max_of(3, 7), it generates int max_of(int a, int b). You can also specify the type explicitly with max_of<std::string>(...) when deduction is ambiguous or you want a specific type.

Each generated version is called a template instantiation. The compiler creates only the instantiations you actually use.

2.1.1 Multiple Template Parameters

You can have more than one template parameter:

template<typename T, typename U>
void print_pair(const T& first, const U& second) {
    std::cout << first << ", " << second << "\n";
}

print_pair("Usher", 2004);       // T = char[6], U = int
print_pair(3.14, "Yeah!");       // T = double, U = char[6]

Because the parameters are references, a string literal deduces as its array type (here char[6]: five characters plus the terminating '\0'). It decays to const char* only when the parameter is taken by value.

2.1.2 Trailing Return Types

The functions you have written so far put the return type before the function name:

int  add(int a, int b);
auto first(const std::vector<int>& v);   // return type deduced

C++ also has a trailing return type form, where you put auto in the usual return-type slot and write the actual return type after the parameter list with ->:

auto add(int a, int b) -> int;

For a simple function like this it looks like extra ceremony, but it earns its keep when the return type depends on the parameters. The leading-return-type form has to name a type before the parameter names exist:

template<typename T, typename U>
???  scale(const T& x, const U& factor);   // what goes in ???

You cannot write decltype(x * factor) there because x and factor are not yet in scope. The trailing form solves this by deferring the return type until after the parameters:

template<typename T, typename U>
auto scale(const T& x, const U& factor) -> decltype(x * factor) {
    return x * factor;
}

auto a = scale(3,    1.5);        // double
auto b = scale(2.0f, 4);          // float

The trailing form is also required for some lambda return types you will see later, and it is what the deduction guides later in this chapter use (Holder(const char*) -> Holder<std::string>;).

Tip: For ordinary functions where the return type does not depend on the parameters, the leading form is shorter and more familiar. Reach for the trailing form when the return type involves the parameter types — typically inside templates and lambdas.

Wut: Since C++14, you can often skip the trailing type entirely and let the compiler deduce it from the return statement. The two forms below are equivalent for most simple cases:

auto add(int a, int b) -> int { return a + b; }
auto add(int a, int b)        { return a + b; }

Pick one — they cannot coexist in the same file, because they declare the same function with conflicting return-type styles. Use the trailing form when you want to constrain the deduction, document the return type, or write the function in a header without giving a body.

2.1.3 Non-Type Template Parameters

Template parameters do not have to be types. They can also be compile-time constants:

template<typename T, int N>
T sum(const T (&arr)[N]) {
    T total = 0;
    for (int i = 0; i < N; ++i) {
        total += arr[i];
    }
    return total;
}

int scores[] = {90, 85, 92, 88};
std::cout << sum(scores) << "\n";  // 355 --- N is deduced as 4

std::array<T, N> uses a non-type template parameter for its size — that is why the size is part of the type.

2.2 Class Templates

A class template lets you define a class that works with any type. You have already used class templates from the standard library: std::vector<T>, std::array<T, N>, std::unique_ptr<T>.

Here is a simple stack:

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

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

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

    bool empty() const { return data_.empty(); }
    int size() const { return static_cast<int>(data_.size()); }

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

int main() {
    Stack<std::string> songs;
    songs.push("Since U Been Gone");
    songs.push("Umbrella");

    while (!songs.empty()) {
        std::cout << songs.pop() << "\n";
    }

    return 0;
}
Umbrella
Since U Been Gone

2.2.1 Member Functions Outside the Class

When you define a member function outside the class template, you repeat the template header:

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

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

template<typename T>
T Stack<T>::pop() {
    if (data_.empty()) {
        throw std::runtime_error("pop from empty stack");
    }
    T top = data_.back();
    data_.pop_back();
    return top;
}

Wut: Template definitions (not just declarations) must be visible at the point of use. This is why template code usually lives in header files, not .cpp files. If you put a template definition in a .cpp file, the compiler cannot see it when other files try to instantiate the template, and you will get linker errors.

2.3 Template Specialization

Sometimes a template’s general implementation does not work well for a particular type. Template specialization lets you provide a custom implementation for specific types.

2.3.1 Full Specialization

A full specialization provides an implementation for one specific type:

#include <cstring>
#include <iostream>

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

// Full specialization for const char*
template<>
const char* max_of<const char*>(const char* a, const char* b) {
    return (std::strcmp(a, b) > 0) ? a : b;
}

int main() {
    std::cout << max_of(3, 7) << "\n";              // uses general template
    std::cout << max_of("Hola", "Adios") << "\n";   // uses specialization

    return 0;
}
7
Hola

Without the specialization, max_of("Hola", "Adios") would compare pointer addresses, not the string contents.

Trap: For function templates, prefer plain overloading over specialization. A specialization does not participate in overload resolution the way a regular overload does — the compiler picks the most-specialized primary template first and only then checks specializations, which can lead to surprising matches when you mix templates and specializations across a codebase.

The overload version of the example above:

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

// plain overload, not a specialization:
const char *max_of(const char *a, const char *b) {
    return (std::strcmp(a, b) > 0) ? a : b;
}

reads the same, behaves the same in this case, and avoids the overload-resolution trap. The specialization form is the right tool for class templates (next subsection); for function templates, reach for an overload first.

2.3.2 Partial Specialization

Partial specialization customizes a class template for a category of types. It only works with class templates, not function templates:

#include <iostream>

template<typename T>
class Wrapper {
public:
    void describe() const { std::cout << "General wrapper\n"; }
};

// Partial specialization for pointer types
template<typename T>
class Wrapper<T*> {
public:
    void describe() const { std::cout << "Pointer wrapper\n"; }
};

int main() {
    Wrapper<int> w1;
    Wrapper<int*> w2;
    w1.describe();  // General wrapper
    w2.describe();  // Pointer wrapper

    return 0;
}
General wrapper
Pointer wrapper

2.4 CTAD (Class Template Argument Deduction)

Before C++17, you always had to specify template arguments when creating objects:

std::pair<int, std::string> p(1, "Complicated");   // verbose
std::vector<int> v = {1, 2, 3};                    // had to write <int>

C++17 introduced CTAD — the compiler can deduce the template arguments from the constructor arguments:

std::pair p(1, std::string("Complicated"));  // deduces pair<int, string>
std::vector v = {1, 2, 3};                   // deduces vector<int>

CTAD works with your own class templates too:

template<typename T>
class Holder {
public:
    Holder(T value) : value_(value) {}
    T get() const { return value_; }
private:
    T value_;
};

Holder h(42);                      // deduces Holder<int>
Holder s("All the Small Things");  // deduces Holder<const char*>

Trap: CTAD deduces const char* for string literals, not std::string. If you want Holder<std::string>, pass a std::string explicitly: Holder h(std::string("All the Small Things")).

2.4.1 Deduction Guides

You can provide deduction guides to control how CTAD works:

template<typename T>
class Holder {
public:
    Holder(T value) : value_(value) {}
    T get() const { return value_; }
private:
    T value_;
};

// Deduction guide: const char* should become std::string
Holder(const char*) -> Holder<std::string>;

Holder h("Boulevard of Broken Dreams");  // now deduces Holder<std::string>

2.5 Variadic Templates

Variadic templates accept any number of template arguments. They use parameter packs — a way to represent zero or more types or values.

#include <iostream>

template<typename... Args>
void print_all(const Args&... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

int main() {
    print_all(1, "Shakira", 3.14, "Drops of Jupiter");

    return 0;
}
1 Shakira 3.14 Drops of Jupiter

The ... after typename declares a parameter pack. The ((std::cout << args << " "), ...) is a fold expression (C++17) — it expands the pack by applying the comma operator between each element.

2.5.1 Fold Expressions

C++17 fold expressions provide a clean syntax for expanding parameter packs with an operator:

Syntax Expansion
(args + ...) a1 + (a2 + (a3 + a4)) (right fold)
(... + args) ((a1 + a2) + a3) + a4 (left fold)
(args + ... + init) a1 + (a2 + (a3 + init)) (right fold with init)
(init + ... + args) ((init + a1) + a2) + a3 (left fold with init)
template<typename... Args>
auto sum(const Args&... args) {
    return (args + ...);
}

std::cout << sum(1, 2, 3, 4) << "\n";  // 10

2.5.2 sizeof...

sizeof... returns the number of elements in a parameter pack:

template<typename... Args>
void count_args(const Args&... args) {
    std::cout << "Got " << sizeof...(args) << " arguments\n";
}

count_args(1, "two", 3.0);  // Got 3 arguments

2.6 Concepts (C++20)

Templates accept any type, and when a type does not support the operations used inside the template, you get an error. Before C++20, these errors were notoriously long and cryptic.

Concepts let you specify what a template type must support, giving clear errors when a type does not qualify.

2.6.1 Using Standard Concepts

The <concepts> header provides ready-made concepts:

#include <concepts>
#include <iostream>
#include <string>

template<std::integral T>
T double_it(T value) {
    return value * 2;
}

int main() {
    std::cout << double_it(21) << "\n";   // 42 --- int is integral
    // double_it(3.14);                   // error: double is not integral

    return 0;
}

Some commonly used standard concepts:

Concept Requires
std::integral Integer type (int, long, char, etc.)
std::floating_point Floating-point type (float, double)
std::same_as<T, U> T and U are the same type
std::convertible_to<From, To> From is convertible to To
std::copyable Type can be copied
std::movable Type can be moved

2.6.2 requires Clauses

You can write ad-hoc constraints with requires:

template<typename T>
    requires std::integral<T> || std::floating_point<T>
T absolute(T value) {
    return (value < 0) ? -value : value;
}

Or use a trailing requires clause:

template<typename T>
T absolute(T value) requires std::integral<T> || std::floating_point<T> {
    return (value < 0) ? -value : value;
}

2.6.3 Writing Custom Concepts

You can define your own concepts:

#include <concepts>
#include <iostream>
#include <string>

template<typename T>
concept Printable = requires(T t) {
    { std::cout << t } -> std::same_as<std::ostream&>;
};

template<Printable T>
void display(const T& value) {
    std::cout << value << "\n";
}

int main() {
    display(42);
    display("Viva la Vida");
    display(3.14);

    return 0;
}
42
Viva la Vida
3.14

The requires expression lists operations the type must support. The -> syntax constrains the return type of the expression.

2.6.4 requires Expressions

A requires expression can test multiple things:

template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;       // can add two T values
    { a += b };                         // supports +=
};

You can also test a concept (or a requires expression) in if constexpr to branch at compile time:

template<typename T>
void process(const T& value) {
    if constexpr (std::integral<T>) {
        std::cout << "Integer: " << value << "\n";
    } else if constexpr (std::floating_point<T>) {
        std::cout << "Float: " << value << "\n";
    } else {
        std::cout << "Other: " << value << "\n";
    }
}

Tip: Concepts make templates easier to use and debug. When a type does not satisfy a concept, the compiler tells you exactly which requirement failed instead of producing pages of nested template errors.

2.7 Try It: Template Playground

Here is a program that exercises several template features. Type it in, compile with g++ -std=c++23, and experiment:

#include <concepts>
#include <iostream>
#include <string>
#include <vector>

// Function template with concept
template<typename T>
    requires std::integral<T> || std::floating_point<T>
T clamp(T value, T lo, T hi) {
    if (value < lo) return lo;
    if (value > hi) return hi;
    return value;
}

// Class template
template<typename T>
class Playlist {
public:
    void add(const T& item) { items_.push_back(item); }

    void print() const {
        for (const auto& item : items_) {
            std::cout << "  " << item << "\n";
        }
    }

    auto size() const { return items_.size(); }

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

// Variadic template
template<typename... Args>
void log(const Args&... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

int main() {
    // Concepts
    std::cout << "Clamped: " << clamp(150, 0, 100) << "\n";
    std::cout << "Clamped: " << clamp(3.14, 0.0, 1.0) << "\n";

    // Class template with an explicit template argument
    Playlist<std::string> songs;
    songs.add("Crazy in Love");
    songs.add("Maps");
    songs.add("Seven Nation Army");

    std::cout << "\nPlaylist (" << songs.size() << " songs):\n";
    songs.print();

    // Variadic template
    log("Track", 1, "playing at", 44100, "Hz");

    return 0;
}
Clamped: 100
Clamped: 1

Playlist (3 songs):
  Crazy in Love
  Maps
  Seven Nation Army
Track 1 playing at 44100 Hz

Try adding a Playlist<int> for track numbers. Write a concept called HasSize that requires a type to have a .size() method, and write a function template constrained by it.

2.8 Key Points

  • Function templates let you write a function once and use it with any type. The compiler generates type-specific versions (instantiations) as needed.
  • Class templates work the same way for classes. std::vector, std::array, and std::unique_ptr are all class templates.
  • The compiler deduces template arguments from function arguments. You can also specify them explicitly with f<int>(...).
  • Non-type template parameters are compile-time constants like int N in std::array<T, N>.
  • Template definitions must be in headers because the compiler needs to see them at every instantiation point.
  • Full specialization provides a custom implementation for one specific type. Partial specialization (class templates only) customizes for a category of types.
  • CTAD (C++17) lets the compiler deduce class template arguments from constructor arguments. Deduction guides can customize this behavior.
  • Variadic templates accept any number of arguments using parameter packs (typename... Args).
  • Fold expressions (C++17) expand parameter packs concisely: (args + ...).
  • Concepts (C++20) constrain what types a template accepts, producing clear error messages.
  • Use requires clauses for ad-hoc constraints or define reusable named concepts.

2.9 Exercises

  1. Think about it: Templates generate code at compile time, while virtual functions dispatch at run time. What are the trade-offs between these two approaches to polymorphism?

  2. What does this print?

    template<typename T>
    T add(T a, T b) { return a + b; }
    
    std::cout << add(3, 4) << "\n";
    std::cout << add(std::string("Hola"), std::string(" mundo")) << "\n";
  3. Where is the bug?

    template<typename T>
    T max_of(T a, T b) {
        return (a > b) ? a : b;
    }
    
    std::cout << max_of(3, 4.5) << "\n";
  4. Calculation: How many template instantiations are generated by this code?

    template<typename T>
    T identity(T x) { return x; }
    
    identity(1);
    identity(2);
    identity(3.0);
    identity(std::string("test"));
    identity(42);
  5. What does this print?

    template<typename... Args>
    auto sum(Args... args) {
        return (args + ...);
    }
    
    std::cout << sum(1, 2, 3, 4, 5) << "\n";
  6. Think about it: Why must template definitions live in header files? What would happen if you put a template function’s definition in a .cpp file and tried to use it from another .cpp file?

  7. Where is the bug?

    template<typename T>
    class Holder {
    public:
        Holder(T val) : value_(val) {}
        T get() const { return value_; }
    private:
        T value_;
    };
    
    Holder h = "Lose Yourself";
    std::cout << h.get() << "\n";

    What type does CTAD deduce for T? Is this likely what the programmer intended?

  8. What does this print?

    template<typename T>
    void describe(T) { std::cout << "general\n"; }
    
    template<>
    void describe<int>(int) { std::cout << "int\n"; }
    
    describe(42);
    describe(3.14);
    describe("hello");
  9. Calculation: What does sizeof...(args) return for this call?

    template<typename... Args>
    int count(Args... args) { return sizeof...(args); }
    
    std::cout << count(1, "two", 3.0, '4', true) << "\n";
  10. Write a program that defines a class template Pair<T, U> with two members first and second, a constructor, and a print() method. Test it with Pair<std::string, int> storing song names and release years. Add a deduction guide so that Pair("I Gotta Feeling", 2009) deduces Pair<std::string, int>.

  11. What does this print? And which return type does each function actually return?

    #include <iostream>
    #include <vector>
    
    auto first_leading(std::vector<int>& v) {
        return v.front();
    }
    
    auto first_trailing(std::vector<int>& v) -> int& {
        return v.front();
    }
    
    int main() {
        std::vector<int> v = {1, 2, 3};
        first_leading(v) = 99;       // (a)
        first_trailing(v) = 99;      // (b)
        std::cout << v.front() << "\n";
        return 0;
    }

    Which assignment compiles, which one does not, and why? What does this say about when to spell the return type explicitly with the trailing form?