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:
- The class inherits
std::enable_shared_from_this<T>. - Instances are always managed by
shared_ptr(enforced via a factory method — callingweak_from_this()on a stack-allocated object is undefined behavior). - The callback captures
weak_from_this(), notthisorshared_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()
| Method | Returns | Ref count | Use when |
|---|---|---|---|
shared_from_this() | shared_ptr<T> | +1 | You need the object to stay alive for the callback |
weak_from_this() (C++17) | weak_ptr<T> | unchanged | Callback 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?
| Capture | Example | Problem |
|---|---|---|
| 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
| Scenario | Capture | Reason |
|---|---|---|
| Cleanup that must run (node disconnect, buffer flush) | shared_ptr | Extend lifetime, guarantee execution |
| Periodic work that should stop when owner is gone | weak_ptr | Let the object die naturally |
| UI refresh, best-effort notification | weak_ptr | Skip silently if view is closed |
| Timer held as a member of the object itself | weak_ptr | Avoid 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, theshared_ptrit returns keeps the object alive for the entire callback.- Require factory construction (
Create()returningshared_ptr) to enforce the invariant thatweak_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_ptrfor shared-ownership resource management - C++ Core Guidelines R.23: prefer factory functions