std::shared_ptr
std::shared_ptr
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, 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:
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")
};
}
In most ways, shared pointers can be used in the same way as unique pointers.
*
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
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
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
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
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.
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.
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
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
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!
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:
std::shared_ptr
to copyIn 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
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.
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.std::make_shared()
, get()
, reset()
, and swap()
offer us practical ways to manage and interact with shared pointers.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.With this knowledge, we’re now even better prepared to write safer and more managable programs
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.
std::shared_ptr
An introduction to shared memory ownership using std::shared_ptr
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.