9

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.

Quirin F. Schroll
  • 1,302
  • 1
  • 11
  • 25

1 Answers1

9

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.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • But in the example, the vector would be deleted right after being yielded, since it is scoped in the while loop right? So you would still end up with copy and delete right after eachother. – David van rijn Jan 13 '22 at 21:29
  • Thank you for referring to clauses in the standard. “[I]n general you would have to specify a set of conditions under which this can be guaranteed to be the case.” I could imagine that the C++ standard committee thought of this case and handled it. Maybe there are good reasons against any such exception clause. – Quirin F. Schroll Jan 14 '22 at 14:37
  • 1
    @David van rijn I don't know if a compiler is smart enough to catch such a rather obvious case when doing optimization (it likely is), but the standard isn't smart enough, and thus the compiler must reject in case of a no-copy type, even if in the binary, no copy takes place after optimizing it away. – Quirin F. Schroll Jan 14 '22 at 14:40
  • But then it isn't specific to co_yield statements. For any object, if the compiler can prove that it is going to be destructed right after, we could consider if the compiler should be allowed to automatically convert it to an xvalue in its last use (or even forced in very restrictive circumstances). – Marc Glisse Feb 10 '22 at 09:44