Background: This question arose when I was reading the source of cppcoro, specifically this line.
Question: Consider the following code:
#include <coroutine>
#include <stdexcept>
#include <cassert>
#include <iostream>
struct result_type {
result_type() {
std::cout << "result_type()\n";
}
result_type(result_type&&) noexcept {
std::cout << "result_type(result_type&&)\n";
}
~result_type() {
std::cout << "~result_type\n";
}
};
struct task;
struct my_promise {
using reference = result_type&&;
result_type* result;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept {
return {};
}
task get_return_object();
void return_value(reference result_ref) {
result = &result_ref;
}
auto yield_value(reference result_ref) {
result = &result_ref;
return final_suspend();
}
void unhandled_exception() {}
};
struct task {
std::coroutine_handle<my_promise> handle{};
~task() {
if (handle) {
handle.destroy();
}
}
void run() {
handle.resume();
}
my_promise::reference result() {
return std::move(*handle.promise().result);
}
};
task my_promise::get_return_object() {
return { std::coroutine_handle<my_promise>::from_promise(*this) };
}
namespace std {
template <>
struct coroutine_traits<task> {
using promise_type = my_promise;
};
}
task f1() {
co_return result_type{};
}
task f2() {
co_yield result_type{};
// silence "no return_void" warning. This should never hit.
assert(false);
co_return result_type{};
}
int main() {
{
std::cout << "with co_return:\n";
auto t1 = f1();
t1.run();
auto result = t1.result();
}
std::cout << "\n==================\n\n";
{
std::cout << "with co_yield:\n";
auto t2 = f2();
t2.run();
auto result = t2.result();
}
}
In the code above:
Calling
f1()
andf2()
both start a coroutine. The coroutine is immediately suspended and atask
object containing the handle of the coroutine is returned to the caller. The promise of the coroutine containsresult
- a pointer toresult_type
. The intention is to make it point to the result of the coroutine when it finishes.task.run()
is called on the returned task, which resumes the stored coroutine handle.Here is where
f1
andf2
diverges:f1
usesco_return result_type{}
to invokereturn_value
on the promise. Note thatreturn_value
accepts an r-value reference ofresult_type
, therefore bounding it to a temporary.f2
usesco_yield result_type{}
to invokeyield_value
on the promise. Same asreturn_value
, it accepts an r-value- In addition,
yield_value
returnsfinal_suspend()
, which in turn returnsstd::suspend_always
, instructing the coroutine to suspend after yielding the value.
- In addition,
- In both
return_value
andyield_value
,result
is set to point to the argument they received.
However, since final_suspend
is also invoked (and its result awaited on) after a co_return
, I expect no difference between using a co_return
and a co_yield
. However, the compiler proved me wrong:
with co_return:
result_type()
~result_type
result_type(result_type&&)
~result_type
==================
with co_yield:
result_type()
result_type(result_type&&)
~result_type
~result_type
Note that in the above output, the co_return
version constructs a result, destroy it, and then move construct from it, invoking undefined behavior. However, the co_yield
version seems to work fine, destroying the result only after move constructing from it.
Why is the behavior different here?