std::move en Utilidad en C++ | Mover semántica, mover constructores y mover operadores de asignación

requisitos previos:

  1. referencia de valor
  2. referencia de valor
  3. Semántica de copia (constructor de copia)

Referencias:

En C++ hay dos tipos de referencias:

  1. Referencia de valor:
    • Un lvalue es una expresión que aparecerá en el lado izquierdo o en el lado derecho de una tarea.
    • Simplemente, una variable u objeto que tiene un nombre y una dirección de memoria.
    • Utiliza un ampersand (&).
  2. referencia de valor:
    • Un valor r es una expresión que aparecerá solo en el lado derecho de una tarea.
    • Una variable u objeto tiene solo una dirección de memoria (objetos temporales).
    • Utiliza dos símbolos de unión (&&).

Move Constructor y semántica:

El constructor de movimiento se introdujo en C++11 . La necesidad o el propósito de un constructor de movimientos es robar o mover tantos recursos como sea posible del objeto fuente (original) , lo más rápido posible, porque la fuente ya no necesita tener un valor significativo y/o porque va a ser destruido en un momento de todos modos. Para que uno pueda evitar crear copias innecesarias de un objeto y hacer un uso eficiente de los recursos.

Si bien uno puede robar los recursos, debe dejar el objeto fuente (original) en un estado válido donde pueda destruirse correctamente.

Los constructores de movimiento normalmente «roban» el recurso del objeto de origen (original) en lugar de hacer varias copias de ellos, y dejan el objeto de origen en un «estado válido pero no especificado».

El constructor de copia usa las referencias de valor l que están marcadas con un ampersand (&), mientras que el constructor de movimiento usa las referencias de rvalue que están marcadas con dos ampersand (&&).

std::move() es una función utilizada para convertir una referencia de lvalue en la referencia de rvalue. Se utiliza para mover los recursos de un objeto de origen, es decir, para una transferencia eficiente de recursos de un objeto a otro.

std::move() se define en el encabezado <utility> .

Sintaxis:

  • template< clase T > 
    typename std::remove_reference<T>::type&& move(T&& t) noexcept; (desde C++11)(hasta C++14)
  • template< clase T > 
    constexpr std::remove_reference_t<T>&& move(T&& t) noexcept (desde C++14)

Ejemplo: a continuación se muestra el programa C++ para mostrar lo que sucede sin usar la semántica de movimiento, es decir, antes de C++11.

C++14

// C++ program to implement
// the above approach
 
// for std::string
#include <string>
 
// for std::cout
#include <iostream>
 
// for EXIT_SUCCESS macro
#include <cstdlib>
 
// for std::vector
#include <vector>
 
// for std::move()
#include <utility>
 
// Declaration
std::vector<std::string> createAndInsert();
 
// Driver code
int main()
{
    // Constructing an empty vector
    // of strings
    std::vector<std::string> vecString;
 
    // calling createAndInsert() and
    // initializing the local vecString
    // object
    vecString = createAndInsert();
 
    // Printing content of the vector
    for (const auto& s : vecString) {
        std::cout << s << '\n';
    }
 
    return EXIT_SUCCESS;
}
 
// Definition
std::vector<std::string> createAndInsert()
{
    // constructing a vector of
    // strings with an size of
    // 3 elements
    std::vector<std::string> vec;
    vec.reserve(3);
 
    // constructing & initializing
    // a string with "Hello"
    std::string str("Hello");
 
    // Inserting a copy of string
    // object
    vec.push_back(str);
 
    // Inserting a copy of an
    // temporary string object
    vec.push_back(str + str);
 
    // Again inserting a copy of
    // string object
    vec.push_back(std::move(str));
 
    // Finally, returning the local
    // vector
    return vec;
}
Producción

Hello
HelloHello
Hello

Explicación:

Suponiendo que el programa se compila y ejecuta con un compilador que no admite la semántica de movimiento. En la función principal(),  

1. std::vector<std::string> vecString;- Se crea un vector vacío sin elementos en él. 
2. vecString = createAndInsert();- Se llama a la función createAndInsert().
3. En la función createAndInsert()-

  • std::vector<std::string> vec;- Se crea otro nuevo vector vacío llamado vec.
  • vec.reserve(3);- Reserva del tamaño de 3 elementos.
  • std::string str(“Hola”);- Una string llamada str inicializada con un “Hola”.
  • vec.push_back( str );- Una string se pasa por valor al vector vec. Por lo tanto, se creará una copia (profunda) de str y se insertará en el vec llamando a un constructor de copias de la clase String.
  • vec.push_back( str + str );- Este es un proceso de tres etapas-
    1. Se creará un objeto temporal (str + str) con su propia memoria separada.
    2. Este objeto temporal se inserta en el vector vec que se pasa por valor nuevamente, lo que significa que se creará una copia (profunda) del objeto de string temporal.
    3. A partir de ahora, el objeto temporal ya no es necesario, por lo que será destruido.

