[C++] smart pointer

Smart pointer description and simple implementation

Article directory

    • Smart pointer description and simple implementation
    • foreword
    • 1. Smart pointer
      • 1 Principle of Smart Pointer
      • 2 smart pointer smart_ptr
    • 2. Smart pointer category
      • 1 C++98 auto_ptr
      • 2 C++11 unique_ptr
      • 3 shared_ptr
    • 3. Summary of the previous article

Foreword

Smart pointers can help us avoid the problem of memory leaks caused by forgetting to release space after applying for it.
In fact, the smart pointer is a class, and we know that the destructor in the class will call the destructor to release the resource when the object is out of the class scope, so the principle of the smart pointer is to automatically release the memory when the function ends space, no need to manually release memory space.

Compilation environment: vs2013

1. Smart pointer

1 Smart pointer principle

In the past, the resources we dynamically applied for in C++ required the user to do it manually, but how can the resources be released automatically?
We can use the compiler in the class to automatically call its constructor and destructor to achieve. This process is implemented based on the RAII mechanism.

RAII mechanism:
A simple technique for controlling program resources such as memory, file handles, network connections, mutexes, and so on, using object lifetimes.

That is: the resources are handed over to the object, and the construction and destruction are all handed over to the object itself. This process does not require the user to manually apply for and release it.

The mechanism of RAII is not enough. Smart pointers, since they are pointers, must have pointer-like behaviors, such as dereferencing * and accessing the content in the pointed space through ->.

Smart pointer: Encapsulate the original ecological pointer and manage resources in a class way.

2 smart pointer smart_ptr

Smart pointer principle: RAII + has pointer-like behavior

Based on the above principles, let’s simply implement such a smart_ptr.

template<class T>
class smart_ptr
{<!-- -->
public:
//RAII
smart_ptr(T* ptr = nullptr)
:_ptr(ptr)
{<!-- -->}
~smart_ptr()
{<!-- -->
//If the pointer manages resources, release the managed resources
if (_ptr)
{<!-- -->
delete_ptr;
_ptr = nullptr;
}
}
// has pointer-like behavior
T & operator*()
{<!-- -->
return *_ptr;
}
T* operator->()
{<!-- -->
return_ptr;
}
//Provide native pointer operations
T* Get()
{<!-- -->
return_ptr;
}
private:
T* _ptr;
};



We have seen above that the implemented smart_ptr has been satisfied: RAII + has pointer behavior. From the results, we can also see that when the user does not explicitly define the copy constructor and the assignment operator overload function, both pointers point to the same block of memory space. address, and this causes the shallow copy problem.

Shallow copy: The default copy construction and assignment operator overloading methods generated by the compiler copy the content of one object to another object intact, causing multiple objects to share the same resource. When the space is released, it will be repeated multiple times Release the same memory space, resulting in memory leaks.

Therefore: once a class involves resource management, such as applying for space, the copy constructor and assignment operator overload of the class must be explicitly defined by the user.

The ways to solve the shallow copy in the class are: deep copy + realistic copy

