Recently I'be been using uvw, a really
wonderful C++ wrapper around libuv. uvw
is designed
around resource handles implemented using
std::shared_ptr
, and the
use of C++ lambdas is encouraged. It's really a nice system, and it sure beats
void*
data callbacks in C event loops.
This is the first time I've used std::shared_ptr
for anything serious, and I
quickly wrote code that leaked memory. Valgrind does a great job of
reporting these leaks, but unfortunately the std::shared_ptr
implementation
does not track actual object graphs, making it difficult to debug circular
references. In this post, I'll share some of what I learned about using these smart pointer types correctly.
Be Careful With Lambda Captures
Callbacks in an event loop typically run much later than where they're defined. This means you must be careful when capturing variables in lambda expressions. If the callbacks are implemented using capture by reference, they will point to an invalid object when called:
void capture_by_reference(uvw::Loop &loop) {
auto conn = loop.resource<uvw::TcpHandle>();
auto timer = loop.resource<uvw::TimerHandle>();
// XXX: capturing using [&] is incorrect
conn->on<uvw::CloseEvent>([&](const auto &, auto &) {
timer->close();
});
};
The problem with the by-reference capture is that timer
exists on the stack,
and will go out of scope when the function exits, even if the underlying
handle still exists due to the std::shared_ptr
. Therefore when the lambda is
actually executed, things will break badly.
If you use a by-value capture, then a copy of the lexically enclosed
std::shared_ptr
object will be made and stored along with the lambda function.
This gives the right semantics: the lambda itself will obtain a reference to the
handle, and the handle's reference count will be decremented when the lambda is
destroyed. Here's an example:
void capture_by_value(uvw::Loop &loop) {
auto conn = loop.resource<uvw::TcpHandle>();
auto timer = loop.resource<uvw::TimerHandle>();
// OK: capture uses [=]
conn->on<uvw::CloseEvent>([=](const auto &, auto &) {
timer->close();
});
// Now timer has a callback to conn, and vice versa...
timer->on<uvw::CloseEvent>([=](const auto &, auto &) {
conn->close();
});
};
However, if you are mixing multiple object types here you can easily run into
circular references that will never be garbage collected. The rules are kind of
complicated here. However, defining two lambdas this way that can refer to each
other can lead to a memory leak, at least under uvw
. The safe thing to do in
this situation is to do a by-value capture of a std::weak_ptr
instead. That
might be something like:
void capture_weak_by_value(uvw::Loop &loop) {
auto conn = loop.resource<uvw::TcpHandle>();
// Create a std::weak_ptr to the connection. weak_from_this() is new
// in C++17, and is enabled on all classes that inherit from
// std::enable_shared_from_this.
auto w_conn = conn->weak_from_this();
auto timer = loop.resource<uvw::TimerHandle>();
auto w_timer = timer->weak_from_this(); // as above
// OK, uses weak_ptr
conn->on<uvw::CloseEvent>([=](const auto &, auto &) {
if (auto t = w_timer.lock()) t->close();
});
// OK, uses weak_ptr
timer->on<uvw::CloseEvent>([=](const auto &, auto &) {
if (auto c = w_conn.lock()) c->close();
});
});
How std::enable_shared_from_this
Works
In the previous example I was able to use weak_from_this()
, which as of C++17
is added to classes that inherit from
std::enable_shared_from_this
.
This class is a bit magical, so I want to explain how it works. I'm using the
GCC/libstdc++ implementation. If you're crazy like me and want to RTFS, most of
it is in this
file,
although there is also related code in shared_ptr_atomic.h
and
shared_ptr_base.h
.
When you subclass std::enable_shared_from_this
a hidden, mutable data member
is added to your class. This member is a weak reference to this
. The standard
calls this member variable weak_this
for the purposes of exposition. In the
GNU implementation, the variable is actually called _M_weak_this
. You can
access the hidden variable from GDB via the underscore name, although generally
this shouldn't be necessary as recent GDB versions should automatically print
reference counts for you.
A quick refresher on how std::shared_ptr
works. When you first create a shared
pointer, a "control block" is created. It holds:
- A pointer back to the original object
- The strong reference count
- The weak reference count
- Optionally, pointers to custom allocators/deallocators.
Note that due to the use of type erasure, information about custom allocators/deallocators is stored in the control block, not as a template/type parameter.
The std::shared_ptr
itself takes up storage space of two words (16 bytes on
64-bit architectures). It holds:
- A pointer to the actual object
- A pointer to the control block
Because the first data member is a pointer to the object, a std::shared_ptr
will decay into a pointer of the base type. This means there is zero overhead
when accessing an object through a std::shared_ptr
, which is pretty cool.
The implementation of std::weak_ptr
works exactly the same way. It's also a
pointer to the object and a pointer to the control block, and decays to the base
pointer type the same way. Pretty neat!
Normally after creating a std::shared_ptr
, the object will have a reference
count of 1 and no weak references. However, if the class inherits from
std::enable_shared_from_this
, it will instead have a reference count of 1 as
well as a weak count of 1. The weak count is due to the implicit weak_this
data member, which is initialized when the object is allocated. The weak_this
data member allows the object to find its own control block. This is what makes
it possible to create a new std::shared_ptr
object that shares the same
control block. This is precisely how shared_from_this()
is implemented for
classes derived from std::enable_shared_from_this
: the method instantiates a
std::shared_ptr
using the implicit weak_this
data member.
Parting Words
I hope this article illustrates that smart pointer classes are very useful, but
also somewhat tricky. You should only use std::shared_ptr
for objects that
genuinely have indeterminate lifetimes, which is usually not the case. There are
of course exceptions, and uvw
is a good example of a library using
std::shared_ptr
objects where it makes sense (due to the nature of how object
lifetimes in event loops work).
Objects that have a single owner should be wrapped with std::unique_ptr
, which
is much simpler (and more efficient). When there's a possibility of creating a
cyclical reference between objects, go back and determine if you really need
the cycle, and then create a std::weak_ptr
if necessary.
As alluded earlier in this article, it is very difficult to debug memory leaks
caused by cyclical references, as there is no object graph being tracked. In a
future article I plan to demonstrate how to implement a poor man's reference
leak checker by scripting GDB to dump information when std::shared_ptr
objects
are created and destroyed.