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 struct
s, 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;
<span class="co">// Circular reference</span>
a->b = b;
b->a = a;
C *c = <span class="kw">new</span> C;
c->a = a;
<span class="co">// Memory leak when going out of scope</span>
}
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>();
<span class="co">// Circular reference</span>
a->b = b;
b->a = a;
<span class="co">// Third resource</span>
c->a = a;
<span class="co">// C is reset</span>
}
<span class="co">// A and B is unreachable</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.
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>();
<span class="co">// Circular reference</span>
a->b = b;
b->a = a;
<span class="co">// Third resource</span>
c->a = a;
<span class="co">// C is reset</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
- In multi-threaded systems
shared_ptr
andweak_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 ornull
. 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 aweak_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 usingunique_ptr
for each of the children, and each child can use a raw pointer to the parent.
See Also
- - C++ Hello World with Classes
- - C++ Hello World
- - Default explicit equality operator
- - Mistakes
- - compile_commands.json with CMake