Smart pointers are types of template classes in C++ that provide a secure way to allocate and de-allocate memory. By ”smart’, we mean that these pointers are lifetime-aware and know when to de-allocate themselves, without explicitly calling delete
or delete[]
. In larger codebases, keeping track of all heap-allocated objects is difficult and failing to call delete
at the right instances can lead to memory leaks or undefined behavior.
Smart pointer classes encapsulate the raw pointer holding the address of the object in memory along with some added mechanisms that help them automatically deallocate the memory through the raw pointer. They also help in managing the ownership of the object which dictates how the object will be modified or de-allocated. In this short blog, we’ll explore four smart pointer classes, unique_ptr
, shared_ptr
and weak_ptr
.
RAII (Resource Acquisition Is Initialization)
RAII is a principle which originated in C++ and then used in other languages like Rust and Ada. It suggests that the memory acquired by an entity (an object) must be tied to the lifetime of the object. Meaning, when the object is created, its memory must be initialized and when the lifetime of the object ends, the memory must be returned back (or the object must be de-allocated). RAII is also called ‘Scope Bound Resource Management, which largely justifies the principle.
Instead of allowing the programmer to manually allocate/de-allocate memory for an object, the responsibility is transferred to the object which uses its lifetime as a means to make decisions regarding the memory. In the upcoming sections, we will observe smart-pointers being used to follow the RAII principle in C++.
Before we start, here’s the code for the dummy HTTPConnection
class that will be used to demonstrate object creation/deletion using smart-pointers in the later sections.
Using unique_ptr
An object enclosed in an unique_ptr
gets deleted when the unique_ptr
instance (smart pointer instance) goes out of scope. Here’s a snippet of code that demonstrates one of the use-cases of an unique_ptr
,
Both, connection1
and connection2
, point to data allocated on the heap. To deallocate connection1
, we need to manually call the delete connection1
whereas connection2
, being a smart pointer, detects the end of its scope (the scope of both pointers is the function download
where they reside) and deletes the enclosed object automatically.
Next, consider a function that accepts some parameters and returns the pointer to a heap-allocated object initialized with the given parameters.
The dumb pointer connection
returned from alloc_return_ptr
and copied multiple times, where each copy (similar to connection_copy
) can be used to modify the object in memory and even deallocate it! Hence, copying a dumb pointer results in the creation of several owners of the same object that may lead to unusual consequences in a large codebase.
Connection (www.github.com,8000) created
0x564e75d352f0
0x564e75d352f0
0x564e75d352f0
Connection (www.github.com,8000) destroyed
Segmentation fault
Notice, when deallocating connection_copy
leads to a SEGFAULT
as the underlying object is already deleted by delete connection
. This is an example of the double free memory leak.
A unique_ptr
does not allow copying objects to sustain the one-owner per object rule. The following code does not compile,
A unique_ptr
cannot be copied like an auto_ptr
, but moved to another unique_ptr
thus transferring ownership. For the above code to compile, we can use std::move
,
Difference between moving and copying objects
A good SO answer suggests “Imagine an object containing a member pointer to some data that is elsewhere in memory. For example, a
std::string
pointing at dynamically allocated character data. Or astd::vector
pointing at a dynamically allocated array. Or astd::unique_ptr
pointing at another object. A copy constructor must leave the source object intact, so it must allocate its own copy of the object’s data for itself. Both objects now refer to different copies of the same data in different areas of memory (for purposes of this beginning discussion, lets not think about reference-counted data, like withstd::shared_ptr
). A move constructor, on the other hand, can simply “move” the data by taking ownership of the pointer that refers to the data, leaving the data itself where it resides. The new object now points at the original data, and the source object is modified to no longer point at the data. The data itself is left untouched. That is what makes move semantics more efficient than copy/value sematics.”
std::move
and unique_ptr::release
make the transfer of ownership more explicit and the use of move-semantics over copy/value-semantics improves efficiency. To create a unique_ptr
, the std::make_unique
function can also be used, which creates the unique_ptr
and the underlying object’s instances in one single memory allocation.
Using shared_ptr
Instead of one heap-allocated object having just one owner, a shared_ptr
allows multiple owners. The enclosed object is deallocated once the last shared_ptr
goes out-of-scope or when the last pointer is transferred to another pointer.
For the object ("www.github.com", 8081)
, we have three references connection
, connection1
and connection2
that can access and modify the underlying object. The object is automatically deallocated when these three references go out of scope,
Just like make_unique
, there exists the std::make_shared
function to create a shared_ptr
+ object instance in one memory allocation.
weak_ptr
Quoting from cppreference.com, std::weak_ptr
is a smart pointer that holds a non-owning (“weak”) reference to an object that is managed by std::shared_ptr
. It can be used to access the object only if it exists and has not been deallocated already (see dangling pointers).
By making use of the expired()
method, one can check if the managed object is valid. The lock()
method creates a shared_ptr
to the object if it is valid. If any part of the code is holding a shared_ptr
for an object, the object cannot be deleted until the code relieves its shared_ptr
. With a weak_ptr
, the code can still access the object if required with actually owning it.
Here’s an excellent answer on StackOverflow highlighting a use-case of weak_ptr
,
A good example would be a cache.
For recently accessed objects, you want to keep them in memory, so you hold a strong pointer to them. Periodically, you scan the cache and decide which objects have not been accessed recently. You don’t need to keep those in memory, so you get rid of the strong pointer.
But what if that object is in use and some other code holds a strong pointer to it? If the cache gets rid of its only pointer to the object, it can never find it again. So the cache keeps a weak pointer to objects that it needs to find if they happen to stay in memory.
This is exactly what a weak pointer does — it allows you to locate an object if it’s still around, but doesn’t keep it around if nothing else needs it.
Closing Thoughts
Smart pointers felt fascinating from the day I saw them being used in a code snippet. After learning Rust, I could understand how concepts like ownership and RAII work. With smart-pointers, I feel these concepts can be applied in C++ too. Not a C++ expert, but this blog will serve as my ‘notes’ while I was learning smart-pointers in C++.
Keep learning, and have a good day ahead!