class foo
{
public:
foo() { std::cout << "called constructor\n"; }
foo( const foo& other ) { std::cout << "called copy constructor\n"; }
~foo() { std::cout << "destructor called\n"; }
};
foo f()
{
std::cout << "-- inside f() before constructing foo\n";
foo blub; // 2) Calling Constructor
std::cout << "-- inside f() after constructing foo\n";
return blub;
}
int main()
{
foo foo1 = f(); // 1) foo is created directly here
return 0;
}
Output:
-- inside f() before constructing foo
called constructor
-- inside f() after constructing foo
destructor called
As we can see the constructor of foo is only called once this means it must have been created directly at the memory location of foo1. I mean when I was learning about RVO a bit i was thinking "Yep, thats normal code no idea why its so special...." and well it depends on the C++ version. Since C++ 17 it is guaranteed that copy elision will be used cppreference. Versions prior to C++ 17 also do this optimisation but it is not guaranteed, that the compiler will do it.
So how can we now see the same code but without copy elision, you may ask. C++ compiler flags to the rescue.. -> -fno-elide-constructors
Output:
-- inside f() before constructing foo
called constructor
-- inside f() after constructing foo
called copy constructor
destructor called
destructor called
Ah now we see some unnecessary copy steps. As we can see in this output this time the object was not created in the memory location of the target location (Same code as before only added flag) and therefore after we create the object in the f() function we have to somehow take it and put it into the target variable foo1 thats when we see the "called copy constructor" (I avoided the word move).
I have to admit that when I learnt about std::move i was thinking to go in every code i have and std::move the fuck everything(Nearly everything) but that is not a smart idea because as we all know std::move does not move anything it just casts it to a rvalue reference.
std::move
is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular,std::move
produces an xvalue expression that identifies its argument t. It is exactly equivalent to astatic_cast
to an rvalue reference type. ---> Without any shame taken from cppreference <---
Okay so what does cppreference tells us, first it does not enforce any casting/moving or whatever it just indicates that an object t MAY be moved. Also it tells us that std::move == static_cast<foo&&>(bar) so std::move is more of a utility function to confuse people...
So what does std::move enables us move semantics check out my other blog for that, but in short. If we have something like a = std::move(b) then we just say okay variable "a" now also points to the resource which variable "b" points to and after that we let "b" point to nullptr basically b is then in a defined state but has no real value to it. Also move constructor and move assignment constructors are playing a role but this blog is about rvo and nrvo.
So now lets try the same code as above without the compiler flag -fno-elide-constructors But with a std::move. I will just show the changed code not the whole file again.
foo f()
{
std::cout << "-- inside f() before constructing foo\n";
foo blub;
std::cout << "-- inside f() after constructing foo\n";
return std::move(blub); // Using std::move
}
Output:
-- inside f() before constructing foo
called constructor
-- inside f() after constructing foo
called copy constructor
destructor called
destructor called
**
This is kinda sad now, it is the same output as before when we disabled the copy elision. But the compiler tells us something: Compiler Output:
main.cpp:23:12: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
return std::move(blub);
^
main.cpp:23:12: note: remove std::move call here
return std::move(blub);
^~~~~~~~~~ ~
1 warning generated.
Hmm looks interesting "moving a local object in a return statement prevents copy eilision" that ladies and gentlemen is a good compiler warning. This warning would develop to a error if we would delete the move constructor. This is also a approach to delete all additional constructors and let the compiler tell you when a copy/move or whatever would be used.
So what went wrong? To make the answer short its the rule of 5. Because i defined a copy constructor and destructor that means i want some special handling for my class and that most probably means i would need some special handling for the other constructors and therefore C++ does not provide default ones. Check cppreference
Even if we would provide a custom move constructor then yes we would call it but we would still create a additional object which moves it to the location of the target variable.
We can learn from that, that RVO, NRVO is better than anything else.
foo f_rvo()
{
std::cout << "-- inside f_rvo()\n";
return foo();
}
Output:
-- inside f_rvo()
called constructor
destructor called
Same as with NRVO the only difference is the named part.
I want to go in a little bit more detail regarding the "NAMED return ..."
If we have for example such a code:
foo f_nrvo_multiple_paths( int32_t x )
{
std::cout << "-- inside f() before constructing foo\n";
if( x % 2 == 0 )
{
std::cout << "Foo1\n";
foo foo1;
return foo1;
}
std::cout << "Foo2\n";
foo foo2;
return foo2;
}
Output:
-- inside f() before constructing foo
Foo1
called constructor
called copy constructor
destructor called
destructor called
If the move constructor would be enabled we would see:
-- inside f() before constructing foo
Foo1
called constructor
Called move constructor
destructor called
destructor called
That means the compiler tries first always to move because it is cheaper than a copy.
The difference here is that we return different foo instances from different paths. Depending of the if statement.
The same code created two different outputs on different machines. On Mac copy elision was executed but on linux you see the output from above. Just in case someone wants to see the mac output.
-- inside f() before constructing foo
Foo1
called constructor
destructor called
With a little change in the code from above we can again get copy elision
foo f_nrvo_multiple_paths( int32_t x )
{
foo foo12; // <--- moved variable to the top
std::cout << "-- inside f() before constructing foo\n";
if( x % 2 == 0 )
{
// do stuff with foo12;
return foo12;
}
// do stuff with foo12;
return foo12;
}
Code:
foo f_nrvo_multiple_paths_working( int32_t x )
{
std::cout << "-- inside f() before constructing foo\n";
foo foo12;
if( x % 2 == 0 )
{
foo12.i = 123;
return foo12;
}
foo12.i = 1222;
return foo12;
}
int main()
{
foo bar;
bar = f_nrvo_multiple_paths_working( 10 );
return 0;
}
Output:
called constructor
-- inside f() before constructing foo
called constructor
called assignemnt constructor
destructor called
destructor called
Because we construct the object prior the call to the function and additional we create a second object in the function itself which is returned the assignment constructor is called. So this brings us to the conclusion to not be a maniac and pass by reference or not creating the object before the function call.
Here is the improved code:
void f_nrvo_multiple_paths_working( foo& foo12, bool flag)
{
std::cout << "-- inside f() before constructing foo\n";
if(flag)
{
foo12.i = 123;
return;
}
foo12.i = 1222;
return;
}
int main()
{
foo bar;
f_nrvo_multiple_paths_working( bar, true );
return 0;
}
Output:
called constructor
-- inside f() before constructing foo
destructor called
C++ Draft book
Better explained rvo
Github repo of the code above