ProgramaciónDesarrollador C++

¿Qué son 'lvalue' y 'rvalue' en C++? Explica sus diferencias, las formas de pasarlos a funciones y por qué aparecieron las referencias a rvalue (rvalue references)?

Supere entrevistas con el asistente de IA Hintsage

Respuesta

En C++ lvalue (valor de la izquierda) es una expresión que se refiere a un objeto en memoria que tiene un nombre y puede ser referenciado (por ejemplo, una variable). rvalue (valor de la derecha) es un valor temporal que no tiene nombre y no es un objeto en el sentido tradicional (por ejemplo, el resultado de a + b, literales como 5).

Se puede tomar la dirección de un lvalue, pero no de un rvalue (hasta la llegada de las referencias a rvalue). Para pasar estos a funciones existen:

  • Referencias normales: void foo(const std::string& s); — aceptan lvalue y rvalue.
  • Referencias lvalue: void bar(std::string& s); — aceptan solo lvalue.
  • Referencias rvalue (C++11+): void baz(std::string&& s); — aceptan solo rvalue.

Ejemplo:

void takeValue(std::string& s) { } // lvalue void takeRValue(std::string&& s) { } // rvalue std::string s = "hello"; takeValue(s); // OK, lvalue takeRValue(std::string("hi")); // OK, rvalue

Las referencias rvalue son necesarias para la transmisión eficiente de objetos temporales, principalmente para la semántica de movimiento, para mover recursos en lugar de copiarlos.

Pregunta trampa

¿Qué tipo de referencia (lvalue o rvalue) obtendrá la expresión std::move(obj)? ¿De qué categoría se convertirá el mismo objeto después de aplicar std::move?

Respuesta:

std::move(obj) siempre devuelve una referencia rvalue (T&&), pero el objeto mismo sigue siendo un lvalue, simplemente se le aplica una conversión explícita. Después de esto, hay que manejar el objeto con mucho cuidado (puede estar en un estado indefinido, pero válido).

Ejemplo:

std::string s = "data"; std::string d = std::move(s); // d recibe los datos de s, s ahora está vacío

Ejemplos de errores reales debido al desconocimiento de los matices del tema.


Historia

En un gran proyecto, uno de los desarrolladores pasaba objetos temporales a través de una referencia lvalue T& (en lugar de T&& o const T&). Esto resultaba en errores de compilación y copias subóptimas: la semántica de movimiento no se utilizaba, y el rendimiento se reducía en un 40%.


Historia

En el motor frontend, se aplicaba incorrectamente std::move a variables que luego se volvían a utilizar. Esto dejaba a las variables en un estado "destruido", provocando cierres inesperados y fallos en los hilos de renderizado.


Historia

En la biblioteca de serialización, se pasaba un contenedor de tipo std::vector<T> a una función como lvalue, mientras que se esperaba un movimiento. En lugar de mover, se realizaba una costosa copia de una gran cantidad de elementos, lo que afectaba drásticamente el tiempo de serialización en grandes arreglos.