Когда следует использовать std:: move для возвращаемого значения функции?

В этом случае

struct Foo {};
Foo meh() {
  return std::move(Foo());
}

Я уверен, что этот шаг не нужен, потому что созданный Foo будет значением xvalue.

Но что в подобных случаях?

struct Foo {};
Foo meh() {
  Foo foo;
  //do something, but knowing that foo can safely be disposed of
  //but does the compiler necessarily know it?
  //we may have references/pointers to foo. how could the compiler know?
  return std::move(foo); //so here the move is needed, right?
}

Нужно ли двигаться, я полагаю?

Ответ 1

В случае return std::move(foo); move является излишним из-за 12.8/32:

Когда критерии для исключения операции копирования выполняются или будут сэкономленные за тот факт, что исходный объект является параметром функции, и подлежащий копированию объект обозначается значением lvalue, перегрузкой разрешение для выбора конструктора для копии сначала выполняется как если объект был обозначен rvalue.

return foo; - это случай NRVO, поэтому копирование разрешено. foo - значение l. Поэтому конструктор, выбранный для "копирования" из foo в возвращаемое значение meh, должен быть конструктором перемещения, если он существует.

Добавление move действительно имеет потенциальный эффект: оно предотвращает отклонение перемещения, потому что return std::move(foo); не подходит для NRVO.

Насколько я знаю, 12.8/32 излагают единственные условия, при которых копия из lvalue может быть заменена движением. Компилятору вообще не разрешено обнаруживать, что lvalue не используется после копирования (используя DFA, скажем), и вносить изменения по собственной инициативе. Я предполагаю, что существует наблюдаемая разница между ними - если наблюдаемое поведение одинаково, применяется правило "как есть".

Итак, чтобы ответить на вопрос в заголовке, используйте std::move для возвращаемого значения, когда вы хотите, чтобы он был перемещен, и он не будет перемещаться в любом случае. То есть:

  • вы хотите, чтобы он был перемещен, и
  • это значение l, и
  • он не подходит для копирования, и
  • это не имя параметра функции по-значения.

Учитывая, что это довольно сложно, и ходы обычно дешевы, вы можете сказать, что в коде без шаблонов вы можете немного упростить это. Используйте std::move, когда:

  • вы хотите, чтобы он был перемещен, и
  • это значение l, и
  • Вы не можете беспокоиться об этом.

Следуя упрощенным правилам, вы жертвуете некоторым движением. Для таких типов, как std::vector, которые дешевы для перемещения, вы, вероятно, никогда не заметите (и если вы заметите, что можете оптимизировать). Для таких типов, как std::array, которые дороги для перемещения, или для шаблонов, где вы не представляете, дешевы ли движения или нет, вы, скорее всего, будете обеспокоены этим.

Ответ 2

В обоих случаях переход не нужен. Во втором случае std::move является излишним, потому что вы возвращаете локальную переменную по значению, и компилятор поймет, что, поскольку вы больше не собираетесь использовать эту локальную переменную, ее можно перенести, а не скопировать.

Ответ 3

В возвращаемом значении, если выражение return ссылается непосредственно на имя локального lvalue (т.е. в этой точке x значение), нет необходимости в std::move. С другой стороны, если выражение return не идентификатор, оно не будет перемещено автоматически, поэтому, например, вам понадобится явный std::move в этом случае:

T foo(bool which) {
   T a = ..., b = ...;
   return std::move(which? a : b);
   // alternatively: return which? std::move(a), std::move(b);
}

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

Ответ 4

Есть много ответов о том, когда он не должен быть перемещен, но вопрос в том, "когда он должен быть перемещен?"

Вот надуманный пример того, когда он должен использоваться:

std::vector<int> append(std::vector<int>&& v, int x) {
  v.push_back(x);
  return std::move(v);
}

т.е. когда у вас есть функция, которая принимает ссылку rvalue, ее модифицирует, а затем возвращает ее копию. Теперь на практике этот дизайн почти всегда лучше:

std::vector<int> append(std::vector<int> v, int x) {
  v.push_back(x);
  return v;
}

который также позволяет принимать параметры не-rvalue.

В принципе, если у вас есть ссылка rvalue в функции, которую вы хотите вернуть, переместившись, вы должны вызвать std::move. Если у вас есть локальная переменная (будь то параметр или нет), возвращающая ее неявно move (и это неявное перемещение может исчезнуть, а явное перемещение не может). Если у вас есть функция или операция, которая принимает локальные переменные и возвращает ссылку на указанную локальную переменную, вам нужно std::move, чтобы получить перемещение (например, оператор trinary ?:).

Ответ 5

Компилятор С++ может использовать std::move(foo):

  • если известно, что foo находится в конце его жизни, а
  • неявное использование std::move не будет влиять на семантику кода на С++, кроме семантических эффектов, разрешенных спецификацией С++.

Это зависит от возможностей оптимизации компилятора С++, может ли он вычислить, какие преобразования от f(foo); foo.~Foo(); до f(std::move(foo)); foo.~Foo(); являются прибыльными с точки зрения производительности или с точки зрения потребления памяти при соблюдении правил спецификации С++.


Концептуально говорящий, год-2017 Компиляторы С++, такие как GCC 6.3.0, могут оптимизировать этот код:

Foo meh() {
    Foo foo(args);
    foo.method(xyz);
    bar();
    return foo;
}

в этот код:

void meh(Foo *retval) {
   new (retval) Foo(arg);
   retval->method(xyz);
   bar();
}

который позволяет вызвать вызов-конструктор и деструктор foo.


Year-2017 Компиляторы С++, такие как GCC 6.3.0, не могут оптимизировать эти коды:

Foo meh_value() {
    Foo foo(args);
    Foo retval(foo);
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(*foo);
    delete foo;
    return retval;
}

в эти коды:

Foo meh_value() {
    Foo foo(args);
    Foo retval(std::move(foo));
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(std::move(*foo));
    delete foo;
    return retval;
}

что означает, что программист года-2017 должен явно указывать такие оптимизации.

Ответ 6

std::move совершенно необязательно при возврате из функции и действительно попадает в сферу вас - программист - пытается прислушиваться к вещам, которые вы должны оставить компилятору.

Что происходит, когда вы std::move что-то из функции, которая не является локальной переменной для этой функции? Вы можете сказать, что вы никогда не будете писать такой код, но что произойдет, если вы напишете код, который просто отлично, а затем реорганизуйте его и рассеянно не измените std::move. Вам будет весело отслеживать эту ошибку.

Компилятор, с другой стороны, в основном неспособен совершать такие ошибки.

Также: важно отметить, что при возврате локальной переменной из функции не обязательно создайте rvalue или используйте семантику перемещения.

Смотрите здесь.