c ++ 11 ¿Optimización del valor de retorno o movimiento? [duplicar]
No entiendo cuándo debo usar std::move
y cuándo debo dejar que el compilador optimice... por ejemplo:
using SerialBuffer = vector< unsigned char >;
// let compiler optimize it
SerialBuffer read( size_t size ) const
{
SerialBuffer buffer( size );
read( begin( buffer ), end( buffer ) );
// Return Value Optimization
return buffer;
}
// explicit move
SerialBuffer read( size_t size ) const
{
SerialBuffer buffer( size );
read( begin( buffer ), end( buffer ) );
return move( buffer );
}
¿Cuál debo usar?
Utilice exclusivamente el primer método:
Foo f()
{
Foo result;
mangle(result);
return result;
}
Esto ya permitirá el uso del constructor de movimientos, si hay alguno disponible. De hecho, una variable local puede vincularse a una referencia de rvalue en una return
declaración precisamente cuando se permite la elisión de copia.
Su segunda versión prohíbe activamente la elisión de copias. La primera versión es universalmente mejor.
Todos los valores de retorno ya están moved
o están optimizados, por lo que no es necesario moverse explícitamente con los valores de retorno.
Los compiladores pueden mover automáticamente el valor de retorno (para optimizar la copia) ¡e incluso optimizar el movimiento!
Sección 12.8 del borrador del estándar n3337 (C++11):
Cuando se cumplen ciertos criterios, una implementación puede omitir la construcción de copiar/mover de un objeto de clase, incluso si el constructor y/o destructor de copiar/mover del objeto tiene efectos secundarios. En tales casos, la implementación trata el origen y el destino de la operación de copiar/mover omitida como simplemente dos formas diferentes de referirse al mismo objeto, y la destrucción de ese objeto ocurre en el último de los momentos en que los dos objetos habrían sido destruido sin la optimización. Esta elisión de operaciones de copiar/mover, llamada elisión de copia , está permitida en las siguientes circunstancias (que pueden combinarse para eliminar múltiples copias):
[...]
Ejemplo :
class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f();
Aquí los criterios de elisión se pueden combinar para eliminar dos llamadas al constructor de copia de la clase
Thing
: la copia del objeto automático localt
en el objeto temporal para el valor de retorno de la funciónf()
y la copia de ese objeto temporal en el objetot2
. Efectivamente,t
se puede considerar que la construcción del objeto local inicializa directamente el objeto globalt2
, y la destrucción de ese objeto se producirá al salir del programa. Agregar un constructor de movimientoThing
tiene el mismo efecto, pero es la construcción de movimiento desde el objeto temporal lat2
que se omite. - fin del ejemplo ]Cuando se cumplen o se cumplirían los criterios para la elisión de una operación de copia, excepto 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 valor l, la resolución de sobrecarga para seleccionar el constructor para la copia es se realizó por primera vez como si el objeto estuviera designado por un valor r. Si la resolución de sobrecarga falla, o si el tipo del primer parámetro del constructor seleccionado no es una referencia rvalue al tipo de objeto (posiblemente calificado por cv), la resolución de sobrecarga se realiza nuevamente, considerando el objeto como un valor l.
Es bastante simple.
return buffer;
Si se hace esto, entonces se producirá la NRVO o no. Si no sucede, buffer
será trasladado.
return std::move( buffer );
Si hace esto, la NVRO no se producirá y buffer
será eliminada.
Así que no hay nada que ganar usándolo std::move
aquí y mucho que perder.
Hay una excepción* a la regla anterior:
Buffer read(Buffer&& buffer) { //... return std::move( buffer ); }
Si
buffer
es una referencia de valor, entonces deberías usarstd::move
. Esto se debe a que las referencias no son elegibles para NRVO, por lo que sinstd::move
ellas se obtendría una copia de un valor l.Este es solo un ejemplo de la regla "siempre
move
referencias de valor yforward
referencias universales", que tiene prioridad sobre la regla "nuncamove
un valor de retorno".
* A partir de C++20 esta excepción se puede olvidar. Las referencias a Rvalue en return
las declaraciones se mueven implícitamente desde ahora.
Si devuelve una variable local, no utilice move()
. Esto permitirá que el compilador use NRVO y, en su defecto, el compilador aún podrá realizar un movimiento (las variables locales se convierten en valores R dentro de una return
declaración). Usarlo move()
en ese contexto simplemente inhibiría NRVO y obligaría al compilador a usar un movimiento (o una copia si el movimiento no está disponible). Si estás devolviendo algo que no sea una variable local, NRVO no es una opción de todos modos y debes usarlo move()
si (y sólo si) tienes la intención de robar el objeto.