Ответ 1
Итак, скажем, у вас есть:
A compute()
{
A v;
…
return v;
}
И вы делаете:
A a = compute();
Есть две передачи (копирование или перемещение), которые участвуют в этом выражении. Сначала объект, обозначенный функцией v
в функции, должен быть перенесен в результат функции, т.е. Значение, переданное выражением compute()
. Позвольте называть это Перенос 1. Затем этот временный объект передается для создания объекта, обозначенного символом a
- Transfer 2.
Во многих случаях как перевод 1, так и 2 могут быть отменены компилятором - объект v
создается непосредственно в местоположении a
, и передача не требуется. В этом примере компилятор должен использовать Именованную Оптимизацию возвращаемых значений для Переноса 1, потому что названный объект называется. Однако, если мы отключим копирование/перемещение, каждая передача включает вызов либо конструктора копирования, либо его конструктора перемещения. В большинстве современных компиляторов компилятор увидит, что v
вот-вот будет уничтожен, и он сначала перенесет его в возвращаемое значение. Затем это временное возвращаемое значение будет перемещено в a
. Если a
не имеет конструктора перемещения, он будет скопирован для обеих переносов.
Теперь посмотрим:
A compute(A&& v)
{
return v;
}
Возвращаемое значение происходит от ссылки, передаваемой в функцию. Компилятор не просто предполагает, что v
является временным и что он может перейти от него 1. В этом случае Transfer 1 будет копией. Тогда Transfer 2 будет двигаться - это хорошо, потому что возвращаемое значение все еще временное (мы не возвращали ссылку). Но так как мы знаем, что мы взяли объект, из которого мы можем перейти, потому что наш параметр является ссылкой rvalue, мы можем прямо сказать компилятору относиться к v
как временному с помощью std::move
:
A compute(A&& v)
{
return std::move(v);
}
Теперь оба Transfer 1 и Transfer 2 будут перемещаться.
1 Причина, по которой компилятор автоматически не обрабатывает v
, определяемый как A&&
, поскольку значение r является безопасным. Это не слишком глупо, чтобы понять это. После того, как объект имеет имя, его можно передать несколько раз на протяжении всего кода. Рассмотрим:
A compute(A&& a)
{
doSomething(a);
doSomethingElse(a);
}
Если a
автоматически обрабатывается как rvalue, doSomething
может свободно разорвать его кишки, что означает, что a
передается doSomethingElse
может быть недействительным. Даже если doSomething
принял свой аргумент по значению, объект будет перенесен из и, следовательно, недействителен в следующей строке. Чтобы избежать этой проблемы, названные ссылки rvalue являются lvalues. Это означает, что при вызове doSomething
a
в худшем случае будет скопирован из, если не просто принят ссылкой lvalue - он все равно будет действителен в следующей строке.
Автору compute
следует сказать: "Хорошо, теперь я разрешаю переносить это значение, потому что я точно знаю, что это временный объект". Вы делаете это, говоря std::move(a)
. Например, вы можете предоставить doSomething
копию, а затем разрешить doSomethingElse
перейти от нее:
A compute(A&& a)
{
doSomething(a);
doSomethingElse(std::move(a));
}