Смотрим как работает RVO C++ в gcc

В данной небольшой статье я предлагаю рассмотреть как работает принцип RVO (return value optimization) в компиляторе gcc. Автор статьи не претендует на уникальность и какую-то новизну. Ориентировано на начинающих и представляет собой больше некую заметку.

Итак, рассмотрим класс и код, его использующий:

#include 

// Для предотвращения закрытия окна консоли сразу после всех вычислений
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;
}

Я использовал вывод this и адреса oth для того, чтобы было понятно, конструктор какого именно объекта вызывается и ссылка на какой объект передается. Компилируется это дело в gcc так. Сохраняется файл с именем, скажем sample01.cpp и вызывается в командной строке следующая команда:
g++ sample01.cpp -osample01 -std=c++11 -fno-elide-constructors -fpermissive

где опция -fno-elide-constructors подавляет RVO, а -fpermissive позволяет забить на ошибки приведения this к unsigned.
Вывод после запуска собранного приложения такой:

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

Видно, что return Dummy () вызывает конструктор по умолчанию при создании объекта с адресом 2453666255, который затем передается в перемещающий конструктор для создания объекта с адресом 2453666335. Вызов функции ExampleRVO () помещен в блок, чтобы временные возвращенные объекты был уничтожены при выходе из блока в тело функции test () и мы смогли увидеть работу деструкторов. Объект с адресом 2453666255 существует от момента вызова return после конструирования этого объекта и до конца работы перемещающего конструктора, вызываемого при создании объекта с адресом 2453666335. При выходе из тела перемещающего конструктора объекта с адресом 2453666335, вызывается деструктор объекта с адресом 2453666255. Далее при выходе из блока, в котором происходит вызов функции ExampleRVO (), происходит вызов деструктора и для объекта с адресом 2453666335.

То, что функция ExampleRVO () возвращает объект в никуда вовсе не означает, что его нет. Он есть — на стеке функции test (), но без имени.

d152481e78cd859860098c294b8ec384.png

Теперь добавим локальный объект в функции test () с явным именем:

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

Поменялись адреса, так как был перезапуск перестроенного приложения. Но видно, что добавился некий третий объект, который и есть та самая локальная переменная dummy с адресом 2164259054. Получается, что функция ExampleRVO () конструирует на стеке test () сначала невидимый безымянный объект (в данном случае объект с адресом 2164259055) из объекта с адресом 2164258975, а уже потом использует объект с адресом 2164259055 для конструирования именованного локального объекта dummy с адресом 2164259054. Весьма странное в плане оптимизации поведение. «Вери стрейндж ситуэйшн!».

Именно, вследствие таких вещей и было придумано и введено понятие RVO. Ясно, что если у нас есть выражение конструирования локального объекта тем объектом, что возвращает функция ExampleRVO () (которая сама, в свою очередь, возвращает то, что возвращает конструктор), нет необходимости в создании невидимых безымянных объектов. Итак, собираем теперь проект такой строкой:

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

Красота! Мы теперь имеем дело только с именованным объектом, что и ожидается нами.
Спасибо за чтение! Приятного дня!

© Habrahabr.ru