Nota: Aquí, asignamos y desasignamos innecesariamente la memoria del objeto de string temporal. que se puede optimizar (mejorar) aún más simplemente moviendo los datos del objeto de origen. 

  • vec.push_back( str );- El mismo proceso que en la Línea no. 5 se llevarán a cabo. Recuerde en este punto que el objeto de string str se usará por última vez.
  • return vec;- Esto está al final de la función createAndInsert()-
    • En primer lugar, el objeto de string strserá destruido porque el alcance se deja donde se declaró.
    • En segundo lugar, se devuelve un vector local de string, es decir, vec. Como el tipo de retorno de la función no es por una referencia. Por lo tanto, se creará una copia profunda de todo el vector mediante la asignación en una ubicación de memoria separada y luego se destruirá el objeto vec local porque el alcance se deja donde se declaró.
    • Finalmente, la copia del vector de strings se devolverá a la función main() de la persona que llama.
  • Por último, después de volver a la función main() de la persona que llama, simplemente imprima los elementos del vector vecString local.

Ejemplo: a continuación se muestra el programa C++ para implementar el concepto anterior utilizando la semántica de movimiento, es decir, desde C++11 y posteriores. 

C++14

// C++ program to implement
// the above approach
 
// for std::string
#include <string>
 
// for std::cout
#include <iostream>
 
// for EXIT_SUCCESS macro
#include <cstdlib>
 
// for std::vector
#include <vector>
 
// for std::move()
#include <utility>
 
// Declaration
std::vector<std::string> createAndInsert();
 
// Driver code
int main()
{
    // Constructing an empty vector
    // of strings
    std::vector<std::string> vecString;
 
    // calling createAndInsert() and
    // initializing the local vecString
    // object
    vecString = createAndInsert();
 
    // Printing content of the vector
    for (const auto& s : vecString) {
        std::cout << s << '\n';
    }
 
    return EXIT_SUCCESS;
}
 
// Definition
std::vector<std::string> createAndInsert()
{
    // constructing a vector of
    // strings with an size of
    // 3 elements
    std::vector<std::string> vec;
    vec.reserve(3);
 
    // constructing & initializing
    // a string with "Hello"
    std::string str("Hello");
 
    // Inserting a copy of string
    // object
    vec.push_back(str);
 
    // Inserting a copy of an
    // temporary string object
    vec.push_back(str + str);
 
    // Again inserting a copy of
    // string object
    vec.push_back(std::move(str));
 
    // Finally, returning the local
    // vector
    return vec;
}
Producción

Hello
HelloHello
Hello

Explicación:

Aquí, para usar la semántica de movimiento. El compilador debe ser compatible con los estándares C++ 11 o superior. La historia de ejecución de la función main() y la función createAndInsert() sigue siendo la misma hasta la línea vec.push_back(str);

Puede surgir una pregunta sobre por qué el objeto temporal no se mueve al vector vec usando std::move(). La razón detrás de esto es el método push_back() del vector. Desde C++11, el método push_back() se ha proporcionado con su nueva versión sobrecargada.

Sintaxis: 

  1. constexpr void push_back(const T& valor); (desde C++20)
  2. void push_back (T&& valor); (desde C++11) (hasta C++20)
  3. void push_back(const T& valor); (hasta C++20)
  4. constexpr void push_back(T&& valor); (desde C++20)
  • vec.push_back(string + string);-
    1. Se creará un objeto temporal (str + str) con su propia memoria separada y realizará una llamada al método push_back() sobrecargado (la versión 2 o 4 depende de la versión de C++) que robará (o moverá) los datos del objeto fuente temporal (str + str) al vector vecas ya no es necesario.
    2. Después de realizar el movimiento, el objeto temporal se destruye. Por lo tanto, en lugar de llamar al constructor de copias (semántica de copias), se optimiza simplemente copiando el tamaño de la string y manipulando los punteros a la memoria de los datos.
    3. Aquí, el punto importante a tener en cuenta es que hacemos uso de la memoria que pronto ya no será propietaria de su memoria. En otras palabras, de alguna manera lo optimizamos. Eso es todo debido a la referencia de rvalue y la semántica de movimiento.
  • vec.push_back(std::move(str));- Aquí se insinúa explícitamente al compilador que «el objeto ya no es necesario» nombrado como str ( referencia de lvalue ) con la ayuda de la función std::move() al convertir el lvalue referencia en referencia rvalue y el recurso de str se moverá al vector. Entonces el estado de str se convierte en un «estado válido pero no especificado». Esto no nos importa porque por última vez vamos a usar y pronto seremos destruidos en un momento de todos modos.
  • Por último, devuelva el vector local de string llamado vecto a su llamador.
  • Al final, volvió a la función main() de la persona que llama y simplemente imprimió los elementos del vecStringvector local.

Puede surgir una pregunta al devolver el objeto vec a su emisor. Como ya no es necesario y también se creará un objeto temporal completo de un vector y también se destruirá el vector vec local, entonces, ¿por qué no se usa std::move() para robar el valor y devolverlo? 
Su respuesta es simple y obvia, existe una optimización a nivel de compilador conocida como (Named) Return Value Object, más conocida popularmente como RVO

