Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Smart Pointers

Smart pointers are a modern C++ feature designed to provide automatic memory management, helping to prevent leaks and dangling pointers commonly associated with raw pointers. They act as wrappers around raw pointers, adding additional functionality such as automatic memory deallocation when the pointer is no longer needed.

Smart pointers are typically implemented as class templates in the C++ standard library. The two most commonly used smart pointers are std::unique_ptr and std::shared_ptr.

Unique pointers

std::unique_ptr is a smart pointer that owns the object exclusively. It ensures that at any given time, only one std::unique_ptr object owns the resource. When the owning std::unique_ptr is destroyed or reset, it automatically destructs the objects and releases its memory.

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

Advantages of std::make_unique()

When creating a std::unique_ptr, it's preferable to use std::make_unique() instead of directly using new to allocate memory. std::make_unique() provides several advantages:

  1. Exception safety: std::make_unique() guarantees exception safety. If an exception is thrown during the construction of the object, memory will be automatically deallocated, preventing memory leaks.
  2. Clarity: Using std::make_unique() makes code clearer and more concise. It eliminates the need to explicitly specify the type being allocated, as the template arguments are deduced automatically.
  3. Optimization Opportunities: Compilers have the opportunity to optimize std::make_unique() more effectively than manually allocating memory with new, potentially resulting in improved performance.
  4. Avoiding Misuse: Deleting the underlying resource is possible, when the std::unique_ptr is constructed manually. That would lead to undefined behavior, when the std::unique_ptr tries to delete it at its end of scope.

Shared pointers

std::shared_ptr is a smart pointer that allows multiple std::shared_ptr objects to share ownership of the same resource. It keeps track of how many shared pointers are referencing the resource, and deallocates the memory only when the last shared pointer owning the resource goes out of scope or is reset.

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;

In C++17 and below, using std::shared_ptr with arrays via std::make_shared<T[]> is not directly supported. While it's possible to allocate arrays with std::make_shared<T[]>, creating shared pointers directly from them may lead to undefined behavior due to differences in memory management between single objects and arrays. Instead, consider using std::vector or custom deletion functions to manage arrays with shared pointers effectively. Always ensure compatibility with your compiler and standard library implementation when dealing with array allocations and shared pointers in C++17.

Advantages of std::make_shared()

Similar to std::make_unique(), std::make_shared() offers benefits such as improved memory efficiency, exception safety, and readability. It combines memory allocation for the control block and the managed object into a single operation, enhancing efficiency and reducing the risk of memory leaks. Additionally, automatic deduction of template arguments simplifies code and enhances readability. Using std::make_shared() promotes cleaner, safer, and more efficient code when working with std::shared_ptr objects in C++.

Weak pointers

std::weak_ptr is a companion class to std::shared_ptr that provides a non-owning "weak" reference to an object managed by a shared pointer. It allows access to the resource without affecting its lifetime. Weak pointers are useful in scenarios where cyclic references need to be broken to prevent memory leaks.

std::weak_ptr and Cyclic Ownership

std::weak_ptr was designed to address the issue of cyclic ownership, also known as circular references, that can occur when using std::shared_ptr.

In a cyclic ownership scenario, two or more std::shared_ptr objects are referencing each other, creating a cycle where none of the objects can be deleted because they have strong references to each other, leading to memory leaks.

std::weak_ptr provides a solution to this problem by allowing weak references to shared objects without contributing to their reference count. This means that it can observe and access the shared object but doesn't prevent it from being deleted. If all strong references to the shared object are released, the object is destroyed, and weak pointers observing it are automatically reset to nullptr.

Example of a circular reference:


#include <iostream>
#include <memory>
#include <string>

struct Human {
    std::string name;
    std::shared_ptr<Human> friend_for_life; // uh-oh

    Human(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }

    ~Human() {
        std::cout << name << " destroyed\n";
    }
};

int main() {
    {
        auto alice = std::make_shared<Human>("Alice");
        auto bob   = std::make_shared<Human>("Bob");

        // circular reference
        alice->friend_for_life = bob;
        bob->friend_for_life   = alice;
    }
    // you'd *expect* both destructors to run here... but nope!

    std::cout << "End of main\n";
}

What happens:

  • Alice and Bob both own each other with shared_ptr.
  • Reference counts never reach zero.
  • Destructor never called: leak.

The output looks like this:

Alice created
Bob created
End of main

The fixed version (break one side with a weak pointer):


#include <iostream>
#include <memory>
#include <string>

struct Human {
    std::string name;
    std::weak_ptr<Human> friend_for_life; // weak = no ownership

    Human(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }

    ~Human() {
        std::cout << name << " destroyed\n";
    }
};

int main() {
    {
        auto alice = std::make_shared<Human>("Alice");
        auto bob   = std::make_shared<Human>("Bob");

        // now Alice and Bob point weakly to each other
        alice->friend_for_life = bob;
        bob->friend_for_life   = alice;
    }
    // destructors run as expected, no leak!

    std::cout << "End of main\n";
}

Output:

Alice created
Bob created
Alice destroyed
Bob destroyed
End of main

Dangling pointers and std::weak_ptr

Dangling pointers occur when a pointer references an object that has been deleted, leading to undefined behavior when the pointer is dereferenced. std::weak_ptr helps avoid dangling pointers by providing a non-owning, weak reference to an object managed by std::shared_ptr. Here's how to avoid dangling pointers with std::weak_ptr:

  1. Create a std::weak_ptr: Instead of directly holding a std::shared_ptr, create a std::weak_ptr to the shared object.

  2. Check for Validity: Before using the std::weak_ptr, check its validity using the expired() function. This function returns true if the associated shared object has been deleted.

  3. Lock the std::weak_ptr: To access the shared object safely, use the lock() function, which returns a std::shared_ptr pointing to the same object if it is still valid. If the object has been deleted, lock() returns an empty std::shared_ptr.

Here's another code example demonstrating the use of std::weak_ptr to avoid dangling pointers:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

In this example, node1->next and node2->next are std::weak_ptr objects. Before accessing the shared object, validity is checked using the expired() function. Then, lock() is used to safely access the shared object. This approach ensures that dangling pointers are avoided when working with std::weak_ptr.

Usage advice

Use smart pointers by default: std::unique_ptr for exclusive ownership and std::shared_ptr for shared ownership. Reserve raw pointers for non-owning references or when interfacing with legacy code. In most cases, std::unique_ptr is sufficient for exclusive ownership, as it offers lightweight memory management without the overhead of reference counting. std::shared_ptr should be used sparingly, as it introduces overhead and complexity unless true shared ownership is needed. std::weak_ptr is specialized for breaking cyclic dependencies or observing shared objects, but it's not commonly used.