Weak Pointers with std::weak_ptr

A full guide to weak pointers, using std::weak_ptr. Learn what they’re for, and how we can use them with practical examples
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In our previous lessons, we've explored the intricacies of smart pointers in C++, focusing on how they automate memory management through an ownership model.

We've seen how unique pointers (std::unique_ptr) provide a way to uniquely own and automatically clean up resources.

We've also examined shared pointers (std::shared_ptr), which allow multiple objects to share ownership of a single resource using reference counting. This approach ensures that the resource is only deallocated when all owners have been destroyed.

Finally, let's turn our attention to the last smart pointer variation: weak pointers.

Understanding Weak Pointers

Weak pointers work closely with shared pointers, with one key difference: they do not actively participate in the ownership and lifecycle management of the objects they point to.

This means they have a view of the resource but do not affect its lifespan. The life of the resource is governed solely by its shared pointer owners.

If all shared pointers to an object are destroyed, the object is deallocated, and the weak pointer becomes expired - a reference to something that no longer exists.

Weak pointers shine in several scenarios:

Breaking Circular References

Consider two objects, each holding a shared pointer to the other. They create a cycle, much like two friends promising to leave a party only when the other does.

This circular dependency prevents either of them from being deallocated. By converting one connection to a weak pointer, we break this cycle, enabling proper cleanup.

Handling Dangling Pointers

Dangling pointers are like outdated addresses - they point to a location where the intended object no longer resides. As we’ll see later in the lesson, it’s quite hard to detect when a raw pointer is dangling, but very easy when we’re using a weak pointer instead.

Caching Mechanisms

In caching, where objects are stored temporarily for quick access, weak pointers can reference these objects without affecting their lifecycle. We’ll cover caching in detail later in the course.

Creating Weak Pointers

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 type within the <> can be deduced from the equivalent type of the shared pointer - int, in this case. So it is not necessary to include it:

#include <memory>

int main() {
  auto SharedPtr { std::make_shared<int>(42) };
  std::weak_ptr WeakPtr{SharedPtr};
}

The arguments we pass between chevrons < and > are called template parameters. We cover templates in detail in a dedicated chapter later in the course.

use_count() and expired() Methods

Similar to shared pointers, weak pointers have the use_count() method, which returns how many pointers are sharing ownership of the object.

#include <memory>
#include <iostream>

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

int main(){
  auto SharedPtr{std::make_shared<Resource>()};
  std::weak_ptr WeakPtr{SharedPtr};

  std::cout << "Use count: " <<
    WeakPtr.use_count(); 

  SharedPtr.reset();

  std::cout << "\nUse count: " <<
    WeakPtr.use_count(); 
}

A weak pointer is not considered an owner - as such, it does not participate in the count, and it does not keep the resource alive:

Use count: 1
Deallocating
Use count: 0

When a weak pointer has a use_count() of 0, that indicates the underlying resource has been deallocated. Any weak pointers that point to it will be considered expired.

We can specifically check for this using the more descriptive expired() method:

#include <memory>
#include <iostream>

int main(){
  auto SharedPtr{std::make_shared<int>(42)};
  std::weak_ptr WeakPtr{SharedPtr};

  if (!WeakPtr.expired()) {
    std::cout << "The pointer hasn't expired";
  }

  SharedPtr.reset();

  if (WeakPtr.expired()) {
    std::cout << "\nBut now it has";
  }
}
The pointer hasn't expired
But now it has

Accessing Objects through a Weak Pointer

Accessing an object through a weak pointer is a little more complex than accessing an object through other smart pointer types, such as shared pointers or unique pointers. This is to account for the fact that, unlike other smart pointers, weak pointers can potentially have expired.

To use the underlying object, we first need to create a shared pointer from our weak pointer, thereby registering an owner. This is sometimes referred to as locking the weak pointer.

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.

We create a shared pointer from our weak pointer using the lock() method:

#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 the expired() method as described earlier in the lesson.

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(){
  auto SharedPtr{std::make_shared<int>(42)};
  std::weak_ptr WeakPtr{SharedPtr};
  Log(WeakPtr);
  SharedPtr.reset();
  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.

The following example replicates the same concepts in a slightly more complex context:

#include <memory>
#include <iostream>

class Character {
public:
  std::string Name;
};

class Party {
public:
  std::weak_ptr<Character> Leader; 

  std::string LeaderName(){
    if (std::shared_ptr Lead{Leader.lock()}) {
      return Lead->Name;
    }
    return "No Leader";
  }
};

int main(){
  auto Leader{
    std::make_shared<Character>("Anna")
  };

  Party MyParty{Leader};

  std::cout << "Party Leader: "
    << MyParty.LeaderName();

  Leader.reset();

  std::cout << "\nParty Leader: "
    << MyParty.LeaderName();
}
Party Leader: Anna
Party Leader: No Leader

Weak Pointers vs Raw Pointers

At this point, a common question is to ponder whether weak pointers have any advantages over raw pointers. They have very similar properties: they both provide access to an object in memory, without claiming any ownership over it or affecting its lifecycle.

The key advantage of weak pointers is the ability to know whether what they point to has expired. This may seem trivial, but this additional layer of awareness is extremely valuable in large projects.

When we have a raw pointer to an object, and that object is later deallocated, we are left with a dangling pointer. The pointer still points to that memory address but is unaware that anything has changed, and we have no easy way to check:

#include <iostream>

int main(){
  auto SharedPtr{std::make_shared<int>(42)};
  int* RawPtr{SharedPtr.get()};
  if (RawPtr) {
    std::cout << "RawPtr is truthy | Value: "
      << *RawPtr;
  }

  SharedPtr.reset();

  if (RawPtr) {
    std::cout << "\nStill truthy | Value: "
      << *RawPtr;
  }
}
RawPtr is truthy | Value: 42
Still truthy | Value: -572662307

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(){
  auto SharedPtr{std::make_shared<int>(42)};
  int* RawPtr{SharedPtr.get()};
  if (RawPtr) {
    std::cout << "RawPtr is truthy | Value: "
      << *RawPtr;
  }

  SharedPtr.reset();
  RawPtr = nullptr;

  if (RawPtr) {
    std::cout << "\nStill truthy | Value: "
      << *RawPtr;
  } else {
    std::cout << "\nIt's now a nullptr";
  }
}
RawPtr is truthy | Value: 42
It's now a nullptr

But in complex programs, this is not always easy to do. Many pointers to a resource can be stored in many different objects around our application. Rather than trying to manage that manually, it’s much easier and safer to just use a weak pointer.

However, that doesn’t mean we should never use raw pointers. Raw pointers are more performant and flexible, and in short-lived code, such as a simple function call, memory management is often trivial or irrelevant.

Because of this, balancing memory management considerations against code complexity is generally something to decide on a case-by-case basis, and our instincts get better here with experience.

Was this lesson useful?

Next Lesson

Copy Semantics and Return Value Optimization

Learn how to control exactly how our objects get copied, and take advantage of copy elision and return value optimization (RVO)
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Weak Pointers with std::weak_ptr

A full guide to weak pointers, using std::weak_ptr. Learn what they’re for, and how we can use them with practical examples

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Memory Management
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Copy Semantics and Return Value Optimization

Learn how to control exactly how our objects get copied, and take advantage of copy elision and return value optimization (RVO)
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved