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

от автора

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

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

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

Я использовал вывод 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(), но без имени.

Теперь добавим локальный объект в функции 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...

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


ссылка на оригинал статьи https://habr.com/ru/articles/798677/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *