Module 7.1

C++ Templates

Templates are the foundation of generic programming in C++. They allow you to write code once and use it with any data type, enabling powerful abstractions while maintaining type safety and performance!

45 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Function templates for generic algorithms
  • Class templates for reusable containers
  • Template specialization techniques
  • SFINAE for conditional compilation
  • SFINAE and type traits fundamentals
Contents
01

Function Templates

Function templates let you write a single function that works with any data type. Instead of writing separate functions for int, double, and string, you write one template and the compiler generates the specific versions you need. This is the foundation of generic programming in C++.

The Problem: Code Duplication

Imagine you need a function to find the maximum of two values. Without templates, you would need to write separate functions for each type you want to support. This leads to massive code duplication and maintenance headaches.

// Without templates - repetitive and error-prone!
int maxInt(int a, int b) {
    return (a > b) ? a : b;
}

double maxDouble(double a, double b) {
    return (a > b) ? a : b;
}

std::string maxString(std::string a, std::string b) {
    return (a > b) ? a : b;
}

// Usage - need different function names
int x = maxInt(5, 10);           // 10
double y = maxDouble(3.14, 2.71); // 3.14
std::string z = maxString("apple", "banana"); // "banana"
The DRY Principle: "Don't Repeat Yourself" - Templates help you follow this fundamental software engineering principle by writing logic once and reusing it for any type.

Basic Function Template Syntax

A function template is declared using the template keyword followed by template parameters in angle brackets. The template parameter T acts as a placeholder for any type that will be specified when the function is used.

Syntax

Function Template

A function template defines a family of functions. The compiler generates specific function instances (called instantiations) for each type used when calling the template.

Syntax: template<typename T> returnType functionName(parameters)

// Function template - one definition for ALL types!
template<typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // Compiler generates int version automatically
    int x = maximum(5, 10);           // 10
    
    // Compiler generates double version
    double y = maximum(3.14, 2.71);   // 3.14
    
    // Compiler generates string version
    std::string z = maximum(std::string("apple"), std::string("banana")); // "banana"
    
    std::cout << x << ", " << y << ", " << z << std::endl;
    return 0;
}

The typename keyword tells the compiler that T represents a type. You can also use class instead of typename - they are interchangeable for template parameters (though typename is preferred in modern C++ for clarity).

// Both declarations are equivalent
template<typename T>  // Modern preferred style
T add(T a, T b) { return a + b; }

template<class T>     // Also valid, older style
T subtract(T a, T b) { return a - b; }

Template Type Deduction

When you call a function template, the compiler can usually figure out (deduce) the template type from the arguments you pass. This is called type deduction and makes templates convenient to use.

template<typename T>
T square(T value) {
    return value * value;
}

int main() {
    // Type deduction - compiler figures out T from arguments
    auto a = square(5);       // T deduced as int, a = 25
    auto b = square(3.14);    // T deduced as double, b = 9.8596
    auto c = square(2.5f);    // T deduced as float, c = 6.25f
    
    // Explicit type specification (when needed)
    auto d = square<double>(5);  // Force T = double, d = 25.0
    
    return 0;
}
Type Mismatch Error: If arguments have different types, deduction fails. Use explicit specification: maximum<double>(5, 3.14)
template<typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // ERROR: T deduced as int from 5, but 3.14 is double
    // auto x = maximum(5, 3.14);  // Compilation error!
    
    // Solution 1: Explicit type specification
    auto x = maximum<double>(5, 3.14);  // T = double, x = 5.0
    
    // Solution 2: Make both arguments the same type
    auto y = maximum(5.0, 3.14);  // Both double, y = 5.0
    
    return 0;
}

Multiple Template Parameters

Templates can have multiple type parameters, allowing functions to work with different types for different arguments. This provides maximum flexibility.

// Two different type parameters
template<typename T, typename U>
auto add(T a, U b) {
    return a + b;  // Return type deduced with auto (C++14)
}

// With explicit return type (C++11)
template<typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
    return a * b;
}

int main() {
    auto sum = add(5, 3.14);        // int + double = double (8.14)
    auto product = multiply(3, 2.5); // int * double = double (7.5)
    
    std::cout << sum << ", " << product << std::endl;
    return 0;
}

Non-Type Template Parameters

Templates can also accept non-type parameters - values known at compile time like integers, pointers, or references. These are powerful for creating compile-time configurations.

// Non-type parameter: size is a compile-time constant
template<typename T, int size>
T arraySum(T (&arr)[size]) {
    T sum = 0;
    for (int i = 0; i < size; ++i) {
        sum += arr[i];
    }
    return sum;
}

