std::enable_shared_from_this

2023-03-17

C++ is a language which, unsurprisingly, leaves lifetime management entirely up to the programmer. The pros and cons of this design are a matter of heated debate, but that’s not what this post is about. This post is to address a feature of C++11 that I have not seen many people use (which is probably a good thing, but it has its place): std::enable_shared_from_this.

C++11 added smart pointers which gave us a way to replace C pointers with reference-counted pointers with lifetime management. Instead of passing around MyType* and just assuming nobody deletes it while someone else is using it, we can now pass around std::shared_ptr<MyType> and the object pointed to will live as long as someone has a copy of this pointer. This also solves the issue of exception safety with dynamically-allocated objects:

void Bad() {
  MyType* obj = new MyType();

  if (!obj->Process()) {
    // Uh-oh! obj will never be deleted!
    throw std::runtime_error("panic!");
  }

  delete obj;
}

void Good() {
  auto obj = std::make_unique<MyType>();

  if (!obj->Process()) {
    // No bug! Regardless of *how* we exit this scope, obj will be deleted.
    throw std::runtime_error("panic!");
  }
}

This is a very useful feature of smart pointers. However, ownership semantics are an often-overlooked feature of smart pointers which have some more contrived uses. Let’s look at a more complex example. In this case, we will have a producer and a consumer. The consumer will register itself as a consumer with the producer and give the producer a callback for it to asynchronously update the consumer. For those of you familiar with Qt, this will look suspiciously similar to signals and slots. Let’s start by mocking up the producer class, which will take a callback function to handle new data and has a function to unregister the callback function. The producer class will spin off some thread asynchronously to produce values and call the callback function when a new value is available. I’ve intentionally omitted the implementation of this class, but this should be enough to get the gist.

class Producer {
 public:
  using ConsumerCallbackType = std::function<void(int)>;

  /* ... */

  void SetCallback(ConsumerCallbackType cb);
  void RemoveCallback();

  /* ... */
};

Now we need a class to consume this value. Let’s look at a naive implementation, then explore what makes this design buggy.

class BadConsumer {
 public:
  explicit BadConsumer(std::shared_ptr<Producer> prod)
      : prod_{prod}, count_{} {}

  ~BadConsumer() {
    if (prod_) {
      prod_->RemoveCallback();
    }
  }

  void Connect() {
    if (prod_) {
      prod_->SetCallback([this](int val) {
        // Simulate doing some long work with this data:
        count_ += val;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::printf("count_: %d\n", count_);
      });
    }
  }

 private:
  std::shared_ptr<Producer> prod_;
  int count_;
};

So far so good. Let’s quickly polish this off with a basic implementation:

int main(void) {
  auto producer = std::make_shared<Producer>();

  {
    BadConsumer consumer{producer};

    // Wait a little bit...
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
  }

  return 0;
}

This program looks like it should work just fine, right? However, remember that consumer will be destroyed before the producer. This means that there is a moment where you can have a race condition: producer may begin a call to consumer’s callback as consumer is deleted. This means that accessing consumer is suddenly unsafe. We need a way to extend the lifetime of consumer for as long as either main() or producer needs access to that object. The obvious choice here is to use a shared pointer for consumer and then the caller and the producer can both own consumer and extend its lifetime as long as either of them need it.

However, once you think about how to implement this, things get a little wonky. An object cannot do something like std::shared_ptr<Consumer>(this). Remember that the outside world has access to the existing shared pointer to the consumer, but it itself would need a copy of this pointer to be able to get a shared pointer to itself. If only there was some way we could implement this without a bunch of annoying boilerplate! Enter std::enable_shared_from_this.

Using std::enable_shared_from_this, our class will store an internal weak pointer to this. When we use the constructor of std::shared_ptr, it will detect that the class has a std::enable_shared_from_this base and will assign the shared pointer from the internal weak pointer. The class can also now get a shared pointer to itself using the shared_from_this() function inherited from the base. The only restriction is that for this to work, the class must always be owned by a std::shared_ptr. If you instantiate the class without it, the weak pointer inside will have a reference count of 0 and therefore shared pointers to this will no longer maintain the lifetime of the object.

