Должен ли я вернуть ссылку на 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?

17

Решение

Итак, допустим, у вас есть:

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));
}
22

Другие решения

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

5

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

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

5

В C ++ 17 и позже

C ++ 17 изменил определение категорий значений, так что тип описания копий, который вы описываете, гарантированный (увидеть: Как работает гарантия elision?). Таким образом, если вы пишете свой код на C ++ 17 или более поздней версии, вы должны абсолютно не возвращается std::move()«ИНГ в этом случае.

1
По вопросам рекламы [email protected]