// Compile-time factorial using non-type parameter
template<int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static const int value = 1;
};

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int total = arraySum(numbers);  // size deduced as 5
    std::cout << "Sum: " << total << std::endl;  // 15
    
    // Compile-time factorial
    std::cout << "5! = " << Factorial<5>::value << std::endl;  // 120
    
    return 0;
}
Feature Type Parameter Non-Type Parameter
Syntax template<typename T> template<int N>
Represents A type (int, double, string, etc.) A compile-time constant value
Use Case Generic algorithms, containers Array sizes, compile-time config
Example std::vector<int> std::array<int, 10>

Interactive: Type Deduction Explorer

Click a type to see how the compiler deduces template parameters:

template<typename T>
T square(T x) { return x * x; }

int result = square(5);
Deduction
T = int
Result
25

The argument 5 is an int literal, so T is deduced as int. The function returns int.

Which Template Should I Use?

Answer these questions to find the right template type:

Do you need the same code to work with multiple types?

Practice Questions: Function Templates

Test your understanding with these coding exercises:

Problem: Create a function template that swaps two values of any type.

Test Case:

int a = 5, b = 10;
mySwap(a, b);
// a should be 10, b should be 5
View Solution
template<typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    mySwap(x, y);
    std::cout << x << ", " << y << std::endl;  // 10, 5
    
    std::string s1 = "hello", s2 = "world";
    mySwap(s1, s2);
    std::cout << s1 << ", " << s2 << std::endl;  // world, hello
    
    return 0;
}

Problem: Write a function template that prints all elements of an array, separated by commas.

Test Case:

int nums[] = {1, 2, 3, 4, 5};
printArray(nums, 5);  // Output: 1, 2, 3, 4, 5
View Solution
template<typename T>
void printArray(T arr[], int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i];
        if (i < size - 1) std::cout << ", ";
    }
    std::cout << std::endl;
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    printArray(nums, 5);  // 1, 2, 3, 4, 5
    
    double vals[] = {1.1, 2.2, 3.3};
    printArray(vals, 3);  // 1.1, 2.2, 3.3
    
    return 0;
}

Problem: Create a function template that finds an element in an array and returns its index, or -1 if not found.

Test Case:

int nums[] = {10, 20, 30, 40, 50};
int idx = findElement(nums, 5, 30);  // Should return 2
int idx2 = findElement(nums, 5, 99); // Should return -1
View Solution
template<typename T>
int findElement(T arr[], int size, T target) {
    for (int i = 0; i < size; ++i) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}

int main() {
    int nums[] = {10, 20, 30, 40, 50};
    
    int idx1 = findElement(nums, 5, 30);
    std::cout << "Index of 30: " << idx1 << std::endl;  // 2
    
    int idx2 = findElement(nums, 5, 99);
    std::cout << "Index of 99: " << idx2 << std::endl;  // -1
    
    std::string words[] = {"apple", "banana", "cherry"};
    int idx3 = findElement(words, 3, std::string("banana"));
    std::cout << "Index of banana: " << idx3 << std::endl;  // 1
    
    return 0;
}
02

Class Templates

Class templates allow you to create generic classes that work with any type. This is how the STL containers like vector, map, and set are implemented, making them incredibly flexible and reusable. Once you understand class templates, you unlock the power to create your own generic data structures.

Basic Class Template Syntax

A class template is defined similarly to a function template, with the template keyword preceding the class definition. The type parameter can then be used throughout the class as if it were a real type.

// A simple generic container that holds one value
template<typename T>
class Box {
private:
    T content;
    
public:
    // Constructor
    Box(T value) : content(value) {}
    
    // Getter
    T getContent() const {
        return content;
    }
    
    // Setter
    void setContent(T value) {
        content = value;
    }
};

int main() {
    // Create boxes of different types
    Box<int> intBox(42);
    Box<double> doubleBox(3.14);
    Box<std::string> stringBox("Hello, Templates!");
    
    std::cout << intBox.getContent() << std::endl;    // 42
    std::cout << doubleBox.getContent() << std::endl; // 3.14
    std::cout << stringBox.getContent() << std::endl; // Hello, Templates!
    
    return 0;
}
Why Class Templates? Without templates, you would need IntBox, DoubleBox, StringBox, etc. Templates let you write the logic once!

Building a Generic Stack

Let's build a more practical example - a generic stack data structure. This demonstrates how templates enable you to create reusable containers that work with any type.

#include <iostream>
#include <stdexcept>

template<typename T>
class Stack {
private:
    static const int MAX_SIZE = 100;
    T data[MAX_SIZE];
    int topIndex;
    
public:
    Stack() : topIndex(-1) {}
    
    void push(T value) {
        if (topIndex >= MAX_SIZE - 1) {
            throw std::overflow_error("Stack overflow");
        }
        data[++topIndex] = value;
    }
    
