Search code examples
c++movec++20c++-coroutine

Is std::move required to move using co_yield?


Say I have a std::vector that is declared in a loop's body and co_yielded:

some_generator<std::vector<int>> vector_sequence() {
    while (condition()) {
        std::vector<int> result = get_a_vector();
        result.push_back(1);
        co_yield std::move(result); // or just: co_yield result; 
    }
}

Quite obviously, result isn't going to be used again after being co_yielded (or I am horribly mistaken), so it would make sense to move it. I tried co_yielding a simple non-copyable type without std::move and it did not compile, so in generic code, one would have use std::move. Does the compiler not recognize this (compiler bug?) or is it intended by the language that co_yield always copies an lvalue, so I have to std::move? I know that returning an lvalue that is a local variable looks like a copy but is guaranteed to be a move or an even better kind of copy elision, and this does not seem so much different from it.

I have read C++: should I explicitly use std::move() in a return statement to force a move?, which is related to this question, but does not answer it, and considered co_return vs. co_yield when the right hand side is a temporary, which as far as I understand, is not related to this question.


Solution

  • The implicit move rule ([class.copy.elision]/3) applies to return and co_return statements and to throw expressions. It doesn't apply to co_yield.

    The reason is that, in the contexts enumerated in [class.copy.elision]/3, the execution of the return or co_return statement or throw expression ensures that the implicitly movable entity's lifetime ends. For example,

    auto foo() {
        std::string s = ...;
        if (bar()) {
            return s;
        }
        // return something else
    }
    

    Here, even though there is code after the return statement, it's guaranteed that if the return statement executes, then any code further down that can see s will not execute. This makes it safe to implicitly move s.

    In contrast, co_yield only suspends the coroutine and does not end it in the manner of co_return. Thus, in general, after co_yield result; is evaluated, the coroutine might later resume and use the very same result variable again. This means that in general, it's not safe to implicitly transform the copy into a move; therefore, the standard does not prescribe such behaviour. If you want a move, write std::move.

    If the language were to allow implicit move in your example, it would have to have specific rules to ensure that, although the variable could be used again after co_yield, it is in fact not. In your case, it might indeed be that the loop will immediately end and thus the result variable's lifetime will end before its value can be observed again, but in general you would have to specify a set of conditions under which this can be guaranteed to be the case. Then, you could propose that an implicit move occur only under those conditions.