📜 ⬆️ ⬇️

Smart pointers for beginners

This small article is primarily intended for novice C ++ programmers who have either heard about smart pointers, but were afraid to use them, or they were tired of following new-delete.

UPD: Article was written when C ++ 11 was not so popular yet.

So, C ++ programmers know that the memory needs to be freed. Always desirable. And they know that if new is written somewhere, there must be an appropriate delete. But manual manipulations with memory can be fraught with, for example, the following errors:

The leak is not so critical in principle, if the program does not work 24/7, or the code that calls it is not in a loop. When dereferencing the null pointer, we are guaranteed to get a segment, it remains only to find the case when it becomes zero (you know what I mean). When you delete again, anything can happen. Usually (although it may not always be), if you are allocated a block of memory, then somewhere next to it there is a value that determines the amount of allocated memory. The details depend on the implementation, but suppose you allocated 1 kb of memory starting at some address. Then the number 1024 will be stored at the previous address, thus it will be possible by calling delete to delete exactly 1024 bytes of memory, no more and no less. The first time you call delete, everything is fine, while the second you wipe the wrong data. To avoid all this, or reduce the likelihood of such errors, smart pointers were invented.
')

Introduction


There is a resource management technique through local objects, called RAII . That is, when a resource is received, it is initialized in the constructor, and, after working with it in a function, it is correctly released in the destructor. A resource can be anything, for example a file, a network connection, and in our case a block of memory. Here is the simplest example:
class VideoBuffer { int *myPixels; public: VideoBuffer() { myPixels = new int[640 * 480]; } void makeFrame() { /*    */ } ~VideoBuffer() { delete [] myPixels; } }; int game() { VideoBuffer screen; screen.makeFrame(); } 

