Class Templates in C++
WHY THEY EXIST: Class templates let you write type-generic containers and abstractions once, and the compiler stamps out a concrete class for each combination of template arguments you use. This is how the entire STL (vector, map, unique_ptr, ...) is built.
Prerequisites: Function templates, type deduction, RAII
Standard: C++98 (basic), C++11 (variadic, alias templates), C++17 (CTAD), C++20 (concepts for constraining)
1. Basic class template: a type-safe stack
Works with any type T and a compile-time capacity N.
Deduction) can deduce unexpected types -- e.g.,
std::vector v{1, 2} deduces vector<int> but
std::vector v{"hello"} deduces vector<const char*>,
not vector<string>. Be explicit when the deduced
type might surprise you. Watch out: CTAD (C++17 Class Template Argument
template<typename T, std::size_t MaxSize = 10>
class Stack {
T data_[MaxSize];
std::size_t top_ = 0;
public:
void push(const T& value) {
if (top_ >= MaxSize)
throw std::overflow_error("Stack overflow");
data_[top_++] = value;
}
T pop() {
if (top_ == 0)
throw std::underflow_error("Stack underflow");
return data_[--top_];
}
[[nodiscard]] const T& peek() const {
if (top_ == 0)
throw std::underflow_error("Stack is empty");
return data_[top_ - 1];
}
[[nodiscard]] bool empty() const { return top_ == 0; }
[[nodiscard]] std::size_t size() const { return top_; }
};
// HOW CLASS TEMPLATE INSTANTIATION WORKS:
// Stack<int, 5> and Stack<double, 10> are completely separate types with
// no inheritance relationship between them. The compiler stamps out an
// independent class definition for each unique combination of template
// arguments, each with its own vtable (if any), its own static members,
// and its own machine code. You cannot implicitly convert one to the
// other.
//
// A crucial detail: only member functions you actually *call* are
// instantiated. If you never call Stack<int, 5>::peek(), the compiler
// never generates code for it. This means a class template can contain
// member functions that would not compile for certain types, as long as
// those functions are never invoked for those types.
//
// For example, you could add a print() method to Stack that uses
// operator<< on T. Stack<int, 5> would work fine, but if you
// instantiated Stack<SomeTypeWithNoOutput, 5> and never called print(),
// the code would still compile — print() is never instantiated for that
// type, so the missing operator<< is never an error.2. Class template with multiple type parameters
A simple key-value pair.
template<typename Key, typename Value>
struct Pair {
Key first;
Value second;
// C++17 CTAD: compiler can deduce Key and Value from constructor args
Pair(Key k, Value v) : first(std::move(k)), second(std::move(v)) {}
void print() const {
std::cout << std::format("({}, {})\n", first, second);
}
};
// Deduction guide (explicit CTAD for C++17)
// Allows: Pair p("hello", 42); -> Pair<const char*, int>
// With this guide: -> Pair<std::string, int>
template<typename V>
Pair(const char*, V) -> Pair<std::string, V>;3. Partial specialization
Specialize the template for pointer types.
template<typename T>
class TypeInfo {
public:
static std::string describe() {
return "Regular type";
}
};
// Partial specialization for pointer types
template<typename T>
class TypeInfo<T*> {
public:
static std::string describe() {
return "Pointer type";
}
};
// Partial specialization for arrays
template<typename T, std::size_t N>
class TypeInfo<T[N]> {
public:
static std::string describe() {
return std::format("Array of {} elements", N);
}
};
// HOW PARTIAL SPECIALIZATION MATCHING WORKS:
// When you write TypeInfo<int*>::describe(), the compiler must choose
// among the primary template and all partial specializations. It does
// this by *pattern matching* the template arguments against each
// specialization's parameter pattern.
//
// TypeInfo<int*>:
// - Primary template TypeInfo<T>: matches with T=int*.
// - Partial specialization TypeInfo<T*>: matches with T=int.
// The pointer specialization is *more specific* (it constrains the
// argument to be a pointer), so it wins.
//
// TypeInfo<int[5]>:
// - Primary template TypeInfo<T>: matches with T=int[5].
// - Partial specialization TypeInfo<T[N]>: matches with T=int, N=5.
// The array specialization is more specific, so it wins.
//
// TypeInfo<int>:
// - Primary template TypeInfo<T>: matches with T=int.
// - Neither TypeInfo<T*> nor TypeInfo<T[N]> matches int.
// The primary template is selected.
//
// The compiler always prefers the most specific match. If two partial
// specializations are equally specific for a given set of arguments
// (neither is "more specialized" than the other), the result is a
// compile error due to ambiguity. You would need to add another
// specialization to break the tie.4. A more realistic example: Matrix
Demonstrates template methods and operator overloading.
template<typename T, std::size_t Rows, std::size_t Cols>
class Matrix {
T data_[Rows][Cols]{}; // Value-initialized to zero
public:
Matrix() = default;
// Initializer list constructor
Matrix(std::initializer_list<std::initializer_list<T>> init) {
std::size_t r = 0;
for (const auto& row : init) {
std::size_t c = 0;
for (const auto& val : row) {
if (r < Rows && c < Cols)
data_[r][c] = val;
++c;
}
++r;
}
}
// Element access
T& operator()(std::size_t r, std::size_t c) { return data_[r][c]; }
const T& operator()(std::size_t r, std::size_t c) const { return data_[r][c]; }
// Matrix addition (same dimensions required at compile time!)
Matrix operator+(const Matrix& other) const {
Matrix result;
for (std::size_t r = 0; r < Rows; ++r)
for (std::size_t c = 0; c < Cols; ++c)
result(r, c) = data_[r][c] + other(r, c);
return result;
}
// Scalar multiplication
Matrix operator*(T scalar) const {
Matrix result;
for (std::size_t r = 0; r < Rows; ++r)
for (std::size_t c = 0; c < Cols; ++c)
result(r, c) = data_[r][c] * scalar;
return result;
}
void print() const {
for (std::size_t r = 0; r < Rows; ++r) {
for (std::size_t c = 0; c < Cols; ++c) {
std::cout << std::format("{:6.1f} ", static_cast<double>(data_[r][c]));
}
std::cout << '\n';
}
}
static constexpr std::size_t rows() { return Rows; }
static constexpr std::size_t cols() { return Cols; }
};5. Type traits: compile-time type introspection
Build your own traits to query type properties.
template<typename T>
struct is_string : std::false_type {};
template<>
struct is_string<std::string> : std::true_type {};
template<>
struct is_string<const char*> : std::true_type {};
// Helper variable template (C++14 convention)
template<typename T>
inline constexpr bool is_string_v = is_string<T>::value;int main() {
// Stack
std::cout << "--- Stack<int, 5> ---\n";
Stack<int, 5> stack;
for (int i = 1; i <= 5; ++i) stack.push(i * 10);
while (!stack.empty()) {
std::cout << stack.pop() << ' ';
}
std::cout << '\n';
// Stack with strings
Stack<std::string> str_stack;
str_stack.push("hello");
str_stack.push("world");
std::cout << std::format("Top: {}\n", str_stack.peek());
// Pair with CTAD
std::cout << "\n--- Pair ---\n";
Pair p1(std::string("age"), 25); // CTAD: Pair<string, int>
Pair p2(std::string("pi"), 3.14); // CTAD: Pair<string, double>
p1.print();
p2.print();
// Partial specialization
std::cout << "\n--- Partial Specialization ---\n";
std::cout << std::format("int: {}\n", TypeInfo<int>::describe());
std::cout << std::format("int*: {}\n", TypeInfo<int*>::describe());
std::cout << std::format("int[5]: {}\n", TypeInfo<int[5]>::describe());
// Matrix
std::cout << "\n--- Matrix<double, 2, 3> ---\n";
Matrix<double, 2, 3> m1 = {{1, 2, 3}, {4, 5, 6}};
Matrix<double, 2, 3> m2 = {{10, 20, 30}, {40, 50, 60}};
auto m3 = m1 + m2;
auto m4 = m1 * 2.0;
std::cout << "m1 + m2:\n";
m3.print();
std::cout << "m1 * 2:\n";
m4.print();
// Type traits
std::cout << "\n--- Custom Type Traits ---\n";
std::cout << std::format("is_string<std::string>: {}\n", is_string_v<std::string>);
std::cout << std::format("is_string<int>: {}\n", is_string_v<int>);
return 0;
}