Class Templates — Generic Containers
A class template is a blueprint for generating classes. The same mechanism that makes std::vector<int> and std::vector<std::string> both exist — as two completely separate, fully specialized classes — is available for your own types. Class templates let you write data structures and utilities once and have the compiler generate a correctly typed, efficient version for every concrete type you use.
Writing a class template
The template<typename T> prefix appears before the class definition, not before each member. When defining member functions outside the class body, each definition must also be prefixed with the template header and the class name must be written as ClassName<T>.
template<typename T>
class Stack {
public:
void push(const T& value) { data_.push_back(value); }
void push(T&& value) { data_.push_back(std::move(value)); }
void pop() { data_.pop_back(); }
const T& top() const { return data_.back(); }
bool empty() const { return data_.empty(); }
std::size_t size() const { return data_.size(); }
private:
std::vector<T> data_;
};
// Instantiation — compiler generates Stack<int> and Stack<std::string>:
Stack<int> ints;
ints.push(1); ints.push(2); ints.push(3);
std::cout << ints.top(); // 3
Stack<std::string> words;
words.push("hello"); words.push("world");
std::cout << words.top(); // worldMember functions are only instantiated when they are actually called — not when the class is instantiated. This means a class template can have member functions that use operations not supported by all types, as long as those functions are not called with those types.
Defining member functions outside the class
For longer member functions it is cleaner to define them after the class body. Each out-of-class definition must repeat the template header and use ClassName<T>:: as the scope prefix.
template<typename T>
class Pair {
public:
Pair(T first, T second);
T min() const;
T max() const;
private:
T first_, second_;
};
// Out-of-class definitions — each needs its own template header:
template<typename T>
Pair<T>::Pair(T first, T second) : first_(first), second_(second) {}
template<typename T>
T Pair<T>::min() const {
return (first_ < second_) ? first_ : second_;
}
template<typename T>
T Pair<T>::max() const {
return (first_ > second_) ? first_ : second_;
}
Pair<int> p{3, 7};
std::cout << p.min(); // 3
std::cout << p.max(); // 7Default template arguments
Template parameters can have default arguments, just like function parameters. This is how std::vector<T, Allocator> works: the allocator defaults to std::allocator<T>so most users only write std::vector<int>.
template<typename T, typename Container = std::vector<T>>
class Stack {
public:
void push(const T& v) { c_.push_back(v); }
void pop() { c_.pop_back(); }
const T& top() const { return c_.back(); }
bool empty() const { return c_.empty(); }
private:
Container c_;
};
Stack<int> s1; // uses vector<int>
Stack<int, std::deque<int>> s2; // uses deque<int>Class Template Argument Deduction (C++17)
Before C++17, class template arguments could not be deduced from constructor arguments — you had to write the type explicitly. C++17's Class Template Argument Deduction (CTAD) allows the compiler to deduce template arguments from constructor arguments, similar to function template deduction.
// Pre-C++17: must specify type explicitly
std::pair<int, std::string> p1(42, "hello");
std::vector<int> v1 = {1, 2, 3};
// C++17 CTAD: type deduced from constructor arguments
auto p2 = std::pair(42, "hello"); // pair<int, const char*>
auto v2 = std::vector{1, 2, 3}; // vector<int>
// Works with your own templates too:
template<typename T>
class Wrapper { public: Wrapper(T v) : val_(v) {} private: T val_; };
auto w = Wrapper(42); // C++17: T deduced as int, no need to write Wrapper<int>
auto s = Wrapper{std::string("hi")}; // T = std::stringKey rules to remember
Class templates must be defined in headers — the definition must be visible at instantiation
Unlike regular classes, you cannot put the member function definitions in a .cpp file. They all go in the header, after the class body.
Member functions are only instantiated when called — not when the class is instantiated
A Stack<int> does not instantiate member functions that are never called, even if those functions wouldn't compile for int.
Each out-of-class member function definition needs its own template<typename T> prefix
The scope operator is ClassName<T>::memberName, not ClassName::memberName.
Use CTAD (C++17) to avoid repeating type arguments in variable declarations
std::pair(1, 2.0) instead of std::pair<int, double>(1, 2.0). Cleaner and less error-prone.