C++--21. Smart pointer

Posted by Huijari on Wed, 15 Dec 2021 00:58:04 +0100

We know that there is no gc in C + +. The resources from new/malloc need to be released manually. At this time, some problems will occur. 1 Forget to release, 2 In case of abnormal security, these problems will lead to resource leakage and serious problems. Therefore, our smart pointer appears

Why do I need a smart pointer?
Memory leak
Use and principle of intelligent pointer
C++11 and boost Relationship between smart pointers in
RAII Extended learning

 

Why do I need a smart pointer?

In fact, in the final analysis, in order to prevent memory leakage, prevent the loss of resources when we forget to release resources or throw exceptions between malloc and free

Memory leak

What is a memory leak: a memory leak is a situation where a program fails to release memory that is no longer used due to negligence or error. Memory leakage does not mean the physical disappearance of memory, but the application loses control of a certain section of memory due to design errors, resulting in a waste of memory.
Harm of memory leakage: long-term running programs have memory leakage, which has a great impact, such as operating system, background services, etc. memory leakage will lead to slower and slower response and finally get stuck

Let's look at this code first

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("Divide by 0 error");
	return a / b;
}
void f1()
{
	int* p1 = new int;
	int* p2 = new int;
	int* p3 = new int;
	int* p = new int;
    try
    {
        cout<<div()<<endl;
    }
    catch(...)
    {
	   div();
	   delete p;
       throw;
    }
}
	int main()
	{
		try
		{
			f1();
		}
		catch (exception& e)
		{
			cout << e.what() << endl;
		}
		
		system("pause");
		return 0;
	}

It can be seen that although we set catch in the main function to receive the thrown exceptions, we new multiple objects, and each object may fail to open up, resulting in exceptions. At this time, we can't judge which object is thrown from, and we can't set catch for each object, That's why we have smart pointers to solve this problem

Use and principle of intelligent pointer

RAII

RAII ( Resource Acquisition Is Initialization )It's a kind of Using object lifecycle to control program resources (such as memory, file handle, network connection, mutex, etc.).
Get resources at object construction time Then, control the access to the resource to keep it valid throughout the life cycle of the object, Finally, in the object deconstruction When to release resources . In this way, we actually entrust the responsibility of managing a resource to an object. This approach has two major benefits:
There is no need to explicitly free resources.
In this way, the resources required by the object remain valid throughout its lifetime.

Let's look at such a piece of code

This is the principle of our smart pointer. We can see that the intelligent pointer is actually a template class. In fact, the class is Trusteed into the intelligent pointer by the object that new comes out, so that it can help us manage the release of resources. No matter the function ends normally or throws exception, it will cause the sp object's life cycle to arrive, and then call the destructor.

This is the idea of our RAII

Principle of intelligent pointer

Above SmartPtr You can't call it a smart pointer yet because it doesn't yet have pointer behavior. Pointers can be dereferenced or through -> To access the content in the indicated space, so: AutoPtr In the template class, you also need to * , -> Under overload, it can be used like a pointer
// RAII + like a pointer
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			_ptr = nullptr;
		}
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

We need to overload the operator * and operator - > of the smart pointer to make it really use like a pointer

Summarize the principle of smart pointer:
1. RAII characteristic
2. heavy load operator* and opertaor-> , with pointer like behavior

At this time, there is actually a problem, which is also the place of C + + pit. Let's see what happens if the following code is executed

int* sp1 = new int;
	int* sp2 = sp1

In fact, the two simple statements are to copy and divide sp2, so that sp1 and sp2 point to the int from new at the same time. At this time, because it is an intelligent pointer, the resource will be released automatically. At this time, the scene of releasing resources many times will appear

This is bound to go wrong, so how can we solve this problem?

std::auto_ptr