This is convenient: after exiting the function, we do not need to worry about freeing the buffer, since a destructor will be called for the screen object, which in turn will release the array of pixels encapsulated in itself. Of course, you can write like this:
 int game() { int *myPixels = new int[640 * 480]; //  delete [] myPixels; } 

In principle, no difference, but imagine the following code:
 int game() { int *myPixels = new int[640 * 480]; //  if (someCondition) return 1; //   if (someCondition) return 4; //  delete [] myPixels; } 

It is necessary to write delete [] in each branch of the exit from the function, or to call any additional functions of deinitialization. And if there are a lot of memory allocations, or do they occur in different parts of the function? Keep track of all this will be more and more difficult. A similar situation arises if we throw an exception in the middle of the function: it is guaranteed that the objects on the stack will be destroyed, but the problem remains open with a bunch.
Ok, we'll use RAII, initialize the memory in the constructors, release the destructor. And let the fields of our class be pointers to portions of dynamic memory:
 class Foo { int *data1; double *data2; char *data3; public: Foo() { data1 = new int(5); ... } ~Foo() { delete data1; ... } } 

Now imagine that the fields are not 3, but 30, which means in the destructor you have to call delete for all of them. And if we hurry to add a new field, but forget to kill him in the destructor, the consequences will be negative. The result is a class loaded with memory allocation / freeing operations, and it’s also unclear whether everything was deleted correctly.
Therefore, it is proposed to use smart pointers: these are objects that store pointers to dynamically allocated memory areas of arbitrary type. And they automatically clear the memory on exit from the scope.
First, we will look at how they look in C ++, then proceed to the overview of some common types of smart pointers.

Simplest smart pointer


 //     template <typename T> class smart_pointer { T *m_obj; public: //       smart_pointer(T *obj) : m_obj(obj) { } //          ~smart_pointer() { delete m_obj; } //  < // .      T  "" T* operator->() { return m_obj; } //             // ,    T& operator* () { return *m_obj; } } int test { //  myClass     smart_pointer<MyClass> pMyClass(new MyClass(/*params*/); //     MyClass   pMyClass->something(); // ,          ostream //          , //       std::cout << *pMyClass << std::endl; //      MyClass   } 

It is clear that our smart pointer is not without flaws (for example, how to store an array in it?), But it fully implements the RAII idiom. It behaves in the same way as a regular pointer (thanks to overloaded operators), and we don’t need to worry about freeing memory: everything will be done automatically. Optionally, const can be added to the overloaded operators, ensuring that the data referenced by the pointer remains unchanged.
Now, if you understand that you get certain advantages when using such pointers, consider their specific implementations. If you do not like this idea, then anyway, try to use them in some of its little program, I assure you should like it.
So, our smart pointers:


boost :: scoped_ptr


He is in the library boost .
The implementation is simple and straightforward, almost identical to ours, with a few exceptions, one of them: this pointer cannot be copied (that is, it has a private copy constructor and an assignment operator). Let me explain with an example:
 #include <boost/scoped_ptr.hpp> int test() { boost::scoped_ptr<int> p1(new int(6)); boost::scoped_ptr<int> p2(new int(1)); p1 = p2; // ! } 

This is understandable, if assignment was allowed, then both p1 and p2 would point to the same memory area. And on exit from the function, both will be deleted. What will happen? Nobody knows. Accordingly, this pointer can not be transferred to functions.
Then why is it needed? I advise you to use it as a pointer-wrapper for any data that is dynamically allocated at the beginning of the function and deleted at the end in order to save yourself from the headache about the correct cleaning of resources.
Detailed description here .

std :: auto_ptr


Slightly improved version of the previous one, besides, it is in the standard library (although in C ++ 11 it seems like deprecated). It has an assignment operator and a copy constructor, but they work somewhat unusually.
I explain:
 #include <memory> int test() { std::auto_ptr<MyObject> p1(new MyObject); std::auto_ptr<MyObject> p2; p2 = p1; } 

Now, when assigned to p2, there will be a pointer to MyObject (which we created for p1), but nothing will happen to p1. That is, p1 is now cleared. This is the so-called movement semantics. By the way, the copy operator does the same.
Why do you need it? Well, for example, you have a function that should create an object:
 std::auto_ptr<MyObject> giveMeMyObject(); 

This means that the function creates a new object of type MyObject and places it at your disposal. It will become clearer if this function is itself a member of a class (say Factory): you are sure that this class (Factory) does not store another pointer to a new object. Your object and one pointer to it.
Due to such unusual semantics, auto_ptr cannot be used in STL containers. But we have shared_ptr.

std :: shared_ptr (C ++ 11)


Smart pointer with reference counting. What does it mean. This means that somewhere there is some variable that stores the number of pointers that refer to an object. If this variable becomes zero, the object is destroyed. The counter is incremented by each call to either the copy operator or the assignment operator. Also shared_ptr has a casting operator to bool, which ultimately gives us the usual syntax of pointers, without worrying about freeing memory.
 #include <memory> //  <tr1/memory>  ,    C++11 #include <iostream> int test() { std::shared_ptr<MyObject> p1(new MyObject); std::shared_ptr<MyObject> p2; p2 = p1; if (p2) std::cout << "Hello, world!\n"; } 

Now both p2 and p1 point to one object, and the reference count is 2. On exiting the scop, the counter is reset and the object is destroyed. We can pass this pointer to a function:
 int test(std::shared_ptr<MyObject> p1) { //  - } 

Note that if you pass a pointer by reference, the counter will not be incremented. You must ensure that the MyObject object is alive while the test function is executed.

So, smart pointers are good, but there are downsides.
Firstly, this is a small overhead projector, but I think you will have a few processor cycles for the sake of such convenience.
Secondly, it is boiler-plate, for example
 std::tr1::shared_ptr<MyNamespace::Object> ptr = std::tr1::shared_ptr<MyNamespace::Object>(new MyNamespace::Object(param1, param2, param3)) 

This can be partially solved with the help of defines, for example:
 #define sptr(T) std::tr1::shared_ptr<T> 

Or using typedef.
Thirdly , there is the problem of circular references. I will not consider it here, so as not to enlarge the article. Also, boost :: weak_ptr, boost :: intrusive_ptr, and pointers for arrays remained unexamined.
By the way, smart pointers are quite well described by Jeff Elger in the book “C ++ for real programmers”.

Source: https://habr.com/ru/post/140222/


All Articles