[C++11] Use of smart pointers and simulation implementation (shared_ptr, unique_ptr, auto_ptr, weak_ptr)

Blog homepage: Homepage
Series column: C + +
Thank you everyone for your likesCollections?Comments
Looking forward to making progress with everyone!

Article directory

  • 1. RAII concept
  • 1. auto_ptr
    • 1.Basic use
    • 2. Simulation implementation
  • 2. unique_ptr
    • 1.Basic use
    • 2. Simulation implementation
  • 3. shared_ptr
    • 1.Basic use
    • 2. Reference counting implementation
    • 3. Upgrade of destructor (for arrays)
    • 4. Circular references (pit points)
    • 5. Simulation implementation
  • 4. weak_ptr

1. RAII concept

RAII (Resource Acquisition Is Initialization) is a method that uses the object life cycle to control program resources (such as internal
storage, 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
Resources are released when the object is destroyed. With this, we actually delegate the responsibility of managing a resource to an object.
This approach has two major benefits:
1. There is no need to explicitly release resources.
2. In this way, the resources required by the object remain valid throughout its lifetime

The smart pointers we talk about below are all designed based on this idea.

1. auto_ptr

1.Basic use

Many companies clearly stipulate that auto_ptr is not allowed to be used because it has too many pitfalls.

Because it will lead to the transfer of management rights and cause the copied object to be suspended, an error will occur when operating on the suspended object

class A
{<!-- -->
public:
A(int a = 0)
:_a(a)
{<!-- -->
cout << "A(int a = 0)" << endl;
}

~A()
{<!-- -->
cout <<this;
cout << " ~A()" << endl;
}
\t 
int _a;
};
int main() {<!-- -->

auto_ptr<A> ap1(new A(1));
\t 

// Management rights transfer. When copying, the resource management rights of the copied object will be transferred to the copy object.
// Hidden danger: causing the copied object to be suspended, causing access problems
auto_ptr<A> ap2(ap1);

// crash
ap1->_a + + ;
ap2->_a + + ;


return 0;
}

2. Simulation implementation

template<class T>
class auto_ptr {<!-- -->
public:
auto_ptr(T *ptr)
:_ptr(ptr)
{<!-- -->}
~auto_ptr() {<!-- -->
delete _ptr;
}

// Use like pointer
T & amp; operator*() {<!-- -->
return *_ptr;
}
T* operator->() {<!-- -->
return _ptr;
}

auto_ptr(auto_ptr<T> & amp; ap)
:_ptr(ap._ptr)
{<!-- -->
//Transfer of management rights
//The original pointer is dangling
ap._ptr = nullptr;
}

auto_ptr<T> & amp; operator=(auto_ptr<T> & amp; ap)
{<!-- -->
// Check whether to assign a value to yourself
if (this != & amp;ap)
{<!-- -->
// Release resources in the current object
if(_ptr)
delete _ptr;
//Transfer resources in ap to the current object
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
private:
T* _ptr;
};

2. unique_ptr

1.Basic use

The unique_ptr smart pointer solves the problems of auto_ptr very simply and crudely. Since copying and assignment will cause so many problems, I will directly disable your copy and assignment functions when we don’t need to copy. In this scenario, it is recommended to use this smart pointer

2. Simulation implementation

template<class T>
class unique_ptr {<!-- -->
public:
unique_ptr(T*ptr)
:_ptr(ptr)
{<!-- -->}
~unique_ptr() {<!-- -->
delete _ptr;
}
T & amp; operator*() {<!-- -->
return *_ptr;
}
T* operator->() {<!-- -->
return _ptr;
}
//Directly set the copy constructor and assignment operator overloaded function as the delete function
unique_ptr(unique_ptr<T> & amp; ap) = delete;
unique_ptr<T>operator=(unique_ptr<T> & amp; ap) = delete;
private:
T* _ptr;
\t\t 
};

3. shared_ptr

1.Basic use

The principle of shared_ptr is to realize resource sharing between multiple shared_ptr objects through reference counting.

  1. Shared_ptr internally maintains a count for each resource to record that the resource is shared by several objects.
    Enjoy.
  2. When the object is destroyed (that is, the destructor is called), it means that the resource is no longer used, and the reference count of the object is decremented.
    one.
  3. If the reference count is 0, it means that you are the last object to use the resource and must release the resource;
  4. If it is not 0, it means that there are other objects using the resource besides itself, and the resource cannot be released, otherwise other objects
    The elephant becomes a wild pointer.

2. Reference counting implementation

Reference counting supports multiple copies to manage the same resource, and the last destructor object releases the resource

private:
T* _ptr;
int* _pcount;//reference count counter
function<void (T*)>_del = [](T* ptr) {<!-- -->delete ptr; };//Destructor wrapper
~shared_ptr() {<!-- -->
//The reference counting timer must be public for pointers pointing to the same address.
//So we choose to change the value of the counter by dereferencing the pointer
//And the change affects both pointers pointing to the same address.
if (--(*_pcount) == 0) {<!-- -->
//(*_pcount) == 0 means that there is no pointer pointing to this location at this time and it can be released.
cout << "delete:" << endl;
_del(_ptr);
delete _pcount;
}
}

3. Upgrade of destructor (for arrays)

What should we do when we encounter an array situation? When we destruct, we only destruct the current position, and the array is a series. At this time, if we still use the original old method to design the destructor, memory leaks will occur.

In the C++ library, you choose to implement a class with a delete method and pass it in as a functor.

At this time, another problem arises. I deleted method D as a template parameter in the destructor. It is only valid within the destructor, instead of being valid in the entire class like the template parameter T. How should we solve this? What’s the problem?

Answer: We can choose to add a private member _del in private as a deletion method

At this time, some people may have doubts, how can I_del be compatible with releasing an address and a string of addresses? The deletion function we write outside may not be passed in to adapt to the type of _del.

Answer: At this time, the function wrapper we introduced before can come in handy [C++11] function wrapper, bind function template use
We can use a wrapper to be compatible with multiple calling object types, so that we can pass in the functor we wrote ourselves.

template
shared_ptr(T* ptr, D del)
: _ptr(ptr),
_pcount(new int(1)),
_del(del)
{}
private:
T* _ptr;
int* _pcount;//reference count counter
function<void (T*)>_del = [](T* ptr) {<!-- -->delete ptr; };//Destructor wrapper
//The default method is to release the lambda of an address. When we want to delete the array, we can write our own method and pass it in.
//Because the return value of the function type that releases the address is void type



//Functions that can be designed when we destruct the array type
template
struct DeleteArrayFunc {
 void operator()(T* ptr)
 {
 cout << "delete[]" << ptr << endl;
 delete[] ptr;
 }
};

4. Circular references (pit points)


  1. The two smart pointer objects node1 and node2 point to two nodes, and the reference count becomes 1. We do not need to manually
    delete.
  2. The _next of node1 points to node2, the _prev of node2 points to node1, and the reference count becomes 2.
  3. Node1 and node2 are destructed and the reference count is reduced to 1, but _next still points to the next node. But _prev still points upward
    a node.
  4. In other words, after _next is destructed, node2 is released.
  5. In other words, _prev is destructed and node1 is released.
  6. But _next is a member of node. When node1 is released, _next will be destructed, and node1 is managed by _prev, _prev
    It is a member of node2, so this is called a circular reference and no one will release it.
    Creates a scene similar to an infinite loop

solution:
In the case of reference counting, just change _prev and _next in the node to weak_ptr

The principle is, when node1->_next = node2; and node2->_prev = node1;, the _next sum of weak_ptr
_prev does not increment the reference count of node1 and node2. Since I rely on whether the reference count is 0 to determine whether destruction is needed, can't it be enough if I don't increase the reference count?

5. Simulation implementation

template
class shared_ptr {
public:
shared_ptr(T*ptr=nullptr)
:_ptr(ptr),
_pcount(new int(1))
{}
\t\t 
template
shared_ptr(T* ptr, D del)
: _ptr(ptr),
_pcount(new int(1)),
_del(del)
{}

~shared_ptr() {
//The reference counting timer must be public for pointers pointing to the same address.
//So we choose to change the value of the counter by dereferencing the pointer
//And the change affects both pointers pointing to the same address.
if (--(*_pcount) == 0) {
cout << "delete:" << endl;
_del(_ptr);
delete _pcount;
}
}

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

shared_ptr & amp; operator=(const shared_ptr & amp; sp) {
if (_ptr == sp._ptr) {
return *this;
}
if (--(*_pcount) == 0)
{
\t\t\t\t 
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
+ + (*_pcount);
return *this;
}

int use_count()const {
return *_pcount;
//Get the reference count number
}

T* get()const {
return _ptr;
}

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

private:
T* _ptr;
int* _pcount;//reference count counter
function<void (T*)>_del = [](T* ptr) {<!-- -->delete ptr; };//Destructor wrapper
};
\t

4. weak_ptr

Let us state in advance that weak_ptr is not a RAII type pointer. Its design purpose is to solve the problem of circular references in shared_ptr. It does not support the use of other smart pointers

template<class T>
class weak_ptr {<!-- -->
public:
weak_ptr()
:_ptr(nullptr)
{<!-- -->}

weak_ptr(const shared_ptr<T> & amp; sp)
:_ptr (sp.get())
{<!-- -->}

weak_ptr<T> & amp;operator=(const shared_ptr<T> & amp; sp) {<!-- -->
_ptr = sp.get();
return *this;
}
/*shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
//This is not designed so that the shared_ptr type can be assigned to the weak_ptr type
//We know that the node type is shared_ptr type, and its prev and next are weak_ptr type
node1->_next = node2;
node2->_prev = node1;*/

T & amp; operator*() {<!-- -->
return *_ptr;
}

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