C++98 Version is available in the library auto_ptr The management right is transferred
public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
        // ap1 = ap2
		auto_ptr<T>& operator=(const auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (_ptr)
					delete _ptr;

				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

We can see that in C++98, the solution is very simple. When sp1 is copied and constructed by sp2, both pointers point to int. at this time, set the previous sp1 to null, leaving only sp2. At this time, sp2 manages the resources of int, and the management right is transferred from sp1 to sp2. However, this method is not good, but it is only allowed, In fact, it is an early design defect, which is prohibited by general companies

	bit::auto_ptr<int> ap1(new int);
	bit::auto_ptr<int> ap2 = ap1;
	 *ap1 = 1; Dangling collapse

In the ap2 = ap1 scenario, ap1 is suspended, and an error will be reported when accessing. If you are not familiar with its features, you will be cheated

std::unique_ptr

C++11 began to provide more reliable unique_ptr, this method is very simple and rough. It prevents copying, that is, it prevents copying and assignment
// C++11  unique_ptr
	// Copy proof. Simple and rough, recommended
	// Defect: if there is a scene that needs to be copied, it cannot be used
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		unique_ptr(unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

The defect of this method is also obvious, that is, when we need to copy, this method won't work

std::shared_ptr

From the name, we can see that shared pointers can share and point to an object. The purpose is to copy without the problem of hanging the auto pointer above. Reference counting is used here. Now the problem comes. Where should reference counting be added?

First of all, you can't add it inside the pointer. If you add it inside, each smart pointer will have a counter. It won't accumulate or add static when - it will affect other pointers, so what should we do?

In fact, our solution is to set count as a pointer so that sp1 and sp2 point to this pointer at the same time, so that count can be responsible for the common counting of sp1 and sp2

However, this operation is not thread safe, because when we count one object + +, it is possible to count another object + + at the same time. At this time, it is not safe to add it to a pointer

shared_ptr The principle of is to realize multiple by reference counting shared_ptr Sharing resources between objects .
1. shared_ptr Inside, A count is maintained for each resource to record that the resource is shared by several objects .
2. stay When the object is destroyed ( That is, the destructor call ) , it means that you do not use the resource, and the reference count of the object is reduced by one.
3. If the reference count is 0 , it means that you are the last object to use the resource, The resource must be released ;
4. If not 0 , it means that other objects are using the resource besides themselves, The resource cannot be released Otherwise, other objects become wild pointers.
	// C++11  shared_ptr
	// Reference count, can be copied
	// Defects: circular references
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)//Initialization list
			:_ptr(ptr)
			, _pcount(new int(1))//When it is pointed to an object, the initial count is 1
			, _pmtx(new mutex)
		{}

		shared_ptr(const shared_ptr<T>& sp)//copy construction 
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)//Introduction counter
			, _pmtx(sp._pmtx)//Introduce mutex
		{
			add_ref_count();//Count + 1
		}

		// sp1 = sp4
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)//Overload operator=
		{
			if (this != &sp)
			{
				// Subtract the reference count, and release the resource if I am the last object to manage the resource
				release();
				
				// I started managing resources with you
				_ptr = sp._ptr;
				_pcount = sp._pcount;//Shared counter
				_pmtx = sp._pmtx;//Shared lock

				add_ref_count();//Count + 1
			}

			return *this;
		}

		void add_ref_count()//Lock + + before and after
		{
			_pmtx->lock();

			++(*_pcount);//For the pcount pointer + 1, if the integer cannot meet the desired effect, we need to make the pointer type so that sp1sp2 all point to it

			_pmtx->unlock();
		}

		void release()//Resources cannot be released at the same time
		{
			bool flag = false;//Used to mark whether the lock can be released

			_pmtx->lock();
			if (--(*_pcount) == 0)//When it is the last smart pointer to the object
			{
				if (_ptr)
				{
					cout << "delete:" << _ptr << endl;//Start resource release
					delete _ptr;
					_ptr = nullptr;
				}

				delete _pcount;//Release counter
				_pcount = nullptr;
				flag = true;//The lock needs to be released because the timer has decreased to 0
			}
			_pmtx->unlock();

			if (flag == true)
			{
				delete _pmtx;//Release lock
				_pmtx = nullptr;
			}
		}


		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

		T* get_ptr() const
		{
			return _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->() 
		{
			return _ptr;
		}
	private:
		T* _ptr;

		// Record how many objects share management resources together, and the last destructor releases resources
		int* _pcount;
		mutex* _pmtx;
	};

std::shared_ Thread safety of PTR

1. The reference count in the smart pointer object is shared by multiple smart pointer objects, and the reference count of the smart pointer in the two threads is the same ++ or -- , this operation is not atomic. The reference count was 1 , ++ Twice, maybe 2. So the reference count is messed up. This can cause problems such as unreleased resources or program crash. Therefore, the count can only be referenced in the pointer++ , -- It needs to be locked, that is, the operation of reference counting is thread safe.
2. The objects managed by smart pointers are stored on the heap and accessed by two threads at the same time, which will lead to thread safety problems.

Circular reference

In fact, we defined share_ptr has solved many problems, but there is still an obvious problem. Let's take a look at this code first

    ListNode* n1 = new ListNode;
	ListNode* n2 = new ListNode;

	n1->_next = n2;
	n2->_prev = n1;

	delete n1;
	delete n2;

This is the use of our general pointer, and the destructor will be called normally when running

    wxy::shared_ptr<ListNode> spn1(new ListNode);
	wxy::shared_ptr<ListNode> spn2(new ListNode);

	cout << spn1.use_count() << endl;
	cout << spn2.use_count() << endl;

	

Let's look at this again. Change the pointer to a smart pointer. When we try to run this code, we find that there is a problem. spn1 and spn2 are not destructed. Why?

In fact, this is because it causes the problem of circular reference and restricts each other

We can see that when we first create two smart pointers to point to the object respectively, the counters are both 1. At this time, we point the two pointers to each other respectively, and the counters reach 2. At this time, we go out of the scope and start destruct, spn1 destruct, reference count-1, spn2 destruct, reference count-1. At this time, both counters become 1, and they are managed by the other party, This is a problem. My release is managed by you, and your release is managed by me. My life cycle needs to end, and your life cycle needs to end. However, we contain each other, and no one can end. At this time, it is impossible to deconstruct the object. There is a problem here

So how do we solve this problem?

C + + introduces another pointer, weak intelligent pointer

