std::weak_ptr
std::weak_ptr in C++: An Overview
std::weak_ptr in C++: An Overviewstd::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
std::weak_ptrNon-Owning Reference: Unlike
std::shared_ptr,std::weak_ptrdoes 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.Breaking Cyclic Dependencies: In data structures like graphs or trees where objects reference each other, using only
std::shared_ptrcan lead to memory leaks.std::weak_ptrallows you to reference an object without prolonging its lifetime.
Key Features of std::weak_ptr
std::weak_ptr1. Non-Owning
std::weak_ptrpoints to an object managed by astd::shared_ptrwithout affecting its reference count.When the last
std::shared_ptrmanaging the object is destroyed, the object is destroyed even if astd::weak_ptrstill 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_ptrdoes 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_ptrcannot directly access the object it references. Instead, you use:lock(): Returns astd::shared_ptrto the object if it is still valid. If the object has been destroyed,lock()returns a nullstd::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_ptrprovides methods to observe the state of the managed object without acquiring ownership.expired(): Returnstrueif the object has been destroyed.lock(): Safely retrieves astd::shared_ptrto the object if it still exists.
Constructors
Default Constructor: Creates an empty
std::weak_ptr.From
std::shared_ptr: Astd::weak_ptrcan be constructed from astd::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_ptrMethods
lock(): Returns astd::shared_ptrto the managed object if it still exists. If the object has been destroyed, it returns a nullstd::shared_ptr.expired(): Returnstrueif the object has already been destroyed.use_count(): Returns the number ofstd::shared_ptrinstances managing the object.reset(): Releases the reference to the managed object.swap(): Exchanges the managed object with anotherstd::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
Breaking Cyclic Dependencies:
In doubly-linked lists, graphs, or parent-child relationships in object hierarchies.
Observer Pattern:
std::weak_ptris 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:
node1ownsnode2throughnode1->next(astd::shared_ptr).node2ownsnode1throughnode2->prev(anotherstd::shared_ptr).These references increment each other's reference count, resulting in a cycle where neither
node1nornode2can ever reach a reference count of zero.
When the scope ends:
node1andnode2go 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
std::weak_ptrIn 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 node1node1->nextis astd::shared_ptr, so it incrementsnode2's reference count.node2->previs astd::weak_ptr, so it does not incrementnode1's reference count.
When the scope ends:
node1goes out of scope, reducingnode2's reference count by 1.If
node2's reference count reaches zero (because there are no otherstd::shared_ptrinstances managing it),node2is 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_ptrdecrements the reference count of the managed object.If there’s a cycle, the reference count never reaches zero because:
node1->nextpoints tonode2.node2->prevpoints back tonode1.
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 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 countnode2->previs astd::weak_ptr, so it observesnode1but does not preventnode1from being destroyed.When
node1is destroyed,node2->prev.lock()will return a null pointer.
This breaks the cycle:
node1can be destroyed, decrementingnode2's reference count.Once
node2's reference count reaches zero, it can also be destroyed.
Detailed Reference Count Behavior
node1->next = node2:node1holds astd::shared_ptrtonode2, sonode2's reference count is incremented by 1.
node2->prev = node1:node2holds astd::weak_ptrtonode1. This does not incrementnode1's reference count.
When both node1 and node2 go out of scope:
node1->nextis destroyed, decrementingnode2's reference count.node2->previs destroyed, but since it’s astd::weak_ptr, it doesn’t affectnode1's reference count.If no other
std::shared_ptrinstances exist, bothnode1andnode2are destroyed.
Why does std::shared_ptr Alone Fail?
std::shared_ptr Alone Fail?When both references (next and prev) are std::shared_ptr, they create mutual ownership:
node1->nextpreventsnode2from being destroyed.node2->prevpreventsnode1from 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
std::shared_ptr
Yes
Yes
Yes
std::weak_ptr
No
No
No
std::weak_ptravoids cyclic dependencies by observing an object without owning it.The cyclic dependency occurs with
std::shared_ptrbecause both objects own each other, keeping their reference counts above zero.std::weak_ptrbreaks 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