Во многих случаях при возврате локального из функции, 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;
}
Я тестировал этот код с VC ++ 11 и GCC 4.71, отладка и выпуск (-O2
) конфиг. Копия ctor никогда не вызывается. Ctor перемещения вызывается только VC ++ 11 в конфигурации отладки. На самом деле, с этими компиляторами все в порядке, но, насколько мне известно, RVO не является обязательным.
Однако, если я явно использую move
:
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return std::move(heavy);
}
ход ctor всегда вызывается. Поэтому попытка сделать его «безопасным» делает его еще хуже.
Мои вопросы:
— Почему std::move
предотвратить РВО?
— Когда лучше «надеяться на лучшее» и полагаться на RVO, и когда я должен явно использовать std::move
? Или, другими словами, как я могу позволить оптимизации компилятора выполнять свою работу и по-прежнему обеспечивать перемещение, если RVO не применяется?
Случаи, когда разрешено копировать и перемещать, описаны в разделе 12.8 § 31 Стандарта (версия N3690):
Когда определенные критерии выполнены, реализация может опустить конструкцию копирования / перемещения объекта класса, даже если конструктор, выбранный для операции копирования / перемещения и / или деструктор для объекта, имеет побочные эффекты. В таких случаях реализация рассматривает источник и цель пропущенной операции копирования / перемещения как просто два разных способа обращения к одному и тому же объекту, и уничтожение этого объекта происходит в более поздние времена, когда два объекта были бы уничтожен без оптимизации. Это исключение операций копирования / перемещения, называемых копия elision, разрешается при следующих обстоятельствах (которые могут быть объединены для устранения нескольких копий):
- в
return
оператор в функции с типом возврата класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или предложения catch) с тем же типом cv-unqualified, что и тип возврата функции, copy / Операция перемещения может быть опущена путем создания автоматического объекта непосредственно в возвращаемое значение функции- […]
- когда временный объект класса, который не был связан со ссылкой (12.2), будет скопирован / перемещен в объект класса с тем же типом cv-unqualified, операция копирования / перемещения может быть опущена путем создания временного объекта непосредственно в цель опущенной копии / перемещения
- […]
(Два случая, которые я пропустил, относятся к случаю создания и отлова объектов исключений, которые я считаю менее важными для оптимизации.)
Следовательно, в выражении возврата исключение может произойти, только если выражение имя локальной переменной. Если ты пишешь std::move(var)
, тогда это уже не имя переменной. Поэтому компилятор не может исключить этот шаг, если он должен соответствовать стандарту.
Стефан Т. Лававей говорил об этом в Going Native 2013 и объяснил именно вашу ситуацию и почему следует избегать std::move()
Вот. Начните смотреть с минуты 38:04. По сути, при возврате локальной переменной возвращаемого типа она обычно обрабатывается как значение, поэтому по умолчанию разрешается перемещение.
Как я могу позволить оптимизации компилятора выполнять свою работу и по-прежнему применять перемещение, если RVO не применяется?
Как это:
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return heavy;
}
Преобразование возврата в ход обязательно.