C++ Templates: Complete Guide to Generic Programming
C++ Templates: Complete Guide to Generic Programming
What Are Templates?
Think of templates as blueprints for creating functions and classes. Just like a cookie cutter can make different shaped cookies from the same metal form, templates can generate different code for different data types while writing the code only once.
The Problem Without Templates
Before templates, we had to write duplicate code for each data type:
#include <iostream>
#include <string>
using std::string;
// Without templates - code duplication!
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;
}
int main() {
std::cout << maxInt(5, 10) << std::endl; // Works
std::cout << maxDouble(3.14, 2.71) << std::endl; // Works
// std::cout << maxInt(3.14, 2.71) << std::endl; // Problem: doesn't handle doubles well
return 0;
}
Function Templates
Function templates let you write one function that works with multiple types.
Basic Function Template
#include <iostream>
#include <string>
// Template function - works with any comparable type
template<typename T>
T getMax(T a, T b) {
return (a > b) ? a : b;
}
// Multiple template parameters
template<typename T1, typename T2>
void printPair(T1 first, T2 second) {
std::cout << "First: " << first << ", Second: " << second << std::endl;
}
void demonstrateFunctionTemplates() {
std::cout << "=== Function Templates ===\n";
// Works with integers
std::cout << "Max of 5 and 10: " << getMax(5, 10) << std::endl;
// Works with doubles
std::cout << "Max of 3.14 and 2.71: " << getMax(3.14, 2.71) << std::endl;
// Works with strings
std::cout << "Max of 'apple' and 'zebra': " << getMax(std::string("apple"), std::string("zebra")) << std::endl;
// Multiple types
printPair(42, "Hello");
printPair(3.14, true);
}
int main() {
demonstrateFunctionTemplates();
return 0;
}
Template Specialization
Sometimes you need special behavior for specific types:
#include <iostream>
#include <cstring>
template<typename T>
bool areEqual(T a, T b) {
return a == b;
}
// Specialization for C-style strings
template<>
bool areEqual<char*>(char* a, char* b) {
return strcmp(a, b) == 0;
}
void demonstrateSpecialization() {
std::cout << "\n=== Template Specialization ===\n";
int x = 5, y = 5;
std::cout << "Integers equal: " << areEqual(x, y) << std::endl;
char str1[] = "hello";
char str2[] = "hello";
std::cout << "Strings equal: " << areEqual(str1, str2) << std::endl;
}
int main() {
demonstrateSpecialization();
return 0;
}
Class Templates
Class templates allow you to create generic classes that work with any data type.
Basic Class Template
#include <iostream>
template<typename T>
class Box {
private:
T content;
public:
Box(const T& item) : content(item) {}
T getContent() const {
return content;
}
void setContent(const T& newContent) {
content = newContent;
}
void display() const {
std::cout << "Box contains: " << content << std::endl;
}
};
template<typename T, int Size>
class FixedArray {
private:
T data[Size];
public:
void set(int index, const T& value) {
if (index >= 0 && index < Size) {
data[index] = value;
}
}
T get(int index) const {
if (index >= 0 && index < Size) {
return data[index];
}
return T(); // Return default value
}
void printAll() const {
for (int i = 0; i < Size; ++i) {
std::cout << data[i] << " ";
}
std::cout << std::endl;
}
};
void demonstrateClassTemplates() {
std::cout << "\n=== Class Templates ===\n";
// Box with different types
Box<int> intBox(42);
Box<std::string> stringBox("Hello Templates!");
Box<double> doubleBox(3.14159);
intBox.display();
stringBox.display();
doubleBox.display();
// Fixed array template with non-type parameter
FixedArray<int, 5> intArray;
intArray.set(0, 10);
intArray.set(1, 20);
intArray.set(2, 30);
intArray.printAll();
FixedArray<std::string, 3> stringArray;
stringArray.set(0, "Apple");
stringArray.set(1, "Banana");
stringArray.set(2, "Cherry");
stringArray.printAll();
}
int main() {
demonstrateClassTemplates();
return 0;
}
Real-World Example: Generic Stack
#include <iostream>
#include <vector>
#include <stdexcept>
template<typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& value) {
elements.push_back(value);
}
void pop() {
if (empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elements.pop_back();
}
T top() const {
if (empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elements.back();
}
bool empty() const {
return elements.empty();
}
size_t size() const {
return elements.size();
}
};
void demonstrateGenericStack() {
std::cout << "\n=== Generic Stack Example ===\n";
// Integer stack
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << "Integer stack: ";
while (!intStack.empty()) {
std::cout << intStack.top() << " ";
intStack.pop();
}
std::cout << std::endl;
// String stack
Stack<std::string> stringStack;
stringStack.push("World");
stringStack.push("Hello");
std::cout << "String stack: ";
while (!stringStack.empty()) {
std::cout << stringStack.top() << " ";
stringStack.pop();
}
std::cout << std::endl;
}
int main() {
demonstrateGenericStack();
return 0;
}
Template Metaprogramming
Template metaprogramming allows computation at compile-time.
Compile-Time Factorial
#include <iostream>
// Template metaprogramming: compile-time factorial
template<int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
// Template specialization for base case
template<>
struct Factorial<0> {
static const int value = 1;
};
// Constexpr function (modern alternative)
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
void demonstrateMetaprogramming() {
std::cout << "\n=== Template Metaprogramming ===\n";
// Computed at compile time!
std::cout << "Factorial of 5 (template): " << Factorial<5>::value << std::endl;
std::cout << "Factorial of 6 (template): " << Factorial<6>::value << std::endl;
// Also computed at compile time (constexpr)
std::cout << "Factorial of 7 (constexpr): " << factorial(7) << std::endl;
// These values are calculated during compilation, not runtime
constexpr int result = factorial(5);
std::cout << "Compile-time result: " << result << std::endl;
}
int main() {
demonstrateMetaprogramming();
return 0;
}
Type Traits
Type traits allow you to inspect and manipulate types at compile-time.
#include <iostream>
#include <type_traits>
template<typename T>
void checkType(const T& value) {
std::cout << "Type checking for value: " << value << std::endl;
std::cout << "Is integer: " << std::is_integral<T>::value << std::endl;
std::cout << "Is floating point: " << std::is_floating_point<T>::value << std::endl;
std::cout << "Is pointer: " << std::is_pointer<T>::value << std::endl;
std::cout << "Size: " << sizeof(T) << " bytes" << std::endl;
std::cout << "---" << std::endl;
}
// Using SFINAE (Substitution Failure Is Not An Error)
template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type
square(const T& value) {
return value * value;
}
// This version won't compile for non-arithmetic types
template<typename T>
typename std::enable_if<!std::is_arithmetic<T>::value, T>::type
square(const T& value) {
static_assert(std::is_arithmetic<T>::value, "T must be arithmetic type");
return value;
}
void demonstrateTypeTraits() {
std::cout << "\n=== Type Traits ===\n";
checkType(42);
checkType(3.14);
checkType(true);
int x = 5;
checkType(&x); // Pointer
std::cout << "Square of 5: " << square(5) << std::endl;
std::cout << "Square of 3.14: " << square(3.14) << std::endl;
// square(std::string("hello")); // Compile error - nice!
}
int main() {
demonstrateTypeTraits();
return 0;
}
Variadic Templates
Variadic templates allow functions to accept any number of arguments.
#include <iostream>
// Base case
void print() {
std::cout << std::endl;
}
// Variadic template
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first;
if (sizeof...(args) > 0) {
std::cout << ", ";
}
print(args...); // Recursive call
}
// Modern fold expression (C++17)
template<typename... Args>
void printFold(Args... args) {
((std::cout << args << ", "), ...) << std::endl;
}
void demonstrateVariadicTemplates() {
std::cout << "\n=== Variadic Templates ===\n";
print(1, 2, 3, "hello", 3.14, 'X');
printFold(1, 2, 3, "hello", 3.14, 'X');
print("Single argument");
print(); // No arguments
}
int main() {
demonstrateVariadicTemplates();
return 0;
}
Best Practices and Common Pitfalls
1. Always Prefer typename over class in Templates
// Good
template<typename T>
class Container { /*...*/ };
// Less clear
template<class T>
class Container { /*...*/ };
2. Use Template Type Deduction
template<typename T>
void process(const T& value) { /*...*/ }
void demonstrateBestPractices() {
std::cout << "\n=== Best Practices ===\n";
// Let compiler deduce type
process(42); // T deduced as int
process(3.14); // T deduced as double
process("hello"); // T deduced as const char*
// Don't specify obvious types
// process<int>(42); // Redundant
}
3. Avoid Template Code Bloat
// Put implementation in .hpp files, not .cpp files
// Templates are compiled when instantiated, not when defined
Complete Example: Generic Repository
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
template<typename T>
class Repository {
private:
std::vector<T> items;
std::string name;
public:
Repository(const std::string& repoName) : name(repoName) {}
void add(const T& item) {
items.push_back(item);
}
void remove(const T& item) {
items.erase(std::remove(items.begin(), items.end(), item), items.end());
}
T* find(const T& key) {
auto it = std::find(items.begin(), items.end(), key);
if (it != items.end()) {
return &(*it);
}
return nullptr;
}
void listAll() const {
std::cout << "Repository: " << name << std::endl;
for (const auto& item : items) {
std::cout << " - " << item << std::endl;
}
}
size_t size() const {
return items.size();
}
};
void demonstrateRepository() {
std::cout << "\n=== Generic Repository Example ===\n";
Repository<int> intRepo("Integer Repository");
intRepo.add(10);
intRepo.add(20);
intRepo.add(30);
intRepo.listAll();
Repository<std::string> stringRepo("String Repository");
stringRepo.add("Apple");
stringRepo.add("Banana");
stringRepo.add("Cherry");
stringRepo.listAll();
if (auto found = stringRepo.find("Banana")) {
std::cout << "Found: " << *found << std::endl;
}
}
int main() {
demonstrateRepository();
return 0;
}
Summary
| Template Feature | Purpose | Use Case |
|---|---|---|
| Function Templates | Generic functions | Algorithms working with multiple types |
| Class Templates | Generic classes | Data structures like Vector, Stack |
| Template Specialization | Custom behavior for specific types | Optimizations, special handling |
| Variadic Templates | Variable number of arguments | Logging, tuple classes |
| Template Metaprogramming | Compile-time computation | Performance optimization, validation |
Key Takeaways
- Templates enable generic programming - write once, use with many types
- Compile-time polymorphism is more efficient than runtime polymorphism
- Template metaprogramming allows computations at compile time
- Use standard library templates like
std::vector<T>as examples - Template code must be in header files (usually .hpp)
English
Română