问题
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.
回答1:
With C++17 guaranteed copy elision, the
invoker
returned byget_return_object()
is a prvalue, and hence should not be materialized until after it is returned fromf()
.
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 toget_return_object
is sequenced before the call toinitial_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.
来源:https://stackoverflow.com/questions/62429881/why-must-the-return-type-of-a-coroutine-be-move-constructible