yuqi-zheng

C++ Async Safety: Guarding Object Lifetime with `weak_from_this`


This is Part 2 of a series on async callback lifetime safety in C++. Part 1 covers the shared_ptr value-capture pattern for callbacks that must always execute.


The previous article showed how capturing shared_ptr members by value keeps objects alive for the duration of a callback. But sometimes you don’t want the callback to keep the object alive — you want it to run if the object still exists, and silently do nothing if it has already been destroyed.

This is exactly what weak_from_this is for.


The Problem

In event-loop based async code (Boost.Asio, libuv, etc.), a common pattern is scheduling a callback that accesses this:

timer->async_wait([this](const auto& error) {
    this->DoSomething();  // dangerous
});

If the object is destroyed before the timer fires, this is a dangling pointer. The callback fires and crashes.

Capturing [self = shared_from_this()] solves the crash — but introduces a different hazard: the callback now holds a strong reference to the object, which can create a reference cycle if the timer (or its owner) is itself a member of the object. The object never gets destroyed.

The right tool for “run if alive, skip if dead” is weak_ptr.


The Solution: enable_shared_from_this + weak_from_this

class MyRunner : public std::enable_shared_from_this<MyRunner> {
public:
    static std::shared_ptr<MyRunner> Create() {
        return std::shared_ptr<MyRunner>(new MyRunner());
    }

    void StartTimer(boost::asio::deadline_timer& timer) {
        timer.async_wait([weak_self = weak_from_this()](const auto& error) {
            if (auto self = weak_self.lock()) {
                self->DoPeriodicWork();
            }
            // object already destroyed — do nothing, no crash
        });
    }

private:
    MyRunner() = default;
    void DoPeriodicWork() { /* ... */ }
};

Three things are required:

  1. The class inherits std::enable_shared_from_this<T>.
  2. Instances are always managed by shared_ptr (enforced via a factory method — calling weak_from_this() on a stack-allocated object is undefined behavior).
  3. The callback captures weak_from_this(), not this or shared_from_this().

Key Details

Init-capture (C++14)

[weak_self = weak_from_this()]

This C++14 init-capture creates a named weak_ptr copy inside the lambda’s closure. It’s equivalent to:

auto weak_self = weak_from_this();
// lambda captures weak_self by value

The weak_ptr does not increment the reference count, so holding it does not extend the object’s lifetime.

weak_from_this() vs shared_from_this()

MethodReturnsRef countUse when
shared_from_this()shared_ptr<T>+1You need the object to stay alive for the callback
weak_from_this() (C++17)weak_ptr<T>unchangedCallback should silently abort if object is gone

weak_from_this() is a C++17 shorthand. On C++14 you write std::weak_ptr<T>(shared_from_this()).

lock() at call time

if (auto self = weak_self.lock()) {
    self->DoPeriodicWork();
}

lock() atomically checks whether the managed object still exists and, if so, returns a shared_ptr that keeps it alive for the duration of the if block. If the object has been destroyed, lock() returns null and the callback exits cleanly.


Why Not the Other Options?

CaptureExampleProblem
Raw pointer[this]Dangling pointer → crash
Strong reference[self = shared_from_this()]Prevents destruction → memory leak if there’s a cycle
Weak reference[weak_self = weak_from_this()]Safe and leak-free

The strong reference case is subtle. If a timer is a member of the object, and the timer holds a callback that holds a shared_ptr to the object, neither the timer nor the object can ever be destroyed — the reference cycle keeps both alive indefinitely.


Real Example: Ray’s PeriodicalRunner

Ray’s PeriodicalRunner schedules a function to run repeatedly at a fixed interval. It uses exactly this pattern:

class PeriodicalRunner : public std::enable_shared_from_this<PeriodicalRunner> {
    // ...
    void DoRunFnPeriodically(std::function<void()> fn,
                             boost::posix_time::milliseconds period,
                             std::shared_ptr<boost::asio::deadline_timer> timer) {
        timer->async_wait(
            [weak_self = weak_from_this(), fn, period, timer](const auto& error) {
                if (auto self = weak_self.lock()) {
                    if (error == boost::asio::error::operation_aborted) return;
                    fn();
                    self->DoRunFnPeriodically(fn, period, timer);
                }
            });
    }
};

When the caller drops its shared_ptr<PeriodicalRunner>, the next timer callback fires, weak_self.lock() returns null, and the periodic loop stops — no crash, no leak, no explicit cancellation needed.

Note that timer itself is captured by value (as a shared_ptr). This ensures the timer stays alive until the final callback fires and sees that the runner is gone.


Choosing Between shared_ptr and weak_ptr Capture

ScenarioCaptureReason
Cleanup that must run (node disconnect, buffer flush)shared_ptrExtend lifetime, guarantee execution
Periodic work that should stop when owner is goneweak_ptrLet the object die naturally
UI refresh, best-effort notificationweak_ptrSkip silently if view is closed
Timer held as a member of the object itselfweak_ptrAvoid reference cycle

Summary

  • Capturing [this] in any callback that may outlive the object is unsafe.
  • [self = shared_from_this()] prevents the crash but can create reference cycles if the callback indirectly keeps the object alive.
  • [weak_self = weak_from_this()] is the correct pattern when the callback should abort cleanly if the object is already gone.
  • lock() at call time is atomic and safe — if it succeeds, the shared_ptr it returns keeps the object alive for the entire callback.
  • Require factory construction (Create() returning shared_ptr) to enforce the invariant that weak_from_this() is only called on heap-managed objects.

References

  • Ray source: src/ray/util/periodical_runner.cc
  • cppreference: std::enable_shared_from_this
  • Scott Meyers, Effective Modern C++ — Item 19: Use shared_ptr for shared-ownership resource management
  • C++ Core Guidelines R.23: prefer factory functions