cpp-rvalue-references

Qué son las referencias R-value en C++

  • 3 min

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 valor
Copied!

Un 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-value
Copied!

Esa 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"; // 50
Copied!

Aunque 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
}
Copied!

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
}
Copied!

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);
Copied!

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";
}
Copied!

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.