std::weak_ptr
std::weak_ptr
. Learn what they’re for, and how we can use them with practical examplesIn previous lessons, we saw how smart pointers are used to automate the allocation and deallocation of memory, based on an “ownership” model.
An object that holds a unique pointer to another object is considered to uniquely own that object. When the owner is deallocated, so too is the unique pointer, which in turn deallocates the objects that it pointed to. The standard library’s implementation of unique pointers is the std::unique_ptr
 object.
Sometimes, we need ownership to be shared between multiple objects. We do this by having the owners share a pointer to the object they all own. The standard library’s implementation of these shared pointers is std::shared_ptr
.
Shared pointers work by a technique called reference counting. Internally, they keep track of how many copies of themselves exist in our application.
When a new copy is created, the count increases. When a copy is deallocated, the reference count decreases. When the reference count reaches zero, the shared pointer, and the object it is pointing to, is automatically deallocated.
The effect of this is that an object managed by a shared pointer is only deallocated when all of its owners are deallocated.
There is a third type of smart pointer commonly used in programming languages. It is typically called a weak pointer.
In principle, a weak pointer works somewhat similarly to a shared pointer. Weak pointers are created from shared pointers, and the objects that hold a weak pointer have easy access to the object it is pointing to.
The key difference is that a weak pointer is considered a non-owning reference.
A weak pointer does not keep the object alive, so if all shared pointers to the object are destroyed, the weak pointer becomes invalid.
Common use cases for weak pointers include:
Similar to other standard library smart pointers, weak pointers are available by including <memory>
, and they have a template parameter specifying which type of object they’re pointing to.
In the following example, we create a weak pointer to point at an int
:
#include <memory>
int main() {
std::weak_ptr<int> WeakPtr;
}
Commonly, weak pointers are initialized from a shared pointer:
#include <memory>
int main() {
auto SharedPtr { std::make_shared<int>(42) };
std::weak_ptr<int> WeakPtr{SharedPtr};
}
When creating a weak pointer from a shared pointer, the template argument can be deduced from the type of the shared pointer, so it is not necessary to include it:
#include <memory>
int main() {
auto SharedPtr { std::make_shared<int>(42) };
std::weak_ptr WeakPtr{SharedPtr};
}
We cover templates in detail in the next chapter.
use_count()
and expired()
MethodsSimilar to shared pointers, weak pointers have the use_count
method, which returns how many pointers are sharing ownership of the object.
As a weak pointer is not considered an owner, it is excluded from this count.
#include <memory>
#include <iostream>
int main(){
std::weak_ptr<int> WeakPtr;
{
auto SharedPtr{std::make_shared<int>(42)};
WeakPtr = SharedPtr;
std::cout << "Use count: " <<
WeakPtr.use_count();
}
std::cout << "\nUse count: " <<
WeakPtr.use_count();
}
Use count: 1
Use count: 0
In the previous example, we create a shared pointer as a local variable within a block scope. We update the weak pointer to point to that same object.
Because of this, our first call to use_count()
returns 1
. This is because the object has one owner - the shared pointer we created.
When the block ends, the shared pointer is deallocated. Because the object has no other owners, it is also deallocated. Therefore, the second call to use_count
returns 0
If we want to specifically check if the use_count()
has fallen to 0
, we can instead use the more descriptive expired()
 method:
