EduC++ Class Templates in C++

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;
}