Articles
C++ Coroutine Interview Questions: Suspension, Ownership, and Dangling Frames
Practice C++ coroutine interview questions with suspension traces. Explain frame ownership, captured lifetimes, cancellation, and safe destruction boundaries.
C++ coroutine interview questions are usually ownership questions hidden behind asynchronous syntax. A coroutine can suspend while its frame remains alive, yet still hold a reference to an object that has already been destroyed. The frame's survival does not extend the lifetime of everything it points to.
The strongest answer names three things separately: the coroutine's current execution state, the owner responsible for destroying its frame, and the lifetime of any external data or callbacks. co_await does not choose a thread, make captured references safe, or define cancellation by itself.
This article is for C++ candidates who need to trace those boundaries in an interview. The language baseline is C++20; task types and schedulers are library contracts, not a universal standard coroutine runtime.
What survives suspension?
A coroutine is a function whose execution can suspend and later continue. Its coroutine state contains the promise object, parameter copies, and storage needed for execution across suspension. A coroutine handle is a reference to that state, not an owning smart pointer with an automatic destruction policy.
The standard's coroutine transformation specifies parameter copying and the promise-controlled initial and final suspension points. Copying a reference parameter preserves a reference relationship; it does not turn the referenced object into owned storage. Coroutine definition and state
Consider these deliberately incomplete task-library declarations:
Task inspect(std::string text); // owns a string parameter
Task inspect_ref(const std::string& text); // borrows a string
Task inspect_view(std::string_view text); // owns a view, not its characters
If these tasks suspend before accessing the text, the first can retain its own string value. The second still requires the caller's string to remain alive. The third has copied a small view object, but its character buffer may be gone. “Passed by value” is not enough when the value itself is a pointer-like object.
Also distinguish scope from usefulness. A local created after initial suspension does not exist before that point. A local whose scope ends before a later suspension is not kept alive merely because the function is a coroutine. Trace construction and destruction rather than drawing every variable inside the frame forever.
Trace initial suspension, body suspension, and final suspension
Use an original task contract with initial_suspend() and final_suspend() both returning std::suspend_always. The return object owns a handle. The body creates a cleanup probe, suspends once, then returns. This is a teaching contract; another library may deliberately start execution immediately.
| Action | State and observable consequence |
|---|---|
| Call the coroutine | Parameter state and promise are initialized; initial suspension returns the task before the body probe exists. |
| First resume | The body starts, constructs the probe, and suspends at its explicit await point. |
| Second resume | The body continues and returns; the probe's scope ends, then final suspension is reached. |
| Destroy the owning task | The suspended coroutine state is released; its retained parameter and promise storage are cleaned up. |
This separates logical completion from storage release. At final suspension, done() is true, but an owner can still have a valid handle to a retained frame. That owner must eventually destroy it under this contract. Resuming again is not a way to “finish the cleanup.”
If final suspension instead permits automatic frame destruction, a wrapper cannot keep treating its old handle as a usable reference. The wrapper and promise must agree on the destruction policy. Mixing conventions from two task tutorials can produce a double destruction even if both individual tutorials are correct.
The standard requires resume to target an appropriate suspended coroutine and excludes a coroutine suspended at its final suspend point. Handle observer operations also have preconditions; a non-null bit pattern is not proof that a previously destroyed frame still exists. Handle resumption, handle observers
Who owns the frame?
For a simple interview implementation, choose one move-only owner. Its destructor calls destroy() only for the frame it still owns, and moving it transfers the handle while emptying the source. Copying a raw handle does not create a second frame or a reference-counted lifetime.
The key ownership excerpt is:
Owner(Owner&& other) noexcept
: h(std::exchange(other.h, {})) {}
~Owner() {
if (h) h.destroy();
}
This excerpt is not a complete task implementation. It assumes destruction occurs only when the coroutine is suspended and no external operation will later resume it. A move-assignment implementation must also dispose of the destination's old owned frame safely before accepting a new one.
Ask what happens if an owner is destroyed before the body starts, during a body suspension, or after final suspension. In the first case, the body probe was never constructed. In the second, active local state must be cleaned up. In the third, body locals have already left scope, while retained frame state still needs release.
Do not fix double destruction by removing every destructor call. That can exchange a crash for a leak. The repair is a single explicit destruction responsibility, plus a protocol that makes outstanding resume attempts impossible before the responsibility is exercised.
Diagnose dangling references without executing undefined behavior
Suppose a factory creates a local string, passes it to inspect_ref, and returns a lazily started task. The factory then exits. When the caller resumes the task, the string reference is dangling before the coroutine reads it.
The failure timeline is enough to establish the bug: local string constructed, reference stored, task returned, local destroyed, task resumed, invalid access attempted. A test that happens to print the old characters does not make this safe; freed stack memory can retain familiar bytes.
Three repairs have different contracts. Pass an owning string by value when the computation needs a private snapshot. Keep a shared owner when several asynchronous operations need the same object, accepting the associated ownership cost. Or require the caller to keep the data alive until completion, making that requirement enforceable and explicit.
Moving a string_view does not select the first repair. Moving a reference-containing wrapper does not either. Inspect the transitive ownership relationship, including buffers, iterators, spans, and this pointers.
Keep unsafe examples in a diagnosis section rather than running them as a correctness test. A sanitizer can provide evidence of a particular failing execution, but its silence cannot prove a lifetime relationship valid for every schedule or optimization level.
Why can a coroutine lambda still dangle?
A capturing coroutine lambda adds another object to the lifetime graph: the closure. A capture stored inside that closure is not automatically copied into independent coroutine-owned storage just because the lambda's call creates a coroutine.
Imagine an immediately invoked lambda that captures a string by value and suspends before reading it. The temporary closure can be destroyed after invocation while the coroutine still needs access to the capture through that closure. Owning the string in the closure is different from owning it in a coroutine parameter.
A useful repair is a noncapturing coroutine function, or noncapturing lambda, with the required data passed as an owning value parameter. Alternatively, keep the closure itself alive for the full asynchronous lifetime. Do not claim that every capture is necessarily unsafe: the problem is the mismatch between closure lifetime and later access.
The same analysis applies to a member coroutine that uses this. The object containing the member function must remain valid if later execution accesses it. A shared task handle alone does not keep that object alive unless the library explicitly connects the two ownership relationships.
Explain the graph out loud: the owner keeps the frame; the frame refers to the closure or receiver; that object owns the needed data. The first edge is insufficient if the second object's lifetime ends early.
What does co_await actually do?
At a simplified level, an awaiter participates through await_ready, await_suspend, and await_resume. Readiness can avoid suspension. The suspension hook decides how control proceeds, and the resume hook produces the await expression's result or failure. The exact overloads and return types determine the behavior. Await expressions
This is not equivalent to spawning a background thread. A coroutine may run synchronously until it suspends, resume on the same thread, or resume elsewhere because a scheduler arranges it. The scheduler and awaiter define that arrangement.
An especially important boundary is publishing a handle to another executor. Once it can resume the coroutine, the original awaiter's assumptions about its own lifetime may no longer hold. Avoid accessing awaiter state after a handoff unless the protocol guarantees that state remains alive.
For an interview trace, choose a manual single-threaded awaiter first. Record whether await_ready is true, whether await_suspend runs, and what await_resume returns. Then add an executor and explain the new ordering obligations. This isolates language mechanics from scheduling races instead of debugging both at once.
Cancellation is a coordination protocol
Destroying a coroutine frame releases its local state; it does not inherently cancel an external I/O operation. If a callback still owns a raw handle and later resumes it, the callback now targets destroyed storage.
A safe task library must coordinate completion, cancellation, callback deregistration, and frame destruction. For a teaching design, establish that exactly one path wins completion and that no callback can execute after ownership is released. A Boolean flag is not enough if another thread can race with the destruction path.
Distinguish “request cancellation” from “cancellation has completed.” The first asks work to stop. The second establishes a lifetime boundary at which the owner can release resources. If the API cannot provide that boundary, consider retaining a separate operation-state object until callbacks are drained.
Also discuss error storage. A promise's unhandled_exception() can capture an exception for a later caller, terminate, or follow another policy. Merely declaring it does not show how the consumer observes failures. The result object needs an explicit observation rule and cleanup behavior even when nobody awaits the result.
Practice ownership before choosing a coroutine library
Full explanations may have access restrictions.
| Question | Coroutine-oriented follow-up |
|---|---|
| Write a Usage Example for a Resource-Owning C++ Class and Explain Its Semantics | Make a suspended frame's owner move-only and define cleanup. |
| Find and Fix C++ Ownership Bugs: Double Free, Shallow Copy, and Object Slicing | Distinguish copied handles from duplicated ownership. |
| Compare C++ Pointers and References | Explain why a reference parameter does not extend a referent's life. |
| Implement a Stoppable Producer–Consumer System in C++ | Separate a stop request from the right to destroy asynchronous state. |
| Detect memory leaks in C++ | Account for frames abandoned at final suspension. |
A complete answer names the destruction boundary
C++ coroutine interview questions are easier to defend when every handle, referent, and callback has a named owner and a last permitted use. Explain the state transition first, then the ownership rule, then the schedule that could violate it.
Practice by extending the resource-owning class exercise with a suspended frame. Trace destruction before first resume, at body suspension, and after logical completion. Keep those cases separate before adding cancellation or multiple threads.
Sources and Further Reading
- C++ working draft: coroutine definitions and state
- C++ working draft: await expressions
- C++ working draft: coroutine-handle resumption and destruction
- C++ working draft: coroutine-handle observers
Research checked October 3, 2026. Examples use C++20 language mechanics and an explicitly chosen task contract. The working draft may contain newer changes; scheduler behavior and library-specific cancellation are not standardized by these examples.