Class Templates
Class templates are classes parameterised by types: std::vector<T>, std::map<K, V> and std::optional<T> are all class templates. Writing your own lets you build reusable containers and utilities that work for any type, with full type safety.
A simple class template
#include <iostream> #include <stdexcept> #include <string> #include <vector> template <typename T> class Stack { public: void push(T value) { items_.push_back(std::move(value)); } T pop() { if (items_.empty()) throw std::out_of_range("pop from empty stack"); T top = std::move(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> undo; undo.push("type 'hello'"); undo.push("delete word"); while (!undo.empty()) std::cout << "undo: " << undo.pop() << '\n'; }
2 1 undo: delete word undo: type 'hello'
Stack<int> and Stack<std::string> are two distinct classes, generated from one template.
Class template argument deduction
Since C++17, the compiler can often deduce template arguments from a constructor, so you can leave them out:
#include <iostream> #include <string> #include <utility> #include <vector> template <typename A, typename B> struct Pair { A first; B second; }; int main() { std::vector v{1, 2, 3}; // std::vector<int> std::pair p{"age", 42}; // std::pair<const char*, int> Pair q{std::string{"pi"}, 3.14}; // Pair<std::string, double> std::cout << v.size() << ' ' << p.second << ' ' << q.first << '=' << q.second << '\n'; }
3 42 pi=3.14
Multiple and default parameters
#include <array> #include <iostream> template <typename T, std::size_t Capacity = 4> class RingBuffer { public: void push(const T& value) { data_[next_] = value; next_ = (next_ + 1) % Capacity; if (count_ < Capacity) ++count_; } std::size_t size() const { return count_; } T oldest() const { return data_[(next_ + Capacity - count_) % Capacity]; } private: std::array<T, Capacity> data_{}; std::size_t next_ = 0, count_ = 0; }; int main() { RingBuffer<int> last4; // Capacity defaults to 4 for (int i = 1; i <= 6; ++i) last4.push(i * 10); std::cout << last4.size() << " kept, oldest " << last4.oldest() << '\n'; RingBuffer<double, 2> last2; last2.push(1.5); last2.push(2.5); last2.push(3.5); std::cout << last2.oldest() << '\n'; }
4 kept, oldest 30 2.5
Member functions outside the class
When defining members outside the class body, repeat the template header:
#include <iostream> template <typename T> class Box { public: explicit Box(T value); const T& get() const; private: T value_; }; template <typename T> Box<T>::Box(T value) : value_(std::move(value)) {} template <typename T> const T& Box<T>::get() const { return value_; } int main() { Box b{3.5}; std::cout << b.get() << '\n'; }
3.5
As with function templates, all of this belongs in a header file.
Specialisation
Sometimes one type needs different behaviour. A specialisation replaces the template for a particular argument:
#include <iostream> #include <string> template <typename T> struct TypeName { static std::string get() { return "unknown"; } }; template <> struct TypeName<int> { static std::string get() { return "int"; } }; template <> struct TypeName<double> { static std::string get() { return "double"; } }; int main() { std::cout << TypeName<int>::get() << ' ' << TypeName<double>::get() << ' ' << TypeName<char>::get() << '\n'; }
int double unknown
The standard library uses specialisation for things like std::hash; you can specialise it so your own types work as unordered_map keys.
Try it yourself
Write a Statistics<T> class template that collects values and reports the count, minimum and maximum.
4 temps, 9.5 to 21 first Ana, last Zoe