    T pop() {
        if (isEmpty()) {
            throw std::underflow_error("Stack underflow");
        }
        return data[topIndex--];
    }
    
    T top() const {
        if (isEmpty()) {
            throw std::underflow_error("Stack is empty");
        }
        return data[topIndex];
    }
    
    bool isEmpty() const {
        return topIndex < 0;
    }
    
    int size() const {
        return topIndex + 1;
    }
};

int main() {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);
    
    std::cout << "Top: " << intStack.top() << std::endl;  // 30
    std::cout << "Pop: " << intStack.pop() << std::endl;  // 30
    std::cout << "Size: " << intStack.size() << std::endl; // 2
    
    Stack<std::string> stringStack;
    stringStack.push("first");
    stringStack.push("second");
    std::cout << "String top: " << stringStack.top() << std::endl; // second
    
    return 0;
}

Defining Member Functions Outside the Class

For larger classes, you often want to separate the declaration from the implementation. With class templates, you must repeat the template<typename T> for each member function defined outside the class.

// Class template declaration
template<typename T>
class Pair {
private:
    T first;
    T second;
    
public:
    Pair(T f, T s);
    T getFirst() const;
    T getSecond() const;
    void setFirst(T value);
    void setSecond(T value);
    void swap();
};

// Member function definitions outside the class
template<typename T>
Pair<T>::Pair(T f, T s) : first(f), second(s) {}

template<typename T>
T Pair<T>::getFirst() const {
    return first;
}

template<typename T>
T Pair<T>::getSecond() const {
    return second;
}

template<typename T>
void Pair<T>::setFirst(T value) {
    first = value;
}

template<typename T>
void Pair<T>::setSecond(T value) {
    second = value;
}

template<typename T>
void Pair<T>::swap() {
    T temp = first;
    first = second;
    second = temp;
}

int main() {
    Pair<int> coords(10, 20);
    std::cout << coords.getFirst() << ", " << coords.getSecond() << std::endl; // 10, 20
    
    coords.swap();
    std::cout << coords.getFirst() << ", " << coords.getSecond() << std::endl; // 20, 10
    
    return 0;
}

Multiple Type Parameters

Class templates can have multiple type parameters, allowing you to create more flexible structures like key-value pairs where the key and value can be different types.

// Class template with two type parameters
template<typename K, typename V>
class KeyValuePair {
private:
    K key;
    V value;
    
public:
    KeyValuePair(K k, V v) : key(k), value(v) {}
    
    K getKey() const { return key; }
    V getValue() const { return value; }
    
    void setValue(V v) { value = v; }
    
    void print() const {
        std::cout << key << ": " << value << std::endl;
    }
};

int main() {
    KeyValuePair<std::string, int> age("Alice", 25);
    age.print();  // Alice: 25
    
    KeyValuePair<int, std::string> errorCode(404, "Not Found");
    errorCode.print();  // 404: Not Found
    
    KeyValuePair<std::string, double> price("Apple", 1.99);
    price.print();  // Apple: 1.99
    
    return 0;
}

Default Template Arguments

Like function parameters, template parameters can have default values. This is commonly used in the STL - for example, std::vector<int> actually uses a default allocator.

// Template with default type argument
template<typename T = int, int Size = 10>
class FixedArray {
private:
    T data[Size];
    int count;
    
public:
    FixedArray() : count(0) {}
    
    void add(T value) {
        if (count < Size) {
            data[count++] = value;
        }
    }
    
    T get(int index) const {
        return data[index];
    }
    
    int size() const { return count; }
    int capacity() const { return Size; }
};

int main() {
    // Uses default: T = int, Size = 10
    FixedArray<> defaultArray;
    defaultArray.add(1);
    defaultArray.add(2);
    
    // Specify type, use default size
    FixedArray<double> doubleArray;
    doubleArray.add(3.14);
    
    // Specify both
    FixedArray<std::string, 5> stringArray;
    stringArray.add("hello");
    
    std::cout << "Default capacity: " << defaultArray.capacity() << std::endl;  // 10
    std::cout << "String capacity: " << stringArray.capacity() << std::endl;    // 5
    
    return 0;
}
Class Templates
  • Must explicitly specify type: Stack<int>
  • Can have member variables of template type
  • Support default template arguments
  • Used for containers, smart pointers
Function Templates
  • Type can be deduced: maximum(5, 10)
  • Work with parameters and return types
  • Default arguments since C++11
  • Used for algorithms, utilities

Practice Questions: Class Templates

Build your skills with these exercises:

Problem: Create a class template Triple that stores three values of the same type with getters for each.

View Solution
template<typename T>
class Triple {
private:
    T first, second, third;
    
public:
    Triple(T a, T b, T c) : first(a), second(b), third(c) {}
    
