【问题标题】:Why must the return type of a coroutine be move-constructible?为什么协程的返回类型必须是可移动构造的?
【发布时间】:2020-10-07 07:46:41
【问题描述】:

考虑以下定义invoker 类的代码 - 协程的最小返回类型。我们显式删除了 invoker 类的复制和移动构造函数。

#include <coroutine>
#include <cstdlib>
class invoker {
public:
    class invoker_promise {
    public:
        invoker get_return_object() { return invoker{}; }
        auto initial_suspend() { return std::suspend_never{}; }
        auto final_suspend() { return std::suspend_never{}; }
        void return_void() {}
        void unhandled_exception() { std::abort(); }
    };
    using promise_type = invoker_promise;
    invoker() {}
    invoker(const invoker&) = delete;
    invoker& operator=(const invoker&) = delete;
    invoker(invoker&&) = delete;
    invoker& operator=(invoker&&) = delete;
};

invoker f() {
    co_return;
}

代码无法在 latest GCC (10.1) 上编译,它应该完全支持 C++20 协程。

相反,我们得到一个错误,表明需要移动构造函数:

<source>: In function 'invoker f()':
<source>:23:1: error: use of deleted function 'invoker::invoker(invoker&&)'
   23 | }
      | ^
<source>:17:5: note: declared here
   17 |     invoker(invoker&&) = delete;
      |     ^~~~~~~

为什么会这样?

invoker对象是通过调用invoker_promiseget_return_object()构造的,除了f()的调用者外无法访问。在 C++17 保证复制省略的情况下,get_return_object() 返回的 invoker 是纯右值,因此在从 f() 返回之前不应实现。

由于无法从协程中访问返回的对象,我看不到任何需要在返回对象之前实现对象的情况。我错过了什么吗?

注意:我知道this question,但它:

  • 两年前被问到,
  • 讲的是TS版本的协程,
  • 是关于VC++的实现,
  • 未得到答复,并且
  • 有主要讨论保证复制省略的 cmets。

【问题讨论】:

  • "有主要讨论保证复制省略的 cmets。" 保证省略就是您要问的。协程函数调用的返回值是一个纯右值,但是到达那个纯右值的路径是不同的。保证省略仅适用于纯右值,所以问题是从其源到协程函数调用者的路径是否纯粹使用纯右值。

标签: c++ coroutine c++20


【解决方案1】:

使用 C++17 保证复制省略,get_return_object() 返回的 invoker 是纯右值,因此在从 f() 返回之前不应实现。

只有当协程函数调用被保证通过调用生成其返回值时才会如此,这相当于在单独的堆栈中构建一堆对象,然后在其中一个上调用get_return_object()。也就是说,问题是从get_return_object()到函数调用本身的路径是否只使用prvalues。

我们来看看what the standard says

表达式promise.get_­return_­object() 用于初始化协程调用的glvalue 结果或prvalue 结果对象。对get_­return_­object 的调用在对initial_­suspend 的调用之前排序,并且最多调用一次。

请注意,它说它初始化了“prvalue 结果对象”。这与behavior of the return statement 的定义中使用的语言相同:

return 语句通过操作数的复制初始化来初始化(显式或隐式)函数调用的泛左值结果或纯右值结果对象。

我唯一要说的是标准明确要求在get_return_object 和协程调用者之间保证省略是关于initial_suspend 的最后一部分。因为在“prvalue 结果对象”的初始化和将控制权返回给调用者之间发生了一些事情,可能必须有一个中介,必须从中复制/移动。

但它使用与return 完全相同的语言这一事实​​表明它也应该提供完全相同的行为

在 MSVC 的协程实现上运行时,您的代码(仅针对某些类型定义的差异进行细微更改)works fine。结合上述证据,我会说这表明这是一个编译器错误。

【讨论】:

  • (感谢编辑,让答案变得更好)
  • 您的 godbolt.org 链接似乎没有任何代码。我想你在复制链接之前忘记点击“分享”按钮了?
  • @Bernard:已修复。
  • 我是否正确地说编译器魔法对于调用任何函数(例如initial_suspend)是必要的,同时确保前一个函数调用(例如get_return_object)返回的prvalue结果对象永远不会实现? (忽略协程无论如何都需要编译器魔法的事实)
  • @Bernard:正如我所说,它与复制省略具有相同的“魔力”。或者您认为 NRVO 是如何运作的?
【解决方案2】:

此错误已在 GCC 10.2 中修复,但目前存在于 Clang 主干bug reported 中。演示:https://gcc.godbolt.org/z/9sYj1qMnc

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-13
    • 2014-11-30
    相关资源
    最近更新 更多