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.
Frequently Asked Questions
QWhat is the difference between full and partial specialization?
QWhat is CTAD and when should I be careful with it?
QWhat are alias templates?
QCan template member functions be virtual?
#include <iostream>
#include <format>
#include <string>
#include <stdexcept>
#include <type_traits>
#include <initializer_list>1 Basic class template: a type-safe stack
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
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
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
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
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
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.
Use this when fixed-size numeric grids benefit from compile-time dimensions and type safety.
It improves clarity and helps prevent common correctness mistakes.
Follow the code pattern shown in this section and adapt it to your types.
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
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
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;
}