Class templates are an essential feature of the C++ programming language that allow you to create generic classes or functions which can work with different data types. Although C does not support class templates natively, they are a fundamental concept for any C++ developer and can be applied indirectly in C through some workarounds. In this guide, we will discuss how to leverage class templates using the C++ subset within C projects.
Class templates offer several benefits in terms of code reusability, flexibility, and efficiency. By creating generic classes that can work with multiple data types, you can write less repetitive code and improve the maintainability of your programs. Furthermore, understanding class templates is essential for transitioning from C to C++ or working on projects that require a combination of both languages.
In this tutorial, we will cover the fundamentals of class templates in C++, including:
Class templates are widely used in software development for creating generic algorithms, containers, and other reusable components. By understanding how to use class templates, you will be better equipped to write efficient and maintainable code for various projects, including game engines, operating systems, and libraries.
Class templates are a mechanism for defining parameterized classes or functions that can operate on multiple data types. The parameters are defined within angle brackets (e.g., <T>
) and represent the type or types that will be used when instantiating the template. Here's an example of a simple class template for a container:
// Example of a basic class template for a container in C++
template <typename T>
class Container {
public:
Container() : data(nullptr), size(0) {}
~Container() { delete[] data; }
void push_back(const T& value) {
if (size >= capacity) {
reserve(capacity * 2);
}
data[size++] = value;
}
size_t size() const { return size; }
const T& operator[](size_t index) const { return data[index]; }
T& operator[](size_t index) { return data[index]; }
private:
T* data;
size_t capacity, size;
void reserve(size_t newCapacity) {
T* temp = new T[newCapacity];
if (data) {
for (size_t i = 0; i < size; ++i) {
temp[i] = data[i];
}
delete[] data;
}
data = temp;
capacity = newCapacity;
}
};
In this example, T
is a placeholder for any valid C++ data type that you want to use with the container. To create an instance of this class template and use it with integers, you would write:
Container<int> myIntContainer; // Instantiate Container template with int as T
myIntContainer.push_back(42); // Add an integer to the container
std::cout << myIntContainer[0] << std::endl; // Print the first element of the container
In this section, we will explore various practical examples using class templates in C++.
Here's an example of a stack class template that can work with any data type for which the push(), pop(), and size() functions are defined:
// Example of a generic Stack class template in C++
template <typename T>
class Stack {
public:
void push(const T& value) {
if (size == capacity) {
reserve(capacity * 2);
}
data[size++] = value;
}
T pop() {
if (isEmpty()) {
throw std::runtime_error("Cannot pop from an empty stack.");
}
return data[--size];
}
size_t size() const { return size; }
bool isEmpty() const { return size == 0; }
private:
T* data;
size_t capacity, size;
void reserve(size_t newCapacity) {
T* temp = new T[newCapacity];
if (data) {
for (size_t i = 0; i < size; ++i) {
temp[i] = data[i];
}
delete[] data;
}
data = temp;
capacity = newCapacity;
}
};
Now that we have a generic stack class template, let's use it with different data types:
#include <iostream>
#include <vector>
using namespace std;
// Instantiate Stack template with int as T
Stack<int> intStack;
void fillIntStack() {
intStack.push(42);
intStack.push(10);
intStack.push(37);
}
// Instantiate Stack template with double as T
Stack<double> doubleStack;
void fillDoubleStack() {
doubleStack.push(3.14);
doubleStack.push(2.718);
doubleStack.push(e); // Magic constant for e (approximately 2.71828)
}
int main() {
fillIntStack();
cout << "Int Stack:\n";
while (!intStack.isEmpty()) {
cout << intStack.pop() << endl;
}
fillDoubleStack();
cout << "\nDouble Stack:\n";
while (!doubleStack.isEmpty()) {
cout << doubleStack.pop() << endl;
}
return 0;
}
What causes it:
// Bad C code example that triggers the error
template <int T>
class MyClass { /* ... */ }; // int is not a valid type for templates
Error message:
error: invalid use of incomplete type 'std::integral_constant<int, T>'
Solution:
Use a valid C++ data type as the template argument (e.g., int
, float
, double
, char
, or user-defined types).
Why it happens: Class templates require valid data types to be used as template arguments, and C++ primitive types like int and float are allowed.
How to prevent it: Make sure you use a valid C++ data type (or user-defined type) when instantiating class templates.
What causes it:
// Bad C code example that triggers the error
template <typename T>
void myFunction(const T& value); // Declare function template, but don't define it
int main() {
myFunction<int>(42); // Instantiate and call the function template with int
}
Error message:
error: undefined reference to 'myFunction<int>'
Solution:
Define the function template body before or after its declaration.
Why it happens: Function templates are not instantiated until they are called with specific arguments, so you must define their bodies for them to work correctly.
How to prevent it: Define the function template body before or after its declaration. If you separate the declaration and definition, make sure to put the definition in a header file (with #ifndef
and #define
guard statements).
What causes it:
// Bad C code example that triggers the error
template <typename T>
class Stack {
public:
void push(const T& value) {
data[size++] = value; // Accessing out-of-bounds memory if size > capacity
}
// ...
};
Error message:
Runtime error: Segmentation fault (core dumped)
Solution:
Use appropriate memory management techniques, such as reserving enough space for the data and expanding the capacity of the container when necessary.
Why it happens: Accessing out-of-bounds memory can lead to segmentation faults, which occur when your program attempts to read or write beyond the allocated memory.
How to prevent it: Use a combination of size
and capacity
variables to manage the memory usage of your container. When adding an element and size
reaches capacity
, expand the capacity by reserving more memory.
What causes it:
// Bad C code example that triggers the error
template <typename T>
void myFunction(const T& value); // Declare function template, but don't define it
template <typename U>
class MyClass {
public:
void myFunction(const U& value) {} // Overloaded function with a different parameter type
};
int main() {
MyClass<int> obj;
obj.myFunction<float>(3.14); // Instantiate and call the overloaded function with float
}
Error message:
error: ambiguous template instantiation for 'myFunction' (with 'T=float' and 'U=int')
Solution:
Either define the function template body to handle all possible template arguments or provide separate overloads for each data type you want to support.
Why it happens: When you have multiple templates with overlapping types, the compiler may not be able to determine which one to use.
How to prevent it: Define a single function template that can handle all possible template arguments, or provide separate overloads for each data type you want to support.
What causes it:
// Bad C code example that triggers the error
template <typename T>
void myFunction(const T& value); // Declare function template, but don't define it
int main() {
myFunction<float>(3.14); // Instantiate and call the function template with float
}
Error message:
error: no instance of function template 'myFunction' matches the argument list — expected 1 arguments, but 0 were provided
Solution:
Modify the function template to accept the required number and types of arguments.
Why it happens: The function template you declared does not match the actual call in the code.
How to prevent it: Ensure that the function template signature matches the arguments provided when calling the function. If necessary, adjust the template parameters or add additional overloads for different argument lists.
.h
or .hpp
) to share class templates across multiple source files.<vector>
, <memory>
, <iostream>
) when needed.