Thinking: Can we use deep copy to solve this problem?
[Deep copy is to allocate independent memory resources to each object to ensure that multiple objects will not cause resource leaks due to multiple releases of shared resources. 】

  • In fact, it is not allowed, because smart_ptr only manages the space applied by others when it is constructed, and it only has the power to manage and release the space.

Therefore, smart pointers have to solve the problem of shallow copying, and different solutions have produced different versions of smart pointers.
So the problem becomes:

Smart pointer principle: RAII + has pointer-like behavior + solves the problem of shallow copy

2. Smart pointer class

1 C++98 auto_ptr

C++98 auto_ptr documentation
The way auto_ptr solves shallow copy is to transfer resources.

1.1

auto_ptr principle: RAII + pointer behavior + resource transfer

//What is the transfer of resources?
auto_ptr<int> sp1(new int(1));
auto_ptr<int> sp2(ap1);
//Use ap1 to copy and construct ap2, ap1 copies the resources it manages to ap2,
//Then ap1 no longer manages this resource, that is, it points to empty
// That is, sp1 transfers management resources to sp2
namespace auo
{<!-- -->
template<class T>
class auto_ptr
{<!-- -->
public:
//RAII
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{<!-- -->}
~auto_ptr()
{<!-- -->
if (_ptr)
{<!-- -->
delete_ptr;
_ptr = nullptr;
}
}
// Behavior with a pointer
T & operator*()
{<!-- -->
return *_ptr;
}
T* operator->()
{<!-- -->
return_ptr;
}
//Solve the shallow copy method: transfer resources
auto_ptr(auto_ptr<T> & ap)
:_ptr(ap._ptr)
{<!-- -->
ap._ptr = nullptr;
}
auto_ptr<T> & amp; operator=(auto_ptr<T> & amp; ap)
{<!-- -->
//First check that you are not assigning a value to yourself
if (this != & ap)
{<!-- -->
//If the current object has management resources, release the resources
if (_ptr)
delete_ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
}


As above, we can see that when sp1 is constructed by copying sp, the resources managed by sp are transferred to sp1. At this time, only the resource manager sp1 can access the resources; when sp1 is used to assign values to sp2, the resources managed by sp1 are transferred to sp2, only the resource manager sp2 can access the resources at this time.

  • We know that multiple pointers can point to the same memory space at the same time and access data. Obviously, the auto_ptr we implement now cannot.
  • Then, when we transfer resources, we do not clear the resources of the original object, but add a state to indicate whether the object has the right to release the resources at this time. In this case, then in this case, wouldn’t all objects have access to resources.

1.2

auto_ptr principle: RAII + behavior with pointers + transfer of resource management rights

namespace auo
{<!-- -->
template<class T>
class auto_ptr2
{<!-- -->
public:
//RAII
auto_ptr2(T* ptr = nullptr)
:_ptr(ptr)
,_owner(false)
{<!-- -->
//Resource management release right, set to false by default, set to true when it has resources
if (_ptr)
{<!-- -->
_owner = true;
}
}
~auto_ptr2()
{<!-- -->
if (_ptr & & _owner)
{<!-- -->
delete_ptr;
_ptr = nullptr;
_owner = false;
}
}
// Behavior with pointers....
//Solve the shallow copy method: transfer resources
auto_ptr2(const auto_ptr2<T> & ap)
:_ptr(ap._ptr)
, _owner(ap._owner)
{<!-- -->
ap._owner = false;
}
auto_ptr2<T> & amp; operator=(const auto_ptr2<T> & amp; ap)
{<!-- -->
//First check that you are not assigning a value to yourself
if (this != & ap)
{<!-- -->
//If the current object has management resources, release the resources
if (_ptr & & _owner)
delete_ptr;
_ptr = ap._ptr;
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
private:
T* _ptr;
mutable bool _owner;//true: Indicates that the current object has the power to release resources
};
}




Now that one resource can be accessed by multiple resources, and only one object can release the resource, is it really okay?
Let’s look at another example:


As we can see above, the disadvantage of the first implementation of auto_ptr is that resources cannot be accessed by multiple objects; and the second implementation of auto_ptr2, although a resource can be shared and accessed by multiple objects, there is a risk of wild pointers question.

From this point of view, compared to the problem of not being able to access resources at the same time, wild pointers are more dangerous, so the first implementation of auto_ptr is relatively safe.

Due to the unsafety of auto_ptr, in C++98, it is recommended not to use auto_ptr at any time.
Of course, there are other smart pointers that are safer to use.

2 C++11 unique_ptr

C++11 unique_ptr documentation

unique_ptr principle: RAII + pointer behavior + anti-copy

In fact, the shallow copy problem is nothing more than: object copy construction (constructing a new object by copying an object) and assignment operator overloading (assigning an object to another object), resulting in multiple objects sharing the same resource.
The way unique_ptr solves shallow copying is to directly not allow objects to be copied and overloaded with assignment operators, and it is forbidden to share resources and monopolize resources.

> And how to disable the call of the two functions?
> C++11:
> //Add "=delete" after the function to disable the function
> unique_ptr(const unique_ptr<T> & amp; sp)=delete;
> unique_ptr<T> & amp; operator=(const unique_ptr<T> & amp; sp)=delete;
> C++98:
> //The method is only declared but not defined, and the two functions are set as private methods, which cannot be accessed by the outside world
> private:
> unique_ptr(const unique_ptr<T> & sp);
> unique_ptr<T> & amp; operator=(const unique_ptr<T> & amp; sp);

The implementation is the same as smart_ptr, except that the copy constructor and assignment operator overloading functions are implemented in C++11 or C++98.


In fact, it can be seen that the unique_ptr solution to the shallow copy problem is simple and rude, directly prohibiting object copy construction and assignment operator overloading.

In the above implementations of smart_ptr, auto_ptr, and unique_ptr, we use delete to release the space in the destructor, but not all the space is applied for with new, such as the space applied for by malloc To use free to release.
So we have to match our destructors, that is, whichever method is used to apply for space, use the corresponding method to release the space.
Therefore: add the parameter DF in the template parameter list, and call different methods to release the space according to the different ways of space application. This is the deleter, indicating the way of resource deletion.

template<class T>
//The default is the space requested by new, use delete to release
class def
{<!-- -->
public:
void operator()(T* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout<<"delete()"<<endl;
delete ptr;
ptr = nullptr;
}
}
};

template<class T,class DF=Def<T>>
class unique_ptr
{<!-- -->
public:
//RAII
unique_ptr(T*ptr = nullptr)
:_ptr(ptr)
{<!-- -->}
~unique_ptr()
{<!-- -->
if (_ptr)
{<!-- -->
DF df;
df(_ptr);
//DF()(_ptr)
}
}
// Behavior with a pointer
//....
//The way to solve shallow copy: anti-copy
//C++11
unique_ptr(const unique_ptr<T> & ap) = delete;
unique_ptr<T> & amp; operator=(const unique_ptr<T> & amp; ap) = delete;
//C++98
//private:
//unique_ptr(const unique_ptr<T> & ap);
//unique_ptr<T> & amp; operator=(const unique_ptr<T> & amp; ap);
private:
T* _ptr;
};
}
//malloc application space release
template<class T>
class free
{<!-- -->
public:
void operator()(T* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout << "Free()" << endl;
free(ptr);
ptr = nullptr;
}
}
};
//Close the space release of the file pointer
class Fclose
{<!-- -->
public:
void operator()(FILE* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout << "Fclose()" << endl;
fclose(ptr);
ptr = nullptr;
}
}
};
//Apply for the release of continuous space
template <class T>
class deleteArray
{<!-- -->
public:
void operator()(T* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout << "delete[]" << endl;
delete[] ptr;
ptr = nullptr;
}
}
};

In this way, our space release can release the corresponding space according to the way of applying for space.
It can be seen that different ways of applying for space correspond to corresponding resource releases, unique_ptr directly eliminates the root cause The shallow copy problem occurs, but the biggest disadvantage of unique_ptr is that resources cannot be shared.
And shared_ptr solves the resource sharing problem.

3 shared_ptr

C++11 shared_ptr documentation

shared_ptr principle: RAII + behavior with pointers + reference counting

The way shared_ptr solves the shallow copy is to maintain a count inside it to record that the resource is shared by several objects. Based on the shallow copy, it is guaranteed that no matter how many objects share the resource, the final resource is only Release once.
When the object is destroyed (that is, the destructor is called), it means that you no longer use the resource, and the reference count of the object is decremented by 1; if the count is 0, it means that you are the last object to use the resource, and you must release the resource; If the reference count is not 0, it means that other objects use the resource besides yourself and cannot release the resource, otherwise other pointers will become wild pointers.
We now need to add a count variable to the class private member implemented earlier.
First of all, we now set the technical variable as an ordinary member variable, int count.

3.1

//shared_ptr: first version
namespace sha
{<!-- -->
template<class T>
//The default is the space requested by new, use delete to release
class def
{<!-- -->
public:
void operator()(T* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout << "delete()" << endl;
delete ptr;
ptr = nullptr;
}
}
};

template<class T, class DF = Def<T>>
class shared_ptr1
{<!-- -->
public:
//RAII
shared_ptr1(T* ptr = nullptr)
:_ptr(ptr)
,_count(0)
{<!-- -->
//If there are resources, use the object of the resource + 1
if (_ptr)
_count = 1;
}
~shared_ptr1()
{<!-- -->
//If no other object uses the resource except itself
//Then release the resource
if (_ptr & amp; & amp; --_count==0)
{<!-- -->
//Release according to the resource method provided by the user
DF df;
df(_ptr);
//DF()(_ptr)
}
}
// Behavior with a pointer
T & operator*()
{<!-- -->
return *_ptr;
}
T* operator->()
{<!-- -->
return_ptr;
}
T* Get()
{<!-- -->
return_ptr;
}
//The way to solve shallow copy: reference counting
shared_ptr1(shared_ptr1<T> & ap)
:_ptr(ap._ptr)
, _count( + + ap._count)
{<!-- -->}
private:
T* _ptr;
int_count;
};
}


The result of this example seems to be no problem, which is in line with our expectations, but is it really possible? Use the following example to verify.

It should be that the number of resource users corresponding to sp1 should be only sp after the scope is removed, but the count in sp still shows 2, so the resource is not actually shared for the count, so it is not feasible to use ordinary member variables for the count variable .

Thinking: We know that there is always only one copy of a static resource object in the entire class, so how about setting the count variable as a static member variable?

3.2

//shared_ptr: second version
namespace sha
{<!-- -->
template<class T>
//The default is the space requested by new, use delete to release
class def
{<!-- -->
public:
void operator()(T* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout << "delete()" << endl;
delete ptr;
ptr = nullptr;
}
}
};

template<class T, class DF = Deff<T>>
class shared_ptr2
{<!-- -->
public:
//RAII
shared_ptr2(T* ptr = nullptr)
:_ptr(ptr)
{<!-- -->
//If there are resources, use the object of the resource + 1
if (_ptr)
_count = 1;
}
~shared_ptr2()
{<!-- -->
//If no other object uses the resource except itself
//Then release the resource
if (_ptr & amp; & amp; --_count == 0)
{<!-- -->
//Release according to the resource method provided by the user
DF df;
df(_ptr);
//DF()(_ptr)
}
}
// Behavior with a pointer
//....
//The way to solve shallow copy: reference counting
shared_ptr2(shared_ptr2<T> & ap)
:_ptr(ap._ptr)
{<!-- -->
+ + _count;
}
private:
T* _ptr;
static int _count;
};
template<class T, class DF>
int shared_ptr2<T, DF>::_count = 0;
}


