Does Returning a Local Variable Return a Copy and Destroy the Original(Nrvo)

Does returning a local variable return a copy and destroy the original(nrvo)?

Does returning a local variable return a copy and destroy the original?

The final answer to your question is that it depends on whether or not optimization is enabled. So lets discuss each case separately. Note also that since the given output in the original question is for C++17, the below discussion is also for the same(C++17 & onwards).

With Optimization

Here we will see what happens when optimization(NRVO) is enabled.

class test {
public:
test(int p) {
cout << "The constructor ( test(int p) ) was called: "<<this<<endl;
}
test(test&&c)noexcept {
cout << "The constructor ( test(test && c) ) was called: "<<this << endl;
}
~test() {
cout << "The distructor was called: "<<this << endl;
}
};
test function() {
test i(8);
return i;
}
int main()
{
test o=function();
return 0;
}

The output of the program is(with NRVO enabled):

The constructor ( test(int p) ) was called: 0x7fff78e42887   <-----object o construction
The distructor was called: 0x7fff78e42887 <-----object o destruction

The above output can be understood using the optimization called named return value optimization(aka NRVO) as described in copy elison which states:

Under the following circumstances, the compilers are permitted, but not required to omit the copy and move (since C++11) construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed:

  • 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".

(emphasis mine)

Lets apply this to our example given above and try to understand the output. The variable named i is a local variable meaning it has automatic storage duration and thus according to the above quoted statement, the compilers are allowed(but not required!) to directly construct the object into the storage for variable named o. That is, it is as if you wrote:

test o(5); //equivalent to this due to NRVO

Thus here we first see the call to the converting constructor test::test(int) for object o and then the destructor call for that object o.

Without Optimization

You have the option to disable this optimization by using the -fno-elide-constructors flag. And when executing the same program with this flag, the output of the program will become:

The constructor ( test(int p) ) was called: 0x7ffda9d94fe7        <-----object i construction
The constructor ( test(test && c) ) was called: 0x7ffda9d95007 <-----object o construction
The distructor was called: 0x7ffda9d94fe7 <-----object i destruction
The distructor was called: 0x7ffda9d95007 <-----object o destruction

This time since we have supplied the -fno-elide-constructors flag to the compiler, NRVO is disabled. This means that now the compiler cannot omit the copy/move construction corresponding to the return statement return i;. This in turn means that first the object i will be constructed using the converting constructor test::test(int) and thus we see the very first line in the output.

Next, this local variable named i will be moved using the move constructor test::test(test&&) and hence we see the second line of the output. Note that the object o will be constructed directly from this moved prvalue directly due to mandatory copy elison since you're using C++17.

Next, the local variable i will be destructed using the destructor test::~test() and we see the third line in the output.

Finally, the object o will get destroyed and we see the fourth line of the output.

In this case, it is as-if you wrote:

test o = std::move(test(5)); //equivalent to this

in C++ which happens first, the copy of a return object or local object's destructors?

For previous standards (here I will use C++ 03), the closest the standard comes to declaring the sequence of operations in a return is from 6.6

6.6 Jump statements


  1. On exit from a scope (however accomplished), destructors (12.4) are called for all constructed objects with automatic storage duration (3.7.2) (named objects or temporaries) that are declared in that scope, in the
    reverse order of their declaration. Transfer out of a loop, out of a block, or back past an initialized variable with automatic storage duration involves the destruction of variables with automatic storage duration that are in scope at the point transferred from...

The return statement must complete in order to exit the [function] scope, implying that the copy-initialization must also complete. This order is not explicit. Various other quotes from 3.7.2 and 12.8 concisely state the same as above without providing explicit order. Working revisions (after Nov. 2014) include the quote below to address that. The defect report clarifies the change.

From the current working draft (N4527) of the standard as seen on the date of this question

6.6.3 The Return Statement


  1. The copy-initialization of the returned entity is sequenced before the destruction of temporaries at the end
    of the full-expression established by the operand of the return statement, which, in turn, is sequenced before
    the destruction of local variables (6.6) of the block enclosing the return statement.

Notice that this quote refers directly to 6.6. So I think it is safe to assume that the Mutex object will always be destroyed after the return expression has copy-initialized the return value.

c++11 Return value optimization or move?

Use exclusively the first method:

Foo f()
{
Foo result;
mangle(result);
return result;
}

This will already allow the use of the move constructor, if one is available. In fact, a local variable can bind to an rvalue reference in a return statement precisely when copy elision is allowed.

Your second version actively prohibits copy elision. The first version is universally better.

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

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).

Returning local variable by copy - how does it work

When retlocal1 returns it copies its value to EAX? But EAX is a register with enough space to hold an integer? So how does EAX hold the entire copy of the std::string (which could of course be a long long string).

This is not correct. You should check the ABI for your platform, but the most common approach is that the calling convention for functions returning large (larger than a register) objects transforms the function into a function that takes an implicit pointer to the returned object. The caller allocates the space for the std::string, and the return statement is transformed into copy construction into that location:

// Transformed function (with no NRVO)
void retlocal(std::string *ret) {
std::string s; s.append(3, 'A');
new (ret) std::string(s);
return;
}

The compiler for that particular case will apply Named Return Value Optimization, which will remove the object s and construct in place of the returned object, avoiding the copy:

void retlocal(std::string *ret) {
new (ret) std::string();
ret->append(3,'A');
return;
}

Shouldn't NRVO guarantee the local named variable and the call-site variable to take the same address?

Definitely seems to be a bug in clang, they should be the same, else things like the following will be erroneous

struct S
{
int i;
int* ptr;

S(int i) : i(i) {
this->ptr = &this->i;
}

S(S&& s)
{
this->i = s.i;
this->ptr = &this->i;
std::cout << "S(S&&)\n";
}

S(S const&) = delete;
};

Where a move (or elision where addresses don't change) is required to ensure that the internal pointer points to the correct integer. But because of elision that pointer points to memory that does not contain the member integer.

See output here https://wandbox.org/permlink/NgNR0mupCfnnmlhK

As pointed out by @T.C., this is actually a bug in the Itanium ABI spec that doesn't take move-ctor into account. Quoting from Clang's dev:

Clang's rule is the one in the ABI: a class is passed indirectly if it
has a non-trivial destructor or a non-trivial copy constructor. This
rule definitely needs some adjustment [...]

Indeed, if we define either a non-trivial dtor or copy-ctor for S in the original example, we get the expected result (i.e. same address).



Related Topics



Leave a reply



Submit