top of page
  • Writer's picturePradeep P

Smart Pointers in C++

Smart pointers eliminate the need to use “new” and “delete” while creating dynamically allocated objects. These ensure that the objects created as such, as freed when the pointer handle goes out of scope, thus preventing memory leaks.

There are three smart pointers supported in C++ : unique_ptr, shared_ptr and weak_ptr

The header file for these classes are <memory>

unique_ptr:

The unique_ptr can be used when we want a single handle to an allocated memory block. Thus, as the name suggests, we cannot copy this pointer into another pointer (this can be achieved by preventing the access for the copy constructor and the copy assignment operator or by using the “delete” keyword to disable these functions”)

When this single handle goes out of scope, the memory allocated is freed up.

Have a look at the following example of how to use the unique_ptr:



Here, we have created a class called VecInt, which just is inherited from a vector of integers.

In the main, we open a code block by using the { (at line 23). We create the unique_ptr by using the “make_unique” option. This creates a pointer of type VecInt and assigns the same to the handle vecPtr.

The next line is commented since uncommenting this will cause a compilation error. We cannot copy the vecPtr since it’s a unique_ptr.

Next, we see how the vecPtr can be accessed like a normal pointer (using the -> operator).

Once we cross the } at line 32, the vecPtr goes out of scope and the memory is freed.



Using “new” vs “make_unique”

Though we can use a “new” to create a unique_ptr, a better approach would be to use “std::make_unique”. This prevents the creation of a dangling pointer, if there was an exception encountered in the constructor of the object being created. Consider a situation where we are creating multiple unique_ptr handles in the same statement (as in a function call):

void Unique_Ptr_Receiver(unique_ptr<vector<int>> ptr1, unique_ptr<vector<int>> ptr2)

{

}

int main()

{

Unique_Ptr_Receiver(make_unique<vector<int>>(), make_unique<vector<int>>());

// or

Unique_Ptr_Receiver(unique_ptr<vector<int>>{new vector<int>()}, unique_ptr<vector<int>>{new vector<int>()});

…}

If we use the second option for creating the unique_ptr using the “new”, if there is an exception in one of the parameter evaluations, the memory allocated by the other might leak (if it is called first). Since the order of evaluation of function parameters is not defined in the C++ standards, we don’t know which will get evaluated first. Now consider the first approach. Using the make_unique, even if one of the parameter evaluations threw an exception, after the other one had created the pointer, this doesn’t leak the memory, since we are creating a smart pointer, which gets freed once the handle goes out of scope.

Hence, always use make_unique, unless you want to provide a custom deleter or if we want a raw pointer (as created by “new”)

The unique_ptr uses the default deleter (i.e., it invokes delete on the created object). If a custom deleter is required, the same can be supplied by the user.

Though the unique_ptr can’t be assigned to another unique_ptr, we can transfer the ownership of the allocated object, via the move operations. Thus, this is valid:



Once done, the vecPtr contents are undefined. Hence, we shouldn’t use it without assigning anything to it again.

Releasing the ownership of the object

To release the ownership of the object held by the unique_ptr, we can use the release call. This returns a pointer to the owned object and clears the unique_ptr. Thus, the unique_ptr no longer points to this memory. A get() on the unique_ptr now returns a nullptr.



The object returned by the “release()” can be collected and it is the responsibility of the caller to later release this memory, since the unique_ptr no longer will be able to do so.

Replacing the object in the unique_ptr

We can use the reset function to delete the current contents of the unique_ptr and replace it with the newly passed object:



The output here is:



The first instance of the “VecInt destroyed” is called from line 43, since we are freeing up the originally held object.

The second instance of this line is from line 50, where we free the new object created at line 43.

Using the unique_ptr to allocate an array

We can make use of the unique_ptr to create an array of elements as well:



Line 46 indicates how to use the make_unique to create an array. The usage of the created pointers is also indicated above.

The output of the above program is:



Do look up the sources mentioned near the bottom of the page, to get more information on this.

shared_ptr

The unique_ptr was uncopiable. Thus, only one handle was available to the allocated memory. What if we have a scenario where multiple pointers want to modify the same memory but the pointer is still smart enough to know that the memory block is no longer required? Enter the shared_ptr.

Here, the first instance of the shared_ptr shall be using the make_shared to allocate the required memory. Once done, the shared_ptr handle can be assigned as many times as we need. Each time a new assignment / copy is done, the internal reference count of the shared_ptr goes up by 1. Each time of these handles goes out of scope, the reference count goes down by 1. When the reference count finally becomes 0, it means that there are no active handles to this memory. It is at this point that the memory is finally freed.

The arguments for using the make_shared instead of a “new” is same as those mentioned with the make_unique, above.

The shared_ptr has an additional block of data, which it uses for its housekeeping data, such as the reference counts. Using the make_shared instead of creating a shared_ptr with “new” means that a single block of memory is allocated, that comprises of both the housekeeping data and the actual data. Using a “new” here instead, means that a separate block of memory is allocated for the housekeeping data and a separate one for the actual data, which is less efficient in comparison.

Let’s have a look at the code that provides an example of the shared_ptr:



Please correspond the below explanation with the output shown below.

Line 22 creates the first shared_ptr handle. When we check the reference count at line 23, we get the value as 1.

Next, we add some data into the VecInt. At line 29, we are creating a copy of the original shared_ptr. Once done, we check the reference count of both vecInt and the vecInt2. Both result in a value of 2 (since both essentially point to the same memory block)

We use the vecInt2 to add a few more elements into the shared_ptr.

We exit a code block at line 37. Thus, the vecInt2 handle goes out of scope.

We check the values in vecInt. We see all the values, filled in using vecInt and vecInt2 (again, since they point to the same memory block).

By checking the reference count at line 43, we see that it is back to 1 now.

We hit another end of code block at line 44. Post this, there are no active handles to the shared_ptr since vecInt would have gone out of scope. Thus, we see the destructor output printed here.





weak_ptr

The weak_ptr works similar to shared_ptr in the sense that you can use it to create multiple pointers, pointing to the same chunk of memory. The main difference, though, is that creating weak_ptrs don’t increase the reference count of the shared memory. Thus, if the shared_ptr goes out of scope, even though a weak_ptr pointing to it is still valid, the memory is freed up.

Thus, before accessing the weak_ptr, we should always check if the value it points to is still valid or not.

A shared_ptr can be assigned to a weak_ptr but the other way around is not possible.

To be able to access the contents pointed to by the weak_ptr, we need to convert it to a shared_ptr, by invoking the weak_ptr::lock() function.

Have a look at the following code, demonstrating the use of the weak_ptr:



Please correspond the above code with the output diagram given below.

A weak_ptr is created at line 21. In the next line, we check the reference count on this. This returns 0, since it’s not pointing to any valid memory block.

At line 25, we are assigning the shared_ptr created at line 24, into the weak_ptr. Checking the reference count on both these yield 1 (since they are both pointing to the same block of memory but since there is only one shared_ptr).

We use the shared_ptr to push some values into the vector here. We could have also used the weak_ptr for this, but after converting it into a shared_ptr (as done at line 33). Once the code block ends at line 38, both the shared_ptr handles, shPtr and sPtrFrmWk, go out of scope. Thus, the weak_ptr is now left empty (as indicated by the check at line 40). The weak_ptr reference count is also zero now.



Please have a look at the reference links near the end of the page for more information about the weak_ptr and its members.

Sources:


33 views

Recent Posts

See All

Comentários


bottom of page