Weak_Ptr in C++


This article and the examples are from the Microsoft C++ Documentation and the LearnCPP.

Sometimes an object must store a way to access the underlying object of a shared_ptr without causing the reference count to be incremented. Typically, this situation occurs when you have cyclic references between shared_ptr instances. In that case, use weak_ptr to give one or more of the owners a weak reference to another shared_ptr. By using a weak_ptr, you can create a shared_ptr that joins to an existing set of related instances, but only if the underlying memory resource is still valid.


Cyclic Reference

A cyclic reference is a series of references where each object references the next, and the last object references back to the first, causing a referential loop. The references do not need to be actual C++ references - they can be pointers, unique IDs, or any other means of identifying specific objects.

Consider the following example, where the shared pointers in two separated objects each point at the other object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Person {
    string m_name;
    shared_ptr<Person> m_partner; // initially created empty

public:
    Person(const string &name): m_name(name) {
        cout << m_name << " created\n";
    }

    ~Person() {
        cout << m_name << " destroyed\n";
    }

    friend bool partnerUp(shared_ptr<Person> &p1, shared_ptr<Person> &p2) {
        if (!p1 || !p2) return false;
        p1->m_partner = p2;
        p2->m_partner = p1;

        cout << p1->m_name << " is now partnered with " << p2->m_name << endl;
        return true;
    }
};

int main() {
    auto lucy = make_shared<Person>("Lucy"); // create a Person named "Lucy"
    auto ricky = make_shared<Person>("Ricky"); // create a Person named "Ricky"

    partnerUp(lucy, ricky);
    return 0;
}

In this example, we dynamically allocate two Persons, “Lucy” and “Ricky” using make_shared(). “Lucy” and “Ricky” are destroyed at the end of main(). Then we partner them up. This sets the shared_ptr inside “Lucy” to point at “Ricky”, and the shared_ptr inside “Ricky” to point at “Lucy”.

The output of this program is:

1
2
3
Lucy created
Ricky created
Lucy is now partnered with Ricky

This is not what we expect. No deallocation took place. What happened?

At the end of main(), the ricky shared pointer goes out of scope first. When that happens, ricky checks if there are any other shared pointers that co-own the Person “Ricky”. There are Lucy’s m_partner. Because of this, it does not deallocate “Ricky”; otherwise, Lucy’s m_partner would end up as a dangling pointer. At this point, we now have one shared pointer to “Ricky” (Lucy’s m_partner) and two shared pointers to “Lucy” (Lucy, and Ricky’s m_partner).

Next the Lucy shared pointer goes out of scope, and the same thing happens. The shared pointer Lucy checks if there are anyother shared pointers co-owning the Person “Lucy”. There are Ricky’s m_partner, so Lucy is not deallocated. At this point, there is one shared pointer to “Lucy” (Ricky’s m_partner) and one shared pointer to “Ricky” (Lucy’s m_partner).

Then the program ends - and neither “Lucy” or “Ricky” have been deallocated. Essentially, “Lucy” ends up keeping “Ricky” from being destroyed, and “Ricky” ends up keeping “Lucy” from being destroyed.


Weak Pointer

The weak_ptr was designed to solve the cyclical ownership problem described above. A weak_ptr is an observer - it can observe and access the same object as a shared_ptr (or other weak_ptrs) but it is not considered an owner. When a shared pointer goes out of scope, it only considers whether other shared_ptr are co-owning the object, and weak_ptr does not count.

The downside of weak_ptr is that weak_ptr are not directly usable (they have no operator ->). To use a weak_ptr, you must first convert it into a shared_ptr. Then you can use the shared_ptr. To convert a weak_ptr into a shared_ptr, you can use the lock() member function.

Let’s rewrite the above example with weak pointers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Person {
    string m_name;
    weak_ptr<Person> m_partner; // This is now a weak_ptr

public:
    Person(const string &name): m_name(name) {
        cout << m_name << " created\n";
    }

    ~Person() {
        cout << m_name << " destroyed\n";
    }

    friend bool partnerUp(shared_ptr<Person> &p1, shared_ptr<Person> &p2) {
        if (!p1 || !p2) return false;
        p1->m_partner = p2;
        p2->m_partner = p1;

        cout << p1->m_name << " is now partnered with " << p2->m_name << endl;
        return true;
    }

    // Use lock() to convert a weak_ptr to a shared_ptr
    const shared_ptr<Person> getPartner() const { return m_partner.lock(); }

    const string& getName() const { return m_name; }
};

int main() {
    auto lucy = make_shared<Person>("Lucy"); // create a Person named "Lucy"
    auto ricky = make_shared<Person>("Ricky"); // create a Person named "Ricky"

    partnerUp(lucy, ricky);

    auto partner = ricky->getPartner(); // get shared_ptr to Ricky's partner
    cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';

    return 0;
}

The output of the above example is:

1
2
3
4
5
6
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky's partner is: Lucy
Ricky destroyed
Lucy destroyed

Now when Ricky goes out of scope, it sees that there are no other shared_ptr pointing at “Ricky” (the weak_ptr from “Lucy” doesn’t count). Therefore, it will deallocate “Ricky”. The same occurs for Lucy.

We don’t have to worry about circular dependencies with shared_ptr variable “partner” since it’s just a local variable inside the function. It will eventually go out of scope at the end of the function and the reference count will be decremented by 1.




Related Posts

Weak_Ptr in C++

Sometimes an object must store a way to access the...

Unique_Ptr in C++

A unique_ptr does not share its pointer. It cannot be...

Smart Pointers in C++

In modern C++ programming, the Standard Library includes smart pointers,...

Shared_Ptr in C++

The shared_ptr type is a smart pointer that is designed...