Coming Soon

This lesson is currently being developed

Class templates

Create generic classes that work with different types.

Compound Types: Enums and Structs
Chapter
Beginner
Difficulty
60min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

13.13 — Class templates

In this lesson, you'll learn about class templates - a powerful feature that allows you to create generic structs and classes that can work with different data types, enabling code reuse and type safety.

The problem with type-specific structs

Imagine you want to create different structs to hold pairs of values:

#include <iostream>

struct IntPair
{
    int first{0};
    int second{0};
};

struct DoublePair
{
    double first{0.0};
    double second{0.0};
};

struct StringPair
{
    std::string first{};
    std::string second{};
};

int main()
{
    IntPair coordinates{3, 5};
    DoublePair prices{19.99, 24.99};
    StringPair names{"Alice", "Bob"};
    
    std::cout << "Coordinates: (" << coordinates.first << ", " << coordinates.second << ")" << std::endl;
    std::cout << "Prices: $" << prices.first << ", $" << prices.second << std::endl;
    std::cout << "Names: " << names.first << ", " << names.second << std::endl;
    
    return 0;
}

Output:

Coordinates: (3, 5)
Prices: $19.99, $24.99
Names: Alice, Bob

This approach has problems:

  • Code duplication: Each struct is nearly identical
  • Maintenance burden: Changes need to be made in multiple places
  • Limited scalability: Need a new struct for each type combination

Introduction to class templates

A class template (which works with structs too) allows you to write a single struct definition that works with multiple data types:

#include <iostream>
#include <string>

template<typename T>
struct Pair
{
    T first{};
    T second{};
};

int main()
{
    Pair<int> coordinates{3, 5};
    Pair<double> prices{19.99, 24.99};
    Pair<std::string> names{"Alice", "Bob"};
    
    std::cout << "Coordinates: (" << coordinates.first << ", " << coordinates.second << ")" << std::endl;
    std::cout << "Prices: $" << prices.first << ", $" << prices.second << std::endl;
    std::cout << "Names: " << names.first << ", " << names.second << std::endl;
    
    return 0;
}

Output:

Coordinates: (3, 5)
Prices: $19.99, $24.99
Names: Alice, Bob

The compiler generates separate versions of the Pair struct for each type you use (int, double, std::string).

Template syntax breakdown

Let's examine the template syntax:

template<typename T>  // Template declaration
struct Pair           // Regular struct definition
{
    T first{};         // T is the template parameter
    T second{};        // T will be replaced with the actual type
};
  • template<typename T> declares this as a template with a type parameter named T
  • T is a placeholder for any type
  • When you use Pair<int>, the compiler replaces all instances of T with int

Template instantiation

When you declare a variable using a template, the compiler creates a specific version called an instantiation:

#include <iostream>

template<typename T>
struct Container
{
    T value{};
    
    void setValue(const T& newValue)
    {
        value = newValue;
    }
    
    T getValue() const
    {
        return value;
    }
};

int main()
{
    Container<int> intContainer;         // Instantiate Container<int>
    Container<double> doubleContainer;   // Instantiate Container<double>
    Container<bool> boolContainer;       // Instantiate Container<bool>
    
    intContainer.setValue(42);
    doubleContainer.setValue(3.14);
    boolContainer.setValue(true);
    
    std::cout << "Int: " << intContainer.getValue() << std::endl;
    std::cout << "Double: " << doubleContainer.getValue() << std::endl;
    std::cout << "Bool: " << (boolContainer.getValue() ? "true" : "false") << std::endl;
    
    return 0;
}

Output:

Int: 42
Double: 3.14
Bool: true

Multiple template parameters

Templates can have multiple type parameters:

#include <iostream>
#include <string>

template<typename T, typename U>
struct KeyValuePair
{
    T key{};
    U value{};
    
    void set(const T& newKey, const U& newValue)
    {
        key = newKey;
        value = newValue;
    }
    
    void print() const
    {
        std::cout << "Key: " << key << ", Value: " << value << std::endl;
    }
};

int main()
{
    KeyValuePair<std::string, int> ageMap;
    ageMap.set("Alice", 25);
    ageMap.print();
    
    KeyValuePair<int, std::string> idMap;
    idMap.set(1001, "Employee");
    idMap.print();
    
    KeyValuePair<std::string, double> priceMap;
    priceMap.set("Coffee", 4.99);
    priceMap.print();
    
    return 0;
}

Output:

