One of the varieties of smart pointers is std::weak_ptr. It is a kind of superstructure over shared_ptr. std::weak_ptr is a smart pointer that holds a non-owning ("weak") reference to an object that is managed by std::shared_ptr. Unlike std::shared_ptr, a std::weak_ptr does not participate in the ownership of the object it references, meaning it does not affect the reference count of the shared object.
std::weak_ptr is like having a temporary pass to a club. You can access the club as long as it is open, but you don't have the power to keep it open. If the club closes, your pass becomes invalid, and you can't access the club anymore.
std::weak_ptr
std::weak_ptr does not participate in the ownership of the object, it simply references it and this feature allows weak_ptr to break cyclic references, which can cause memory leaks in structures such as doubly-linked lists or graphs where two objects reference each other using std::shared_ptr.
Here are some useful methods provided by std::weak_ptr:
expired(): - this method checks whether the object managed by thestd::weak_ptrhas been deleted or not. It returns true if thestd::shared_ptrthat shares ownership of the object's memory has been destroyed or if there were nostd::shared_ptrinstances that owned the object to begin with. Otherwise, it returns false. This is useful for checking if the object is still accessible before attempting to access it.lock()method is used withstd::weak_ptrin C++ to safely access an object if it still exists. It tries to make astd::shared_ptrfrom astd::weak_ptr. If the object is still around (meaning there's astd::shared_ptrmanaging it),lock()will give you a newstd::shared_ptrto work with that object. If the object is gone (nostd::shared_ptrmanaging it),lock()returns an emptystd::shared_ptr. This way, you can avoid accessing objects that no longer exist.
int main() {
std::weak_ptr<int> weakPtr;
{
auto sharedPtr = std::make_shared<int>(42); // Create a shared_ptr
weakPtr = sharedPtr;
auto lockedSharedPtr = weakPtr.lock();
if (lockedSharedPtr)
std::cout << "Locked value: " << *lockedSharedPtr << std::endl;
} // sharedPtr goes out of scope and is destroyed here.
auto lockedSharedPtrAfter = weakPtr.lock(); // Try to lock weakPtr again after sharedPtr is destroyed.
if (!lockedSharedPtrAfter)
std::cout << "The object has been destroyed and cannot be locked." << std::endl;
return 0;
}The result of our example work:
Locked value: 42
The object has been destroyed and cannot be locked.use_count(): - this method returns the number ofstd::shared_ptrinstances that currently share ownership of the object. This can be useful for diagnostics or validating the state of ownership.reset(): - this method resets thestd::weak_ptr, breaking the existing weak reference to an object. After callingreset(), thestd::weak_ptrwill be empty (i.e., not pointing to any object), andexpired()will return true. This can be useful when you explicitly want to release a weak reference before theshared_ptr's lifetime ends.
Class Book
To illustrate std::weak_ptr, let's implement an extended version of the library management system by introducing a function where a book can optionally remember the last visitor who borrowed it (the book can store a reference to the last visitor).
However, to prevent a circular reference (where a Book has a std::shared_ptr to a Patron, and a Patron has a std::shared_ptr to a Book), we'll use std::weak_ptr for the back-reference from the book to the patron.
First of all, let's implement the Book class:
// #include all header files and libraries
class Patron; // Forward declaration
class Book {
private:
std::string title;
std::string author;
public:
std::weak_ptr<Patron> lastPatron; // Weak reference to avoid ownership
Book(const std::string& title, const std::string& author)
: title(title), author(author) {}
//Here we declare a method, its implementation will be below the Patron class definition.
void display() const;
~Book() {
std::cout << "Destroying Book: " << title << std::endl;
}
};There are two key things we should be looking at:
We create a smart pointer
lastPatronof typePatron, however at the moment the classPatronis not declared or described. We can put thePatronclass before theBookclass, however we will run into a similar problem for the book class. So we use the "Forward declaration" for thePatronclass.We only specified the prototype of the
display()method. We want to check the owners in it and output the last user. Here is how ourdisplay()method could look like:
void display() const {
std::cout << "Title: " << title << ", Author: " << author;
// Use std::weak_ptr::expired() to check if the referenced Patron object has been deleted
if (!lastPatron.expired()) {
auto patronPtr = lastPatron.lock(); // Safely acquire a shared_ptr if the Patron still exists
std::cout << ", Last Borrowed By: " << patronPtr->name;
} else {
std::cout << ", Last Patron's info is expired or not set";
}
std::cout << std::endl;
}Here we are trying to use the name from the Patron(patronPtr->name) class, however, as we already know the implementation of the class has not been presented yet. Therefore, if we try to implement the display() method inside the Book class, we will get an error:
member access into incomplete type 'std::shared_ptr<Patron>::element_type' (aka 'Patron')This happens because the compiler scans our program sequentially and when it reaches the line std::cout << ", Last Borrowed By: " << patronPtr->name; it does not know about the implementation of the Patron class.
The solution to this problem is quite simple, we simply put the implementation of the display() method behind the implementation of the Patron class:
// #include all header files and libraries
class Patron; // Forward declaration
class Book {
// Full implementation of the class
};
class Patron{
// Full implementation of the class
};
// Now that Patron is fully defined, we can define Book::display
void Book::display() const {
std::cout << "Title: " << title << ", Author: " << author;
// Use std::weak_ptr::expired() to check if the referenced Patron object has been deleted
if (!lastPatron.expired()) {
auto patronPtr = lastPatron.lock(); // Safely acquire a shared_ptr if the Patron still exists
std::cout << ", Last Borrowed By: " << patronPtr->name;
} else {
std::cout << ", Last Patron's info is expired or not set";
}
std::cout << std::endl;
}Class Patron
Let's implement our Patron class:
class Patron : public std::enable_shared_from_this<Patron> {
public:
std::string name;
std::vector<std::shared_ptr<Book>> borrowedBooks;
Patron(const std::string& name) : name(name) {}
void borrowBook(std::shared_ptr<Book>& book) {
borrowedBooks.push_back(book);
book->lastPatron = shared_from_this(); // Sets this Patron as the last one to borrow the book
}
void showBorrowedBooks() const {
std::cout << name << " has borrowed:" << std::endl;
for (const auto& book : borrowedBooks) {
book->display();
}
}
};The use of std::enable_shared_from_this in the Patron class facilitates obtaining a std::shared_ptr to the current object from within member functions of the object itself. This is particularly useful when you need to pass a std::shared_ptr to the current object to other functions or objects that store std::shared_ptrs, ensuring that the reference count is maintained correctly.
When a Patron borrows a book, the Book class stores a weak pointer (std::weak_ptr) back to the Patron to avoid circular references, which would prevent the automatic cleanup of resources. However, to set this weak pointer, the Book needs a std::shared_ptr to the Patron. Directly creating a new std::shared_ptr to this inside a member function (e.g., std::shared_ptr<Patron>(this)) is incorrect and dangerous because it creates a new control block for the shared pointer, leading to multiple control blocks for the same raw pointer, which can cause the object to be deleted more than once.
By inheriting from std::enable_shared_from_this, a class provides its instances the ability to safely generate additional std::shared_ptr instances to this, all sharing the same control block as any existing std::shared_ptr instances that manage the this pointer. This ensures that all std::shared_ptr instances referring to the object cooperate in the reference count management.
In the Patron class, when a book is borrowed, we want the book to keep a weak reference to the patron. To do this correctly, we call shared_from_this() to get a std::shared_ptr that shares ownership with any existing std::shared_ptrs that own *this:
void borrowBook(std::shared_ptr<Book>& book) {
borrowedBooks.push_back(book);
// Sets this Patron as the last one to borrow the book
book->lastPatron = shared_from_this();
}The Book object can keep a weak reference to the Patron that last borrowed it without interfering with the ownership and lifetime of the Patron object. The lifetime of the Patron object is properly managed, with all std::shared_ptr instances to a given Patron sharing the same control block, thus preventing premature destruction or double deletion of the Patron object.
In summary, std::enable_shared_from_this is used to safely manage shared ownership and weak references in scenarios where objects need to be able to provide std::shared_ptrs to themselves without violating the principles of safe memory management in C++.
Full program
Let's now add the code for the main() function and combine all our pieces into a single working program that allows us to manage and track our library:
#include <iostream>
#include <string>
#include <memory>
#include <vector>
class Patron; // Forward declaration
class Book {
private:
std::string title;
std::string author;
public:
std::weak_ptr<Patron> lastPatron; // Weak reference to avoid ownership
Book(const std::string& title, const std::string& author)
: title(title), author(author) {}
//Here we declare a method, its implementation will be below the Patron class definition.
void display() const;
~Book() {
std::cout << "Destroying Book: " << title << std::endl;
}
};
class Patron : public std::enable_shared_from_this<Patron> {
public:
std::string name;
std::vector<std::shared_ptr<Book>> borrowedBooks;
Patron(const std::string& name) : name(name) {}
void borrowBook(std::shared_ptr<Book>& book) {
borrowedBooks.push_back(book);
book->lastPatron = shared_from_this(); // Sets this Patron as the last one to borrow the book
}
void showBorrowedBooks() const {
std::cout << name << " has borrowed:" << std::endl;
for (const auto& book : borrowedBooks) {
book->display();
}
}
};
// Now that Patron is fully defined, we can define Book::display
void Book::display() const {
std::cout << "Title: " << title << ", Author: " << author;
// Use std::weak_ptr::expired() to check if the referenced Patron object has been deleted
if (!lastPatron.expired()) {
auto patronPtr = lastPatron.lock(); // Safely acquire a shared_ptr if the Patron still exists
std::cout << ", Last Borrowed By: " << patronPtr->name;
} else {
std::cout << ", Last Patron's info is expired or not set";
}
std::cout << std::endl;
}
int main() {
auto book1 = std::make_shared<Book>("The Great Gatsby", "F. Scott Fitzgerald");
auto book2 = std::make_shared<Book>("1984", "George Orwell");
auto alice = std::make_shared<Patron>("Alice");
alice->borrowBook(book1);
alice->borrowBook(book2);
auto bob = std::make_shared<Patron>("Bob");
bob->borrowBook(book1); // Both Alice and Bob share book1
alice->showBorrowedBooks();
bob->showBorrowedBooks();
// Display book details to show the last patron
book1->display();
book2->display();
return 0;
}The result of executing our code:
Alice has borrowed:
Title: The Great Gatsby, Author: F. Scott Fitzgerald, Last Borrowed By: Bob
Title: 1984, Author: George Orwell, Last Borrowed By: Alice
Bob has borrowed:
Title: The Great Gatsby, Author: F. Scott Fitzgerald, Last Borrowed By: Bob
Title: The Great Gatsby, Author: F. Scott Fitzgerald, Last Borrowed By: Bob
Title: 1984, Author: George Orwell, Last Borrowed By: Alice
Destroying Book: 1984
Destroying Book: The Great GatsbyReasons not to use raw pointers
We have looked at all the types of smart pointers used in modern C++ programming, let's take a look at the key features that make it definitely worth not using raw pointers:
Its declaration does not specify whether it points to a single object or to an array.
Its declaration says nothing about whether you need to destroy what it points to. What it points to when you are done using it, that is, whether the pointer owns what it points to.
If you determine that you need to destroy what the pointer points to, there is no way to tell how to do it. Should you use
delete, or is there another destruction mechanism (such as a special destruction function to which you should pass the pointer)?If you were able to figure out that you should use
delete, Reason 1 means that you cannot determine whether you should use the single-object form ("delete") or the array form ("delete []"). If you use the wrong form, the results will be undefined.Assuming that a pointer owns what it points to and knowing how to destroy it, it is hard to guarantee that you perform the destruction exactly once along all paths in the code (including those associated with exceptions). Skipping a path leads to resource leaks, and performing destruction more than once leads to undefined behavior.
There is usually no way to determine that a pointer is "dangling," that is, pointing to memory that no longer holds the object it was supposed to point to. Dangling pointers occur when objects are destroyed and pointers continue to point to them.
Conclusion
In C++, managing memory efficiently is crucial to avoid errors and leaks. std::weak_ptr is a tool that helps programmers do just that, without taking full control or ownership of the memory it points to.
This is like having a key to a shared space that doesn't let you change the locks. It's particularly useful in preventing problems where programs keep running in circles, holding onto memory they don't need anymore.
By understanding and using std::weak_ptr, programmers can write cleaner, safer code that's less prone to bugs and easier to manage, moving away from older, riskier ways of handling memory with raw pointers.