    T getFirst() const { return first; }
    T getSecond() const { return second; }
    T getThird() const { return third; }
};

int main() {
    Triple<int> coords(1, 2, 3);
    std::cout << coords.getFirst() << ", " 
              << coords.getSecond() << ", " 
              << coords.getThird() << std::endl;  // 1, 2, 3
    
    Triple<std::string> names("Alice", "Bob", "Charlie");
    std::cout << names.getSecond() << std::endl;  // Bob
    
    return 0;
}

Problem: Create a Queue class template with enqueue, dequeue, front, and isEmpty methods.

View Solution
template<typename T>
class Queue {
private:
    static const int MAX_SIZE = 100;
    T data[MAX_SIZE];
    int frontIdx, rearIdx, count;
    
public:
    Queue() : frontIdx(0), rearIdx(-1), count(0) {}
    
    void enqueue(T value) {
        if (count >= MAX_SIZE) {
            throw std::overflow_error("Queue full");
        }
        rearIdx = (rearIdx + 1) % MAX_SIZE;
        data[rearIdx] = value;
        count++;
    }
    
    T dequeue() {
        if (isEmpty()) {
            throw std::underflow_error("Queue empty");
        }
        T value = data[frontIdx];
        frontIdx = (frontIdx + 1) % MAX_SIZE;
        count--;
        return value;
    }
    
    T front() const {
        if (isEmpty()) {
            throw std::underflow_error("Queue empty");
        }
        return data[frontIdx];
    }
    
    bool isEmpty() const { return count == 0; }
    int size() const { return count; }
};

int main() {
    Queue<int> q;
    q.enqueue(10);
    q.enqueue(20);
    q.enqueue(30);
    
    std::cout << "Front: " << q.front() << std::endl;    // 10
    std::cout << "Dequeue: " << q.dequeue() << std::endl; // 10
    std::cout << "New front: " << q.front() << std::endl; // 20
    
    return 0;
}

Problem: Create a class template that tracks the minimum and maximum values added to it.

View Solution
template<typename T>
class MinMaxTracker {
private:
    T minValue, maxValue;
    bool hasValues;
    
public:
    MinMaxTracker() : hasValues(false) {}
    
    void add(T value) {
        if (!hasValues) {
            minValue = maxValue = value;
            hasValues = true;
        } else {
            if (value < minValue) minValue = value;
            if (value > maxValue) maxValue = value;
        }
    }
    
    T getMin() const {
        if (!hasValues) throw std::runtime_error("No values");
        return minValue;
    }
    
    T getMax() const {
        if (!hasValues) throw std::runtime_error("No values");
        return maxValue;
    }
    
    bool empty() const { return !hasValues; }
};

int main() {
    MinMaxTracker<int> tracker;
    tracker.add(5);
    tracker.add(2);
    tracker.add(8);
    tracker.add(1);
    tracker.add(9);
    
    std::cout << "Min: " << tracker.getMin() << std::endl;  // 1
    std::cout << "Max: " << tracker.getMax() << std::endl;  // 9
    
    MinMaxTracker<std::string> strTracker;
    strTracker.add("banana");
    strTracker.add("apple");
    strTracker.add("cherry");
    
    std::cout << "Min: " << strTracker.getMin() << std::endl;  // apple
    std::cout << "Max: " << strTracker.getMax() << std::endl;  // cherry
    
    return 0;
}
03

Template Specialization

Sometimes you need different behavior for specific types. Template specialization lets you provide custom implementations for particular types while keeping the generic version for everything else. This is how the STL optimizes vector<bool> differently from other vectors.

Why Specialize Templates?

The generic template works for most types, but sometimes you need optimized or different behavior for specific types. For example, comparing C-strings with > compares pointers, not the actual string content!

// Generic maximum - works for most types
template<typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // Works correctly for int and double
    std::cout << maximum(10, 5) << std::endl;      // 10
    std::cout << maximum(3.14, 2.71) << std::endl; // 3.14
    
    // PROBLEM: For C-strings, compares pointers, not content!
    const char* s1 = "apple";
    const char* s2 = "banana";
    std::cout << maximum(s1, s2) << std::endl;  // Undefined - compares addresses!
    
    return 0;
}
The Problem: The generic template compares const char* pointers (memory addresses), not the actual string content. We need specialization to fix this!

Full Template Specialization

Full specialization provides a completely custom implementation for a specific type. The syntax uses template<> (empty angle brackets) followed by the function with the specific type.

#include <cstring>

// Primary template (generic version)
template<typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Full specialization for const char*
template<>
const char* maximum<const char*>(const char* a, const char* b) {
    return (std::strcmp(a, b) > 0) ? a : b;
}

