C ++ 11 valores y confusión de semántica de movimiento (statement de retorno)

Estoy tratando de entender las referencias de valores y mover la semántica de C ++ 11.

¿Cuál es la diferencia entre estos ejemplos, y cuál de ellos va a hacer ninguna copia vectorial?

Primer ejemplo

std::vector return_vector(void) { std::vector tmp {1,2,3,4,5}; return tmp; } std::vector &&rval_ref = return_vector(); 

Segundo ejemplo

 std::vector&& return_vector(void) { std::vector tmp {1,2,3,4,5}; return std::move(tmp); } std::vector &&rval_ref = return_vector(); 

Tercer ejemplo

 std::vector return_vector(void) { std::vector tmp {1,2,3,4,5}; return std::move(tmp); } std::vector &&rval_ref = return_vector(); 

Primer ejemplo

 std::vector return_vector(void) { std::vector tmp {1,2,3,4,5}; return tmp; } std::vector &&rval_ref = return_vector(); 

El primer ejemplo devuelve un temporal que es capturado por rval_ref . Esa vida temporal se extenderá más allá de la definición rval_ref y puede usarla como si la hubiera atrapado por valor. Esto es muy similar a lo siguiente:

 const std::vector& rval_ref = return_vector(); 

excepto que en mi reescritura obviamente no puedes usar rval_ref de una manera no constante.

Segundo ejemplo

 std::vector&& return_vector(void) { std::vector tmp {1,2,3,4,5}; return std::move(tmp); } std::vector &&rval_ref = return_vector(); 

En el segundo ejemplo, ha creado un error de tiempo de ejecución. rval_ref ahora contiene una referencia al tmp destruido dentro de la función. Con un poco de suerte, este código se bloqueará inmediatamente.

Tercer ejemplo

 std::vector return_vector(void) { std::vector tmp {1,2,3,4,5}; return std::move(tmp); } std::vector &&rval_ref = return_vector(); 

Su tercer ejemplo es más o menos equivalente al primero. El std :: move on tmp es innecesario y en realidad puede ser una pesimismo de rendimiento, ya que nhibernateá la optimización del valor de retorno.

La mejor manera de codificar lo que estás haciendo es:

Mejores prácticas

 std::vector return_vector(void) { std::vector tmp {1,2,3,4,5}; return tmp; } std::vector rval_ref = return_vector(); 

Es decir, como lo haría en C ++ 03. tmp se trata implícitamente como un valor r en la statement de devolución. Se devolverá a través de optimización de valor de retorno (sin copia, sin movimiento), o si el comstackdor decide que no puede realizar RVO, usará el constructor de movimiento del vector para hacer la devolución. 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.

Ninguno de ellos copiará, pero el segundo se referirá a un vector destruido. Las referencias con nombre de rvalue casi nunca existen en el código normal. Usted escribe exactamente cómo habría escrito una copia en C ++ 03.

 std::vector return_vector() { std::vector tmp {1,2,3,4,5}; return tmp; } std::vector rval_ref = return_vector(); 

Excepto ahora, el vector se mueve. El usuario de una clase no se ocupa de sus referencias de valores en la gran mayoría de los casos.

La respuesta simple es que debe escribir el código para las referencias de valores como lo haría con el código de referencias regulares, y debe tratarlas mentalmente el 99% del tiempo. Esto incluye todas las reglas anteriores sobre el retorno 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 del movimiento de constructor y asignación de movimiento es que si los defines, el comstackdor puede usarlos en los casos en que no se invocaron el RVO (optimización del valor de retorno) y NRVO (optimización del valor de retorno). Esto es bastante grande para devolver objetos costosos como contenedores y cadenas por valor de manera eficiente de los métodos.

Ahora, donde las cosas se ponen interesantes con las referencias rvalue, es que también puedes usarlas como argumentos para las funciones normales. Esto le permite escribir contenedores que tienen sobrecargas tanto para referencia de referencia (const foo y otra) como para referencia de valor real (foo && other). Incluso si la discusión es demasiado difícil de pasar con una simple llamada de constructor, aún puede hacerse:

 std::vector vec; for(int x=0; x<10; ++x) { // automatically uses rvalue reference constructor if available // because MyCheapType is an unamed temporary variable vec.push_back(MyCheapType(0.f)); } std::vector vec; for(int x=0; x<10; ++x) { MyExpensiveType temp(1.0, 3.0); temp.initSomeOtherFields(malloc(5000)); // old way, passed via const reference, expensive copy vec.push_back(temp); // new way, passed via rvalue reference, cheap move // just don't use temp again, not difficult in a loop like this though . . . vec.push_back(std::move(temp)); } 

Los contenedores STL se han actualizado para tener sobrecargas de movimiento para casi cualquier cosa (clave de hash y valores, inserción de vectores, etc.), y es donde más los verá.

También puede usarlos para las funciones normales, y si solo proporciona un argumento de referencia de valor real, puede forzar a la persona que llama a crear el objeto y dejar que la función realice el movimiento. Esto 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:

 TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName) { std::unique_ptr tex = D3DCreateTexture(width, height, fmt); tex->friendlyName = std::move(friendlyName); return tex; } 

Es una forma de 'abstracción con goteras' pero me permite aprovechar el hecho de que tuve 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 función. Este código en realidad requiere que la variable sea temporal a la llamada o std :: move invocado:

 // move from temporary TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard")); 

o

 // explicit move (not going to use the variable 'str' after the create call) string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str)); 

o

 // explicitly make a copy and pass the temporary of the copy down // since we need to use str again for some reason string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str)); 

¡pero esto no comstackrá!

 string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str); 

No es una respuesta per se , sino una guía. La mayoría de las veces no tiene mucho sentido declarar variables T&& locales (como lo hizo con std::vector&& rval_ref ). Aún tendrá que std::move() para usar en los métodos de tipo foo(T&&) . También está el problema que ya se mencionó que cuando intente devolver tal rval_ref de la función obtendrá el estándar referencia-a-destruido-temporal-fiasco.

La mayoría de las veces me gustaría seguir el siguiente patrón:

 // Declarations A a(B&&, C&&); B b(); C c(); auto ret = a(b(), c()); 

No tiene ninguna referencia a los objetos temporales devueltos, por lo que evita el error del progtwigdor (inexperto) que desea usar un objeto movido.

 auto bRet = b(); auto cRet = c(); auto aRet = a(std::move(b), std::move(c)); // Either these just fail (assert/exception), or you won't get // your expected results due to their clean state. bRet.foo(); cRet.bar(); 

Obviamente hay casos (aunque bastante raros) en que una función realmente devuelve un T&& que es una referencia a un objeto no temporal que puede mover a su objeto.

En cuanto a RVO: estos mecanismos generalmente funcionan y el comstackdor puede evitar fácilmente la copia, pero en los casos donde la ruta de retorno no es obvia (excepciones, if condicionales determinan el objeto nombrado que devolverá, y probablemente pares otros) rrefs son sus salvadores (incluso si potencialmente más caro).

Ninguno de ellos hará ninguna copia adicional. Incluso si no se utiliza RVO, el nuevo estándar dice que la construcción de movimiento es preferible para copiar cuando hago devoluciones, creo.

Sin embargo, creo que su segundo ejemplo causa un comportamiento indefinido porque está devolviendo una referencia a una variable local.