C++ Lesson 17: Class Templates

1. What is a C++ Class Template

In C++, class templates allow developers to write a generic class that can handle different data types without the need to write a separate class for each data type. In simple terms, a class template is a blueprint for a class that can generate specific classes based on different type parameters. By using class templates, code reusability and maintainability can be significantly improved.

For example, suppose we want to implement a simple container class called Box to store a value. Without using class templates, we might need to write a Box class for each data type, such as IntBox for integers, DoubleBox for double precision floating-point numbers, StringBox for strings, and so on. This not only results in a large amount of code but also makes maintenance very cumbersome.

However, by using class templates, we only need to write a generic Box class template that can accept any type of parameter. Here is a simple definition of a Box class template:

template <typename T>

In this example, template <typename T> declares a template where typename T indicates that T is a type parameter that can be any data type. In the Box class, we use T to define the member variable value, as well as the parameter and return types of the member functions getValue and setValue. This way, the Box class template can handle data of any type.

When we need to use the Box class to store a specific type of value, we need to instantiate this class template. For example, to create a Box object that stores integers, we can write:

Box<int> intBox(10);

Here, Box<int> is the specific class instantiated from the Box class template, where int is the type parameter. Similarly, to create a Box object that stores strings, we can write:

Box<std::string> stringBox("Hello, C++ Templates!");

In this way, we can use the same class template Box to handle different types of data without having to write a separate class for each data type. This is the basic concept and usage of C++ class templates, which makes our code more generic, flexible, and efficient.

2. Basic Usage of Class Templates

2.1. Declaration and Definition of Class Templates

The declaration and definition of class templates require the use of the template keyword to introduce the template parameter list. The template parameter list can contain one or more type parameters, each of which must be declared with the typename or class keyword (in this case, typename and class have the same meaning).

Here is a simple example of the declaration and definition of a class template Pair, which is used to store two different types of values:

template <typename T1, typename T2>class Pair {private:    T1 first;    T2 second;public:    Pair(T1 f, T2 s) : first(f), second(s) {}    T1 getFirst() const {        return first;    }    T2 getSecond() const {        return second;    }    void setFirst(T1 f) {        first = f;    }    void setSecond(T2 s) {        second = s;    }}

In this example, the Pair class template has two type parameters T1 and T2, which represent the types of the first and second member variables, respectively. The class template includes a constructor, accessors, and mutators, all of which use the template parameters T1 and T2.

2.2. Using Class Templates

When creating objects using class templates, you need to specify the specific type parameters in the angle brackets <> following the class name. This process is called instantiation of the class template. The compiler generates a specific class based on the specified type parameters and then uses this class to create objects.

Here is an example of creating objects using the Pair class template:

// Create a Pair object that stores int and double typesPair<int, double> p1(10, 3.14); // Create a Pair object that stores string and bool typesPair<std::string, bool> p2("Hello", true); std::cout << "p1.first: " << p1.getFirst() << ", p1.second: " << p1.getSecond() << std::endl;std::cout << "p2.first: " << p2.getFirst() << ", p2.second: " << p2.getSecond() << std::endl;

In the above code, Pair<int, double> and Pair<std::string, bool> are specific classes instantiated from the Pair class template, creating objects p1 and p2, and calling their member functions to retrieve the stored values.

2.3. Class Templates as Function Parameters

When class templates are used as function parameters, it is necessary to explicitly specify the specific types of the template parameters, meaning that the parameters must be of a specific instantiated type and cannot be the generic class template itself. For example, suppose there is a function printPair that prints the values of Pair objects:

template <typename T1, typename T2>void printPair(const Pair<T1, T2>& p) {    std::cout << "first: " << p.getFirst() << ", second: " << p.getSecond() << std::endl;}

When calling this function, the argument passed must be an already instantiated Pair object, such as:

Pair<int, double> p(10, 3.14);printPair(p); 

Here, the p passed is an object of type Pair<int, double>, not the Pair class template itself. If you try to pass an uninstantiated class template directly, such as printPair(Pair);, this will result in an error, as the compiler does not know what types T1 and T2 are. Only after explicitly specifying the template parameter types can the compiler generate the correct function instance to handle the corresponding type of Pair object.

3. Advanced Features of Class Templates

3.1. Inheritance of Template Classes

In C++, there can also be inheritance relationships between template classes, but there are some points to note.

First, regarding the handling of the parent class constructor. If the parent class template defines a custom constructor, the child class needs to use the constructor initialization list to call the parent class’s constructor during construction to ensure that the parent class’s member variables are correctly initialized. For example:

template <typename T>class Base {public:    Base(T value) : data(value) {}protected:    T data;};template <typename T>class Derived : public Base<T> {public:    // Use initialization list to call parent class constructor    Derived(T value, int extra) : Base<T>(value), extraData(extra) {}     void printData() {        std::cout << "Base data: " << this->data << ", Extra data: " << extraData << std::endl;    }private:    int extraData;};

In this example, the Derived class template inherits from the Base class template. The constructor of Derived calls the constructor of Base using the initialization list Base<T>(value), passing the value parameter to initialize the data member variable in the Base class, while also initializing its own member variable extraData.

Secondly, when the child class is a template class, there are two ways to specify the type of the parent class when inheriting from the parent class template: one is to directly specify a specific type, and the other is to use the child class’s template parameter to specify the parent class’s type. For example:

// Child class template uses its own template parameter to specify parent class typetemplate <typename T>class Child : public Parent<T> { public:    Child(T a, T b) : Parent<T>(b) {        this->childData = a;    }private:    T childData;};// Child class template directly specifies parent class as a specific typetemplate <typename T>class AnotherChild : public Parent<int> { public:    AnotherChild(T a, int b) : Parent<int>(b) {        this->otherChildData = a;    }private:    T otherChildData;};

In the Child class template, the same template parameter T is used to specify the type of the parent class Parent; while in the AnotherChild class template, the parent class Parent’s type is directly specified as int. If the child class is not a template class, the specific type of the parent class template must be explicitly indicated, as the compiler needs to know the exact layout of the parent class to allocate memory space for the child class objects. For example:

class NonTemplateChild : public Parent<int> { public:    NonTemplateChild(int a, int b) : Parent<int>(b) {        this->nonTemplateData = a;    }private:    int nonTemplateData;};

Here, NonTemplateChild is not a template class, so the type of the parent class Parent is explicitly specified as int during inheritance. In the inheritance hierarchy of template classes, properly handling the calling of parent class constructors and accurately specifying the parent class type is key to ensuring that the code runs correctly and efficiently.

3.2. Internal Declaration and Definition of Regular Template Functions and Friend Template Functions

Declaring and defining regular template functions and friend template functions within class templates is an important method in C++ class template programming.

Regular template functions are declared and defined within class templates similarly to regular member functions, with the addition of the template parameter declaration. For example, in a class template Vector representing a mathematical vector, a regular template function can be defined to calculate the sum of two vectors:

template <typename T>class Vector {public:    Vector(T x, T y) : m_x(x), m_y(y) {}    // Regular template function to calculate the sum of two vectors    template <typename U>    Vector<U> add(const Vector<U>& other) const {        return Vector<U>(m_x + other.m_x, m_y + other.m_y);    }private:    T m_x;    T m_y;};

In this example, the add function is a regular template function that accepts another Vector object as a parameter and returns a new Vector object whose type is determined by the parameter type passed during the call. This approach allows the add function to handle the addition of different types of Vector objects, enhancing code generality.

Friend template functions allow a function to access the private and protected members of a class. When declaring and defining friend template functions within class templates, the friend keyword must be added before the function declaration. For example, to implement a global function that outputs the contents of a Vector object, it can be defined as a friend template function:

template <typename T>class Vector {    // Friend template function declaration and definition    friend std::ostream&amp; operator<<(std::ostream&amp; os, const Vector<T>&amp; vec) {        os << "(" << vec.m_x << ", " << vec.m_y << ")";        return os;    }public:    Vector(T x, T y) : m_x(x), m_y(y) {}private:    T m_x;    T m_y;};

Here, operator<< is a friend template function that is declared as a friend of the Vector class template, allowing it to access the private members m_x and m_y of the Vector object, thus enabling the output of the contents of the Vector object in a specific format to the stream. Friend template functions break the encapsulation boundary of the class, providing the class with the ability to interact with external functions in a special way, but caution is needed to avoid breaking the encapsulation of the class.

3.3. Internal Declaration of Friend Template Functions + External Definition of Friend Template Functions

When a friend template function is declared inside a class template and defined outside, some special issues arise, the most significant of which is the problem of secondary compilation.

During the compilation process of C++, the compiler first compiles the template, generating a “blueprint” of the template. When the template is instantiated, the compiler compiles the template again based on the specific type parameters, generating code of a specific type, a process known as secondary compilation. For friend template functions, if declared inside the class and defined outside, the compiler may not be able to find the definition of that function during the linking phase, as it is not certain what the specific instantiation type is during the first compilation, leading to linking errors.

For example, suppose there is a Complex class template used to represent complex numbers, and there is a friend template function operator<< used to output the complex number:

template <typename T>class Complex {    // Friend template function declaration    friend std::ostream&amp; operator<<(std::ostream&amp; os, const Complex<T>&amp; c); public:    Complex(T real, T imag) : m_real(real), m_imag(imag) {}private:    T m_real;    T m_imag;};// Friend template function external definitiontemplate <typename T>std::ostream&amp; operator<<(std::ostream&amp; os, const Complex<T>&amp; c) {    os << c.m_real << " + " << c.m_imag << "i";    return os;}

In this case, errors may occur during compilation and linking, indicating that the definition of the operator<< function cannot be found. To resolve this issue, it is necessary to perform forward declarations of the class and the function, allowing the compiler to correctly recognize the definition of the friend template function during compilation. Specifically, this involves explicitly stating the type parameters of the template before the friend template function declaration and definition. For example:

// Forward declaration of the classtemplate <typename T>class Complex; // Forward declaration of the friend template functiontemplate <typename T>std::ostream&amp; operator<<(std::ostream&amp; os, const Complex<T>&amp; c); template <typename T>class Complex {    // Friend template function declaration, adding template parameter    friend std::ostream&amp; operator<< <T>(std::ostream&amp; os, const Complex<T>&amp; c); public:    Complex(T real, T imag) : m_real(real), m_imag(imag) {}private:    T m_real;    T m_imag;};template <typename T>std::ostream&amp; operator<<(std::ostream&amp; os, const Complex<T>&amp; c) {    os << c.m_real << " + " << c.m_imag << "i";    return os;}

By using the above forward declarations and explicitly stating the template parameter <T> in the friend template function declaration, it can ensure that the compiler can correctly find the definition of the friend template function during secondary compilation, thus avoiding linking errors and allowing the program to compile and run normally.

3.4. Declaration and Definition in Different Files (Template Functions, Friend Templates)

In C++ programming, it is often desirable to separate the declaration and definition of a class into different files to improve code readability and maintainability. However, for class templates, especially those containing template functions and friend templates, this approach can lead to some special issues that require special handling.

Due to C++’s compilation model, each source file (.cpp file) is compiled independently. When the declaration and definition of a class template are placed in a .h file and a .cpp file respectively, the compiler cannot know the instantiation status of the class template in other files during the compilation of the .cpp file, leading to linking errors when the definitions of template functions and friend templates cannot be found.

To solve this problem, a common method is to use .hpp files (also known as “header file implementation” files). .hpp files typically contain the declaration and definition of class templates, and they can be included by multiple source files just like header files. Specifically, the declaration of the class template and the function declarations are placed in the .h file, while the definition of the class template and the definitions of template functions and friend templates are placed in a similarly named .hpp file. In the source files that need to use the class template, the .hpp file is included instead of the .h file. For example:

#include "Complex.h"template <typename T>Complex<T>::Complex(T real, T imag) : m_real(real), m_imag(imag) {}template <typename T>Complex<T> Complex<T>::operator+(const Complex<T>& other) {    return Complex<T>(m_real + other.m_real, m_imag + other.m_imag);}template <typename T>std::ostream&amp; operator<<(std::ostream&amp; os, const Complex<T>&amp; c) {    os << c.m_real << " + " << c.m_imag << "i";    return os;}

In main.cpp, using the Complex class template:

#include "Complex.hpp"int main() {    Complex<int> c1(1, 2);    Complex<int> c2(3, 4);    Complex<int> c3 = c1 + c2;    std::cout << c3 << std::endl;    return 0;}

By this method, the definitions in the .hpp file will be compiled in the source files that include it, ensuring that the definitions of template functions and friend templates can be correctly found, thus solving the linking issues caused by separating the declaration and definition of class templates. Another method is to explicitly instantiate all required template types in the .cpp file, but this method is not flexible enough and requires prior knowledge of all possible instantiation types, so in practice, using .hpp files is more common.

4. Common Problems and Solutions with Class Templates

During the use of C++ class templates, developers often encounter various problems. Mastering these common problems and their solutions is crucial for writing efficient code.

4.1. Separation Compilation Issues

As mentioned earlier, there are challenges with the separation compilation of C++ class templates. When the declaration and definition of class templates are placed in header files (.h) and source files (.cpp) respectively, unresolved external symbol errors can easily occur during the linking phase. This is because the instantiation of templates occurs during compilation, and each translation unit needs to access the template definition to correctly instantiate it, while separation compilation leads to inconsistent access to the template definition across different units during compilation.

To solve this problem, there are mainly two methods. One is to use explicit instantiation, specifying the template types that need to be instantiated in the source file. For example, for the Complex class template:

// Complex.cpp#include "Complex.h"// Explicit instantiation of Complex<int>template class Complex<int>; // Explicit instantiation of Complex<double>template class Complex<double>; 

This approach requires prior knowledge of all possible instantiation types and is not flexible.

The second method is to use the include model, placing the definitions of class templates in header files (.hpp) and including that .hpp file in the source files where needed. As in the previous example of the Complex class template, the declaration is placed in Complex.h, and the definition is placed in Complex.hpp, which is included in main.cpp. This method is more general and effectively solves the separation compilation problem, making it a commonly used approach in practical projects.

4.2. Function Object Usage Issues

When using function objects as parameters for class templates, issues such as function object type mismatches or improperly defined member functions can easily arise. For example, when using the findMax function template to find the maximum value in an array, the function object passed must contain a correctly defined comparison function.

template <typename T, typename Comparator>const T&& findMax(const std::vector<T>& arr, Comparator cmp) {    int maxIndex = 0;    for (int i = 1; i < arr.size(); i++) {        if (cmp.isLessThan(arr[maxIndex], arr[i])) {            maxIndex = i;        }    }    return arr[maxIndex];}class CaseInsensitiveCompare {public:    bool isLessThan(const std::string&& lhs, const std::string&& rhs) const {        return stricmp(lhs.c_str(), rhs.c_str()) < 0;    }};

If the isLessThan function in the CaseInsensitiveCompare class has parameter types or return types that do not match what the findMax function template expects, or if there are spelling errors in the function name, it will lead to compilation errors. To resolve such issues, it is necessary to carefully check the definition of the function object to ensure that its member function signatures match the requirements of the template function. Additionally, when defining function objects, consider their generality and maintainability to avoid overly complex logic, so they can be correctly used in different template functions.

Leave a Comment