std::shared_ptr: a Smart Pointer that Keeps Count of its Copies

A std::shared_ptr owns the object it points to but, unlike std::unique_ptr, it allows for multiple references. A special internal counter is decreased each time a std::shared_ptr pointing to the same resource goes out of scope. This technique is called reference counting. When the very last one is destroyed the counter goes to zero and the data will be deallocated.

This type of smart pointer is useful when you want to share your dynamically-allocated data around, the same way you would do with raw pointers or references.

How to construct a std::shared_ptr

A std::shared_ptr is constructed like this:

std::shared_ptr<Type> p(new Type);

For example:

std::shared_ptr<int>    p1(new int);
std::shared_ptr<Object> p2(new Object("Lamp"));

There is an alternate way to build a std::shared_ptr, powered by the special function std::make_shared:

std::shared_ptr<Type> p = std::make_shared<Type>(...parameters...);

For example:

std::shared_ptr<int>    p1 = std::make_shared<int>();
std::shared_ptr<Object> p2 = std::make_shared<Object>("Lamp");

This should be the preferred way to construct this kind of smart pointer. I'll show you why in the last (sub)section.

Issues with arrays

Until C++17 there is no easy way to build a std::shared_ptr holding an array. Prior to C++17 this smart pointer always calls delete by default (and not delete[]) on its resource: you can create a workaround by using a custom deleter. One of the many std::shared_ptr constructors takes a lambda as second parameter, where you manually delete the object it owns. For example:

std::shared_ptr<int[]> p2(new int[16], [] (int* i) {
  delete[] i; // Custom delete
});

Unfortunately there's no way to do this when using std::make_shared.

std::shared_ptr in action

One of the main features of std::shared_ptr is the ability to track how many pointers refer to the same resource. You can get information on the number or references with the method use_count(). Consider this:

void compute()
{
  std::shared_ptr<int> ptr = std::make_shared<int>(100);
  // ptr.use_count() == 1
  std::shared_ptr<int> ptr_copy = ptr;   // Make a copy: with shared_ptr we can!
  // ptr.use_count() == 2
  // ptr_copy.use_count() == 2, it's the same underlying data after all
} // `ptr` and `ptr_copy` go out of scope here. No more references to the
  // original data (i.e. use_count() == 0), so it is automatically cleaned up.

int main()
{
  compute();
}

Notice how both ptr and ptr_copy go out of scope at the end of the function, bringing the reference count down to zero. At that point, the destructor of the last object detects that there aren't any more references around and triggers the memory cleanup.

One resource, many std::shared_ptr. Mind the circular references!

The power of multiple references may lead to nasty surprises. Say I'm writing a game where a player has another player as companion, like this:

struct Player
{
  std::shared_ptr<Player> companion;
  ~Player() { std::cout << "~Player\n"; }
};

int main()
{
  std::shared_ptr<Player> jasmine = std::make_shared<Player>();
  std::shared_ptr<Player> albert  = std::make_shared<Player>();

  jasmine->companion = albert; // (1)
  albert->companion  = jasmine; // (2)
}

Makes sense, doesn't it? Unfortunately, I have just created the so-called circular reference. At the beginning of my program I create two smart pointers jasmine and albert that store dynamically-created objects: let's call this dynamic data jasmine-data and albert-data to make things clearer.

Then, in (1) I give jasmine a pointer to albert-data, while in (2) albert holds a pointer to jasmine-data. This is like giving each player a companion.

When jasmine goes out of scope at the end of the program, its destructor can't cleanup the memory: there is still one smart pointer pointing at jasmine-data, that is albert->companion. Likewise, when albert goes out of scope at the end of the program, its destructor can't cleanup the memory: a reference to albert-data still lives through jasmine->companion. At this point the program just quits without freeing memory: a memory leak in all its splendor. If you run the snippet above you will notice how the ~Player() will never get called.

This is not a huge problem here, as the operating system will take care of cleaning up the memory for you. However you don't really want to have such circular dependencies (i.e. memory leaks) in the middle of your program. Fortunately the last type of smart pointer will come to the rescue.