Подтвердить что ты не робот

Return by rvalue reference

A С++ Следующий пост в блоге сказал, что

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?

4b9b3361

Ответ 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));
}

Ответ 2

Неявное перемещение результатов функции возможно только для автоматических объектов. Параметр ссылки rvalue не обозначает автоматический объект, поэтому вы должны явно запросить перемещение в этом случае.

Ответ 3

Первый использует NVRO, который даже лучше, чем перемещение. Никакая копия не лучше дешевой.

Второй не может воспользоваться NVRO. Предполагая, что исключение, return temp; вызовет конструктор копирования, а return std::move(temp); вызовет конструктор перемещения. Теперь я считаю, что у любого из них есть равный потенциал, который можно было бы отклонить, и поэтому вы должны пойти с более дешевым, если не отказаться, используя std::move.