[C++]– Template Detailed Explanation

Table of contents

1. Why use templates

Second, the function template

1. Function template concept

2. Function template format

3. The principle of function template

4. Instantiation of function templates

(1) Implicit instantiation

(2) Explicit instantiation

(3) The matching principle of template parameters

Third, the class template

1. Class template definition

2. Instantiation of class template

1. Why use templates

In the C language, a function is defined to exchange the values ​​of two variables. If there are multiple types of variables, then the function must also define multiple:

void Swapi(int* p1, int* p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

void Swapd(double* p1, double* p2)
{
    double temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

void Swapc(char* p1, char* p2)
{
    char temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main()
{
    int a = 1, b = 2;
    Swap (&a, &b);

    double c = 1.1, d = 2.2;
    Swapd(&c, &d);

    char e = 'a', f = 'b';
    Swapc(&e, &f);

    return 0;
}

Since the function name modification rule of C language directly uses the function name to generate the symbol table, multiple functions with the same name are not allowed to exist at the same time, and multiple function names must be different, so only multiple similar swap functions can be written. Exchange, and have to redefine other functions. C++ supports function overloading. If the parameter types are different, multiple functions can use the same function name:

void Swap(int& p1, int& p2)
{
    int temp = p1;
    p1 = p2;
    p2 = temp;
}

void Swap(double& p1, double& p2)
{
    double temp = p1;
    p1 = p2;
    p2 = temp;
}

void Swap(char& p1, char& p2)
{
    char temp = p1;
    p1 = p2;
    p2 = temp;
}

int main()
{
    int a = 1, b = 2;
    Swap(a, b);

    double c = 1.1, d = 2.2;
    Swap(c, d);

    char e = 'a', f = 'b';
    Swap(e, f);

    return 0;
}

But if other types of variables have to be exchanged, you have to redefine the function overloading of different parameter types.

Constantly using function overloading has the following problems:

(1) The overloaded functions are only of different types, and the reuse rate of the code is relatively low. As long as a new type appears, the corresponding function needs to be added.
(2) The maintainability of the code is relatively low, and one error may cause all repetitions. load average error

Can we just write a type-independent function to adapt all parameter types?

For example, give the [compiler] a model, and let the compiler use this model to generate code according to different types. C++ adds generic programming: writing generic code that is independent of types is a means of code reuse, and templates are the basis of generic programming.

Second, the function template

1. Function Template Concept

Function templates represent a family of functions. Function templates are type-independent, parameterized when used, and produce a type-specific version of the function based on the type of the actual parameter.

2. Function Template Format

template<typename T1,typename T2,,typename T3,typename T4,……,typename Tn>
return value type function name (parameter list) {}

typename is used to define template parameter keywords, and class can also be used (must not use struct).

With the template, the swap function can be written like this:

#include<iostream>
using namespace std;

//function template 
template < typename T>
 void  Swap (T& t1, T& t2)
 {
    T temp = t1;
    t1 = t2;
    t2 = temp;
}

int main()
{
    int a = 1, b = 2;
    Swap(a, b);

    double c = 1.1, d = 2.2;
    Swap(c, d);

    return 0;
}

F10 – Debug – Window – Monitor – F11, found that Swap(a, b); and Swap(c, d); both call the void Swap(T& t1, T& t2) function:

3. The principle of function template

Actually Swap(a, b); and Swap(c, d); call one function or two functions?

F10 – Debug – Window – Disassembly, and found that they called two different function addresses, that is, two different functions were called, indicating that the template was instantiated after being processed by the compiler, which is convenient for debugging:

principle: 

As shown in the figure below, a template is not a function, it is a mold used by the compiler to generate a specific type of function, handing over to the compiler what should be done by us:

The compiler deduces the formal parameters of the template board by calling the actual parameters, and then deduces whether T is a double type, an int type or a char type, and instantiates and generates 3 codes.

After deducing 3 codes in the preprocessing stage, the middle template does not exist. The compiler converts it into the following 3 functions, and then adjusts the corresponding 3 functions.

The whole process is actually our own laziness, that is, we should have written these three functions ourselves, but we didn’t want to repeat them, so we wrote a template ourselves, and the compiler helped us generate the corresponding code through the template.

Note: If there are multiple parameters, and the parameter types are different, the template cannot be used, because the compiler does not know which type T is passed to.

For example, a is of int type, c is of double type, and the template parameter has only one T, and T does not know whether to deduce to int or to double:

//Function template 
template < typename T>
 T Add (T& t1, T& t2) //The return value type is generic
 {
     return t1 + t2;
}

int main()
{
    int a = 1, b = 2;
    Add(a, b);

    double c = 1.1, d = 2.2;
    Add(c, d);

    return 0;
}

Template parameters can also be used as return values:

#include<iostream>
using namespace std;

//Function template 
template < typename T>
 T Add (T& t1, T& t2) //The return value type is generic
 {
     return t1 + t2;
}

int main()
{
    int a = 1, b = 2;
    Add(a, b);

    double c = 1.1, d = 2.2;
    Add(c, d);

    return 0;
}

4. Instantiation of function templates

Instantiation of function templates: Use function templates with different types of parameters. Template parameter instantiation is divided into implicit instantiation and explicit instantiation.

(1) Implicit instantiation

Implicit instantiation is to let the compiler deduce the actual type of the function template parameter from the actual parameter. As shown above, a and b are of the same type, both of which are int, and the compiler will deduce the actual type T of the template parameter according to the actual parameter passed by the Add function:

Add(a,(int)c);
Add((double)a, c);

The above is implicit instantiation.

But how to make the following code also compile and pass?

template < typename T>
 T Add ( const T& t1, const T& t2) //The return value type is generic
 {
     return t1 + t2;
}

int main()
{
    int a = 1, b = 2;
    Add(a, b);

    double c = 1.1, d = 2.2;
    Add(c, d);

    Add(a, (int)c);
    Add((double)a, c);

    return 0;
}

The compiler doesn’t know whether to deduce T to int or to double, but we can cast the parameters. Before the compiler deduces the type of T, cast the two parameters to the same type:

Add< int >(a, c); //both a and c will be passed to the parameter as int 
Add< double >(a, c); //a and c will be passed to the parameter as double

But the compilation fails:

This is because the implicit type conversion will occur in the cast, c is a double type, and the cast is cast to an int type, a temporary variable of type int will be generated in the middle, and the temporary variable is constant, and the parameters t1 and t2 of the Add function do not use const Modification will lead to enlarged permissions, which is not allowed. Therefore, the parameter type of the Add function should be modified with the const keyword:

int  Add ( int t1, int t2) //The return value type is generic
 {
     return t1 + t2;
}

template < typename T>
 T Add ( const T& t1, const T& t2) //The return value type is generic
 {
     return t1 + t2;
}

int main()
{
    int a = 1, b = 2;
    Add(a, b);

    Add<int>(a, c);

    return 0;
}

In order to make Add(a,c); compile and pass, casting is only one way to solve it, and there is another way: explicit instantiation.

(2) Explicit instantiation

Explicit instantiation is simply specifying the actual type of the template parameter in <> after the function name:

int  Add ( int t1, int t2) //The return value type is generic
 {
     return t1 + t2;
}

template < typename T>
 T Add ( const T& t1, const T& t2) //The return value type is generic
 {
     return t1 + t2;
}

int main()
{

    Add(1, 2);
    Add(1, 2.0);

    return 0;
}

If the types do not match, the compiler will try to perform an implicit type conversion, and if the conversion cannot be successful, the compiler will report an error. 

(3) The matching principle of template parameters

①A non-template function can exist at the same time as a function template with the same name, and the function template can also be instantiated as this non-template function:

template < class T1, class T2, ..., class Tn>
 class template name
{
    // member definition in class 
};

F10 – Debug – Window – Disassembly:

It is found that Add(a,b) matches a non-template function, and the compiler does not need to instantiate the template function:

It is found that Add(a,c) calls the function template, the template function is instantiated, and a non-template function of the same name is generated:

②For non-template functions and function templates of the same name, if other conditions are the same, the non-template function will be called first when mobilized without generating an instance from the template. If the template can produce a function with a better match, then the template will be chosen:

#include<iostream>
#include<assert.h>
using namespace std;

typedef  int VDataType; //limits that the type of VDataType can only be int 
class  vector 
{ 
public :
     //constructor 
    vector ()
        :_a(nullptr)
        , _size(0)
        , _capacity(0)

    {}

    //destructor 
    ~ vector ()
    {
        delete[] _a;
        _a = nullptr;
        _size = _capacity = 0;
    }

    //Read and write the element in the posth position 
    int & operator []( size_t pos)
    {
        assert(pos < _size);
        return _a[pos];
    }

    // Find the size of the array 
    size_t size()
    {
        return _size;
    }

    //Insert node 
    void  push_back ( const  int & x)
     {
         if (_size == _capacity)
        {
            int newCapacty = _capacity == 0 ? 4 : _capacity * 2;
            int* tmp = new int[newCapacty];
            if (_a)
            {
                memcpy(tmp, _a, sizeof(int) * _size);
                delete[] _a;
            }
            _a = tmp;
            _capacity = newCapacty;
        }

        _a[_size] = x;
        ++_size;
    }

private:
    VDataType* _a;
    int _size;
    int _capacity;
};

F10 – Debug – Window – Disassembly:

It is found that Add(1,2) matches the non-function template, and the ready-made Add function is called directly, eliminating the deduction of the template parameter type T, and the compiler does not need to instantiate the template function:

It is found that Add(1,2.0) does not match the non-template function, and the compiler can only generate a more matching Add function based on the actual parameters:

Third, the class template

1. Class template definition

When there are multiple classes, their functions are the same, only the data types are different, you can use class templates:

#include<iostream>
#include<assert.h>
using namespace std;

template < class  T >//Class template definition
 class  vector 
{ 
public :
     //Constructor 
    vector ()
        :_a(nullptr)
        , _size(0)
        , _capacity(0)

    {}

    //destructor 
    ~ vector ()
    {
        delete[] _a;
        _a = nullptr;
        _size = _capacity = 0;
    }

    //Read and write the element at the posth position 
    T& operator []( size_t pos)
    {
        assert(pos < _size);
        return _a[pos];
    }

    // Find the size of the array 
    size_t size()
    {
        return _size;
    }

    //Insert node 
    void  push_back ( const T& x)  //Specify parameter type
     {
         if (_size == _capacity)
        {
            int newCapacty = _capacity == 0 ? 4 : _capacity * 2;
            T* tmp = new T[newCapacty]; //Specify the tmp type so that it can be assigned to _a later 
            if (_a)
            {
                memcpy(tmp, _a, sizeof(int) * _size);
                delete[] _a;
            }
            _a = tmp;
            _capacity = newCapacty;
        }

        _a[_size] = x;
        ++_size;
    }

private:
    T* _a; //Specify member variable type 
    int _size;
     int _capacity;
};

For example, for a linked list, the following code can only insert integer nodes, because the type of VDataType is limited to int:

int main()
{
    delia:: vector < int > v1; //v1 stores int data and generates a vector where T is int 
    v1.push_back( 1 );
    v1.push_back(2);

    for (size_t i = 0; i < v1.size(); i++)
    {
        cout << v1[i] << " ";
    }
    cout << endl;

    for (size_t i = 0; i < v1.size(); i++)
    {
        v1[i] *= 2;
        cout << v1[i] << " ";
    }
    cout << endl;

    delia:: vector < double > v2; //v2 stores double data and generates a vector where T is double 
    v2.push_back( 1.1 );
    v2.push_back(2.2);

    return 0;
}

But now if you want to use this piece of code, let the linked list v1 store int type data, and let another linked list v2 store double type data, how to achieve it? Use class template:

#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;

namespace delia
{
    template < class  T >//Class template definition
     class  vector 
    { 
    public :
         //Constructor 
        vector ()
            :_a(nullptr)
            , _size(0)
            , _capacity(0)

        {}

        //destructor 
        ~ vector ()
        {
            delete[] _a;
            _a = nullptr;
            _size = _capacity = 0;
        }

        //Read and write the element at the posth position 
        T& operator []( size_t pos)
        {
            assert(pos < _size);
            return _a[pos];
        }

        // Find the size of the array 
        size_t size()
        {
            return _size;
        }

        //Insert node 
        void  push_back ( const T& x)  //Specify parameter type
         {
             if (_size == _capacity)
            {
                int newCapacty = _capacity == 0 ? 4 : _capacity * 2;
                T* tmp = new T[newCapacty]; //Specify the tmp type so that it can be assigned to _a later 
                if (_a)
                {
                    memcpy(tmp, _a, sizeof(int) * _size);
                    delete[] _a;
                }
                _a = tmp;
                _capacity = newCapacty;
            }

            _a[_size] = x;
            ++_size;
        }

    private:
        T* _a; //Specify member variable type 
        int _size;
         int _capacity;
    };
}

Vector is not a concrete class, it is a mold for the compiler to generate a concrete class according to the type being instantiated . The above is just the explicit specification of the class template. How to insert int type data into v1 and double type data into v2?

int main()
{
    delia:: vector < int > v1; //v1 stores int data and generates a vector where T is int 
    v1.push_back( 1 );
    v1.push_back(2);

    for (size_t i = 0; i < v1.size(); i++)
    {
        cout << v1[i] << " ";
    }
    cout << endl;

    for (size_t i = 0; i < v1.size(); i++)
    {
        v1[i] *= 2;
        cout << v1[i] << " ";
    }
    cout << endl;

    delia:: vector < double > v2; //v2 stores double data and generates a vector where T is double 
    v2.push_back( 1.1 );
    v2.push_back(2.2);

    return 0;
}

Since there are vector header files in the library, if the header file of #include is included in the library, the compiler will not know whether to adjust the vector in the library or the vector we wrote ourselves. Put the vector in the library in the std namespace, and put the vector written by yourself in another namespace, you can isolate the two vectors:

#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;

namespace delia
{
    template < class  T >//Class template definition
     class  vector 
    { 
    public :
         //Constructor 
        vector ()
            :_a(nullptr)
            , _size(0)
            , _capacity(0)

        {}

        //destructor 
        ~ vector ()
        {
            delete[] _a;
            _a = nullptr;
            _size = _capacity = 0;
        }

        //In the class, read and write the element at position pos: function declaration 
        T& operator []( size_t pos);

        // Find the size of the array 
        size_t size()
        {
            return _size;
        }

        //Insert node 
        void  push_back ( const T& x)  //Specify parameter type
         {
             if (_size == _capacity)
            {
                int newCapacty = _capacity == 0 ? 4 : _capacity * 2;
                T* tmp = new T[newCapacty];
                if (_a)
                {
                    memcpy(tmp, _a, sizeof(int) * _size);
                    delete[] _a;
                }
                _a = tmp;
                _capacity = newCapacty;
            }

            _a[_size] = x;
            ++_size;
        }

    private:
        T* _a; //Specify member variable type 
        int _size;
         int _capacity;
    };

    //Outside the class, read and write the element in the posth position: function definition 
    template < class  T >//Specify the specific type
     T & vector <T> with the template type: : operator []( size_t pos)
    {
        assert(pos < _size);
        return _a[pos];
    }
}

Now you can print the data at a location:

int main()
{
    delia:: vector < int > v1; //v1 stores int data and generates a vector where T is int 
    v1.push_back( 1 );
    v1.push_back(2);

    for (size_t i = 0; i < v1.size(); i++)
    {
        cout << v1[i] << " ";
    }
    cout << endl;

    for (size_t i = 0; i < v1.size(); i++)
    {
        v1[i] *= 2;
        cout << v1[i] << " ";
    }
    cout << endl;

    delia:: vector < double > v2; //v2 stores double data and generates a vector where T is double 
    v2.push_back( 1.1 );
    v2.push_back(2.2);

    return 0;
}

If the function declaration and definition are separated, such as declaring the function inside the class, but defining the function outside the class, you need to define another template parameter at the position of the function definition to let the compiler know that T is a template type, otherwise the compiler will not recognize that T is Where did it come from, what is it:

#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;

namespace delia
{
    template < class  T >//Class template definition
     class  vector 
    { 
    public :
         //Constructor 
        vector ()
            :_a(nullptr)
            , _size(0)
            , _capacity(0)

        {}

        //destructor 
        ~ vector ()
        {
            delete[] _a;
            _a = nullptr;
            _size = _capacity = 0;
        }

        //In the class, read and write the element at position pos: function declaration 
        T& operator []( size_t pos);

        // Find the size of the array 
        size_t size()
        {
            return _size;
        }

        //Insert node 
        void  push_back ( const T& x)  //Specify parameter type
         {
             if (_size == _capacity)
            {
                int newCapacty = _capacity == 0 ? 4 : _capacity * 2;
                T* tmp = new T[newCapacty];
                if (_a)
                {
                    memcpy(tmp, _a, sizeof(int) * _size);
                    delete[] _a;
                }
                _a = tmp;
                _capacity = newCapacty;
            }

            _a[_size] = x;
            ++_size;
        }

    private:
        T* _a; //Specify member variable type 
        int _size;
         int _capacity;
    };

    //Outside the class, read and write the element in the posth position: function definition 
    template < class  T >//Specify the specific type
     T & vector <T> with the template type: : operator []( size_t pos)
    {
        assert(pos < _size);
        return _a[pos];
    }
}

2. Instantiation of class template

Class template instantiation is different from function template instantiation. Class template instantiation needs to be followed by <> after the class template name, and then the instantiated type can be placed in <>. The class template name is not a real class, but the instantiated The result is the real class.

delia:: vector < int > v1; //vector is the class template name, vector<int> is the type

Leave a Comment

Your email address will not be published. Required fields are marked *