Let’s look at how we can implement all of this and create a new good consumer class which works much better:

/* Note: inheritance must be public here! */
class GoodConsumer : public std::enable_shared_from_this<GoodConsumer> {
 private:
  explicit GoodConsumer(std::shared_ptr<Producer> prod)
      : prod_{prod}, count_{} {}

 public:
  [[nodiscard]]
  static std::shared_ptr<GoodConsumer> Create(
      std::shared_ptr<Producer> prod) {
    // Sadly we cannot use the superior std::make_shared here since the
    // constructor is private, but that's the price we pay.
    return std::shared_ptr<GoodConsumer>{new GoodConsumer{prod}};
  }

  ~GoodConsumer() {
    if (prod_) {
      prod_->RemoveCallback();
    }
  }

  // Note that it's not safe to call this from the CTOR since the base
  // class may not have been initialized yet.
  void Connect() {
    if (prod_) {
      auto weak_self = weak_from_this();
      prod_->SetCallback([weak_self](int val) {
        // Grab a shared pointer to this if possible. This is thread safe,
        // so as long as this succeeds, we can guarantee that this object
        // will live at least as long as we hold this pointer.
        if (auto self = weak_self.lock()) {
          // Simulate doing some time-consuming processing of the data:
          self->count_ += val;
          std::this_thread::sleep_for(std::chrono::milliseconds(500));
          std::printf("count_: %d\n", self->count_);
        }
      });
    }
  }

 private:
  std::shared_ptr<Producer> prod_;
  int count_;
};

This may look really scary, but it’s actually not that bad. First, we solved the problem of requiring the class to be owned by a shared pointer. This is achieved by making the constructor private and making a factory function which returns a shared pointer. This is a really weird design and may smell bad (which is probably a good thing), but it isn’t horrible to work with and handles the issue effectively.

The major change here is that callback function. This time, we start by getting a weak pointer to self (weak_self). We capture this pointer in the lambda and start by attempting to lock the weak pointer. This function atomically increments the reference count of the object and returns an owning shared pointer. If the object has already been destroyed, this function will return a null pointer. This is a thread-safe way to get an owning pointer to this object. As long as this pointer exists, the object does, too.

To tie it all together, let’s look at the final implementation of our main function:

int main(void) {
  auto producer = std::make_shared<Producer>();

  {
    auto consumer = GoodConsumer::Create(producer);

    // Sleep for 200ms. Let's assume this is a perfect world and in this
    // perfect world, this puts us precisely 200ms into the 500ms sleep
    // in the consumer callback function when this thread wakes up.
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    // Now that we're 200ms into the consumer's callback, consumer goes
    // out of scope--but it is not deleted yet! The thread executing the
    // callback still has that temporary shared pointer which owns the
    // consumer...
  }

  // ...and ~300ms later, consumer is finally deleted, safely avoiding
  // disaster in the producer thread.

  return 0;
}

Now that we gave the producer temporary ownership of the consumer during its callback execution, the consumer will not be destroyed until after the callback is completed. All of our problems are solved!


I get that this example may seem contrived, but this is not an uncommon situation. I mentioned Qt briefly earlier, but a UI framework is not the only place where things like this happen. Many applications these days have a lot of IO going on and use some form of IO thread to asynchronously wait for data. This thread will then call some callback function to handle the data when it’s received. In a situation like this, you could certainly run into issues like the above–especially when the consumer is temporary. You may have a few consumers which are added and removed over time while the producer is still working, and it’s absolutely possible (and probable!) that this sort of lifetime issue will arise. Boost.asio’s example code even uses a similar setup (albeit using shared_from_this(), as the example was written before C++17 introduced weak_from_this()).