Smart pointers in C++ provide a safer and cleaner way of manipulating pointers. They save all the hassle caused, for example, by memory leaks and dangling pointers.
This article covers smart pointers both from a theoretical and practical point of view:
- it introduces the three smart pointer classes available in the standard library and shows you how to use them;
- it shows how to implement a simple version of two of those classes from scratch so you can understand the underlying mechanisms.
General idea and motivation
When dynamically allocating memory in a C++ program, we need to manually deallocate it, usually with the delete
keyword:
This program displays:
Oh hello! I am X!
I'm dying…
The first line was printed by instruction line 20 and the second one by instruction line 21.
Having to deallocate a dynamically allocated memory block can cause some problems. In particular:
- We may forget to delete a pointer, causing a memory leak.
- It may make it harder to make a program exception-safe. For example, if
saySomething
or the constructor ofMyClass
throws an exception then thedelete
statement line 21 will not be executed and the destructor ofMyClass
will not be called, resulting in memory leaks and potentially more severe consequences.
If myClass
was not allocated dynamically then the destructor would be called automatically when the object goes out of scope. One of the main goals of smart pointers is to extend this behaviour to pointer: smart pointers are wrappers around pointers which provide, among other things, automatic memory management.
They use operator overloading to mimic the syntax of regular pointer manipulation, like *
and ->
.
The standard library provides three smart pointer classes: unique_ptr
, shared_ptr
and weak_ptr
.
Unique pointers
The class unique_ptr
is the simplest kind of smart pointers in C++. There are two things you need to know about them.
- As soon as they go out of scope, they are automatically deleted. We call such pointers “scoped pointers”. For this reason, they are especially useful when we want to reference dynamically allocated objects within a restricted scope.
- You can’t copy them (hence the qualifier unique).
Those two points are connected: if you have a copy x
of a scoped pointer y
then when y
goes out of scope, the memory block to which both x
and y
are pointing is deallocated. x
would thus become a dangling pointer.
What you can’t do with unique pointers
As we just said, there are two operations that you cannot perform with unique pointers: copy and assignment. The program below shows two operations that would cause an error:
If you need to move around a unique pointer and pass it to another function, you may want to use the standard library function move
, as described in the example below:
After using move
, ptr
becomes a null pointer: you transfer the ownership of the block ptr
pointed to from ptr
to bar
.
Example
Let’s go back to the example we gave earlier. First of all, we need to include the memory
header on top of the file:
#include <memory>
We’ll then modify the main
function so that it looks like this:
We create a unique pointer using std::make_unique
. It takes the sequence of arguments to pass to the constructor of MyClass
. We can then use the arrow operator to access attributes and methods of the object, just like we would do with regular pointers.
The code above prints the following two lines:
Oh hello! I am X!
I'm dying…
As you can see, the destructor is called when we get out of scope, meaning that the memory block where the instance of MyClass
was stored has indeed been automatically deallocated.
Smart pointers work with primitive types as well:
This yields the following output:
0x7fc6d3405b40
1
The first line indicates the memory address of the pointer. The second line indicates the value to which it points.
Implement unique pointers from scratch
We’ll create a generic class called MiniUniquePointer
and dependant on type T
. It will store a pointer mPtr
of type T*
and the following methods:
- the constructor, which takes a single parameter of type
T*
and assigns it tomPtr
, - the destructor, which deletes
mPtr
, operator*
andoperator->
which correspond to the regular pointer operators.operator*
returns a reference to the value behindmPtr
(i.e.*mPtr
) andoperator->
returnsmPtr
itself.
Here is the implementation:
In addition to defining the four methods we described above, we explicitly delete the copy constructor and the assignment operator to prevent users from making copies.
We then instantiate and use MiniUniquePointer
as follows:
Shared pointers
The second kind of smart pointer that exists in C++ is shared pointer. Shared pointers can be copied and the memory block they point to is not deallocated until all other shared pointers that point to it have been destructed. When a shared pointer is instantiated, an extra memory block is allocated. It is called the control block and keeps track of the number of copies that have been made. The object shared pointers point to is deleted as soon as the reference counter reaches 0, that is when the last remaining copy is destroyed or assigned another pointer.
This additional functionality is at the expense of a heavier overhead, so you should not use shared pointers when a unique pointer suffices.
Example
Just as we did with unique pointers, we create shared pointers by passing the arguments of the constructor of a class to the function std::make_shared
:
Like with a unique pointer, we can access members of the corresponding instance of MyClass
using ->
and the memory block it takes is freed when the main
function terminates. However, we can now make new copies of sharedPtr
and share them across multiple scopes:
If we run the program above, we’ll get the following output:
From main Oh hello! I am X!
From hello Oh hello! I am X!
Back in main
I'm dying…
As we can see — and as we should expect — the instance of MyClass
to which sharedPtr
and localPtr
point is deleted once the main function terminates. localPtr
went out of scope before, but the reference counter remembered that sharedPtr
could still need to use it.
Implementation
We’ll start by creating a class ControlBlock
which will be in charge of counting references. It will have one attribute, mReferenceCount
of type int
and two methods, add
and remove
which increment and decrement mReferenceCount
respectively. The implementation is very straightforward:
One thing to notice: we are not interested in the number of references after adding a new reference, but we’ll need to check if it has reached zero after removing a reference. This is why the return type of add
and remove
are different.
The structure of the core MiniSharedPointer
class is pretty similar to the one of MiniUniquePointer
. However, we need to store a pointer to a control block (mControlBlock
) and there are many differences in the way we implement the methods:
- the constructor instantiates a new control block in addition to storing the pointer,
- the destructor tells the control block that a reference has been removed and deletes the pointer (and frees the control block) only if the reference counter has reached zero,
- the copy constructor increments the reference counter and copies the value of
mControlBlock
andmPtr
from the existing instance to the one which is being constructed, - the assignment operator decrements the reference counter of the left-hand side and performs the appropriate cleanup if it reaches zero, then does the same thing as if we called the copy constructor to copy the left-hand side into the right-hand side.
We implement it as follows:
Addressing dangling pointers and circular dependency
The last kind of smart pointer we cover is weak pointers. Weak pointers are similar to shared pointers except that they have no control block and reference counter. This means that you can share them, but the object they point to may be destroyed before they go out of scope or are assigned another pointer.
There are two methods you need to know to manipulate weak pointers:
expired
: it takes no arguments and returnstrue
is the memory block it points to has been deallocated,false
otherwise;lock
: it takes no arguments and returnsexpired() ? shared_pointer<T>() : shared_pointer(*this)
. It is a "safe dereferencing" method as it enables to get the value of the memory block after checking if it has not been deallocated.
The following example demonstrates their behaviour:
Here is the output:
I'm dying…
weakPtr points to a deallocated block
sharedPtr
goes out of scope (line 8). The memory block it points to is subsequently deallocated, even if weakPtr
still holds a reference to it. Thus, weakPtr.lock()
returns an empty shared pointer and the condition line 11 is not satisfied.
Weak pointers have two main advantages:
- They prevent errors caused by dangling pointers: by providing a way of checking whether the block they point to has been deallocated, they prevent users from dereferencing and manipulating dangling pointers.
- They address circular dependency issues. For example, if you run the following code:
then neither a
nor b
will be destructed: b
cannot be destructed as long as a
is alive, because a
contains a reference to b
and similarly, a
cannot be destructed because b
contains a reference to it. This causes a memory leak.
To avoid this problem, we can simply change the type of the attributes a
and b
of B
and A
from shared_ptr
to weak_ptr
.
References:
Implementing a simple smart pointer in C++
https://www.youtube.com/watch?v=UOB7-B2MfwA&list=PLlrATfBNZ98dudnM48yfGUldqGD0S4FFb&index=44
http://ootips.org/yonat/4dev/smart-pointers.html