Smart pointers

The smart pointers of C++ are library data types that automate the deallocation 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.

It would be possible to use normal pointers in dynamic memory management, but in such case it was programmer’s duty to deallocate memory, which is can easily be forgotten. On this course, we will only use smart pointers in dynamic memory management.

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 pointers (shared_ptr)

Automatic memory deallocation is based on reference counters. It is kept track on how many pointers there are pointing to a dynamic variable. When the reference counter has the value zero (i.e. there are no pointers pointing to a dynamic variable), memory allocated for the variable can be deallocated. We can find out the value of the reference counter by calling the function use_count.

There is a specific function make_shared for constructing a smart (shared) pointer, with the following properties:

  • The lifetime of a variable begins at the moment when make_shared is called.

  • A dynamic variable does not have a name, but the operation make_shared returns a pointer, the value of which reveals where the new dynamic variable is located within the main memory.

  • To make the dynamic variable useful, its address (the return value of make_shared) needs to be stored in a pointer variable.

  • If the variable is a class-type one, make_shared will take care of calling the constructor. Now, the parameters of make_shared are those required by the constructor.

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 = make_shared<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_1 = *int_ptr_1 + 4;
        int_ptr_2 = int_ptr_1;

        cout << *int_ptr_1 << " " << *int_ptr_2 << endl;
        cout <<  int_ptr_1 << " " <<  int_ptr_2 << endl;
        cout << int_ptr_1.use_count() << endl;
    }
    cout << int_ptr_1.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.

Defining another shared_ptr, but now with the value of 9.

Using shared_ptr pointers.

Calling the method use_count to print the value of the reference counter of 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_2 to point to the same memory address as int_ptr_1. Now, there are two shared_ptr-type pointers pointing to the memory cell where int_ptr_1 originally pointed to.

The code block, where int_ptr_2 was declared, ends. Therefore the lifetime of the variable int_ptr_2 ends. Now, there is only one shared_ptr-type pointer pointing to the memory cell where int_ptr_1 originally pointed to.

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
1

Here and in the other execution examples that print pointer values to the screen, please remember that the value of the memory address can be different at different executions.

Let us consider reference counters with the following code:

Person person; // Person object
shared_ptr<Person> p1 = make_shared<Person>(person);
shared_ptr<Person> p2 = make_shared<Person>(person);
shared_ptr<Person> p3 = p1;

cout << p1.use_count() << endl; // prints 2
cout << p2.use_count() << endl; // prints 1
cout << p3.use_count() << endl; // prints 2

Based on the code you could easily imagine that the pointers p1 and p2 point to the same object. However, from the point of view of reference counters, this is not so. The figure below clarifies the situation:

Values of reference counters

The function make_shared allocates memory as much as is needed for an object or a value of a certain type, which is given as a parameter. In the above code, memory is allocated twice as much as is needed for a Person object.

Properties of shared 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 = make_shared<double>( 1.0 );
    ...
    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;
};

void fun()
{
    shared_ptr<Test> ptr1 = make_shared<Test>( ... );
    shared_ptr<Test> ptr2 = make_shared<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.

Example: reservations

From this week’s examples you can find the one: examples/06/reservations. It handles reserving books from a library by using reference counters. When a book is reserved, a new pointer is put to point to the book. In this way, the value of the reference counter tells the number of reservations.