Notes on std::shared_ptr and std::weak_ptr

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:

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:

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.