#include <memory>
#include <iostream>
int main(){
std::weak_ptr<int> WeakPtr;
{
auto SharedPtr{std::make_shared<int>(42)};
WeakPtr = SharedPtr;
if (!WeakPtr.expired()) {
std::cout << "The pointer hasn't expired";
}
}
if (WeakPtr.expired()) {
std::cout << "\nBut now it has";
}
}
The pointer hasn't expired
But now it has
In order to access our object through the weak pointer, we first need to create a shared pointer from it.
This may seem like a weird restriction, but it’s a protection against issues that can arise when we’re working in multi-threaded environments.
If we don’t first lock our object, something happening on another thread might drop the use_count()
to 0
. This would cause the object to be deallocated whilst we’re still using it.
So, we first create a shared pointer from our weak pointer. This ensures our process becomes a shared owner of the object, preventing it from being deleted.
We do this by calling lock()
on our weak pointer. This returns a shared pointer, which we can use in the normal way with operators such as *
and ->
:
#include <memory>
#include <iostream>
int main(){
auto SharedPtr{std::make_shared<int>(42)};
std::weak_ptr WeakPtr{SharedPtr};
std::shared_ptr LockedPtr{WeakPtr.lock()};
std::cout << "The number is: " << *LockedPtr;
}
The number is: 42
In this basic example, we know the object will not have expired, but rarely are our use cases this simple. Typically, when we have a weak pointer, we don’t know if it has expired or not - we need to check.
We can do that using expired()
as described before. Alternatively, if the pointer has expired, lock()
will return an empty shared pointer. We can check if a pointer is empty simply by coercing it into a boolean.
As such, a common pattern for working with weak pointers that may have expired looks like this:
#include <memory>
#include <iostream>
void Log(std::weak_ptr<int> Ptr){
if (std::shared_ptr LockedPtr{Ptr.lock()}) {
std::cout << "The number is " << *LockedPtr;
} else {
std::cout << "\nThe pointer has expired";
}
}
int main(){
std::weak_ptr<int> WeakPtr;
{
auto SharedPtr{std::make_shared<int>(42)};
WeakPtr = SharedPtr;
Log(WeakPtr);
}
Log(WeakPtr);
}
The number is 42
The pointer has expired
The most common place we’ll see weak pointers is class variables. We’ll often have a class that needs to keep a reference to some other object, without necessarily keeping that object alive. This is the core use case for weak pointers.
The following example replicates the previous setup, but we’re working with user-defined types instead:
#include <memory>
#include <iostream>
using std::string, std::weak_ptr,
std::make_shared, std::shared_ptr, std::cout;
class Character {
public:
string Name;
};
class Party {
public:
weak_ptr<Character> Leader;
string LeaderName(){
if (shared_ptr Lead{Leader.lock()}) {
return Lead->Name;
}
return "No Leader";
}
};
int main(){
Party MyParty;
{
auto Leader{make_shared<Character>("Anna")};
MyParty.Leader = Leader;
cout << "Party Leader: "
<< MyParty.LeaderName();
}
cout << "\nParty Leader: "
<< MyParty.LeaderName();
}
Party Leader: Anna
Party Leader: No Leader
The main advantage of weak pointers over raw pointers is the ability to check if the pointer is valid before dereferencing it.
When we have a raw pointer to an object, and that object is later deallocated, we are left with a dangling pointer. This is often a problem as, with a raw pointer, we have no easy way to check if it’s “dangling”.
A dangling pointer is created when the object it was pointing to, unbeknownst to it, has been deleted. The pointer still points to that memory address, but that memory address may have been allocated to something else. Using a dangling pointer will result in undefined behavior - likely a crash.
What’s worse is that we cannot easily check if a pointer is dangling. Unlike a null pointer, dangling pointers are “truthy”:
#include <iostream>
int main(){
int* DanglingPtr{new int(42)};
delete DanglingPtr;
if (DanglingPtr) {
// this WILL run, and will be undefined behavior
std::cout << *DanglingPtr;
}
}
When working with raw pointers, a common pattern to prevent dangling pointers is to ensure we set them to null when we delete the underlying resource:
#include <iostream>
int main(){
int* Ptr{new int(42)};
delete Ptr;
Ptr = nullptr;
if (Ptr) {
// this WILL NOT run
std::cout << *Ptr;
}
}
But in complex programs, this is not always easy to do. There can be many pointers to a resource, stored in many different objects around our application.
We saw a basic form of that in our earlier example with the Character
and Party
class. Were our Party
storing a raw pointer to the Character
leading it, the Party
would have no way to tell if the Character
had been deallocated.
We could have written a load of extra code to keep the Party
aware, but by simply using a weak pointer instead, that work becomes unnecessary. This is one of the many reasons we should generally try to manage memory through smart pointers rather than raw pointers.
However, weak pointers also have some overhead, and the code we write to work with them is more verbose than the equivalent raw pointer syntax. Because of this, balancing dangling pointer considerations against code complexity is generally something to decide on a case-by-case basis, and our instincts get better here with experience.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.