Coming Soon
This lesson is currently being developed
Class templates
Create generic classes that work with different types.
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
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 namedT
T
is a placeholder for any type- When you use
Pair<int>
, the compiler replaces all instances ofT
withint
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
-
Class templates allow generic programming - one definition works with multiple types.
-
Template parameters are specified in angle brackets -
template<typename T>
. -
Template instantiation happens at compile time - the compiler generates specific versions for each type used.
-
Multiple template parameters are supported - types, integers, and other compile-time constants.
-
Each template instantiation is a distinct type -
Container<int>
andContainer<double>
are different types. -
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
- What is a class template and how does it differ from a regular struct?
- How do you specify the template parameters when using a class template?
- What happens when you instantiate a template with different types?
- How do you create a template with multiple type parameters?
- What is template specialization and when might you use it?
Practice exercises
Try these exercises with class templates:
- 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. - Create a
Pair<T, U>
template that can store two different types, with methods to swap the values and compare pairs. - Create a
SafeArray<T, Size>
template that provides bounds-checked access to a fixed-size array. - Create a template specialization for a
Printer<T>
template that handles strings differently from numeric types.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions