Hello again!
In this video, we are going to look at shared_ptr. The shared_ptr type was introduced in C++11,
in the library. It uses reference counting, so we can have different shared_ptr objects which
share the same memory allocation.
When we copy or assign one of these objects, there are no memory operations.
Instead, it just increments the reference counter.
If one of the objects is destroyed, then the reference counter is decremented.
in the destructor.
And if that was the last shared_ptr object which was sharing that memory, then the counter is
equal to zero, and the allocated memory is released.
shared_ptr
has a private situation member, which points to the allocated memory, and it also has another
private data member, which points to the so-called "control block".
This control block contains the reference counter, and a few other things.
So a shared memory object will have the memory allocation and the control block.
The shared_ptr
class is defined in the header. To create a shared_ptr object,
we can pass a pointer to the constructor, or we can call make_shared().
So it is just like creating a unique_ptr.
We can also initialize a ahared_ptr object by moving a unique_ptr into it.
So we have this unique_ptr, which is a single object, which has total ownership of the memory.
And we can transfer that ownership to shared_ptr objects, which share that ownership amongst themselves.
So this could be useful, if you are working with a factory function which returns a unique_ptr, and
you actually want a shared_ptr.
And also, if you start writing a program using unique_ptrs, and you realize later on that you actually
want shared_ptrs, then you can just convert them.
But you cannot go the other way around.
You cannot convert a shared_ptr to a unique_ptr. make_shared() is available in C++11, unlike
make_uunique(), which they forgot about until C++14!
So we need to give the type of the object in memory.
And we pass the constructor arguments. And when we call make_shared(), this will perform a single call
to new. And that allocates the shared memory and the control block, in one single contiguous block of
memory.
If we pass a pointer which was returned by new, then the shared_ptr constructor will call new again,
to allocate the control block.
So that is two calls to new. Two memory allocations. And with most memory managers in most operating systems, these
two allocations could well be in different parts of memory, which means that accessing them is less
efficient.
Modern hardware is optimised for accessing memory objects which are next to each other.
And then the copy instructor, assignment operator and the move operators work just like the example
in the last video.
So if we copy, then the reference counter is incremented.
If we assign, then the reference counter is incremented in the object that is being assigned from,
and decremented in the object that is being assigned to.
And that could cause the memory to be released, if the counter goes down to zero.
And then, after this assignment, "p3" will now have the same memory pointer and control block pointer
as "p1".
If we move from a shared_ptr object, the memory allocation pointer is set to null.
The reference counter in "p4" is the same as it was in "p1".
So, if you like, this target object will steal the memory allocation and the control block from the argument.
shared_ptr supports the same observations, more or less, that unique_ptr does.
So we can dereference a shared_ptr to access the memory, we can set it to null to delete the memory.
We can also assign and copy them, as well as moving them.
But we still cannot do pointer arithmetic.
If we have code which uses threads, then there is a possible data race.
If we have two threads, it is possible that they could try to modify the reference counter concurrently.
In fact, the reference counter is "atomic".
So copying or assigning shared_ptr objects is thread-safe.
However, there is no protection for the actual allocated memory.
So if you want to access that, within threads, you must provide your own protection, typically by locking
a mutex.
In C++20, there is an atomic shared_ptr.
The drawback of this is that operations on atomic variables take much longer,
even if your code is not threaded.
So, should you replace all your unique_ptrs with shared_ptrs?
And the answer is, probably not.
unique_ptr is very efficient.
There's no more overhead to using a unique_ptr than there is to using a traditional pointer with
new. The shared_ptr has more overhead.
The control block has to be initialized when you create the first shared_ptr object. And also, if
you copy, assign or destroy a shared_ptr object, the reference counter needs to be updated, which
is a slow operation.
And as we said, you can convert a unique_ptr to shared_ptr, but not vice-versa.
So with unique_ptrs, you can change your mind.
So the recommendation is, only use a shared_ptr when you need the sharing or if it adds value.
Some people say that you should use a shared_ptr all the time, because it is easier.
And yes, you can do that, if you are not worried about efficiency.
So, where are shared_ptrs useful? If you have different objects which need to have access to the
same area of heap memory.
For example, if we have a large document, which takes a lot of memory to store, and it has a lot of duplicated
words, we can reduce the memory usage, by storing the words in a shared_ptr.
So we just have one memory allocation for each word, as opposed to having a different memory allocation
each time the word appears in the document.
And if we have a web browser where we have several tabs which have the same image, so these could be
a company logo or maybe avatars in a discussion forum.
Again, we could save memory by using a shared_ptr, so we only store the image once.
Okay, so that is it for this video.
I will see you next time.
Until then, keep coding!