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.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

So far in this section, we’ve coverered how smart pointers are tools for managing dynamically allocated memory, ensuring that memory is automatically deallocated when it is no longer needed.

The C++ standard library provides a few variations of smart pointers. std::unique_ptr and std::shared_ptr are the most commonly used, but they serve different purposes:

Unique Pointers

Unique pointers, such as std::unique_ptr enforces unique ownership of the memory resource it manages. It implies that only one unique pointer can point to a specific resource at any time.

When the std::unique_ptr is destroyed or revokes its ownership through the std::move(), release() or reset() methods, the resource it points to is automatically deallocated. This type of smart pointer is lightweight and efficient, making it an ideal choice for most single-owner scenarios.

We covered unique pointers in detail a dedicated lesson:

Shared Pointers

Unlike std::unique_ptr, std::shared_ptr allows multiple pointers to share ownership of a single resource. The resource is only deallocated when the last std::shared_ptr pointing to it is destroyed or reset.

This shared ownership is managed through reference counting - an internal mechanism that keeps track of how many std::shared_ptr instances are managing the same resource. This comes at a performance cost, so in general, std::unique_ptr, should be our default choice of smart pointer.

However, there are scenarios where an object needs to be accessed and managed by multiple owners. In such cases, std::shared_ptr becomes invaluable, and we’ll cover it in detail in this lesson.

Similar to the previous lesson, we’ll be using the following Character class to demonstrate how shared pointers work.

It has a simple constructor and destructor that logs when objects are created and destroyed, so we can better understand when these steps happen:

#include <iostream>

class Character {
 public:
  std::string Name;
  Character(std::string Name = "Frodo")
  : Name{Name} {
    std::cout << "Creating " << Name << '\n';
  }

  ~Character(){
    std::cout << "Deleting " << Name << '\n';
  }
};

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 <iostream>
#include <memory>

class Character {/*...*/}; int main() { auto FrodoPointer { std::make_shared<Character>() }; }

The std::make_shared() function creates an object of the type specified between the < and > tokens, allocated in dynamic memory. It 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.

Any function arguments passed to std::make_shared() are forwarded to the underlying type’s constructor:

#include <iostream>
#include <memory>

class Character {/*...*/}; int main() { auto GandalfPointer { std::make_shared<Character>("Gandalf") }; }

Using Shared Pointers

In most ways, shared pointers can be used in the same way as unique pointers.

Dereferencing Shared Pointers using * and ->

We can dereference shared pointers using the * and -> operators:

#include <iostream>
#include <memory>

class Character {/*...*/}; int main() { auto FrodoPointer{ std::make_shared<Character>() }; std::cout << (*FrodoPointer).Name << '\n'; std::cout << FrodoPointer->Name << '\n'; std::cout << "Main Function Ending" << '\n'; }
Creating Frodo
Frodo
Frodo
Main Function Ending
Deleting Frodo

Accessing the Raw Pointer using get()

We can access the raw memory address being managed by a shared pointer using the get() method:

#include <iostream>
#include <memory>

class Character {/*...*/}; void LogName(const Character* Ptr){ std::cout << Ptr->Name << '\n'; } int main() { auto FrodoPointer{ std::make_shared<Character>()}; LogName(FrodoPointer.get()); std::cout << "Main Function Ending" << '\n'; }
Creating Frodo
Frodo
Main Function Ending
Deleting Frodo

Transferring Ownership using std::move()

A shared pointer can also transfer its ownership of a resource using std::move():

#include <iostream>
#include <memory>

class Character {/*...*/}; void TakeOwnership( std::shared_ptr<Character> Ptr ){ std::cout << Ptr->Name << '\n'; } int main() { auto FrodoPointer{ std::make_shared<Character>() }; TakeOwnership(std::move(FrodoPointer)); std::cout << "Main Function Ending" << '\n'; }

As before with unique pointers, note how our Character is now being deleted at the end of the TakeOwnership() function, rather than at the end of main:

Creating Frodo
Frodo
Deleting Frodo
Main Function Ending

Swapping Ownership using swap()

We also have access to swap(), allowing two shared pointers to exchange ownership of the objects they’re managing:

#include <iostream>
#include <memory>

class Character {/*...*/}; int main() { auto Pointer1{ std::make_shared<Character>("Frodo") }; auto Pointer2{ std::make_shared<Character>("Gandalf") }; std::cout << "1: " << Pointer1->Name << '\n'; std::cout << "2: " << Pointer2->Name << '\n'; Pointer1.swap(Pointer2); std::cout << "1: " << Pointer1->Name << '\n'; std::cout << "2: " << Pointer2->Name << '\n'; }
Creating Frodo
Creating Gandalf
1: Frodo
2: Gandalf
1: Gandalf
2: Frodo
Deleting Frodo
Deleting Gandalf

Resetting Ownership using reset()

Similar to unique pointers, a shared pointer can revoke its ownership of the underlying resource using reset(). Below, note how our Character is automatically deleted as soon as our shared pointer revokes its ownership over it:

#include <iostream>
#include <memory>

class Character {/*...*/}; int main() { auto Pointer{std::make_shared<Character>()}; Pointer.reset(); std::cout << "Main Function Ending" << '\n'; }
Creating Frodo
Deleting Frodo
Main Function Ending

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

#include <iostream>
#include <memory>

class Character {/*...*/}; int main() { auto Pointer{ std::make_shared<Character>("Frodo") }; Pointer.reset(new Character{"Gandalf"}); std::cout << "Main Function Ending" << '\n'; }
Creating Frodo
Creating Gandalf
Deleting Frodo
Main Function Ending
Deleting Gandalf

Unlike the unique pointer variation, the shared pointer’s reset() method may not cause the object to be deleted.

Smart pointers only delete the underlying resource if the resource has no owners.

With unique pointers, by design, the resource only has one owner. So, after calling reset() on a unique pointer, the resource will have no owners, so it will automatically be deleted.

However, there can be multiple shared pointers sharing ownership of the same resource. A single shared pointer being deleted, or revoking its ownership through reset() or move(), doesn’t necessarily mean the underlying resource will be deleted.

There may be other shared pointers that still have an ownership stake over it. It will only be destroyed after all of those shared pointers are destroyed, or they give up ownership.

In the rest of this lesson, we show how we can set up a collection of shared pointers such that they can all share ownership of an underlying resource.

Sharing Ownership

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:

#include <iostream>
#include <memory>