The initial static member variable records the number of resource sharers corresponding to sp, but after creating sp2 that manages another resource, the count changes to record the number of sharers of resources corresponding to sp2, so if the count is set as a static member variable It will confuse the number of copies recorded, so this method will not work.

3.3
If we want to count the different resources that can be shared and should be recorded correspondingly, how did we realize the sharing of resources at the beginning? If pointers are used, then our reference counting can also be performed using pointers.

//shared_ptr: third version
namespace sha
{<!-- -->
template<class T>
//The default is the space requested by new, use delete to release
class De
{<!-- -->
public:
void operator()(T* & ptr)
{<!-- -->
if (ptr)
{<!-- -->
cout << "delete()" << endl;
delete ptr;
ptr = nullptr;
}
}
};
template<class T, class DF = De<T>>
class shared_ptr3
{<!-- -->
public:
//RAII
shared_ptr3(T* ptr = nullptr)
:_ptr(ptr)
, _count(nullptr)
{<!-- -->
//If there are resources, use the object of the resource + 1
if (_ptr)
{<!-- -->
_count = new int(1);
}
}
~shared_ptr3()
{<!-- -->
Release();
}
// Behavior with a pointer
//....
//The way to solve shallow copy: reference counting
shared_ptr3(const shared_ptr3<T> & ap)
:_ptr(ap._ptr)
,_count(ap._count)
{<!-- -->
+ + (*_count);
}
shared_ptr3<T> & amp; operator=(const shared_ptr3<T> & amp; ap)
{<!-- -->
if (this != & ap)
{<!-- -->
//If this object originally had resources, first release the resources of this
Release();
//Both share resources
_ptr = ap._ptr;
_count = ap._count;
+ + (*_count);
}
return *this;
}
private:
void Release()
{<!-- -->
if (_ptr & amp; & amp; --(*_count)==0)
{<!-- -->
DF df;
df(_ptr);
delete_count;
_count = nullptr;
}
}
private:
T* _ptr;
int*_count;
};
}



?(?)?, in this way, resources are successfully shared!

3. Summary

  • 1 In the final implementation of shared_ptr, is the smart pointer thread-safe?
    In multithreading, the process of counting addition and subtraction is not atomic. At the same time, multiple objects may operate on the counter at the same time, + + or –, this operation is not atomic, and the reference count is originally 1, + + Twice, maybe 2, so the reference count is messed up. Therefore, the atomicity of the reference counting operation must be guaranteed, so it is necessary to lock the reference counting ++ and – operations in the smart pointer to protect this process, so that only one thread can operate on the counting at a time.
  • 2 Is the resource pointed to by the smart pointer thread-safe?
    Pointers only have the right to manage and release resources, and the resources they manage point to thread safety issues on resources on the heap
    It is handled by the user himself, and the pointer does not matter, nor can he control it.
    Because the objects managed by smart pointers are stored on the heap, accessing them in two threads at the same time will cause thread safety problems.
    question.

This article is over! ! !