Detailed explanation of c++---smart pointers

Why are there smart pointers?

Based on the previous knowledge, we know that using exceptions may cause some resources to not be released normally, because after the exception is thrown, it will jump directly to the place where the exception is caught, thus skipping some very important code, such as the following situation:

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
    
    
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
}
int main()
{
    
    
	try{
    
    Func();}
	catch (exception& e)
	{
    
    cout << e.what() << endl;}
	return 0;
}

The func function is called in the main function, and the div function is called in the func function. The exception is not caught in the func function, but the exception is caught in the main function, so if an exception occurs, part of the code in the func function will not be executed. This will lead to memory leaks. For example, if the following running result is not divided by 0, the running result will be correct:
Insert image description here
If divided by 0, the space pointed to by p1 and p2 cannot be released normally:
Insert image description here
So in order to solve this problem, we have To re-throw this concept, add the code to catch the exception in the func function, then release the resources in the catch, and finally re-throw the exception, and finally hand it over to the catch in the main function for processing, for example, the following code:

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
    
    
	int* p1 = new int;
	int* p2 = new int;
	try
	{
    
    
		cout << div() << endl;
	}
	catch (exception& e)
	{
    
    
		cout << "delete p1" << endl;
		delete p1;
		cout << "delete p2" << endl;
		delete p2;
		throw invalid_argument("除0错误");
	}
}
int main()
{
    
    
	try{
    
    Func();}
	catch (exception& e)
	{
    
    cout << e.what() << endl;}
	return 0;
}

In this way, you will not forget to release resources when everything is divided by 0. For example, the following running results:
Insert image description here
You can see that even if divided by 0, the above resources can still be released normally, but is it correct to write it this way? Definitely not, because new itself will also throw an exception. When there is insufficient memory but new is used to apply for space, it will cause the space to be opened and an exception will be thrown. So what will be the result of new throwing an exception? Let's discuss by situation. First of all, will there be any problems if p1 throws an exception? The answer is that if p1 throws an exception, it will jump directly to the main function for capture, and p2 has not been developed yet, and p1 has not been successfully developed, which will not cause any memory leak. What if p2 fails to be developed? At this time, it will also jump directly to the main function to capture, but p1 has already opened up space, so if p2 fails to open up space, it will cause the resources requested by p1 to not be released normally, so we also add it to p2 for safety. A try goes up and the following code must be included in the try block, because once the memory application fails, the subsequent call function does not need to be executed, so the code here is as follows:

void Func()
{
    
    
	int* p1 = new int;
	try 
	{
    
    
		int* p2 = new int;
		try{
    
    cout << div() << endl;}
		catch (exception& e)
		{
    
    
			cout << "delete p1" << endl;
			delete p1;
			cout << "delete p2" << endl;
			delete p2;
			throw invalid_argument("除0错误");
		}
	}
	catch (...)
	{
    
    
		//...
	}
}

And the div inside will also throw an exception. This exception may be caught by the outermost catch of the func function. Will this be very troublesome when handling exceptions? And we only applied for two integer variables here, so how to nest try catch if there are three, four or more? So when exceptions are thrown continuously, it is difficult to deal with the try catch statement we learned before. So in order to solve this problem, someone proposed the concept of smart pointers.

Smart pointer simulation implementation

First of all, a smart pointer is a class, and this class needs to handle a variety of data, so this class must be a template class, such as the following code:

template<class T> 
class Smart_Ptr
{
    
    
public:
private:
	T* _ptr;
};

Then we need a constructor and a destructor. The constructor needs a parameter, and then use this parameter to initialize the internal _ptr. There is also a destructor. The destructor uses delete internally to release the space pointed by the pointer. That’s it, then the code here is as follows:

template<class T> 
class Smart_Ptr
{
    
    
public:
	Smart_Ptr(T*ptr)
		:_ptr(ptr)
	{
    
    }
	~Smart_Ptr()
	{
    
    
		delete[] _ptr;
		cout << "~ptr:" <<_ptr <<endl;
	}
private:
	T* _ptr;
};

With this smart pointer class, we can assign the address of the requested resource to the smart pointer object to solve the above problems, for example, the following code:

void Func()
{
    
    
	int* p1 = new int;
	int* p2 = new int;
	Smart_Ptr<int> sp1 = p1;
	Smart_Ptr<int> sp2 = p2;
	cout << div() << endl;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
	throw invalid_argument("除0错误");
}

The running results of the code are as follows:
Insert image description here
You can see that even if a divide-by-0 error occurs, the two applied spaces can be released. The principle is that the life cycle of the smart pointer object belongs to the Func function. When a divide-by-0 error throws an exception, it will be directly Jump to the main function. At this time, the Func function also ends. As soon as it ends the life of the class object, it will also end. When it ends, the destructor will be called to release the space, so the previous problem is solved. Then if Is it possible to solve the exception here when new is thrown? The answer is yes and the principle is exactly the same, so I won’t describe it here. Of course, only the constructor and destructor are not enough to meet our needs. The constructor is responsible for storing resources and the destructor is responsible for releasing resources. Then we also need some functions to help us use these resources, so we can add three The overloaded functions of the operator include dereference overloading, -> operator overloading, and square bracket overloading. The implementation of these three functions is as follows:

template<class T> 
class Smart_Ptr
{
    
    
public:
	Smart_Ptr(T*ptr)
		:_ptr(ptr)
	{
    
    }
	T& operator *(){
    
    return *_ptr;}
	T* operator->(){
    
    return _ptr;}
	T& operator[](size_t pos) {
    
     return _ptr[pos]; }
	
	~Smart_Ptr()
	{
    
    
		delete[] _ptr;
		cout << "~ptr:" <<_ptr <<endl;
	}
private:
	T* _ptr;
};

With these three functions, we can use smart pointers to modify the content pointed to by the address, such as the following code:

int main()
{
    
    
	Smart_Ptr<int> sp1(new int[10]{
    
     1,2,3,4,5 });
	cout << sp1[3] << endl;
	sp1[3]++;
	cout << sp1[3] << endl;
	return 0;
}

The running results of the code are as follows:
Insert image description here
You can see that the function of internal data reading is implemented here. We call the form of the smart pointer above RAII. RAII (Resource Acquisition Is Initialization) is a method that uses the object life cycle to control program resources (such as memory, file handles, network connections, mutexes, etc.). Obtain resources when the object is constructed, then control access to the resources so that they remain valid during the life cycle of the object, and finally
release the resources when the object is destroyed. Through this, we actually entrust the responsibility of managing a resource to An object is obtained, that is to say, the resource and the object are bound together, and the resource will be released when the object ends. This approach has two major benefits: There is no need to explicitly release the resource. In this way, the resources required by the object remain valid throughout its lifetime.

Smart pointers in libraries

Before looking at the smart pointers in the library, let's first take a look at the following code:

void func2()
{
    
    
	Smart_Ptr<int>sp1(new int(10));
	Smart_Ptr<int>sp2(sp1);
}

Run this code and you will find that there is a problem with the smart pointer class we implemented:
Insert image description here
the reason is very simple. There is no copy constructor in the class we implemented, so the compiler automatically generates one, and uses shallow copy to construct it. This causes two smart pointers to point to the same space, so when the function call ends and the object's life cycle ends, delete will be called to destruct the same space twice, so the above error will be reported. So in order to solve this The problem is that we have to implement a copy constructor ourselves, but the copy constructor here cannot be a deep copy because the purpose of our class is to make it like a pointer. When we assign one pointer to another pointer, two Two pointers point to the same space, instead of two pointers pointing to two spaces respectively, so deep copy cannot be used for copy construction here. So how does the library solve this problem? Let's first look at how the earliest auto_ptr can solve this problem.

auto_ptr

Let’s first take a look at the introduction of auto_ptr:
Insert image description here
Insert image description here

The usage method is the same as the smart pointer we implemented ourselves, then we can write the following code:

void func2()
{
    
    
	auto_ptr<int>sp1(new int(10));
	auto_ptr<int>sp2(sp1);
}

Run the code and you will see that the result here is no problem:
Insert image description here
but when we use dereference to view the content pointed to by the pointer, we will find that an error is reported here. For example, the following code:

void func2()
{
    
    
	auto_ptr<int>sp1(new int(10));
	auto_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

The result of running the code is as follows:

Insert image description here
The reason for this error is related to the implementation method of auto_ptr. The solution to auto_ptr is to transfer the management rights, change the original smart pointer to null, and let the new smart pointer point to this space. For example, the following picture: copy
Insert image description here
construction Previously, data was stored in sp1, but after the copy construction, the data in sp1 becomes as follows:
Insert image description here

Therefore, the reason for the above error is to understand the reference access to the null pointer. If we want to implement the above form of copy construction, we must remove the const in the parameter, for example, the following code:

Smart_Ptr(Smart_Ptr<T>& sp)
	:_ptr(sp._ptr)
{
    
    
	sp._ptr = nullptr;
}

So this is the implementation principle of auto_ptr. You don’t need to understand it because no one uses this form. Many companies even explicitly request that this form of smart pointers cannot be used, because the way auto_ptr is used is too unreasonable. Then, in order to solve the problem of auto_ptr being difficult to use, there are unique_ptr and share_ptr/weak_ptr.

unique_ptr

First, let’s take a look at the introduction of this function:
Insert image description here
The operations supported by this smart pointer:
Insert image description here
Then let’s see if using unique_ptr will also cause auto_ptr problems, then the code here is as follows:

#include<memory>
void func2()
{
    
    
	unique_ptr<int>sp1(new int(10));
	unique_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

Run the code and you can see the following results:
Insert image description here
You can see that there will be problems when using unique_ptr, but this problem is different from auto_ptr. It is because the copy constructor has been deleted, so there is a problem. Then through this example we will It can be known that unique_ptr's idea to solve the copy construction problem is to directly prevent the use of copy construction, so the implementation logic here is as follows:

Smart_Ptr(const Smart_Ptr<T>& sp) = delete;

This kind of implementation logic is definitely not very good, so we don't need to understand it too deeply. Let's look at the next form of smart pointer.

shared_ptr

First, let’s take a look at the introduction of this smart pointer:
Insert image description here
Insert image description here
Insert image description here
it looks similar to the above method of use, so let’s test whether this form of smart pointer will cause the previous problems, then the code here is as follows:

void func2()
{
    
    
	shared_ptr<int>sp1(new int(10));
	shared_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	*sp1=20;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

The running results of the code are as follows:
Insert image description here
It can be seen that using share_ptr will neither prevent copying nor leave it blank after copying, and this smart pointer points to the same area as an ordinary pointer. So this is a smart pointer that meets our needs, so how is it implemented? The principle is very simple and is implemented using reference counting. When using smart pointers to store data, we also open up a space and use integers to store it. How many objects are pointed to by this data? When the number of pointed objects is 0, it is destructed. Delete is used in the function to release this space. For example, in the picture below:
Insert image description here
a smart pointer points to two spaces. One space is used to store data. The other space records that the current space is pointed to by several smart pointers. Currently, only the object sp1 points to it. The current count of this space is 1. When we create another object sp2 and point to this space, the picture becomes as follows: Because there is one more
Insert image description here
object pointed to, the count variable becomes 2. When the sp1 object life cycle ends , or when sp1 points to other content, the picture becomes as follows:
Insert image description here
Because there are fewer objects pointed to, the technical variable has changed from 2 to 1. This is the storage principle of share_ptr, but there is a problem here. How to allocate the space of the counting variable? Can it be an ordinary integer variable and be placed in the object? Obviously it can't be because when the value of the counting variable changes, all the internal counting variables pointing to the space object must change. So how do I find these objects, right? The enemy is hiding and I am hiding. Obviously this is difficult. If it is implemented, can it be implemented using static member variables? It seems possible, because no matter how many objects are instantiated by the class, there is only one static variable, and all objects will share this static variable. Then as long as one object modifies this static variable, other objects will follow. Modification, then does this achieve our purpose? Actually no, because although static variables can be viewed by all objects modified by one object, this object refers to all instantiated objects of this class. Some smart pointers point to the integer array arr1, and some smart pointers point to integers. Array arr2, but because there is only one static variable, their internal counting variables all have the same value. Isn’t this illogical, right? So using static variables to count is not possible, so here is just The last remaining method is to use new in the constructor to open a space, record the number of points pointed to in the space, and then add an integer pointer variable to the class to let the pointer point to the opened space. Then the constructor code here is as follows:

template<class T>
class shared_ptr
{
    
    
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{
    
    }
private:
	int* _pcount;
	T* _ptr;
};

Copy construction only requires assigning the values ​​​​of two pointers, and then adding one to the content pointed to by the integer pointer. For example, the following code:

shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
{
    
    
	++(*_pcount);
}

When the destructor releases space, it must first make a judgment. If the count variable of the current object is equal to 1, we will release both spaces. If the count variable is greater than 1, we will subtract the count value. 1 is enough, then the code here is as follows:

~shared_ptr()
{
    
    
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
	}
}

The remaining three operators are overloaded for the same reason. I won’t explain it here, just go to the code:

T& operator*()
{
    
    
	return *_ptr;
}
T* operator->()
{
    
    
	return  _ptr;
}
T& operator[](size_t pos)
{
    
    
	return _ptr[pos];
}

Then we can use the following code to test:

void func2()
{
    
    
	YCF::shared_ptr<int>sp1(new int(10));
	YCF::shared_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	*sp1=20;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

The result of running the code is as follows:
Insert image description here
you can see that the result is normal, and through debugging, you can see that the count variable of sp1 is 1 at the beginning:
Insert image description here
when we complete the copy construction, we can see that the count variables of these two objects are all It becomes 2:
Insert image description here
The addresses pointed to by the internal pointers are also the same, so this means that our implementation method is correct. Then let's take a look at how to implement assignment overloading. First, assignment overloading does not allow itself to Overload by yourself, so first use the if statement to determine whether the passed object and the _ptr inside the object are the same. If they are the same, we will not do any operation. If they are not the same, we will perform the following operations:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    

	}
}

If the pointers are not the same, we first determine whether the count variable of the current class is 1. If it is 1, we release the two spaces of the object and point to the two spaces in the parameters, and add the count variables of the parameters. , if the current count variable is greater than 1, we will decrement our own count variable, and then change the pointing of the two pointers. Then the code here is as follows:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    
		if (--(*_pcount) == 0)
		{
    
    
			delete _pcount;
			delete _ptr;
		}	
		_pcount = sp._pcount;
		_ptr = sp._ptr;
		++(*_pcount);
	}
}

Then we can use the following code to test:

void func2()
{
    
    
	YCF::shared_ptr<int>sp1(new int(10));
	YCF::shared_ptr<int>sp2(new int(20));
	YCF::shared_ptr<int>sp3(sp1);
	YCF::shared_ptr<int>sp4(sp2);
	cout << "*sp1: " << *sp1 << endl;
	cout << "*sp2: " << *sp2 << endl;
	cout << "*sp3: " << *sp3 << endl;
	cout << "*sp4: " << *sp4 << endl;
	sp1 = sp2;
	cout << "*sp1: " << *sp1 << endl;
	cout << "*sp2: " << *sp2 << endl;
	cout << "*sp3: " << *sp3 << endl;
	cout << "*sp4: " << *sp4 << endl;
}

The running results of the code are as follows:
Insert image description here
You can see that the running results here meet our needs. Through debugging, you can see that the addresses pointed to by sp1 and sp3 are the same before assignment, and the reference count is 2. The spaces pointed by sp2 and sp4 are the same. And the reference count value is also 2.
Insert image description here
When we assign sp2 to sp1, we can see that the address pointed to by sp1, sp2, sp4 is the same and the reference count becomes 3, while the reference count of sp3 becomes 1: Then
Insert image description here
this The complete code for the simulation implementation of shared_ptr is as follows:

namespace YCF
{
    
    
	template<class T>
	class shared_ptr
	{
    
    
		public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
    
    }
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
    
    
			if (_ptr != sp._ptr)
			{
    
    
				if (--(*_pcount) == 0)
				{
    
    
					delete _pcount;
					delete _ptr;
				}	
				_pcount = sp._pcount;
				_ptr = sp._ptr;
				++(*_pcount);
			}
			return *this;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
    
    
			++(*_pcount);
		}
		~shared_ptr()
		{
    
    
			if (--(*_pcount) == 0)
			{
    
    
				delete _pcount;
				delete _ptr;
			}
		}
		T& operator*(){
    
    return *_ptr;}
		T* operator->(){
    
    return _ptr;}
		T& operator[](size_t pos){
    
    return _ptr[pos];}
	private:
		int* _pcount;
		T* _ptr;
	};
}

Thread safety issues of smart pointers

When we write code, multiple threads may share the same resource. Will the smart pointer we implemented above cause problems when facing multiple threads? We add a function to verify

int get()
{
    
    
	return *_pcount;
}

This function can help us get the value of the count variable in the object, and then we can use the following code to test:

void func3()
{
    
    
	int n = 50000;
	YCF::shared_ptr<int> sp1(new int(10));
	thread t1([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<int> sp2(sp1);
			}
		});
	thread t2([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<int> sp3(sp1);
			}
		});
	t1.join();
	t2.join();
	cout << sp1.get() << endl;
}

If the running result is always 1, it means the code is safe. If the running result shows other values, it means there is a problem with the above code: you can see that it has been
Insert image description here
Insert image description here
Insert image description here
run multiple times but the results are different each time. Then Why is this? The reason is very simple. Multiple threads are independent of each other when working. When process 1 ++ the counting variable, process 2 will also use this variable to ++, but ++ cannot be completed in one step. It also requires some step, this will lead to a process that has not finished adding one to this variable. Another process will then use this variable to add one. For example, the current value of the counting variable x is 1, and process 1 takes x to perform ++, but ++ is divided into 3 steps. When process 1 reaches the first step, process 2 will use the value of x to perform ++. The result of process 1 after execution is 2, and the result of process 2 after execution is also 2, so this is As a result, we created two smart pointer objects, but the count variable corresponding to this space only increased by 1, but there may not be a problem during destruction. The count variable is decremented by two normally, so this is the problem. We above The execution result is greater than 1 because there was a problem during destruction but there was no problem during construction, so it is greater than 1. How to solve this problem? The answer is to add a lock to
Insert image description here
the mutex object to prevent users from copying:
Insert image description here
and objects pointing to the same space must use the same lock to ensure that there is no conflict in the use of space, so when creating a smart pointer object, you must apply for another space. It is used to store locks, so we have to add a lock type pointer to the class object. In the constructor, the lock pointer points to the newly created lock. During copy construction, the lock pointer is copied. Then the code here is as follows :

template<class T>
class shared_ptr
{
    
    
	public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		,_pmtx(new mutex)
	{
    
    }
private:
	int* _pcount;
	T* _ptr;
	mutex* _pmtx;
};

Then during copy construction, you have to add a lock in front of ++, and then unlock it after the execution of ++ is completed. With the lock, when process 1 executes this code, process 2 can only wait outside, and then execute it after process 1 is unlocked. Lock the code inside and lock the code, then the code for copy construction is as follows:

shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
	,_pmtx(sp._pmtx)
{
    
    
	_pmtx->lock();
	++(*_pcount);
	_pmtx->unlock();
}

The same is true for assignment overloading. The code is locked before ++, and the code is unlocked after ++. Then the code here is as follows:

void Release()
{
    
    
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
	}
	_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    
		Release();
		_pcount = sp._pcount;
		_ptr = sp._ptr;
		_pmtx = sp._pmtx;
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}
	return *this;
}

Then after we have the lock, when we run the above code, we can see that no matter how many times the program is run, the execution result is 1: Although adding a
Insert image description here
lock can help us avoid errors, we still have to perform this lock in the destructor. Delete, can we write it like this?

void Release()
{
    
    
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
		delete _pmtx;
	}
	_pmtx->unlock();
}
~shared_ptr()
{
    
    
	Release();
}

Run the code and you can see such an error:
Insert image description here
Insert image description here
The reason is also very simple because the lock is still in working state when the lock is deleted, so we create a variable flag to record whether the lock can be deleted currently. If we enter the if statement, we Just modify the value of the flag. After the lock is unlocked, we will judge the value of the flag. If the flag is true, we will delete the lock. Then the code here is as follows:

void Release()
{
    
    
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
    
    
		delete _pmtx;
	}
}
~shared_ptr()
{
    
    
	Release();
}

The flag here will not have thread safety issues, because the local variables of multi-threads are stored on the stack, and each thread has its own stack, so there will be no conflicts. The problem with the counting variable is because this variable is stored is on the heap. No matter how many threads we have, there is only one heap, so problems may arise. Through the above changes, we make the internal count variable of the smart pointer thread-safe, but the object pointed to by the smart pointer must be thread-safe. Of? Let's take a look at the following code:

struct Date
{
    
    
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void func3()
{
    
    
	int n = 50000;
	YCF::shared_ptr<Date> sp1(new Date);
	thread t1([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp2(sp1);
				sp2->_day++;
				sp2->_month++;
				sp2->_year++;
			}
		});
	thread t2([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp3(sp1);
				sp3->_day++;
				sp3->_month++;
				sp3->_year++;
			}
		});
	t1.join();
	t2.join();
	cout << "day: " << sp1->_day << endl;
	cout << "month: " << sp1->_month << endl;
	cout << "year: " << sp1->_year << endl;
}

The running results of the code are as follows:
Insert image description here
You can see that the result of the above improvements is to make the counting variable of the smart pointer safe, but it does not make the object pointed to by the smart pointer safe. The principle is also very simple. The improvements we made above are all in Inside the object, and the objects pointed to by smart pointers are outside the object, so the two have nothing to do with each other. If you want to ensure that the pointed content is thread-safe, you must also add a lock when modifying it, then The code here is as follows:

void func3()
{
    
    
	int n = 50000;
	mutex mtx;//这个也可以被捕捉
	YCF::shared_ptr<Date> sp1(new Date);
	thread t1([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp2(sp1);
				mtx.lock();
				sp2->_day++;
				sp2->_month++;
				sp2->_year++;
				mtx.unlock();
			}
		});
	thread t2([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp3(sp1);
				mtx.lock();
				sp3->_day++;
				sp3->_month++;
				sp3->_year++;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	cout << "day: " << sp1->_day << endl;
	cout << "month: " << sp1->_month << endl;
	cout << "year: " << sp1->_year << endl;
}

The running result of the code is as follows:
Insert image description here
Then the content pointed to by the smart pointer and the internal count variable of the smart pointer are thread-safe.

Loop smart pointer

First, we create a class named listnode. The class contains two listnode pointers and an int variable to store data. Then we create a destructor to use as a marker. The code here is as follows:

struct List_Node
{
    
    
	List_Node* prev;
	List_Node* next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};

Then we can use the following code to test:
Insert image description here
You can see that the running result is normal. So if we use shared_ptr to store the pointer here, can it still compile normally? Let's take a look at the following code:

void func4()
{
    
    
	YCF::shared_ptr<List_Node> n1 = new List_Node;
	YCF::shared_ptr<List_Node> n2 = new List_Node;
	n1->next = n2;
	n2->prev = n1;
}

But there is a problem with this modification, because the type of the pointer in the class is List_Node*, and n2 and n1 are both smart pointer types. How can smart pointers be assigned to ordinary pointers? Right, so an error was reported. The solution to the problem is also very simple. Just modify the ordinary pointer in List_Node into a smart pointer. Then the code here is as follows:

struct List_Node
{
    
    
	YCF::shared_ptr<List_Node> prev;
	YCF::shared_ptr<List_Node> next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};

After running the code, the following results will appear:
Insert image description here
The above code does not call the destructor, so why is this? We can draw a picture for analysis. First, each List_Node grows like this:
Insert image description here
Our above code creates two list_node objects, then it becomes like this:
Insert image description here
And we also create two shared pointers pointing to these two objects respectively. , then the picture becomes as follows:
Insert image description here
Then we do another thing, which is to make a shared_ptr in the two list_nodes point to each other, then the picture becomes as follows:
Insert image description here
All the internal count variables become 2. Then the function call ends. We created two shared_ptr in the function, so as the function ends, these two pointers are destroyed. Whenever a shared pointer stops pointing to the space, then it points to this The reference count of other shared pointers in the space will be reduced by one, so the above picture will become like this:
Insert image description here
We say that when the reference count inside the smart pointer becomes 0, the object pointed to by the pointer will be released, that is to say If you want to release the object on the left in the above picture, you must first release the object on the right (because the smart pointer inside the object will be destroyed after it is destroyed), and if you want to release the object on the right, you must first release the object on the left. So A contradiction arises here, so the above code does not show the call to the destructor at the end, so the above problem cannot be solved according to the implementation logic of shared_ptr, so C++ introduces a new smart pointer called weak_ptr, which can point to resources Access resources but it cannot manage resources:
Insert image description here
Insert image description here
we can think of shared_ptr as the big brother, then weak_ptr is its younger brother. weak_ptr does not support RAII, which means it does not support management of resources, but it supports the copy of shared_ptr, that is, it is possible to use shared_ptr to construct weak_ptr. , then here we can use the following code to test it. If the internal part of list_node is implemented by shared_ptr, what will be the running result of the following code:

void func4()
{
    
    
	YCF::shared_ptr<List_Node> n1 = new List_Node;
	YCF::shared_ptr<List_Node> n2 = new List_Node;
	n1->next = n2;
	n2->prev = n1;
	cout << n1.get() << endl;
	cout << n2.get() << endl;
}

Obviously they are all 2
Insert image description here
, but what if the internal content of list_node is modified to weak_ptr? For example, the following code:

struct List_Node
{
    
    
	std::weak_ptr<List_Node> prev;
	std::weak_ptr<List_Node> next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};
void func4()
{
    
    
	std::shared_ptr<List_Node> n1 (new List_Node);
	std::shared_ptr<List_Node> n2 (new List_Node);
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;//得到引用计数的值
	cout << n2.use_count() << endl;
}

The running result of the code is as follows:
Insert image description here
You can see that the reference count here becomes 1, so this is because weak_ptr is only responsible for pointing and not managing. weak_ptr points to a space and does not cause the reference counting of other shared pointers in the space. value, then this is its function. When we find that circular references may occur in some scenarios, we should use weak_ptr to point to instead of shared_ptr. How is weak_ptr implemented? Let's look down.

weak_ptr

Here we will implement a rough logic. The smart pointers in the library will be more complicated than what we implement. So our implementation here is just to allow everyone to better understand the logic here, so the implementation of this container is very simple. , there is only one pointer variable inside it to point to the space, and then the parameterless constructor just initializes the pointer to null, then the code here is as follows:

	template<class T>
	class weak_ptr
	{
    
    
	public:
		weak_ptr()
			:_ptr(nullptr)
		{
    
    }
	public:
		T* _ptr;
	};
}

Then there is the copy constructor, which just assigns the pointer in the passed shared_ptr. Of course, there may be a problem here. The space pointed to by the passed shared_ptr has disappeared, so when implementing this in the library We have also made a judgment, but we only have a rough implementation here, so we won’t consider so much. Then the code here is as follows:

weak_ptr(const shared_ptr<T>& sp)
	:_ptr(sp.get())
	//这里改了一下get是获取内容的地址
{
    
    }

Then the same is true for assignment overloading:

weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	_ptr = sp.get();
	return *this;
}

Finally add a few operator overloaded functions:

// 像指针一样
T& operator*()
{
    
    
	return *_ptr;
}
T* operator->()
{
    
    
	return _ptr;
}

In this way, our weak_ptr has been implemented. Let's use our own weak_ptr to test:

struct List_Node
{
    
    
	YCF::weak_ptr<List_Node> prev;
	YCF::weak_ptr<List_Node> next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};
void func4()
{
    
    
	YCF::shared_ptr<List_Node> n1 (new List_Node);
	YCF::shared_ptr<List_Node> n2 (new List_Node);
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;//得到引用计数的值
	cout << n2.use_count() << endl;
}

The running results are as follows:
Insert image description here
It can be seen that it meets our expectations, then the simulation implementation of weak_ptr is completed.

