Templates and Generic Programming

Function templates, class templates, non-type template parameters, and a complete generic Stack<T> container.

Why templates

Without templates, writing a max function that works for both int and double would mean writing (and maintaining) two nearly identical functions differing only in their parameter types — or falling back to void* and casting, throwing away type safety entirely. Templates let you write one piece of code — a function or a whole class — parameterized over a type, and have the compiler generate a fully type-checked, specialized version for each concrete type it's actually used with, at compile time, with no runtime overhead at all. This is C++'s core mechanism for generic programming, and it's the foundation the entire Standard Template Library (covered on the next page) is built on.

Function templates

C++
#include <iostream>

template <typename T>
T maxOf(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maxOf(3, 7) << "\n";        // 7 — T deduced as int
    std::cout << maxOf(3.5, 2.1) << "\n";     // 3.5 — T deduced as double
    std::cout << maxOf('a', 'z') << "\n";     // z — T deduced as char
}

template <typename T> declares T as a placeholder type — typename and class are interchangeable here for this purpose (template <class T> means exactly the same thing). The compiler figures out what T actually is from the arguments at each call site (template argument deduction), so you almost never have to spell it out explicitly:

C++
std::cout << maxOf<double>(3, 7.5) << "\n"; // 7.5 — explicit T, forcing both args to double

Critically, maxOf<int> and maxOf<double> aren't the same function reused with different types at runtime — the compiler generates two entirely separate, independently type-checked functions at compile time (a process called template instantiation), one for each type it's actually invoked with. This is exactly why templates carry no runtime cost: by the time the program runs, there's no generic dispatch happening at all, just ordinary, fully-typed function calls that happen to have been generated from a shared source.

Class templates

A class template parameterizes an entire class over one or more types, so the same class definition works for any type that supports the operations the class actually needs:

C++
template <typename T>
class Box {
public:
    explicit Box(T value) : value_(value) {}

    T get() const { return value_; }
    void set(T value) { value_ = value; }

private:
    T value_;
};

int main() {
    Box<int> intBox(42);
    Box<std::string> stringBox("hello");

    std::cout << intBox.get() << "\n";     // 42
    std::cout << stringBox.get() << "\n";   // hello
}

Box<int> and Box<std::string> are, again, genuinely distinct compiler-generated types — intBox and stringBox don't share a runtime representation of any kind, and the compiler catches any type mismatch (calling intBox.set("oops"), for instance) as an ordinary compile error, exactly as if Box<int> had been hand-written specifically for int.

A complete example: a generic stack

Putting function templates and a class template together, here's a genuinely useful generic container — a fixed-capacity stack that works for any element type:

C++
#include <iostream>
#include <stdexcept>
#include <vector>

template <typename T>
class Stack {
public:
    void push(const T& value) {
        items_.push_back(value);
    }

    T pop() {
        if (items_.empty()) {
            throw std::out_of_range("pop() called on an empty Stack");
        }
        T top = items_.back();
        items_.pop_back();
        return top;
    }

    bool isEmpty() const {
        return items_.empty();
    }

    size_t size() const {
        return items_.size();
    }

private:
    std::vector<T> items_;
};

int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    std::cout << intStack.size() << "\n";  // 3
    std::cout << intStack.pop() << "\n";   // 3 — last in, first out
    std::cout << intStack.pop() << "\n";   // 2

    Stack<std::string> stringStack;
    stringStack.push("first");
    stringStack.push("second");
    std::cout << stringStack.pop() << "\n"; // second

    Stack<int> empty;
    try {
        empty.pop(); // throws — empty.isEmpty() would have avoided this
    } catch (const std::out_of_range& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
}

This one Stack<T> definition works correctly, with full compile-time type checking, for int, std::string, or any other type — including types you write yourself — with zero code duplication and zero runtime type-checking overhead. Notice it's implemented on top of std::vector<T> internally, itself a class template — this layered reuse of generic components is the normal, idiomatic way generic C++ code is built.

Non-type template parameters

A template parameter doesn't have to be a type — it can be a compile-time constant value, most commonly used for a fixed size known up front:

C++
template <typename T, size_t N>
class FixedArray {
public:
    T& operator[](size_t index) { return data_[index]; }
    size_t size() const { return N; }

private:
    T data_[N];
};

int main() {
    FixedArray<int, 5> arr;   // N = 5 baked in at compile time, part of the TYPE itself
    arr[0] = 10;
    std::cout << arr.size() << "\n"; // 5
}

FixedArray<int, 5> and FixedArray<int, 10> are different types entirely, each with its size fixed and known at compile time — this is exactly how std::array<T, N> (covered on the STL page) is implemented under the hood.

Common mistakes

  • Writing a template function's implementation in a .cpp file rather than the header — templates are typically instantiated at compile time wherever they're used, so a template's full definition normally needs to be visible in every translation unit that uses it, which means it belongs in the header, not a separately-compiled source file.
  • Assuming a class template is one single compiled entity — Box<int> and Box<double> are entirely separate types generated independently, each fully type-checked on its own; a bug that only shows up for one instantiation won't necessarily show up for another.
  • Forgetting typename/class are interchangeable in a template parameter list, and being confused when seeing both used across different codebases for exactly the same purpose.
  • Reaching for a template when a simple base class and virtual functions (runtime polymorphism) would model the problem more naturally — templates buy compile-time genericity with zero runtime cost, but at the cost of every distinct instantiation being compiled separately, which can noticeably increase build times and binary size in a large project.

Interview questions

Q: What actually happens when you call maxOf<int>(3, 7) versus maxOf<double>(3.5, 2.1) — is it the same function running twice? No — the compiler generates two entirely separate, fully type-checked functions at compile time, one instantiated for int and one for double (a process called template instantiation), each independently compiled as if it had been hand-written for that specific type. This is exactly why templates have zero runtime overhead: by the time the program actually runs, there's no generic dispatch happening at all, only ordinary calls to concrete, already-generated functions.

Q: Why does a class template's implementation usually need to live in the header file rather than a separate .cpp file? Because the compiler needs to see a template's full definition at the point where it's instantiated for a particular type, and that instantiation can happen in any translation unit that uses the template — not just the one where it was originally written. Splitting the implementation into a separately-compiled .cpp file (the normal pattern for ordinary, non-template classes) would mean other files including only the header have no definition to instantiate from, producing a linker error.