Должен ли я вернуть ссылку на rvalue (с помощью std::move'ing)?
C++ Следующий пост в блоге сказал, что
A compute(…)
{
A v;
…
return v;
}
Если A
имеет доступный конструктор копирования или перемещения, компилятор может выбрать удаление копии. В противном случае, если A
имеет конструктор перемещения, v
перемещен В противном случае, если A
имеет конструктор копирования, v
копируется. В противном случае выдается ошибка времени компиляции.
Я думал, что всегда должен возвращать значение без std::move
потому что компилятор сможет определить лучший выбор для пользователей. Но в другом примере из поста в блоге
Matrix operator+(Matrix&& temp, Matrix&& y)
{ temp += y; return std::move(temp); }
Здесь std::move
необходимо, потому что y
должен рассматриваться как lvalue внутри функции.
Ах, моя голова чуть не взорвалась после изучения этого поста. Я изо всех сил старался понять рассуждения, но чем больше я учился, тем больше смущался. Почему мы должны возвращать значение с помощью std::move
?
4 ответа
Итак, допустим, у вас есть:
A compute()
{
A v;
…
return v;
}
И ты делаешь:
A a = compute();
В этом выражении участвуют две передачи (копирование или перемещение). Сначала объект обозначается v
в функции должен быть передан результат функции, то есть значение, пожертвованное compute()
выражение. Давайте назовем это Transfer 1. Затем этот временный объект передается для создания объекта, обозначенного как a
- Трансфер 2.
Во многих случаях как Transfer 1, так и 2 могут быть исключены компилятором - объект v
построен непосредственно в месте a
и перевод не требуется. В этом примере компилятор должен использовать оптимизацию именованных возвращаемых значений для Transfer 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&&
Как ценность это безопасность. Это не слишком глупо, чтобы понять это. Если у объекта есть имя, к нему можно обращаться несколько раз по всему коду. Рассматривать:
A compute(A&& a)
{
doSomething(a);
doSomethingElse(a);
}
Если a
был автоматически обработан как значение, 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));
}
Первый использует преимущество NVRO, которое даже лучше, чем перемещение. Нет копии лучше дешевой.
Второй не может воспользоваться NVRO. Предполагая, что нет выбора, return temp;
вызовет конструктор копирования и return std::move(temp);
вызовет конструктор перемещения. Теперь, я считаю, что любой из них имеет равный потенциал для исключения, и поэтому вы должны пойти с более дешевым, если не исключенным, который использует std::move
,
Неявное перемещение результатов функции возможно только для автоматических объектов. Ссылочный параметр rvalue не обозначает автоматический объект, поэтому в этом случае вы должны явно запросить перемещение.
В C++17 и позже
C++17 изменил определение категорий значений, так что гарантируемый вами тип разрешения на копирование гарантирован (см.: Как работает гарантированное разрешение на копирование?). Таким образом, если вы пишете свой код на C++17 или более поздней версии, вам абсолютно не следует возвращаться std::move()
в этом случае.