Let's see how RVO C++ works in gcc

In this short article, I propose to look at how the RVO (return value optimization) principle works in the gcc compiler. The author of the article does not claim uniqueness or any novelty. It is aimed at beginners and is more of a note.

So, let's look at the class and the code that uses it:

#include <iostream>

// Для предотвращения закрытия окна консоли сразу после всех вычислений
void WaitForAnyKey()
{
    static std::string q;
    std::cout << "Press any key to continue...\n";
    std::cin >> q;
}

namespace my
{
	
using std::cout;
using std::cin;
using std::endl;

// Класс для эксперимента
struct Dummy 
{
    Dummy() { cout << "[" << ((unsigned)this) << "]: " << "c'tor" << endl; }
    ~Dummy() { cout << "[" << ((unsigned)this) << "]: " << "d'tor" << endl; }

  Dummy(const Dummy& oth) 
  { 
    cout << "[" << ((unsigned)this) << "]: " << "copy c'tor from ["
				<< ((unsigned)&oth) << "]" << endl; 
  }
    
  Dummy(Dummy&& oth) 
  { 
    cout << "[" << ((unsigned)this) << "]: " << "move c'tor from ["
				<< ((unsigned)&oth) << "]" << endl; 
  }

  Dummy& operator=(const Dummy& oth) 
  {
    cout << "[" << ((unsigned)this) << "]: " << "copy assignment from ["
				<< ((unsigned)&oth) << "]" << endl;
    return *this;
  }

  Dummy& operator=(Dummy&& oth) 
  {
    cout << "[" << ((unsigned)this) << "]: " << "move assignment from ["
				<< ((unsigned)&oth) << "]" << endl;
    return *this;
  }
};

Dummy ExampleRVO() 
{
  return Dummy();
}

void test()
{
  cout << "my::test()\n";
  cout << "==========\n";

  {
    ExampleRVO();
  }
			

  cout << "my::test() -- end\n";
  cout << "=================\n";

}
  
} // end of my

int main()
{
	my::test();
	WaitForAnyKey();
	return 0;
}

I used the output of this and oth addresses to make it clear which object's constructor is being called and which object's reference is being passed. This thing is compiled in gcc like this. A file is saved with a name, say sample01.cpp, and the following command is called on the command line:
g++ sample01.cpp -osample01 -std=c++11 -fno-elide-constructors -fpermissive

where the -fno-elide-constructors option suppresses RVO, and -fpermissive allows you to ignore errors in casting this to unsigned.
The output after running the assembled application is:

my::test()
==========
[2453666255]: c'tor
[2453666335]: move c'tor from [2453666255]
[2453666255]: d'tor
[2453666335]: d'tor
my::test() -- end
=================
Press any key to continue...

You can see that return Dummy() calls the default constructor when creating an object at address 2453666255, which is then passed to the move constructor to create an object at address 2453666335. The call to ExampleRVO() is placed in a block so that the temporary returned objects are destroyed when the block exits into the body of the test() function and we were able to see the work of the destructors. An object with address 2453666255 exists from the moment return is called after the construction of this object and until the end of the moving constructor, called when creating an object with address 2453666335. When leaving the body of the moving constructor of an object with address 2453666335, the destructor of the object with address 2453666255 is called. Then, when exiting of the block in which the ExampleRVO() function is called, the destructor is also called for the object with address 2453666335.

The fact that the ExampleRVO() function returns an object to nowhere does not mean that it does not exist. It is there – on the stack of the test() function, but without a name.

Now let's add a local object to the test() function with an explicit name:

Dummy dummy = ExampleRVO();
cout << "[" << ((unsigned)&dummy) << "]: in test()" << endl;
my::test()
==========
[2164258975]: c'tor
[2164259055]: move c'tor from [2164258975]
[2164258975]: d'tor
[2164259054]: move c'tor from [2164259055]
[2164259055]: d'tor
[2164259054]: in test()
[2164259054]: d'tor
my::test() -- end
=================
Press any key to continue...

The addresses changed because the rebuilt application was restarted. But it is clear that a third object has been added, which is the same local dummy variable with address 2164259054. It turns out that the ExampleRVO() function first constructs an invisible nameless object on the test() stack (in this case, an object with address 2164259055) from an object with address 2164258975, and only then uses an object with address 2164259055 to construct a named local dummy object with address 2164259054. Very strange behavior in terms of optimization. “Very Strange Situation!”

It is precisely because of such things that the concept of RVO was invented and introduced. It is clear that if we have an expression for constructing a local object by the object that the ExampleRVO() function returns (which itself, in turn, returns what the constructor returns), there is no need to create invisible nameless objects. So, now we assemble the project with the following line:

g++ sample01.cpp -osample01 -std=c++11 -fpermissive

my::test()
==========
[4223663343]: c'tor
[4223663343]: in test()
[4223663343]: d'tor
my::test() -- end
=================
Press any key to continue...

Beauty! We are now dealing only with a named object, which is what we expect.
Thank you for reading! Have a nice day!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *