std::weak_ptr

std::weak_ptr in C++: An Overview

std::weak_ptr is a smart pointer introduced in C++11 as part of the Standard Template Library (STL) and resides in the <memory> header. It serves as a non-owning "weak" reference to an object managed by std::shared_ptr. Its primary purpose is to break cyclic dependencies in shared ownership scenarios, allowing objects to be properly destroyed even when they reference each other.

Below is a detailed explanation of std::weak_ptr, its features, and common use cases.


Purpose of std::weak_ptr

  1. Non-Owning Reference: Unlike std::shared_ptr, std::weak_ptr does not increment the reference count of the managed object. This means it does not participate in ownership and does not prevent the object from being destroyed.

  2. Breaking Cyclic Dependencies: In data structures like graphs or trees where objects reference each other, using only std::shared_ptr can lead to memory leaks. std::weak_ptr allows you to reference an object without prolonging its lifetime.


Key Features of std::weak_ptr

1. Non-Owning

  • std::weak_ptr points to an object managed by a std::shared_ptr without affecting its reference count.

  • When the last std::shared_ptr managing the object is destroyed, the object is destroyed even if a std::weak_ptr still references it.

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp;

    std::cout << "Reference count: " << sp.use_count() << std::endl; // Output: 1
    sp.reset(); // Object destroyed
    if (wp.expired()) {
        std::cout << "The object is no longer valid." << std::endl;
    }

    return 0;
}

2. Non-Intrusive

  • Since std::weak_ptr does not increase the reference count of the shared object, it avoids cyclic dependencies.

  • This is particularly useful for complex data structures like graphs or doubly-linked lists.


3. Accessing the Object

  • A std::weak_ptr cannot directly access the object it references. Instead, you use:

    • lock(): Returns a std::shared_ptr to the object if it is still valid. If the object has been destroyed, lock() returns a null std::shared_ptr.

    • expired(): Checks whether the object has been destroyed.

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(100);
    std::weak_ptr<int> wp = sp;

    if (auto locked = wp.lock()) { // Lock converts weak_ptr to shared_ptr
        std::cout << "Locked value: " << *locked << std::endl;
    }

    sp.reset(); // Destroy the object
    if (wp.expired()) {
        std::cout << "Object is destroyed." << std::endl;
    }

    return 0;
}

4. Breaking Cyclic Dependencies

When std::shared_ptr is used exclusively, cyclic dependencies can occur because shared pointers increment the reference count of each other. std::weak_ptr solves this problem by not incrementing the reference count.

Example of Cyclic Dependency:

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next; // Shared ownership creates a cycle
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

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

    node1->next = node2;
    node2->next = node1; // Creates a cyclic dependency

    // Memory leak: Neither node1 nor node2 is destroyed because of the cycle.
    return 0;
}

Breaking the Cycle with std::weak_ptr:

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // Weak reference breaks the cycle
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

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

    node1->next = node2;
    node2->prev = node1; // Non-owning reference

    // Both nodes are destroyed correctly.
    return 0;
}

5. Observing the Managed Object

  • std::weak_ptr provides methods to observe the state of the managed object without acquiring ownership.

    • expired(): Returns true if the object has been destroyed.

    • lock(): Safely retrieves a std::shared_ptr to the object if it still exists.


Constructors

  • Default Constructor: Creates an empty std::weak_ptr.

  • From std::shared_ptr: A std::weak_ptr can be constructed from a std::shared_ptr.

  • Copy and Move: Supports copy and move construction/assignment.

std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // Create a weak_ptr from shared_ptr

Methods

  1. lock(): Returns a std::shared_ptr to the managed object if it still exists. If the object has been destroyed, it returns a null std::shared_ptr.

  2. expired(): Returns true if the object has already been destroyed.

  3. use_count(): Returns the number of std::shared_ptr instances managing the object.

  4. reset(): Releases the reference to the managed object.

  5. swap(): Exchanges the managed object with another std::weak_ptr.


Comparison with Other Smart Pointers

Feature

std::shared_ptr

std::weak_ptr

std::unique_ptr

Ownership Model

Shared

Non-owning (observing only)

Exclusive

Reference Count

Increases

Does not affect

N/A

Prevents Cyclic Dependency

No

Yes

N/A

Use Case

Shared ownership

Break cyclic dependencies

Exclusive ownership


Common Use Cases

  1. Breaking Cyclic Dependencies:

    • In doubly-linked lists, graphs, or parent-child relationships in object hierarchies.

  2. Observer Pattern:

    • std::weak_ptr is useful when a group of "observers" need non-owning references to a shared "subject".


Code Example: Comprehensive Usage

#include <iostream>
#include <memory>

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

void observer(std::weak_ptr<Resource> wp) {
    if (auto sp = wp.lock()) {
        std::cout << "Resource is alive\n";
    } else {
        std::cout << "Resource is destroyed\n";
    }
}

