C++ Smart Pointers: weak_ptr

Kent - November 14, 2017

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. Please note the thread safety is for the shared pointer itself, when being copied concurrently in different threads. Using shared_ptr will not make the object pointed to automatically thread safe.

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.

<span class="co">// Forward declare structs</span>
<span class="kw">struct</span> A;
<span class="kw">struct</span> B;

<span class=“co”>// Define structures with circular dependencies</span> <span class=“kw”>struct</span> A { B *b; };

<span class=“kw”>struct</span> B { A *a; };

<span class=“co”>// Struct to own the circular dependency</span> <span class=“kw”>struct</span> 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.

<span class="dt">void</span> testRawPtrs()
{
    <span class="co">// Initialize pointers</span>
    A *a = <span class="kw">new</span> A;
    B *b = <span class="kw">new</span> B;
&lt;span class="co">// Circular reference&lt;/span>
a-&gt;b = b;
b-&gt;a = a;

C *c = &lt;span class="kw">new&lt;/span> C;
c-&gt;a = a;

&lt;span class="co">// Memory leak when going out of scope&lt;/span>

}

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.

<span class="ot">#include <memory></span>
<span class="ot">#include <iostream></span>

<span class=“kw”>struct</span> A; <span class=“kw”>struct</span> B;

<span class=“kw”>using</span> A_ptr = std::shared_ptr<A>; <span class=“kw”>using</span> B_ptr = std::shared_ptr<B>;

<span class=“kw”>struct</span> A { B_ptr b; ~A() { std::cout << <span class=“st”>"~A()</span><span class=“ch”>\n</span><span class=“st”>"</span>; } };

<span class=“kw”>struct</span> B { A_ptr a; ~B() { std::cout << <span class=“st”>"~B()</span><span class=“ch”>\n</span><span class=“st”>"</span>; } };

<span class=“kw”>struct</span> C { A_ptr a; ~C() { std::cout << <span class=“st”>"~C()</span><span class=“ch”>\n</span><span class=“st”>"</span>; } };

Testing with a trivial example.

<span class="dt">void</span> testCircularRef()
{
    {
        <span class="kw">auto</span> a = std::make_shared<A>();
        <span class="kw">auto</span> b = std::make_shared<B>();
        <span class="kw">auto</span> c = std::make_shared<C>();
    &lt;span class="co">// Circular reference&lt;/span>
    a-&gt;b = b;
    b-&gt;a = a;

    &lt;span class="co">// Third resource&lt;/span>
    c-&gt;a = a;

    &lt;span class="co">// C is reset&lt;/span>
}

&lt;span class="co">// A and B is unreachable&lt;/span>

}

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.

<span class="kw">struct</span> A;
<span class="kw">struct</span> B;

<span class=“kw”>using</span> A_ptr = std::shared_ptr<A>; <span class=“kw”>using</span> B_ptr = std::shared_ptr<B>;

<span class=“kw”>using</span> A_ref = std::weak_ptr<A>; <span class=“kw”>using</span> B_ref = std::weak_ptr<B>;

<span class=“kw”>struct</span> A { B_ptr b; ~A() { std::cout << <span class=“st”>"~A()</span><span class=“ch”>\n</span><span class=“st”>"</span>; } };

<span class=“kw”>struct</span> B { A_ref a; ~B() { std::cout << <span class=“st”>"~B()</span><span class=“ch”>\n</span><span class=“st”>"</span>; } };

<span class=“kw”>struct</span> C { A_ptr a; ~C() { std::cout << <span class=“st”>"~C()</span><span class=“ch”>\n</span><span class=“st”>"</span>; } };

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.

<span class="dt">void</span> testCircularRef()
{
    {
        <span class="kw">auto</span> a = std::make_shared<A>();
        <span class="kw">auto</span> b = std::make_shared<B>();
        <span class="kw">auto</span> c = std::make_shared<C>();
    &lt;span class="co">// Circular reference&lt;/span>
    a-&gt;b = b;
    b-&gt;a = a;

    &lt;span class="co">// Third resource&lt;/span>
    c-&gt;a = a;

    &lt;span class="co">// C is reset&lt;/span>
}

}

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

See Also

Comments

Any comments? Create a new discussion on GitHub.
There used to be an inline comment form here, but it was removed.