C++ Smart Pointers: weak_ptr

By , last updated May 13, 2018

C++ smart pointers are pointers that wrap raw C++ pointers. There are several types of smart pointers in C++.

This article will be about the weak pointer (std::weak_ptr) in the C++ standard that is a type of a smart pointer. It’s closely related to the shared pointer (std::shared_ptr) that is also a type of C++ smart pointers.

Other types are unique_ptr and scoped_ptr. There was also an auto_ptr, but it was removed in C++17.

A shared pointer is shared ownership of a resource in C++. It can be memory (pointer to heap), a file handle or something else. Anyone who holds a copy of the shared pointer, partakes in the ownership. When all copies of the pointer go out of scope, the last one clean up the resource. If it’s a memory, the memory is deleted. If it’s a file, the file is closed.

Another great thing with std::shared_ptr is that it’s designed to be thread safe. This solves the dangling pointer problem in an elegant way.

Weak Pointer (weak_ptr) in C++

Weak pointer (weak_ptr) is a smart pointer, and closely related to the shared pointer. It holds a weak reference to an object that is managed by a shared pointer (shared_ptr). A weak pointer must be converted to shared_ptr in order to access the referenced object.

The way shared_ptr works is by having a reference count. When you copy the shared pointer, the reference count will increase. When a shared pointer goes out of scope, the reference count is decreased. If the reference count goes to 0, the object is released.

Using a weak_ptr will not increase the reference count. A weak_ptr will only return a non-null shared_ptr when there is a shared_ptr somewhere keeping the object alive.

Circular references

To better understand what weak_ptr is useful for, have a look at the following examples. One of the most useful purposes with std::weak_ptr is to avoid circular dependencies when using smart pointers.

Raw pointers example

This example uses raw pointers to create a circular dependency between two objects. Please don’t do this at home or at work, using raw pointers.

Given the following structs, we can define circular dependencies where A and B point to each other, and C points to A, keeping the dependency alive.

// Forward declare structs
struct A;
struct B;

// Define structures with circular dependencies
struct A
{
    B *b;
};

struct B
{
    A *a;
};

// Struct to own the circular dependency
struct C
{
    A *a;
};

When using this class, it will leak memory when going out of scope. All three pointers will be leaked. The type of memory leak will be detectable by a memory leak analyser, and will be reported as definitely lost.

void testRawPtrs()
{
    // Initialize pointers
    A *a = new A;
    B *b = new B;

    // Circular reference
    a->b = b;
    b->a = a;

    C *c = new C;
    c->a = a;

    // Memory leak when going out of scope
}

Shared pointer example

With the same construct, but with shared pointers. The problem is the same. There will be a memory leak when the pointers go out of scope. This is a common pitfall with std::shared_ptr, when two objects keep each other alive, with no one having a pointer to either of them.

Using a using to make types makes typing easier.

#include <memory>
#include <iostream>

struct A;
struct B;

using A_ptr = std::shared_ptr<A>;
using B_ptr = std::shared_ptr<B>;

struct A
{
    B_ptr b;
    ~A() { std::cout << "~A()\n"; }
};

struct B
{
    A_ptr a;
    ~B() { std::cout << "~B()\n"; }
};

struct C
{
    A_ptr a;
    ~C() { std::cout << "~C()\n"; }
};

Testing with a trivial example.

void testCircularRef()
{
    {
        auto a = std::make_shared<A>();
        auto b = std::make_shared<B>();
        auto c = std::make_shared<C>();

        // Circular reference
        a->b = b;
        b->a = a;

        // Third resource
        c->a = a;

        // C is reset
    }

    // A and B is unreachable
}

At this point, there will be a memory leak. But it will probably not be reported as definitely lost by tools. It depends on the tool to report this leak.

The output is:

~C()

Only C is released, while A and B is unreachable. While the variables a, b and c are released, only C is properly cleaned up. A and B keeps each other alive. When c is cleaned up, the only reference to A is severed. The memory will be kept until the program is terminated.

Weak pointer from shared example

Using a weak pointer as a reference solves the problem with unreachable memory. As usual, using will save typing and prevent errors later.

struct A;
struct B;

using A_ptr = std::shared_ptr<A>;
using B_ptr = std::shared_ptr<B>;

using A_ref = std::weak_ptr<A>;
using B_ref = std::weak_ptr<B>;

struct A
{
    B_ptr b;
    ~A() { std::cout << "~A()\n"; }
};

struct B
{
    A_ref a;
    ~B() { std::cout << "~B()\n"; }
};

struct C
{
    A_ptr a;
    ~C() { std::cout << "~C()\n"; }
};

Structs A and B have a reference to each other, while C have the full ownership pointer to A. Note the slight difference where B points to A_ref.

void testCircularRef()
{
    {
        auto a = std::make_shared<A>();
        auto b = std::make_shared<B>();
        auto c = std::make_shared<C>();

        // Circular reference
        a->b = b;
        b->a = a;

        // Third resource
        c->a = a;

        // C is reset
    }

}

At the end of the scope, all three structures are deleted and there are no leaks.

Output is:

~C()
~A()
~B()

Why is the order C, A and B? The order is not random. Resources are released in a FILO-style, where the last acquired is the first to be released. But why CAB and not CBA?

The first one to be released is C. That is expected. To understand why A is released before B, one has to look at the use count and where they are referenced. The code block and C holds a shared_ptr to A, and A holds a shared_ptr to B. B on the other hand, only has a weak_ptr to A. This allows A to be deleted even though B has a (weak) reference to it.

Other uses

  • In multi-threaded systems shared_ptr and weak_ptr are of tremendous value. They assure the lifetime of an object is deterministic. They prevent the dangling pointer problem, where a pointer points to an invalid location. It’s either valid memory or null. Behaviors for both are well defined.
  • Recently used cache. A weak_ptr can be used to implement a recent used cache, while not holding the object alive itself.
  • Parent-child-parent references. A parent holds a shared_ptr to all the children, while each child has a weak_ptr to access the parent again. This avoids the circular reference problem. When the parent is released, and the children are released too without keeping the parent alive. This problem can also be solved by using unique_ptr for each of the children, and each child can use a raw pointer to the parent.

Professional Software Developer, doing mostly C++. Connect with Kent on Twitter.

Comments

Be the first to comment.

Leave a Reply


You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*