C++ Shared Pointers using std::shared_ptr

An introduction to shared memory ownership using std::shared_ptr
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

vb2.jpg
Ryan McCombe
Ryan McCombe
Posted

For most cases, we want our objects to have a single owner. Therefore, std::unique_ptr should be our preferred way to store pointers.

However, in some cases, we need our objects to have multiple owners. This is where std::shared_ptr can help us.

Similar to the previous lesson, we’ll be using this file to demonstrate how shared pointers work:

#include <iostream>
#include <memory>

using namespace std;

class Character {
public:
  string Name;
  Character(string Name = "Frodo") :
    Name { Name }
  {
    cout << "Creating " << Name << endl;
  }

  ~Character() {
    cout << "Deleting " << Name << endl;
  }
};

int main() {
  // ...
}

std::make_shared

We previously saw how the helper function std::make_unique created a std::unique_ptr for us. Predictably, we also have std::make_shared to create a std::shared_ptr:

#include <memory>

int main() {
  auto FrodoPointer { std::make_shared<Character>() };
}

The std::make_shared function creates an object of the type specified between the < and > tokens, and returns a std::shared_ptr of that same type.

For example std::make_shared<Character> will create a Character in dynamic memory, and return a std::shared_ptr<Character> that points to it.

Constructor arguments can be passed between the ():

#include <memory>

int main() {
  auto FrodoPointer {
    std::make_shared<Character>("Gandalf")
  };
}

Using Shared Pointers

In most ways, shared pointers can be used in exactly the same way as unique pointers. We can dereference them using the * and -> operators, and they automatically delete the object when appropriate:

#include <iostream>
#include <memory>
using namespace std;

int main() {
  auto FrodoPointer { make_shared<Character>() };
  cout << (*FrodoPointer).Name << endl;
  cout << FrodoPointer->Name << endl;
  cout << "Main Function Ending" << endl;
}
Creating Frodo
Frodo
Frodo
Main Function Ending
Deleting Frodo

We can access the raw pointer using get:

#include <iostream>
#include <memory>
using namespace std;

void LogName(const Character* Ptr) {
  cout << Ptr->Name << endl;
}

int main() {
  auto FrodoPointer { make_shared<Character>() };
  LogName(FrodoPointer.get());
  cout << "Main Function Ending" << endl;
}
Creating Frodo
Frodo
Main Function Ending
Deleting Frodo

We can also transfer ownership using std::move:

#include <iostream>
#include <memory>
using namespace std;

void TakeOwnership(shared_ptr<Character> Ptr) {
  cout << Ptr->Name << endl;
}

int main() {
  auto FrodoPointer { make_shared<Character>() };
  TakeOwnership(move(FrodoPointer));
  cout << "Main Function Ending" << endl;
}

As before with unique pointers, note how Frodo is now deleted at the end of the TakeOwnership function, rather than at the end of main

Creating Frodo
Frodo
Deleting Frodo
Main Function Ending

We can also reset :

#include <iostream>
#include <memory>

int main() {
  using namespace std;
  auto Pointer { make_shared<Character>() };
  Pointer.reset();
  cout << "Main Function Ending" << endl;
}
Creating Frodo
Deleting Frodo
Main Function Ending

If the pointer we called reset on was the only shared pointer that owned the resource, the resource will be deleted. More information on how ownership can be shared is coming later in this lesson.

Reset can also be used to replace the managed object with a new one of the same type (or convertible to the same type):

int main() {
  using namespace std;
  auto Pointer { make_shared<Character>("Frodo") };
  Pointer.reset(new Character {"Gandalf"});
  cout << "Main Function Ending" << endl;
}
Creating Frodo
Creating Gandalf
Deleting Frodo
Main Function Ending
Deleting Gandalf

And finally, we still have access to swap:

#include <iostream>
#include <memory>

int main() {
  using namespace std;
  auto Pointer1 { make_shared<Character>("Frodo") };
  auto Pointer2 { make_shared<Character>("Gandalf") };

  cout << "1: " << Pointer1->Name << endl;
  cout << "2: " << Pointer2->Name << endl;
  
  Pointer1.swap(Pointer2);

  cout << "1: " << Pointer1->Name << endl;
  cout << "2: " << Pointer2->Name << endl;
}
Creating Frodo
Creating Gandalf
1: Frodo
2: Gandalf
1: Gandalf
2: Frodo
Deleting Frodo
Deleting Gandalf

Unlike unique pointers, shared pointers do not have a release function.

Sharing Shared Pointers

The key difference between unique and shared pointers is that, predictably, shared pointers can be shared. This is done simply by creating copies of them:

auto Pointer1 { std::make_shared<Character>() };
auto Pointer2 { Pointer1 };

Most commonly, these copies are created by passing the shared pointer by value into a function, or setting it as a member variable on some object:

auto Pointer { make_shared<Character>() };
SomeFunction(Pointer);
SomeObject.SomeVariable = Pointer;

This is the key mechanism that allows a resource to have multiple owners. Every function or object that has a copy of the shared pointer is considered an owner.

The underlying resource is deleted only when all of its owners are deleted.

We can see how many owners a shared pointer has using the use_count class function:

#include <iostream>
#include <memory>

using namespace std;

class Party {
public:
  shared_ptr<Character> Member;
};

int main() {
  auto Frodo { make_shared<Character>() };
  auto Party1 { make_unique<Party>()};
  auto Party2 { make_unique<Party>()};
  
  cout << "Frodo has "
       << Frodo.use_count()
       << " owner (the main function)"
       << endl << endl;

  cout << "Giving Party1 a copy" << endl;
  Party1->Member = Frodo;
  cout << "Frodo has "
       << Frodo.use_count()
       << " owners (main and Party1)"
       << endl << endl;

  cout << "Moving main's ownership to Party2" << endl;
  Party2->Member = move(Frodo);
  cout << "Frodo has "
       << Party1->Member.use_count()
       << " owners (Party1 and Party2)"
       << endl << endl;

  cout << "Deleting Party2" << endl;
  Party2.reset();
  
  cout << "Frodo has "
       << Party1->Member.use_count()
       << " owner left (Party1)"
       << endl << endl;

  cout << "Deleting Party1"
       << endl
       << "Frodo will have no owners left so "
       << "he will be automatically deleted"
       << endl << endl;
  Party1.reset();
  
  cout << "Main function ending" << endl;
}
Creating Frodo
Frodo has 1 owner (the main function)

Giving Party1 a copy
Frodo has 2 owners (main and Party1)

Moving main's ownership to Party2
Frodo has 2 owners (Party1 and Party2)

Deleting Party2
Frodo has 1 owner left (Party1)

Deleting Party1
Frodo will have no owners left so he will be automatically deleted

Deleting Frodo
Main function ending

Common Mistake: Not Copying Shared Pointers

By far, the most common mistake people make with shared pointers is not copying them when they intend to.

For example, the following code is not copying the shared pointer. Rather, it is creating a new shared pointer, from a raw pointer:

int main() {
  auto Pointer1 { make_shared<Character>() };
  shared_ptr<Character> Pointer2 { Pointer1.get() };
}

The mechanism that enables shared pointers to be “connected” is predicated on the copy operator. Therefore, for a new shared pointer to be connected to the existing set of pointers, it needs to be copied from one of the pointers in that set.

Creating a new shared pointer in any other way will bypass this mechanism, even if the shared pointer happens to be pointing to the same object as another shared pointer:

int x { 42 };
std::shared_ptr<int> Pointer1 { &x };
std::shared_ptr<int> Pointer2 { &x };
std::cout "Owners: " << Pointer1.use_count();
Owners: 1

Therefore, when we want to share ownership of an object, we should ensure we are triggering the copy constructor of the shared pointer.

int x { 42 };
std::shared_ptr<int> Pointer1 { &x };
std::shared_ptr<int> Pointer2 { Pointer1 };
std::cout "Owners: " << Pointer1.use_count();
Owners: 2

Shared Pointer Casting

Just like raw pointers can be cast to other types with std::dynamic_cast and std::static_cast, so too can shared pointers.

The only difference is, for shared pointers, we instead use std::dynamic_pointer_cast and std::static_pointer_cast, available from including <memory>

We have dedicated lessons on static and dynamic casting available here:

An example of using std::dynamic_pointer_cast being used with a std::shared_ptr is below:

#include <iostream>
#include <memory>

class Character {
public:
  virtual ~Character() = default;
};

class Monster : public Character {};

void Downcast(std::shared_ptr<Character> Attacker) {
  auto MonsterPtr {
    std::dynamic_pointer_cast<Monster>(Attacker)
  };
  if (MonsterPtr) {
    std::cout << "It's a monster!\n";
  } else {
    std::cout << "It's not a monster!\n";
  }
}

int main() {
  // A character pointer pointing at a monster
  std::shared_ptr<Character> CharacterPtrA {
    std::make_shared<Monster>()
  };
  Downcast(CharacterPtrA);

  // A character pointer pointing at a character
  std::shared_ptr<Character> CharacterPtrB {
    std::make_shared<Character>()
  };
  Downcast(CharacterPtrB);
}
It's a monster!
It's not a monster!

Shared Pointer Aliasing

Shared pointers have an interesting feature where the object they are sharing ownership over need not be the same as the object they are pointing at. This is typically referred to as aliasing.

It can be done by passing a pointer as the second argument to the shared_ptr constructor:

int main() {
  auto One { make_shared<int>(1) };
  int Two { 2 };
  shared_ptr<int> Alias { One, &Two };

  cout << "Number Pointed At: " << *Alias << endl;
}
Number Pointed At: 2

Care should be used when doing this, as it can create a memory management setup that is very confusing.

The simplest and most useful application of this is to create a pointer that shares ownership of an object, whilst pointing to a specific member of that same object.

For example, below we have a smart pointer that points to a character’s Name field, but shares ownership of the Character as a whole.

This gives us a pointer we can use to directly access the specific object property we want, whilst also preventing the object itself from being deleted from under us:

int main() {
  auto Frodo { make_shared<Character>() };
  shared_ptr<string> Name { Frodo, &Frodo->Name };

  cout << "Name: " << *Name << endl;
  cout << "Owners: " << Name.use_count() << endl;
}
Name: Frodo
Owners: 2

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Weak Pointers with std::weak_ptr

A full guide to weak pointers, using std::weak_ptr. Learn what they’re for, and how we can use them with practical examples
02b.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved