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
  • Choose garbage collected languages such as Go or Java(not convenient when working on low level applications);
  • Stick to unsafe C code and try to avoid this kind of bugs(also not convenient and practically impossible);
  • Choose a new way(such as smart pointers)or a new language(such as Rust).
  • 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.