Cont

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

  1. Templates enable generic programming - write once, use with many types
  2. Compile-time polymorphism is more efficient than runtime polymorphism
  3. Template metaprogramming allows computations at compile time
  4. Use standard library templates like std::vector<T> as examples
  5. Template code must be in header files (usually .hpp)

Trebuie să fii autentificat pentru a accesa laboratorul cloud și a experimenta cu codul prezentat în acest tutorial.

Autentifică-te