[C + + Experiment 3] understanding of C + + template and smart pointer

Getting started with C + +: templates and smart pointers

In the daily programming process, whether it is to define variables and class members, or to define the incoming parameters or return types of a function, or an implementation method in a class. Generally, we need to give these parameters specific types (which can be int, float or a class). However, in many cases, the implementation methods of some functions are exactly the same except for the incoming parameters. At this time, there are generally two solutions. One is to define two functions. The difference between the two functions is that the types of parameters are different, which is somewhat similar to overload. Another scheme is what this blog will focus on: using template programming.

Template is the basis of generic programming and the blueprint or formula for creating generic classes or functions. Like the library container in STL, iterators and algorithms are examples of generic programming. They all use the concept of template. Next, we discuss the application of templates in C + + in detail.

1. Function template

The general form of template function definition is as follows:

template <typename type> ret-type func-name(parameter list)
{
   // Body of function
}

Type here is the abstract form of template type (a specific type can be passed in when calling this function). typename indicates that type is a template type, or it can be replaced by class keyword.

1.1 general template functions

Here is a simple example of using template functions:

template <class Type> // Function template (all template functions need to be added)
int compare(const Type& v1, const Type& v2)
{
    if(v1<v2) return -1;
    if(v1>v2) return 1;
    return 0;
}

It is worth noting that template functions can only be defined in. h header files.

In this comparison function, we replace the Type of the passed in parameter with the template Type. In this way, the compiler can automatically call the corresponding comparison method under this Type during the comparison operation according to the Type of the passed in parameter.

int main()
{
    const char* a = "aba";
    const char* b = "aaa";
    cout<<compare(a, b)<<endl;
    cout<<compare(6,6)<<endl;
    cout<<compare(1.321,0.23)<<endl;

    return 0;
}

Output:

-1
0
1

1.2 specialized template function

However, in some cases, we hope that the specific implementation of functions based on specific type parameters is different, such as string comparison in the above example, because only using the comparison operator for comparison is actually comparing the address of the string, which is obviously not the way we want to compare. At this time, you can use the specialized template function to write another implementation method:

// Function template specialization declaration
// Many times, we need a template that can deal with all kinds of situations, and it also needs to have special treatment for a specific type. In this case, specialization is needed.
template<>
int compare(const char* const& v1, const char* const& v2);
// Function template specialization
// Implementation can only be in cpp file:
template<>
int compare<const char*>(const char* const& v1, const char* const& v2){
    // Call the strcmp function for comparison
    return strcmp(v1, v2);
}

It is also worth noting that the implementation of function template specialization can only be defined in the. cpp file. This is because the specialized template function is essentially the same as the non specialized function, so the compiler will report an error of multiple definition.

Output of comparison results by adding specialized template functions:

1
0
1

2. Class template

Like template functions, class templates are defined as follows:

template <class type> class class-name {
// Class implementation
}

The specific application of class templates has been well reflected in C + + containers, such as vector,queue or stack. To declare a container variable, you generally need to add a "< >" to tell the compiler the specific type of the passed in parameters.

In the following example, I will write a template class of queue queue (implemented in the form of linked list) and explain more details about the template class:

Declare the template class QueueItem, which generates an instance block in the queue:

// Class template:
// Declare the Queue first, because it is used in the QueueItem:
template<class Type> class Queue;
template<class Type> class QueueItem{
    // constructor 
    QueueItem(const Type &t):item(t), next(0){};
    // Queue elements
    Type item;
    // Pointer to the queue
    QueueItem *next;
    // Queue is a friend class
    friend class Queue<Type>;
    // Output operator overload
    friend ostream& operator<<(ostream& os, const Queue<Type> &q);

public:
    // ++Operator overload (address of pointer + +)
    QueueItem<Type>* operator++(){return next;}
    // *Value operator overload
    Type & operator*(){return item;}
};

Declare the template class Queue, which generates a Queue container instance:

template<class Type> class Queue
{
private:
    // Head pointer
    QueueItem<Type>* head;
    // Tail pointer
    QueueItem<Type>* tail;
    // Destroy
    void destroy();

public:
    Queue():head(0),tail(0){};
    // Copy constructor (copy all)
    Queue(const Queue& q):head(0),tail(0){copy_items(q);}
    // Use template constructor (slice copy)
    template<class It> Queue(It begin, It end):head(0),tail(0)
    {copy_items(begin, end);}
    template<class It> void assign(It begin, It end);
    // copying functions 
    void copy_items(const Queue&);
    template<class It> void copy_items(It begin, It end);
    // Assignment operator overload
    Queue& operator=(const Queue&);
    // Destructor
    ~Queue(){destroy();}
    // Get header pointer element
    Type& front(){return head->item;}
    // Unchangeable acquisition
    const Type& front() const{return head->item;};
    // Join the team
    void push(const Type&);
    // Out of the team
    void pop();
    // Determine whether the queue is empty
    bool empty()const{return head==0;}
    //Output operator overload
    friend ostream& operator<<(ostream& os, const Queue<Type> &q){
        os<<"< ";
        QueueItem<Type> *p;
        for(p=q.head;p;p=p->next){
            os<<p->item<<" ";
        }
        os<<">\n";
        return os;
    }
    // Get header pointer
    const QueueItem<Type>* Head() const{return head;}
    // Get tail pointer
    const QueueItem<Type>* End() const{return (tail==NULL)?NULL:tail->next;}

};

