CLOSE
Updated on 18 Jun, 202529 mins read 68 views

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 <iostream>
#include <memory>

class Child; // Forward declaration

class Parent {
public:
    std::shared_ptr<Child> childPtr; // Shared ownership of Child

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

class Child {
public:
    std::shared_ptr<Parent> parentPtr; // Shared ownership of Parent

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

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

    // Establish mutual (cyclic) ownership
    parent->childPtr = child;  // Parent owns Child
    child->parentPtr = parent; // Child owns Parent

    // ❗ Both shared_ptrs now keep each other alive
    // ❌ Memory will NOT be freed — destructors won't be called

    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

Real-World Example: Breaking a Cycle

Let’s say you have two classes that hold shared references to each other:

#include <iostream>
#include <memory>

class Child; // Forward declaration

class Parent {
public:
    std::shared_ptr<Child> childPtr; // Shared ownership of Child

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

class Child {
public:
    std::shared_ptr<Parent> parentPtr; // Shared ownership of Parent

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

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

    // Establish mutual (cyclic) ownership
    parent->childPtr = child;  // Parent owns Child
    child->parentPtr = parent; // Child owns Parent

    // ❗ Both shared_ptrs now keep each other alive
    // ❌ Memory will NOT be freed — destructors won't be called

    return 0;
}

Output of this Program:

(no output)

Notice that neither Parent nor Child is destroyed, even though main() ends. That’s because their shared_ptrs are keeping each other alive:

  • parent owns child

  • child owns parent

  • Reference count never hits zero

Breaking Cycle Dependencies with std::weak_ptr

#include <iostream>
#include <memory>

class Child; // Forward declaration

class Parent {
public:
    std::weak_ptr<Child> childPtr; // Non-owning reference to Child

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

class Child {
public:
    std::weak_ptr<Parent> parentPtr; // Non-owning reference to Parent

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

int main() {
    // Create shared ownership for both Parent and Child
    std::shared_ptr<Parent> parent = std::make_shared<Parent>();
    std::shared_ptr<Child> child = std::make_shared<Child>();

    // Establish mutual references using weak_ptr
    parent->childPtr = child;  // Parent observes (but does not own) Child
    child->parentPtr = parent; // Child observes (but does not own) Parent

    // Exiting main will destroy both parent and child
    // No memory leak occurs because weak_ptr does not increase the reference count

    return 0;
}
Child destroyed
Parent destroyed
  • Both Parent and Child use std::weak_ptr for non-owning references to avoid increasing reference counts.

  • When main() ends, both shared_ptrs (parent and child) go out of scope.

  • The Parent and Child objects are properly destroyed, and no memory leaks occur.

  • Destructors print confirmation that both objects were cleaned up.

Leave a comment

Your email address will not be published. Required fields are marked *