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

435

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();
Tarántula
fuente
51
Por favor, no devuelva variables locales por referencia, nunca. Una referencia rvalue sigue siendo una referencia.
fredoverflow
63
Eso fue obviamente intencional para entender las diferencias semánticas entre los ejemplos jajaja
Tarántula
@FredOverflow Antigua pregunta, pero me llevó un segundo entender tu comentario. Creo que la pregunta con el n. ° 2 fue si se std::move()creó una "copia" persistente.
3Dave
55
@DavidLively 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ón std::move(expression).
fredoverflow

Respuestas:

563

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 extendida más allá de la rval_refdefinición y puede usarlo como si lo hubiera captado 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_refde 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_refahora contiene una referencia a lo destruido tmpdentro de la función. Con suerte, este código se bloqueará 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();

Su tercer ejemplo es más o menos equivalente al primero. El std::moveencendido tmpes 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

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. 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.

Howard Hinnant
fuente
65
Los compiladores harán un RVO cuando devuelves un objeto local por valor, y el tipo de local y el retorno de la función son los mismos, y ninguno de los dos es calificado por cv (no devuelve tipos const). Manténgase alejado de regresar con la declaración de condición (:?) Ya que puede inhibir el RVO. No envuelva el local en alguna otra función que devuelva una referencia al local. Justo return my_local;. Múltiples declaraciones de retorno están bien y no inhibirán RVO.
Howard Hinnant
27
Hay una advertencia: al devolver un miembro de un objeto local, el movimiento debe ser explícito.
boycy
55
@NoSenseEtAl: no se ha creado temporalmente en la línea de retorno. 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 el movede 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.
Howard Hinnant
15
"Las declaraciones de devolución múltiple están bien y no inhibirán RVO": solo si devuelven la misma variable.
Deduplicador
55
@Dupuplicator: Tienes razón. No estaba hablando con la precisión que pretendía. Quise decir que las declaraciones de retorno múltiples no prohíben el compilador de RVO (aunque hace que sea imposible de implementar) y, por lo tanto, la expresión de retorno todavía se considera un valor r.
Howard Hinnant
42

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.

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

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

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.

Perrito
fuente
¿Estás realmente seguro de que el tercer ejemplo va a hacer una copia vectorial?
Tarántula
@Tarantula: va a reventar tu vector. Si lo copió o no antes de romperlo, realmente no importa.
Puppy
44
No veo ninguna razón para el revés que propones. Está perfectamente bien vincular una variable de referencia de valor r local a un valor r. En ese caso, la vida útil del objeto temporal se extiende a la vida útil de la variable de referencia rvalue.
fredoverflow
1
Solo un punto de aclaración, ya que estoy aprendiendo esto. En este nuevo ejemplo, el vector tmpno se mueve hacia adentro rval_ref, sino que se escribe directamente rval_refusando RVO (es decir, copia de elisión). Hay una distinción entre std::movey copiar elisión. A std::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.
Mark Lakata
@MarkLakata Esto es NRVO, no RVO. NRVO es opcional, incluso en C ++ 17. Si no se aplica, tanto el valor de retorno como las rval_refvariables se construyen utilizando el constructor de movimiento de std::vector. No hay constructor de copia involucrado tanto con / sin std::move. tmpse trata como un valor de returndeclaración en este caso.
Daniel Langr
16

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:

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 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:

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 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:

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

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

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 con std::vector<int>&& rval_ref). Todavía tendrá std::move()que usarlos en foo(T&&)métodos de escritura. También existe el problema que ya se mencionó: cuando intente regresar rval_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:

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

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

No tiene referencias para objetos temporales devueltos, por lo tanto, evita el error (inexperto) del programador que desea utilizar 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 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).

Rojo XIII
fuente
2

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.

Edward extraño
fuente
1

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 sin std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

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 donde class Crepresenta objetos de adaptador de corta duración con el único propósito de crear instancias de struct A.

Observe cómo struct Asiempre se copian de class B, incluso cuando el class Bobjeto es un valor R. Esto se debe a que el compilador no tiene forma de decir que class Bla instancia de struct Aya no se usará. En class C, el compilador tiene esta información std::move(), por lo que struct Ase mueve , a menos que la instancia de class Csea ​​constante.

Andrej Podzimek
fuente