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_refdefinició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_refde una manera no constante.Segundo ejemplo
En el segundo ejemplo, ha creado un error de tiempo de ejecución.
rval_refahora contiene una referencia a lo destruidotmpdentro 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::moveencendidotmpes 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.
tmpse 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.moveno 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 elmovede 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
tmpno se mueve hacia adentrorval_ref, sino que se escribe directamenterval_refusando RVO (es decir, copia de elisión). Hay una distinción entrestd::movey copiar elisión. Astd::movetodaví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_refvariables se construyen utilizando el constructor de movimiento destd::vector. No hay constructor de copia involucrado tanto con / sinstd::move.tmpse trata como un valor dereturndeclaració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_refde 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,
ifcondicionales 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 Crepresenta objetos de adaptador de corta duración con el único propósito de crear instancias destruct A.Observe cómo
struct Asiempre se copian declass B, incluso cuando elclass Bobjeto es un valor R. Esto se debe a que el compilador no tiene forma de decir queclass Bla instancia destruct Aya no se usará. Enclass C, el compilador tiene esta informaciónstd::move(), por lo questruct Ase mueve , a menos que la instancia declass Csea constante.fuente