c++ - ¿Cómo devolver un objeto desde una función considerando valores de C ++ 11 y mover semántica?




3 Answers

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();

El primer ejemplo devuelve un temporal que es capturado por rval_ref . Ese temporal tendrá su vida útil más allá de la definición de rval_ref y puede usarlo como si lo hubiera capturado por valor. Esto es muy similar a lo siguiente:

const std::vector<int>& rval_ref = return_vector();

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

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();

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 estrellaría inmediatamente.

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();

Tu tercer ejemplo es aproximadamente equivalente a tu primer ejemplo. El std::move on tmp es necesario y puede ser un pesimismo de 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

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Es decir, como lo haría en C ++ 03. tmp se trata implícitamente como un valor de r en la declaración de retorno. Se devolverá mediante la optimización del valor de retorno (sin copia, sin movimiento), o si el compilador decide que no puede realizar RVO, entonces usará el constructor de movimientos de vector para hacer la devolución. Solo si no se realiza RVO, y si el tipo devuelto no tuviera un constructor de movimiento, el constructor de copia se usaría para la devolución.

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 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();



La respuesta simple es que debe escribir el código para las referencias de valor como lo haría con el código de referencias regulares, y debe tratarlas mentalmente el 99% del tiempo. Esto incluye todas las reglas antiguas acerca de devolver referencias (es decir, nunca devolver una referencia a una variable local).

A menos que esté escribiendo una clase contenedora de plantillas que necesite aprovechar std :: forward y poder escribir una función genérica que tome referencias de valores lvalue o rvalue, esto es más o menos cierto.

Una de las grandes ventajas del 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 con nombre) no se puedan invocar. Esto es bastante grande para devolver objetos caros como contenedores y cadenas por valor de manera eficiente a partir de métodos.

Ahora, cuando las cosas se ponen interesantes con las referencias de valor, es que también puede usarlas como argumentos para las funciones normales. Esto le permite escribir contenedores que tienen sobrecargas tanto para la referencia const (const foo y otra) como para la referencia de valor (foo && otra). Incluso si el argumento es demasiado difícil de manejar con una simple llamada de constructor, aún se puede hacer:

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 que tengan sobrecargas de movimientos para casi cualquier cosa (clave y valores hash, inserción de vectores, etc.), y es donde más los verán.

También puede usarlas para las funciones normales, y si solo proporciona un argumento de referencia de rvalor, puede forzar a la persona que llama a crear el objeto y dejar que la función haga el movimiento. Esto es más un ejemplo que un uso realmente bueno, pero en mi biblioteca de representación, asigné una cadena a todos los recursos cargados, de modo 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<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

Es una forma de 'abstracción con fugas', pero me permite aprovechar el hecho de que ya tenía que crear la cadena la mayor parte del tiempo y evitar realizar otra copia. Este no es exactamente un código de alto rendimiento, pero es un buen ejemplo de las posibilidades que tienen las personas para entender esta característica. Este código realmente requiere que la variable sea temporal para la llamada o std :: move invoked:

// 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 compilará!

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



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

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

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

auto ret = a(b(), c());

No tiene referencias a objetos temporales devueltos, por lo tanto, evita el error del programador (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 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 copiar, pero en los casos en los que la ruta de retorno no es obvia (excepciones, if condicionales determinan el objeto nombrado que devolverá, y probablemente un par de otros) rrefs son sus salvadores (aunque potencialmente más caro).




Related

c++ c++11 move-semantics rvalue-reference c++-faq