Estoy tratando de comprender r-value referencias y mover la semántica de C++11.

¿Cuál es la diferencia entre estos ejemplos, y que de ellos va a hacer ningún vector copia?

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();
  • Por favor, no devuelva local de variables por referencia, nunca. Un r-value de referencia sigue siendo una referencia.
  • Estaba claro que era intencional con el fin de entender las diferencias semánticas entre los ejemplos lol
  • La vieja pregunta, pero me tomó un segundo para entender tu comentario. Creo que la pregunta #2 era si std::move() creado un persistente «copiar».
  • std::move(expression) no crear nada, simplemente se proyecta la expresión de un xvalue. No hay objetos se copian o mueven en el proceso de evaluación de std::move(expression).
InformationsquelleAutor Tarantula | 2011-02-13

6 Comentarios

  1. 536

    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 se devuelve un temporal que es atrapada por rval_ref. Temporal tendrá su vida extendido más allá de la rval_ref definición y se puede utilizar como si hubiera cogido por valor. Esto es muy similar a la siguiente:

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

    excepto que en mi reescritura que, obviamente, no se puede utilizar rval_ref en un no-const manera.

    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 se ha creado un error de tiempo de ejecución. rval_ref ahora contiene una referencia a la destruye tmp dentro de la función. Con suerte, este código sería inmediatamente accidente.

    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 aproximadamente equivalente a su primero. El std::move en tmp es innecesario y puede ser en realidad un rendimiento pessimization como se puede inhibir el valor de retorno de la optimización.

    La mejor forma de código de 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();

    I. e. como en C++03. tmp implícitamente es tratado como un r-value en la instrucción return. Será devuelto a través de devolver el valor de la optimización (no copiar, no se mueven), o si el compilador decide que no se puede realizar la RVO, entonces se vectoriales de uso mover del constructor para hacer la devolución. Sólo si RVO no se realiza, y si devuelve el tipo no tiene un movimiento constructor sería el constructor de copia se usa para el retorno.

    • Así, a partir de lo que deduzco que la mejor cosa a hacer es que los objetos tienen un movimiento constructor. Yo probablemente debería google acaba esto, pero estoy perezoso en el momento; hay directrices comunes para los compiladores en RVO?
    • Los compiladores del RVO cuando regresa un objeto local, por su valor, y el tipo de local y el retorno de la función es el mismo, y tampoco es cv-calificados (no retorno const tipos). Manténgase alejado de volver con la condición (:?) declaración ya que puede inhibir el RVO. No envuelva el local en alguna otra función que devuelve una referencia a la local. Sólo return my_local;. Varias sentencias return están bien y no inhibir el RVO.
    • Hay una advertencia: cuando la devolución de un miembros de un objeto local, el movimiento debe ser explícito.
    • hola, ¿puede explicar esto: «rval_ref ahora contiene una referencia a la destruye tmp dentro de la función. «¿Te refieres temporal creado en la línea de retorno, o func variable local llamada tmp.
    • No hay temporal creado en la línea de retorno. move no crea un temporal. Se proyecta un lvalue a un xvalue, la no realización de copias, la creación de la nada, la destrucción de la nada. Ese ejemplo es exactamente la misma situación, como si volvía por lvalue de referencia y se retira el move de la línea de retorno: sea como Sea, tienes un colgando referencia a una variable local dentro de la función, y que ha sido destruido.
    • Sólo un nit: Desde que nombre la variable (tmp) en la «Mejor práctica» de la sección, es la NRVO que entra en acción, no el RVO. Estas son dos cosas diferentes optimizaciones. Aparte de eso, gran respuesta!
    • por qué tipo de resultado no puede tener const para RVO?
    • Yo estaba equivocado. RVO puede trabajar con const tipos de retorno. Es simplemente una mala idea hacerlo. Si el RVO falla, ir semántica no entran en juego.
    • «De regreso múltiple declaraciones están bien y no inhibir RVO»: Sólo en caso de volver a mismo variable.
    • Estás en lo correcto. Yo no estaba hablando de forma tan precisa como en la intención. Me refería a que varias sentencias return no prohíben el compilador de RVO (aunque no hace imposible aplicar), y por lo tanto el retorno de expresión es considerada todavía una r-value.
    • En todo esto estamos hablando de la operación de REGRESO, posiblemente, de ser una forma implícita de mover. En el caso de los implícito un cambio, ¿cómo es la posterior asignación de los afectados? El valor de retorno de la función es por-valor, no es un r-value de referencia, entonces, ¿cómo se std::vector saber usar un movimiento para la construcción de la variable local en el sitio de llamada?
    • La expresión return_vector() es un r-value, puesto que la función devuelve un objeto de valor. Cuando esa expresión se utiliza para la construcción de un objeto en el sitio de llamada, la resolución de sobrecarga se elija un movimiento constructor, si es que existe. Si el objeto ya está construido, a continuación, la resolución de sobrecarga en lugar de elegir un operador de asignación. Desde el lado derecho es un r-value, que se elija mover el operador de asignación, si es que existe.
    • No entiendo el «si el compilador decide»… yo decido, yo SOY el programador. Por qué habría de hacer algo que yo no decirle que lo haga?
    • El estándar de C++ dice que el compilador los escritores llegan a tomar algunas de las decisiones. Uno de esos es el RVO. Bajo un conjunto específico de circunstancias, el compilador está permitido, pero no es necesario realizar RVO. Y el compilador, no de usted, puede decidir si es o no realiza RVO. No se ha hablado sobre el requisito de RVO, pero en este momento, que no ha sido estandarizada.
    • La verdad es que estoy teniendo problemas con NRVO/RVO, por favor ver stackoverflow.com/questions/35506708/… y/o cplusplus.com/forum/general/187009
    • Ok, he mirado en la pregunta. Parece que has respondido a ti mismo, y no tengo nada para agregar a su respuesta.
    • No creo que mi respuesta es correcta: he oído que copiar elisión PUEDE producirse incluso cuando hay más de 1 volver declaraciones. Me gustaría saber cuando una copia elisión es prohibido
    • Ok, he dado un tiro.
    • Extraña pregunta, pero ¿por qué estándar de los componentes de la biblioteca de retorno por r-value de referencia y no por valor? Es que es más eficiente en el caso de que el valor no está obligado a mover y es descartado después de la captura? Lo pregunto porque en este caso, provoca un comportamiento indefinido wandbox.org/permlink/kUqjfOWWRP6N57eS
    • No puedo pensar en una buena razón para devolver una referencia a un r-value. Hice este mismo error en 2005, cuando proponiendo r-value-sobrecargas para string+string pero afortunadamente se corrigió antes de C++11, finalizado. Aquí es un poco torpe manera de trabajar alrededor de ella: wandbox.org/permlink/HQPGEOMAUXwUCMb4
    • Estoy en lo cierto que el uso de return_vector(std::vector& x) + vector::resize(0) podría ahorrar un par de mallocs a través de múltiples return_vector llamadas, y ser aún más eficiente del tiempo de NRVO, a costa de un mayor uso de la memoria en general? Más precisa de código: stackoverflow.com/questions/10476665/…
    • Sí estás en lo correcto. Y usted también es correcto que contar las asignaciones/deallocations es una buena técnica para calcular el rendimiento.
    • En caso de que esté interesado: stackoverflow.com/q/56400313/102937

  2. 41

    Ninguno de ellos se copia, pero el segundo se referirá a una destruido vector. Denominado r-value referencias casi nunca existe en regular de código. Se escribe cómo te hubiera 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 que ahora, el vector se mueve. El usuario de una clase no tratar con él de la r-value referencias en la gran mayoría de los casos.

    • ¿Estás realmente seguro de que el tercer ejemplo se va a hacer de vectores de copia ?
    • Va a la quiebra de su vector. Si o no o que no copiarlo antes de romper en realidad no importa.
    • No veo ninguna razón por la que revienta el que te proponemos. Es perfectamente bien para enlazar un local r-value variable de referencia a un r-value. En ese caso, el objeto temporal de la vida se extiende a la vida de la r-value variable de referencia.
    • Sólo una aclaración, ya que estoy aprendiendo esto. En este nuevo ejemplo, el vector tmp no se trasladó en rval_ref, pero se escriben directamente en rval_ref utilizando RVO (es decir, copia elisión). Hay una distinción entre std::move y copia de la elisión. Un std::move todavía puede implicar que algunos datos se copian; en el caso de un vector, un nuevo vector es en realidad construida en el constructor de copia y datos se asignan, pero el grueso de la matriz de datos es sólo copiar por copiar el puntero (esencialmente). La copia elisión evita el 100% de todas las copias.
    • Este es NRVO, no RVO. NRVO es opcional, incluso en C++17. Si es que no se aplican, tanto en valor de retorno y rval_ref variables se construyen utilizando mover constructor de std::vector. No hay ningún constructor de copia involucrado tanto con / sin std::move. tmp es tratado como un r-value en return declaración en este caso.
    • es correcto. En este caso, debido a que el valor de retorno se denomina tmp, entonces NRVO puede aplicar (o no, ya que es opcional). Si return_vector era simplemente {return std::vector{1,2,3,4,5}; sería RVO. Mi punto era que con un buen compilador que puede hacer RVO y NRVO, rval_ref no es copia construido o mover construido – se construye de manera directa como std::vector<int>{1,2,3,4,5}.

  3. 16

    La respuesta simple es que usted debe escribir el código para r-value referencias como lo haría regular referencias de código, y usted debe tratar de la misma mentalmente el 99% del tiempo. Esto incluye todas las viejas reglas sobre la devolución de referencias (es decir, nunca devolver una referencia a una variable local).

    A menos que usted está escribiendo una plantilla de clase de contenedor que necesita para tomar ventaja de std::adelante y ser capaz de escribir una función genérica que tiene ya sea lvalue o r-value referencias, esto es más o menos cierto.

    Una de las grandes ventajas de la jugada, constructor y mover la asignación es que si se definen ellos, el compilador puede utilizar en los casos en que el RVO (valor de retorno de optimización) y NRVO (denominado valor de retorno de optimización) no se invoca. Esto es bastante grande para el retorno de los caros objetos como contenedores de & cadenas de valor de manera eficiente a partir de métodos.

    Ahora cuando las cosas se ponen interesantes con r-value referencias, es que también se pueden utilizar como argumentos a funciones normales. Esto permite escribir los contenedores que tienen sobrecargas para const referencia (const foo& otros) y r-value de referencia (foo&& otros). Incluso si el argumento es demasiado difícil de manejar para pasar con una simple llamada al constructor que 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 mover sobrecargas para casi cualquier cosa (el hash de la clave y los valores del vector de inserción, etc), y es donde se ve la mayoría de ellos.

    También se pueden utilizar para funciones normales, y si sólo proporcionan una r-value argumento de referencia puede obligar a la persona que llama para crear el objeto y dejar que la función de hacer el movimiento. Esto es más de un ejemplo de un buen uso, pero en mi biblioteca de procesamiento, me han asignado una cadena a todos los recursos cargados, por lo que es más fácil ver lo que cada objeto representa 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 «filtraciones abstracción’, pero me permite tomar ventaja del hecho de que tuve que crear la cadena ya que la mayoría del tiempo, y evitar hacer otra copia de la misma. Esto no es exactamente de alto rendimiento de código, pero es un buen ejemplo de las posibilidades como de las personas de obtener la caída de esta característica. Este código requiere que la variable de ser temporal a la llamada, o std::mover invoca:

    //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);
  4. 3

    No una respuesta de por sí, sino una guía. La mayoría del tiempo no hay mucho sentido en la declaración de local T&& variable (como hizo con std::vector<int>&& rval_ref). Usted todavía tendrá que std::move() a utilizar en foo(T&&) tipo de métodos. También existe el problema que ya se ha mencionado que cuando se intenta el retorno de los rval_ref de la función obtendrá el estándar de referencia-a-destruido-temporal-fiasco.

    La mayor parte de el tiempo que me gustaría ir con el siguiente patrón:

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

    Usted no tiene ninguna referencias a devuelven objetos temporales, por lo que evitar (sin experiencia) del programador de error que desean usar un objeto que se mueve.

    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 (aunque bastante raros) casos en los que una función verdaderamente devuelve un T&& que es una referencia a un no temporal objeto que se puede mover en su objeto.

    Respecto RVO: estos mecanismos generalmente el trabajo y el compilador bien puede evitar la copia, pero en los casos donde el camino de retorno no es obvio (excepciones, if condicionales determinar el nombre del objeto que se va a devolver, y probablemente la par de los demás) rrefs son sus salvadores (aunque potencialmente más caro).

  5. 2

    Ninguno de esos va a realizar ninguna otra copia. Incluso si RVO no se usa, la nueva norma dice que se mueven de la construcción es el preferido para copiar al hacer devuelve creo.

    Creo que tu segundo ejemplo, provoca un comportamiento indefinido, porque aunque está devolviendo una referencia a una variable local.

  6. 0

    Como ya se ha mencionado en los comentarios a la primera respuesta, la return std::move(...); construcción puede hacer una diferencia en los casos que devolver de las variables locales. He aquí un ejecutable ejemplo de que los documentos lo que sucede cuando usted 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); sólo tiene sentido si usted realmente desea mover el particular, miembro de la clase, por ejemplo, en un caso donde class C representa corta duración adaptador de objetos con el único propósito de crear instancias de struct A.

    Observe cómo struct A siempre se copiado de class B, incluso cuando el class B objeto es un R-valor. Esto es porque el compilador no tiene manera de saber que class B‘s instancia de struct A no ser utilizada. En class C, el compilador tiene esta información de std::move(), que es la razón por la struct A obtiene movido, a menos que la instancia de class C es constante.

Dejar respuesta

Please enter your comment!
Please enter your name here