co_return 对比 co_yield 右侧是临时的
co_return vs. co_yield when the right hand side is a temporary
背景:这个问题是在看cppcoro, specifically this line的源码时产生的。
问题:考虑以下代码:
#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();
}
}
在上面的代码中:
调用f1()
和f2()
都启动一个协程。协程立即挂起,并将包含协程句柄的 task
对象返回给调用者。协程的承诺包含 result
- 指向 result_type
的 指针 。目的是让它在完成时指向协程的结果。
task.run()
在返回的任务上调用,恢复存储的协程句柄。
这里是 f1
和 f2
的分歧点:
f1
使用 co_return result_type{}
调用 return_value
承诺。请注意,return_value
接受 result_type
的 r 值引用,因此将其绑定到临时值。
f2
使用 co_yield result_type{}
调用 yield_value
承诺。与 return_value
相同,它接受一个 r 值
- 此外,
yield_value
returnsfinal_suspend()
,进而returnsstd::suspend_always
,指示协程在产生值后挂起。
- 在
return_value
和 yield_value
中,result
被设置为指向他们收到的参数。
但是,由于 final_suspend
在 co_return
之后也被调用(并等待其结果),我预计使用 co_return
和 co_yield
之间没有区别.但是,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
请注意,在上面的输出中,co_return
版本构造一个结果,销毁它,然后从中移动构造,调用未定义的行为。然而,co_yield
版本似乎工作正常,仅在 移动构建后破坏结果。
为什么这里的行为不同?
您违反了 C++ 的基本规则:您编写了一个接受潜在纯右值的函数,并存储了一个指针,该指针 比为它们提供纯右值的函数调用寿命更长。事实上,任何时候你看到一个函数接受右值引用(或 const-lvalue-reference)并将 pointer/reference 存储到那个将比该函数更长寿的对象,你应该认为该代码是 高度充其量是可疑的。
如果一个函数将一个参数作为右值引用,这意味着您应该在该函数调用中使用它或从它移动。纯右值通常不会超过传递给它们的函数调用,因此您要么使用它们,要么丢失它们。
无论如何,您看到的行为正是您应该看到的。当协程发出 co_return
时,它... returns。这意味着协程块的主体已经退出。 return_value
在块仍然存在时被调用,但一旦完成,协程块及其所有自动变量(包括参数)就会消失。
这就是为什么 return 从普通函数引用自动变量不是一个好主意。这对于 co_return
来说同样是一个糟糕的想法,即使您间接将该引用指向调用者。
co_yield
版本有效(你仍然不应该这样做,因为这不是你应该如何处理纯右值,但它需要工作)因为 co_yield
语句本身yield_value
的 return 值告知暂停。这会保留协程的堆栈,包括 co_yield
语句本身内的任何纯右值,直到恢复协程。
但同样,您应该在 yield_value
函数中执行移动,就像您通常对右值引用参数所做的那样。
背景:这个问题是在看cppcoro, specifically this line的源码时产生的。
问题:考虑以下代码:
#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();
}
}
在上面的代码中:
调用
f1()
和f2()
都启动一个协程。协程立即挂起,并将包含协程句柄的task
对象返回给调用者。协程的承诺包含result
- 指向result_type
的 指针 。目的是让它在完成时指向协程的结果。task.run()
在返回的任务上调用,恢复存储的协程句柄。这里是
f1
和f2
的分歧点:f1
使用co_return result_type{}
调用return_value
承诺。请注意,return_value
接受result_type
的 r 值引用,因此将其绑定到临时值。f2
使用co_yield result_type{}
调用yield_value
承诺。与return_value
相同,它接受一个 r 值- 此外,
yield_value
returnsfinal_suspend()
,进而returnsstd::suspend_always
,指示协程在产生值后挂起。
- 此外,
- 在
return_value
和yield_value
中,result
被设置为指向他们收到的参数。
但是,由于 final_suspend
在 co_return
之后也被调用(并等待其结果),我预计使用 co_return
和 co_yield
之间没有区别.但是,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
请注意,在上面的输出中,co_return
版本构造一个结果,销毁它,然后从中移动构造,调用未定义的行为。然而,co_yield
版本似乎工作正常,仅在 移动构建后破坏结果。
为什么这里的行为不同?
您违反了 C++ 的基本规则:您编写了一个接受潜在纯右值的函数,并存储了一个指针,该指针 比为它们提供纯右值的函数调用寿命更长。事实上,任何时候你看到一个函数接受右值引用(或 const-lvalue-reference)并将 pointer/reference 存储到那个将比该函数更长寿的对象,你应该认为该代码是 高度充其量是可疑的。
如果一个函数将一个参数作为右值引用,这意味着您应该在该函数调用中使用它或从它移动。纯右值通常不会超过传递给它们的函数调用,因此您要么使用它们,要么丢失它们。
无论如何,您看到的行为正是您应该看到的。当协程发出 co_return
时,它... returns。这意味着协程块的主体已经退出。 return_value
在块仍然存在时被调用,但一旦完成,协程块及其所有自动变量(包括参数)就会消失。
这就是为什么 return 从普通函数引用自动变量不是一个好主意。这对于 co_return
来说同样是一个糟糕的想法,即使您间接将该引用指向调用者。
co_yield
版本有效(你仍然不应该这样做,因为这不是你应该如何处理纯右值,但它需要工作)因为 co_yield
语句本身yield_value
的 return 值告知暂停。这会保留协程的堆栈,包括 co_yield
语句本身内的任何纯右值,直到恢复协程。
但同样,您应该在 yield_value
函数中执行移动,就像您通常对右值引用参数所做的那样。