11

Bueno, estoy muy interesado en aprender sobre esta nueva característica aparentemente útil, pero se me dificulta porque toda la información de calidad disponible se encuentra en ingles. De igual manera trato de aprender pero mi ingles no es tan bueno que digamos solo llego a comprender algunas cosas. Por eso consulto aquí si alguien podría explicar que es la semántica de movimiento y los detalles que lo acompañan.

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
cheroky
  • 551
  • 1
  • 3
  • 13
  • 1
    Información adicional: [¿Cuándo usar std::move y por qué?](https://es.stackoverflow.com/questions/854/cu%c3%a1ndo-usar-stdmove-y-por-qu%c3%a9) – eferion May 29 '17 at 10:59

1 Answers1

15

El problema.

Las semánticas de movimiento son una herramienta que acude en ayuda del programador de C++ para resolver un problema de rendimiento que se lleva arrastrando desde la creación del lenguaje, vamos a ver un ejemplo de código ineficiente:

std::string Quijote("En un lugar de La Mancha de cuyo nombre no quiero acordarme ...");
// Continua durante centenares de miles de caracteres -------------------------> ~~~

std::string TO_UPPER(std::string in)
{
    std::transform(in.begin(), in.end(), in.begin(), std::toupper);
    return in;
}

int main()
{
    std::cout << TO_UPPER(Quijote);
    return 0;
}

Cuando se llama a TO_UPPER se copia la variable Quijote en la variable local de la función in, esta copia es procesada y después se devuelve la copia procesada y esta es a su vez copiada otra vez.

Antes de las semánticas de movimiento en C++ podíamos usar referencias para resolver el problema anterior:

std::string Quijote("En un lugar de La Mancha de cuyo nombre no quiero acordarme ...");
// Continua durante centenares de miles de caracteres -------------------------> ~~~

//          v------- referencia
std::string &TO_UPPER(std::string &in)
// referencia --------------------^
{
    std::transform(in.begin(), in.end(), in.begin(), std::toupper);
    return in;
}

int main()
{
    std::cout << TO_UPPER(Quijote);
    return 0;
}

Usando referencias evitamos hacer copias innecesarias de las variables y mejora el rendimiento del código. Pero las referencias no resuelven todos los problemas:

std::vector<std::string> &read_table(std::string table)
{
    std::vector<std::string> result;
    // fill result...
    return result;
}

int main()
{
    auto customers = read_table("customers");
    return 0;
}

En el código anterior tenemos una referencia colgante ya que la variable result interna de la función read_table deja de existir en el momento en que se sale de la función y por ello, usarla fuera de la función provocará errores... así que volvemos a las socorridas referencias:

//                                  referencia -------------v
void read_table(std::string table, std::vector<std::string> &result)
{
    // fill result...
}

int main()
{
    std::vector<std::string> customers;
    read_table("customers", customers);
    return 0;
}

La solución.

Imagina que en lugar de copiar datos de una función a otra pudiéramos mover estos datos, eso son las semánticas de movimiento.

Las semánticas de movimiento se apoyan en las "Referencia a Valor Derecho" (RvD), que permiten referenciar valores temporales, cosa imposible antes de C++11.

Las Referencias a Valor Derecho usan el declarador && (en el contexto de declaración se corresponde a "Referencia a Valor Derecho" en expresión es el operador AND lógico ).

Pero ¿Qué es una RvD?, un Valor Derecho es todo aquello que tenga capacidad de estar a la derecha de una expresión y generalmente un valor temporal, todas las siguientes expresiones a la derecha del = son Valores Derechos temporales:

const float PI          = 3.14159265359f;
float sqrPi             = std::sqrt(PI);
auto t                  = std::time(nullptr);
auto f                  = t & 1? PI : sqrPi;
std::string hocus_pocus = "Hocus Pocus!";
auto lorem_ipsum        = std::string("Lorem Ipsum!");

Antes de C++11 no se podía referenciar Valores Derechos, pero al añadir las semánticas de movimiento estos valores son referenciables y se pueden sobrecargar funciones para aceptarlos:

std::string hocus_pocus = "Hocus Pocus!";

/* 1 */ void f(std::string &s);  // Funcion que recibe una referencia normal.
/* 2 */ void f(std::string &&s); // Funcion que recibe una RvD.

f(hocus_pocus);                // Llama a la primera sobrecarga.
f("Hocus Pocus!");             // Llama a la segunda sobrecarga.
f(std::string("Lorem Ipsum!"); // Llama a la segunda sobrecarga.

Para objetos que manejan recursos, cuando reciben un valor temporal (un valor que sabemos que va a ser eliminado después de evaluar la expresión) podemos decidir intercambiar el recurso del temporal para evitar operaciones más costosas:

Semánticas de movimiento ¡gratis!.

La librería estándar adapta a partir de C++11 todos sus constructos para sacar provecho de las semánticas de movimiento, así que cambiando a un compilador de C++11 o superior obtenemos estas semánticas sin cambiar el código. ¡Esto generalmente implica una mejora del rendimiento con tan sólo recompilar!

Así que ahora podemos solucionar los problemas que antes teníamos:

// Referencia a Valor Derecho ---vv
std::string TO_UPPER(std::string &&in)
{
    std::transform(in.begin(), in.end(), in.begin(), std::toupper);
    return std::move(in);
 // ~~~~~~~~~~~~~~~~~~~~ <----- mover el valor fuera de la función.
}

Dado que la plantilla std::string de C++11 incorpora las semánticas de movimiento, cuando llamemos a la función TO_UPPER con valores temporales, se moverán los valores ahorrando copias innecesarias. La función std::move transforma un valor en RvD, así que volviendo al ejemplo de funciones sobrecargadas:

std::string hocus_pocus = "Hocus Pocus!";

/* 1 */ void f(std::string &s);  // Funcion que recibe una referencia normal.
/* 2 */ void f(std::string &&s); // Funcion que recibe una RvD.

f(hocus_pocus);                 // Llama a la primera sobrecarga.
f("Hocus Pocus!");              // Llama a la segunda sobrecarga.
f(std::string("Lorem Ipsum!")); // Llama a la segunda sobrecarga.
f(std::move(hocus_pocus));      // Llama a la SEGUNDA sobrecarga.

Es importante remarcar que std::move, pese a recibir el nombre mover, no mueve nada, tan sólo transforma una variable a RvD, así que esta instrucción no mueve nada a ningún lado:

std::string hocus_pocus = "Hocus Pocus!";

std::move(hocus_pocus); // Esto NO HACE NADA de nada.

¿Qué pasa con lo que muevo?

Como hemos visto, std::move transforma de referencia (&) a RvD (&&) y es en realidad la instrucción o constructor que recibe dicha RvD la que realiza la operación de movimiento, pero ¿Qué pasa con los objetos movidos?

std::string hocus_pocus = "Hocus Pocus!"; // Crear objeto

void f(std::string &&s);

f(std::move(hocus_pocus)); // Mover objeto dentro de funcion...

std::cout << hocus_pocus; // Que contiene ahora hocus_pocus?

Según el estándar de C++ , los objetos movidos son válidos aunque su contenido es indeterminado (traducción y resaltado míos):

7.6.5.15 Estado de los tipos de librería movidos

Los objetos con tipos definidos en la librería estándar de C++ pueden ser movidos. Las operaciones de movimiento pueden ser generadas explícitamente o implícitamente. Salvo que se indique lo contrario, estos objetos movidos serán establecidos a un estado válido pero indeterminado.

No es oro todo lo que reluce.

Como casi siempre que se añade una característica nueva, surgen usos incorrectos de la misma, no es conveniente abusar de std::move ya que puede derivar en un empeoramiento del rendimiento al evitar que el compilador aplique optimizaciones, por ejemplo el siguiente código tiene un problema:

std::string Publicar_Quijote()
{
    std::string Quijote("En un lugar de La Mancha de ..."
    return std::move(Quijote);
}

Según el estándar de C++ (traducción y resaltado míos):

« 12.8, 31.1 y 31.3 Copiar y mover objetos »

En una instrucción return en una función, cuando la expresión es el nombre de un objeto, la operación de copiar/mover puede ser omitida construyendo el objeto directamente en el valor de retorno de la función.

Cuando un objeto temporal pueda ser copiado/movido, la operación de copiar/mover puede ser omitida construyendo el objeto temporal directamente en el destino del objeto copiado/movido.

Así que conviene no abusar de las llamadas a std::move pues evitan estas optimizaciones del compilador conocidas como "Optimización del Valor de Retorno" (OvR) (RVO en Inglés ). Vale la pena destacar que la OvR existe en C++ antes que C++11 y no requiere ninguna acción por parte del programador para sacarle provecho.

Más Información.

Puedes ver esta explicación de las semánticas de movimiento en este vídeo (vídeo en Español, texto en Inglés).

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82