This is my learning notes from the course - Mastering C++ Standard Library Features.
Shared Pointers and Weak Pointers
- the concept of shared ownership
- What
std::shared_ptr
andstd::make_shared
are - What
std:: weak_ptr
is - Run-time overhead
Shared Ownership
The reasons of using shared ownership:
- Some resources need to be shared between multiple owners
- After acquisition of a resource, multiple objects can take/lose ownership of it
- When a resource has no more owners, it is automatically release
- Useful where there isn’t a clear lifetime for a particular resource (for example might depend on run-time conditions)
std::shared_ptr<T>
is a copyable template class that represents shared ownership over a dynamically allocated object.
It uses “reference counting“ to keep track of how many alive owners are present and releases the memory when that count reaches zero.
Copying a std::shared_ptr
shares ownership.
Moving a std::shared_ptr
transfer ownership.
1 |
|
In the example above, you can see that although the resource is acquired in a nested scope, it can be kept alive by sharing it with an owner living outside of the scope.
How does std::shared_ptr
work?
1 | std::shared_ptr<int> sp0{new int{42}}; |
1 | ---------------- ------------------------------------- --------------- |
std::make_shared
std::make_shared
has all the advantages of std::make_unique
:
- Prevents memory leaks due to unspecified order of evaluation
- Makes code terser and more readable
- Doesn’t require an explicit
new
call
std::make_shared
has, however, one more very important feature: it prevents an unnecessary additional allocation and improves cache locality.
Reference: http://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared
Consider the following code:
1 | std::shared_ptr<int> s0{new int{5}}; |
The compiler is forced to allocate twice here:
- one for the
int
- one for the
shared_ptr
‘s control block
This is wasteful, as both allocations could be coalesced into one.
However, if we use std::make_shared
, it allows implementations to only allocate once for both the shared object and the control block.
Consider the following code:
1 | auto s1 = std::make_shared<int>(5); |
Only one allocation will be made, and it’s good for both the int
and the shared_ptr
‘s control block.
This is a great optimization because it will prevent an extra allocation and also places both the object and control block in the same memory location, increasing cache locality, and this can have a big impact on performance.
Therefore, the recommendation is to always us std::make_shared
when dealing with shared pointers.
Run-Time Overhead
Shared pointer is NOT a zero-cost abstraction and it needs to deal with reference counting and control blocks, which might have a significant cost on your application.
Here’s the example code in C++ and its corresponding assembly output: (You can try the example on Compiler Explorer - here)
1 |
|
1 | f0(): |
In the case of the shared pointer(f1()
), even though the C++ code might look similar to f0()
, every time we are copying the original shared pointer, what we’re actually doing is accessing the control block and incrementing the reference count
.
Every time an instance of a shared pointer is being destroyed, we are again accessing the control block and decrementing the reference count
, until finally it reaches zero and the int is deallocated.
As you can see for the assembly, we are doing a lot more work compared to the raw pointer version. This is a very simple demonstration showing that shared pointer is not a zero-cost abstraction and might have a significant performance impact, depending on your application.
Strongly recommend to use Compiler Explorer and also write simple benchmarks to understand what the cost of std::make_shared
is and whether or not it is applicable to your library or application.
std::weak_ptr
std::weak_ptr
can only be constructed from instances of std::shared_ptr
or other weak pointers.
Reference: http://en.cppreference.com/w/cpp/memory/weak_ptr
Weak pointers can be used to check whether or not an object managed by std::shared_ptr
is alive:
1 |
|
Accessing an object through an std::weak_ptr
requires a conversion to std::shared_ptr
first as you can see in accessing_objects()
.
Note that, the interface of weak pointer
is explicitly designed to prevent you from accessing the object unless you have a shared pointer
to it. Weak pointer
can be used to observe the state of a heap-allocated object and if you are aware of its state and you want to access it, you are forced to use the lock member function to get a shared pointer
to it and gain access to the object.
What is std::weak_ptr
used for?
std::weak_ptr
can be used to break dependency cycles of std::shared_ptr
.
1 |
|
If we run the above code, nothing will print out upon exit from cycle()
, meaning nothing is being destroyed and released. The problem is that the shared pointer
to b
is keeping the b
instance alive, and the shared pointer
to a
is keeping the a
instance alive. So, basically the shared pointers
are preventing each other from being destroyed and from releasing ownership because they are creating a dependency cycle. This is something that cannot be solved by design with shared pointer
as there is no way that the reference count
can evaluate to the correct value if there is a dependency cycle.
What we can do instead is design our class in such a way that the ownership is linear and the cycles are handled by using weak pointer
, which is a non-owning temporary ownership modeling smart pointer.
In weak_cycle()
, again we have struct a
and struct b
, but there is a major difference, struct a
is still containing a shared pointer
to b
, but struct b
is containing a weak pointer
to a
, breaking the dependency cycle and making b
only observe an instance of a
. So, it is temporary ownership.
This is one of the most common use cases of weak pointer
, but this problem could be solved in other ways, mainly by using a reference, or a raw pointer, or maybe an index. So, weak pointer
is a tool in your toolbox that’s very useful, but it’s not always the right tool for the job.
Recap
A std::shared_ptr
wraps a reference counting mechanism around a raw pointer. So for each instance of the std::shared_ptr
the reference count is increased by one. If two std::shared_ptr
objects refer to each other, they will never get deleted because they will never end up with a reference count of zero.
std::weak_ptr
points to a std::shared_ptr
but does not increase its reference count. This means that the underlying object can still be deleted even though there is a std::weak_ptr
reference to it.
The way that std::weak_ptr
works is that it can be used to create a std::shared_ptr
for whenever one wants to use the underlying object. If, however, the object has already been deleted, then an empty instance of a std::shared_ptr
is returned. Since the reference count on the underlying object is not increased with a std::weak_ptr
reference, a circular reference will not result in the underlying object not being deleted.
Summary
- Learned that
std::shared_ptr
models shared ownership over a dynamically allocated object - Learned that it takes ownership of dynamically allocated memory with its constructor, and releases it in destructor
- Understood that the ownership can be transferred to other instances by using
std::move
- Understood that the ownership can be shared by copying
- Always used
std::make_shared
instead of directly instantiatestd::shared_ptr
to avoid memory leaks and unnecessary extra allocations - Learned that
std::shared_ptr
can have a significant run-time overhead
-> only use it when necessary - Used
std::weak_ptr
to *”observe”* shared ownership or break cycles