Next, let's look at the shared pointer.
The shared pointer is a smart pointer that provides a shared ownership model
of the heap object.
So like a unique pointer, a shared pointer points to an object on the heap.
However, unlike a unique pointer,
that heap object may be shared among many shared pointers.
So we can have one heap object referenced by multiple shared pointers that manage it.
Unlike unique pointers, shared pointers can be copied and assigned.
This is exactly how we make them access the same heap object.
For efficiency, shared pointers also support move semantics.
While unique pointers can be used to allocate arrays of objects on the heap,
shared pointers do not support this by default.
So you might be thinking we can have multiple shared pointers
all referencing the same heap object,
so how do we know when that object needs to be destroyed and that's a great question.
There are different ways that this can be accomplished.
The most common technique is to use reference counting. The idea is simple.
Every time we instantiate a shared pointer object
and have it point or reference to the heap object, we increment the counter.
This counter simply has the number of shared pointers referencing that heap object.
When the reference count becomes 0,
then we know nothing is referring to the heap object, and it can be safely destroyed.
It's a pretty simple but effective model,
but it does introduce some overhead to the process.
However, the overhead introduced is in most cases not a big deal
compared to the benefits provided by having the system manage these shared objects for us.
Let's see how we can declare and use shared pointers.
First, we'll see the same technique we used when we created unique pointers.
And then we'll see a more efficient technique using the make shared function.
Here you can see that we're declaring p1 to be a shared pointer to an integer.
We're also initializing it to point to a new integer that we create on the heap
and that integer has been initialized to a 100.
Now p1 owns the object on the heap.
When p1 goes out of scope, the int on the heap will be de-allocated.
That is unless other shared pointers are referencing it.
You don't need to call delete with a shared pointer.
Now as far as using the shared pointer,
the syntax is pretty much like a raw pointer or unique pointer.
You can see that if we de-reference p1, we get the integer that it points to,
in this case, 100, and we display it.
We can also modify the integer we're pointing to in a similar manner.
In this case, since we only have one shared pointer referencing the integer on the heap,
when p1 goes out of scope, the integer will be de-allocated from the heap.
Before we see how we can create other shared pointers
and have them reference the same heap object,
let's look at a very useful method in the shared pointer class called use count.
The use count method
returns the number of shared pointer objects that are currently referring to the heap object.
In this example, we'll have two shared pointers, p1 and p2
that both reference the same heap object.
So first we declare p1 as a shared pointer that will manage an integer on the heap
and we initialize that integer to 100.
Now we call the use count method on p1, and we see that it displays one.
That's what we expected since p1 is the only shared
pointer referencing the integer on the heap.
Now we create another shared pointer p2.
And we use the copy constructor to initialize it so that it refers to the same
object as p1.
Now we call the use count on p1 and we display 2
since both p1 and p2 are now referencing that same integer.
But notice that if we call use count on p2, we'll also see a 2,
which makes perfect sense. Now suppose that I call reset on p1.
Recall that with a unique pointer, this destroyed the heap object
and set the unique pointer to null.
However, with shared pointers, this decrements the use count by 1
and sets p1 to null.
Now the use count for p1 is 0, and the use count for p2 is 1.
So just because we reset p1 doesn't mean that we de-allocate the integer on the heap.
We can't in this case because p2 is still referring to it.
Finally, when p2 goes out of scope, the integer on the heap is de-allocated
since it's no longer being referenced.
So you can see that reference counting is pretty simple in concept.
Of course, shared pointers also allow us to point to user-defined types.
In this example, we're creating a shared pointer that will point to an account object,
and we initialize it to point to the Larry account.
We declare account as the template argument in the shared pointer declaration.
What this says is that the shared pointer p1
will manage an account object on the heap,
and we're creating that account object in the initializer list for p1.
Now we can use this pointer as we would a raw pointer.
We can dereference it to get the account object
as well as access the account object member methods
via the member selection or arrow operator.
And again, when p1 goes out of scope,
the heap storage associated with the account object is de-allocated.
As you can see, this code looks exactly the same as the code that we used with the unique pointer.
Unlike unique pointers, shared pointers can be copied, assigned and moved.
In this example, we create a vector that will contain shared pointers to integers.
And we also create ptr, which is a shared pointer to an integer,
and it points to an integer on the heap, which been initialized to a 100.
We can now push back that shared pointer to the vector.
Remember, we couldn't do this with unique pointer since they don't allow copying,
but we can do it with shared pointers.
Also notice that if we display the use count for pointer, we'll see a 2.
This is because that integer on the heap is being referenced from
pointer as well as from the shared pointer in the vector.
Now when the vector and pointer both go out of scope,
we're sure that the last one referring to that integer on the heap will clean it up.
There's no need to call delete or to figure out when and from where to call delete.
This is a huge deal. It makes writing this kind of code so much easier and less error prone.
Now let's see a better way to initialize shared pointers.
The make unique function was introduced in c++14,
but the make shared function has been around since c++11.
The make shared function returns a shared pointer of the specified type
and it allows us to pass initialization values
into the constructor for the managed object.
This is the preferred way to create shared pointers.
Not only do we not have to use the new keyword,
but the compiler will generate much more efficient code since remember,
the compiler has to generate a data structure
that holds information about the reference count, the raw pointer,
the actual heap object and other data as well.
So this creates all of this in one shot when the pointer is initialized.
In this example, we create three shared pointers: p1, p2 and p3.
P1 is created using make shared.
And we reference an integer on the heap, and we initialize it to 100.
Now we can create p2 as a shared pointer to an int
and use its copy constructor to initialize it to p1.
P1 and p2 refer to the same integer on the heap,
and its use count will be 2.
The third pointer p3 is initialized as empty.
So it doesn't point to any shared object yet,
but it is initialized, it's null.
So there's no chance that we can have an uninitialized or a wild pointer when we use smart pointers.
Then we assign p1 to p3.
Now p1 also refers to the shared integer,
and our use count increases to 3.
All three pointers now go out of scope,
which shared pointer is responsible for cleaning up the heap storage?
Well it's the last one that references it.
In this case, it will be p1 since p2 and p3
will be destroyed before p1, but the point is that you don't have to worry about it.
So now let's head over to the IDE, and we'll see some examples of using shared pointers
in live code.
Okay. So I'm back in the IDE.
I'm in the section 17 workspace in the SharedPointers project.
This project has three sections.
The first section is all about using a shared pointer to an integer
and sharing pointers, so that we can see these reference counts moving up and down.
The second section we're going to create test objects now,
and then the third section we'll use vectors.
So let's start with this section first.
And I want to walk through it with the debugger, so you can actually see what's going on.
So I'm going to debug this program.
Okay. So we're going to start right here on line 31.
You can see what we're doing here. We're creating p1 as a shared pointer to an integer,
and we're setting that integer to a 100.
Now if i step through that,
you can see p1 over here I'll refresh,
and you can see that p1
has a reference count of 1.
You can see it right there. I know it's kind of small, but I couldn't make this font any bigger on this window here.
So there's the count of 1. So this tells you that p1
is pointing to that shared integer, right. And that's the only thing
pointing to that shared integer right
now. So on line 32, if I display the use count point for that shared pointer,
I am going to get 1, so I'll execute that.
And you can see up here on the display the use count is 1,
which is actually the same thing we're seeing right in here.
Okay. So now in line 34, what we're doing is we're creating p2
in terms of p1. So that's my copy constructor.
Now we're sharing ownership. Remember, this is one of the things that we could not
do with unique pointers.
So in this case, let me execute that line.
And you can see now that p1 and p2
are both referring to that same storage. So when I refresh here,
you'll see that they both have a reference count of 2 right there.
Okay. So again, p1 and p2 are managing that same area in memory.
They have co-ownership.
So at this point, when I display the use count, we'll see a 2.
Now in line 37, I'm resetting p1.
What is that going to do? Well, it's going to set p1 to point nowhere.
So we're going to have that set to null pointer,
and p2's reference count will go down by 1 because now p2
is the only shared pointer that owns that memory.
So let's execute that line.
And let's refresh over here, and you can see now that p1 is a null pointer basically,
and p2
its reference count went down to 1.
So if we display these last two statements, we'll see that p1's use count to 0,
and p2's use count is 1. And the program is complete now.
Okay. So now let's do part 2.
Okay. So now let's execute the second section of this project.
You can see the second section starts here on line 41.
I've commented out the section one that we just ran.
So let me walk through this with the debugger,
so we can see what's going on, again, with these reference counts.
Okay. So we're here on line 42, and we'll step through that.
All that's doing is creating ptr as a variable. It's a shared pointer to a test object,
and we're using make shared this time to create that test object.
So I'll step through that. And you can see that right now when I refresh my local variables,
ptr has a reference count of 1.
So we've got one reference to that shared test object on the heap.
Now we're going to call this function. And we're going to pass ptr into it.
This function expects a shared pointer to a test,
and it expects it by value.
So what we expect is for it to make a copy of that shared pointer.
Remember, we're making a copy of the shared pointer object,
not of the test object on the heap. It's important to understand the difference there.
So let's walk through this.
Now we're in the function. You can see we're right up here on line 23 at the top of the screen.
And right now, we've made a copy. So if you look at p,
p is a shared pointer to that same test object.So we expect p's use count
to be 2 because we just created a copy.
So let's take a look at that. And you can see the use count is 2.
So now when p goes out of scope in this function,
what happens is that the reference count again is reduced. So let's take a look at that.
Now we're back here,
right back to where we were. We're on line 44.
And if I refresh, you can see that ptr now is back down to 1, right.
So we temporarily increased that reference count while we were in that
function because we just made a copy
of something else that's using that same resource.
So now we'll print out the use count for ptr,
which we expect to be 1, and that's exactly what displays.
Now notice what happens. I've created a block here.
And then there's another block here. This just establishes scope
so that we can see these inner shared pointers going in and out of scope.
So in this case, we've got ptr1,
and we're creating ptr1 based on ptr. So now we've got
ptr and ptr1, both pointing to that shared resource.
So let me execute that line.
And what we expect now is the use count to be 2.
And I'll refresh here.
And you can see that the count right there is 2 for both of them,
which is exactly what we expect, it will display 2 now
right over here on the output.
What happens now is we create another block. So we're creating ptr2,
also referring to that same test object.
So now we've got these three pointers, right. These three shared pointer objects,
all referring to the same managed object on the heap.
So if i execute this line here on line 49,
what we have is we've got ptr, ptr1 and ptr2,
all with a reference count of 3 which is exactly as we expect.
And we can display that reference count, you can see the 3 right here.
Now what we'll do is we'll reset pointer, right.
So this pointer right here will be nulled out, which means that,
that will become null, and then the reference count for the other two pointers
ptr1 and ptr2 will go down to 2
since now there's one less shared pointer.
So let's execute that line and refresh.
And you can see that now ptr is null,
ptr1 and ptr2 have a reference count of 2, just as we expect.
All right. So now ptr is going to go out of scope right now,
and it just did. So now we're going to print out the use count
for ptr in this case, which will be 0,
right, because we reset it.
Okay. So now let's walk through this one more time. And we're right here right now.
And let's step through this one more time. We're on this last statement. And what's the use count for ptr?
Well, let's refresh here. You can see that ptr's use count is now 0, right.
Now it's not referring to any managed object.
So that's it. That's part 2.
We'll do next is part 3 where we'll create
a couple of shared pointers, watch we'll create 3 shared pointers to account objects,
and then we'll put them on a vector and print out the vector.
Okay. So let's wrap up this video by going over the third section
of the shared pointers project.
In this case, you can see we're starting on line 58.
And what we're doing is we're creating three account pointers,
and these are shared pointers to account objects.
We're using the same account framework that we've been using all along.
This is the one that's using dynamic polymorphism.
So we're coming at this with a base class pointer right here.
And we're using std make shared here. We're creating a trust account Larry,
we're creating a checking account Moe, and we're creating a savings account Curly.
So there you go. At this point, we've created three objects.
Then what we'll do is we'll take those three shared pointers,
and we'll put them in a vector. So notice this vector declaration here.
A vector is a vector of shared pointers to account objects, and we'll call it accounts.
And we'll simply push back acc 1 2 and 3.
Remember, we're not pushing account objects, we're pushing
shared pointer objects that point to account objects.
And then we'll just loop through that vector and display them.
Okay. So let's step through the debugger with this example and see exactly what's happening.
Okay so here we are. We're on line 59.
What we want to do is we want to create those 3 shared pointers.
So let's do that. We'll step through the first one, the second one and the third one.
I'll refresh my screen here. And you can see that account one
is a shared pointer with a reference count of 1,
account two is the same and account three is the same.So each one of them is pointing
to its own managed object, right. In one case, it's a trust account,
a checking account and a savings account.
Now what we're going to do is we're going to instantiate that
vector of shared pointers to accounts.
And we'll do that right now. I'll step through that one line right now.
And there it is. There's nothing in there right now.
Now what we want to do is we want to add those three shared pointers to the vector.
Remember, acc 1 2 and 3 are shared pointers, they're not accounts,
they're not raw pointers.
They're shared pointers to accounts.
So our vector holds shared pointers to accounts, that's how we declared it.
So what's interesting here is remember,
these guys right now all acc 1 2 and 3, all have a reference count of 1.
But when we add them to the vector, the vector's gonna make a copy.
So now we have shared ownership. Acc1
shares the ownership with that trust account
with accounts, which has a vector that has a copy of that. Okay.
So let's walk through this. I'll step three times through this code here.
And we've just added all three accounts.
I'll refresh. And you can see the actual accounts right here.
You can see that it's got three entries, and notice the reference count for them 2.
If i look at acc 1 2 and 3,
you can see they also have a reference count of 2 because we made copies.
So now we've got shared ownership.
Again, acc 1 2 and 3
have shared ownership to that trust account, checking account, savings account,
and so does the vector.
Now let's loop through this and display them. So what we're going to do is we're going to use a range-based
for loop here, and we're going to iterate over the accounts object, which is that vector.
And we're just using const auto here. But remember, the vector is a vector of shared pointers.
So this acc variable is going to be a shared pointer.
And if we want to display the account, we just dereference the pointer to get to the account.
So we expect this to print out the trust account, the checking account and the savings account
and the use count as we go. Now the use count should be 2.
We're using a reference to a base class, so the dynamic polymorphism will be just fine.
So let's walk through this. And you'll see that we've got --
right here you can see up here trust account Larry.
And let's display the reference count for that, which we expect to be 2,
right because acc1 is managing it as well as the vector.
Then we'll execute it again. We'll see the second object is the checking account,
use count is also 2.
And then the last one is the savings account for Curly, and the use count is 2.
Now what happens when the vector goes out of scope,
what happens when these pointers go out of scope.
Well, the vector is going to go out of scope first, so it's going to be destroyed.
But the objects that the trust account, checking account and savings account objects on the
heap won't be destroyed yet.
What will happen is that those shared pointers in the vector will be destroyed,
and then the reference counts for those three objects will go down by 1.
Then when acc 3 2 and 1 are destroyed,
that's when the heap storage will be dynamically de-allocated.
And that's it. That's the program. It's done.
You can walk through this and use the debugger.
You can see the big difference here between using unique pointers and shared pointers.
When we had a vector of unique pointers,
that vector exclusively owned those resources.
Here we're sharing those resources. And the use cases are different obviously.
It all depends on what you're trying to accomplish.