int main() {
    // Uses generic version
    std::cout << maximum(10, 5) << std::endl;  // 10
    
    // Uses specialized version - compares string content!
    const char* s1 = "apple";
    const char* s2 = "banana";
    std::cout << maximum(s1, s2) << std::endl;  // banana (correct!)
    
    return 0;
}

Class Template Specialization

Class templates can also be specialized. This is commonly used to optimize storage or provide different interfaces for specific types.

// Primary template
template<typename T>
class Storage {
private:
    T value;
public:
    Storage(T v) : value(v) {}
    T get() const { return value; }
    void set(T v) { value = v; }
    void print() const {
        std::cout << "Generic: " << value << std::endl;
    }
};

// Full specialization for bool - more efficient storage
template<>
class Storage<bool> {
private:
    unsigned char value;  // Use single byte
public:
    Storage(bool v) : value(v ? 1 : 0) {}
    bool get() const { return value != 0; }
    void set(bool v) { value = v ? 1 : 0; }
    void print() const {
        std::cout << "Bool specialized: " << (value ? "true" : "false") << std::endl;
    }
};

int main() {
    Storage<int> intStore(42);
    intStore.print();  // Generic: 42
    
    Storage<bool> boolStore(true);
    boolStore.print();  // Bool specialized: true
    
    return 0;
}

Partial Template Specialization

Partial specialization allows you to specialize for a family of types rather than one specific type. This only works for class templates, not function templates.

// Primary template
template<typename T, typename U>
class Pair {
public:
    T first;
    U second;
    
    Pair(T f, U s) : first(f), second(s) {}
    
    void print() const {
        std::cout << "Generic: (" << first << ", " << second << ")" << std::endl;
    }
};

// Partial specialization: when both types are the same
template<typename T>
class Pair<T, T> {
public:
    T first;
    T second;
    
    Pair(T f, T s) : first(f), second(s) {}
    
    void print() const {
        std::cout << "Same type: (" << first << ", " << second << ")" << std::endl;
    }
    
    // Extra method only available for same-type pairs
    T sum() const { return first + second; }
};

// Partial specialization: when T is a pointer type
template<typename T>
class Pair<T*, T*> {
public:
    T* first;
    T* second;
    
    Pair(T* f, T* s) : first(f), second(s) {}
    
    void print() const {
        std::cout << "Pointer pair: (" << *first << ", " << *second << ")" << std::endl;
    }
};

int main() {
    Pair<int, std::string> p1(42, "hello");
    p1.print();  // Generic: (42, hello)
    
    Pair<int, int> p2(10, 20);
    p2.print();  // Same type: (10, 20)
    std::cout << "Sum: " << p2.sum() << std::endl;  // Sum: 30
    
    int a = 5, b = 10;
    Pair<int*, int*> p3(&a, &b);
    p3.print();  // Pointer pair: (5, 10)
    
    return 0;
}
Rule

Specialization Selection Order

The compiler selects the most specialized template that matches:

1. Full specialization (exact type match) - highest priority
2. Partial specialization (pattern match)
3. Primary template (generic) - lowest priority

Introduction to Type Traits

Type traits use template specialization to query type properties at compile time. The standard library provides many in <type_traits>.

#include <type_traits>

// Custom type trait: is it an integer type?
template<typename T>
struct is_integer {
    static const bool value = false;
};

// Specializations for integer types
template<> struct is_integer<int> { static const bool value = true; };
template<> struct is_integer<long> { static const bool value = true; };
template<> struct is_integer<short> { static const bool value = true; };

// Using standard type traits
template<typename T>
void checkType() {
    std::cout << "Is integral: " << std::is_integral<T>::value << std::endl;
    std::cout << "Is floating: " << std::is_floating_point<T>::value << std::endl;
    std::cout << "Is pointer: " << std::is_pointer<T>::value << std::endl;
}

int main() {
    std::cout << "Custom trait:" << std::endl;
    std::cout << "int: " << is_integer<int>::value << std::endl;     // 1
    std::cout << "double: " << is_integer<double>::value << std::endl; // 0
    
    std::cout << "\nStandard traits for int:" << std::endl;
    checkType<int>();     // integral: 1, floating: 0, pointer: 0
    
    std::cout << "\nStandard traits for double:" << std::endl;
    checkType<double>();  // integral: 0, floating: 1, pointer: 0
    
    return 0;
}
Specialization Type Syntax Use Case
Full template<> class X<int> Optimize for one specific type
Partial template<typename T> class X<T*> Handle family of types (pointers, references)
Function template<> void f<int>(int) Special behavior for specific type

Practice Questions: Specialization

Problem: Create a print function template and specialize it for bool to print "true"/"false" instead of 1/0.

