c++ - retorno - tipos de funciones en c
¿Cuándo se debería usar std:: move en un valor de retorno de función? (6)
El movimiento es innecesario en ambos casos. En el segundo caso, std::move
es superfluo porque está devolviendo una variable local por valor, y el compilador entenderá que ya que no va a usar esa variable local nunca más, se puede mover en lugar de copiarse.
Esta pregunta ya tiene una respuesta aquí:
En este caso
struct Foo {};
Foo meh() {
return std::move(Foo());
}
Estoy bastante seguro de que el movimiento es innecesario, porque el Foo
recién creado será un xvalor.
Pero, ¿qué pasa en casos como estos?
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?
}
Allí se necesita el movimiento, supongo.
En el caso de return std::move(foo);
el move
es superfluo debido a 12.8 / 32:
Cuando se cumplen o se cumplirían los criterios para elisión de una operación de copia, salvo por el hecho de que el objeto fuente es un parámetro de función, y el objeto que se va a copiar está designado por un lvalue, la resolución de sobrecarga para seleccionar el constructor para la copia es primero se realiza como si el objeto fuera designado por un valor r.
return foo;
es un caso de NRVO, por lo que se permite la elisión de copia. foo
es un lvalue Por lo tanto, se requiere que el constructor seleccionado para la "copia" de foo
al valor de retorno de meh
sea el constructor de movimientos, si existe.
Sin embargo, agregar move
sí tiene un efecto potencial: evita que se elimine el movimiento, porque return std::move(foo);
no es elegible para NRVO.
Hasta donde yo sé, 12.8 / 32 establece las únicas condiciones bajo las cuales una copia de un lvalue puede ser reemplazada por un movimiento. El compilador no tiene permitido en general detectar que un lvalue no se utiliza después de la copia (usando DFA, por ejemplo) y realizar el cambio por iniciativa propia. Supongo que hay una diferencia observable entre los dos: si el comportamiento observable es el mismo, se aplica la regla "como si".
Por lo tanto, para responder a la pregunta en el título, use std::move
en un valor de retorno cuando desee que se mueva y de todos modos no se moverá. Es decir:
- quieres que se mueva, y
- es un lvalue, y
- no es elegible para la elisión de copia, y
- no es el nombre de un parámetro de función de valor por valor.
Teniendo en cuenta que esto es bastante complicado y los movimientos suelen ser baratos, puede decir que en el código sin plantilla puede simplificarlo un poco. Use std::move
cuando:
- quieres que se mueva, y
- es un lvalue, y
- no se puede preocupar por preocuparse por eso.
Al seguir las reglas simplificadas, sacrificas la elisión de un movimiento. Para tipos como std::vector
que son baratos de mover, probablemente nunca lo notarás (y si lo notas puedes optimizar). Para tipos como std::array
que son caros de mover, o para plantillas en las que no tienes idea de si los movimientos son baratos o no, es más probable que te preocupes por preocuparte por ello.
En un valor de retorno, si la expresión de retorno se refiere directamente al nombre de un lvalue local (es decir, en este punto un valor x) no es necesario el std::move
. Por otro lado, si la expresión de retorno no es el identificador, no se moverá automáticamente, por lo que, por ejemplo, necesitaría el estándar std::move
explícito en este caso:
T foo(bool which) {
T a = ..., b = ...;
return std::move(which? a : b);
// alternatively: return which? std::move(a), std::move(b);
}
Al devolver directamente una variable local nombrada o una expresión temporal, debe evitar el estándar std::move
explícito. El compilador debe (y lo hará en el futuro) moverse automáticamente en esos casos, y agregar std::move
podría afectar otras optimizaciones.
Hay muchas respuestas sobre cuándo no se debe mover, pero la pregunta es "¿cuándo se debe mover?"
Aquí hay un ejemplo artificial de cuándo debería usarse:
std::vector<int> append(std::vector<int>&& v, int x) {
v.push_back(x);
return std::move(v);
}
es decir, cuando tiene una función que toma una referencia rvalue, la modifica y luego devuelve una copia de la misma. Ahora, en la práctica, este diseño casi siempre es mejor:
std::vector<int> append(std::vector<int> v, int x) {
v.push_back(x);
return v;
}
que también le permite tomar parámetros que no sean de valor.
Básicamente, si tiene una referencia rvalue dentro de una función que desea devolver moviendo, debe llamar a std::move
. Si tiene una variable local (ya sea un parámetro o no), al devolverla se move
implícitamente s (y este movimiento implícito puede eliminarse, mientras que un movimiento explícito no puede). Si tiene una función u operación que toma variables locales y devuelve una referencia a dicha variable local, tiene que std::move
para que ocurra movimiento (como un ejemplo, el operador trinary ?:
:).
Un compilador de C ++ es libre de usar std::move(foo)
:
- si se sabe que
foo
está al final de su vida, y - el uso implícito de
std::move
no tendrá ningún efecto en la semántica del código C ++ que no sean los efectos semánticos permitidos por la especificación C ++.
Depende de las capacidades de optimización del compilador de C ++ si puede calcular qué transformaciones de f(foo); foo.~Foo();
f(foo); foo.~Foo();
a f(std::move(foo)); foo.~Foo();
f(std::move(foo)); foo.~Foo();
son rentables en términos de rendimiento o en términos de consumo de memoria, mientras se adhieren a las reglas de especificación de C ++.
Conceptualmente hablando, los compiladores de C ++ del año 2017, como GCC 6.3.0, pueden optimizar este código:
Foo meh() {
Foo foo(args);
foo.method(xyz);
bar();
return foo;
}
en este código:
void meh(Foo *retval) {
new (retval) Foo(arg);
retval->method(xyz);
bar();
}
lo cual evita llamar al constructor de copias y al destructor de Foo
.
Los compiladores de C ++ del año 2017, como GCC 6.3.0, no pueden optimizar estos códigos:
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;
}
en estos códigos:
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;
}
lo que significa que un programador del año 2017 necesita especificar tales optimizaciones de manera explícita.
std::move
es totalmente innecesario cuando regresas de una función, y realmente entra en tu dominio - el programador - tratando de cuidar las cosas que debes dejarle al compilador.
¿Qué ocurre cuando se establece std::move
algo fuera de una función que no es una variable local para esa función? Puedes decir que nunca escribirás un código como ese, pero qué sucede si escribes código que está bien, y luego lo refactorizas y sin distracciones no cambias el std::move
. Te divertirás rastreando ese error.
El compilador, en cambio, es incapaz de cometer este tipo de errores.
Además: Es importante tener en cuenta que devolver una variable local desde una función no necesariamente crea un valor r o usa la semántica de movimiento.