A std::weak_ptr is a smart pointer that observes without owning an object managed by std::shared_ptr.
We have seen std::unique_ptr for when we want absolute ownership. We have seen std::shared_ptr for when we want to share ownership among multiple parts of the program.
But what happens when we want to look but not touch? Or better yet, what happens when we want a reference to an object managed by others, but without preventing it from being destroyed when they stop using it?
For that, we have the third type of smart pointer: the std::weak_ptr.
This pointer is fundamentally a weak observer. Unlike its older brother shared_ptr, the weak_ptr does not increase the object’s reference count.
The Problem of Circular References
To understand the usefulness of weak_ptr, we first need to understand the typical problem with shared_ptr: circular references.
We have two classes, A and B:
Ahas ashared_ptrtoB.Bhas ashared_ptrtoA.
struct B; // Forward declaration
struct A {
std::shared_ptr<B> pointer_to_b;
~A() { std::cout << "Goodbye A\n"; }
};
struct B {
std::shared_ptr<A> pointer_to_a;
~B() { std::cout << "Goodbye B\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
// Create the cycle
a->pointer_to_b = b;
b->pointer_to_a = a;
}
What happens at the end of main?
- Variable
a(stack) is destroyed. A’s count drops from 2 to 1 (becausebstill points to it). - Variable
b(stack) is destroyed. B’s count drops from 2 to 1 (becauseastill points to it).
Result: Both objects remain with a count of 1. Neither is destroyed. The memory is never freed. We have a Memory Leak.
Breaking the Cycle
The practical criterion in C++ for such structures (like graphs, trees with parent references, doubly linked lists) is:
“The owner has a strong pointer (
shared_ptr). The subordinate (or the backward reference) has a weak pointer (weak_ptr).”
If we change class B to use std::weak_ptr:
struct B {
// IMPORTANT CHANGE: weak_ptr instead of shared_ptr
std::weak_ptr<A> pointer_to_a;
~B() { std::cout << "Goodbye B\n"; }
};
Now, when b points to a, it does not increase A’s reference count. When main ends and the local variables are destroyed, the counts will correctly reach zero, and everything will be cleaned up.
How to Use std::weak_ptr?
A weak_ptr has a particularity: it does not allow direct access to the object.
Since it does not own the object, we cannot be sure the object is still alive when we try to access it. It could have been deleted by its owners (shared_ptr) in another thread or at another moment.
Therefore, weak_ptr has no * operator or -> operator.
The .lock() Method
To use the object pointed to by a weak_ptr, we must temporarily “promote” it to a shared_ptr. This is done with the .lock() method.
This creates a temporary shared_ptr. If the object exists, the count increases (preventing it from being deleted while we use it). If the object has already died, it returns nullptr.
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// ATTEMPT 1: The object exists
if (std::shared_ptr<int> temp = weak.lock()) {
std::cout << "The value is: " << *temp << "\n";
} else {
std::cout << "The object no longer exists.\n";
}
// "Kill" the original object by resetting the shared_ptr
shared.reset();
// ATTEMPT 2: The object no longer exists
if (std::shared_ptr<int> temp = weak.lock()) {
std::cout << "The value is: " << *temp << "\n";
} else {
std::cout << "The object no longer exists (correctly detected).\n";
}
}
Always, always use .lock() and check if the resulting pointer is valid. It is the only safe way to work with weak references.
Another Useful Method: .expired()
If we only want to know if the object is still alive, but don’t need to access it, we can use .expired(). It returns true if the object has already been released.
if (weak.expired()) {
std::cout << "We are too late, the object is dead.\n";
}
However, in concurrent (multithreaded) environments, it is safer to use .lock(). Between the call to .expired() and the actual access, another thread might destroy the object. .lock() attempts to acquire temporary ownership atomically.
The Control Block
In the previous article, we saw that shared_ptr creates a control block in memory to keep track.
In reality, this block has two counters:
- Strong Count: How many
shared_ptrinstances exist. If it reaches 0, the Object is destroyed. - Weak Count: How many
weak_ptrinstances exist. If it reaches 0 (and the Strong Count is also 0), the Control Block is destroyed.
This means that as long as a single weak_ptr is alive, the control block (a few bytes) will still occupy memory, even though the large object it managed has already been released. It is a small price to pay for the safety it provides.
Now that we know how to manage memory, let’s see how to optimize data movement with another key concept: