Skip to main content
C++ · lesson 10 of 24
C++ advanced Lesson 10 of 24

Templates in C++

C++ templates with real compiler output: function and class templates, specialization, type traits, SFINAE vs concept errors and compile-time recursion.

By SkillByExample · Updated

Templates let you write code once and have the compiler generate a concrete version for every type you use. It all happens at compile time — std::vector<int> and std::vector<std::string> are separate classes with no runtime genericity cost.

Function Templates

A function template takes type parameters that the compiler deduces from the arguments. GCC’s __PRETTY_FUNCTION__ shows which instantiation actually ran.

#include <iostream>
#include <string>

template <typename T>
T larger(T a, T b) {
    std::cout << "[" << __PRETTY_FUNCTION__ << "] ";
    return (a > b) ? a : b;            // works for any T with operator>
}

int main() {
    std::cout << larger(3, 7) << "\n";
    std::cout << larger(3.14, 2.72) << "\n";
    std::cout << larger('a', 'z') << "\n";
    std::cout << larger<double>(3, 7.5) << "\n";          // explicit T: 3 converts
    std::cout << larger(std::string("pear"), std::string("apple")) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o max max.cpp
$ ./max
[T larger(T, T) [with T = int]] 7
[T larger(T, T) [with T = double]] 3.14
[T larger(T, T) [with T = char]] z
[T larger(T, T) [with T = double]] 7.5
[T larger(T, T) [with T = std::__cxx11::basic_string<char>]] pear

Four distinct functions were generated: int, double, char and std::string (spelled std::__cxx11::basic_string<char> inside libstdc++). The larger<double>(3, 7.5) call names T explicitly, because deduction from 3 and 7.5 would conflict — int versus double. Strings compare lexicographically, so “pear” wins. The function is named larger rather than max to avoid colliding with std::max through argument-dependent lookup on std::string.

Class Templates

A class template parameterizes a whole class. Stack<int> and Stack<std::string> are two unrelated types generated from one definition.

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

template <typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(T value) { data_.push_back(std::move(value)); }
    T pop() {
        if (data_.empty()) throw std::underflow_error("pop on empty Stack");
        T top = std::move(data_.back());
        data_.pop_back();
        return top;
    }
    bool empty() const { return data_.empty(); }
};

int main() {
    Stack<int> ints;
    for (int i : {1, 2, 3}) ints.push(i);
    while (!ints.empty()) std::cout << ints.pop() << " ";
    std::cout << "\n";

    Stack<std::string> words;           // a separate class, generated on demand
    words.push("template");
    std::cout << words.pop() << "\n";
    try { words.pop(); } catch (const std::underflow_error& e) { std::cout << e.what() << "\n"; }
}
$ g++ -std=c++20 -Wall -Wextra -o stack stack.cpp
$ ./stack
3 2 1 
template
pop on empty Stack

The stack is last-in, first-out, so 1, 2, 3 come back as 3 2 1. push takes T by value and moves it into the vector — one signature that copies lvalues and moves rvalues. Member functions of a class template are only instantiated when used, so a Stack<T> whose T can’t be moved still compiles as long as nobody calls push or pop, which both move.

Non-Type Template Parameters

Template parameters can be compile-time values. This is how std::array<T, N> carries its size in the type.

#include <array>
#include <iostream>
#include <numeric>

template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {                 // N deduced from the type
    return std::accumulate(arr.begin(), arr.end(), T{});
}

template <int Capacity>
class RingBuffer {
    int data_[Capacity]{};                            // size fixed at compile time
    int next_ = 0;
public:
    void add(int v) { data_[next_] = v; next_ = (next_ + 1) % Capacity; }
    int at(int i) const { return data_[i]; }
    static constexpr int capacity() { return Capacity; }
};

int main() {
    std::array<int, 5> a{1, 2, 3, 4, 5};
    std::array<double, 2> b{0.5, 0.25};
    std::cout << sum(a) << " " << sum(b) << "\n";

    RingBuffer<3> rb;
    for (int v = 1; v <= 4; ++v) rb.add(v);          // 4 overwrites 1
    std::cout << rb.at(0) << rb.at(1) << rb.at(2) << ", capacity "
              << RingBuffer<3>::capacity() << ", sizeof " << sizeof(rb) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o nttp nttp.cpp
$ ./nttp
15 0.75
423, capacity 3, sizeof 16

sum deduces both T and N from std::array<int, 5>, so there is no size argument to pass or get wrong. RingBuffer<3> holds a plain int[3]: after four adds, 4 has overwritten slot 0, and sizeof is 16 bytes (12 of data + 4 for next_) with no heap allocation. RingBuffer<4> would be a different type.

Template Specialization

When the generic version is wrong for a specific type, specialize. Functions allow only full specialization; classes also allow partial specialization for a family of types.

#include <cstring>
#include <iostream>
#include <string>

template <typename T>
bool equal(T a, T b) { return a == b; }

template <>                                           // full specialization
bool equal<const char*>(const char* a, const char* b) {
    return std::strcmp(a, b) == 0;                    // compare content, not addresses
}

template <typename T> struct TypeName     { static std::string get() { return "something"; } };
template <> struct TypeName<int>          { static std::string get() { return "int"; } };
template <typename T> struct TypeName<T*> {           // partial: any pointer
    static std::string get() { return "pointer to " + TypeName<T>::get(); }
};

int main() {
    char buf1[] = "hello", buf2[] = "hello";          // two different arrays
    const char* p1 = buf1;
    const char* p2 = buf2;
    std::cout << std::boolalpha;
    std::cout << "equal(p1, p2):          " << equal(p1, p2) << "\n";
    std::cout << "p1 == p2:               " << (p1 == p2) << "\n";
    std::cout << "equal(2, 2):            " << equal(2, 2) << "\n";
    std::cout << TypeName<int**>::get() << ", " << TypeName<double*>::get() << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o specialize specialize.cpp
$ ./specialize
equal(p1, p2):          true
p1 == p2:               false
equal(2, 2):            true
pointer to pointer to int, pointer to something

buf1 and buf2 are separate arrays holding the same text: comparing the pointers says false, but the const char* specialization compares with strcmp and says true. TypeName<T*> is a partial specialization matching any pointer, and it recurses: int** peels one * at a time until it hits the full specialization for int. double has no specialization, so it falls back to the primary template’s “something”.

Try the same partial trick on a function and it fails:

template <typename T>
bool equal(T a, T b) { return a == b; }

template <typename T>
bool equal<T*>(T* a, T* b) { return *a == *b; }   // partial specialization of a function

int main() {}
$ g++ -std=c++20 -Wall -Wextra -o partial_fail partial_fail.cpp 2>&1 | grep error
partial_fail.cpp:5:6: error: non-class, non-variable partial specialization 'equal<T*>' is not allowed

Use an overload — template <typename T> bool equal(T* a, T* b) — which is a separate template that overload resolution prefers for pointers.

Type Traits

<type_traits> answers questions about types at compile time. They are the building blocks for if constexpr, SFINAE and concepts.

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
void describe(const char* name) {
    std::cout << std::boolalpha << name << ": integral " << std::is_integral_v<T>
              << ", pointer " << std::is_pointer_v<T> << ", const " << std::is_const_v<T>
              << ", const after remove_pointer "
              << std::is_const_v<std::remove_pointer_t<T>> << "\n";
}

int main() {
    describe<int>("int");
    describe<const int*>("const int*");
    describe<int* const>("int* const");
    describe<bool>("bool");

    using D = std::decay_t<const std::string&>;          // strips const and &
    using C = std::common_type_t<int, long, float>;
    std::cout << "decay_t<const string&> is string: " << std::is_same_v<D, std::string> << "\n";
    std::cout << "common_type_t<int, long, float> is float: " << std::is_same_v<C, float> << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o traits traits.cpp
$ ./traits
int: integral true, pointer false, const false, const after remove_pointer false
const int*: integral false, pointer true, const false, const after remove_pointer true
int* const: integral false, pointer true, const true, const after remove_pointer false
bool: integral true, pointer false, const false, const after remove_pointer false
decay_t<const string&> is string: true
common_type_t<int, long, float> is float: true

The two pointer rows are the ones people get wrong. const int* is a (non-const) pointer to const int: the pointer itself isn’t const, the pointee is. int* const is the reverse — a const pointer to a mutable int. bool is an integral type. std::decay_t strips references and const the way passing by value does, and std::common_type_t<int, long, float> is float, the type the three would convert to in a ?: expression.

SFINAE with std::enable_if

SFINAE — substitution failure is not an error — lets a template remove itself from overload resolution when its signature can’t be formed.

#include <iostream>
#include <type_traits>

template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void print_bits(T value) {
    for (int i = sizeof(T) * 8 - 1; i >= 0; --i) std::cout << ((value >> i) & 1);
    std::cout << "\n";
}

int main() {
    print_bits(42);
    print_bits(static_cast<unsigned char>(5));
#ifdef BAD
    print_bits(3.14);                   // no integral T: the template is discarded
#endif
}
$ g++ -std=c++20 -Wall -Wextra -o sfinae sfinae.cpp
$ ./sfinae
00000000000000000000000000101010
00000101
$ g++ -std=c++20 -Wall -Wextra -DBAD -o sfinae sfinae.cpp 2>&1 | grep -E 'error|note'
sfinae.cpp:14:15: error: no matching function for call to 'print_bits(double)'
sfinae.cpp:5:6: note: candidate: 'template<class T, class> void print_bits(T)'
sfinae.cpp:5:6: note:   template argument deduction/substitution failed:
/usr/include/c++/13/type_traits:2610:11: error: no type named 'type' in 'struct std::enable_if<false, void>'

For int and unsigned char the default template argument std::enable_if_t<true> is void, and the function works. For double, enable_if<false> has no type member, substitution fails, and the template is silently discarded — leaving no candidate at all. The error message points into /usr/include/c++/13/type_traits, which is typical: SFINAE errors describe the machinery, not your intent.

C++20 Concepts

Concepts name a requirement and check it directly. Here’s the same print_bits with the standard std::integral concept:

#include <concepts>
#include <iostream>

void print_bits(std::integral auto value) {
    for (int i = sizeof(value) * 8 - 1; i >= 0; --i) std::cout << ((value >> i) & 1);
    std::cout << "\n";
}

int main() {
    print_bits(42);
    print_bits(3.14);
}
$ g++ -std=c++20 -Wall -Wextra -o concepts_fail concepts_fail.cpp 2>&1 | grep -E 'error|note'
concepts_fail.cpp:11:15: error: no matching function for call to 'print_bits(double)'
concepts_fail.cpp:4:6: note: candidate: 'template<class auto:16>  requires  integral<auto:16> void print_bits(auto:16)'
concepts_fail.cpp:4:6: note:   template argument deduction/substitution failed:
concepts_fail.cpp:4:6: note: constraints not satisfied
/usr/include/c++/13/concepts:100:24: note: the expression 'is_integral_v<_Tp> [with _Tp = double]' evaluated to 'false'

Compare with the SFINAE error: GCC now says “constraints not satisfied” and shows the exact check that was false — is_integral_v<double>. Writing your own concepts:

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

template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;   // a + b must compile and convert back to T
};

template <Addable T>
T sum(T a, T b) { return a + b; }

template <typename T>
    requires std::totally_ordered<T>
T clamp(T v, T lo, T hi) { return v < lo ? lo : v > hi ? hi : v; }

auto add(std::integral auto a, std::integral auto b) { return a + b; }  // abbreviated

int main() {
    std::cout << sum(1, 2) << " " << sum(1.0, 2.5) << " "
              << sum(std::string("a"), std::string("b")) << "\n";
    std::cout << std::boolalpha << "sum(true, true) = " << sum(true, true) << "\n";
    std::cout << clamp(15, 0, 10) << " " << add(2, 40L) << "\n";
    std::cout << "Addable<int*>: " << Addable<int*> << ", Addable<char>: "
              << Addable<char> << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o concepts concepts.cpp
$ ./concepts
3 3.5 ab
sum(true, true) = true
10 42
Addable<int*>: false, Addable<char>: true

A requires expression lists what must compile: here, a + b must exist and convert back to T. int, double and std::string all satisfy Addable; int* doesn’t, because adding two pointers is ill-formed. The surprise is bool: true + true is the int 2, which converts back to bool as true, so the concept is satisfied and sum(true, true) compiles. Concepts check syntax, not meaning — a concept is only as strict as its requirements. add(2, 40L) shows the abbreviated form: each std::integral auto is its own template parameter, so int and long mix freely.

Template Metaprogramming Basics

Before constexpr, compile-time computation was done with recursive template specializations. You’ll meet this style in older code.

#include <iostream>

template <int N>
struct Factorial { static constexpr long value = N * Factorial<N - 1>::value; };

template <>
struct Factorial<0> { static constexpr long value = 1; };      // base case

constexpr long factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }

int main() {
    static_assert(Factorial<5>::value == 120);
    static_assert(factorial(5) == 120);
    std::cout << Factorial<20>::value << " " << factorial(20) << "\n";
    int n = 10;
    std::cout << factorial(n) << "\n";            // the constexpr version also runs at runtime
    // Factorial<n>::value would not compile: n is not a constant
}
$ g++ -std=c++20 -Wall -Wextra -o tmp tmp.cpp
$ ./tmp
2432902008176640000 2432902008176640000
3628800

Factorial<20> instantiates Factorial<19> down to the Factorial<0> specialization, and each value is computed by the compiler. The constexpr function produces the identical 2432902008176640000 (20! still fits in a 64-bit long) and can also run on a runtime value like n, which Factorial<n> cannot. Prefer constexpr functions for values; reserve template metaprogramming for computing types.

Practice

  1. Add an overload template <typename T> bool equal(T* a, T* b) to specialize.cpp that compares pointees, and call it with two int* pointing at equal values.

    Solution
    template <typename T>
    bool equal(T* a, T* b) { return *a == *b; }
    
    int x = 5, y = 5;
    std::cout << equal(&x, &y) << "\n";   // true

    equal(&x, &y) is true. But check equal(p1, p2) again with buf2 changed to "help!": it now prints true too. Explicit specializations don’t take part in overload resolution — the compiler first picks between the two primary templates, prefers the more specialized T* one for any pointer, and only then would look for specializations of the winner. So const char* now goes to the pointee overload, which compares just the first characters. Mixing specializations and overloads is fragile; for C-strings, add a plain non-template bool equal(const char*, const char*), which beats both templates.

  2. Add const T& peek() const to Stack and make pop() on an empty stack return an empty std::optional<T> instead of throwing.

    Solution
    std::optional<T> pop() {
        if (data_.empty()) return std::nullopt;
        T top = std::move(data_.back());
        data_.pop_back();
        return top;
    }
    const T& peek() const { return data_.back(); }   // precondition: !empty()

    Callers then write while (auto v = s.pop()) use(*v);.

  3. Tighten Addable so bool no longer satisfies it.

    Solution
    template <typename T>
    concept Addable = !std::same_as<T, bool> && requires(T a, T b) {
        { a + b } -> std::convertible_to<T>;
    };

    sum(true, true) now fails with “constraints not satisfied”, and Addable<bool> prints false.

  4. Rewrite print_bits with if constexpr instead of a constraint, printing "not an integer" for other types.

    Solution
    template <typename T>
    void print_bits(T value) {
        if constexpr (std::is_integral_v<T>) {
            for (int i = sizeof(T) * 8 - 1; i >= 0; --i) std::cout << ((value >> i) & 1);
            std::cout << "\n";
        } else {
            std::cout << "not an integer\n";
        }
    }

    The difference: now print_bits(3.14) compiles and runs. A constraint rejects the call at compile time; if constexpr accepts it and picks behavior per type.

  5. Write a class template Pair<T, U> with a swap() member returning Pair<U, T>, and use class template argument deduction: Pair p{1, std::string("one")};.

    Solution
    template <typename T, typename U>
    struct Pair {
        T first; U second;
        Pair<U, T> swap() const { return {second, first}; }
    };
    template <typename T, typename U> Pair(T, U) -> Pair<T, U>;   // deduction guide
    
    Pair p{1, std::string("one")};
    auto q = p.swap();   // Pair<std::string, int>

    For an aggregate like this, C++20 deduces the arguments without the guide in GCC 13 as well; the explicit guide makes it work in C++17.

Frequently Asked Questions

What is SFINAE?
Substitution Failure Is Not An Error: when substituting template arguments into a template's signature produces invalid code, the compiler silently drops that candidate instead of failing. std::enable_if uses this to switch overloads on and off by type properties.
Should I use concepts or SFINAE?
Use concepts in C++20 code. They express the requirement by name, work on any template including abbreviated auto parameters, and produce errors that say which constraint failed instead of pointing into <type_traits>.
Can a function template be partially specialized?
No. Only class templates and variable templates can be partially specialized; GCC rejects a partial specialization of a function with 'non-class, non-variable partial specialization is not allowed'. Overload the function instead, for example with a separate template taking T*.
Do templates have a runtime cost?
No runtime dispatch is involved: the compiler generates a separate concrete function or class for each set of template arguments used, so the calls are as fast as hand-written code. The costs are compile time and binary size, since each instantiation is its own code.
What is a non-type template parameter?
A template parameter that is a compile-time value rather than a type, such as the N in std::array<T, N>. The value becomes part of the type, so RingBuffer<3> and RingBuffer<4> are different types with sizes fixed at compile time.