An introduction to smart pointers in C++

Zakarie A
CodeX
Published in
7 min readSep 9, 2021

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.
Photo by ROBIN WORRALL on Unsplash

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 of MyClass throws an exception then the delete statement line 21 will not be executed and the destructor of MyClasswill 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 to mPtr,
  • the destructor, which deletes mPtr,
  • operator* and operator-> which correspond to the regular pointer operators. operator* returns a reference to the value behind mPtr (i.e. *mPtr) and operator-> returns mPtr 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 mReferenceCountrespectively. 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 and mPtr 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 returns true is the memory block it points to has been deallocated, falseotherwise;
  • lock: it takes no arguments and returns expired() ? 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

https://iamsorush.com/posts/weak-pointer-cpp/

Move smart pointers in and out functions in modern C++

--

--