Smart pointers

Although all the actions with the dynamic memory can be implemented with the C++ pointers and the commands new and delete we presented to you earlier, using them is often pretty messy. Especially when handling complicated dynamic structures, we often end up in a situation where we allocate memory with new but do not remember to use delete to deallocate it. To make this duty easier, C++ includes so-called smart pointers among its tools.

The smart pointers of C++ are library data types that automate the deallocating of memory when nothing points to it anymore. In plain language: the allocated memory will be automatically deallocated when there are no (smart) pointer variables pointing to that memory location in the program.

Smart pointer types are great because you use them mostly like you use normal (raw) pointers, and on top of that, you do not have to worry about deallocating memory.

To use smart pointers, you must include in the beginning of your program the line

#include <memory>

which lets us use the types

shared_ptr
unique_ptr
weak_ptr

On this course, we will only get to know the type shared_ptr.

shared_ptr pointers

A simple example on the use of shared_ptr:

#include <iostream>
#include <memory>  // You must remember this.

using namespace std;

int main() {
    shared_ptr<int> int_ptr_1( new int(1) );
    shared_ptr<int> int_ptr_2( make_shared<int>(9) );

    cout << *int_ptr_1 << " " << *int_ptr_2 << endl;
    cout <<  int_ptr_1 << " " <<  int_ptr_2 << endl;
    cout <<  int_ptr_1.use_count() << " " <<  int_ptr_2.use_count() << endl << endl;

    *int_ptr_2 = *int_ptr_2 - 4;
    int_ptr_1 = int_ptr_2;

    cout << *int_ptr_1 << " " << *int_ptr_2 << endl;
    cout <<  int_ptr_1 << " " <<  int_ptr_2 << endl;
    cout <<  int_ptr_1.use_count() << " " <<  int_ptr_2.use_count() << endl;
}
Defining a pointer variable of the type shared_ptr, into which we can store the memory address of a int-type variable. Initializing it to point to the variable that is reserved dynamically with new and that has the value of 1.
The same action as on the line above, but we use the pointer formed by the function make_shared as the initial value of the shared_ptr pointer. The difference to the previous one is that this way is faster.
shared_ptr pointers are used mostly in the same way as normal pointers.

Calling the method use_count to print the value of the reference counter for both pointers. The method use_count returns the number of shared_ptr pointers pointing to the same memory location as the target object of the use_count method.

When the value of the reference counter reaches zero, the allocated memory will be deallocated automatically.

The most relevant part about the example: we set int_ptr_1 to point to the same memory address as int_ptr_2. Now, there are no shared_ptr-type pointers pointing to the allocated memory where int_ptr_1 originally pointed to: the allocated memory is automatically deallocated.
The lifetime of the variables int_ptr_1 and int_ptr_2 ends, which means that the memory area they pointed to has no shared_ptr pointers: the allocated memory is automatically deallocated.

The execution of the program creates the following prints:

1 9
0x2589010 0x2589060
1 1

5 5
0x2589060 0x2589060
2 2

Please note that the example does not have any delete commands, even though dynamic memory is allocated both with new and the function make_shared. This is precisely the idea of the smart pointers, i.e. moving the responsibility for deallocating dynamically allocated memory to shared_ptr objects. We often use the term owner, which is just a fancy term to describe whose responsibility memory deallocation is.

In the example above, we compared using shared_ptr pointers to using normal pointers. Almost all the operations (the unary *, ->, comparison, and printing) that work with normal pointers work with shared_ptr pointers as well. The greatest difference is that the operators ++ and -- do not work with shared_ptr. Also, assignment is possible if you do it from another shared_ptr of the same type.

Below we list a couple of useful characteristics of shared_ptr that you might have use for later:

  • If you want to get the memory address from a shared_ptr pointer as a normal C++ pointer, you can do it with the method get:

    shared_ptr<double> shared_double_ptr( new double );
    ...
    double *normal_double_ptr = nullptr;
    ...
    normal_double_ptr = shared_double_ptr.get();
    
  • You cannot use the operator = to assign a normal pointer to a shared_ptr pointer.

  • However, you can assign a nullptr to a shared_ptr pointer.

  • A shared_ptr pointer cannot be directly compared to a normal pointer. The comparison is possible if the shared_ptr is changed into a normal pointer with the method get, for example:

    if(normal_pointer == shared_pointer.get()) {
        ...
    }
    
  • A shared_ptr pointer can be compared with a nullptr.

The type shared_ptr has a troublesome characteristic: If you create a ”loop” of them, the memory will never be deallocated:

#include <iostream>
#include <memory>

using namespace std;

struct Test {
    // Other fields
    // ···
    shared_ptr<Test> s_ptr;
};

int main() {
    shared_ptr<Test> ptr1(new Test);
    shared_ptr<Test> ptr2(new Test);

    ptr1->s_ptr = ptr2;
    ptr2->s_ptr = ptr1;
}

It is useful to draw a picture of the situation above so that you understand the kind of ”egg or chicken” problem we have here. The memory that ptr1 points to cannot be deallocated because the pointer ptr2->s_ptr points to it. Then again, the memory that ptr2 points to cannot be deallocated either because ptr1->s_ptr points to it.

Task list with shared_ptr pointers

See the project examples/10/task_list_v2 in Qt Creator. If you are also interested in executing and editing the program code, copy it under your student directory.

The project includes an implementation of a list structure that uses shared_ptr pointers, equivalent to an example on the previous round. The only algorithmically new thing in the modified example is that there is no need to implement a destructor for the List class (or to use delete commands on any other occasion) because the shared_ptr pointers deallocate memory when nothing points to it anymore.

Please note that all the different situations in handling a linked list must be taken into account, just like when using normal pointers (inserting the first element into an empty list, removing the last element from the list, etc).