Sandrino's WEBSITE

   ABOUT  BLOG  


What is RVO and NRVO this is some fancy stuff and it means we omit copying the value and instead it creates it in the memory location of the target variable.

Example of omitting copy and not

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).

std::move enters the chat

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 a static_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.

RVO

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.

NRVO

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

Multiple paths NRVO working solution

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;
}

Bad ways to to things

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

Resources

C++ Draft book
Better explained rvo
Github repo of the code above