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[…]

The solutions to this problem are essentially the following:

  • Choose garbage collected languages such as Go or Java(not convenient when speed is required);
  • 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: Bind exactly one owner to the pointed resources. This means that declaring another smart pointer to the same pointer will cause a compilation error. The resource pointed by a unique_ptr is automatically being removed after the pointer goes out of scope;
  • std::shared_ptr: Allows multiple owners to the same object. In this case multiple pointers can point to the same resource, to keep count of how many owners a given resource has, shared_ptrs keep an internal counter that it is either incremented or decremented each time a new pointer is declared or deleted. The allocated object will not be terminated until all owners have gone out of scope;
  • std::weak_ptr: Another kind of smart pointer to be used along with shared_ptrs. weak_ptrs give access to a resource owned by one or more shared_ptrs but they do not alter the ownership count. That is, when all the shared_ptrs are gone out of scope, the resource will be terminated even if there are one or more weak_ptr pointed to that location. The new weak_ptr value will be marked as expired. weak_ptr can be used to observe an object without interfere with the ownership count.

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.