Search code examples
c++c++17return-value-optimization

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


I saw this question When is an object "out of scope"?

I have taken a look at the sparc_spread's answer and I found one problem in it. In this section of his answer:

Circle myFunc () {
    Circle c (20);
    return c;
}
// The original c went out of scope. 
// But, the object was copied back to another 
// scope (the previous stack frame) as a return value.
// No destructor was called.

He has said that " No destructor was called. " But when I try to run this code ( Which was written by me):

   /* Line number 1 */ #include <iostream>
   /* Line number 2 */ #include <string>
   /* Line number 3 */ using namespace std;
   /* Line number 4 */ class test {
   /* Line number 5 */ public:
   /* Line number 6 */  test(int p) {
   /* Line number 7 */      cout << "The constructor ( test(int p) ) was called"<<endl;
   /* Line number 8 */  }
   /* Line number 9 */  test(test&&c)noexcept  {
   /* Line number 10 */        cout << "The constructor ( test(test && c) ) was called" << endl;
   /* Line number 11 */ }
   /* Line number 12 */     ~test() {
   /* Line number 13 */         cout << "The distructor was called" << endl;
   /* Line number 14 */     }
   /* Line number 15 */ };
   /* Line number 16 */ test function() {
   /* Line number 17 */     test i(8);
   /* Line number 18 */     return i;
   /* Line number 19 */ } 
   /* Line number 20 */ int main()
   /* Line number 21 */ {
   /* Line number 22 */     test o=function();
   /* Line number 23 */     return 0;
   /* Line number 24 */ }

The Output:

The constructor ( test(int p) ) was called
The constructor ( test(test && c) ) was called
The distructor was called
The distructor was called

So the output of my code shows that:

  1. Two constructors were called ( and this is not the point I want to discuss. So I will not discuss ( Why, When or How) are two constructors called?)

  2. Two destructors were called

And when I use the debugger (to know when the first destructor was called) I found that The first destructor is called in line number 18 (the line number 18 in my code).

And in the end. Is my point of view right?


Solution

  • 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