[C + +] smart pointer

Keywords: C++ pointer

1. Why do I need a smart pointer?

   when using pointers, we generally need to apply for a piece of memory space from the memory for use, but if we forget to release the block space, it will cause memory leakage.
   and if the program has exception handling and throws an exception when applying for memory usage time, there is no space release in this process, which also leads to memory leakage.

Hazards of memory leakage:

Memory leakage of long-running programs has a great impact, such as operating system, background services, etc. memory leakage will lead to slower and slower response and finally get stuck

   therefore, in order to avoid the above situation, only pointers are introduced. Using intelligent pointers for resource management can avoid the above problems.

2. RAII (resource acquisition and initialization)

RAII (Resource Acquisition Is Initialization) is a simple technology that uses the object life cycle to control program resources (such as memory, file handle, network connection, mutex, etc.).

   skillfully use the feature that the compiler will automatically call the constructor and destructor to complete the automatic release of resources. In fact, it is to define a pointer class and use its constructor and destructor to manage resources.

Constructor: obtain resources during object construction and control access to resources to keep them valid throughout the object's life cycle.
Destructors: releasing resources

advantage

  • There is no need to explicitly free resources
  • In this way, the resources required by the object remain valid throughout its life cycle

2.1 principle of raii mode

Note: in classes that can only pointer, the constructor cannot apply for space. Instead, the user applies for space externally and passes it to classes that can only pointer to manage the resource.
   smart pointer just manages resources and finds an appropriate time to release them.

// SmartPtr class designed using RAII idea
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr(){
		if(_ptr)
			delete _ptr;
	}
	// Let this kind of object have pointer like operations
	T& operator*(){
		return *ptr;
	}
	//  ->The pointer can only point to these scenes of objects or structures
	T* operator->(){
		return ptr;
	}
private:
	T* _ptr; // Using classes to manage pointers
};
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	// Delegate the tmp pointer to the sp object and find a terrible girlfriend for the tmp pointer! Take care of you every day until you go die^^
	SmartPtr<int> sp(tmp);
	// _MergeSort(a, 0, n - 1, tmp);
	
	// This assumes that some other logic is handled
	vector<int> v(1000000000, 10);
	// ...
}

    the above only implements resource management, but this class cannot use operations unique to the pointer, so some operators of the pointer need to be overloaded.

2.2 major issues

   if the copy constructor is not provided in the above code, the default copy constructor provided by the compiler will be called when copying. Smart pointers cannot apply for resources and can only manage resources for users. Therefore, deep copies cannot be used instead of shallow copies

How to solve: the C + + standard library provides the optimal solution, which will be introduced below.

2.3 principle of intelligent pointer

Therefore, the principle of smart pointer: raii + operator * () / operator - > () + solves the shallow copy problem

RAII: it can ensure that resources can be automatically released
Operator () / operator - > () *: it can ensure that objects can run as pointers
Solution to shallow copy: it may ensure that resources are not released many times and cause code crash

3. C++98 auto_ PTR (do not use)

Raii + operator * () / operator - > () + solve the shallow copy problem

3.1 solution to shallow copy I

auto_ptr: resource transfer is used to solve shallow copy

When copying or assignment occurs, the resources in the copied object are transferred to the new object, and then the copied object is disconnected from the resources

However, only one pointer can be used at the same time, and the other pointer does not point

// Solution: resource transfer
// auto_ptr<int>  ap2(ap1)
auto_ptr(auto_ptr<T>& ap)
	: _ptr(ap._ptr)
{
	ap._ptr = nullptr;
}

// ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
	if (this != &ap)
	{
		// Here, you need to transfer the resources in the ap to this
		// However, it cannot be transferred directly, because this may have managed resources, otherwise it will cause resource leakage
		if (_ptr)
		{
			delete _ptr;
		}

		// ap can transfer its resources to this
		_ptr = ap._ptr;
		ap._ptr = nullptr;   // Disconnect the ap from the previously managed resources because the resources in the ap have been transferred to this
	}

	return *this;
}

3.2 method 2 for solving shallow copy

Resource management permission transfer

If a copy or assignment occurs, the connection between the original pointer and the resource block is continuously opened, but the release permission of the resource is given to the new pointer, but the problem of wild pointer will be caused, because once the new pointer is destructed, the resource will be released, and the old pointer still points to the space

4. C++11 unique_ptr

Raii + operator * () / operator - > () + solve the shallow copy problem

Shallow copy solution: resource exclusive

Copying is prohibited. A resource can only be managed by one object, and resources cannot be shared among objects.

Usage scenario: it can only be used in scenarios where resources are managed by one object and resources will not be shared.

How
  let the compiler not generate the default copy structure and overload of assignment operators.

In C++11: delete keyword, which can be used to modify the default member function, indicating that the compiler will not generate modified member functions.
In C++98, the copy constructor and assignment operator are overloaded, only declared undefined, and the access permission is set to private.

It is only declared but not defined, and the permission is not set to private. It can still be defined outside the class.

4.1 how to release the diversity of managed resources

  managed resources may be space requested from the heap, file pointers, etc

   customized delegator: the release methods here cannot be written dead. Different release methods should be corresponding to different types of resources.

When only pointer classes are defined, the template parameters are added with the method of resource release. Users can customize the class of resource release and specify the method of resource release themselves. When initializing the smart pointer, the resource release method should be determined. When initializing the template parameters, the resource release class will be passed in.

4.2 Simulation Implementation

// Responsible for releasing new resources
template<class T>
class DFDef
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			delete ptr;
			ptr = nullptr;
		}
	}
};

// Responsible for: release of malloc resources
template<class T>
class Free
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			free(ptr);
			ptr = nullptr;
		}
	}
};

// Close file pointer
class FClose
{
public:
	void operator()(FILE* & ptr)
	{
		if (ptr)
		{
			fclose(ptr);
			ptr = nullptr;
		}
	}
};


namespace test
{
	// T: The type of data placed in the resource
	// DF: resource release method
	// Custom delete
	template<class T, class DF = DFDef<T>>
	class unique_ptr
	{
	public:
		/
		// RAII
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				// Question:_ ptr managed resources: memory space, file pointer, malloc space requested from the heap
				// delete _ptr; //  Note: the method of releasing resources here cannot be written dead. You should find the corresponding method of releasing resources according to different resource types
				// malloc--->free
				// new---->delete
				// Fopen - > Fclose off
				DF df;
				df(_ptr);
			}
		}
		// C++11: the compiler can not generate the default copy structure and the overload of assignment operator -- delete
		// In C++11, the function extension of delete keyword: release the space of new application and modify the default member function with it, indicating that the compiler will not generate
		 unique_ptr(const unique_ptr<T,DF>&) = delete;  // Indicates that the compiler does not generate a default copy constructor
		 unique_ptr<T,DF>& operator=(const unique_ptr<T,DF>&) = delete;// Indicates that the compiler does not generate a default assignment operator overload

	private:
		unique_ptr(const unique_ptr<T,DF>&);
		unique_ptr<T,DF>& operator=(const unique_ptr<T,DF>&);

	private:
		T* _ptr;
	};

	// Users can define methods externally - in unique_ In the PTR class, if the permission is set to private
	//template<class T>
	//unique_ptr<T>::unique_ptr(const unique_ptr<T>& up)
	//{}
}

5. C++11 shared_ptr

Resources can be shared among multiple objects

5.1 how to solve shallow copy

  solve shallow copy by reference counting

Reference count: in fact, it is an integer space that records the number of resource objects. When releasing resources, judge whether other objects are using resources. If not, release resources.

5.2 implementation principle

When copying:

The new object shares all resources with the previous object, and the counter is updated++

On release:

1. Check whether the resource still exists
2. If it exists, check the counter –, and check whether the counter is currently 0

  • =0: the current object is the object that uses the resource last. You need to release the resource and counter.
  • != 0: there are other objects using resources. The current object does not need to be released. If it is released, other objects will become wild pointers.

5.3 problems in multithreading

   the above operations will not cause problems in a single thread, but there are multiple execution streams in multiple threads.
   multiple threads share a resource. Multiple threads need to release the resources they manage at the end. Multiple threads may release the same space at the same time. The operation of the counter is not atomic. Therefore, multiple threads judge that the current resources are still being used, resulting in no release of resources and memory leakage.

   the reference count in the smart pointer object is shared by multiple smart pointer objects. The reference count of the smart pointer in the two threads is + + or –. This operation is not atomic. The reference count was originally 1, + + twice, maybe 2. In this way, the reference count is disordered. This can cause problems such as unreleased resources or program crash. Therefore, only the reference count + +, – in the pointer needs to be locked, that is, the operation of reference count is thread safe

Solution

1. Lock to ensure that the operation of the counter is safe
2. Change the counter to atomic operation

Share in C++11_ PTR itself is thread safe, and the standard library is atomic implemented according to atomic type variables_ int

5.4 defects - circular references

