c++c++14return-value-optimizationnrvo

When in C++14 with RVO/NRVO closed, how is the object returned?


I am learning about move semantics, so I wrote a small program as follows to practice:

#include <iostream>

using namespace std;

int one_int = 123;

class A {
public:
    int *a;

    A(int *ptr) : a(ptr) {
        cout << "In A's constructor..." << endl;
    }

    A(const A &other) {
        cout << "In A's copy constructor..." << endl;
        a = other.a;
    }

    A(A &&other) noexcept {
        cout << "In A's move constructor..." << endl;
        a = other.a;
        other.a = nullptr;
    }

    ~A() {
        cout << "In A's destructor..." << endl;
    }
};

A make_obj() {
    cout << "In make_obj..." << endl;
    return A(&one_int);
}

A make_obj_by_move(A &&source) {
    cout << "In make_obj_by_move..." << endl;
    return A(static_cast<A&&>(source));
}

A make_obj_by_copy(A source) {
    cout << "In make_obj_by_copy..." << endl;
    return A(source);
}

int main() {
    A obj2 = make_obj_by_move(make_obj());
    cout << endl;
    A obj1 = make_obj_by_copy(make_obj());
    cout << endl;
    // to ensure that the information printed when obj1 and obj2 are destroyed is separated from the above ones
    obj1.a = nullptr;
    obj2.a = nullptr;
    return 0;
}

I turned off RVO/NRVO of clang by the -fno-elide-constructors compiler option and use -std=c++14. I got the following output, very confusing:

In make_obj...                                
In A's constructor...                   // make tmp obj
In A's move constructor...              // w/o RVO, need another construction to get return value?
In A's destructor...                    // destory the temp obj in make_obj scope?                
In make_obj_by_move...                
In A's move constructor...              // call the move constructor like expected? I guess by the position of "copy constructor" below
In A's move constructor...              // what happened here ???
In A's destructor...                    // destory which?            
In A's move constructor...              // w/o NRVO, need another construction to get obj1?
In A's destructor...                    // destory which?
In A's destructor...                    // destory which?

In make_obj...                            
In A's constructor...                   // make tmp obj
In A's move constructor...              // w/o RVO, need another construction to get return value?
In A's destructor...                    // destory the temp obj in make_obj scope?            
In A's move constructor...              // what happened here ???
In make_obj_by_copy...                
In A's copy constructor...              // call the copy constructor like expected
In A's move constructor...              // what happened here ???
In A's destructor...                    // destory which?            
In A's move constructor...              // w/o NRVO, need another construction to get obj2?
In A's destructor...                    // destory which?
In A's destructor...                    // destory which?
In A's destructor...                    // destory which?            

In A's destructor...                    // destory obj1/obj2
In A's destructor...                    // destory obj2/obj1

What exactly happened here? Why have many unexpected move constructions and destructions been called? I tried to explain some of the output, but there are still many lines of output that I don't understand. Can you help me? Thank you very much.


Solution

  • In the absence of any copy elision, there are usually three objects involved in returning from a function:

    1. The object you pass to the return keyword
    2. The function's return value
    3. The object that gets initialized by the return value

    Copy elision lets the compiler collapse all three of those objects into one in the correct circumstances.


    I've annotated your program's output to show exactly which objects each line is talking about:

    In make_obj...                                
    In A's constructor...         // create function-local temporary object
    In A's move constructor...    // construct make_obj's return value by move from the function local temp object
    In A's destructor...          // destroy the function-local temp object
    In make_obj_by_move...
    In A's move constructor...    // create function-local temporary object
    In A's move constructor...    // construct make_obj_by_move's return value by move from the function-local temp object
    In A's destructor...          // destroy the function-local temp object
    In A's move constructor...    // construct obj1 in main by move from make_obj_by_move's return object
    In A's destructor...          // destroy make_obj_by_move's return object
    In A's destructor...          // destroy make_obj's return object
    
    In make_obj...
    In A's constructor...         // create function-local temporary object
    In A's move constructor...    // construct make_obj's return value by move from the function local temp object
    In A's destructor...          // destroy the function-local temp object
    In A's move constructor...    // construct make_obj_by_copy's source parameter by move from make_obj's return object
    In make_obj_by_copy...                
    In A's copy constructor...    // create function-local temp object by copy from the source parameter
    In A's move constructor...    // construct make_obj_by_copy's return value by move from the function-local temp object
    In A's destructor...          // destroy the function-local temp object
    In A's move constructor...    // construct obj2 in main by move from make_obj_by_move's return object
    In A's destructor...          // destroy make_obj_by_copy's return object
    In A's destructor...          // destroy make_obj_by_copy's source parameter
    In A's destructor...          // destroy make_obj's return object
    
    In A's destructor...          // destroy obj2
    In A's destructor...          // destroy obj1
    

    As a side-note, in these sorts of situations it can be useful to print the value of the this pointer in each of your instrumented operations so that you can easily see exactly which object the operation was performed on.