Custom deleter

Our shared_ptr destructor is implemented as follows:

void Release()
{
    
    
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
    
    
		delete _pmtx;
	}
}
~shared_ptr()
{
    
    
	Release();
}

We say that when we use new to apply for a type space, we use delete to release the space. When we use new to apply for an array space, we use delete [ ] to release the space. But how do we know that the user is pointing to a shared pointer? Single data or an array? For example, the following code:

void func5()
{
    
    
	YCF::shared_ptr<string> n1(new string[10]);
}

The result of running the code is as follows:

Insert image description here
You can see that an error is reported here directly, but the error reported here is not our problem. The same problem will occur when using shared pointers in the library, such as the following code:

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10]);
}

The result of running the code is as follows:
Insert image description here

Then in order to solve this problem, we propose a concept called a custom deleter. By observing the documents in the library, we can see the figure of the custom deleter: then this custom deleter is equivalent to
Insert image description here
a functor, and the deletion method we implement ourselves is in There may be problems when deleting some data. Then you can provide us with a deletion method at this time. If you provide us with the method you provide, we will use the method you provide to delete. For example, there is a problem when deleting the array above. We can provide a release function specially used to release the data of the array, then the code here is as follows:

template<class T>
struct DeleteArray
{
    
    
	void operator()(const T* ptr)
	{
    
    
		delete[] ptr;
		cout << "delete [] "<<ptr<< endl;
	}
};

Then we pass this custom deleter to, for example, the following code:

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10],DeleteArray<string>());
}

Run it again and you will find that there is nothing wrong:
Insert image description here
and not only functors but also lambda expressions can be passed here, for example, the following code:

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10], DeleteArray<string>());
	std::shared_ptr<string> n2(new string[10], [](string* ptr) {
    
    delete[] ptr; });
}

And we can also use share_ptr to open the file and then use lambda plus fclose to close the file when passing the custom deleter, for example, the following code:

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10], DeleteArray<string>());
	std::shared_ptr<string> n2(new string[10], [](string* ptr) {
    
    delete[] ptr; });
	std::shared_ptr<FILE> n3(fopen("test.cpp","r"), [](FILE* ptr) {
    
     fclose(ptr); });
}

Then this is the usage in the library, then we will implement the function of a custom deleter by ourselves.

Implementation of custom deleter

The deleter in the library is passed in the parameter list of the constructor, but the implementation method in the library is very complicated, so we settled for the second best, adding a parameter to the class template to represent the deleter, and then modifying the class. Release:

template<class T>
class default_delete
{
    
    
public:
	void operator()(T* ptr)
	{
    
    
		delete ptr;
	}
};
template<class T, class D = default_delete<T>>
class shared_ptr
{
    
    
public:
	// RAII
	// 保存资源
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{
    
    }
	//template<class D>//这里就是库实现的方法在构造函数上添加模板
	//shared_ptr(T* ptr = nullptr, D del)
	//	:_ptr(ptr)
	//	, _pcount(new int(1))
	//	, _pmtx(new mutex)
	//{}

	// 释放资源
	~shared_ptr()
	{
    
    
		Release();
	}
	void Release()
	{
    
    
		bool flag = false;
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
    
    
			//delete _ptr;
			_del(_ptr);

			delete _pcount;
			flag = true;
		}
		_pmtx->unlock();

		if (flag == true)
		{
    
    
			delete _pmtx;
		}
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;
	D _del;
}

Then we can use the following code to test:

void func5()
{
    
    
	YCF::shared_ptr<List_Node, DeleteArray<List_Node>> n2(new List_Node[10]);
}

The result of the code running is as follows:
Insert image description here
it meets our expected results, but this implementation method is invalid for lambda expressions, because lambda is an anonymous object and what the template needs here is a type, even if decltype is added, it will not work because decltype is obtained at runtime The result, but the result can only be obtained when the template is compiled. The unique_ptr is the same as the principle we implemented, so the lambda expression cannot be put into the unique_ptr, so this is the whole content of this article. I hope everyone can understand it.

Guess you like

Origin blog.csdn.net/qq_68695298/article/details/131625126