Skip to content

C++ Templates

Templates are a fundamental feature of C++ enabling generic programming. They allow you to write code (functions or classes) that can operate on different data types without needing to rewrite the code for each type.

Think of a template as a blueprint. You define the structure or algorithm once, using placeholder type names. When you use the template with a specific type (like int or std::string), the compiler generates the actual code for that specific type based on the blueprint.

The C++ Standard Library heavily relies on templates. Containers like std::vector, std::list, and std::map, as well as algorithms like std::sort and std::find, are all template-based. This allows you to have std::vector<int>, std::vector<double>, or std::vector<MyClass> using the same underlying std::vector template code.

Function templates define a generic function that can operate on various data types. The syntax typically looks like this:

template <typename T> // or template <class T>
return_type function_name(parameter_list) {
// Function body using type T
}

Here, typename T (or class T) declares T as a template parameter, a placeholder for a type. You can use T within the function signature and body as if it were a specific type.

Example: A function template to find the maximum of two values.

#include <iostream>
#include <string>
// Define a function template named 'maximum'
// It takes two constant references of type T and returns a constant reference of type T
template <typename T>
const T& maximum(const T& a, const T& b) {
// Assumes type T supports the less than operator (<)
return (a < b) ? b : a;
}
int main() {
int i = 39;
int j = 20;
// Compiler generates maximum<int>(const int&, const int&)
std::cout << "Max(i, j): " << maximum(i, j) << std::endl;
double f1 = 13.5;
double f2 = 20.7;
// Compiler generates maximum<double>(const double&, const double&)
std::cout << "Max(f1, f2): " << maximum(f1, f2) << std::endl;
std::string s1 = "Hello";
std::string s2 = "World";
// Compiler generates maximum<std::string>(const std::string&, const std::string&)
std::cout << "Max(s1, s2): " << maximum(s1, s2) << std::endl;
// You can also explicitly specify the type:
// std::cout << "Max(i, j): " << maximum<int>(i, j) << std::endl;
return 0;
}

Output:

Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World

The compiler deduces the type T from the arguments passed to maximum. Using const T& for parameters is efficient as it avoids copying potentially large objects.

Class templates allow you to define generic classes. The syntax is similar to function templates:

template <typename T1, typename T2, ...> // or class T1, class T2, ...
class ClassName {
// Class members using T1, T2, etc.
private:
T1 member1;
T2 member2;
public:
ClassName(T1 val1, T2 val2);
void someMethod(T1 arg);
// ...
};

Here, T1, T2, etc., are placeholder types specified when an object of the class is created.

Example: A generic Stack class template.

#include <iostream>
#include <vector>
#include <stdexcept> // For std::out_of_range
#include <string>
#include <utility> // For std::move
template <typename T>
class Stack {
private:
std::vector<T> elems; // Internal storage using std::vector
public:
// Push element by copying
void push(const T& elem) {
elems.push_back(elem);
}
// Push element by moving (C++11)
void push(T&& elem) {
elems.push_back(std::move(elem)); // Efficient for movable types
}
// Pop (remove) the top element
void pop() {
if (empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back();
}
// Return a const reference to the top element
const T& top() const {
if (empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems.back();
}
// Return a mutable reference to the top element
T& top() {
if (empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems.back();
}
// Check if the stack is empty
[[nodiscard]] bool empty() const noexcept { // C++11 noexcept, C++17 nodiscard
return elems.empty();
}
// Get the current size of the stack
[[nodiscard]] size_t size() const noexcept {
return elems.size();
}
};
int main() {
try {
Stack<int> intStack; // Stack of integers
Stack<std::string> stringStack; // Stack of strings
// Manipulate int stack
std::cout << "Pushing 7 onto intStack..." << std::endl;
intStack.push(7);
std::cout << "Top element: " << intStack.top() << std::endl;
std::cout << "Pushing 15 onto intStack..." << std::endl;
intStack.push(15);
std::cout << "Top element: " << intStack.top() << std::endl;
std::cout << "Popping intStack..." << std::endl;
intStack.pop();
std::cout << "Top element: " << intStack.top() << std::endl;
// Manipulate string stack
std::cout << "\nPushing \"hello\" onto stringStack..." << std::endl;
stringStack.push("hello");
std::cout << "Top element: " << stringStack.top() << std::endl;
std::string world = "world";
std::cout << "Pushing \"world\" onto stringStack..." << std::endl;
stringStack.push(std::move(world)); // Move semantics example
std::cout << "Top element: " << stringStack.top() << std::endl;
std::cout << "Is 'world' string empty now? " << (world.empty() ? "Yes" : "No") << std::endl;
std::cout << "Popping stringStack twice..." << std::endl;
stringStack.pop();
stringStack.pop();
// This will throw an exception
std::cout << "Attempting to pop empty stringStack..." << std::endl;
stringStack.pop();
} catch (const std::out_of_range& ex) {
std::cerr << "\nCaught Exception: " << ex.what() << std::endl;
return 1; // Indicate error
} catch (const std::exception& ex) {
std::cerr << "\nCaught general exception: " << ex.what() << std::endl;
return 1;
}
return 0;
}

Example Output:

Pushing 7 onto intStack...
Top element: 7
Pushing 15 onto intStack...
Top element: 15
Popping intStack...
Top element: 7
Pushing "hello" onto stringStack...
Top element: hello
Pushing "world" onto stringStack...
Top element: world
Is 'world' string empty now? Yes
Popping stringStack twice...
Attempting to pop empty stringStack...
Caught Exception: Stack<>::pop(): empty stack

Note: When defining member functions of a class template outside the class definition, you need to repeat the template declaration and use the scope resolution operator with the template parameters (e.g., template <typename T> void Stack<T>::pop() { ... }).

Modern C++ has introduced powerful template features like:

  • Variadic Templates (C++11): Allow templates to accept a variable number of arguments.
  • Fold Expressions (C++17): Provide a concise syntax for applying an operator to all elements of a parameter pack (from variadic templates).
  • Class Template Argument Deduction (CTAD) (C++17): Allows the compiler to deduce template arguments for class templates from constructor arguments in many cases (e.g., std::pair p(1, "hello"); instead of std::pair<int, const char*> p(1, "hello");).
  • Concepts (C++20): Allow specifying constraints on template parameters, improving error messages and enabling template overloading based on requirements.