Templates in C++
Templates are C++'s mechanism for generic programming — writing code that works with any data type without duplicating logic. The C++ Standard Template Library (STL) that powers competitive programming on Codeforces, LeetCode, and GATE preparation is built almost entirely on templates.
When a Bangalore-based startup asks you to write a type-safe, reusable data structure in an interview, templates are the tool the interviewer expects you to reach for. Senior engineers at companies like Flipkart and Razorpay who earn ₹40–80 LPA routinely work with template-heavy codebases.
The Problem Templates Solve
Imagine writing a max() function for integers, then again for doubles, then again for string. Without templates:
int maxInt (int a, int b) { return a > b ? a : b; }
double maxDouble(double a, double b) { return a > b ? a : b; }
string maxString(string a, string b) { return a > b ? a : b; }
Three identical functions differing only in type. Templates collapse these into one.
Function Templates
A function template is a blueprint. When the compiler encounters a call with a concrete type, it instantiates a version of that function for that type.
#include <iostream>
using namespace std;
// T is a type parameter — replaced by the actual type at compile time
template <typename T>
T maxOf(T a, T b) {
return a > b ? a : b;
}
int main() {
cout << maxOf(10, 20) << endl; // T = int
cout << maxOf(3.14, 2.71) << endl; // T = double
cout << maxOf('z', 'a') << endl; // T = char
cout << maxOf(string("Ravi"), string("Anuj")) << endl; // T = string
return 0;
}
Output:
20
3.14
z
Ravi
The compiler generates four separate functions at compile time. There is no runtime type resolution — templates are entirely a compile-time feature (unlike virtual functions).
Multiple Template Parameters
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
int main() {
cout << add(3, 4.5) << endl; // int + double = double (7.5)
cout << add(10, 20) << endl; // int + int = int (30)
return 0;
}
decltype(a + b) deduces the return type from the expression, so mixing int and double gives double.
typename vs class in Template Parameters
Both keywords are interchangeable in template parameter lists. By convention, typename is preferred for clarity when the parameter represents a type:
template <typename T> // preferred
template <class T> // identical, older style
class carries no special meaning here — it does not require T to be a class type. The two are truly synonymous in this context.
Class Templates
Class templates let you create type-parameterised data structures. The STL containers vector, map, and pair are all class templates.
#include <iostream>
using namespace std;
template <typename T>
class Pair {
T first, second;
public:
Pair(T a, T b) : first(a), second(b) {}
T getFirst() const { return first; }
T getSecond() const { return second; }
void swap() { T temp = first; first = second; second = temp; }
void print() const {
cout << "(" << first << ", " << second << ")" << endl;
}
};
int main() {
Pair<int> p1(10, 20);
Pair<string> p2("Delhi", "Mumbai");
p1.print(); // (10, 20)
p2.print(); // (Delhi, Mumbai)
p1.swap();
p1.print(); // (20, 10)
return 0;
}
Member function definitions outside the class body need the template header repeated:
template <typename T>
void Pair<T>::swap() {
T temp = first;
first = second;
second = temp;
}
Template Specialisation
Sometimes the generic template is wrong for a specific type and you need a custom implementation.
Full Specialisation
A full specialisation provides a completely different implementation for one specific type.
#include <iostream>
#include <cstring>
using namespace std;
// Primary template
template <typename T>
class Printer {
public:
void print(T val) {
cout << "Value: " << val << endl;
}
};
// Full specialisation for const char* (C-style strings)
template <>
class Printer<const char*> {
public:
void print(const char* val) {
cout << "String (length " << strlen(val) << "): " << val << endl;
}
};
int main() {
Printer<int> pi; pi.print(42);
Printer<double> pd; pd.print(3.14);
Printer<const char*> ps; ps.print("Namaste India");
return 0;
}
Output:
Value: 42
Value: 3.14
String (length 13): Namaste India
Partial Specialisation
Partial specialisation applies to a subset of template arguments — for example, specialising for all pointer types.
// Primary template
template <typename T>
class TypeInfo {
public:
static void describe() {
cout << "Regular type" << endl;
}
};
// Partial specialisation: matches T* for any T
template <typename T>
class TypeInfo<T*> {
public:
static void describe() {
cout << "Pointer type" << endl;
}
};
// Partial specialisation for pair<T, T> (same type both slots)
template <typename T>
class TypeInfo<pair<T, T>> {
public:
static void describe() {
cout << "Homogeneous pair" << endl;
}
};
int main() {
TypeInfo<int>::describe(); // Regular type
TypeInfo<int*>::describe(); // Pointer type
TypeInfo<pair<int,int>>::describe();// Homogeneous pair
return 0;
}
Note: partial specialisation applies to class templates only. Function templates support only full specialisation; for functions, prefer overloading instead.
Non-Type Template Parameters
Template parameters do not have to be types. They can be compile-time constant values — integers, pointers to functions, enum values, and more.
#include <iostream>
using namespace std;
// N is a compile-time integer constant, not a type
template <typename T, int N>
class FixedArray {
T data[N]; // Stack-allocated array of size N
int size = 0;
public:
void push(T val) {
if (size < N) data[size++] = val;
}
T operator[](int i) const { return data[i]; }
int getSize() const { return size; }
int capacity() const { return N; }
};
int main() {
FixedArray<int, 5> arr;
arr.push(10);
arr.push(20);
arr.push(30);
cout << "Size: " << arr.getSize() << endl; // 3
cout << "Capacity: " << arr.capacity() << endl; // 5
cout << "arr[1]: " << arr[1] << endl; // 20
return 0;
}
std::array<T, N> from the STL uses exactly this pattern. The size is baked in at compile time, so there is no heap allocation and no size overhead.
Variadic Templates (C++11)
Variadic templates accept any number of type parameters. They are the foundation of std::tuple, std::make_unique, printf-like type-safe formatters, and modern logging frameworks.
#include <iostream>
using namespace std;
// Base case: zero arguments
void printAll() {
cout << endl;
}
// Recursive case: peel off first argument, recurse on the rest
template <typename First, typename... Rest>
void printAll(First first, Rest... rest) {
cout << first << " ";
printAll(rest...); // Recursive call with one fewer argument
}
int main() {
printAll(1, 2.5, "Hello", 'A', true);
// Output: 1 2.5 Hello A 1
return 0;
}
typename... Rest declares a parameter pack — zero or more type parameters. rest... expands the pack. The recursion terminates when no arguments remain and the base case is called.
Variadic Template: Sum of Any Types
template <typename T>
T sum(T val) { return val; }
template <typename T, typename... Args>
T sum(T first, Args... args) {
return first + sum(args...);
}
int main() {
cout << sum(1, 2, 3, 4, 5) << endl; // 15
cout << sum(1.1, 2.2, 3.3) << endl; // 6.6
cout << sum(string("TCS"), string(" India")) << endl; // TCS India
return 0;
}
C++17 introduced fold expressions that simplify this pattern considerably, but understanding the recursive variadic approach is a strong indicator of C++ depth in senior interviews.
Worked Example: Generic Stack Using Class Templates
A stack is a Last-In-First-Out (LIFO) data structure. Implementing it generically demonstrates mastery of class templates, non-type parameters, and good API design — all in one.
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T, int MaxSize = 100>
class Stack {
T data[MaxSize];
int topIndex;
public:
Stack() : topIndex(-1) {}
// Push an element onto the stack
void push(const T& val) {
if (isFull()) throw overflow_error("Stack overflow");
data[++topIndex] = val;
}
// Pop and return the top element
T pop() {
if (isEmpty()) throw underflow_error("Stack underflow");
return data[topIndex--];
}
// Peek at the top without removing
const T& peek() const {
if (isEmpty()) throw underflow_error("Stack is empty");
return data[topIndex];
}
bool isEmpty() const { return topIndex == -1; }
bool isFull() const { return topIndex == MaxSize - 1; }
int size() const { return topIndex + 1; }
// Print all elements (top to bottom)
void print() const {
cout << "Stack (top to bottom): ";
for (int i = topIndex; i >= 0; i--) {
cout << data[i] << " ";
}
cout << endl;
}
};
int main() {
// Integer stack with default capacity 100
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
intStack.print();
cout << "Top: " << intStack.peek() << endl;
cout << "Pop: " << intStack.pop() << endl;
intStack.print();
// String stack with capacity of 5
Stack<string, 5> nameStack;
nameStack.push("Arjun");
nameStack.push("Priya");
nameStack.push("Rahul");
nameStack.print();
// Double stack — same template, different type
Stack<double> priceStack;
priceStack.push(199.99);
priceStack.push(49.50);
priceStack.push(999.00);
priceStack.print();
// Use with a struct
struct Student { string name; int rank; };
Stack<Student, 10> toppers;
toppers.push({"Sneha", 1});
toppers.push({"Amit", 2});
Student s = toppers.pop();
cout << "Popped: " << s.name << " (Rank " << s.rank << ")" << endl;
return 0;
}
Output:
Stack (top to bottom): 30 20 10
Top: 30
Pop: 30
Stack (top to bottom): 20 10
Stack (top to bottom): Rahul Priya Arjun
Stack (top to bottom): 999 49.5 199.99
Popped: Amit (Rank 2)
The non-type parameter MaxSize = 100 provides a default that can be overridden — Stack<string, 5> creates a stack limited to 5 strings, stack-allocated with no heap overhead.
Template Instantiation and Compilation
Templates are compiled lazily — only the template code actually used in your program is compiled. If you write Stack<int> and Stack<string>, the compiler generates two separate classes. This is why template code almost always lives in header files: the compiler needs the full definition to instantiate it.
| Feature | Runtime Cost | Compile Time Cost |
|---|---|---|
| Virtual functions | One vtable lookup per call | Low |
| Function templates | None (fully resolved at compile time) | Higher (per instantiation) |
| Class templates | None | Higher (per instantiation) |
This trade-off — zero runtime cost but potentially longer compile times — is fundamental to understanding C++ performance at the systems level.
Common Pitfalls
-
Putting template definitions in
.cppfiles. The linker cannot find the instantiation at link time. Always put template definitions in header files (.hor.hpp), or use explicit instantiation. -
Missing
typenamein dependent names. When a template parameter appears in a qualified name (likeT::value_type), the compiler needstypename T::value_typeto know it is a type. Forgettingtypenamecauses confusing compiler errors. -
Infinite recursion in variadic templates. If the base case is missing or its overload is not found, the compiler reports an error about infinite template instantiation. Always provide a zero-argument base case.
-
Assuming
Tsupports all operators. A template function using<will fail to compile ifTdoes not defineoperator<. Document requirements or use C++20 Concepts to enforce them. -
Excessive code bloat. Each unique combination of template arguments generates new machine code. Instantiating
Stack<int>,Stack<double>,Stack<float>produces three copies of theStackcode. Design templates so that type-independent logic can be factored into non-template base classes.
Practice Exercises
-
Write a function template
bubbleSort(T arr[], int n)that sorts any array whose element type supports<. Test it withint,double, andstringarrays. -
Implement a class template
MinStack<T>that supportspush,pop,top, andgetMin— all in O(1) time. This is a popular interview problem at Amazon and Microsoft India. -
Write a full specialisation of
Stack<bool>that uses abitsetinternally to store booleans efficiently (one bit per element instead of onebool). -
Create a variadic template function
printCSV(args...)that prints its arguments separated by commas, followed by a newline. Handle the trailing comma carefully. -
Build a
Pair<T, U>class template (two different types) and overloadoperator<so that pairs sort lexicographically — first by the first element, then by the second. Use this to sort a list ofPair<string, int>(student name, GATE score) and find the topper.
Summary
- Templates provide compile-time generics — one definition, many types, zero runtime overhead.
- Function templates let you write a single algorithm that works for any compatible type.
- Class templates let you build type-parameterised data structures like
Stack<T>. typenameandclassare interchangeable in template parameter lists;typenameis preferred for clarity.- Full specialisation provides a completely different implementation for one specific type.
- Partial specialisation (class templates only) matches a family of types, like all pointer types.
- Non-type template parameters allow compile-time constants (sizes, flags) to be part of the type.
- Variadic templates (
typename... Args) accept any number of arguments and are the basis ofstd::tupleand type-safe printf. - Templates live in headers because the compiler needs the full definition at instantiation time.
- The cost of templates is paid at compile time, not at runtime — the generated code is as fast as hand-written type-specific code.