Understanding Smart Pointers in C++ | by Joseph Robinson, PhD | Apr, 2022

Photo by John Schnobrich on Unsplash
Table of Contents· Introduction
· The Problem
· The Solution
· To access smart pointers:
· Sample Usage
· Questions
∘ Why was the constructor invoked but not the destructor.
∘ What happens if we pass a std::unique_ptr by value to a function?
· Conclusion

C++ smart pointers are class objects that act like raw pointers but manage the lifetime of objects created on the heap. They can be very useful in software development, as they automate the process of deleting objects and help to prevent memory leaks. This blog post series will discuss the basics of smart pointers and how to use them in your C++ programs. Specifically, we now cover the fundamentals, with the following blogs covering the different types of smart pointers in depth.

Smart pointers are class objects that behave like built-in pointers and manage objects created via new, so there is no worrying about when and whether to delete — the managed smart pointers automatically delete the object at the appropriate time.

A smart pointer gets defined and used syntactically, almost exactly like a built-in (or raw) pointer. We may use smart pointers for replacing plain pointers in a wide range of situations. Essentially, doing the same thing by simply replacing a normal pointer with a smart one.

A smart pointer contains a built-in pointer as part of its template class whose type parameter is the type of the pointed-to object, so we can declare smart pointers that point to a class object of any kind. When it comes to dynamically-allocated objects, we often talk about who owns them. Owning something means it is yours to keep or destroy as you see fit. Ownership, in C++, does not only mean the code that refers to or uses the object but mostly what code is responsible for deleting it.

We implement ownership regarding where dealslocates that object’s memory with smart pointers. If we do not execute ownership correctly, we may get memory leaks or unexpected behavior from attempting to follow pointers to objects that no longer exist.

Smart pointers make it easier to implement ownership correctly by deleting the smart pointer destructor (ie, where objects get deleted). Since the compiler ensures that a class object’s destructor will get called when destroying the object, the smart pointer destruction can automatically handle the pointed-to object’s deletion. The smart pointer owns the object and deletes it for us.

This blog series starts with an overview of smart pointers: problems, types, and usages. Then, we will hone in on the three classes that come as a part of the standard library of C++.

  1. std::unique_ptr (source)
  2. std::shared_ptr (source)
  3. std::weak_ptr (source)

Where the naming scheme reflects the functionality, each serves a specific purpose; The type of smart pointer chosen varies with the use case.

Before continuing to the basics, let us first define the types listed: (1) unique_ptr implements unique ownership — only one smart pointer owns the object at a time; when destroying the owning smart pointer, the owned object gets destroyed automatically. (2) shared_ptr implements shared ownership. Any number of these smart pointers jointly own the object. The owned object gets destroyed upon destroying the last owning smart pointer.

Finally, (3) weak_ptr owns no object and plays no role in when or whether the object gets deleted. Rather, a weak_ptr simple observes objects under the management of shared_ptr and provides facilities for determining whether the observed object still exists or not. The C++11 implementation of weak_ptr uses shared_ptr.

Resources sometimes require allocation from the heap (eg, static variables, locks): resources that must get released at some point. Otherwise, memory leaks: a long-running program with a memory leak will slowly run out of memory, killing performance.

As put in Modern C++ written at Microsoft,

“Smart pointers are defined in the std namespace in the header file. They are crucial to the RAII or Resource Acquisition Is Initialization programming idiom. The main goal of this idiom is to ensure that resource acquisition occurs at the same time that the object is initialized, so that all resources for the object are created and made ready in one line of code.

In practical terms, the main principle of RAII is to give ownership of any heap-allocated resource—for example, dynamically-allocated memory or system object handles—to a stack-allocated object whose destructor contains the code to delete or free the resource and also any associated cleanup code.

In most cases, when you initialize a raw pointer or resource handle to point to an actual resource, pass the pointer to a smart pointer immediately. In modern C++, raw pointers are only used in small code blocks of limited scope, loops, or helper functions where performance is critical and there is no chance of confusion about ownership.” [source]

The smart pointers in C++98: std::auto_ptr. (source)

The smart pointers in C++11: std::unique_ptr, std::shared_ptr, std::weak_ptr.

Tip

std::auto_ptr, deprecated from C++98 and removed in C++17. It was an attempt to standardize what became C++11, which is better. The only legitimate use case std::auto_ptr is a need to compile code with C++98 compilers. Otherwise, replace std::auto_ptr with std::unique_ptr and never look back.

Smart pointers are class objects that behave like raw pointers but manage objects that are new and when or whether to delete them— smart pointers automatically delete the managed object at the appropriate time.

Tip

A smart pointer contains a built-in pointer, defined as a template class whose type parameter is the type of the pointed-to object, so you can declare smart pointers that point to a class object of any type.

Hence, this container for a raw pointer that automatically dealslocates memory, meaning memory leaks in the program is of no concern.

The three types of smart pointers, as of C++11, are listed as follows:

  1. std::unique_ptr
  2. std::shared_ptr
  3. std::weak_ptr

Each is accessible via the standard library header<memory> (source).

#include <memory>

To instantiate a unique pointer and assign it a value of 25 (ie, of type int):

std::unique_ptr<int>myPtr1 = std::make_unique<int>(25);

How do we use this pointer?

std::cout << myPtr1 << std::endl;

0x60000049c030

Pointers store addresses of objects. To access the value of the smart pointer, we must dereference it. Using the asterisk symbol *.

std::cout << myPtr1 << std::endl;
std::cout << *myPtr1 << std::endl;

0x6000003ec030 25

Hence, there is the address and value.

Unique pointers cannot be shared.

std::unique_ptr<int>myPtr1 = std::make_unique<int>(25);
std::unique_ptr<int>myPtr2 = myPtr1;

error: call to implicitly-deleted copy constructor of ‘std::unique_ptr

We could use the move method to move the value pointed to by myPtr1 to that of myPtr2.

std::unique_ptr<int>myPtr1 = std::make_unique<int>(25);
std::unique_ptr<int>myPtr2 = std::move(myPtr1);
std::cout << *myPtr2 << std::endl;

25

Now, what if we deference myPtr1 after move.

std::unique_ptr<int>myPtr1 = std::make_unique<int>(25);
std::unique_ptr<int>myPtr2 = std::move(myPtr1);
std::cout << *myPtr2 << std::endl;
std::cout << *myPtr1 << std::endl;

25 Exception: Clang-Tidy: myPtr1 used after it was moved

The varmyPtr1 is empty.

The memory is deallocated automatically.

Here is a class to demonstrate the concept:

class MyClass{
public:
MyClass(){
std::cout << "Constructor invoked" << std::endl;
}
~MyClass(){
std::cout << "Destructor invoked" << std::endl;
}
};

When creating the object, the constructor gets invoked; the destructor gets invoked when deleting it.

int main() {    std::unique_ptr<MyClass>myPtr1 = std::make_unique<MyClass>();
std::system("read -p 'Press Enter to continue...' var");
return 0;
}

Constructor invoked Press Enter to continue…

Why was the constructor invoked but not the destructor?

The object is deleted once it is no longer in the scope. In the above example, we never finish executing main(). Hence, the object is always in scope. A way to further constrain the scope is by adding squiggly brackets.

int main() {
{
std::unique_ptr<MyClass>myPtr1 = std::make_unique<MyClass>();

}

std::system("read -p 'Press Enter to continue...' var");
return 0;
}

Constructor invoked Destructor invoked Press Enter to continue…

A unique_ptr takes ownership of a pointer

  • A template: template parameter is the type that the owned pointer references (ie, the T in pointer type T*)
  • Part of C++’s standard library (C++11)
  • Its destructor information delete on the owned pointer
  • Invoked when unique_ptr object is delete‘d or fell out of scope

What happens if we pass a std::unique_ptr by value to a function?

Memory management is a nontrivial burden for the C++ programmer. Smart pointers are helpful, but a complete understanding is a must.

This code will not compile, as there is no copying of the std::unique_ptr. Hence, passing it as a parameter to a function will fail to compile.

To convince the compiler that this is fine std::move can be used.

ptr = f(std::move(ptr));

Smart pointers are a useful class type that enables the programmer to use pointers without the need of allocating and deallocating memory manually. Per Microsoft, we ought to stay away from raw pointers unless the scope is limited and the use is simple (eg, inside a loop or simple block of code).

You are now ready for deep dives into the three types of smart pointers: next will be std::unique_ptrthen std::shared_ptr and std::weak_ptr together, and, finally, we will compare, contrast, and employ each in practice.

Leave a Comment