Basic Class Template in C++: Creating Generic Classes Step by Step

Basic Class Template in C++

Basic Class Template in C++

The moment class templates clicked for me was when I stopped thinking of std::vector<int> and std::vector<std::string> as two flavors of the same thing, and started seeing them as two entirely different, compiler-generated classes that just happen to share a single source blueprint. That mental shift is really the whole story of class templates: you write the structure and behavior of a class exactly once, using a placeholder for the type, and the compiler stamps out a fully concrete, independent class for every type you actually use.

In this guide I’ll build a class template step by step — starting from a minimal generic Box<T>, through a proper generic Stack<T> with dynamic memory management, and into multi-parameter and specialized templates. I’ll explain what the compiler is doing at each stage, cover the rule of five for template classes managing memory, and go through the debugging pitfalls that show up most often when people move from function templates to class templates.

What Is a Class Template?

A class template is a pattern for generating classes, parameterized on one or more types (or values). Instead of hand-writing a Box class for every type you need to store, you write it once:

#include <iostream>

template<typename T>
class Box {
private:
    T value;

public:
    Box(const T& val) : value(val) {}

    T getValue() const {
        return value;
    }

    void setValue(const T& val) {
        value = val;
    }
};

int main() {
    Box<int> intBox(42);
    Box<std::string> stringBox("Hello, Templates!");

    std::cout << intBox.getValue() << "\n";
    std::cout << stringBox.getValue() << "\n";

    intBox.setValue(100);
    std::cout << "Updated: " << intBox.getValue() << "\n";

    return 0;
}

Output:

42
Hello, Templates!
Updated: 100

Box<int> and Box<std::string> are, to the compiler, two completely separate and unrelated classes. They don’t share a vtable, don’t share static members, and one can’t be implicitly converted to the other. They simply both originate from the same template definition.

Anatomy of a Class Template Declaration

template<typename T>   // template parameter list
class ClassName {      // the templated class body
    T member;           // T can be used anywhere a type is expected
public:
    ClassName(T val);   // constructors, methods, etc. can all use T
};

You can use class instead of typename in the parameter list (template<class T>) — they are functionally identical in this position; typename is generally preferred by convention for clarity, though both appear constantly in real codebases and the standard library itself uses both historically.

Defining Member Functions Outside the Class

When member functions are defined outside the class body, you must repeat the template parameter list and qualify the class name with <T>:

#include <iostream>

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

// Out-of-class definitions
template<typename T>
Box<T>::Box(const T& val) : value(val) {}

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

int main() {
    Box<double> b(3.14159);
    std::cout << b.getValue() << "\n";
    return 0;
}

Output:

3.14159

This is important to internalize early: Box<T>::Box and Box<T>::getValue are not standalone functions — they’re still part of the template and get instantiated together with the rest of the class whenever a particular T is used.

Building a Complete Generic Class: A Dynamic Stack

Let’s build something with real state and memory management, since that’s where class templates actually earn their keep. Here’s a generic Stack<T> backed by a dynamically-resized array.

#include <iostream>
#include <stdexcept>

template<typename T>
class Stack {
private:
    T* data;
    size_t capacity;
    size_t count;

    void resize(size_t newCapacity) {
        T* newData = new T[newCapacity];
        for (size_t i = 0; i < count; ++i) {
            newData[i] = data[i];
        }
        delete[] data;
        data = newData;
        capacity = newCapacity;
    }

public:
    explicit Stack(size_t initialCapacity = 4)
        : data(new T[initialCapacity]), capacity(initialCapacity), count(0) {}

    // Rule of five: destructor
    ~Stack() {
        delete[] data;
    }

    // Copy constructor
    Stack(const Stack& other)
        : data(new T[other.capacity]), capacity(other.capacity), count(other.count) {
        for (size_t i = 0; i < count; ++i) {
            data[i] = other.data[i];
        }
    }

    // Copy assignment
    Stack& operator=(const Stack& other) {
        if (this == &other) return *this;
        T* newData = new T[other.capacity];
        for (size_t i = 0; i < other.count; ++i) {
            newData[i] = other.data[i];
        }
        delete[] data;
        data = newData;
        capacity = other.capacity;
        count = other.count;
        return *this;
    }

    // Move constructor
    Stack(Stack&& other) noexcept
        : data(other.data), capacity(other.capacity), count(other.count) {
        other.data = nullptr;
        other.capacity = 0;
        other.count = 0;
    }

    // Move assignment
    Stack& operator=(Stack&& other) noexcept {
        if (this == &other) return *this;
        delete[] data;
        data = other.data;
        capacity = other.capacity;
        count = other.count;
        other.data = nullptr;
        other.capacity = 0;
        other.count = 0;
        return *this;
    }

    void push(const T& value) {
        if (count == capacity) {
            resize(capacity * 2);
        }
        data[count++] = value;
    }

    void pop() {
        if (count == 0) {
            throw std::out_of_range("Stack::pop(): stack is empty");
        }
        --count;
    }

    T& top() {
        if (count == 0) {
            throw std::out_of_range("Stack::top(): stack is empty");
        }
        return data[count - 1];
    }

    bool isEmpty() const { return count == 0; }
    size_t size() const { return count; }
};

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

    std::cout << "Top: " << intStack.top() << "\n";
    std::cout << "Size: " << intStack.size() << "\n";

    intStack.pop();
    std::cout << "After pop, top: " << intStack.top() << "\n";

    Stack<std::string> stringStack;
    stringStack.push("first");
    stringStack.push("second");
    std::cout << "String stack top: " << stringStack.top() << "\n";

    // Test copy
    Stack<int> copyStack = intStack;
    copyStack.push(99);
    std::cout << "Original top: " << intStack.top() << ", Copy top: " << copyStack.top() << "\n";

    return 0;
}

Output:

Top: 30
Size: 3
After pop, top: 20
String stack top: second
Original top: 20, Copy top: 99

This example demonstrates why the rule of five (destructor, copy constructor, copy assignment, move constructor, move assignment) matters especially for class templates that manage a resource like a raw dynamic array — without it, copying a Stack<T> would shallow-copy the pointer and cause a double-free when both stacks were destroyed.

Multiple Template Parameters

Class templates aren’t limited to one type parameter. A generic key-value pair, for instance:

#include <iostream>
#include <string>

template<typename K, typename V>
class KeyValuePair {
private:
    K key;
    V value;

public:
    KeyValuePair(const K& k, const V& v) : key(k), value(v) {}

    K getKey() const { return key; }
    V getValue() const { return value; }

    void display() const {
        std::cout << key << " => " << value << "\n";
    }
};

int main() {
    KeyValuePair<std::string, int> p1("age", 30);
    KeyValuePair<int, std::string> p2(101, "Engineering");

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

    return 0;
}

Output:

age => 30
101 => Engineering

Default Template Arguments

Just like function default parameters, class templates can have default type arguments:

#include <iostream>

template<typename T = int, int Size = 10>
class FixedArray {
private:
    T elements[Size];

public:
    void set(int index, const T& val) {
        if (index >= 0 && index < Size) elements[index] = val;
    }
    T get(int index) const { return elements[index]; }
    constexpr int getSize() const { return Size; }
};

int main() {
    FixedArray<> defaultArray;                // T=int, Size=10
    FixedArray<double, 5> customArray;        // T=double, Size=5

    defaultArray.set(0, 42);
    customArray.set(0, 3.14);

    std::cout << "Default array size: " << defaultArray.getSize() << ", val[0]: " << defaultArray.get(0) << "\n";
    std::cout << "Custom array size: " << customArray.getSize() << ", val[0]: " << customArray.get(0) << "\n";

    return 0;
}

Output:

Default array size: 10, val[0]: 42
Custom array size: 5, val[0]: 3.14

Note the second parameter, int Size, is a non-type template parameter — a compile-time constant value rather than a type. This is exactly the mechanism std::array<T, N> is built on.

Template Specialization

Sometimes the generic implementation is wrong (or inefficient) for a particular type. Full specialization lets you override the entire class body for one specific type:

#include <iostream>

template<typename T>
class Printer {
public:
    void print(const T& value) {
        std::cout << "Value: " << value << "\n";
    }
};

// Full specialization for bool
template<>
class Printer<bool> {
public:
    void print(const bool& value) {
        std::cout << "Boolean: " << (value ? "true" : "false") << "\n";
    }
};

int main() {
    Printer<int> intPrinter;
    Printer<bool> boolPrinter;

    intPrinter.print(42);
    boolPrinter.print(true);

    return 0;
}

Output:

Value: 42
Boolean: true

Partial specialization lets you specialize for a category of types, such as all pointer types, while still being generic in another respect:

#include <iostream>

template<typename T>
class TypeDescriber {
public:
    static void describe() {
        std::cout << "A regular type\n";
    }
};

// Partial specialization for pointer types
template<typename T>
class TypeDescriber<T*> {
public:
    static void describe() {
        std::cout << "A pointer type\n";
    }
};

int main() {
    TypeDescriber<int>::describe();
    TypeDescriber<int*>::describe();
    return 0;
}

Output:

A regular type
A pointer type

Note: partial specialization is only available for class templates, not function templates — this is a common point of confusion. Function templates can only be overloaded, not partially specialized.

Class Template Argument Deduction (CTAD, C++17)

Before C++17, you always had to specify template arguments explicitly when constructing an object: Box<int> b(5);. C++17 introduced CTAD, which lets the compiler deduce the type parameters from constructor arguments, much like it already did for function templates:

#include <iostream>

template<typename T>
class Box {
    T value;
public:
    Box(T val) : value(val) {}
    T get() const { return value; }
};

int main() {
    Box b1(42);          // CTAD deduces Box<int>
    Box b2(3.14);        // CTAD deduces Box<double>
    Box b3("hello");     // CTAD deduces Box<const char*>

    std::cout << b1.get() << "\n";
    std::cout << b2.get() << "\n";
    std::cout << b3.get() << "\n";

    return 0;
}

Output:

42
3.14
hello

If the deduced type isn’t what you want (as with Box<const char*> above, when you probably wanted Box<std::string>), you can write a deduction guide to steer CTAD:

template<typename T>
class Box {
    T value;
public:
    Box(T val) : value(val) {}
};

Box(const char*) -> Box<std::string>; // deduction guide (needs a matching constructor or conversion in practice)

Internal Working and Compiler Process

  1. Instantiation on use. The compiler doesn’t generate any code for Box<T> in the abstract — it waits until you actually write Box<int> or Box<std::string> somewhere, and only then generates the concrete class, including only the member functions you actually call (member functions of class templates are themselves lazily instantiated, one at a time, unlike the class’s data members which are all instantiated together).
  2. Two-phase name lookup. Similar to function templates, the compiler checks non-dependent code at template definition time, and dependent code (anything relying on T) at instantiation time. This is why a class template can be defined and compiled into an object file’s template metadata without ever having its T-dependent logic type-checked against a concrete type until instantiation.
  3. Independent static members per instantiation. Every distinct instantiation of a class template gets its own, entirely separate set of static members:
#include <iostream>

template<typename T>
class Counter {
public:
    static int count;
    Counter() { ++count; }
};

template<typename T>
int Counter<T>::count = 0;

int main() {
    Counter<int> a, b;
    Counter<double> c;

    std::cout << "Counter<int>::count = " << Counter<int>::count << "\n";
    std::cout << "Counter<double>::count = " << Counter<double>::count << "\n";

    return 0;
}

Output:

Counter<int>::count = 2
Counter<double>::count = 1

This confirms Counter<int> and Counter<double> really are unrelated classes as far as static state is concerned — a frequent source of surprise for people expecting templates to behave like generics in languages with type erasure.

  1. Memory layout is fixed and concrete per instantiation. sizeof(Box<int>) and sizeof(Box<double>) will differ, exactly as if you’d hand-written two separate classes — there’s no hidden indirection or boxing.

Best Practices

Constraining Class Templates with Concepts (C++20)

#include <iostream>
#include <concepts>

template<typename T>
concept Comparable = requires(T a, T b) {
    { a < b } -> std::convertible_to<bool>;
};

template<Comparable T>
class SortedPair {
    T first, second;
public:
    SortedPair(T a, T b) {
        if (a < b) { first = a; second = b; }
        else { first = b; second = a; }
    }
    void display() const {
        std::cout << first << ", " << second << "\n";
    }
};

int main() {
    SortedPair<int> p(5, 2);
    p.display();
    return 0;
}

Output:

2, 5

If you tried to instantiate SortedPair with a type lacking operator<, the compiler produces a clear “constraint not satisfied” error instead of a deep, confusing failure buried inside the class body — a significant readability win over pre-C++20 SFINAE-based constraints.

Performance Optimization

Class templates carry no inherent runtime overhead versus hand-written classes — each instantiation compiles to concrete, non-generic machine code. The performance considerations that matter in practice:

// In a .cpp file:
template class Stack<int>;
template class Stack<double>;

This tells the compiler to generate Stack<int> and Stack<double> exactly once in this translation unit, which other translation units can then link against instead of each instantiating their own copies (useful in large projects to reduce build time and binary size).

Debugging Common Mistakes

Mistake 1: Forgetting to repeat the template parameter list on out-of-class definitions

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

// WRONG:
// T Box::get() const { return value; }

// RIGHT:
template<typename T>
T Box<T>::get() const { return value; }

Mistake 2: Shallow copy bugs from missing the rule of five If a class template owns a raw pointer and you don’t define a copy constructor/assignment, the compiler-generated defaults will shallow-copy the pointer, leading to double-free or use-after-free bugs the moment two instances are destroyed.

Mistake 3: Assuming a template class instantiation is generated even if unused

template<typename T>
class Thing {
public:
    void onlyValidForNumbers() { T x = 5; /* ... */ }
};

Thing<std::string> t; // fine, as long as onlyValidForNumbers() is never called
// t.onlyValidForNumbers(); // would fail to compile: std::string x = 5; is invalid

Member functions of class templates are instantiated lazily, individually, only when called — this can hide bugs until a specific method is actually invoked with a specific type.

Mistake 4: Confusing class template parameter keyword with actual class inheritance. template<class T> does not mean T must be a class type — it can be any type, including built-ins like int. class and typename are interchangeable here.

Real-World Applications

Interview Questions

  1. What is the difference between a class template and a template instantiation?
  2. Why must you repeat the template parameter list when defining member functions outside the class body?
  3. Are static members shared across different instantiations of the same class template? Explain why or why not.
  4. What is the difference between full and partial specialization of a class template?
  5. Why is the rule of five especially important for class templates managing raw resources?
  6. What is CTAD, and how do deduction guides influence it?
  7. Can class templates have non-type template parameters? Give an example.
  8. Why can function templates be overloaded but not partially specialized, while class templates can be partially specialized?

FAQs

Q: Can a class template inherit from a non-template class, or vice versa? Yes to both — templates can freely mix with ordinary inheritance, as long as you’re careful about accessing dependent names (you may need this-> or typename Base<T>::member in some inheritance scenarios involving templated base classes).

Q: Do class templates support virtual functions? Yes. A class template can declare virtual member functions normally; each instantiation gets its own independent vtable.

Q: Is there a difference between template<typename T> and template<class T> for class templates? No functional difference in this position — both declare a type template parameter. typename is often preferred by convention for readability, though you’ll see both extensively in real code and in the standard library’s own historical usage.

Q: How do I limit which types a class template accepts? Use static_assert inside the class body for a basic compile-time check, std::enable_if with SFINAE pre-C++20, or concepts in C++20 for the clearest, most maintainable constraint syntax.

Q: Can I explicitly instantiate a class template to speed up builds? Yes — template class ClassName<Type>; in a single translation unit forces instantiation there, avoiding redundant instantiation across every other file that uses the same type.

Troubleshooting Tips

Summary and Key Takeaways

Class templates let you define a class exactly once and have the compiler generate independent, fully concrete versions for every type you actually use — with the same performance characteristics as if you’d hand-written each version separately. The concepts that matter most in practice are: instantiation happens on demand and per-member-function; every distinct type parameter produces an unrelated class with its own static state and memory layout; and any class template that manages a resource directly needs the rule of five to avoid shallow-copy bugs. Combined with non-type parameters, specialization, CTAD, and — since C++20 — concepts for clean constraints, class templates are the mechanism behind essentially every container and smart pointer in the C++ Standard Library, and they’re a foundational skill for writing reusable, type-safe C++ code.

References

Exit mobile version