Search code examples
c++c++11c++17return-value-optimizationnrvo

NRVO. Turning off elision. C++11 vs C++17


I am looking at copy elision and RVO/NRVO. When I run g++ with -fno-elide-constructors I see different behaviour for c++11 and c++17.

I understand that c++17 mandates RVO under certain circumstances. I would have assumed that the below counts as NRVO (unless the compiler first eliminates the named variable y too?)

struct rvo 
{       
        rvo () : val {0} { std::cout << "Default Constructor" << std::endl;};
        ~rvo (){ std::cout << "Destructor" << std::endl;        };
        rvo(const rvo& in){ std::cout << "Copy Constructor" << std::endl; }
        rvo(rvo&& in){ std::cout << "Move Constructor" << std::endl; }
        
        double val;
};

rvo f1(rvo& x){ 
        rvo y {x};
        y.val++;
        return y;
}

int main()
{
        rvo A{};
        rvo B {f1(A)};
        return 0;
}

Output when compiled up with no additional flags is as expected

Default Constructor
Copy Constructor
Destructor
Destructor

When compiled up with -fno-elide-constructors and --std=c++11 the output is

Default Constructor
Copy Constructor
Move Constructor
Destructor
Move Constructor
Destructor
Destructor
Destructor

And this is fine as the temporary is being created.

My question is that when I compile up with -fno-elide-constructors and --std=c++17, it eliminates one of the moves.

Default Constructor
Copy Constructor
Move Constructor
Destructor
Destructor
Destructor

It appears that it is eliminating the temporary return variable, and doing a move from the function variable y back to B in main. Even though I have switched off elision. Is it considering it as mandatory RVO or am I misunderstanding something basic?

I found this example, RVO/NRVO can not be disabled in C++17 in Clang++? but that one is obvious RVO whereas I thought my example was NRVO and thus not mandatory.

Thanks very much if anyone can confirm.


Solution

  • This is because from there is mandatory copy elision and instead the object B is created directly from the return value f(A) so that there is no use of a move constructor or a copy ctor and hence the output.

    This can be seen from copy elision documentation:

    Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:

    • In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:
    T x = T(T(f())); // only one call to default constructor of T, to initialize x