weak_ptr

// Strictly speaking, weak_ptr is not a smart pointer because it does not have RAII resource management mechanism
	// Dedicated to shared_ Circular reference of PTR
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr() = default;//Declare private

		weak_ptr(const shared_ptr<T>& sp)//The parameter is share_ptr
			:_ptr(sp.get_ptr())//Get pointer
		{}

		weak_ptr<T>& operator = (const shared_ptr<T>& sp)
		{
			_ptr = sp.get_ptr();

			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

We found that, in fact, wake_ In fact, PTR does not increase the count. Remove the shell share_ptr

// Circular reference
	spn1->_spnext = spn2; // Solution: use weak_ptr, do not increase the reference count
	spn2->_spprev = spn1;

	cout << spn1.use_count() << endl;
	cout << spn2.use_count() << endl;

Relationship between smart pointer in C++11 and boost

History of smart pointers
There is no gc (garbage collection period) in C + +, so it is a problem that the applied resources need to be released, especially when encountering abnormal security problems, which is particularly difficult to deal with
If you don't pay attention, there will be a memory leak. Memory leakage leads to less and less memory available to the program, and many operations in the program need memory. That will cause the program to be basically paralyzed, so we try to eliminate the memory leakage problem.
Therefore, the intelligent pointer based on RAII idea is developed, but since there is no gc pit, the intelligent pointer is introduced
The smart pointer has experienced more than ten years of development
Phase I:
auto is introduced in C++98 for the first time_ ptr, but auto_ The design of ptr has significant defects and is not recommended.
Phase II:
C + + officials didn't do anything in the next ten years. A group of cattle were angry and thought the C + + library was too simple, so they set up an unofficial community and wrote a library called boost. The smart pointer is rewritten in the boost library. Note that there are many other implementations in the boost library, scoped_  ptr/scoped_  _ array anti copy version
shared_  ptr/shared_ array # refers to the count version
weak_ ptr .
Phase III:
Smart pointer is introduced into C++11. It is slightly changed with reference to the implementation of boost. In fact, C++11 is similar to R-value reference, move statement, etc. it also refers to bootunique_ PTR (reference scoped_ptr)
shared_ ptr
weak_ ptr

Custom remover (understand)

In fact, the custom remover is to reformulate the destructor by using imitation functions and overloads for objects that cannot be recognized by smart pointers, such as arrays, malloc objects, fopen files, etc

// Custom remover -- (understand)
#include<memory>
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1;
	int _a2;
};

template<class T>
struct DeleteArry
{
	void operator()(T* pa)
	{
		delete[] pa;
	}
};

struct Free
{
	void operator()(void* p)
	{
		cout << "free(p)" << endl;

		free(p);
	}
};

struct Fclose
{
	void operator()(FILE* p)
	{

		cout << "fclose(p)" << endl;

		fclose(p);
	}
};

int x4()
{
	std::shared_ptr<A> sp1(new A);
	std::shared_ptr<A> sp2(new A[10], DeleteArry<A>());
	std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());
	std::shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());

	return 0;
}

RAII extended learning

RAII In addition to designing smart pointers, the idea can also be used to design guard locks to prevent deadlock caused by abnormal security
// Lock management guard designed using RAII idea
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lock)//Constructor saves resources, locks and references to ensure the same lock
		:_lk(lock)
	{
		_lk.lock();
	}

	~LockGuard()
	{
		cout << "Unlock" << endl;
		_lk.unlock();//Destructor unlock
	}

	LockGuard(LockGuard<Lock>&) = delete;
	LockGuard<Lock>& operator=(LockGuard<Lock>&) = delete;
private:
	Lock& _lk;  // Note that this is a reference
};


//void f()
//{
//	mutex mtx;
//	mtx.lock();
//
//	//func() / / it is assumed that func function may throw an exception. In this case, it is a deadlock
//
//	mtx.unlock();
//}

void f()
{
	mutex mtx;
	LockGuard<mutex> lg(mtx);

	cout << div() << endl;   // Suppose div function may throw exceptions
}

The lock also uses the RAII idea to set up a guard to solve the problem

Supplement: memory leak

Memory leak:

1. What is a memory leak?

Memory embroidered building generally means that we have applied for a resource, but the resource is not used, but we forget to release it, or it is not released because of abnormal security and other problems

2. What is the harm of memory leakage?

If we apply for the memory and do not release it, if the process ends normally, the memory will also be released

Generally, if a program encounters a memory leak, it is OK to restart it. However, if it runs for a long time, the program that cannot be restarted will encounter a memory leak, which will do great harm, such as the operating system and services on the server. The harm is that these programs run for a long time, the unused memory is not released, and there are more and more memory leaks, resulting in the failure of many service operations because the container stores data, Memory is needed to open files, create sockets, send data, and so on

 3. How to solve the memory leak problem

a. Be careful when writing code

b. Where it is difficult to handle, use smart pointers to manage and prevent in advance

c. If a memory leak is suspected or has occurred, you can use the memory leak tool to detect it and solve it afterwards. For example, valgrind is a powerful tool under Linux. You can also try other tools

Topics: C++