Why should coroutine parameters usually be passed by value?
Study the coroutine below. The caller passes a temporary string, the coroutine suspends on co_await, and later uses the parameter s.
Task<void> process(const std::string& s) {
co_await something(); // coroutine suspends here
use(s); // is s still valid?
}
void caller() {
process(std::string{"temp"});
}
Explain whether use(s) is safe, what determines the parameter's lifetime relative to the coroutine frame, and what changes if s is declared by value (std::string s) instead of by reference. State which form is correct and why.
A by-value parameter is copied into the coroutine frame, so it stays alive across suspensions for the coroutine's whole lifetime. A reference parameter only refers to the caller's object — if the caller returns first, the reference dangles, which is undefined behavior.
- ✗Thinking a reference parameter is copied into the frame like a by-value one
- ✗Assuming the caller's stack frame stays alive until the coroutine finishes
- ✗Believing the compiler rejects or rewrites reference parameters automatically
- →Is a captured lambda used as a coroutine safe across suspensions?
- →When is a reference parameter actually acceptable in a coroutine?
Dangerous: a reference parameter
Task<void> danger(const std::string& s) { // s — a reference to the caller's object
co_await something(); // coroutine suspended; danger()'s frame is gone
use(s); // UB: s may no longer exist
}
void caller() {
danger(std::string{"temp"}); // the temporary lives only to end of expression
} // ...but the coroutine resumes later
The coroutine frame stores the reference itself (an address), not the string. The string object stays on the caller's stack or temporary storage. After the first suspension caller returns, the temporary is destroyed — and s becomes a dangling reference.
Safe: a by-value parameter
Task<void> safe(std::string s) { // s is COPIED into the coroutine frame
co_await something(); // the copy lives together with the frame
use(s); // OK: s is valid for the coroutine's whole lifetime
}
Passed by value, the parameter copy becomes part of the coroutine frame. The frame lives until the coroutine is destroyed, so the parameter outlives every suspension regardless of when the caller returned.