Key: Alice, Value: 25
Key: 1001, Value: Employee
Key: Coffee, Value: 4.99

Template with member functions

Template structs can have member functions that also use the template parameters:

#include <iostream>
#include <vector>

template<typename T>
struct SimpleStack
{
    std::vector<T> elements;
    
    void push(const T& item)
    {
        elements.push_back(item);
    }
    
    T pop()
    {
        if (!elements.empty())
        {
            T item = elements.back();
            elements.pop_back();
            return item;
        }
        return T{};  // Return default-constructed value if empty
    }
    
    bool isEmpty() const
    {
        return elements.empty();
    }
    
    size_t size() const
    {
        return elements.size();
    }
};

int main()
{
    SimpleStack<int> intStack;
    
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);
    
    std::cout << "Stack size: " << intStack.size() << std::endl;
    
    while (!intStack.isEmpty())
    {
        std::cout << "Popped: " << intStack.pop() << std::endl;
    }
    
    return 0;
}

Output:

Stack size: 3
Popped: 30
Popped: 20
Popped: 10

Non-type template parameters

Templates can also accept non-type parameters like integers:

#include <iostream>
#include <array>

template<typename T, size_t Size>
struct FixedArray
{
    std::array<T, Size> data{};
    
    T& at(size_t index)
    {
        if (index < Size)
            return data[index];
        
        // For simplicity, return first element if index is out of bounds
        return data[0];
    }
    
    const T& at(size_t index) const
    {
        if (index < Size)
            return data[index];
        
        return data[0];
    }
    
    constexpr size_t size() const
    {
        return Size;
    }
    
    void fill(const T& value)
    {
        data.fill(value);
    }
};

int main()
{
    FixedArray<int, 5> intArray;
    intArray.fill(42);
    
    FixedArray<double, 3> doubleArray;
    doubleArray.at(0) = 1.1;
    doubleArray.at(1) = 2.2;
    doubleArray.at(2) = 3.3;
    
    std::cout << "Int array (size " << intArray.size() << "): ";
    for (size_t i = 0; i < intArray.size(); ++i)
    {
        std::cout << intArray.at(i) << " ";
    }
    std::cout << std::endl;
    
    std::cout << "Double array (size " << doubleArray.size() << "): ";
    for (size_t i = 0; i < doubleArray.size(); ++i)
    {
        std::cout << doubleArray.at(i) << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Output:

Int array (size 5): 42 42 42 42 42 
Double array (size 3): 1.1 2.2 3.3 

Template specialization (brief introduction)

You can provide specialized implementations for specific types:

#include <iostream>
#include <string>

template<typename T>
struct Formatter
{
    std::string format(const T& value)
    {
        return "Value: " + std::to_string(value);
    }
};

// Specialization for std::string
template<>
struct Formatter<std::string>
{
    std::string format(const std::string& value)
    {
        return "Text: \"" + value + "\"";
    }
};

// Specialization for bool
template<>
struct Formatter<bool>
{
    std::string format(const bool& value)
    {
        return "Boolean: " + std::string(value ? "true" : "false");
    }
};

int main()
{
    Formatter<int> intFormatter;
    Formatter<double> doubleFormatter;
    Formatter<std::string> stringFormatter;
    Formatter<bool> boolFormatter;
    
    std::cout << intFormatter.format(42) << std::endl;
    std::cout << doubleFormatter.format(3.14) << std::endl;
    std::cout << stringFormatter.format("Hello") << std::endl;
    std::cout << boolFormatter.format(true) << std::endl;
    
    return 0;
}

Output:

Value: 42
Value: 3.140000
Text: "Hello"
Boolean: true

Real-world example: Generic point class

Here's a practical example of a 2D point that works with different numeric types:

#include <iostream>
#include <cmath>

template<typename T>
struct Point2D
{
    T x{};
    T y{};
    
    // Constructor
    Point2D() = default;
    Point2D(T xVal, T yVal) : x(xVal), y(yVal) {}
    
    // Add two points
    Point2D operator+(const Point2D& other) const
    {
        return Point2D{x + other.x, y + other.y};
    }
    
    // Calculate distance from origin
    double distanceFromOrigin() const
    {
        return std::sqrt(static_cast<double>(x * x + y * y));
    }
    
    // Calculate distance to another point
    double distanceTo(const Point2D& other) const
    {
        T dx = x - other.x;
        T dy = y - other.y;
        return std::sqrt(static_cast<double>(dx * dx + dy * dy));
    }
    
    void print() const
    {
        std::cout << "(" << x << ", " << y << ")";
    }
};

int main()
{
    Point2D<int> intPoint{3, 4};
    Point2D<double> doublePoint{1.5, 2.5};
    
    std::cout << "Integer point: ";
    intPoint.print();
    std::cout << ", Distance from origin: " << intPoint.distanceFromOrigin() << std::endl;
    
    std::cout << "Double point: ";
    doublePoint.print();
    std::cout << ", Distance from origin: " << doublePoint.distanceFromOrigin() << std::endl;
    
    // Add points (note: different types can't be added directly)
    Point2D<int> sum = Point2D<int>{1, 2} + Point2D<int>{3, 4};
    std::cout << "Sum: ";
    sum.print();
    std::cout << std::endl;
    
    // Distance between points of same type
    Point2D<int> point1{0, 0};
    Point2D<int> point2{3, 4};
    std::cout << "Distance between (0,0) and (3,4): " << point1.distanceTo(point2) << std::endl;
    
    return 0;
}

Output:

Integer point: (3, 4), Distance from origin: 5
Double point: (1.5, 2.5), Distance from origin: 2.91548
Sum: (4, 6)
Distance between (0,0) and (3,4): 5

Template best practices

1. Use descriptive template parameter names

// Good: descriptive names
template<typename ElementType, typename SizeType>
struct Array { /* ... */ };

// Less clear: single letters (but T is conventional)
template<typename T, typename S>
struct Array { /* ... */ };

2. Provide meaningful default template arguments when appropriate

template<typename T, size_t DefaultSize = 10>
struct Buffer
{
    T data[DefaultSize];
    // ...
};

// Usage:
Buffer<int> defaultBuffer;      // Uses size 10
Buffer<int, 20> largerBuffer;   // Uses size 20

3. Use const correctness in template member functions

template<typename T>
struct Container
{
    T value;
    
    const T& getValue() const { return value; }  // Const version
    T& getValue() { return value; }              // Non-const version
};

4. Consider type requirements

template<typename T>
struct MathContainer
{
    T value;
    
    // This template requires T to support arithmetic operations
    T double_value() const
    {
        return value + value;  // Requires T to support operator+
    }
};

Common template pitfalls

1. Template instantiation happens at compile time

template<typename T>
struct Container
{
    T data;
    
    void problematic_function()
    {
        T::non_existent_member;  // This will only cause an error if instantiated
    }
};

int main()
{
    Container<int> c;  // OK so far
    // c.problematic_function();  // This would cause compilation error
    
    return 0;
}

2. Each template instantiation is a separate type

template<typename T>
struct Box { T value; };

int main()
{
    Box<int> intBox{42};
    Box<double> doubleBox{3.14};
    
    // intBox = doubleBox;  // ERROR: different types!
    // Box<int> and Box<double> are completely different types
    
    return 0;
}

Key concepts to remember

  1. Class templates allow generic programming - one definition works with multiple types.

  2. Template parameters are specified in angle brackets - template<typename T>.

  3. Template instantiation happens at compile time - the compiler generates specific versions for each type used.

  4. Multiple template parameters are supported - types, integers, and other compile-time constants.

  5. Each template instantiation is a distinct type - Container<int> and Container<double> are different types.

  6. Template specialization allows custom behavior for specific types.

Summary

Class templates are a powerful feature that enables generic programming in C++. They allow you to write flexible, reusable code that works with multiple data types while maintaining type safety. By understanding template syntax, instantiation, and best practices, you can create versatile data structures and utilities that adapt to different types as needed. Templates form the foundation of much of the C++ standard library, including containers like std::vector and std::array, making them an essential concept for effective C++ programming.

Quiz

  1. What is a class template and how does it differ from a regular struct?
  2. How do you specify the template parameters when using a class template?
  3. What happens when you instantiate a template with different types?
  4. How do you create a template with multiple type parameters?
  5. What is template specialization and when might you use it?

Practice exercises

Try these exercises with class templates:

  1. Create a generic Box<T> template that can store any type of value, with methods to set, get, and check if the box is empty.
  2. Create a Pair<T, U> template that can store two different types, with methods to swap the values and compare pairs.
  3. Create a SafeArray<T, Size> template that provides bounds-checked access to a fixed-size array.
  4. Create a template specialization for a Printer<T> template that handles strings differently from numeric types.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion