Должен ли я вернуть ссылку на 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() в этом случае.

Другие вопросы по тегам