Chapter 6: Introduction to Class Templates in C++
Declaring and defining class templates, copy constructors, assignment operators, and function objects
In the first section of the book, we have learned about the basic syntax and usage of a function template. In this second part of the book, we will focus on the class templates. Class templates are an essential tool for the programmer to design classes that can work on the generic data type. While class templates are an excellent tool for the programmers, they are slightly more complex than function templates. The syntax and usage may look a bit daunting for new programmers. In this chapter, we will learn about how to declare and define classes with templated parameters step by step with simple examples at each step.
In this chapter, we will cover the following topics:
- Technical Requirements
- Introduction to class templates
- Learning about custom non-template storage class
- Learning about STL storage class vector
- Syntax of Class template declaration
- Defining the member functions of a class template
- Writing copy constructor of a class template
- Writing copy assignment operator of a class template
- Creating an instance of a class template
- Putting it all together
- Learning about function objects
- Summary
- Questions
- Answers
Technical requirements
Most of the examples in this chapter will work with a standard C++11 compiler. Some of the examples in this chapter may require support of a C++14 or higher compiler.
The code files for this chapter can be found on GitHub at Source Code.
Introduction to class templates
So far, we have seen many examples that evolved around template functions in the previous chapters. However, templates are not restricted to functions only. You can write a class with template type parameters as well. A class with template type parameters is called a class template. Class templates enable programmers to write generic programs that can operate on different data types. Class templates act as the container of data or data storage.
In this part of the book, we will dive into the details of class templates. We will start with the basic understanding of class template syntax and then gradually examine other aspects of it.
Learning about custom non-template storage class
As mentioned before, template classes act as data stores. But what is a data store? To understand that let us first declare a rudimentary data store class ourselves as shown in the following listing:
#include <iostream>
#include <iomanip>
class MyStore {
private:
int *store;
size_t len;
size_t curr_len;
public:
explicit MyStore(size_t);
void add_element(int);
double average();
~MyStore();
};
As you can see, we have declared a class MyStore with a pointer type member variable store that points to int type data. The class also has counters to keep track of the total capacity of the store, len, and the current size of the store, curr_len. The member variable curr_len is initialized to 0 in the constructor and is incremented every time a new element is added. The class provides two public methods: add_element() to add new elements to the store, and average(), which returns the average of all the elements in the store.
Now, let us define the member functions of the class. The class MyStore has a single parameter constructor which takes the initial size of the store as the input parameter at the time of object creation. In the single parameter constructor, we allocate the initial memory required for the store using new. The following is the single parameter constructor for the class:
MyStore::MyStore(size_t len) : len(len), curr_len(0)
{
store = new int[len];
}
In the destructor we do the opposite — we deallocate the memory area. The destructor code looks like the following:
MyStore::~MyStore()
{
delete [] store;
}
The class MyStore provides a member function add_element() for adding new elements to the store. The add_element() member function is defined as the following:
void MyStore::add_element(int elem)
{
if(curr_len < len) {
store[curr_len++] = elem;
} else {
std::cout << "Number of elements added exceeds the allocated size" << std::endl;
}
}
In the member function add_element(), we check if the curr_len is within the store’s limit. The member variable len keeps the current size of the store. If we try to add more elements to the store than the store size, a message is printed on the console informing the store size exceeded the maximum allowed size. The MyStore class also provides a member function average() which returns the average of all the elements in the store:
double MyStore::average()
{
auto avg = 0.0;
for(size_t index = 0; index < curr_len; ++index)
avg += store[index];
avg = (double)avg / curr_len;
return avg;
}
Finally, in the main() function, we have created an object of the class MyStore by calling the single parameter constructor:
int main()
{
MyStore s(5); // store created for 5 elements
s.add_element(10);
s.add_element(30);
s.add_element(5);
s.add_element(22);
s.add_element(1);
s.add_element(50); // 6th element added
std::cout << std::fixed
<< std::setprecision(2)
<< s.average()
<< std::endl;
return 0;
}
In the main() function, when we pass the value of 5 to the single parameter constructor, a memory area large enough to accommodate 5 int type data is dynamically allocated. The memory allocation happens in the following line:
store = new int[len];
After the allocation, the pointer store points to a memory area which can fit 5 int type data. Notice that we have called the add_element() function 6 times with different data — that means we are trying to add more elements than the initial size of the store. If you compile and run the program, you will see the following output:
$ g++ custom_storage_class.cpp -o custom_storage_class -Wall -Wextra -Wpedantic -Werror
$ ./custom_storage_class
Maximum store size exceeded.
13.60
So, we have a storage class, MyStore, which can accommodate int type data on request. The class also provides a member function that calculates the average of all the data elements. However, what happens if we want to change the data type of the store? The only option we have is to write another class with the new data type. In the next section, we will see how the STL-provided template container classes can be beneficial in that scenario.
Learning about STL storage class vector
Now that we have implemented a custom storage class, let us look at how we can achieve the same using STL storage classes or containers. One of the most popular STL containers is std::vector. Let us try to understand how STL vectors can help us achieve a similar goal:
#include <iostream>
#include <vector>
#include <iomanip>
int main()
{
std::vector<int> my_vector_store(5);
my_vector_store.push_back(10);
my_vector_store.push_back(30);
my_vector_store.push_back(5);
my_vector_store.push_back(22);
my_vector_store.push_back(1);
my_vector_store.push_back(50);
auto avg = 0.0;
for(auto itr : my_vector_store)
avg = avg + itr;
std::cout << std::fixed
<< std::setprecision(2)
<< (double)avg/6
<< std::endl;
return 0;
}
In this program, we have declared an STL vector container my_vector_store with type int, which means it can hold data of type int. The vector container class provides a member function push_back() to add new data to the container’s back. As elements are added to the vector container, its size keeps on increasing. The internal mechanism of the vector container takes care of allocating memory space for the new elements.
Using std::vector, we can write clean code as most of the mechanisms of allocating and reallocating memory space are internalized by the vector class implementation. The main advantage is that we can write generic code using vectors. If we want a container for the double data type, we can simply declare another vector with data type double. That is possible because the vector is a template class. To give you a bit of an idea how a vector class definition may look like, here is a skeleton of a vector class definition:
template<typename _Tp, typename _Allocator = std::allocator<_Tp> >
class vector
{
… // implementation details
…
}
Looking at the keyword template, you must have guessed this is to do with templates. Yes, vectors are special template classes provided in the STL. In the next section, we will learn about the basic syntax of a template class.
Syntax of Class template declaration
The syntax of a class template is more complex than function templates. We will try to learn it by breaking it down and analyzing each part in steps. The declaration of a template class starts with the keyword template, followed by an opening angle bracket, then the keyword typename to denote the type parameter, the name of the type parameter, and the closing angle bracket. T is used widely as the type parameter name, but you can use any other name if you would like. Like function templates, you can have multiple type parameters, each of which must be declared using the keyword typename or class. In Figure 6.1 we have outlined the basic syntax of a class template:
Following the class template declaration syntax, let us modify the previously written custom storage class declaration and turn it into a template class declaration:
template<typename T>
class MyStore {
private:
T *store;
size_t len;
size_t curr_len;
public:
explicit MyStore(size_t);
void add_element(T);
double average();
~MyStore();
};
As you can see, the template class declaration starts with the keyword template, followed by an angle bracket and the keyword typename. The keyword typename is used to declare the type parameter T. The rules for the type parameter(s) remain the same for class templates as we have seen in function templates. Once we have declared all the type parameters, we close the parameter declaration list using a closing angle bracket.
Inside the class declaration, we can use T for specifying the type of any variable or type of the member function parameters as needed. In this example, we have declared a pointer member variable store of type T. Similarly, while declaring the member function add_element(), we have used type parameter T to declare the type of the function parameter. The member function add_element() takes a single parameter of type T. This completes the template class declaration. Now, we need to define the member functions of the template class. In the next section, we will learn how to do that.
Defining the member functions of a class template
Defining the member functions of a class template is a little clumsy, but it is not difficult. The following is the syntax for defining the member function of a class template:
Let us define the member functions one by one. The first in the list is the single parameter constructor:
template<typename T>
MyStore<T>::MyStore(size_t len) : len(len), curr_len(0)
{
store = new T[len];
}
While defining the member functions, we must use the template<typename T> expression before every member function. Also, the member function name must be preceded by the class name and the type parameter within angle braces.
The MyStore<T> template class’s single parameter constructor is preceded by the class name MyStore along with the type parameter T within the opening and closing angle brackets. This will be the same for all other member function definitions as well. Next, we will define the destructor:
template<typename T>
MyStore<T>::~MyStore()
{
delete [] store;
}
As we have mentioned, the destructor ~MyStore() is preceded by the class type MyStore<T>. In this case, even if the destructor does not use the type parameter T, we must specify the template class type. If we do not use the class type, we will get an error like the following:
invalid use of template-name 'MyStore' without an argument list
We will next define the add_element() member function:
template<typename T>
void MyStore<T>::add_element(T elem)
{
if(curr_len < len) {
store[curr_len++] = elem;
} else {
std::cout << "Maximum size of the store reached." << std::endl;
}
}
The definition starts with the keyword template and then the type parameter list declaration using the keyword typename within the angle brackets. Then we specify the return type of the member function, which is void in this case. Then class type MyStore<T> followed by the scope resolution operator (::) precedes the member function name add_element(). The add_element() has a function parameter elem of type T, representing a new element to be added to the store.
The last member function average() is defined as follows:
template<typename T>
double MyStore<T>::average()
{
auto avg = 0.0;
for(size_t index = 0; index < curr_len; ++index)
avg += store[index];
avg = (double)avg / curr_len;
return avg;
}
This member function average() does not use the type parameter T inside the function body. However, we still have to start the definition using the keyword template and the type parameter list, and the name must be preceded by the class type MyStore<T>. Next, we will learn how to define a vital member of a class template: the copy constructor.
Writing copy constructor of a class template
The copy constructor is used when we initialize an object of the class at the time of its creation using another existing object of that class. Let us say we have a class A, and we have created an instance of it named a. Now, let us say we are creating another instance of class A called b as the following:
A a;
A b = a;
This is a valid expression for initializing a class object at the time of its creation from an existing object of the same class. The compiler internally invokes the copy constructor of the class to initialize the new object. By default, the copy constructor performs shallow copy, which is not suitable for classes with pointer-type data members. Hence, often we define the custom copy constructor of user-defined classes to support deep copy.
Writing the custom copy constructor of a template class is a bit complex but not difficult. Let us now first declare and then define the copy constructor of the MyStore<T> template class.
Declaring the copy constructor of a template class
In the public section of the class, we declare the copy constructor like the following:
public:
…
MyStore<T>(const MyStore<T>&);
…
As you can see, the copy constructor has the same name as the class, followed by the template argument in angle brackets. Then we have specified the function parameter for the copy constructor, which is a const type reference to the MyStore<T> type class. Interestingly, if we drop the template type argument in the copy constructor declaration, it would be fine as well:
public:
…
MyStore(const MyStore&);
…
In the next section, we will see how we can define the copy constructor for the MyStore<T> template class.
Defining the copy constructor
In the copy constructor body, we must ensure that the pointer store is not just copied from one object to the other. Instead we need to first allocate memory for the store pointer with the right size and then copy the bytes of all the elements from the other object into the current object:
template<typename T>
MyStore<T>::MyStore(const MyStore<T>& other)
{
len = other.len;
curr_len = other.curr_len;
store = (T *) malloc(sizeof(T) * len);
memset(store, 0, sizeof(T) * len);
memcpy(store, other.store, len);
}
The definition of the copy constructor starts with the keyword template followed by the template type parameter declaration. Then we specify the template class MyStore<T>, followed by the scope resolution operator indicating the copy constructor is a member of the template class. Note that a copy constructor’s parameter must be passed by reference and not by value — passing by value would require the input object to be copied into the function parameter, which would trigger another copy constructor call, leading to infinite recursion and a stack overflow. The const qualifier ensures the copy constructor does not modify the object from which it is copying. In the copy constructor body, we copy the length of the store from the input object, then allocate memory for the required number of elements, and finally copy the content of the memory pointed to by the store pointer of the input object into the current object.
In the next section, we will learn how to declare and define the copy assignment operator.
Writing copy assignment operator of a class template
The copy assignment operator is used when we first create a new object of a class and then, once the object is created, assign the content of an existing object of the same class into the newly created object.
The difference between the copy constructor and the assignment operator is that in the case of the copy constructor, the new object is created and assigned the content of an existing object at the same time. In the case of the copy assignment operator, we first create the object and then assign the content of an existing object into it — creation and assignment happen in two separate stages. Unlike the copy constructor, the copy assignment operator has a return value.
Declaring copy assignment operator of a template class
The copy assignment operator’s declaration goes in the public section of the class template MyStore<T>:
public:
…
MyStore<T>& operator=(const MyStore<T>&);
…
As you can see in the copy assignment operator’s declaration, we first declare the return type. The return type of the copy assignment operator is a non-constant reference of the class template. The return type is followed by the keyword operator and then the assignment operator (=) itself. The argument list is specified within brackets. The copy assignment operator has only one argument: a const type reference of the template class. In the next section, we will see how we can define the copy assignment operator.
Defining copy assignment operator of a class template
Defining the copy assignment operator of the template class MyStore<T> begins with the keyword template followed by the template type parameter specification. Think of the copy assignment operator as a member function whose name consists of the keyword operator followed by the assignment operator (=) itself, and whose return value is a reference to the template class, i.e. MyStore<T>&:
template<typename T>
MyStore<T>& MyStore<T>::operator=(const MyStore<T>& other)
{
if( this != &other) {
if(!store) {
len = other.len;
curr_len = other.curr_len;
store = new T[len];
memset(store, 0, sizeof(T) * len);
memcpy(store, other.store, len);
} else {
std::cout << "Store not empty!" << std::endl;
}
}
return *this;
}
Inside the copy assignment operator body, we first check if the input object is the same as the current object by comparing their addresses, ensuring we are not copying the same object into itself. Then we check if the store pointer is already initialized. If it is, we print an error message reporting that the object is not empty and return. If the store pointer is not yet initialized, we copy the len and curr_len member variables from the input object, allocate memory, clear it with memset(), and then copy the elements from the input object into the current object using memcpy(). Once everything is done, we return the content of the this pointer — a special pointer which always points to the current object.
Creating an instance of a class template
Creating an instance of a template class is simple. We have to specify the name of the class template followed by the type (specified within angle brackets) and then the new object’s name. The following is the syntax of template class instantiation:
In our present case, we can instantiate an object of the MyStore<T> template class as the following:
MyStore<int> s(5); // store created for 5 elements
This will tell the compiler to create an object of the MyStore<T> template class with an initial store size of 5. Whether we pass any value in the constructor and what we pass is class implementation-specific and will vary on a case by case basis.
Putting it all together
We have discussed all the various bits of a whole program. In this section we will put all the different pieces together and put them into action. We will separate the template class implementation into a header file, following the template code organization discussed in Chapter 5. First we will declare the template class MyStore<T> in a header file:
#ifndef __MYSTORE_TEMPLATE_CLASS__
#define __MYSTORE_TEMPLATE_CLASS__
#include <iostream>
#include <iomanip>
#include <string.h>
template<typename T>
class MyStore {
private:
T *store;
size_t len;
size_t curr_len;
public:
MyStore();
explicit MyStore(size_t);
MyStore<T>(const MyStore<T>&);
MyStore<T>& operator =(const MyStore<T>&);
void add_element(T);
double average();
~MyStore();
};
The declaration of a template class outlines the data being encapsulated within the class and the interfaces to access it. With the template mechanism, we generalize the data type to instantiate the class for different data types. For the complete definition of the member functions please look into the GitHub link: MyStore_template_class.h.
Once we have defined the template class, we are ready to use it in a program. In the following code listing, we have shown a cpp file which creates an object of the template class MyStore<T> and calls various member functions throughout the program. Inside the main() function, we have created an instance of the MyStore<T> template class with an initial store size of 5 and the type specified as int:
📎 template_class_implementation.cpp
#include "MyStore_template_class.h"
int main()
{
MyStore<int> s(5); // store created for 5 elements
s.add_element(10);
s.add_element(30);
s.add_element(5);
s.add_element(22);
s.add_element(1);
s.add_element(50); // 6th element added
std::cout << std::fixed << std::setprecision(2) << s.average() << std::endl;
MyStore<int> s2 = s;
std::cout << std::fixed << std::setprecision(2) << s2.average() << std::endl;
MyStore<int> s3;
s3 = s2;
std::cout << std::fixed << std::setprecision(2) << s3.average() << std::endl;
return 0;
}
You will notice we have deliberately added more elements (a total of 6) than the original store size (5). While adding a new element, the add_element() function checks the current size of the store tracked by curr_len. If the number of elements exceeds the store’s storage capacity held by the len member variable, an error message is printed on the console:
if(curr_len < len) {
store[curr_len++] = elem;
} else {
std::cout << "Maximum size of the store reached." << std::endl;
}
If you compile and run the program, it will give you output like the following:
$ g++ template_class_implementation.cpp -o template_class_implementation -Wall -Wextra -Wpedantic -Werror
$ ./template_class_implementation
Maximum size of the store reached.
13.60
8.00
8.00
If we want to change the datastore type to double, we simply create another instance of the MyStore<T> template class with the type double:
📎 template_class_implementation_double.cpp
#include "MyStore_template_class.h"
int main()
{
MyStore<double> s(5); // store created for 5 elements
s.add_element(23.5);
s.add_element(50.1);
s.add_element(10.4);
s.add_element(12.3);
s.add_element(5.7);
s.add_element(34.8); // 6th element added
std::cout << std::fixed << std::setprecision(2) << s.average() << std::endl;
return 0;
}
In this program, we have created an instance of the MyStore<T> template class with the double data type specified at the time of object creation within the angle brackets. This tells the compiler that the substitution of T has to be double — anywhere T has been used in the class is replaced with the type double. So, this is how we can write generic classes using the template mechanism which can work with different data types. In the next section, we will learn about the function objects.
Learning about function objects
Function objects are function-like objects of a class. By function-like we mean function objects are callable — at least they look like that. In a program, function objects are called the same way as functions are called. To understand this better let us have a look at the following program:
#ifndef __LOGGER_TEMPLATE_CLASS__
#define __LOGGER_TEMPLATE_CLASS__
#include <iostream>
#include <iomanip>
template<typename T>
class Logger {
public:
Logger() = default;
void operator () (const T&);
~Logger() = default;
};
template<typename T>
void Logger<T>::operator()(const T& val)
{
std::cout << __FILE__ << ":" << __LINE__ << " " << val << std::endl;
}
#endif
As you can see we have defined the template class Logger<T> with a template type parameter T in the header file Logger_template_class.h. Inside the class body, we have declared a public function operator() which takes a const reference of type T. The function operator() adds the source file name and the line number, then prints the value on the console. The object of the Logger<T> template class can be used to trace the program flow for debugging purposes. In the following code listing we will write a cpp file which uses our Logger<T> template class for logging purposes:
#include <iostream>
#include <iomanip>
#include "Logger_template_class.h"
int main()
{
Logger<int> logger_i;
logger_i(10);
Logger<double> logger_d;
logger_d(50.9);
Logger<const char *> logger_char;
logger_char("John Grisham");
Logger<std::string> logger_s;
logger_s("Hello World");
return 0;
}
In the main() function, we first created an object of the Logger<T> template class with type int. Then we called that object with an int type value, just like a function call:
logger_i(10);
Similarly, we did the same for the double, C-style string (char *), and std::string type data. If we compile and run the program, the output will look like the following:
$ g++ function_object.cpp -o function_object -Wall -Wextra -Wpedantic -Werror
$ ./function_object
Logger_template_class.h:17 10
Logger_template_class.h:17 50.9
Logger_template_class.h:17 John Grisham
Logger_template_class.h:17 Hello World
Summary
In this chapter, we have learned the importance of class templates. Class templates help in designing generic data stores. We can change the data type of the store with minimal change in the code. The class template also provides generic member functions which can operate on the generic data to provide the same algorithmic solution.
We have learned the syntax of a class template. We have also learned how a class template is declared with examples. Then we have learned how to define the various member functions of a class template. We have defined the no-parameter and single-parameter constructor of a sample class template.
Then we briefly understood the need for a copy constructor in a class. We have defined a copy constructor for the sample storage class. We have finally learned about the copy assignment operator, why it is essential, and how to define the copy assignment operator for the sample storage class. We have also briefly seen how to instantiate a template class.
In the next chapter, we will learn more details about template class instantiation.
Questions
- The STL container vector is an example of a class template in C++. [True/False]
- Class templates are user-defined generic data storage, whereas
std::vectoris a class template that acts as a container provided by the STL library. [True/False] - We cannot create an instance of a user-defined class template; instantiation is only possible for STL-defined class templates. [True/False]
- It is not possible to write generic member functions of a class template that can act on the generic data members. [True/False]
- In the case of a copy constructor, the class’s object is created first, and then at a later time, the object is assigned the content of another existing object. [True/False]
- Copy constructors are used for inline initialization of an object at the time of creation. [True/False]
- Copy constructors return the reference to the same object. [True/False]
- The copy assignment operator is another name for the copy constructor. [True/False]
- Defining a custom copy constructor is unnecessary because there is a default copy constructor already provided by the compiler. [True/False]
- Default copy constructor performs shallow copy, whereas custom copy constructors are defined to provide deep copy. [True/False]
Answers
- True
- True
- False
- False
- False
- True
- False
- False
- False
- True
1 Comment