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

Leave a Comment