[The road to C++ practice] 32. Smart pointer

insert image description here
Every day without dancing is a disappointment to life

1. Why do we need smart pointers?

For exceptions, they will jump after being caught, as follows:

#include<iostream>
using namespace std;

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
    
    
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = new int[20];

	try
	{
    
    
		cout << div() << endl;
	}
	catch (...)
	{
    
    
		delete[] p1;
		delete[] p2;
		throw;
	}

	delete[] p1;
	delete[] p2;
}

int main()
{
    
    
	try
	{
    
    
		Func();
	}
	catch (exception& e)
	{
    
    
		cout << e.what() << endl;
	}

	return 0;
}

For the above code, if p1 is abnormal when it is new, it will be caught by the catch in the main function and jump directly to the outermost. Since there is no need to release if new is not successful, if div throws an exception, it will be caught by the catch in Func capture. Then p1 succeeds, p2 throws an exception, and the exception generated by p2's application for heap space will be directly caught by the catch in main. At this time, the program continues to run downward from main, but because new applies for memory in the heap, even if the function is jumped out, the application space will not be returned to the OS as the function stack frame is destroyed, so memory is generated leakage. Therefore, in order to avoid this situation, it is necessary to prevent p2 from jumping out of the function directly after failing to apply for memory, or at least wait until p1 releases the space before jumping out, thus giving p1 a gap to release space and avoiding memory leaks.

Then, you need to continue to nest a layer of try-catch in the Func function to match p2, as shown in the following code:

#include<iostream>
using namespace std;

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
    
    
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = nullptr;
	try
	{
    
    
		p2 = new int[20];
		try
		{
    
    
			cout << div() << endl;
		}
		catch (...)
		{
    
    
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch (...)
	{
    
    
		//...
	}
	delete[] p1;
	delete[] p2;
}

int main()
{
    
    
	try
	{
    
    
		Func();
	}
	catch (exception& e)
	{
    
    
		cout << e.what() << endl;
	}

	return 0;
}

That is to say, another layer of try catch is set to correspond to the situation of p2 throwing an exception, and in //..., it needs to be processed according to the specific situation. As above, the memory of p1 needs to be released. In addition, the situation of re-throwing an exception in div should also be considered Also needs to be dealt with here. If you add new several times, you need to nest several try catches, which is very unrealistic. It can be seen that the exception thrown by new itself cannot be solved in this way. In order to prevent memory leaks caused by heap space not being released in time when an exception is thrown, smart pointers are introduced.

2. An example of smart pointers solving exceptions thrown by new

#include<iostream>
using namespace std;

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

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


void Func()
{
    
    
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	SmartPtr<int> sp1(p1);
	int* p2 = new int[20];
	SmartPtr<int> sp2(p2);

	cout << div() << endl;
}

int main()
{
    
    
	try
	{
    
    
		Func();
	}
	catch (exception& e)
	{
    
    
		cout << e.what() << endl;
	}

	return 0;
}

image-20230630164909555

After learning about classes and objects, we know that the destructor will be automatically executed before the stack frame is released. That is to say, for the above code, even if p2 fails to apply for heap space, jump to main, and the stack frame of Func will be destroyed when it jumps out. The destructor of sp1 will also be executed before the stack frame is destroyed, and the destructor of sp1 contains the delete operation to release p1, which is equivalent to automatically releasing the space of sp1, avoiding the memory leak problem.

Then, it will be much easier to construct directly through the constructor and omit p1 and p2, but Smartptr, as a class defined by ourselves, cannot be dereferenced like p1 and p2, so if you want to dereference directly, you can consider dereferencing operator overloading :

#include<iostream>
using namespace std;

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
template<class T>
class SmartPtr
{
    
    
public:
    //RAII
    //保存资源
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
    
    }
    //释放资源
	~SmartPtr()
	{
    
    
		delete[] _ptr;
		cout << _ptr << endl;
	}
    //像指针一样
    T& operator*()
    {
    
    
        return *_ptr;
    }

    T& operator[](size_t pos)
    {
    
    
        return _ptr[pos];
    }
private:
	T* _ptr;
};
void Func()
{
    
    
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);
    *sp1 = 10;
    sp1[0]--;
    cout << *sp1 << endl;
	cout << div() << endl;
}
int main()
{
    
    
	try
	{
    
    
		Func();
	}
	catch (exception& e)
	{
    
    
		cout << e.what() << endl;
	}
	return 0;
}

image-20230701155156109

In this way, it is no different from pointers, and it can also prevent memory leaks caused by exceptions. It can also be seen that operator overloading solves many problems.

The above Smartptr can be roughly divided into two parts:

  • RAII: The design idea of ​​this Smartptr constructor and destructor
  • Overload: like a pointer.

What is RAII?

3. The use and principle of smart pointers

3.1 RAII

RAII (Resource Acquisition Is Initialization) is a simple technique that uses the object life cycle to control program resources (such as memory, file handles, network connections, mutexes, etc.).

Acquire resources when the object is constructed, then control access to the resources so that they remain valid throughout the life of the object, and finally release the resources when the object is destructed. In this way, we actually entrust the responsibility of managing a resource to an object. This approach has two advantages:

  • No need to explicitly release resources
  • In this way, the resources required by the object remain available throughout its lifetime.

3.2 Problems with SmartPtr

For smart pointers, the idea of ​​RAII+ like a pointer makes it have very practical functions, but there are certain problems in copying:

int main()
{
    
    
   
    SmartPtr<int> sp1(new int);
    SmartPtr<int> sp2 = sp1;
    return 0;
}

image-20230701163908409

The value of sp2 is the same as that of sp1. When the function ends, the destructor will be called. Since it points to the same location, it will be destructed twice, and an error will occur.

Obviously, it can be solved by using a counter, and deep copy does not meet the original intention of pointer assignment.

Smart pointers are already available in the C++ library, such as auto_ptr, weak_ptr, share_ptr, unique_Ptr, etc. There are different ways to solve the above copying problems:

3.3 std::auto_ptr

std::auto_ptr documentation

The smart pointer of auto_ptr is provided in the C++98 version of the library. The use and problems of auto_ptr demonstrated below.

The realization principle of auto_ptr: the idea of ​​transfer of management rights –> object dangling

image-20230701164812021

image-20230701164828659

Because in order to prevent the error of destructing twice after copying, auto_ptr transfers the management right, so if after such an operation, ap1 is assigned again, a null pointer error will occur.

The implementation steps of the simple version of auto_ptr are as follows:

  1. Acquire resources in the constructor, release resources in the destructor, and use the life cycle of the object to control resources.

  2. Overload the * and -> operators so that the auto_ptr object behaves like a pointer.

  3. In the copy constructor, construct the current object with the resource managed by the passed object, and empty the pointer of the resource managed by the passed object.

  4. In the copy assignment function, the resources managed by the current object are released first, then the resources managed by the incoming object are taken over, and finally the pointer of the resource managed by the incoming object is set to NULL.

#pragma once
#include<iostream>
using namespace std;
namespace cfy
{
    
    
	template<class T>
	class auto_ptr
	{
    
    
	public:
		//保存资源
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{
    
    }
		//释放资源
		~auto_ptr()
		{
    
    
			delete[] _ptr;
			cout << _ptr << endl;
		}

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

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

		T& operator[](size_t pos)
		{
    
    
			return _ptr[pos];
		}
	private:
		T* _ptr;
	};
}

int main()
{
    
    
	cfy::auto_ptr<int> ap1(new int);
	cfy::auto_ptr<int> ap2 = ap1;
	(*ap1)++;
	(*ap2)++;
	return 0;
}

image-20230701172228566

It can be seen that auto_ptr is an incomplete and difficult to use smart pointer. (Many companies explicitly request that they cannot be used)

3.4 std::unique_ptr

unique_ptr implementation principle: anti-copy

unique_ptr is a smart pointer introduced in C++11. unique_ptr solves the copying problem of smart pointers by means of anti-copying, that is, simply and rudely prevents copying of smart pointer objects, which also ensures that resources will not be released multiple times. for example:

int main()
{
    
    
	std::unique_ptr<int> up1(new int(0));
	//std::unique_ptr<int> up2(up1); //error
	return 0;
}

But anti-copying is actually not a good way, because there are always some scenes that need to be copied.

The implementation steps of the simple version of unique_ptr are as follows:

  1. Acquire resources in the constructor, release resources in the destructor, and use the life cycle of the object to control resources.
  2. Overload the *AND ->operator so that the unique_ptr object behaves like a pointer.
  3. Use C++98 to declare the copy constructor and copy assignment function as private, or use C++11 to add after these two functions to =deleteprevent external calls.

code show as below:

template<class T>
class unique_ptr
{
    
    
    public:
    //RAII
    unique_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        {
    
    }
    ~unique_ptr()
    {
    
    
        if (_ptr != nullptr)
        {
    
    
            cout << "delete: " << _ptr << endl;
            delete _ptr;
            _ptr = nullptr;
        }
    }
    //可以像指针一样使用
    T& operator*()
    {
    
    
        return *_ptr;
    }
    T* operator->()
    {
    
    
        return _ptr;
    }
    //防拷贝
    unique_ptr(unique_ptr<T>& up) = delete;
    unique_ptr& operator=(unique_ptr<T>& up) = delete;
    private:
    T* _ptr;//管理的资源
};

3.5 std::shared_ptr

Basic design of std::shared_ptr

shared_ptr is a smart pointer introduced in C++11. shared_ptr solves the copy problem of smart pointers by means of reference counting .

  • Each managed resource has a corresponding reference count, which records how many objects are currently managing this resource.
  • When an object is added to manage this resource, the reference count corresponding to the resource is ++, and when an object no longer manages this resource or the object is destroyed, the reference count corresponding to the resource is performed –.
  • When the reference count of a resource is reduced to 0, it means that no object is managing the resource, and the resource can be released.

Through this reference counting method, multiple objects can be supported to manage a certain resource together, that is, the copy of the smart pointer is supported, and the resource will be released only when the reference count key corresponding to a resource is reduced to 0, so it is guaranteed The same resource will not be released multiple times.

for example:

int main()
{
    
    
	cfy::shared_ptr<int> sp1(new int(1));
	cfy::shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl; //2

	cfy::shared_ptr<int> sp3(new int(1));
	cfy::shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl; //2
	return 0;
}

Explain: The use_count member function is used to obtain the reference count corresponding to the resources managed by the current object.

Analog implementation of shared_ptr

The implementation steps of the simple version of shared_ptr are as follows:

  1. Add a member variable count in the shared_ptr class, indicating the reference count corresponding to the resource managed by the smart pointer object.
  2. Obtain the resource in the constructor, and set the reference count corresponding to the resource to 1, indicating that only one object is currently managing the resource.
  3. In the copy constructor, the resource it manages is managed together with the incoming object, and the reference count of the corresponding resource is ++.
  4. In the copy assignment function, first count the reference count corresponding to the resource managed by the current object – (if it is reduced to 0, it needs to be released), and then manage the resource it manages together with the incoming object, and at the same time need to count the reference corresponding to the resource ++.
  5. In the destructor, the reference count corresponding to the resource will be managed – if it is reduced to 0, the resource needs to be released.
  6. Overload *the AND ->operator, and use the shared_ptr object to behave like a pointer.

code show as below:

#pragma once
#include<iostream>
using namespace std;
namespace cfy
{
    
    
	template<class T>
	class shared_ptr
	{
    
    
	public:
		//RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
    
    }
		~shared_ptr()
		{
    
    
			if (--(*_pcount) == 0)
			{
    
    
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
			delete _pcount;
			_pcount = nullptr;
		}

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

		shared_ptr& operator=(shared_ptr<T>& sp)
		{
    
    
			if (_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
    
    
				if (--(*_pcount) == 0)//将管理的资源对应的引用计数--
				{
    
    
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				(*_pcount)++;         //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}

		//获取引用计数
		int use_count()
		{
    
    
			return *_pcount;
		}
		//可以像指针一样使用
		T& operator*()
		{
    
    
			return *_ptr;
		}
		T* operator->()
		{
    
    
			return _ptr;
		}

	private:
		T* _ptr;	 //管理的资源
		int* _pcount;//管理的资源对应的引用计数
	};
}

Why do reference counts need to be stored in the heap?

First of all, the reference count count in shared_ptr cannot simply be defined as a member variable of type int, because this means that the shared_ptr object has its own count member variable, and when multiple objects want to manage the same resource, these several objects should use the same reference count.

As shown below:

image-20230707165537339

Secondly, the reference count count in shared_ptr cannot be defined as a static member variable, because static member variables are shared by all types of objects, which will cause objects that manage the same resource and objects that manage different resources to use the same Reference counting.

As shown below:

image-20230707170605840

And if the reference count count in shared_ptr is defined as a pointer, when a resource is managed for the first time, a space is opened in the heap area to store its corresponding reference count. If other objects also want to manage this resource, Then in addition to giving this resource to it, you also need to give it this reference count.

At this time, multiple objects that manage the same resource access the same reference count, while objects that manage different resources access different reference counts, which is equivalent to binding each resource with its corresponding reference count.

As shown below:

image-20230707170958003

But at the same time, it should be noted that since the reference counted memory space is also opened on the heap, when the reference count corresponding to a resource is reduced to 0, in addition to releasing the resource, the reference counted memory space corresponding to the resource is also required to release.

Thread safety issues of std::shared_ptr

Thread safety issue of shared_ptr

The shared_ptr implemented by the current simulation still has the problem of thread safety. Since the reference counts of multiple objects that manage the same resource are shared, multiple threads may perform self-increment or self-decrement operations on the same reference count at the same time. Both increment and decrement operations are not atomic operations, so the reference count needs to be protected by locking, otherwise it will lead to thread safety issues.

For example, in the following code, a shared_ptr is used to manage an integer variable, and then two threads are used to copy the shared_ptr object 1000 times, and these objects will be destroyed immediately after being copied. for example:

void func(cfy::shared_ptr<int>& sp, size_t n)
{
    
    
	for (size_t i = 0; i < n; i++)
	{
    
    
		cfy::shared_ptr<int> copy(sp);
	}
}
int main()
{
    
    
	cfy::shared_ptr<int> p(new int(0));
	const size_t n = 1000;
	thread t1(func, p, n);
	thread t2(func, p, n);
	t1.join();
	t2.join();
	cout << p.use_count() << endl;//预期:1
	return 0;
}

During this process, the two threads will continuously increase and decrease the reference count. In theory, the value of the reference count should be 1 after the execution of the two threads, because the copied objects are destroyed, and only The original shared_ptr object is still managing this integer variable, but the value of the reference count may be different each time the program is run. The fundamental reason is that the self-increment and self-decrement of the reference count are not atomic operations.

Locking solves thread safety issues

To solve the thread safety problem of reference counting, the essence is to make the self-increment and self-decrement operations of reference counting become an atomic operation, so the operation of reference counting can be protected by locking, and the atomic class atomic can also be used to count references For encapsulation, here we take locking as an example.

  • Add a mutex member variable in the shared_ptr class. In order to allow multiple threads that manage the same resource to access the same mutex, threads that manage different resources access different mutexes, so the mutex Locks also need to be created on the heap.
  • When calling the copy constructor and copy assignment function, in addition to handing over the corresponding resources and reference counts to the current object for management, you also need to hand over the corresponding mutex to the current object.
  • When the reference count corresponding to a resource is reduced to 0, in addition to releasing the corresponding resource and reference count, since the mutex is also created in the heap area, the corresponding mutex also needs to be released.
  • In order to simplify the code logic, the self-increment operation of the reference count in the copy constructor and copy assignment function can be extracted and encapsulated into the AddRef function, and the self-decrement operation of the reference count in the copy assignment function and destructor can be extracted and encapsulated into ReleaseRef function, so that only the AddRef and ReleaseRef functions need to be locked and protected.

code show as below:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
namespace cfy
{
    
    
	template<class T>
	class shared_ptr
	{
    
    
	private:
		//++引用计数
		void AddRef()
		{
    
    
			_pmutex->lock();
			(*_pcount)++;
			_pmutex->unlock();
		}
		//--引用计数
		void ReleaseRef()
		{
    
    
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0)
			{
    
    
				if (_ptr != nullptr)
				{
    
    
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pmutex->unlock();
			if (flag == true)
			{
    
    
				delete _pmutex;
			}
		
		}
	public:
		//RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
    
    }
		~shared_ptr()
		{
    
    
			ReleaseRef();
		}

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

		shared_ptr& operator=(shared_ptr<T>& sp)
		{
    
    
			if (_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
    
    
				ReleaseRef();
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				AddRef();       //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}

		//获取引用计数
		int use_count()
		{
    
    
			return *_pcount;
		}
		//可以像指针一样使用
		T& operator*()
		{
    
    
			return *_ptr;
		}
		T* operator->()
		{
    
    
			return _ptr;
		}

	private:
		T* _ptr;	 //管理的资源
		int* _pcount;//管理的资源对应的引用计数
		mutex* _pmutex;//管理的资源对应的互斥锁
	};
}

Explain:

  • In the ReleaseRef function, when the reference count is reduced to 0, the mutex resource needs to be released, but the mutex cannot be released in the critical section, because the unlock operation needs to be performed later, so a flag variable is used in the code, through the flag Variables to determine the need to release mutex resources after unlocking.
  • shared_ptr only needs to ensure the thread safety of reference counting, but not the thread safety of managed resources. Just like the native pointer manages a memory space, the native pointer only needs to point to this space, and the thread safety of this space It should be guaranteed by the operator of this space.

Custom deleter for std::shared_ptr

Usage of custom deleters

When the life cycle of the smart pointer object ends, all smart pointers deleterelease resources by default, which is not appropriate, because the smart pointer does not only manage the newmemory space applied by the method, but the smart pointer manages It is also possible to new[]apply for space in a way, or manage a file pointer. for example:

struct ListNode
{
    
    
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
    
    
		cout << "~ListNode()" << endl;
	}
};
int main()
{
    
    
	std::shared_ptr<ListNode> sp1(new ListNode[10]);   //error
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error

	return 0;
}

At this time, when the life cycle of the smart pointer object ends, deletereleasing the managed resources in the way will cause the program to crash, because the new[]memory space applied in the way must delete[]be released in the way, and the file pointer must fclosebe changed by calling the function . freed.

At this time, you need to use a custom deleter to control the way to release resources. The shared_ptr in the C++ standard library provides the following constructor:

template <class U, class D>
shared_ptr (U* p, D del);

Parameter Description:

  • p: The resource that needs to be managed by the smart pointer.
  • del: deleter, this deleter is a callable object, such as a function pointer, a functor, a lambda expression, and a callable object wrapped by a wrapper.

When the life cycle of the shared_ptr object ends, the deleter passed in will be called to release the resource. When the deleter is called, the resource managed by the shared_ptr will be passed in as a parameter.

Therefore, when the resource managed by the smart pointer is not newthe memory space applied for in the way, it is necessary to pass in a custom deleter when constructing the smart pointer object. for example:

template<class T>
struct DelArr
{
    
    
	void operator()(const T* ptr)
	{
    
    
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
    
    
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
    
    
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	});

	return 0;
}

Mock implementation of custom deleter

Implementation issues with custom deleters:

  • The implementation of shared_ptr in the C++ standard library is divided into many classes, so the type of the deleter can be set as the template parameter of the constructor in the C++ standard library, and then the type of the deleter can be passed between classes.
  • But we directly use a class to simulate the implementation of shared_ptr, so the type of the deleter cannot be set as the template parameter of the constructor. Because the deleter is not called in the constructor, but needs to be called in the ReleaseRef function, it is necessary to use a member variable to save the deleter, and when defining this member variable, you need to specify the type of the deleter, so When simulating the implementation here, the type of the deleter cannot be set as the template parameter of the constructor.
  • To support the custom deleter on the basis of the shared_ptr implemented by the current simulation, you can only add another template parameter to the shared_ptr class, and you need to specify the type of the deleter when constructing the shared_ptr object. Then add a constructor that supports passing in the deleter, save the deleter when constructing the object, and call the deleter to release when the resource needs to be released. It is better to set a default deleter. If the user does not pass in the deleter when defining the shared_ptr object, the resource will be released by delete by default.

code show as below:

namespace cfy
{
    
    
    //默认的删除器
	template<class T>
	struct Delete
	{
    
    
		void operator()(const T* ptr)
		{
    
    
			delete ptr;
		}
	};
	template<class T, class D = Delete<T>>
	class shared_ptr
	{
    
    
	private:
		void ReleaseRef()
		{
    
    
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
			{
    
    
				if (_ptr != nullptr)
				{
    
    
					cout << "delete: " << _ptr << endl;
					_del(_ptr); //使用定制删除器释放资源
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pmutex->unlock();
			if (flag == true)
			{
    
    
				delete _pmutex;
			}
		}
		//...
	public:
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
			, _del(del)
		{
    
    }
		//...
	private:
		T* _ptr;        //管理的资源
		int* _pcount;   //管理的资源对应的引用计数
		mutex* _pmutex; //管理的资源对应的互斥锁
		D _del;         //管理的资源对应的删除器
	};
}

At this time, our simulated shared_ptr supports custom deleters, but it is not as convenient to use as in the C++ standard library.

  • If the incoming deleter is a functor, then the type of the functor needs to be specified when constructing the shared_ptr object.
  • It is even more troublesome if the deleter passed in is a lambda expression, because the type of the lambda expression is not easy to obtain. Here you can specify the type of the lambda expression as a wrapper type, and let the compiler deduce it when passing parameters, or you can use auto to receive the lambda expression first, and then use decltype to declare the type of the deleter.
template<class T>
struct DelArr
{
    
    
	void operator()(const T* ptr)
	{
    
    
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
    
    
	//仿函数示例
	cfy::shared_ptr<ListNode, DelArr<ListNode>> sp1(new ListNode[10], DelArr<ListNode>());

	//lambda示例1
	cfy::shared_ptr<FILE, function<void(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {
    
    
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
		});

	//lambda示例2
	auto f = [](FILE* ptr) {
    
    
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	};
	cfy::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "r"), f);

	return 0;
}

3.6 std::weak_ptr

Circular reference problem of std::shared_ptr

circular reference problem

The circular reference problem of shared_ptr will only occur in some specific scenarios. For example, define the following node class, and print a prompt statement in the destructor of the node class, so as to judge whether the node is released correctly.

struct ListNode
{
    
    
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
    
    
		cout << "~ListNode()" << endl;
	}
};

Now newbuild two nodes on the heap in the way of and connect the two nodes, and deleterelease the two nodes in the way of the program at the end. for example:

int main()
{
    
    
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//...
	delete node1;
	delete node2;
	return 0;
}

There is no problem with the above procedure, and both nodes can be released correctly. In order to prevent the node from being released due to reasons such as returning or throwing an exception in the middle of the program, we hand over the two nodes to two shared_ptr objects for management. The types of the next and prev member variables in the ListNode class are also changed to the shared_ptr type. for example:

struct ListNode
{
    
    
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
    
    
		cout << "~ListNode()" << endl;
	}
};
int main()
{
    
    
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	//...

	return 0;
}

At this time, the program has not been released after running, but if any one of the two lines of code when connecting the nodes is removed, then the two nodes can be released correctly. The fundamental reason is that these two lines of connecting nodes The code caused a circular reference.

When two ListNode nodes are applied for in the form of new and handed over to two smart pointers for management, the reference counts corresponding to these two resources are added to 1. As shown below:

image-20230707193704201

After connecting these two nodes, the next member in resource 1 manages resource 2 together with node2, and the prev member in resource 2 manages resource 1 together with node1. At this time, the reference counts corresponding to these two resources are added to 2. As shown below:

image-20230707193812369

When the scope of the main function is out, the life cycle of node1 and node2 is over, so the reference counts corresponding to these two resources are finally reduced to 1. As shown below:

image-20230707193856239

Reasons why resources are not released due to circular references:

  • The corresponding resource will be released when the reference count corresponding to the resource is reduced to 0, so the release of resource 1 depends on the prev member in resource 2, and the release of resource 2 depends on the next member in resource 1.
  • The release of the next member in resource 1 depends on resource 1, and the release of the prev member in resource 2 depends on resource 2, so this becomes an endless loop, and eventually the resource cannot be released.

And if only one connection operation is performed when connecting nodes, then when the life cycle of node1 and node2 ends, the reference count corresponding to a resource will be reduced to 0, and the resource will be released at this time. The reference count of a resource will also be reduced to 0, and eventually both resources will be released, which is why both nodes can be released correctly when only one connection operation is performed.

std::weak_ptr solves circular reference problem

Solve the circular reference problem

weak_ptr is a smart pointer introduced in C++11, weak_ptr is not used to manage the release of resources, it is mainly used to solve the circular reference problem of shared_ptr.

  • weak_ptr supports using shared_ptr objects to construct weak_ptr objects. The constructed weak_ptr objects and shared_ptr objects manage the same resource, but the reference count corresponding to this resource will not be increased.

Changing the types of the next and prev members in ListNode to weak_ptr will not cause circular reference problems. At this time, when the life cycle of node1 and node2 ends, the reference counts corresponding to the two resources will be reduced to 0, and then the two resources will be released. resources of a node. for example:

struct ListNode
{
    
    
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
    
    
		cout << "~ListNode()" << endl;
	}
};
int main()
{
    
    
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	//...
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

Obtain the reference counts corresponding to these two resources through use_count, and you will find that the reference counts corresponding to these two resources are 1 before and after the node connection. The root cause is that weak_ptr will not increase the reference counts corresponding to the managed resources.

Analog implementation of weak_ptr

The implementation steps of the simple version of weak_ptr are as follows:

  1. Provide a no-argument constructor, for example, the no-argument constructor of weak_ptr will be called just now when new ListNode is created.
  2. Supports copying and constructing weak_ptr objects with shared_ptr objects, and obtains resources managed by shared_ptr objects during construction.
  3. Supports copying and assigning shared_ptr objects to weak_ptr objects, and obtaining resources managed by shared_ptr objects when assigning values.
  4. Overload the * and -> operators to make weak_ptr objects behave like pointers.

code show as below:

namespace cfy
{
    
    
	template<class T>
	class weak_ptr
	{
    
    
	public:
		weak_ptr()
			:_ptr(nullptr)
		{
    
    }
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{
    
    }
		weak_ptr& operator=(const shared_ptr<T>& sp)
		{
    
    
			_ptr = sp.get();
			return *this;
		}
		//可以像指针一样使用
		T& operator*()
		{
    
    
			return *_ptr;
		}
		T* operator->()
		{
    
    
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
	};
}

Explain: shared_ptr also provides a get function for obtaining the resources it manages.

The relationship between smart pointers in C++11 and boost

  1. The first smart pointer auto_ptr was produced in C++98.
  2. C++boost gives more practical scoped_ptr, shared_ptr and weak_ptr.
  3. C++TR1 introduces shared_ptr in boost, etc. But note that TR1 is not a standard version.
  4. C++11 introduces unique_ptr, shared_ptr and weak_ptr in boost. It should be noted that unique_ptr corresponds to scoped_ptr in boost, and the implementation principles of these smart pointers are implemented in reference to boost.

To explain: The boost library is the general term for some C++ libraries that provide extensions to the C++ language standard library. One of the original intentions of the boost library community is to provide reference implementations for the standardization of C++, such as in the C++ standard library TR1 submitted for review , there are ten boost libraries that have become candidates for the standard library.

Guess you like

Origin blog.csdn.net/NEFUT/article/details/131603769