View Solution
// Primary template
template<typename T>
void print(T value) {
    std::cout << value << std::endl;
}

// Specialization for bool
template<>
void print<bool>(bool value) {
    std::cout << (value ? "true" : "false") << std::endl;
}

int main() {
    print(42);       // 42
    print(3.14);     // 3.14
    print(true);     // true (not 1!)
    print(false);    // false (not 0!)
    
    return 0;
}

Problem: Create an is_numeric type trait that returns true for int, double, and float.

View Solution
// Primary template - default to false
template<typename T>
struct is_numeric {
    static const bool value = false;
};

// Specializations for numeric types
template<> struct is_numeric<int> { static const bool value = true; };
template<> struct is_numeric<float> { static const bool value = true; };
template<> struct is_numeric<double> { static const bool value = true; };
template<> struct is_numeric<long> { static const bool value = true; };

int main() {
    std::cout << std::boolalpha;  // Print true/false instead of 1/0
    std::cout << "int: " << is_numeric<int>::value << std::endl;        // true
    std::cout << "double: " << is_numeric<double>::value << std::endl;  // true
    std::cout << "string: " << is_numeric<std::string>::value << std::endl; // false
    std::cout << "char: " << is_numeric<char>::value << std::endl;      // false
    
    return 0;
}

Problem: Create a Wrapper class with a partial specialization for pointer types that auto-deletes.

View Solution
// Primary template - stores value directly
template<typename T>
class Wrapper {
private:
    T data;
public:
    Wrapper(T val) : data(val) {}
    T get() const { return data; }
    ~Wrapper() {
        std::cout << "Generic destructor" << std::endl;
    }
};

// Partial specialization for pointers
template<typename T>
class Wrapper<T*> {
private:
    T* data;
public:
    Wrapper(T* ptr) : data(ptr) {}
    T* get() const { return data; }
    T& operator*() const { return *data; }
    ~Wrapper() {
        std::cout << "Pointer destructor - deleting" << std::endl;
        delete data;
    }
};

int main() {
    {
        Wrapper<int> w1(42);
        std::cout << "Value: " << w1.get() << std::endl;
    }  // "Generic destructor"
    
    {
        Wrapper<int*> w2(new int(100));
        std::cout << "Pointed value: " << *w2 << std::endl;
    }  // "Pointer destructor - deleting"
    
    return 0;
}
04

SFINAE and Concepts

SFINAE (Substitution Failure Is Not An Error) and C++20 Concepts are powerful techniques for constraining templates. They allow you to enable or disable template overloads based on type properties, creating more robust and expressive generic code.

Understanding SFINAE

SFINAE is a C++ rule that allows template substitution failures to silently remove overloads from consideration rather than causing compilation errors. This enables conditional template selection based on type characteristics.

Concept

SFINAE

Substitution Failure Is Not An Error - When the compiler substitutes template arguments and encounters an invalid type expression, it doesn't produce an error. Instead, it removes that template from the overload set.

Key Insight: SFINAE lets us create templates that only exist for types meeting certain criteria, enabling compile-time conditional logic.

#include <iostream>
#include <type_traits>

// This function only exists for integral types
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
doubleValue(T value) {
    std::cout << "Integer version: ";
    return value * 2;
}

// This function only exists for floating-point types
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
doubleValue(T value) {
    std::cout << "Float version: ";
    return value * 2.0;
}

int main() {
    std::cout << doubleValue(5) << std::endl;      // Integer version: 10
    std::cout << doubleValue(3.14) << std::endl;   // Float version: 6.28
    
    // doubleValue("hello");  // Error: no matching function
    
    return 0;
}

Using std::enable_if

std::enable_if is the primary tool for SFINAE. It conditionally defines a type member only when a boolean condition is true, causing substitution failure when false.

#include <type_traits>

// Enable only for types with a size() method (containers)
template<typename Container>
auto getSize(const Container& c) 
    -> typename std::enable_if<
        !std::is_array<Container>::value,
        decltype(c.size())
    >::type 
{
    return c.size();
}

// Enable only for C-style arrays
template<typename T, size_t N>
size_t getSize(const T (&arr)[N]) {
    return N;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    int arr[] = {1, 2, 3};
    
    std::cout << "Vector size: " << getSize(vec) << std::endl;  // 5
    std::cout << "Array size: " << getSize(arr) << std::endl;   // 3
    
    return 0;
}
Common Pattern: Place std::enable_if in the return type or as a default template parameter. The trailing return type syntax (-> type) is often cleaner for complex conditions.

Type Traits for SFINAE

The <type_traits> header provides many useful type predicates for SFINAE conditions:

