Advanced Topics

Templates

Master C++ templates — function templates, class templates, template specialisation, variadic templates, SFINAE, and C++20 concepts.

Templates

Templates are C++'s mechanism for generic programming — writing code once that works for any type. The STL is built entirely on templates: std::vector<T>, std::sort, std::unique_ptr<T> all work because of templates. The compiler generates type-specific code from your template at compile time — zero runtime overhead.


Function Templates

cpp
#include <iostream>
using namespace std;

// Template — T is a type parameter deduced from arguments
template <typename T>
T maxOf(T a, T b) {
    return (a > b) ? a : b;
}

// Multiple type parameters
template <typename T, typename U>
auto add(T a, U b) {
    return a + b;   // Return type deduced (C++14)
}

// Non-type template parameter
template <int N>
void printN(const string& s) {
    for (int i = 0; i < N; i++) cout << s << " ";
    cout << "\n";
}

int main() {
    cout << maxOf(10, 20)       << "\n";  // 20 (int)
    cout << maxOf(3.14, 2.71)   << "\n";  // 3.14 (double)
    cout << maxOf('a', 'z')     << "\n";  // z (char)
    cout << maxOf(string("apple"), string("banana")) << "\n";  // banana

    cout << add(1, 2.5)   << "\n";  // 3.5 (int + double)
    cout << add(1, 2)     << "\n";  // 3   (int + int)

    printN<3>("Hello");   // Hello Hello Hello
    printN<5>("*");       // * * * * *

    return 0;
}

Class Templates

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

template <typename T>
class Stack {
    vector<T> data;
    size_t maxSize;

public:
    Stack(size_t maxSize = 100) : maxSize(maxSize) {}

    void push(const T& val) {
        if (data.size() >= maxSize) throw overflow_error("Stack full");
        data.push_back(val);
    }

    void push(T&& val) {   // Move overload
        if (data.size() >= maxSize) throw overflow_error("Stack full");
        data.push_back(move(val));
    }

    T pop() {
        if (data.empty()) throw underflow_error("Stack empty");
        T val = move(data.back());
        data.pop_back();
        return val;
    }

    const T& top() const {
        if (data.empty()) throw underflow_error("Stack empty");
        return data.back();
    }

    bool   empty() const { return data.empty(); }
    size_t size()  const { return data.size(); }
};

int main() {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);
    cout << intStack.pop() << "\n";   // 30

    Stack<string> strStack;
    strStack.push("hello");
    strStack.push("world");
    cout << strStack.top() << "\n";   // world

    // Works for any type
    Stack<pair<int,string>> pairStack;
    pairStack.push({1, "one"});
    auto [n, s] = pairStack.pop();
    cout << n << " " << s << "\n";   // 1 one

    return 0;
}

Template Specialisation

You can provide a custom implementation for specific types:

cpp
#include <iostream>
#include <string>
using namespace std;

// Primary template
template <typename T>
class Formatter {
public:
    string format(const T& val) {
        return to_string(val);
    }
};

// Full specialisation for bool
template <>
class Formatter<bool> {
public:
    string format(const bool& val) {
        return val ? "true" : "false";
    }
};

// Full specialisation for string (just quote it)
template <>
class Formatter<string> {
public:
    string format(const string& val) {
        return "\"" + val + "\"";
    }
};

// Partial specialisation — for any pointer type
template <typename T>
class Formatter<T*> {
public:
    string format(T* ptr) {
        if (!ptr) return "null";
        return "&(" + to_string(*ptr) + ")";
    }
};

int main() {
    Formatter<int>    fi;
    Formatter<bool>   fb;
    Formatter<string> fs;
    Formatter<int*>   fp;

    cout << fi.format(42)    << "\n";  // 42
    cout << fb.format(true)  << "\n";  // true
    cout << fs.format("hi")  << "\n";  // "hi"

    int x = 100;
    cout << fp.format(&x)    << "\n";  // &(100)
    cout << fp.format<int>(nullptr) << "\n";  // null

    return 0;
}

Variadic Templates

Templates that accept any number of type arguments:

cpp
#include <iostream>
#include <string>
using namespace std;

// Base case — no more arguments
void print() { cout << "\n"; }

// Variadic template — recurse on the rest
template <typename First, typename... Rest>
void print(First first, Rest... rest) {
    cout << first << " ";
    print(rest...);   // Recurse with one fewer argument
}

// Fold expression (C++17) — much simpler
template <typename... Args>
auto sum(Args... args) {
    return (... + args);   // Left fold: ((a + b) + c)
}

template <typename... Args>
void printAll(Args&&... args) {
    ((cout << forward<Args>(args) << " "), ...);  // Comma fold
    cout << "\n";
}

int main() {
    print(1, 2.5, "hello", true);   // 1 2.5 hello 1

    cout << sum(1, 2, 3, 4, 5)   << "\n";  // 15
    cout << sum(1.0, 2.5, 3.14)  << "\n";  // 6.64

    printAll(42, "world", 3.14, true);   // 42 world 3.14 1

    return 0;
}

Template Type Traits (C++11)

<type_traits> provides compile-time type introspection:

cpp
#include <iostream>
#include <type_traits>
using namespace std;