class Character {/*...*/}; int main() { 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 using the assignment operator =:

#include <iostream>
#include <memory>

class Character {/*...*/}; struct Party { std::shared_ptr<Character> Leader; }; void SomeFunction(std::shared_ptr<Character> C){ // ... } int main() { auto Pointer{std::make_shared<Character>()}; SomeFunction(Pointer); Party MyParty; MyParty.Leader = 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.

Getting the Owner Count using use_count()

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

#include <iostream>
#include <memory>

class Character {/*...*/}; struct Party { std::shared_ptr<Character> Leader; }; int main(){ auto Pointer{std::make_shared<Character>()}; std::cout << "Owners: " << Pointer.use_count() << '\n'; Party MyParty; MyParty.Leader = Pointer; std::cout << "Owners: " << Pointer.use_count() << '\n'; }
Creating Frodo
Owners: 1
Owners: 2
Deleting Frodo

A more complex example is below, showing the effect of functions like std::move() and reset() on the the ownership model:

#include <iostream>
#include <memory>

class Character {/*...*/}; struct Party { std::shared_ptr<Character> Leader; }; void SomeFunction(std::shared_ptr<Character> C){ // ... } int main(){ auto Frodo{std::make_shared<Character>()}; auto Party1{std::make_unique<Party>()}; auto Party2{std::make_unique<Party>()}; std::cout << "Frodo has " << Frodo.use_count() << " owner (the main function)\n\n"; std::cout << "Giving Party1 a copy\n"; Party1->Leader = Frodo; std::cout << "Frodo has " << Frodo.use_count() << " owners (main and Party1)\n\n"; std::cout << "Moving main's ownership" " to Party2\n"; Party2->Leader = std::move(Frodo); std::cout << "Frodo has " << Party1->Leader.use_count() << " owners (Party1 and Party2)\n\n"; std::cout << "Resetting Party2\n"; Party2.reset(); std::cout << "Frodo has " << Party1->Leader.use_count() << " owner left (Party1)\n\n"; std::cout << "Resetting Party1\n" << "Frodo will have no owners left so he" "\nwill be automatically deleted:\n"; Party1.reset(); std::cout << "\nMain function ending"; }
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)

Resetting Party2
Frodo has 1 owner left (Party1)

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

Main function ending

Pitfall: Not Copying Shared Pointers

The most common mistake people make with shared pointers is not creating them correctly.

The mechanism that enables a collection of shared pointers to co-ordinate their ownership is predicated on new pointers in the collection being created from existing pointers.

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.

For example, the following code is not copying the shared pointer:

#include <memory>
#include <iostream>

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

In the above code, even though Pointer1 and Pointer2 are managing the same object, they’re not aware of each other. This means they cannot correctly determine when the object should be deleted, which in turn will cause memory issues.

Therefore, when we want to share ownership of an object, we should ensure we’re instantiating new pointers from existing ones:

#include <memory>
#include <iostream>

int main(){
  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 dynamic_cast and static_cast, so too can shared pointers.

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

The only difference is, for shared pointers, we instead use std::dynamic_pointer_cast and std::static_pointer_cast. Rather than returning raw pointers, these functions return std::shared_ptr instances.

If std::dynamic_pointer_cast fails, it returns an "empty" shared pointer - that is, one that is not managing any underlying object.

Such a pointer evaluates to false when converted to a boolean therefore code using std::dynamic_pointer_cast looks very similar to code using dynamic_cast:

#include <iostream>
#include <memory>
using std::shared_ptr;

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

class Monster : public Character {};

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

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

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

Owning and Pointing At Different Objects

Shared pointers have an interesting feature where the object they are sharing ownership over does not necessarily need to be the same object they are pointing at.

This is enabled through an alternative std::shared_ptr constructor, sometimes referred to as the aliasing constructor, that accepts two arguments:

  1. The object to own , in the form of a std::shared_ptr to copy
  2. The object to point at, in the form of a memory address

In the following contrived example, we show the mechanism of this using integers. Alias is managing the dynamically allocated integer that has the value of 1, but is pointing at the stack-allocated integer that has a value of 2:

#include <iostream>
#include <memory>

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

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

The 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 before we’re done with it:

#include <iostream>
#include <memory>

class Character {/*...*/}; int main(){ auto Frodo{std::make_shared<Character>()}; std::shared_ptr<std::string> Name{ Frodo, &Frodo->Name }; std::cout << "Name: " << *Name; std::cout << "\nOwners: " << Name.use_count() << '\n'; }
Name: Frodo
Owners: 2

Summary

As we conclude this lesson, we've equipped ourselves with a thorough understanding of shared pointers, and the standard library’s std::shared_ptr in particular. We've seen how this type of smart pointer is using in scenarios where multiple ownership of a dynamic resource is necessary.

Key Points:

  • The Role of Shared Pointers and std::shared_ptr: We've learned that std::shared_ptr is used when an object needs multiple owners, and it keeps track of how many pointers own the resource.
  • Practical Functions: Functions like std::make_shared(), get(), reset(), and swap() offer us practical ways to manage and interact with shared pointers.
  • Shared Pointer Memory Management: Through std::shared_ptr, we've seen another example of how smart pointers handle automatic memory management, and the implications of multiple ownerships on that process.
  • Avoiding Common Mistakes: We discussed common pitfalls such as improper copying of shared pointers and how to avoid them.

With this knowledge, we’re now even better prepared to write safer and more managable programs

Looking Ahead: Weak Pointers

In our next lesson, we'll dive into std::weak_ptr, another type of smart pointer. We'll explore how weak pointers work, their relationship with shared pointers, and their practical applications.

This lesson will further enhance our understanding of memory management in C++, preparing us for more advanced projects.

Was this lesson useful?

Next Lesson

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
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
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
Next Lesson

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
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved