HOW TO USE SMART POINTERS IN C++

2021-11-16
In this guide we will see smart pointers in C++ as a replacement to the old, unsafe, C-like pointers. I will assume that you have some knowledge of modern C++(i.e., \( \geq \) C++11) and of object oriented programming.

What Is a Smart Pointer?

A smart pointer is an object that simulates a normal pointer while ensuring the program to be free of memory leaks. It achieves that by providing an automatic memory management system that deletes an object if no longer in use. In order to act like a normal pointer, smart pointers need to provide a method for both dereferencing (*) and indirection(->). Smart pointers are defined into the standard library of C++ in the <memory> header.

How Does Smart Pointers work?

Smart pointers follow the RAII idiom(Resource Acquisition Is Initialization); that is, ensuring that the object acquisition occurs when the object is being declared. In order to do that, smart pointers take the ownership of any resource allocated on the heap, when the ownership count of an object reach zero(for example, when a pointer reach the end of the scope), the resource is being automatically terminated.

Why Should I Use Smart Pointers?

The main reason to use smart pointers is that they prevent some of the most common bugs in both C and C++: memory leaks, dangling pointers and any other memory related problem. If you think that you are not affected by this kind of issues, take a look at this excellent Microsoft article. In particular, consider the following statement:
>
[…] ~70% of the vulnerabilities Microsoft assigns a CVE each year continue to be memory safety issues[…]
-- Microsoft
The solutions to this problem are essentially the following:

Three Different Smart Pointers

Modern C++ provides three different ways to define a smart pointer:

std::unique_ptr

Without smart pointers, to declare a pointer to an object, we would write:
                
#include <iostream>
#include <string>
// Compile with: g++ foo.cpp -Wall -Wextra -Werror -std=c++14

class Object {
public:
    Object(const unsigned int num, const std::string val) {
        this->num = num;
        this->val = val;
    }
    std::string print() { return this->val + ": " + std::to_string(this->num); }

private:
    unsigned int num;
    std::string val;
};

int main() {
    Object *obj = new Object(0, "zero"); // Declare a new object

    std::cout << obj->print() << std::endl; // Do something with the object

    delete obj; // Manually delete the object

    return 0;
}
                
            
with smart objects, this becomes
                
// [...]
int main() {
    std::unique_ptr<Object> obj = std::make_unique<Object>(0, "zero"); // Declare a new object

    std::cout << obj->print() << std::endl; // Do something with the object

    return 0;
} // obj will be terminated here

                
            
If we try to declare a new smart pointer that points to the same location, i.e.
                
// [...]
int main() {
    std::unique_ptr<Object> obj = std::make_unique<Object>(0, "zero"); // Declare a new object
    std::unique_ptr<Object> obj2 = obj; // New smart pointer

    std::cout << obj->print() << std::endl; // Do something with the object

    return 0;
} // obj will be terminated here

                
            
we will get the same error from the compiler:
                
sp.cpp:21:29: error: call to implicitly-deleted copy constructor of 'std::unique_ptr<Object>'
    std::unique_ptr<Object> obj2 = obj;
                
            
Resources initialized with unique_ptr pointer must have one owner only. The only thing we can do in these cases is to move the ownership to another smart pointer. That is:
                
// [...]
int main() {
    std::unique_ptr<Object> obj = std::make_unique<Object>(0, "zero"); // Declare a new object
    std::unique_ptr<Object> obj2 = std::move(obj); // Move ownership to this pointer
    // obj is no longer available

    std::cout << obj2->print() << std::endl; // Do something with the object

    return 0;
} // obj2 will be terminated here

                
            
Do note however, after moving the ownership to obj2, the old obj is no longer available. In fact, invoking it will cause a segmentation fault exception by the operating system.

std::shared_ptr

let's now discuss about shared_ptrs. As we described before, this kind of smart pointer allows multiple referencing to the same resource. After the number of owners of an object reaches zero, the resource is automatically terminated. let's see an example:
                
// [...]
int main() {
    std::shared_ptr<Object> obj = std::make_shared<Object>(0, "zero");
    // Prints out the number of owners
    std::cout << "Current owner(s): " << obj.use_count() << std::endl;
    {
        std::shared_ptr<Object> obj2 = obj;
        std::cout << "Current owner(s): " << obj.use_count() < std::endl;
    } // obj2 will be terminated here

    std::cout < "Current owner(s): " < obj.use_count() < std::endl;
    std::cout << obj->print() << std::endl; // Do something with the object

    return 0;
} // obj will be terminated here
                
            
Which produces:
                
$> ./a.out
Current owner(s): 1
Current owner(s): 2
Current owner(s): 1
zero: 0
                
            
This is what happens in the previous code:
  1. We first declare a new shared pointer, the owner's count is equal to one;
  2. In the inner scope, we declare a new shared pointer to the same object, the owner's count is now equal to two;
  3. The inner scope ends, the owner's count of obj decrease to one, so it will not be terminated;
  4. The outer scope ends, so owner's count of obj decrease to zero, the resource will now be terminated.

std::weak_ptr

Finally, let's see weak_ptrs. This kind of smart pointer does not alter the ownership count of an object, instead it will only provide access to a shared pointer resource. Let's see an example:
                
// [...]
int main() {
    std::weak_ptr<Object> w_ptr; // Declare a new weak_ptr
    {
        std::shared_ptr<Object> obj = std::make_shared<Object>(0, "zero");
        // Prints out owners count
        std::cout << "Owner(s) count(before w_ptr): " << obj.use_count() << std::endl;
        // Owner's count does not change, even after last assignment
        w_ptr = obj;
        std::cout << "Owner(s) count(after w_ptr): " << obj.use_count() << std::endl;
    } // obj goes out of scope, owner's count is zero, so the resource is terminated

    if(w_ptr.expired())
        std::cout << "The weak pointer is now invalid" << std::endl;

return 0;
}
                
            
which produces
                
$> ./a.out 
Owner(s) count(before w_ptr): 1
Owner(s) count(after w_ptr): 1
The weak pointer is now invalid
                
            
The previous code does the following:
  1. Create a new empty weak_ptr;
  2. In the inner scope, declare a new shared_ptr, the owner's count increase to 1;
  3. Assign the same resource to w_ptr, the owner's count does not change;
  4. The inner scope ends, so the owner's count drops to zero, the resource is being terminated;
  5. If we now try to check if w_ptr is expired, we get a true result.

Compilation flags

All the previous examples were compiled using the following flags:
                
-Wall -Wextra -Werror -std=c++14
                
            
Some of these features, were not available prior to C++14.