struct ListNode
{
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;
	//weak_ptr<ListNode> next;
	//weak_ptr<ListNode> prev;
	int data;

	ListNode(int x)
		//: data(x)
		: next(nullptr)
		, prev(nullptr)
		, data(x)
	{
		cout << "ListNode(int):" << this << endl;
	}

	~ListNode()
	{
		cout << "~ListNode():" << this << endl;
	}
};

void TestLoopRef()
{
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));

	cout << sp1.use_count() << endl;    // 1
	cout << sp2.use_count() << endl;    // 1

	sp1->next = sp2;
	sp2->prev = sp1;

	cout << sp1.use_count() << endl;    // 2
	cout << sp2.use_count() << endl;    // 2
}

The result of the above code is a memory leak, and the destructor is not called when releasing resources.

What is a circular reference

In the function, sp1 and sp2 refer to each other, and the reference count of the two resources is 2. When you want to jump out of the function, the reference count of the two resources will be reduced by one when the smart pointers sp1 and sp2 are destructed, but the reference count of the two is still 1, resulting in that the resources are not released when you jump out of the function (the destructor number is not called). If you change one of them to weak_ptr is OK. Let's put the shared in the class_ PTR pointer; Change to weak_ptr pointer; The operation results are as follows. In this case, the reference of resource B is only 1 at the beginning. When pb is destructed, the count of B becomes 0 and B is released. When B is released, the count of A will be reduced by one. At the same time, when pa is destructed, the count of A will be 0 and A will be released

5.5 weak_ptr

weak_ The role of PTR, combined with shared_ptr solves the problem of circular reference. weak_ptr cannot manage resources independently
Do not control the object life cycle, point to a shared_ptr managed objects only provide access to managed objects. weak_ The purpose of PTR design is to cooperate with shared_ptr solves the problem of circular reference, resulting in failure to free memory space. weak_ The construction and Deconstruction of PTR will not increase or decrease the reference count

principle

Actually, shared_ptr maintains two counters, one for recording shared_ The number of times PTR is used. One is used to record the number of times weakptr is used. weak_ptr points to resource, shared_ptr use counter does not understand, weak counter + 1

When destroyed

When a resource is shared_ When PTR type objects are shared, use+1 is given
When a resource is broken_ For PTR type objects, give weak+1
During destruction: first give use-1 to confirm that the resources can be released, and then give weak-1 to confirm that the resources can be released

5.5 code implementation

#include <mutex>

namespace test
{
	// shared_ptr: itself is safe --- locking: to ensure shared_ptr's own security
	template<class T, class DF = DFDef<T>>
	class shared_ptr
	{
	public:
		//
		// RAII
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(nullptr)
			, _pMutex(new mutex)
		{
			if (_ptr)
			{
				// At this time, only one newly created object is using this resource
				_pCount = new int(1);
			}
		}

		~shared_ptr()
		{
			Release();
		}

		/
		// Has pointer like behavior
		T& operator*()
		{
			return *_ptr;
		}

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

		T* Get()
		{
			return _ptr;
		}

		//
		// The user may need to get the reference count
		int use_count()const
		{
			return *_pCount;
		}

		///
		// Solution to shallow copy: use reference counting
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
			, _pMutex(sp._pMutex)
		{
			AddRef();
		}

		shared_ptr<T, DF>& operator=(const shared_ptr<T, DF>& sp)
		{
			if (this != &sp)
			{
				// Before sharing with sp, this must clear the previous state
				Release();

				// this can share resources and count with sp
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMutex = sp._pMutex;
				AddRef();
			}

			return *this;
		}

	private:
		void AddRef()
		{
			if (nullptr == _ptr)
				return;

			_pMutex->lock();
			++(*_pCount);
			_pMutex->unlock();
		}

		void Release()
		{
			if (nullptr == _ptr)
				return;

			bool isDelete = false;
			_pMutex->lock();

			if (_ptr && 0 == --(*_pCount))
			{
				// delete _ptr;
				DF df;
				df(_ptr);
				delete _pCount;
				_pCount = nullptr;
				isDelete = true;
			}

			_pMutex->unlock();

			if (isDelete)
			{
				delete _pMutex;
			}
		}
	private:
		T* _ptr;        // Used to receive resources
		int* _pCount;   // Points to the reference count space - records the number of objects using resources
		mutex* _pMutex; // Purpose: to ensure that the operation of reference counting is safe
	};
}

Posted by korporaal on Thu, 02 Sep 2021 15:55:42 -0700