std::weak_ptr in C++

Definition

The definition of std::weak_ptr can be summarized as follows:

  1. std::weak_ptr is a smart pointer that provides non-owning, weak references to objects managed by std::shared_ptr.
  2. It allows observing an object without affecting its lifetime, preventing strong reference cycles and memory leaks.
  3. It can be converted to a std::shared_ptr to access the managed object if it's still valid.

Understanding std::weak_ptr

std::weak_ptr is a smart pointer introduced in C++11 as part of the standard library <memory> header. It complements std::shared_ptr by providing a non-owning, weak reference to an object managed by std::shared_ptr. Unlike std::shared_ptr, std::weak_ptr does not contribute to the reference count of the managed object. Instead, it allows observing the object without extending its lifetime. If the object is deleted, the std::weak_ptr automatically becomes empty, preventing access to the invalid memory.

Need of weak_ptr

The development of std::weak_ptr addresses the issue of reference cycles and potential memory leaks that may arise when using std::shared_ptr. Reference cycles occur when objects have circular dependencies, causing each object to hold a strong reference to the other, preventing them from being deallocated. By introducing std::weak_ptr, developers can break these cycles and avoid memory leaks while still being able to observe the objects when necessary.

Consider a scenario where you have two classes, Parent and Child, with a parent-child relationship managed using std::shared_ptr. Each Parent object holds a std::shared_ptr to its child, and each Child object holds a std::shared_ptr to its parent. This creates a circular reference between the parent and child objects.

Without std::weak_ptr:

#include <memory>

class Child;

class Parent {
public:
    std::shared_ptr<Child> childPtr;
};

class Child {
public:
    std::shared_ptr<Parent> parentPtr;
};

int main() {
    std::shared_ptr<Parent> parent = std::make_shared<Parent>();
    std::shared_ptr<Child> child = std::make_shared<Child>();

    // Establishing circular reference
    parent->childPtr = child;
    child->parentPtr = parent;

    // Memory leak due to circular reference
    return 0;
}

In this example, parent and child objects form a circular reference through std::shared_ptr, resulting in a memory leak. Even after all external references to the parent and child objects are gone, they will not be deallocated because each holds a strong reference to the other.

Using std::weak_ptr to Break Circular References:

#include <memory>

class Child;

class Parent {
public:
    std::weak_ptr<Child> childPtr; // Using weak_ptr instead of shared_ptr
};

class Child {
public:
    std::weak_ptr<Parent> parentPtr; // Using weak_ptr instead of shared_ptr
};

int main() {
    std::shared_ptr<Parent> parent = std::make_shared<Parent>();
    std::shared_ptr<Child> child = std::make_shared<Child>();

    // Establishing circular reference
    parent->childPtr = child;
    child->parentPtr = parent;

    // No memory leak, as weak_ptr does not contribute to reference count
    return 0;
}

In this modified example, we have replaced std::shared_ptr with std::weak_ptr for the parent-child relationship. By using std::weak_ptr, we break the strong reference cycle, ensuring that the parent and child objects can be deallocated when no external references exist.

Creating a Weak Pointer

You can create a std::weak_ptr from a std::shared_ptr or directly from a raw pointer.

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

// or

std::weak_ptr<int> weakPtr = std::make_shared<int>(42);

Function

1 expired() Function (Checking Validity):

  • The expired() function checks whether the referenced object has been deleted (expired) or is still valid.
  • It returns true if the referenced object has been deleted, and false otherwise.

Example:

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

// Check if the referenced object is still valid
if (!weakPtr.expired()) {
    std::cout << "Referenced object is still valid." << std::endl;
} else {
    std::cout << "Referenced object has been deleted." << std::endl;
}

2 lock() Function:

  • The lock() function converts the std::weak_ptr to a std::shared_ptr, allowing access to the referenced object if it's still valid.
  • It returns an empty std::shared_ptr if the referenced object has been deleted.

Example:

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

// Attempt to convert weak_ptr to shared_ptr
std::shared_ptr<int> sharedPtrCopy = weakPtr.lock();

if (sharedPtrCopy) {
    std::cout << "Referenced object value: " << *sharedPtrCopy << std::endl;
} else {
    std::cout << "Referenced object has been deleted." << std::endl;
}

3 use_count() Function:

  • The use_count() function returns the number of std::shared_ptr instances sharing ownership of the referenced object.
  • It's important to note that use_count() is not part of std::weak_ptr itself but is accessed through a std::shared_ptr obtained using the lock() function.

Example:

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

// Obtain a shared_ptr from weak_ptr
std::shared_ptr<int> sharedPtrCopy = weakPtr.lock();

// Access use_count through the shared_ptr
if (sharedPtrCopy) {
    std::cout << "Number of shared_ptr instances: " << sharedPtrCopy.use_count() << std::endl;
}

4 Reset:

  • The reset() function resets the std::weak_ptr, releasing its reference of the objects.

Example:

weakPtr.reset(); // Releases the reference