¿Qué se esconde detrás de las letras l, r, x, gl y pr? Analicemos las categorías de valores en C++
\n\nSi alguna vez has escrito código en C++ y has recibido un error como "cannot bind non-const lvalue reference to an rvalue", entonces ya te has encontrado con esta «magia». Las categorías de valores — lvalue, rvalue, xvalue, glvalue y prvalue — no son solo una clasificación académica. Son la base sobre la que se sustenta la semántica de movimiento (move semantics) y el reenvío perfecto (perfect forwarding).
Aclaremos de una vez por todas qué significa cada letra, cómo distinguirlas en la práctica y por qué sin este conocimiento es imposible escribir C++ moderno y eficiente.
\n\nBreve historia: ¿de dónde surgieron estos cinco términos?
\n\nOriginalmente, en la época de C, solo existían dos conceptos: lvalue (valor izquierdo — lo que puede estar a la izquierda de una asignación) y rvalue (valor derecho — lo que está a la derecha). Todo era simple: una variable es lvalue, un número es rvalue.
\n\nCon la llegada de C++11 y la semántica de movimiento, este panorama simple dejó de funcionar. Aparecieron las referencias a rvalue (&&) y la necesidad de distinguir objetos temporales «moribundos». Así nació la nueva clasificación, que el estándar de C++ describe a través de cinco categorías.
Observa la jerarquía (diagrama en forma de texto):
\n\n expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue\n\nRecuerda esta imagen — es la clave de todo.
\n\nlvalue (left value) — objetos «longevos»
\n\nlvalue es una expresión que hace referencia a un objeto que tiene una dirección en memoria. Dicho objeto no desaparecerá después de que termine la expresión. Puedes tomar su dirección, modificarlo, y seguirá existiendo.
\n\nEjemplos de lvalue:
\n- \n
- Nombre de variable:
int x = 5; xes un lvalue. \n - Puntero desreferenciado:
*ptr. \n - Referencia a una variable:
int& ref = x; ref. \n - Literal de cadena:
"hello"(sí, es un lvalue, aunque constante). \n
Código:
\nint main() {\n int a = 10;\n int* p = &a; // Tomar dirección — ok, a es lvalue\n int& ref = a; // Referencia a lvalue\n\n // &(a + 1); // ¡Error! a+1 es rvalue, no tiene dirección\n return 0;\n}\n\nrvalue (right value) — valores «desechables»
\n\nrvalue es una expresión que no tiene una dirección permanente en memoria. Solo existe en el momento de evaluar la expresión. No puedes tomar su dirección (con raras excepciones).
\n\nLos rvalue incluyen:
\n- \n
- Literales (excepto cadenas):
42,3.14f,true. \n - Resultados de operaciones aritméticas:
a + b. \n - Objetos temporales:
std::string("temp"). \n - Expresiones lambda. \n
Código:
\nint foo() { return 5; }\n\nint main() {\n int x = 42; // 42 — rvalue\n int y = x + 10; // x+10 — rvalue\n // int* p = &(x + 10); // ¡Error! No se puede tomar la dirección de un rvalue\n return 0;\n}\n\nprvalue (pure rvalue) — rvalue «puros»
\n\nprvalue es el rvalue clásico de C++98. Es un valor temporal que no tiene nombre y no está vinculado a la memoria. A este prvalue solemos llamarlo simplemente rvalue en el lenguaje coloquial.
\n\nEjemplos de prvalue:
\n- \n
- Números, enumeraciones, resultados de operadores. \n
- Llamada a una función que devuelve una no-referencia:
std::to_string(123). \n - Objeto temporal creado explícitamente:
MyClass(). \n
xvalue (expiring value) — objetos «moribundos»
\n\nAquí es donde empieza lo más interesante. xvalue es un objeto que pronto será destruido, pero cuyos recursos aún pueden «salvarse» (moverse). Este es un pu