The C++ Textbook

Part 5 · Generic programming

Class templates

Containers and wrappers that work for any element type.

By the end of this chapter you can

  • Write a class template with a type parameter
  • Use a deduction guide
  • Explain what a partial specialization does

std::vector<T>, std::optional<T>, std::unique_ptr<T> — every one is a class template. This chapter is about writing your own, and about the two features that make them pleasant to use: deduction and specialisation.

A container of your own

A fixed-capacity stack, for any element type
#include <cstddef>
#include <iostream>
#include <stdexcept>
#include <string>

template <class T, std::size_t Capacity>
class FixedStack {
public:
    void push(const T& value) {
        if (size_ == Capacity) throw std::out_of_range("stack is full");
        items_[size_++] = value;
    }

    T pop() {
        if (size_ == 0) throw std::out_of_range("stack is empty");
        return items_[--size_];
    }

    bool empty() const { return size_ == 0; }
    std::size_t size() const { return size_; }
    static constexpr std::size_t capacity() { return Capacity; }

private:
    T items_[Capacity]{};
    std::size_t size_ = 0;
};

int main() {
    FixedStack<int, 4> numbers;
    numbers.push(1);
    numbers.push(2);
    std::cout << "popped " << numbers.pop() << ", " << numbers.size() << " left\n";

    FixedStack<std::string, 2> words;
    words.push("hello");
    std::cout << "capacity " << words.capacity() << ", top " << words.pop() << '\n';
}

Two kinds of parameter there. class T is a type parameter. std::size_t Capacity is a non-type parameter — a compile-time value, which is what lets the array be a fixed-size member rather than a heap allocation.

Non-type parameters must be compile-time constants, which is why FixedStack<int, n> with a run-time n will not compile. Chapter 2.6’s rule again: a fixed size means a constant.

Deduction guides

Class template arguments used to be mandatory. Since C++17 the compiler can deduce them from the constructor arguments — class template argument deduction, or CTAD:

Deducing the element type
#include <iostream>
#include <string>
#include <vector>

template <class T>
class Box {
public:
    explicit Box(T value) : value_(std::move(value)) {}
    const T& get() const { return value_; }
private:
    T value_;
};

int main() {
    Box<int> explicit_type{42};        // always worked
    Box deduced{42};                   // C++17: T deduced as int
    Box text{std::string{"hello"}};    // T = std::string

    std::cout << explicit_type.get() << ' ' << deduced.get() << ' ' << text.get() << '\n';

    // The standard library relies on it heavily:
    std::vector v{1, 2, 3};            // std::vector<int>
    std::cout << "vector of " << v.size() << '\n';
}

Deduction works automatically when the constructor’s parameters mention the type parameter directly. When they do not — or when you want a different answer — you write a deduction guide:

A guide, and why one is needed
#include <iostream>
#include <string>
#include <vector>

template <class T>
class Collection {
public:
    template <class Iterator>
    Collection(Iterator first, Iterator last) : items_(first, last) {}

    std::size_t size() const { return items_.size(); }
    const T& front() const { return items_.front(); }

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

// Without this, T cannot be deduced: the constructor's parameters are
// Iterators, and nothing mentions T.
template <class Iterator>
Collection(Iterator, Iterator) -> Collection<typename std::iterator_traits<Iterator>::value_type>;

int main() {
    std::vector<int> source{4, 8, 15};
    Collection c{source.begin(), source.end()};        // deduces Collection<int>

    std::cout << c.size() << " items, first is " << c.front() << '\n';

    std::vector<std::string> words{"alpha", "beta"};
    Collection w{words.begin(), words.end()};
    std::cout << w.size() << " words, first is " << w.front() << '\n';
}

The guide reads: given a constructor call with two Iterators, the class is Collection<the iterator's value type>. std::vector has exactly this guide, which is what makes std::vector v(first, last) work.

Member functions are instantiated lazily

A class template’s member functions are only compiled when called. That has a useful consequence: a member that would not compile for some T is harmless as long as nobody calls it for that T.

Only what you use is compiled
#include <iostream>
#include <string>

template <class T>
class Wrapper {
public:
    explicit Wrapper(T value) : value_(std::move(value)) {}

    const T& get() const { return value_; }

    // Only valid for types supporting +. Never instantiated for others.
    T doubled() const { return value_ + value_; }

private:
    T value_;
};

struct NotAddable { int id; };

int main() {
    Wrapper<int> n{21};
    std::cout << n.doubled() << '\n';

    // Constructing this is fine; doubled() is simply never instantiated.
    Wrapper<NotAddable> odd{NotAddable{7}};
    std::cout << "id " << odd.get().id << '\n';
    // odd.doubled();   // this line would be the error
}

This is why std::vector<T> can offer operator< even for element types that have none: the member exists in the template and is only checked if you use it.

Specialisation

Sometimes one type needs a different implementation. Full specialisation replaces the template entirely for one set of arguments:

A special case for bool
#include <iostream>
#include <string>

template <class T>
struct Describe {
    static std::string text(const T&) { return "some value"; }
};

// Full specialisation: this version is used when T is exactly bool.
template <>
struct Describe<bool> {
    static std::string text(bool value) { return value ? "true" : "false"; }
};

int main() {
    std::cout << Describe<int>::text(42) << '\n';
    std::cout << Describe<bool>::text(true) << '\n';
    std::cout << Describe<bool>::text(false) << '\n';
}

Partial specialisation applies to a family of arguments rather than one — all pointers, all std::vectors, anything with two parameters where they match:

Partial specialisation over a family
#include <iostream>
#include <string>
#include <vector>

template <class T>
struct Describe {
    static std::string text() { return "a value"; }
};

// Any pointer type.
template <class T>
struct Describe<T*> {
    static std::string text() { return "a pointer to " + Describe<T>::text(); }
};

// Any vector.
template <class T>
struct Describe<std::vector<T>> {
    static std::string text() { return "a vector of " + Describe<T>::text(); }
};

int main() {
    std::cout << Describe<int>::text() << '\n';
    std::cout << Describe<int*>::text() << '\n';
    std::cout << Describe<int**>::text() << '\n';
    std::cout << Describe<std::vector<int>>::text() << '\n';
    std::cout << Describe<std::vector<double*>>::text() << '\n';
}

Note that Describe<int**> matched the pointer specialisation with T = int*, which then matched it again — recursion over types, resolved entirely at compile time. That mechanism is what Chapter 5.7 builds type traits from.

The rule of zero applies here too

A class template that holds standard types needs no special member functions, exactly as in Chapter 3.5:

Nothing to write
#include <iostream>
#include <string>
#include <utility>
#include <vector>

template <class T>
class Registry {
public:
    void add(T value) { items_.push_back(std::move(value)); }
    std::size_t size() const { return items_.size(); }
    const T& at(std::size_t i) const { return items_[i]; }

    // No destructor, no copy, no move — the vector handles all of it.

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

int main() {
    Registry<std::string> r;
    r.add("alpha");
    r.add("beta");

    Registry<std::string> copy = r;        // correct deep copy, generated
    Registry<std::string> moved = std::move(r);

    std::cout << copy.size() << ' ' << moved.at(0) << '\n';
}

Check yourself

Practice