template <typename T>
void typeInfo() {
    cout << "  is_integral:       " << is_integral_v<T>       << "\n";
    cout << "  is_floating_point: " << is_floating_point_v<T> << "\n";
    cout << "  is_pointer:        " << is_pointer_v<T>        << "\n";
    cout << "  is_class:          " << is_class_v<T>          << "\n";
}

// Enable a function only for arithmetic types (SFINAE)
template <typename T>
enable_if_t<is_arithmetic_v<T>, T>
clamp(T val, T lo, T hi) {
    return max(lo, min(val, hi));
}

int main() {
    cout << "int:\n";      typeInfo<int>();
    cout << "double:\n";   typeInfo<double>();
    cout << "string:\n";   typeInfo<string>();
    cout << "int*:\n";     typeInfo<int*>();

    cout << clamp(15, 0, 10) << "\n";   // 10
    cout << clamp(-5, 0, 10) << "\n";   // 0
    cout << clamp(5.5, 0.0, 10.0) << "\n";  // 5.5

    return 0;
}

C++20 Concepts

Concepts constrain template parameters — better error messages and clearer intent:

cpp
#include <iostream>
#include <concepts>
#include <string>
using namespace std;

// Define a concept
template <typename T>
concept Numeric = is_arithmetic_v<T>;

template <typename T>
concept Printable = requires(T t) {
    { cout << t } -> same_as<ostream&>;
};

// Constrained function template
template <Numeric T>
T square(T x) { return x * x; }

// Constrained with 'requires'
template <typename T>
requires Printable<T> && (!is_pointer_v<T>)
void printValue(const T& val) {
    cout << "Value: " << val << "\n";
}

// Abbreviated function template (C++20)
auto multiply(Numeric auto a, Numeric auto b) {
    return a * b;
}

int main() {
    cout << square(5)     << "\n";   // 25
    cout << square(3.14)  << "\n";   // 9.8596

    // square("hello");  // Compile error — string is not Numeric

    printValue(42);
    printValue(3.14);
    printValue(string("Hello"));

    cout << multiply(3, 4)     << "\n";  // 12
    cout << multiply(2.5, 4.0) << "\n";  // 10

    return 0;
}

Practical Example: Generic Matrix

cpp
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;

template <typename T>
class Matrix {
    int                 rows, cols;
    vector<vector<T>>   data;

public:
    Matrix(int r, int c, T init = T{})
        : rows(r), cols(c), data(r, vector<T>(c, init)) {}

    T& operator()(int r, int c)             { return data[r][c]; }
    const T& operator()(int r, int c) const { return data[r][c]; }

    Matrix operator+(const Matrix& rhs) const {
        Matrix result(rows, cols);
        for (int r = 0; r < rows; r++)
            for (int c = 0; c < cols; c++)
                result(r, c) = data[r][c] + rhs.data[r][c];
        return result;
    }

    Matrix operator*(const Matrix& rhs) const {
        Matrix result(rows, rhs.cols);
        for (int r = 0; r < rows; r++)
            for (int c = 0; c < rhs.cols; c++)
                for (int k = 0; k < cols; k++)
                    result(r, c) += data[r][k] * rhs.data[k][c];
        return result;
    }

    // Scalar multiplication
    Matrix operator*(T scalar) const {
        Matrix result = *this;
        for (auto& row : result.data)
            for (auto& val : row)
                val *= scalar;
        return result;
    }

    void print() const {
        for (int r = 0; r < rows; r++) {
            cout << "[ ";
            for (int c = 0; c < cols; c++) {
                cout << setw(6) << fixed << setprecision(1) << data[r][c];
            }
            cout << " ]\n";
        }
    }
};

int main() {
    Matrix<double> A(2, 2);
    A(0,0) = 1; A(0,1) = 2;
    A(1,0) = 3; A(1,1) = 4;

    Matrix<double> B(2, 2);
    B(0,0) = 5; B(0,1) = 6;
    B(1,0) = 7; B(1,1) = 8;

    cout << "A * B:\n";
    (A * B).print();
    // [  19.0  22.0 ]
    // [  43.0  50.0 ]

    cout << "A * 2:\n";
    (A * 2.0).print();
    // [   2.0   4.0 ]
    // [   6.0   8.0 ]

    // Also works for integers!
    Matrix<int> C(2, 2, 0);
    C(0,0) = 1; C(0,1) = 0;
    C(1,0) = 0; C(1,1) = 1;
    // Identity matrix

    return 0;
}

Summary

  • Function templates are instantiated for each unique set of argument types — zero runtime overhead
  • Class templates parameterise entire classes — the basis of all STL containers
  • Template specialisation provides custom implementations for specific types or type categories
  • Variadic templates accept any number of type arguments — used for tuple, make_unique, and printf-style utilities
  • C++20 concepts constrain template parameters with readable, compiler-enforced requirements
  • <type_traits> provides compile-time type information: is_integral_v<T>, is_pointer_v<T>, etc.
  • Templates are resolved at compile time — template errors can produce long, cryptic messages; concepts improve this dramatically

Next, we'll learn about exception handling — how C++ reports and recovers from errors at runtime.