int main() {
    std::weak_ptr<Resource> wp;

    {
        auto sp = std::make_shared<Resource>();
        wp = sp; // Weak pointer observes the shared pointer
        observer(wp); // Resource is alive
    }

    // Outside the scope, sp is destroyed
    observer(wp); // Resource is destroyed

    return 0;
}


Understanding the Cyclic Dependency Problem

1. Will the cyclic dependency problem occur with this setup?

If you use the following code:

struct Node {
    std::shared_ptr<Node> next; // Shared ownership creates a cycle
    std::shared_ptr<Node> prev;
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

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

node1->next = node2;
node2->prev = node1;

Yes, this setup will create a cyclic dependency and cause a memory leak.

Here’s why:

  • node1 owns node2 through node1->next (a std::shared_ptr).

  • node2 owns node1 through node2->prev (another std::shared_ptr).

  • These references increment each other's reference count, resulting in a cycle where neither node1 nor node2 can ever reach a reference count of zero.

When the scope ends:

  • node1 and node2 go out of scope.

  • Their destructors are not called because the reference counts never drop to zero, causing a memory leak.


2. Breaking the Cycle with std::weak_ptr

In the corrected example:

struct Node {
    std::shared_ptr<Node> next; // Shared ownership
    std::weak_ptr<Node> prev;   // Non-owning reference
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

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

node1->next = node2; // node1 owns node2
node2->prev = node1; // node2 observes node1
  • node1->next is a std::shared_ptr, so it increments node2's reference count.

  • node2->prev is a std::weak_ptr, so it does not increment node1's reference count.

When the scope ends:

  1. node1 goes out of scope, reducing node2's reference count by 1.

  2. If node2's reference count reaches zero (because there are no other std::shared_ptr instances managing it), node2 is destroyed.

This avoids the cyclic dependency because std::weak_ptr does not participate in ownership and therefore does not contribute to reference counting.


3. Why does the cycle happen if we’re already out of scope?

When you’re out of scope, all local variables (like node1 and node2) are destroyed. However:

  • Destruction of a std::shared_ptr decrements the reference count of the managed object.

  • If there’s a cycle, the reference count never reaches zero because:

    • node1->next points to node2.

    • node2->prev points back to node1.

Since the reference counts are stuck (both objects are referencing each other), neither object is destroyed, and their memory is leaked.


4. Why does std::weak_ptr avoid the cycle?

std::weak_ptr does not increment the reference count of the object it observes. This is crucial because it allows one side of the relationship to avoid ownership.

In the example:

node1->next = node2; // Increments node2's reference count
node2->prev = node1; // Does not increment node1's reference count
  • node2->prev is a std::weak_ptr, so it observes node1 but does not prevent node1 from being destroyed.

  • When node1 is destroyed, node2->prev.lock() will return a null pointer.

This breaks the cycle:

  • node1 can be destroyed, decrementing node2's reference count.

  • Once node2's reference count reaches zero, it can also be destroyed.


Detailed Reference Count Behavior

  • node1->next = node2:

    • node1 holds a std::shared_ptr to node2, so node2's reference count is incremented by 1.

  • node2->prev = node1:

    • node2 holds a std::weak_ptr to node1. This does not increment node1's reference count.

When both node1 and node2 go out of scope:

  1. node1->next is destroyed, decrementing node2's reference count.

  2. node2->prev is destroyed, but since it’s a std::weak_ptr, it doesn’t affect node1's reference count.

  3. If no other std::shared_ptr instances exist, both node1 and node2 are destroyed.


Why does std::shared_ptr Alone Fail?

When both references (next and prev) are std::shared_ptr, they create mutual ownership:

  • node1->next prevents node2 from being destroyed.

  • node2->prev prevents node1 from being destroyed.

This mutual ownership causes the objects to stay alive even when the program exits the scope where node1 and node2 were declared, leading to a memory leak.


Summary

Pointer Type
Increments Reference Count?
Participates in Ownership?
Prevents Destruction?

std::shared_ptr

Yes

Yes

Yes

std::weak_ptr

No

No

No

  • std::weak_ptr avoids cyclic dependencies by observing an object without owning it.

  • The cyclic dependency occurs with std::shared_ptr because both objects own each other, keeping their reference counts above zero.

  • std::weak_ptr breaks the cycle by not incrementing the reference count, allowing objects to be destroyed correctly.

Conclusion

std::weak_ptr is a powerful tool for managing shared ownership scenarios without affecting the lifetime of the managed object. It plays a crucial role in breaking cyclic dependencies, observing shared objects, and implementing patterns like the observer pattern. Its non-owning nature makes it lightweight and efficient while providing essential thread-safe observation of shared resources.

Last updated