The smart winter class from the previous lesson has one major flaw.
What if we declare a new pointer, c2, and initialize
it with the c pointer. As you know,
this will implicitly call the copy constructor, which
by default creates a shallow copy.
In other words,
both of these pointers will now point to the same resource in memory.
This is the reason why we redefined copy semantics in the
Subject class, because shallow copy will just make a copy
of the memory address itself.
But we want our smart pointers to have an exclusive
ownership of the resource, so copy semantics should actually
be disabled in the smart_ptr class.
We can achieve this by assigning the copy constructor and
the copy assignment operator to delete.
If we don't do that, then we are risking the potential case
when two smart pointers point to the same place in memory and
both of them try to deallocate it.
Unlike the example with the DNA when we made a deep copy of the resource,
in this case, we don't want to do anything with the resource itself.
Smart pointers are here just to point to them and manage their lifetime.
Since we want exclusive ownership, only the last pointer that
has the address should manage that resource.
So in this case I would want the c2 pointer to take the ownership from
the pointer c. This kind of sounds like we need to steal the resource,
and that is exactly what we need to do. Stealing resources and
invalidating the previous owner is the job of the move semantics. Move
constructor will take the address from the other raw pointer and set it
to null. Move assignment operator will do the same, but before that we
also need to deallocate the current resource, if there is one. Otherwise,
we would risk a memory leak.
So if we want to transfer the ownership from one pointer to another,
we need to do it explicitly.
Let's include the utility header, because we will need
to use the move function to do this.
Okay, and now if I want to transfer the ownership from the pointer c, I first
need to use the move function to turn it into an x value.
This will force the c2 pointer to use the move constructor and
steal the resources. Let's compile this now.
No segmentation faults,
no errors, which means that the resource was successfully
transferred and deallocated. What we implemented here is a
simpler version of a smart pointer from the standard library,
also known as the unique pointer.
We need to include the memory header to use it.
The name unique stems from the fact that this kind of
pointer has an exclusive ownership.
Only one unique pointer can point to the same resource in memory,
which in that context makes it unique.
Let's replace our own implementation with this unique_ptr class.
Implicit conversion will not work here because unique pointers have
an explicit constructor, so we can only use this new expression as a
constructor parameter, like this.
Let's compile this now to make sure that it works and that the complex
object is correctly deallocated. It seems like it is.
However,
it is not a good practice to allocate resources with the new
operator when using unique pointers. Instead,
you should initialize this pointer by using the special make_unique function.
Just like new operator, make_unique will allocate an object on the heap.
The type of the object should be passed in as a template argument,
and we can pass the constructor parameters for that object through
the make_unique function arguments.
Unlike the new operator,
this function will actually return a unique_ptr which points
to the allocated resource on the heap.
Since that's the case,
we can simply use automatic deduction here to declare a new unique pointer.
Using make_unique function is optional.
It was initially introduced to resolve a specific exception safety issue,
but that issue was fixed since C++ 17, so as of now,
the only reason to use this function instead of the new
operator is to be clear that this newly allocated resource
will be owned by some unique pointer.
Some people also prefer it because they can use the auto keyword here,
but I think that the most important reason to use make_unique
is to stay consistent. In the next lesson, we will talk about
another type of a smart pointer which is instantiated by a
similar function to this one.
So we can use this make_unique function to be explicit about the
ownership type of the allocated resources.
Now that we know how unique pointers work,
let's use them to improve our two custom classes DNA and Subject.
We know that Subject should have an exclusive ownership over the DNA
objects, so this is a perfect use case for unique pointers.
Don't forget to include the memory header.
So every subject has a sample pointer which points
to the DNA object on the heap.
Let's turn this raw sample pointer into a unique_ptr. Since we prefer
using the make_unique function over the new operator, let's modify the
member initializer list of the three constructors. Default constructor
makes a DNA with zeros, and Parameterized constructor makes one with a
string literal.
The Copy constructor should also use make‑unique to create a new
object by using the DNA's copy constructor.
So for now everything is still the same.
We just replaced new with make_unique. But here is where the power of
unique pointers kicks in. Move constructor will steal the resources
from the other subjects, so all we have to do is to apply std::move
on this other sample pointer.
This will transfer ownership of the DNA resource from one unique
pointer to another, so this simple pointer from the other
temporary subject will be invalidated.
That means that we don't have to set this other sample pointer
to null, unique_ptr will take care of this.
Same goes for the Move assignment operator.
We just moved the DNA from one pointer to another, so we
don't have to set this one to null either.
But we also don't have to delete the old DNA object, because once the
unique_ptr gets the ownership of this other resource,
the old resource will automatically get deallocated to prevent memory leaks.
The same is true for the Subject's destructor.
We can remove it altogether, because the memory management of the DNA
resource is now the responsibility of the sample unique_ptr.
When Subject goes out of scope,
the sample member will also be removed, and if the sample is removed,
then the resource it points to is also deallocated.
So you can see that by using unique pointers, memory
management is a lot easier and more intuitive.