Una referencia R-value es una referencia capaz de enlazarse a valores temporales, es decir, a objetos que normalmente están a punto de desaparecer.
Las referencias R-value se escriben con && y son una de las bases de C++ moderno. Gracias a ellas, el lenguaje puede distinguir entre objetos que debemos conservar y objetos cuyos recursos podemos mover.
Este artículo continúa lo que vimos con L-values y R-values. Si esa distinción todavía te baila un poco, no pasa nada: aquí la vamos a usar de forma práctica.
Recordatorio rápido
En C++, una expresión puede comportarse como L-value o R-value.
Un L-value es algo con identidad: una variable con nombre, una posición reconocible en memoria, algo que sigue existiendo después de la expresión.
int x = 10; // x es un L-value
x = 20; // podemos asignarle otro valorUn R-value suele ser un valor temporal: un literal, el resultado de una operación o un objeto que se va a destruir enseguida.
int y = x + 5; // x + 5 es un R-valueEsa diferencia nos permite decirle al compilador: esto es estable, cópialo con cuidado o esto es temporal, puedes aprovechar sus recursos.
Declarar una referencia R-value
Una referencia R-value se declara con dos ampersands (&&).
int&& ref = 42;
ref += 8;
std::cout << ref << "\n"; // 50Aunque parezca raro, ref tiene nombre. Por tanto, dentro del código ref se comporta como un L-value. Esto es importante, porque es uno de esos detalles que al principio parece una broma de mal gusto.
void procesar(int&& valor) {
// valor tiene nombre, así que aquí se comporta como L-value
}Sobrecargar funciones con & y &&
Las referencias R-value permiten crear funciones distintas para objetos persistentes y objetos temporales.
#include <iostream>
void procesar(int& valor) {
std::cout << "L-value: " << valor << "\n";
}
void procesar(int&& valor) {
std::cout << "R-value: " << valor << "\n";
}
int main() {
int x = 10;
procesar(x); // L-value
procesar(20); // R-value
procesar(x + 5); // R-value
}Esto es la puerta de entrada a la semántica de movimiento. Si recibimos un R-value, sabemos que probablemente podemos mover sus recursos en lugar de copiarlos.
Relación con std::move
std::move no mueve nada por sí mismo. Lo que hace es convertir una expresión en R-value, para permitir que se llame a constructores u operadores de movimiento.
#include <string>
#include <utility>
std::string origen = "texto largo";
std::string destino = std::move(origen);Después de std::move, origen sigue siendo un objeto válido, pero su contenido queda en un estado válido aunque no especificado. Puedes destruirlo o asignarle otro valor, pero no debes asumir qué contiene.
No uses std::move por costumbre. Úsalo cuando quieras transferir recursos y sepas que el valor actual del objeto original ya no te hace falta.
Un ejemplo con un vector
Con objetos grandes, mover puede ahorrar mucho trabajo. Copiar un std::vector implica duplicar todos sus elementos; moverlo normalmente consiste en transferir el buffer interno.
#include <iostream>
#include <utility>
#include <vector>
int main() {
std::vector<int> datos = {1, 2, 3, 4, 5};
std::vector<int> copia = datos; // copia los elementos
std::vector<int> movido = std::move(datos); // transfiere el recurso
std::cout << "movido: " << movido.size() << "\n";
}La diferencia no se nota con cinco enteros, claro. Pero con un buffer grande, imágenes, cadenas enormes o recursos del sistema, evitar copias innecesarias sí importa.