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

Does NRVO also apply to coroutines?


In the following example, NRVO (Named Return Value Optimization) applies as per this article:

std::string f1()
{
    std::string str;

    return str; // NVRO applies here!
}

However, consider:

task<std::string> f2()
{
    std::string str;

    co_return str; // Does NVRO also apply here?
}

Solution

  • NRVO as defined by the article you linked (i.e. not even creating a temporary) isn't a thing for coroutines because how co_return works is up to the user-provided coroutine promise type: the expression in the co_return statement is fed to the promise's return_value method, which can decide what to do with it.

    However there is a related optimization that still may be useful. [class.copy.elision]/3 says the following:

    An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation:

    • If the expression in a return ([stmt.return]) or co_­return ([stmt.return.coroutine]) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
    • [...]

    overload resolution to select the constructor for the copy or the return_­value overload to call is first performed as if the expression or operand were an rvalue. If the first overload resolution fails or was not performed, overload resolution is performed again, considering the expression or operand as an lvalue.

    This means that if you return a local variable by name from a coroutine it will be moved, not copied (as long as the promise type supports this). For example, clang accepts the following despite the fact that it's not possible to copy a std::unique_ptr<int>:

    // Assume a coroutine task type called Task<T> whose associated promise has a
    // return_value(T) method. The co_return here will successfully call that
    // method.
    Task<std::unique_ptr<int>> MakeInt() {
      auto result = std::make_unique<int>(17);
      co_return result;
    }
    

    So the optimization "value is fed to coroutine promise as an rvalue reference even though std::move wasn't used" does apply. But the standard doesn't say "move constructor isn't even called", and it can't because it's up to the promise what to do with the expression it's given.