Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why must the return type of a coroutine be move-constructible?

Consider the following code that defines the invoker class - a minimal return type for a coroutine. We explicitly delete the copy and move constructors of the invoker class.

#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;
}

The code does not compile on latest GCC (10.1), which is supposed to have full support for C++20 coroutines.

Instead, we get an error that indicates that the move constructor is required:

<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;
      |     ^~~~~~~

Why is this so?

The invoker object is constructed by calling get_return_object() of the invoker_promise, it can't be accessed except from the caller of f(). With C++17 guaranteed copy elision, the invoker returned by get_return_object() is a prvalue, and hence should not be materialized until after it is returned from f().

Since the returned object cannot be accessed from within the coroutine, I cannot see any situation where we might need to materialize the object before returning it. Am I missing something?

Note: I'm aware of this question, but it:

  • was asked two years ago,
  • is about the TS version of coroutines,
  • is about VC++'s implementation,
  • is unanswered, and
  • has comments that mainly talk about guaranteed copy elision.
like image 828
Bernard Avatar asked Jun 17 '20 13:06

Bernard


1 Answers

With C++17 guaranteed copy elision, the invoker returned by get_return_object() is a prvalue, and hence should not be materialized until after it is returned from f().

That would only be true if a coroutine function call were guaranteed to generate its return value by a call equivalent to building a bunch of objects in a separate stack, then calling get_return_object() on one of them. That is, the question is whether the path from get_return_object() to the function call itself only uses prvalues.

Let's look at what the standard says:

The expression promise.get_­return_­object() is used to initialize the glvalue result or prvalue result object of a call to a coroutine. The call to get_­return_­object is sequenced before the call to initial_­suspend and is invoked at most once.

Note that it says that it initializes the "prvalue result object". This is the same language used in the definition of the behavior of the return statement:

the return statement initializes the glvalue result or prvalue result object of the (explicit or implicit) function call by copy-initialization from the operand.

The only hesitation I would have in saying that the standard clearly requires guaranteed elision between get_return_object and the caller of the coroutine is the last part about initial_suspend. Because something happens between the initialization of the "prvalue result object" and returning control to the caller, it could be that there has to be an intermediary, which must be copied/moved from.

But the fact that it's using the exact same language as return suggests that it ought to be providing the exact same behavior too.

When running on MSVC's coroutine implementation, your code (with only minor changes for differences in where certain types are defined) works fine. Coupled with the above evidence, I would say that this suggests that this is a compiler bug.

like image 82
Nicol Bolas Avatar answered Sep 23 '22 16:09

Nicol Bolas