Type Trait Checks For Example Types
std::is_integral Integer types int, char, bool, long
std::is_floating_point Floating-point types float, double
std::is_arithmetic Numeric types All integral and floating-point
std::is_class Class/struct types std::string, custom classes
std::is_pointer Pointer types int*, char*, void*
std::is_same Type equality Compare two types
#include <type_traits>

// Safe division - only for arithmetic types
template<typename T, 
         typename = std::enable_if_t<std::is_arithmetic_v<T>>>
T safeDivide(T a, T b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero");
    }
    return a / b;
}

// Print pointer address or value
template<typename T>
void printValue(T value) {
    if constexpr (std::is_pointer_v<T>) {
        std::cout << "Pointer to: " << *value << std::endl;
    } else {
        std::cout << "Value: " << value << std::endl;
    }
}

int main() {
    std::cout << safeDivide(10, 3) << std::endl;    // 3
    std::cout << safeDivide(10.0, 3.0) << std::endl; // 3.33333
    
    int x = 42;
    printValue(x);   // Value: 42
    printValue(&x);  // Pointer to: 42
    
    return 0;
}

C++20 Concepts

Concepts (C++20) revolutionize template constraints by providing a clean, readable syntax. They replace SFINAE for most use cases with much clearer error messages.

C++20

Concepts

A concept is a named set of requirements that constrain template parameters. Concepts provide clear, readable constraints and dramatically improve error messages.

Syntax: template<Concept T> or requires Concept<T>

#include <concepts>
#include <iostream>

// Define a concept for types that can be added
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

// Define a concept for numeric types
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use concept in template declaration
template<Numeric T>
T add(T a, T b) {
    return a + b;
}

// Alternative: requires clause
template<typename T>
    requires Addable<T>
T sum(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(5, 3) << std::endl;       // 8
    std::cout << add(2.5, 3.5) << std::endl;   // 6.0
    std::cout << sum(10, 20) << std::endl;     // 30
    
    // add("hello", "world");  // Clear error: constraint not satisfied
    
    return 0;
}

Standard Library Concepts

C++20 provides many predefined concepts in the <concepts> header:

#include <concepts>

// Using standard concepts
template<std::integral T>
T bitwiseOr(T a, T b) {
    return a | b;  // Only valid for integral types
}

template<std::floating_point T>
T squareRoot(T value) {
    return std::sqrt(value);
}

// Combining concepts with &&
template<typename T>
    requires std::copyable<T> && std::equality_comparable<T>
bool contains(const std::vector<T>& vec, const T& value) {
    for (const auto& item : vec) {
        if (item == value) return true;
    }
    return false;
}

int main() {
    std::cout << bitwiseOr(5, 3) << std::endl;     // 7
    std::cout << squareRoot(16.0) << std::endl;    // 4.0
    
    std::vector<int> nums = {1, 2, 3, 4, 5};
    std::cout << std::boolalpha;
    std::cout << contains(nums, 3) << std::endl;   // true
    std::cout << contains(nums, 10) << std::endl;  // false
    
    return 0;
}
Core Concepts
  • std::same_as<T, U>
  • std::derived_from<D, B>
  • std::convertible_to<From, To>
  • std::integral<T>
  • std::floating_point<T>
Object Concepts
  • std::copyable<T>
  • std::movable<T>
  • std::default_initializable<T>
  • std::equality_comparable<T>
  • std::totally_ordered<T>

Creating Custom Concepts

You can define custom concepts using requires expressions to specify exactly what operations a type must support:

#include <concepts>
#include <string>

// Concept: type must have a .toString() method
template<typename T>
concept Stringifiable = requires(T obj) {
    { obj.toString() } -> std::convertible_to<std::string>;
};

// Concept: type must be a container with size() and begin()/end()
template<typename T>
concept Container = requires(T container) {
    { container.size() } -> std::convertible_to<std::size_t>;
    { container.begin() };
    { container.end() };
};

// Concept: type must support arithmetic operations
template<typename T>
concept Arithmetic = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
    { a - b } -> std::same_as<T>;
    { a * b } -> std::same_as<T>;
    { a / b } -> std::same_as<T>;
};

// Using our custom concepts
class Point {
public:
    int x, y;
    std::string toString() const {
        return "(" + std::to_string(x) + ", " + std::to_string(y) + ")";
    }
};

template<Stringifiable T>
void print(const T& obj) {
    std::cout << obj.toString() << std::endl;
}

template<Container C>
void printSize(const C& container) {
    std::cout << "Size: " << container.size() << std::endl;
}

int main() {
    Point p{10, 20};
    print(p);  // (10, 20)
    
    std::vector<int> vec = {1, 2, 3, 4, 5};
    printSize(vec);  // Size: 5
    
    return 0;
}

SFINAE vs Concepts Comparison

When to use which? Use Concepts (C++20) for new code - they're cleaner and produce better error messages. Use SFINAE when working with older codebases or compilers without C++20 support.
// ==========================================
// SFINAE approach (C++11/14/17)
// ==========================================
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
T sfinae_increment(T value) {
    return value + 1;
}

// ==========================================
// Concepts approach (C++20) - Much cleaner!
// ==========================================
template<std::integral T>
T concepts_increment(T value) {
    return value + 1;
}

// Or with requires clause
template<typename T>
    requires std::integral<T>
T requires_increment(T value) {
    return value + 1;
}

// Or with abbreviated syntax (auto + concept)
std::integral auto auto_increment(std::integral auto value) {
    return value + 1;
}

int main() {
    // All four work identically
    std::cout << sfinae_increment(5) << std::endl;    // 6
    std::cout << concepts_increment(5) << std::endl;  // 6
    std::cout << requires_increment(5) << std::endl;  // 6
    std::cout << auto_increment(5) << std::endl;      // 6
    
    return 0;
}

Practice Questions: SFINAE and Concepts

Problem: Create a concept that checks if a type can be printed using std::cout.

View Solution
#include <concepts>
#include <iostream>

// Concept: type can be output to ostream
template<typename T>
concept Printable = requires(std::ostream& os, T value) {
    { os << value } -> std::same_as<std::ostream&>;
};

template<Printable T>
void print(const T& value) {
    std::cout << value << std::endl;
}

int main() {
    print(42);           // Works
    print("Hello");      // Works
    print(3.14);         // Works
    
    // print(std::vector<int>{});  // Error: not Printable
    
    return 0;
}

Problem: Create a type trait that detects if a type has a serialize() method.

View Solution
#include <type_traits>

// Primary template: assume no serialize method
template<typename T, typename = void>
struct has_serialize : std::false_type {};

// Specialization: has serialize if this expression is valid
template<typename T>
struct has_serialize<T, 
    std::void_t<decltype(std::declval<T>().serialize())>>
    : std::true_type {};

// Helper variable template
template<typename T>
inline constexpr bool has_serialize_v = has_serialize<T>::value;

class Serializable {
public:
    std::string serialize() { return "data"; }
};

class NotSerializable {
public:
    void doSomething() {}
};

int main() {
    std::cout << std::boolalpha;
    std::cout << "Serializable: " << has_serialize_v<Serializable> << std::endl;     // true
    std::cout << "NotSerializable: " << has_serialize_v<NotSerializable> << std::endl; // false
    std::cout << "int: " << has_serialize_v<int> << std::endl;  // false
    
    return 0;
}

Problem: Create overloaded process() functions using concepts that handle integers, containers, and strings differently.

View Solution
#include <concepts>
#include <ranges>
#include <string>
#include <vector>

// Concept for string-like types
template<typename T>
concept StringLike = std::convertible_to<T, std::string_view>;

// Concept for range/container types (but not strings)
template<typename T>
concept RangeLike = std::ranges::range<T> && !StringLike<T>;

// Process integers
void process(std::integral auto value) {
    std::cout << "Integer: " << value << " (doubled: " << value * 2 << ")" << std::endl;
}

// Process strings
void process(StringLike auto const& str) {
    std::cout << "String: \"" << str << "\" (length: " << std::string_view(str).size() << ")" << std::endl;
}

// Process containers
void process(RangeLike auto const& container) {
    std::cout << "Container with " << std::ranges::size(container) << " elements: ";
    for (const auto& item : container) {
        std::cout << item << " ";
    }
    std::cout << std::endl;
}

int main() {
    process(42);                          // Integer: 42 (doubled: 84)
    process("Hello World");               // String: "Hello World" (length: 11)
    process(std::vector{1, 2, 3, 4, 5});  // Container with 5 elements: 1 2 3 4 5
    process(std::string("Test"));         // String: "Test" (length: 4)
    
    return 0;
}

Key Takeaways

Write Once, Use Anywhere

Templates let you write generic code that works with any type while maintaining full type safety

Zero Runtime Cost

Template code is resolved at compile time, resulting in optimized machine code with no overhead

STL Foundation

All STL containers and algorithms are built on templates, making them incredibly flexible

Specialization Power

Customize behavior for specific types while keeping generic implementations for everything else

Variadic Flexibility

Accept any number of template arguments with type safety using parameter packs

Modern C++ Essential

Templates are fundamental to writing professional, reusable, and efficient C++ code

Knowledge Check

Quick Quiz

Test what you've learned about C++ templates

1 What keyword is used to declare a template?
2 What is the difference between typename and class in template parameters?
3 When is template code instantiated?
4 What is template specialization used for?
5 What does template<typename... Args> declare?
6 Where should template definitions typically be placed?
Answer all questions to check your score