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 ataskobject 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
f1andf2diverges:f1usesco_return result_type{}to invokereturn_valueon the promise. Note thatreturn_valueaccepts an r-value reference ofresult_type, therefore bounding it to a temporary.f2usesco_yield result_type{}to invokeyield_valueon the promise. Same asreturn_value, it accepts an r-value- In addition,
yield_valuereturnsfinal_suspend(), which in turn returnsstd::suspend_always, instructing the coroutine to suspend after yielding the value.
- In addition,
- In both
return_valueandyield_value,resultis 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?