Estoy tratando de entender las referencias de valor y mover la semántica de C ++ 11.
¿Cuál es la diferencia entre estos ejemplos y cuál de ellos no va a hacer una copia vectorial?
Primer ejemplo
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Segundo ejemplo
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Tercer ejemplo
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
c++
c++11
move-semantics
rvalue-reference
c++-faq
Tarántula
fuente
fuente
std::move()
creó una "copia" persistente.std::move(expression)
no crea nada, simplemente convierte la expresión a un valor x. No se copian ni mueven objetos en el proceso de evaluaciónstd::move(expression)
.Respuestas:
Primer ejemplo
El primer ejemplo devuelve un temporal que es capturado por
rval_ref
. Ese temporal tendrá su vida extendida más allá de larval_ref
definición y puede usarlo como si lo hubiera captado por valor. Esto es muy similar a lo siguiente:excepto que en mi reescritura obviamente no puedes usar
rval_ref
de una manera no constante.Segundo ejemplo
En el segundo ejemplo, ha creado un error de tiempo de ejecución.
rval_ref
ahora contiene una referencia a lo destruidotmp
dentro de la función. Con suerte, este código se bloqueará inmediatamente.Tercer ejemplo
Su tercer ejemplo es más o menos equivalente al primero. El
std::move
encendidotmp
es innecesario y en realidad puede ser una pesimización del rendimiento, ya que inhibirá la optimización del valor de retorno.La mejor manera de codificar lo que estás haciendo es:
Mejores prácticas
Es decir, como lo haría en C ++ 03.
tmp
se trata implícitamente como un valor r en la declaración de devolución. Se devolverá mediante la optimización del valor de retorno (sin copia, sin movimiento), o si el compilador decide que no puede realizar RVO, utilizará el constructor de movimiento del vector para hacer el retorno . Solo si no se realiza RVO, y si el tipo devuelto no tenía un constructor de movimiento, se usaría el constructor de copia para la devolución.fuente
return my_local;
. Múltiples declaraciones de retorno están bien y no inhibirán RVO.move
no crea un temporal. Lanza un valor l a un valor x, sin hacer copias, sin crear nada, sin destruir nada. Ese ejemplo es exactamente la misma situación que si regresara por lvalue-reference y elimine elmove
de la línea de retorno: De cualquier manera, tiene una referencia colgante a una variable local dentro de la función y que ha sido destruida.Ninguno de ellos copiará, pero el segundo se referirá a un vector destruido. Las referencias de valor nominado casi nunca existen en el código regular. Lo escribes exactamente como habrías escrito una copia en C ++ 03.
Excepto ahora, el vector se mueve. El usuario de una clase no se ocupa de sus referencias de valor en la gran mayoría de los casos.
fuente
tmp
no se mueve hacia adentrorval_ref
, sino que se escribe directamenterval_ref
usando RVO (es decir, copia de elisión). Hay una distinción entrestd::move
y copiar elisión. Astd::move
todavía puede involucrar algunos datos para ser copiados; en el caso de un vector, se construye un nuevo vector en el constructor de la copia y se asignan los datos, pero la mayor parte de la matriz de datos solo se copia copiando el puntero (esencialmente). La elisión de copia evita el 100% de todas las copias.rval_ref
variables se construyen utilizando el constructor de movimiento destd::vector
. No hay constructor de copia involucrado tanto con / sinstd::move
.tmp
se trata como un valor dereturn
declaración en este caso.La respuesta simple es que debe escribir código para las referencias de valor como lo haría con el código de referencias regular, y debería tratarlas mentalmente el 99% de las veces. Esto incluye todas las reglas antiguas sobre la devolución de referencias (es decir, nunca devuelva una referencia a una variable local).
A menos que esté escribiendo una clase de contenedor de plantilla que necesite aprovechar std :: forward y poder escribir una función genérica que tome referencias lvalue o rvalue, esto es más o menos cierto.
Una de las grandes ventajas para el constructor de movimientos y la asignación de movimientos es que si los define, el compilador puede usarlos en los casos en que RVO (optimización del valor de retorno) y NRVO (optimización del valor de retorno) no se invoquen. Esto es bastante grande para devolver objetos caros como contenedores y cadenas por valor eficiente de los métodos.
Ahora, cuando las cosas se ponen interesantes con las referencias de valor, es que también puedes usarlas como argumentos para las funciones normales. Esto le permite escribir contenedores que tienen sobrecargas tanto para la referencia constante (const foo y otros) como para la referencia de valor (foo y otros). Incluso si el argumento es demasiado difícil de manejar con una simple llamada de constructor, todavía se puede hacer:
Los contenedores STL se han actualizado para tener sobrecargas de movimiento para casi cualquier cosa (clave hash y valores, inserción de vectores, etc.), y es donde más los verá.
También puede usarlos para funciones normales, y si solo proporciona un argumento de referencia de valor de valor, puede obligar a la persona que llama a crear el objeto y dejar que la función haga el movimiento. Este es más un ejemplo que un uso realmente bueno, pero en mi biblioteca de representación, he asignado una cadena a todos los recursos cargados, para que sea más fácil ver qué representa cada objeto en el depurador. La interfaz es algo como esto:
Es una forma de 'abstracción permeable', pero me permite aprovechar el hecho de que ya tenía que crear la cadena la mayor parte del tiempo y evitar hacer otra copia de ella. Este no es exactamente un código de alto rendimiento, pero es un buen ejemplo de las posibilidades a medida que las personas se acostumbran a esta característica. Este código realmente requiere que la variable sea temporal a la llamada o que se invoque std :: move:
o
o
¡pero esto no se compilará!
fuente
No es una respuesta per se , sino una pauta. La mayoría de las veces no tiene mucho sentido declarar la
T&&
variable local (como lo hizo constd::vector<int>&& rval_ref
). Todavía tendrástd::move()
que usarlos enfoo(T&&)
métodos de escritura. También existe el problema que ya se mencionó: cuando intente regresarrval_ref
de la función, obtendrá la referencia estándar a fiasco temporal destruido.La mayoría de las veces iría con el siguiente patrón:
No tiene referencias para objetos temporales devueltos, por lo tanto, evita el error (inexperto) del programador que desea utilizar un objeto movido.
Obviamente, hay casos (aunque bastante raros) en los que una función realmente devuelve un
T&&
que es una referencia a un objeto no temporal que puede mover a su objeto.Con respecto a RVO: estos mecanismos generalmente funcionan y el compilador puede evitar la copia, pero en los casos en que la ruta de retorno no es obvia (excepciones,
if
condicionales que determinan el objeto nombrado que devolverá, y probablemente otros pares) rrefs son sus salvadores (incluso si potencialmente más costoso).fuente
Ninguno de ellos hará ninguna copia adicional. Incluso si no se usa RVO, el nuevo estándar dice que se prefiere copiar la construcción del movimiento al hacer devoluciones, creo.
Creo que su segundo ejemplo causa un comportamiento indefinido porque está devolviendo una referencia a una variable local.
fuente
Como ya se mencionó en los comentarios a la primera respuesta, la
return std::move(...);
construcción puede marcar la diferencia en otros casos además del retorno de variables locales. Aquí hay un ejemplo ejecutable que documenta lo que sucede cuando devuelve un objeto miembro con y sinstd::move()
:Presumiblemente,
return std::move(some_member);
solo tiene sentido si realmente desea mover el miembro de la clase en particular, por ejemplo, en un caso dondeclass C
representa objetos de adaptador de corta duración con el único propósito de crear instancias destruct A
.Observe cómo
struct A
siempre se copian declass B
, incluso cuando elclass B
objeto es un valor R. Esto se debe a que el compilador no tiene forma de decir queclass B
la instancia destruct A
ya no se usará. Enclass C
, el compilador tiene esta informaciónstd::move()
, por lo questruct A
se mueve , a menos que la instancia declass C
sea constante.fuente