Когда следует использовать 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 или используйте семантику перемещения.
Смотрите здесь.