C++ Class Templates

A class template lets you write a single class definition that works with any data type, instead of writing nearly identical classes for int, double, std::string, and so on. The compiler generates a concrete class for each type you actually use, at compile time, with zero runtime overhead. Class templates are the foundation of generic containers like std::vector, std::map, and std::stack, and understanding them is essential for writing reusable, type-safe C++ code.

Overview: What Is a Class Template?

A class template is a blueprint for a class where one or more types (or values) are left as parameters, to be filled in later. When you write template <typename T> class Box { T value; };, you have not created a class yet — you have created a recipe for creating classes. No code is generated, and no memory is allocated, until you actually use the template with a specific type, such as Box<int> or Box<std::string>. This process is called template instantiation.

Internally, the compiler treats each distinct instantiation as a completely separate, independent class. Box<int> and Box<std::string> share no code, no static members, and no relationship to each other beyond having been generated from the same source pattern. This is different from inheritance or polymorphism — there is no common base class connecting them unless you explicitly create one. The compiler essentially performs a textual substitution of T with the concrete type, then compiles the resulting class as if you had typed it out by hand.

Because instantiation happens at compile time, all the type-checking is done statically: if you try to use an operation that the type T doesn’t support (for example, calling .push_back() on a type with no such method), you get a compile error at the point of instantiation, not a runtime crash. This is one of the biggest advantages of templates over older techniques like using void* pointers for generic containers: templates are fully type-safe and just as fast as hand-written code, because there is no boxing, no casting, and no dynamic dispatch involved.

Class templates can take more than one type parameter (like std::map<Key, Value>), can mix type parameters with non-type parameters (like a fixed-size array’s length), and can have default values for their parameters, similar to default function arguments.

Syntax

template <typename T>
class ClassName {
public:
    ClassName(T value);
    T getValue() const;
private:
    T data;
};
  • template <typename T> — declares that this class definition depends on a placeholder type named T. You can use class instead of typename here; they are interchangeable in this context.
  • T — a conventional name for the template parameter; you can name it anything, but T, U, Key, and Value are common conventions.
  • ClassName<T> — inside the class body and in out-of-class definitions, the class must be referred to with its template parameter attached.
  • Multiple parameters are separated by commas: template <typename T1, typename T2>.
  • Non-type parameters are also allowed: template <typename T, int Size> lets you pass a compile-time constant like an array length.
  • To use the class, you supply concrete types in angle brackets: ClassName<int> obj(5);.

Examples

Example 1: A Simple Generic Box

#include <iostream>
using namespace std;

template <typename T>
class Box {
private:
    T value;
public:
    Box(T v) : value(v) {}
    T getValue() const { return value; }
    void setValue(T v) { value = v; }
};

int main() {
    Box<int> intBox(42);
    Box<string> strBox("Hello");

    cout << "intBox: " << intBox.getValue() << endl;
    cout << "strBox: " << strBox.getValue() << endl;

    intBox.setValue(100);
    cout << "intBox after update: " << intBox.getValue() << endl;

    return 0;
}

Output:

intBox: 42
strBox: Hello
intBox after update: 100

Here the compiler generates two separate classes behind the scenes: one where every T is replaced with int, and one where every T is replaced with string. Both share the exact same source but are compiled as distinct, unrelated types — you could not assign an intBox to a variable of type Box<string>.

Example 2: Multiple Type Parameters and Out-of-Class Definitions

#include <iostream>
using namespace std;

template <typename T1, typename T2>
class Pair {
private:
    T1 first;
    T2 second;
public:
    Pair(T1 a, T2 b) : first(a), second(b) {}
    void display() const;
    T1 getFirst() const { return first; }
    T2 getSecond() const { return second; }
};

template <typename T1, typename T2>
void Pair<T1, T2>::display() const {
    cout << "(" << first << ", " << second << ")" << endl;
}

int main() {
    Pair<int, double> p1(3, 4.5);
    Pair<string, char> p2("grade", 'A');

    p1.display();
    p2.display();

    cout << "First of p1: " << p1.getFirst() << endl;

    return 0;
}

Output:

(3, 4.5)
(grade, A)
First of p1: 3

This example shows two important things. First, a class template can take several independent type parameters — Pair<int, double> and Pair<string, char> are both valid, fully independent instantiations. Second, it shows how to define a member function outside the class body: you must repeat template <typename T1, typename T2> immediately before the definition, and qualify the function name with Pair<T1, T2>:: rather than just Pair::.

Example 3: A Generic Stack With Error Handling

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

template <typename T>
class Stack {
private:
    vector<T> elements;
public:
    void push(const T& item) {
        elements.push_back(item);
    }

    void pop() {
        if (elements.empty()) {
            throw runtime_error("Stack is empty");
        }
        elements.pop_back();
    }

