MiddleDebuggingOccasionalNot answered yet
How can a coroutine leak memory, and how do you prevent it?
fibonacci() is an infinite generator coroutine. Running this code under a leak sanitizer reports a leaked coroutine frame every call.
Generator fibonacci(); // infinite generator, final_suspend = suspend_always
void buggy() {
std::coroutine_handle<promise_type> h = fibonacci().handle;
h.resume();
}
Identify the bug and explain the cause.
coroutine_handle does not own the frame. If you lose the handle to an unfinished coroutine, or never call destroy(), the frame leaks. Prevent it by wrapping the handle in a RAII type whose destructor calls destroy(), and follow proper move semantics so ownership is unique.
- ✗Believing
coroutine_handleowns or reference-counts the frame - ✗Copying a handle so two owners both call
destroy()— a double free - ✗Thinking
final_suspendreturningsuspend_alwayscleans up by itself
- →What goes wrong if
final_suspendreturnssuspend_never? - →How do you detect a leaked coroutine frame with a sanitizer?
Contents
The leak: a lost handle
Generator fibonacci(); // infinite generator, final_suspend = suspend_always
void buggy() {
std::coroutine_handle<promise_type> h = fibonacci().handle;
h.resume();
// h goes out of scope — destroy() is never called.
// The frame of the infinite coroutine is NEVER freed → leak.
}
A coroutine_handle is just a pointer. It does not reference-count and does not call destroy() for you. If the coroutine never finishes (or final_suspend returns suspend_always), the frame lives until someone explicitly calls destroy().
The fix: a RAII wrapper with unique ownership
struct Generator {
std::coroutine_handle<promise_type> handle;
Generator(Generator&& o) noexcept
: handle(std::exchange(o.handle, {})) {} // transfer ownership
Generator(const Generator&) = delete; // forbid copying
~Generator() { if (handle) handle.destroy(); } // frame freed exactly once
};
Key rules:
- the wrapper's destructor calls
destroy()— the frame is freed deterministically; - copying is deleted and move nulls the source — no double
destroy(); - the
if (handle)guard prevents callingdestroy()on a moved-from empty handle.
Contents