Hello again!
In this video, we are going to look at weak_ptrs. weak_ptrs are not actually smart pointers.
They are something that you can use, when you are working with shared_ptrs. So they do belong in this
section.
The main point of having weak_ptrs is to provide a safe way of aliasing
shared_ptrs. There is no provision for aliasing unique_ptrs, because, as the name
suggests, they are meant to be unique!
There is only one object which can access the memory. With a shared_ptr,
there are several objects which can access the memory.
And then we have the question of aliasing.
There were some problems with the traditional way of aliasing, using raw pointers, so let's remind ourselves
what those are.
Here, we have allocated some memory, and we have a pointer to it.
Then we have another pointer, which is an alias to this allocated memory, and then we release the allocated
memory.
So we still have this pointer here, and it is still in scope, and it is possible we could use it by accident.
How do we know if this pointer is still valid?
Well, in this code we can look, and it is fairly obvious that it is not valid.
But if we have more complicated code, it is very hard to keep track of these things.
So if we do run that, then we get a garbage result.
We were lucky!
We could have got a program crash.
A weak_ptr is something that is bound to a shared_ptr object.
So it is a bit like binding a reference to a normal variable, if you like.
We start off by having a shared_ptr.
So we call
make_shared(). We have a reference of one, because there is just one object which is accessing the shared memory,
which is this one.
Then we bind the weak_ptr to this shared_ptr.
The weak_ptr cannot directly access the shared_ptr's memory.
So we still only have one object which can access the memory.
So the reference count is still one.
And then, when we release the memory, the reference counts goes down to zero.
The only way that the weak_ptr can access the shared_ptr's memory is by being converted
back to a shared_ptr.
So, it is a bit like doing a cast, if you like.
We have to convert it to the shared_ptr. And that will only be possible if the shared_ptr is
still valid.
So after this code, we can use the weak_ptr and convert it back, get the original shared_ptr.
But once we release the memory from the shared_ptr, then we cannot do the conversion, so we cannot
use the weak_ptr
alias, after the shared_ptr has been released.
To convert the shared_ptr, to recover the original object,
we call the lock() member function of the weak_ptr. And this will return the original shared_ptr,
if it is still valid. If it is no longer valid, then this will return null.
So we could have a conditional.
And if this is true, then we have a valid shared_ptr object, and we can safely use it.
If it is false, then we got null back, and there is no shared_ptr object.
There is another way of recovering the original shared_ptr, which is to initialize it. Again,
this will only succeed if the weak_ptr is bound to a valid shared object.
If it is not bound to a shared object, then this will throw an exception.
bad_weak_ptr.
The lock() member function is implemented as an atomic operation, again like the reference counter in the
shared_ptr.
This means it is thread-safe, but not very fast.
So let's try our code again,
using shared_ptrs and weak_ptrs. We create our shared_ptr.
The reference count is one. We bind a weaker pointer to this shared_ptr.
This does not affect the reference count, so the reference count is still one.
This weak_ptr is now bound to a valid shared_ptr object, so we could use the weak_ptr here,
to access the memory.
Then we release the shared_ptr's memory.
The reference count goes to zero, and the weak_ptr is no longer bound to a valid shared_ptr object.
We call the lock() member function to get back the original shared_ptr.
And if this succeeds, then we have a valid shared_ptr, and we can safely use it.
If this fails, it returns null, and then we cannot use the shared_ptr.
So what you think will happen?
And there we are.
The shared_ptr is not valid.
So, because we released the shared_ptr's memory, we do not recover a valid shared_ptr.
And we can also try the other version with the exception.
So we have a try/catch block.
We try to initialize the shared_ptr from the weak_ptr, in the try block.
If this succeeds, then we can go on and safely use that shared_ptr.
If the weak_ptr is not bound to a valid shared_ptr, then the exception is thrown and we go into
the catch block.
So there is no way there that we could use the invalid shared_ptr by mistake.
And there we are.
So that throws the bad_weak_ptr, an exception.
Let's look
now at a slightly more interesting example.
We have a vector of shared_ptrs, then we print them out.
Then we delete one of the members.
We set the pointer to null. That will release the memory for that particular shared_ptr. And then we print out the
vectors again.
In the print() function, we just loop over the shared_ptr elements of this vector, and we
dereference them and print them out.
So what do you think will happen here?
Bearing in mind that we have deleted one of these elements.
So as you can see, when we got to the deleted element, we had a very long pause.
And that was probably the program trying to recover from some kind of memory access error.
So we need to check that the shared_ptr is valid before we dereference it.
One way to do this is to use a weak_ptr as the element type in the loop.
Then we call lock() to recover the original shared_ptr.
If this succeeds, then we get a valid shared_ptr and we can safely dereference it.
If this fails, then we get null, and we go into the else branch. And the rest of the code is the same.
So what do you think will happen here?
Yep.
So when we get to the deleted elements, the weak_ptr lock() returns.
null.
And we do not do an
unsafe
dereference.
So, practical applications of weak pointers. In a cache.
For example, in your web browser, when you go to a website for the first time, the web browser has
to fetch the HTML, the JavaScript, the images, and so on, over the Internet. And it will store them in
its memory, but it also stores them in a cache.
Then the next time you want to go to that page, the web browser will go to the cache and it can get
all that data from the cache, instead of having to fetch to over the Internet again.
So that will speed up your web browsing experience!
One way to implement this is to store the data in shared_ptrs.
So that is the data that has been retrieved over the Internet.
And then in the cache there's a weak_ptr to the data.
So when you want to go to a website, the browser will first look in the cache.
It will call lock() on the object, which has the data for that website.
And if there is data for that website, then this will return the shared_ptr.
Sometimes this may not work. in that case,
this will return null, and then the browser will need to fetch the original data over the Internet again.
Okay, so that is it for this video.
I will see you next time.
But until then, keep coding!