    T top() const {
        if (elements.empty()) {
            throw runtime_error("Stack is empty");
        }
        return elements.back();
    }

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

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

int main() {
    Stack<int> numbers;
    numbers.push(10);
    numbers.push(20);
    numbers.push(30);

    cout << "Stack size: " << numbers.size() << endl;
    cout << "Top element: " << numbers.top() << endl;

    numbers.pop();
    cout << "Top after pop: " << numbers.top() << endl;

    Stack<string> words;
    words.push("hello");
    words.push("world");
    cout << "Top word: " << words.top() << endl;

    try {
        Stack<int> empty;
        empty.pop();
    } catch (const runtime_error& e) {
        cout << "Caught exception: " << e.what() << endl;
    }

    return 0;
}

Output:

Stack size: 3
Top element: 30
Top after pop: 20
Top word: world
Caught exception: Stack is empty

This is a much more realistic use of class templates: a reusable Stack<T> built on top of std::vector<T>, which itself is a class template. It works identically for int and string with no code duplication, and it demonstrates that a class template can safely hold any type that supports the operations the template needs (here, just copyability, which both int and string support).

How It Works Under the Hood

When the compiler encounters Stack<int> numbers;, it looks at the template definition and substitutes every occurrence of T with int, effectively generating a brand-new class named something like Stack<int> internally. It then compiles that generated class exactly as if you had written it by hand, checking every member function against the substituted type. If you never call pop() on a particular instantiation, the compiler typically won’t even try to compile the body of pop() for that type — this is why templates can technically contain code that wouldn’t compile for every possible type, as long as you never instantiate that particular member function with an incompatible type.

This substitution process is why template definitions almost always live in header files rather than .cpp files. The compiler needs to see the full template definition at the point where it is instantiated (e.g., in your main.cpp), not just a declaration. If the definition is hidden in a separate .cpp file that isn’t included, the compiler has nothing to instantiate from, and you get a linker error (“undefined reference”) instead of a compile error.

Each distinct combination of template arguments produces its own compiled machine code — Stack<int> and Stack<string> are stored as entirely separate sets of instructions in the final binary. This is sometimes called code bloat: heavy use of templates with many different types can increase executable size, though modern linkers merge identical instantiations across translation units to reduce duplication.

Common Mistakes

Mistake 1: Forgetting the template prefix on out-of-class definitions. Every member function defined outside the class body needs the template parameter list repeated, and the class name must include its template argument.

template <typename T>
class Box {
public:
    Box(T v);
    T getValue() const;
private:
    T value;
};

// Wrong: missing "template <typename T>" and wrong qualification
Box::Box(T v) : value(v) {}

The corrected version repeats the template declaration and qualifies the constructor with Box<T>:::

template <typename T>
Box<T>::Box(T v) : value(v) {}

Mistake 2: Splitting template declarations and definitions across a header and a .cpp file, the way you would with an ordinary (non-template) class. This compiles fine on its own but fails to link because the template body isn’t visible wherever the class is used.

// Box.h
template <typename T>
class Box {
public:
    Box(T v);
    T getValue() const;
private:
    T value;
};

// Box.cpp -- definitions placed here, separately compiled
template <typename T>
Box<T>::Box(T v) : value(v) {}

template <typename T>
T Box<T>::getValue() const { return value; }

When main.cpp includes only Box.h and writes Box<int> b(5);, the linker cannot find the generated code because it was compiled in a different translation unit without ever being instantiated there. The fix is to keep the full member function definitions in the header (either inline in the class body, or below it in the same .h file), so every file that includes the header can instantiate the template itself.

Best Practices

  • Keep the entire class template — declaration and member function definitions — in a single header file, unless you’re using explicit template instantiation (an advanced technique for large projects).
  • Use meaningful parameter names like Key and Value for multi-parameter templates instead of always defaulting to T1, T2, especially for public-facing library code.
  • Prefer const T& parameters for functions that accept but don’t modify a template value, to avoid unnecessary copies for expensive types like std::string or std::vector.
  • Constrain what operations your template assumes a type supports, and document them, since template error messages for unsupported types can be long and hard to read.
  • Reach for the standard library’s own templates (std::vector, std::pair, std::optional) before writing your own, unless you have a genuinely different requirement — they’re heavily tested and optimized.
  • Use default template arguments (template <typename T = int>) sparingly, and only when there’s an obviously sensible default.

Practice Exercises

  1. Write a class template Triple<T> that stores three values of the same type and has a method sum() that returns their total (assume T supports +). Test it with Triple<int> and Triple<double>.
  2. Write a class template Queue<T> backed by std::vector<T> with enqueue, dequeue, and front methods, throwing an exception when dequeuing from an empty queue. Verify it works for both int and std::string.
  3. Write a class template KeyValue<K, V> with two type parameters, a constructor, and a member function print() defined outside the class body that prints the key and value. Instantiate it once as KeyValue<string, int> and once as KeyValue<int, double>.

Summary

  • A class template is a blueprint for a family of classes, parameterized by one or more types (and optionally non-type values).
  • No code is generated until the template is instantiated with concrete types, and each instantiation produces a fully independent, unrelated class.
  • Templates give you compile-time type safety and zero runtime overhead compared to generic containers built on void* or dynamic dispatch.
  • Member functions defined outside the class body must repeat the template <...> declaration and qualify the class name with its parameters, e.g. Box<T>::getValue().
  • Template definitions must be visible at the point of instantiation, which is why they almost always live entirely in header files.
  • Prefer the standard library’s existing templates when they meet your needs, and reserve custom templates for genuinely new requirements.