Unique pointer is not the only smart pointer class
provided by the standard library.
We can also use shared pointers and weak pointers.
Let's first talk about shared pointers.
We said that unique pointer is named unique because it has
an exclusive ownership of the resource.
Shared pointers have non‑exclusive ownership.
In other words,
multiple shared pointers can share the ownership of a single resource in memory.
Their declaration looks similar to the declaration of unique pointers.
We use the templated shared pointer class to declare a new shared
pointer, and just like with unique pointers,
there is a complimentary make_shared function which allocates
an object of this specific type on the heap.
But unlike make_unique,
this make_shared function has a more important role. Since shared
pointers can share the same resource, copy semantics are allowed.
So, we can do something like this.
I will initialize a new shared pointer with another one.
Now both of these pointers share the ownership of this single resource, but
which one of these two will be responsible for deallocating this resource
once they both go out of scope? Well it turns out that the shared pointer
has two internal raw pointers, instead of one. The first one, of course,
points to the resource itself,
but the other one points to the so‑called control block.
This control block is also an object on the heap, and among other
things, it keeps track of the reference count.
Reference count is the number of shared pointers which
point to the same exact object in memory.
So, when we declare this first pointer,
the resource and the control block are allocated on the heap, and then
this copy constructor will make a shallow copy of both these internal
pointers, so this other shared pointer will point to the same resource
and the same control block.
The reference count on this shared control block will be incremented by one,
because two pointers are now pointing to the same resource.
So this is how shared ownership works.
The reference count in the common control black
holds the total number of owners.
When one of these pointers goes out of scope,
the reference count will decrease by one.
Only the last remaining pointer is responsible for
deallocating the owned resource from the heap.
The make_shared function is useful because it can optimize
performance by allocating the resource and the control block in a
single block of memory, so only one heap allocation is necessary
to initialize this shared pointer.
If you just use new, then the resource and the control block will be
allocated separately, so two potentially slow heap allocations.
Since I already implemented my own version of the unique pointer,
I thought that it will be useful to modify the custom smart pointer class
to turn it into a simpler version of a shared pointer.
This will help you visualize how the standard
shared pointer class is implemented.
You can download this script from the exercise files. I included the utility
header because we will use the standard move function later.
So, my version of the shared pointer is also a
templated class with two pointer members.
This second pointer should point to a control block, but to keep this
simple, we will only implement a reference count, so my reference count
will be an integer allocated on the heap.
Default constructor will initialize the raw pointer to
null and the reference count to zero.
It is important that this reference count is allocated on the heap,
because it will be shared by multiple smart pointers. Parameterized
constructor takes in an address of the allocated resource, and it also
sets the reference count to one, because this smart pointer is now
pointing to this resource.
I also defined a special get_ref_count function which will simply return the
reference count integer. Copy constructor will make a shallow copy of the
internal pointers from the other shared pointer.
So now that both of these pointers point to the same
resource and the same reference count, that reference
count needs to be increased by one.
I am also checking if this other pointer is pointing to something.
If it isn't,
then it makes no sense to increase the reference count. Copy
assignment operator is a little bit more verbose.
Since we are going to make this pointer point to another resource,
we first need to remove its shared ownership from the current resource.
We can do that by decreasing the reference count and then checking if it's 0.
If it is,
that means that this was the last share pointer which
was pointing to this old resource.
And if that's the case, then we need to deallocate that resource and the
reference count from the heap. Then we can finally make a shallow copy again
and increase the reference count of the new resource.
Shared pointers also implement move semantics. Move constructor will
make a shallow copy of the other shared pointer, but it will also
invalidate that same pointer by setting both of its internal pointers
to null. Move assignment operator has the same definition as the copy
assignment operator, but instead of increasing the reference count of
the new resource, we are invalidating the other shared pointer.
This is because we are stealing both the resource and the
reference count from that other pointer, so it makes no sense to
increase the reference count because this other pointer is not
pointing to anything anymore.
And finally, the destructor implements the same logic we saw in the both
assignment operators. When this shared pointer is deleted,
the destructor will decrease the reference count and check if this
pointer was the last one pointing to that owned resource.
If it is, the resource and the control block will
both be deallocated from the heap.
So, that's how my custom shared pointer works, and I will make a
short demonstration by using this simple complex class again. Inside
of the main function, I declared a new shared pointer pointing to the
complex object from the heap.
Then I made a copy of this pointer two times by using the copy
constructor for creating these two new pointers.
I will output the reference count of this first pointer after each statement.
Finally,
I'm also allocating another complex object and assigning it to this c4 pointer.
Then, I will transfer the ownership of this new object
to the already existing c3 pointer.
Now let's compile this. As you can see, everything works, because only
two complex objects were actually constructed and then they were
properly destructed later. The reference count is first one because
only the pointer c is pointing to the first complex object. Then it
increases by one after we share the resource with each one of these two
new shared pointers.
Finally, the ownership of the second complex number is
transferred to the c3 shared pointer,
which means that this pointer loses the ownership
of the previous complex number,
therefore, decreasing the reference count by one. I will now include the
memory header to show you that this implementation is similar to the one of
the shared pointer from the standard library. Replace all of the smart pointer
declarations with the standard shared pointer, and replace new operator calls
with the make_shared function.
Shared pointers don't have a get_ref_count function,
but you can get the reference count by utilizing the similar use_count function.
So this is how shared pointers work.
Be careful not to misuse them by assigning two shared pointers to the same
resource without using copy semantics, because then the control block will not
be updated, we would instead have two control blocks.
I should also mention that since the release of C++20,
you can point to raw arrays on the heap with both unique and shared pointers.
But this is a really niche case, because we have vectors to do that in most
situations, so, that's why I didn't include it in the lesson.