Почему std:: move предотвращает RVO?

Во многих случаях при возврате локального из функции RVO запускается. Однако я думал, что явно использование std::move по крайней мере обеспечит перенос, когда RVO не произойдет, но RVO по-прежнему применяется, когда это возможно. Однако, похоже, это не так.

#include "iostream"

class HeavyWeight
{
public:
    HeavyWeight()
    {
        std::cout << "ctor" << std::endl;
    }

    HeavyWeight(const HeavyWeight& other)
    {
        std::cout << "copy" << std::endl;
    }

    HeavyWeight(HeavyWeight&& other)
    {
        std::cout << "move" << std::endl;
    }
};

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return heavy;
}

int main()
{
    auto heavy = MakeHeavy();
    return 0;
}

Я протестировал этот код с настройками VС++ 11 и GCC 4.71, debug и release (-O2). Копия ctor никогда не вызывается. Перемещение ctor вызывается только VС++ 11 в конфигурации отладки. На самом деле все, кажется, отлично подходит для этих компиляторов, в частности, но, насколько мне известно, RVO является необязательным.

Однако, если я явно использую move:

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return std::move(heavy);
}

перемещение ctor всегда вызывается. Поэтому попытка сделать его "безопасным" делает его еще хуже.

Мои вопросы:
 - Почему std::move предотвращает RVO?
 - Когда лучше "надеяться на лучшее" и полагаться на RVO, и когда я должен явно использовать std::move? Или, другими словами, как я могу позволить оптимизацию компилятора выполнить свою работу и по-прежнему принудительно перемещать, если RVO не применяется?

Ответы

Ответ 1

Случаи, когда разрешение на копирование и перемещение разрешены, содержатся в разделе 12.8 § 31 Стандарта (версия N3690):

При выполнении определенных критериев реализации разрешается опускать конструкцию копирования/перемещения объекта класса, даже если конструктор, выбранный для операции копирования/перемещения и/или деструктор объекта, имеет побочные эффекты. В таких случаях реализация рассматривает источник и цель пропущенной операции копирования/перемещения как просто два разных способа обращения к одному и тому же объекту, а уничтожение этого объекта происходит в более поздние времена, когда эти два объекта были бы разрушен без оптимизации. Это исключение операций копирования/перемещения, называемое копированием, разрешено в следующих случаях (которые могут быть объединены для устранения нескольких копий):

  • в выражении return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимого автоматического объекта (кроме функции или параметра catch-clause) с тем же CV-неквалифицированным типом, что и возвращаемый тип функции, операцию копирования/перемещения можно опустить, построив автоматический объект непосредственно в возвращаемое значение функции
  • [...]
  • когда объект временного класса, который не был привязан к ссылке (12.2), будет скопирован/перенесен в объект класса с тем же самым cv-неквалифицированным типом, операция копирования/перемещения может быть опущена путем непосредственного конструирования временного объекта в цель пропущенной копии/перемещения
  • [...]

(Два случая, которые я оставил, относятся к случаю бросания и ловушки объектов исключения, которые я считаю менее важными для оптимизации.)

Следовательно, в возвратном выражении копирование может возникать только в случае, если выражение имя локальной переменной. Если вы пишете std::move(var), это больше не имя переменной. Поэтому компилятор не может преодолеть ход, если он должен соответствовать стандарту.

Стефан Т. Лававей говорил об этом в Going Native 2013 и объяснил именно вашу ситуацию и почему избежать std::move() здесь. Начните смотреть в минуту 38:04. В принципе, при возврате локальной переменной возвращаемого типа, она обычно обрабатывается как rvalue, что позволяет перемещать по умолчанию.

Ответ 2

как я могу позволить оптимизацию компилятора выполнить свою работу и по-прежнему принудительно перемещать, если RVO не применяется?

Вот так:

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return heavy;
}

Преобразование возврата в ход является обязательным.