Algunas alternativas de semántica de movimiento: 

  1. Llamar a std::move() en un objeto const generalmente no tiene efecto.
    • No tiene ningún sentido robar o mover los recursos de un objeto const.
    • Consulte la función constObjectCallFunc() en el siguiente programa
  2. La semántica de copia se utiliza como respaldo para la semántica de movimiento si y solo si se admite la semántica de copia.
    • Ver la función baz() en el siguiente programa
  3. Si no hay una implementación que tome la referencia rvalue como argumento, se usará la referencia ordinaria const lvalue.
    • Ver la función baz() en el siguiente programa
  4. Si falta una función o un método con la referencia rvalue como argumento y la referencia const lvalue como argumento. Entonces se generará el error en tiempo de compilación.
    • Ver la función bar() en el siguiente programa

Nota: La función foo() tiene todos los tipos de argumentos necesarios.

A continuación se muestra el programa C++ para implementar todos los conceptos anteriores: 

C++14

// C++ program to implement
// the above concept
 
// for std::cout & std::endl
#include <iostream>
 
// for std::move()
#include <utility>
 
// for std::string
#include <string>
 
// for EXIT_SUCCESS macro
#include <cstdlib>
 
// foo() taking a non-const lvalue
// reference argument
void foo(std::string& str);
 
// foo() taking a const lvalue
// reference argument
void foo(const std::string& str);
 
// foo() taking a rvalue
// reference argument
void foo(std::string&& str);
 
// baz() taking a const lvalue
// reference argument
void baz(const std::string& str);
 
// baz() taking a non-const lvalue
// reference argument
void baz(std::string& str);
 
// bar() taking a non-const lvalue
// reference argument
void bar(std::string& str);
 
// constObjectCallFunc() taking a
// rvalue reference argument
void constObjectCallFunc(std::string&& str);
 
// Driver code
int main()
{
    // foo(std::string&& str) will
    // be called
    foo(std::string("Hello"));
 
    std::string goodBye("Good Bye!");
 
    // foo(std::string& str) will be called
    foo(goodBye);
 
    // foo(std::string&& str) will be called
    foo(std::move(goodBye + " using std::move()"));
 
    std::cout << "\n\n\n";
 
    // move semantics fallback
    // baz(const std::string& str) will be called
    baz(std::string("This is temporary string object"));
 
    // baz(const std::string& str) will be called
    baz(std::move(std::string(
        "This is temporary string object using std::move()")));
 
    std::cout << "\n\n\n";
 
    std::string failToCall("This will fail to call");
 
    /*
      Reasons to fail bar() call -
          1. No rvalue reference implementation
           available         // First Preference
          2. No const lvalue reference implementation
           available    // Second Preference
          3. Finally fails to invoke bar() function
      */
    // bar(std::move(failToCall));
    // Error : check the error message for more
    // better understanding
    std::cout << "\n\n\n";
 
    const std::string constObj(
        "Calling a std::move() on a const object usually has no effect.");
    // constObjectCallFunc(std::move(constObj));
    // Error : because of const qualifier
    // It doesn't make any sense to steal or
    // move the resources of a const object
   
    return EXIT_SUCCESS;
}
 
void foo(const std::string& str)
{
    // do something
    std::cout << "foo(const std::string& str) : "
              << "\n\t" << str << std::endl;
}
 
void foo(std::string& str)
{
    // do something
    std::cout << "foo(std::string& str) : "
              << "\n\t" << str << std::endl;
}
 
void foo(std::string&& str)
{
    // do something
    std::cout << "foo(std::string&& str) : "
              << "\n\t" << str << std::endl;
}
 
void baz(const std::string& str)
{
    // do something
    std::cout << "baz(const std::string& str) : "
              << "\n\t" << str << std::endl;
}
 
void baz(std::string& str)
{
    // do something
    std::cout << "baz(std::string& str) : "
              << "\n\t" << str << std::endl;
}
 
void bar(std::string& str)
{
    // do something
    std::cout << "bar(std::string&& str) : "
              << "\n\t" << str << std::endl;
}
 
void constObjectCallFunc(std::string&& str)
{
    // do something
    std::cout << "constObjectCallFunc(std::string&& str) : "
              << "\n\t" << str << std::endl;
}
Producción

foo(std::string&& str) : 
    Hello
foo(std::string& str) : 
    Good Bye!
foo(std::string&& str) : 
    Good Bye! using std::move()



baz(const std::string& str) : 
    This is temporary string object
baz(const std::string& str) : 
    This is temporary string object using std::move()

Resumen: 

  • La semántica de movimiento nos permite optimizar la copia de objetos, donde no necesitamos el valor. A menudo se usa implícitamente (para objetos temporales sin nombre o valores de retorno locales) o explícitamente con std::move().
  • std::move() significa «ya no necesita este valor» .
  • Un objeto marcado con std::move() nunca se destruye parcialmente. es decir , se llamará al destructor para que destruya el objeto correctamente .

Publicación traducida automáticamente

Artículo escrito por mayurkedari16 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *