Why is the local variable copied despite being returned by name?

Viewed 633

In the following code snippet, the return s gives the warning local variable 's' will be copied despite being returned by name [-Wreturn-std-move]. Why is this the case?

My goal with this lambda function is to take ownership of the input string and then return it after modification through RVO or move semantics. I would really like to avoid any copying.

const auto to_upper = [](std::string&& s) {
    std::transform(s.begin(), s.end(), s.begin(), 
        [](unsigned char c){ return std::toupper(c); }
    );
    return s;
};

Returning std::move(s) or std::forward<std::string>(s) will resolve the issue, but I thought this was not necessary since the compiler can elide the use of copy constructor. Also, I think I should be using std::forward but which one is correct and why?

3 Answers

In the following code snippet, the return s gives the warning local variable 's' will be copied despite being returned by name [-Wreturn-std-move]. Why is this the case?

That's because automatically deduced return value type of a function or lambda capture call operator is never a reference. See Return type deduction

If the return type does not use decltype(auto), the deduction follows the rules of template argument deduction.

Template argument deduction never deduces a reference.

If you need a reference return value, you must specify that explicitly:

const auto to_upper = [](std::string&& s) -> std::string&& {
    // ...
    return std::move(s);
}

std::move(s) fixes the compiler warning, but it doesn't change the return value type, unless reference return type is specified explicitly. An example. The compiler warning is broken.


When a function returns by value, function parameters are exempt from return value copy elision.

See copy elision for full details:

In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".

class.copy.elision:

in a return statement in a function with a class return type, when the expression is the name of a non-volatile object with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler ([except.handle])) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the object directly into the function call's return object

The function parameters and return values must co-exist at the same time. When the return value is constructed, local variables still exist, and may refer to the function parameter. Only after the return value is constructed, the destructors of locals are then executed. Those destructors may refer to the function parameter, and this is why its storage cannot be reused for the return value that is constructed earlier.

(N)RVO only works when a local variable that would be created within the called function can be aliased to the return memory instead.

The parameter s is a reference. The function is getting an address passed to it, where the actual value lives somewhere else that is beyond the control of the this function.

In order to work, define the parameter s to be a value instead.

Also, note that the concepts of (N)RVO, move semantics, and C++17's new copy elision are all different things. Any one of them will avoid making a copy of the string data if it's allocated separately from the string's struct (but SSO prevents that anyway). The middle one still copies the structure itself, which will include the pointers or SSO internal buffer, but these primitives may themselves be optimized on a lower level if the function is inlined.

There's a rule that allows local variables (including function parameters) to be implicitly moved (instead of copying) when returning, if the function returns by value, and variable that's being returned has the same type as the return type.

The warning tells you that such implicit move will not happen in this case, because s is a reference and the rule doesn't apply to references.


I think I should be using std::forward

No, you should use std::move, because your parameter is not a forwarding reference. T && is only considered to be a forwarding reference if T is a template parameter (or auto), that's being deduced when the call is made.


It seems the rule was changed in C++20, and rvalue-references can now be implicitly moved as well. Starting from C++20, std::move here can be removed.

This is governed by:

[class.copy.elision] (C++20)

3 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:

(3.1) — 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 ...

The pre-C++20 wording is different, it permits only "automatic objects" (not references).

Related