Most programmers will frown upon the usage of raw pointers in modern C++,
and they actually have a good reason to do so. I'm not saying that raw
pointers don't have their own place, after all,
we cannot escape from legacy on which the C++ was built on,
but you should only use them if you have a specific reason, or if
you are working with legacy code. Since we are now familiar with raw
pointers and dynamic memory allocation,
learning about smart pointers will be effortless.
So, what are these smart pointers anyway, and what makes them so smart?
The answer can be summed up in one word: ownership. To explain this,
I created a simple smart pointer class. Just like the standard
array is a wrapper around a C style array, smart pointer is a
wrapper around the raw pointer.
This is also a templated class because the pointer should
be able to point to any type of data.
Smart pointer is an RAII class,
its responsibility is to manage resources at which this raw pointer is
pointing to. A smart pointer has two main states,
it can either be empty, which means that its raw pointer is pointing to null.
This is the result of using the default constructor.
Other state is when the pointer is pointing to some
resource. The address of this resource is received here,
through the constructor's parameter.
Raw pointers also have these two states,
they can either point to something or not.
However, unlike the raw pointer,
a smart pointer assumes the ownership of the pointed to
resources. Once the smart pointer goes out of scope,
it will deallocate the resources from the heap.
If this still seems kind of abstract,
let's go down to the main function and try it out.
Notice that I'm again using the Complex class from one of the previous lessons.
I also added a special message to the constructor and destructor, so that we
know when the complex object is constructed or destroyed.
Until now, we used raw pointers to get access to
allocated resources on the heap.
We allocate a complex object with the new operator and assign
the returned memory address to the c pointer.
Let's now replace this raw pointer with a smart one. We need to pass
the objects data type as a template argument. Also,
we don't need the asterisk character anymore, because this c variable is now a
smart pointer object. Implicit conversion will send the returned memory address
from the heap to the smart pointer's constructor.
Now the raw pointer inside of a smart pointer is
pointing to this complex subject.
Once the main scope ends,
the smart pointer will be removed, because it belongs to the stack, but the
destructor of the smart pointer will call delete on the internal raw pointer,
and since that pointer points to the complex object,
that object will be removed too.
This is similar to the subject class we were working on,
because when the subject goes out of scope, the DNA code
from the heap gets deallocated too.
If I compile this,
you can see that the complex object is constructed and destructed properly.
And this is actually unusual, because complex object is allocated on the heap.
If this was a raw pointer, we would have a memory leak.
But since the smart pointer assumes the ownership over the allocated resource,
we can use dynamic objects from the heap,
just like any other stack allocated object, they will
be managed by the surrounding scope.
Higher level languages like Python or JavaScript don't usually allow
you to explicitly use different parts of memory.
Smart pointers abstract away the dangerous parts of
memory management, like memory leaks,
but they also provide us with the same low‑level access to memory as C.
For now, this custom smart pointer class is just an RAII class which
deallocates the owned resources in the destructor.
But as we know,
pointer variables are kind of special, so we need to implement additional
pointer semantics to make this class act like a real pointer.
And when I say pointer semantics,
I mean things like dereference operator and the arrow operator.
Dereference operator will simply the dereference the internal
pointer to get to the pointed object, and it will return a
reference to that place in memory.
So, now that we defined it,
we can do something like this. I can dereference a smart pointer and
access the real member from the complex object.
If I compile this,
you can see that it works. Since we want to access the objects member,
I would rather use the arrow operator to do that.
But of course, to do that, we need to overload the arrow operator first.
The arrow operator should return the raw pointer
itself, that is all we need to do.
Let's compile this.
And as you can see, both operators are working,
this smart pointer can now be used just like a regular raw pointer.
But if you look closely into the implementation of this arrow operator,
you might have some questions about how it works.
The arrow operator simply returns the internal raw pointer.
So instead of using this arrow operator directly,
let's rewrite this statement to use it as a member function.
We know that this expression will return a raw pointer.
So, if this returns a pointer,
how do we get the access to the actual member of the complex
object? Shouldn't we dereference this pointer first and then use
the dot operator? We could do that, but using the arrow operator
like this will still work.
This is because the legacy implementation of the
arrow operator is kind of magical.
It will cascade down until it reaches a raw pointer, and then
it will do its usual thing, it will dereference a pointer and
access the required member.
So, once we call it here,
the arrow operator will first check if the smart pointer has an overload.
Since it has, it will call that overloaded function
which will return a raw pointer.
Once it has a pointer,
it will again apply its usual behavior to the pointer itself.
So, this statement is actually equal to this one,
and that's why it works. Now that we know the general idea
behind smart pointers, let's check out the available
implementations from the standard library.