Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En este tema se presentan y describen las distintas categorías de valores (y referencias a valores) que existen en C++:
- glvalue
- lvalue
- xlvalue
- prvalue
- rvalue
Sin duda habrá oído hablar de lvalues y rvalues. Pero es posible que no piense en ellos en los términos que presenta este tema.
Cada expresión de C++ produce un valor que pertenece a una de las cinco categorías enumeradas anteriormente. Hay aspectos del lenguaje C++, sus instalaciones y reglas, que exigen un conocimiento adecuado de estas categorías de valor, así como referencias a ellas. Estos aspectos incluyen tomar la dirección de un valor, copiar un valor, mover un valor y reenviar un valor a otra función. Este tema no profundiza en todos esos aspectos, pero proporciona información fundamental para una comprensión sólida de ellos.
La información de este tema se enmarca en términos del análisis de las categorías de valor de Stroustrup por las dos propiedades independientes de identidad y movibilidad [Stroustrup, 2013].
Un valor lvalue tiene identidad
¿Qué significa que un valor tenga identidad? Si tiene (o puede tomar) la dirección de memoria de un valor y la usa de forma segura, el valor tiene identidad. De este modo, puede hacer más que comparar el contenido de los valores; puede compararlos o distinguirlos por identidad.
Un valor lvalue tiene identidad. Hoy en día, no es más que una curiosidad histórica que la "l" de "lvalue" sea una abreviatura de "left" (es decir, el lado izquierdo de una asignación). En C++, un valor l puede aparecer a la izquierda o a la derecha de una asignación. La "l" de "lvalue", por tanto, no ayuda realmente a comprender ni a definir qué son. Solo necesita entender que lo que llamamos un lvalue es un valor que tiene identidad.
Entre los ejemplos de expresiones que son lvalues se incluyen: una variable con nombre o una constante; o una función que devuelve una referencia. Entre los ejemplos de expresiones que no son lvalues se incluyen: una temporal; o una función que devuelve por valor.
int& get_by_ref() { ... }
int get_by_val() { ... }
int main()
{
std::vector<byte> vec{ 99, 98, 97 };
std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.
int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}
Ahora bien, aunque es cierto que los lvalues tienen identidad, eso también se aplica a los xvalues. Veremos exactamente qué es un valor x más adelante en este tema. Por ahora, tenga en cuenta que hay una categoría de valor denominada glvalue (para "valor lvalue generalizado"). El conjunto de valores glvalues es el superconjunto de ambos valores lvalues (también conocidos como lvalues clásicos) y xvalues. Por lo tanto, aunque «un lvalue tiene identidad» es cierto, el conjunto completo de elementos que tienen identidad es el de los glvalues, como se muestra en esta ilustración.
Un rvalue es movible; un lvalue no lo es
Pero hay valores que no son glvalues. En otras palabras, hay valores para los que no se puede obtener una dirección de memoria (o no se puede confiar en que sea válido). Hemos visto algunos de estos valores en el ejemplo de código anterior.
No tener una dirección de memoria confiable parece una desventaja. Pero, de hecho, la ventaja de un valor como ese es que puede moverlo (que suele ser barato), en lugar de copiarlo (que suele ser costoso). Mover un valor significa que ya no está en el lugar donde solía ser. Por lo tanto, intentar acceder a él en el lugar donde solía ser es algo que se debe evitar. Una explicación de cuándo y cómo mover un valor está fuera del ámbito de este tema. En este apartado, solo necesitamos saber que un valor movible se conoce como rvalue (o rvalue clásico).
La «r» de «rvalue» es una abreviatura de «right» (es decir, el lado derecho de una asignación). Pero puede usar rvalues y referencias a rvalues fuera de las operaciones de asignación. La «r» de «rvalue», por tanto, no es en lo que hay que centrarse. Solo necesita entender que lo que llamamos rvalue es un valor que se puede mover.
Un valor lvalue, por el contrario, no es extraíble, como se muestra en esta ilustración. Si se movera un valor lvalue, eso contradecía la definición misma de lvalue. Y supondría un problema inesperado para el código que, con toda razón, esperaba poder seguir accediendo al lvalue.
Por lo tanto, no se puede mover un valor lvalue. Pero existe un tipo de glvalue (el conjunto de cosas con identidad) que puedes mover —si sabes lo que haces (incluido tener cuidado de no acceder a él después de moverlo)—, y ese es el xvalue. Revisaremos esa idea una vez más adelante en este tema cuando veamos la imagen completa de las categorías de valor.
Referencias a valor r y reglas de vinculación de referencias
En esta sección se presenta la sintaxis de una referencia a un valor r. Tendremos que esperar a otro tema para abordar en profundidad el movimiento y el reenvío, pero baste decir que las referencias a valores r son una pieza necesaria para resolver esos problemas. Sin embargo, antes de examinar las referencias de rvalue, primero debemos ser más claras sobre T&: lo que anteriormente hemos estado llamando simplemente a "una referencia". En realidad, es "una referencia lvalue (no const)", que se refiere a un valor en el que el usuario de la referencia puede escribir.
template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.
Una referencia lvalue puede enlazar a un valor lvalue, pero no a un valor rvalue.
A continuación, hay referencias const lvalue (T const&), que hacen referencia a objetos a los que el usuario de la referencia no puede escribir (por ejemplo, una constante).
template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.
Una referencia lvalue const puede enlazarse a un valor lvalue o a un valor rvalue.
La sintaxis de una referencia a un valor rvalue de tipo T se escribe como T&&. Una referencia rvalue hace referencia a un valor extraíble, un valor cuyo contenido no es necesario conservar después de haberlo usado (por ejemplo, un valor temporal). Dado que la idea es mover (y, por tanto, modificar) el valor asociado a una referencia a un valor temporal, los calificadores const y volatile (también conocidos como calificadores cv) no se aplican a las referencias a valores temporales.
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.
Una referencia de rvalue se enlaza a un valor rvalue. De hecho, en términos de resolución de sobrecarga, un valor r prefiere vincularse a una referencia a un valor r antes que a una referencia const a un valor l. Pero una referencia rvalue no puede enlazarse a un valor lvalue porque, como hemos dicho, una referencia rvalue hace referencia a un valor cuyo contenido se supone que no es necesario conservar (por ejemplo, el parámetro para un constructor de movimiento).
También puedes pasar un valor r cuando se espera un argumento por valor, mediante construcción por copia (o mediante construcción por movimiento si el valor r es un xvalue).
Un glvalue tiene identidad; un prvalue no
En esta fase, sabemos qué tiene identidad. Y sabemos lo que es extraíble y lo que no es. Pero aún no hemos denominado el conjunto de valores que no tienen identidad. Ese conjunto se conoce como prvalue o rvalue puro.
int& get_by_ref() { ... }
int get_by_val() { ... }
int main()
{
int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}
Imagen completa de categorías de valor
Sólo queda combinar la información y las ilustraciones anteriores en una sola imagen general.
glvalue (i)
Un valor glvalue (lvalue generalizado) tiene identidad. Usaremos "i" como abreviatura de "tiene identidad".
lvalue (i&!m)
Un «lvalue» (un tipo de «glvalue») tiene identidad, pero no se puede mover. Normalmente son valores de lectura-escritura que se suelen pasar por referencia, por referencia a constante o por valor si copiarlos resulta barato. Un valor lvalue no se puede enlazar a una referencia rvalue.
xvalue (i&m)
Un xvalue (un tipo de glvalue, pero también un tipo de rvalue) tiene identidad y, además, se puede mover. Este podría ser un valor l anterior que has decidido mover porque copiarlo resulta costoso, y tendrás cuidado de no volver a acceder a él después. Aquí se muestra cómo puede convertir un valor l en un valor xvalue.
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.
En el ejemplo de código anterior, aún no hemos movido nada. Simplemente hemos creado un valor xvalue mediante la conversión de un valor lvalue a una referencia rvalue sin nombre. Todavía se puede identificar por su nombre lvalue; pero, como un valor x, ahora es capaz de moverse. Las razones para moverlo y cómo sería hacerlo en la práctica tendrán que dejarse para otro tema. Pero puedes interpretar la "x" de "xvalue" como que significa "solo para expertos", si eso ayuda. Al convertir un lvalue en un xvalue (un tipo de rvalue, recuerda), el valor entonces puede vincularse a una referencia a rvalue.
Aquí tienes otros dos ejemplos de xvalues: llamar a una función que devuelve una referencia rvalue sin nombre y acceder a un miembro de un xvalue.
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.
prvalue (!i&m)
Un valor prvalue (rvalue puro; una clase de rvalue) no tiene identidad, pero es movible. Normalmente, se trata de objetos temporales, o del resultado de llamar a una función que devuelve por valor, o del resultado de evaluar cualquier otra expresión que no sea un glvalue.
rvalue (m)
Un rvalue se puede mover. Usaremos "m" como abreviatura de "es extraíble".
Una referencia a un valor r siempre se refiere a un valor r (un valor cuyo contenido se supone que no necesitamos conservar).
Pero ¿una referencia rvalue es en sí una rvalue? Una referencia de rvalue sin nombre (como las que se muestran en los ejemplos de código xvalue anteriores) es un valor xvalue, por lo que, sí, es un valor r. Prefiere enlazarse a un parámetro de función de referencia rvalue, como el de un constructor de movimiento. Por el contrario (y quizás contra-intuitivamente), si una referencia rvalue tiene un nombre, la expresión que consta de ese nombre es un valor lvalue. Por lo tanto , no se puede enlazar a un parámetro de referencia rvalue. Pero es fácil hacer que vuelva a hacerlo: basta con convertirlo de nuevo en una referencia rvalue sin nombre (un xvalue).
void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
foo(a); // Calls foo(A&).
foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.
!i&!m
El tipo de valor que no tiene identidad y que no es extraíble es la combinación que aún no hemos analizado. Pero podemos ignorarlo, porque esa categoría no es una idea útil en el lenguaje C++.
Reglas de contracción de referencia
Varias referencias similares en una expresión (una referencia lvalue a una referencia lvalue o una referencia rvalue a una referencia rvalue) se cancelan entre sí.
-
A& &se contrae enA&. -
A&& &&se contrae enA&&.
Varias referencias de distinto tipo en una expresión colapsan en una referencia a un valor l.
-
A& &&se contrae enA&. -
A&& &se contrae enA&.
Referencias de reenvío
En esta sección final se contrastan las referencias de rvalue, que ya hemos analizado, con el concepto diferente de una referencia de reenvío. Antes de que se acuñara el término "referencia de reenvío", algunas personas usaban el término "referencia universal".
void foo(A&& a) { ... }
-
A&&es una referencia rvalue, como hemos visto. Const y volatile no se aplican a las referencias rvalue. -
foosolo acepta rvalues de tipo A. - La razón por la que existen las referencias rvalue (como
A&&) es que puedas definir una sobrecarga optimizada para el caso en que se pase un valor temporal (u otro rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&es una referencia de reenvío. Dependiendo de lo que se le pase abar, el tipo _Ty puede ser const o no const independientemente de que sea volatile o no volatile. -
baracepta cualquier valor lvalue o rvalue de tipo _Ty. - Pasar un valor lvalue hace que la referencia de reenvío se convierta en
_Ty& &&, que se contrae a la referencia_Ty&lvalue . - Pasar un valor r hace que la referencia de reenvío se convierta en la referencia
_Ty&&rvalue . - La razón por la que existen las referencias de reenvío (como
_Ty&&) no es para optimizar, sino para tomar aquello que se les pasa y reenviarlo de manera transparente y eficiente. Es probable que solo te encuentres con una referencia de reenvío si escribes código de bibliotecas (o lo estudias detenidamente), por ejemplo, una función de fábrica que reenvía los argumentos al constructor.
Sources
- [Stroustrup, 2013] B. Stroustrup: El lenguaje de programación C++, cuarta edición. Addison-Wesley. 2013.