Не используйте std::move при возврате объектов

Конструкторы и операторы перемещения являются важным нововведением С++11. Про них много написано, и я не стану повторять основы. Вместо этого я хотел бы сосредоточиться на таком вопросе, как возвращение объектов. Нужно ли для этого использовать функцию std::move или нет?

До С++11 все объекты возвращались так:

return myobject;

Нужно ли теперь использовать стиль:

return std::move(myobject);

или же он является избыточным?

Для ответа на свой вопрос я решил провести серию тестов с использованием различных компиляторов. Замечу сразу, что во всех тестах использовались ключи по умолчанию, без «агрессивной» оптимизации. Ниже приводится исходный код класса, который поможет нам в их проведении.

class MyClass
{
public:
  MyClass() { printf("Constructor\r\n"); };
  MyClass(const MyClass&) { printf("Copy constructor\r\n");};
  MyClass(MyClass&&) { printf("Move constructor\r\n");};
  
  ~MyClass() { printf("Destructor\r\n");};
  
  MyClass& operator = (const MyClass&)
  {
    printf("Copy =\r\n");
    return *this;
  };
  
  MyClass& operator = (MyClass&&)
  {
    printf("Move = \r\n");
    return *this;
  };
};

С помощью него мы легко сможем отслеживать когда и какой метод вызывается. Также напишем две тестовые функции:

MyClass getCopyMyClass()
{
  MyClass obj;
  return obj;
}

MyClass getMoveMyClass()
{
  MyClass obj;
  return std::move(obj);
}

Они реализуют разные способы возврата объекта. Изначально я предполагал, что getCopyMyClass будет использовать конструктор копирования, а getMoveMyClass — конструктор перемещения. Но мои предположения оказались ошибочными.

Код ниже реализует само тестирование

int main()
{	
  printf("getCopyMyClass\r\n");
  MyClass myc1 = getCopyMyClass();
  printf("_____________\r\n");
  myc1 = getCopyMyClass();
  
  printf("\r\ngetMoveMyClass\r\n");
  MyClass myc2 = getMoveMyClass();
  printf("_____________\r\n");
  myc2 = getMoveMyClass();

  printf("\r\n");	
  return 0;
}

Полный исходный текст файла и makefile его сборки (для gcc) вы можете скачать по ссылке.

После того как тестовая программа готова, мы можем перейти к самим тестам и анализу их результатов.

 

MS VS 2017 и Embarcadero RAD Studio 10

Компиляторы, входящие в состав Microsoft Visual Studio 2017 Community Edition и Embarcadero RAD Stusio 10, ведут себя одинаково и в данном случае выдают один и тот же результат. Он приведен ниже

getCopyMyClass
Constructor
Move constructor
Destructor
_____________
Constructor
Move constructor
Destructor
Move =
Destructor

getMoveMyClass
Constructor
Move constructor
Destructor
_____________
Constructor
Move constructor
Destructor
Move =
Destructor

Destructor
Destructor

Итак, при выполнении строки

MyClass myc1 = getCopyMyClass();

хронология вызовов выглядит следующим образом:

  • классический конструктор при создании объекта obj внутри функции getCopyMyClass;
  • конструктор перемещения для создания объекта myc1 на основе obj;
  • деструктор для уничтожения obj.

При выполнении же строки

myc1 = getCopyMyClass();

последовательность вызовов выглядит несколько сложнее:

  • классический конструктор при создании объекта obj внутри функции getCopyMyClass;
  • конструктор перемещения при создании временного объекта, возвращаемого функцией getCopyMyClass;
  • деструктор для уничтожения obj;
  • оператор присваивания перемещением, в нём классу myc1 присваивается временный объект;
  • деструктор для уничтожения временного объекта.

Из вывода программы видно, что замена getCopyMyClass на getMoveMyClass ничего не меняет. Результаты остаются такими же.

Последние два вызова деструктора уничтожают объекты myc1 и myc2.

Таким образом, можно сделать вывод: для компиляторов, входящих в состав Microsoft Visual Studio 2017 Community Edition и Embarcadero RAD Studio 10, нет никакой разницы — используете вы std::move или нет. Это никак не сказывается на их поведении.

 

GCC
Гораздо интереснее дело обстоит с GCC (проверялись Linux- и Windows-версии). Его результаты показаны ниже

getCopyMyClass
Constructor
_____________
Constructor
Move = 
Destructor

getMoveMyClass
Constructor
Move constructor
Destructor
_____________
Constructor
Move constructor
Destructor
Move = 
Destructor

Destructor
Destructor

При выполнении строки

MyClass myc1 = getCopyMyClass();

вызывается только классический конструктор. Но как же объект obj? Может быть GCC встроил функцию getCopyMyClass в место ее вызова? Необязательно.

Чтобы понять происходящее нужно вспомнить, как в C++ осуществляется возврат объектов. Поскольку размер объектов как правило превышает размер регистров процессора, то последние не используются. Вместо них в функцию передается указатель на участок памяти, в котором следует разместить возвращаемый объект. Поскольку GCC видит, что obj будет возвращен вовне, то он сразу располагает его там, где нужно, т.е. на месте объекта myc1. Благодаря этому ему нет нужды вызывать конструктор перемещения и деструктор.

А вот в строке

myc1 = getCopyMyClass();

без промежуточного объекта не обойтись, и GCC его создает. Последовательность вызовов выглядит так:

  • классический конструктор промежуточного объекта;
  • оператор присваивания перемещением;
  • деструктор для уничтожения промежуточного объекта.

Всё это касалось функции getCopyMyClass. С getMoveMyClass такой трюк уже не проходит. Функция std::move заставляет GCC переместить объект, поэтому он вынужден дополнительно создавать объекты. Вообще, как видно из результатов тестов, в случае функции getMoveMyClass GCC ведет себя так же, как и его собратья из мира Windows.

Получается, что в GCC использование функции std::move при возврате объектов не только не помогает, но даже вредит, вынуждая GCC создавать лишние объекты.

 

Заключение

Как показали мои эксперименты: вызывать функцию std::move при возврате объектов не нужно. Ее использование не только не ускоряет работу программы, но и в некоторых случаях даже замедляет ее. Однако справедливости ради нужно сделать два замечания:

  1. Тесты проводились на простых примерах. В более сложных случаях мы можем получить другие результаты.
  2. Проверялись не все компиляторы С++. Не исключено, что другие компиляторы ведут себя иначе.

Конечно эти случаи маловероятны, но и полностью исключать их нельзя.

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

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