2.1 member template function

Template type parameters are often required for member functions of template classes. Like template functions, template member functions need to be implemented in header files. Next, we implement the member function in the template class Queue:

//Destroy all elements in the queue
template<class Type>
void Queue<Type>::destroy(){
    // Queue out until the header pointer is empty
    while(!empty()){
        pop();
    }
}

//Element out of queue:
template<class Type>
void Queue<Type>::pop(){
    QueueItem<Type> * p = head;
    head = head->next;
    delete p;
}

//Queue elements:
template<class Type>
void Queue<Type>::push(const Type& val){
    QueueItem<Type> * pt = new QueueItem<Type>(val);
    if(empty()){
        // In the case of only one element, the head and tail pointers coincide
        head = tail = pt;
    }else{
        tail->next = pt;
        tail = pt;
    }
}

// Deep copy:
template<class Type>
void Queue<Type>::copy_items(const Queue &orig){
    for(QueueItem<Type> * pt = orig.head;pt;pt=pt->next){
        push(pt->item);
    }
}

// Overload assignment operator:
template<class Type>
Queue<Type>& Queue<Type>::operator=(const Queue& q)
{
    //If you assign yourself a value, you don't do anything
    if(this!=&q){
        // If the original instance is not empty, you need to clear it first
        destroy();
        // Then assign values with the deep copy function
        copy_items(q);
    }
}

// Slice assignment function (delete original instance):
template<class Type>
template<class It>
void Queue<Type>::assign(It beg, It end)
{
    // If the original queue is not empty, it needs to be emptied first
    destroy();
    // Then assign values with the deep copy function
    copy_items(beg, end);
}

// Slice copy function (attached to the original instance):
template<class Type>
template<class It>
void Queue<Type>::copy_items(It beg, It end){
    // Batch queue entry in a given interval:
    while(beg!=end){
        push(*beg);
        // Pointer++
        ++beg;
    }
}

main function:

int main()
{
    Queue<int> q1;
    // If the template class instance is int, the compiler converts the double type to int
    double d = 3.3;
    q1.push(1);
    q1.push(d);
    q1.push(10);
    cout<<q1;

    double num[7] = {2.1, -3.3, 1, -4, 0.5};
    Queue<double> q2(num, num+7);
    cout<<q2;

    Queue<double> q3(num, num+3);
    q2 = q3;
    cout<<q2;
    q2.copy_items(num, num+5);
    cout<<q2;
    q2.assign(num, num + 2);
    cout<<q2;
    return 0;
}

Operation results:

< 1 3 10 >
< 2.1 -3.3 1 -4 0.5 0 0 >
< 2.1 -3.3 1 >
< 2.1 -3.3 1 2.1 -3.3 1 -4 0.5 >
< 2.1 -3.3 >

However, for the push operation of template class, the general non pointer type, the push method will automatically open up a new space, so there is no need to consider the problem of address coincidence. However, if Queue is a pointer type, the push method has defects:

    Queue<const char *> qchar;
    char str[10];
    strcpy(str,"htyan");
    qchar.push(str);
    strcpy(str,"is");
    qchar.push(str);
    strcpy(str,"me");
    qchar.push(str);
    cout<<qchar;

Output:

< me me me >

Member template function specialization

This result is obviously inconsistent with our expected output < htyan is me >, because even if the push method opens up a new space, it is also the address of the pointer, and finally points to the original address space. Therefore, for a Queue of pointer type, it is necessary to specialize its push method, which is called template member function specialization. This implementation takes char * specialization as an example (similarly, it is declared in the header file and defined in the. cpp file):

template<>
void Queue<const char*>::push(const char * const &val){
    // At this time, each time a pointer is passed in, a new memory will be created to copy the value pointed to by the pointer instead of storing the new pointer.
    // In this way, you can avoid repeated modification on the same address.
    char* new_item = new char[strlen(val)+1];
    strncpy(new_item,val,strlen(val)+1);
    QueueItem<const char*> * pt = new QueueItem<const char*>(new_item);
    if(empty()){
        head=tail=pt;
    }else{
        tail->next = pt;
        tail = pt;
    }
}

In addition, there is no difference between delete and delete [] for general basic types of c + +. However, for the specialized member template function push, the space generated by dynamic new must use delete [] to release a series of memory, and the corresponding pop member function also needs specialization:

template<>
void Queue<const char*>::pop(){
    QueueItem<const char*> * p = head;
    delete[] head->item; // That's more
    head = head->next;
    delete p;
}

That is, new and delete. New [] and delete [] are used correspondingly

At this time, execute the call in the main function, and the output is:

< htyan is me >

2.2 template specialization

Similarly, for a class, when the class template needs to handle some types separately, use the template class specialization (note that the class template specialization should not be mixed with the member template function specialization of this class, otherwise an error explicit specialization of 'XXX' after instance will be reported). Let's take a very simple and intuitive example:

First, define a class template:

template<typename T1, typename T2>
class Test
{
public:
	Test(T1 i,T2 j):a(i),b(j){cout<<"template class"<<endl;}
private:
	T1 a;
	T2 b;
};

Full specialization

When a template is fully specialized, it must have a main template class. At the same time, the template type needs to be clear.

// Full specialization
template<>
class Test<int , char>
{
public:
	Test(int i, char j):a(i),b(j){cout<<"Full specialization"<<endl;}
private:
	int a;
	char b;
};

Partial specialization

If a template class contains multiple template types, only some of the template types will be specified when the template is specialized.

template <typename T2>
class Test<char, T2>
{
public:
	Test(char i, T2 j):a(i),b(j){cout<<"Partial specialization"<<endl;}
private:
	char a;
	T2 b;

It is worth mentioning that for function templates, only full specialization but not partial specialization.

Actual combat: full specialization of template class Queue:

A simpler way is that for const char * type, we don't need to specialize the member function push or pop of Queue, but just convert its type to string type (the compiler will automatically help us deal with it). We can deal with it according to the way of string class:

// Queue full specialization
template<>
class Queue<const char*>{
public:
     void Push(const char* str){real_queue.push(str);}
     void Pop(){real_queue.pop();}
     bool isEmpty()  {return real_queue.empty();}
     string front() const {return real_queue.front();}
     friend ostream & operator<<(ostream& os, Queue<const char*> &que){
         os<<que.real_queue;
     }

 private:
    //Here, a queue of string type is defined. All the passed const char * parameters can be converted to string type for processing
     Queue<string> real_queue;
 };

3. Smart pointer

Different from the object-oriented language Java, C + + does not automatically release the applied memory when dynamically allocating memory. Therefore, for many C + + programmers, when using C + + dynamic memory management, if they do not understand the logic of the code, they often produce some confusing bugs. These bugs are usually caused by errors in dynamic memory management: for example, forgetting to release memory, resulting in memory leakage, or releasing memory in advance when there is still pointer reference memory. At this time, the error of pointer reference to illegal memory may occur (this kind of bug is really bald! 👴)

In order to avoid cumbersome memory management problems and use system memory more safely, c + + introduces smart pointer. The difference between smart pointer and conventional pointer is that it can automatically judge whether the memory pointed to still points to it. If there is no conventional pointer pointing to it, it is considered that the life cycle of this memory has ended and is automatically released This area of memory is reserved for others.

For the C++11 standard, there are three built-in smart pointer types:

  1. STD:: unique_ptr < T >: pointer to exclusive resource ownership.
  2. STD:: shared_ptr < T >: pointer to shared resource ownership.
  3. STD:: weak_ptr < T >: observers sharing resources need to be used together with std::shared_ptr without affecting the life cycle of resources.

But in order to consolidate what we have learned, let's implement a smart pointer class ourselves today:

According to the above, the smart pointer needs to have the following functions:

① The smart pointer needs to have a global counter, and the memory address pointed to by the binding is used to judge whether there are other pointers using this address.

② When the counter value of the address pointed to by the smart pointer is 0, this address needs to be released (automatic management).

It should be noted that releasing the memory pointed to by the smart pointer does not mean that it will be released itself. Conversely, it is not true.

#ifndef AUTOPTR_H
#define AUTOPTR_H
#include<iostream>
using namespace std;


// Because the types of data are diverse, smart pointers use template classes
template<class T>
class AutoPtr
{
public:
    AutoPtr(T* pData);
    AutoPtr(const AutoPtr<T>& handle);
    ~AutoPtr();
    //=Heavy load
    AutoPtr<T> & operator=(const AutoPtr<T> & handle);
    void decr();
    //->Overload, directly take Ptr in AutoPtr, that is, directly operate the original data pointed by the smart pointer rather than the smart pointer itself
    T* operator->(){return ptr;}
    const T* operator->() const {return ptr;}
    //*Overload, same as - > overload
    T& operator*(){return ptr;}
    const T& operator*() const {return ptr;}
    friend ostream& operator<<(ostream& os, const AutoPtr<T> &q){
        os<<&q<<'\n';
        return os;
    }


private:
    // Pointer to the data storage memory
    T * ptr = NULL;
    // Record how many users are using this data
    // * is used because it needs to be modified uniformly
    int * user = 0;

};


// Constructor to build an instance that uses a smart pointer
template<class T>
AutoPtr<T>::AutoPtr(T* pData)
{
    ptr = pData;
    // When opening up a space with a smart pointer, the number of users defaults to 1 (because it includes itself)
    user = new int(1);
}
// Destructor:
template<class T>
AutoPtr<T>::~AutoPtr()
{
    cout<<"use "<<this<<" destructor"<<endl;
    // If you are released, the number of users of the variable you point to will be reduced by 1
    decr();
}

//The initial value of the smart pointer is the variable pointed to by another smart pointer:
template<class T>
AutoPtr<T>::AutoPtr(const AutoPtr<T>& handle)
{
    ptr = handle.ptr;
    user = handle.user;
    // Number of users of that variable + 1
    (*user)++ ;
}

//=The operation means to change the data to handle
template<class T>
AutoPtr<T> & AutoPtr<T>::operator=(const AutoPtr<T> & handle)
{
    // Quoting yourself is not pointing to others
    if(this == &handle) return *this;
    //Before I point to others, the number of users of the variable I currently point to is reduced by 1:
    decr();
    ptr = handle.ptr;
    user= handle.user;
    (*user)++;

    return * this;
}

// When the number of users decreases, you should judge whether to release variable memory (= 0)
template<class T>
void AutoPtr<T>::decr()
{
    (*user)-- ;
    if ((*user)==0){
        delete ptr;
        ptr = 0;
        delete user;
        user = 0;
        cout<<"release "<<this<<" points' caches"<<endl;
    }
}
#endif // AUTOPTR_H

Test example:

First of all, the "-" separator is added to more intuitively see which memory is released before the function is finished, which also reflects the intelligence of the smart pointer.

int main()
{
    AutoPtr<Queue<int>> autoq1(new Queue<int>);
    AutoPtr<Queue<int>> autoq2(new Queue<int>);
    AutoPtr<Queue<int>> autoq3(new Queue<int>);
    cout<<"autoq1:"<<autoq1;
    cout<<"autoq2:"<<autoq2;
    cout<<"autoq3:"<<autoq3;
    cout<<"========================="<<endl;
    autoq1->push(10);
    autoq2->push(1);
    autoq3 = autoq1;
    autoq1 = autoq2;
    AutoPtr<Queue<int>> autoq4(autoq3);
    cout<<"-------------------------"<<endl;

    return 0 ;

}

Operation results:

Operation result analysis:

First, the first two sentences operate on the variables pointed to by the smart pointer, and do not involve the change of the number of memory address references.

In the third sentence, autoq3 = autoq1; it starts with the change of the number of memory address references: autoq3 no longer references the original memory address, so the number of original memory address references is reduced by 1. At this time, the number of original memory address references is 0, so the memory address is released, and the terminal outputs release 0x61fea0 points' caches.

In the fourth sentence, autoq1 = autoq2; it also involves the change of memory address references. The original memory address references of autoq1 are reduced by 1. However, since autoq3 still references this memory, this memory will not be released.

In the fifth sentence, autoptr < queue < int > > autoq4 (autoq3); means to create a new smart pointer to the memory pointed to by autoq3. At this time, the number of references to this memory is 2.

However, when the whole program runs, the destructors of all classes that open up space will be called, so these smart pointers will be released. The terminal outputs use 0x61fe98 destructor to release the smart pointer autoq4, and the memory references pointed to by autoq4 will be reduced by 1; then releases autoq3, and outputs use 0x61fea0 destructor, and the memory references pointed to by autoq3 will be reduced to 0, Release the memory and output release 0x61fea0 points' caches. Then release autoq2 and output use 0x61fea8 destructor. The number of memory references pointed by autoq2 is reduced by 1. Then release autoq1 and output use 0x61feb0 destructor. At this time, the number of memory references pointed by autoq1 is reduced to 0. Release the memory and output release 0x61feb0 points' caches. The program ends.

Tags: C++

Posted on Tue, 16 Nov 2021 11:53:41 -0500 by Mad_Mike