В данной небольшой статье я предлагаю рассмотреть как работает принцип 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...
Красота! Мы теперь имеем дело только с именованным объектом, что и ожидается нами.
Спасибо за чтение! Приятного дня!