Categorías de valor y referencias a estas
En este tema se presentan y describen las distintas categorías de valores (y referencias a los 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.
Todas las expresiones de C++ dan como resultado un valor que pertenece a una de las cinco categorías indicadas anteriormente. Hay aspectos del lenguaje C++, sus utilidades y reglas, que exigen una comprensión apropiada de estas categorías de valor, así como las referencias a ellas. Entre estos aspectos se incluye tomar la dirección de un valor, copiarlo, moverlo y reenviarlo a otra función. En este tema no se tratan todos esos aspectos en profundidad, pero se proporciona información básica para una comprensión sólida.
La información de este tema se encuadra en términos de análisis de Stroustrup de categorías de valor mediante las dos propiedades independientes de identidad y movilidad [Stroustrup, 2013].
Un valor lvalue tiene identidad
¿Qué significa que un valor tiene identidad? Si tiene la dirección de memoria de un valor (o la puede tomar) y la usa con seguridad, el valor tiene identidad. De este modo, puede hacer algo más que comparar el contenido de los valores: puede compararlos o distinguirlos por su identidad.
Un valor lvalue tiene identidad. Como apunte de interés únicamente histórico, la "l" en "lvalue" es una abreviación de "left" (izquierda), como en el lado izquierdo de una asignación. En C++, un valor lvalue puede aparecer a la izquierda o a la derecha de una asignación. La "l" de "lvalue" realmente no ayuda a comprender ni definir lo que son. Solo tienes que entender que lo que llamamos lvalue es un valor que tiene identidad.
Entre los ejemplos de expresiones que son lvalues se incluyen: una variable o una constante con nombre, o una función que devuelve una referencia. Entre los ejemplos de expresiones que no son lvalues se incluyen: un archivo 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, si bien la afirmación de que lvalues tiene identidad es una instrucción verdadera, lo es también para xvalues. Veremos exactamente lo que es un valor xvalue más adelante en este tema. Por ahora, simplemente tenga en cuenta que hay una categoría de valor llamada glvalue (para "lvalue generalizado"). El conjunto de glvalues es el superconjunto de ambos valores lvalues (también conocidos como lvalues clásicos) y xvalues. De este modo, mientras "un valor lvalue tiene identidad" es verdadero, el conjunto completo de cosas que tienen identidades es el conjunto de glvalues, tal como se muestra en esta ilustración.
Un valor rvalue es móvil; un elemento lvalue no lo es
Sin embargo, 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 puede basarse en ella para que sean válidos). Hemos visto algunos de estos valores en el ejemplo de código anterior.
No tener una dirección de memoria confiable suena como una desventaja. Pero, en realidad, la ventaja de un valor como este es que se puede mover desde él (lo que es normalmente barato), en lugar de copiar desde él (que normalmente es costoso). Mover un valor significa que ya no está en el lugar en el que solía estar. Por lo tanto, intentar acceder a él en el lugar en el que solía estar es algo que se debe evitar hacer. En el ámbito de este tema no se explica cuándo y cómo mover un valor. Para ello, nos basta con saber que un valor que se puede mover es conocido como un valor rvalue (o rvalue clásico).
La "r" de "rvalue" es una abreviatura de "right" (derecha), como en el lado derecho de una asignación. Pero puedes usar rvalues y referencias a rvalues fuera de las asignaciones. Por tanto, la "r" de "rvalue" no es el elemento en el que centrarse. Solo tienes que entender que lo que llamamos rvalue es un valor que es móvil.
Un valor lvalue, por el contrario, no es móvil, tal como se muestra en esta ilustración. Si un valor lvalue se moviese, eso contradiría la definición misma de lvalue. Y sería un problema inesperado para el código que razonablemente se esperaba que pudiese seguir teniendo acceso al valor lvalue.
Por lo tanto, no se puede mover un valor lvalue. Sin embargo, existe un tipo de valores glvalue (un conjunto de cosas con identidad) que se puede mover, si sabe lo que está haciendo (incluido tener cuidado de no acceder a él después de moverlo), y se llama xvalue. Retomaremos esta idea más adelante en este tema, cuando nos centremos en la imagen completa de las categorías de valor.
Referencias rvalue y reglas de enlace de referencias
En esta sección se presenta la sintaxis para una referencia a un valor rvalue. Tendremos que esperar a otro tema para profundizar en el movimiento y reenvío, pero basta decir que las referencias rvalue son una parte necesaria de la solución a esos problemas. Aunque, antes de adentrarnos en las referencias rvalue, primero es necesario ser más claros sobre T&
, lo que anteriormente hemos llamado simplemente "una referencia". Es realmente "una referencia lvalue (no const), que hace referencia a un valor al 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 uno rvalue.
Luego, hay referencias const de lvalue (T const&
), que hacen referencia a objetos en 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 const de lvalue puede enlazar a un valor lvalue o a uno rvalue.
La sintaxis para una referencia a un valor rvalue de tipo T
se escribe como T&&
. Una referencia rvalue hace referencia a un valor móvil: un valor cuyo contenido no es necesario conservar una vez que lo hemos usado (por ejemplo, un archivo temporal). Puesto que el objetivo es moverlo desde el valor enlazado a una referencia rvalue (y de esta forma, modificarlo), los calificadores const
y volatile
(también conocidos como calificadores cv) no se aplican a las referencias rvalue.
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 rvalue se enlaza a un valor rvalue. De hecho, en términos de resolución de sobrecarga, un valor rvalue prefiere enlazarse a una referencia rvalue más que a una referencia const de lvalue. Pero una referencia rvalue no se puede enlazar a un valor lvalue porque, como ya 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 puede pasar un valor rvalue donde se espera un argumento por valor, con la construcción de copia (o con la construcción de movimiento, si el valor rvalue es un valor xvalue).
Un valor glvalue tiene identidad; un valor prvalue no
En esta etapa ya sabemos lo que tiene identidad. Y sabemos lo que es móvil y lo que no. Pero aún no hemos denominado el conjunto de valores que no tiene identidad. Este conjunto se conoce como el valor 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.
}
La imagen completa de las categorías de valor
Solo 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 "has identity".
lvalue (i&!m)
Un valor lvalue (un tipo de glvalue) tiene identidad, pero no es móvil. Normalmente estos son los valores de lectura y escritura que distribuyes por referencia o referencia de tipo const, o por valor si copiar es barato. No se puede enlazar un valor lvalue a una referencia rvalue.
xvalue (i&m)
Un valor xvalue (un tipo de glvalue, pero también un tipo de rvalue) tiene identidad y también es móvil. Esto podría ser un valor antiguo lvalue que has decidido mover porque la copia es costosa. En este caso, tendrás que tener cuidado de no acceder a él posteriormente. Aquí se muestra cómo puedes convertir un valor lvalue en uno 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, todavía no hemos movido nada. Simplemente hemos creado un valor xvalue al convertir un valor lvalue en una referencia rvalue sin nombre. Todavía puede identificarse por su nombre de lvalue; pero, como un valor xvalue, ahora se puede mover. Los motivos para hacerlo y el aspecto que tiene realmente el movimiento se tratarán en otro tema. Pero se puede pensar que el significado de la "x" en "xvalue" es "solo experto", si esto ayuda. Al convertir un valor lvalue en uno xvalue (recuerde, un tipo de valor rvalue), el valor pasa a poder enlazarse a una referencia rvalue.
Estos son dos ejemplos más de xvalues, que llaman a una función que devuelve una referencia rvalue sin nombre y acceden a un miembro de un valor 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; un tipo de valor rvalue) no tiene identidad, pero es móvil. Normalmente son temporales, el resultado de llamar a una función que devuelve por valor o el resultado de evaluar cualquier otra expresión que no es un valor glvalue.
rvalue (m)
Un valor rvalue es móvil. Usaremos "m" como abreviatura de "is movable".
Una referencia rvalue siempre hace referencia a un valor rvalue (un valor cuyo contenido se supone que no es necesario conservar).
Sin embargo, ¿una referencia rvalue no es en sí misma un valor rvalue? Una referencia rvalue sin nombre (como las que se muestran en los ejemplos de código xvalue anteriores) es un valor xvalue, por lo tanto, es un valor rvalue. Prefiere enlazarse a un parámetro de función de referencia rvalue, como el de un constructor de movimiento. En cambio, y quizás contra toda lógica, si una referencia rvalue tiene un nombre, la expresión que forma ese nombre es un valor lvalue. Por tanto, no se puede enlazar a un parámetro de referencia rvalue. Es sencillo: simplemente conviértalo otra vez en una referencia de rvalue sin nombre (un valor 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 no es móvil es la combinación que todavía no hemos analizado. Pero podemos pasarlo por alto, porque esa categoría no es una idea útil en el lenguaje C++.
Reglas de contracción de referencia
Varias referencias de tipo like en una expresión (una referencia lvalue a una referencia lvalue o una referencia rvalue en una referencia rvalue) se anulan mutuamente.
A& &
se contrae enA&
.A&& &&
se contrae enA&&
.
Varias referencias de tipo unlike en una expresión se contraen a una referencia lvalue.
A& &&
se contrae enA&
.A&& &
se contrae enA&
.
Referencias de reenvío
En esta última sección se contrastan las referencias rvalue, que ya se han analizado, con el concepto distinto de una referencia de reenvío. Antes de que se acuñase el término "referencia de reenvío", algunos usuarios usaban el término "referencia universal".
void foo(A&& a) { ... }
A&&
es una referencia rvalue, tal como hemos visto. Los tipos const y volatile no se aplican a las referencias rvalue.foo
solo acepta valores rvalue de tipo A.- Las referencias rvalue (como
A&&
) existen para que puedas crear una sobrecarga que esté optimizada en el caso de que se pase un archivo temporal (u otro valor rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
_Ty&&
es una referencia de reenvío. En función de lo que pases abar
, el tipo _Ty puede ser de tipo const o no const independientemente de si es de tipo volatile o no volatile.bar
acepta cualquier valor lvalue o rvalue de tipo _Ty.- Pasar un valor lvalue provoca que la referencia de reenvío se convierta en
_Ty& &&
, que se contrae a la referencia lvalue_Ty&
. - Pasar un valor rvalue provoca que la referencia de reenvío se convierta en la referencia rvalue
_Ty&&
. - Las referencias de reenvío (como
_Ty&&
) no existen para la optimización, sino para que puedas tomar lo que les pasas y reenviarlo de forma transparente y eficiente. Es probable que encuentre una referencia de reenvío solo si escribe (o estudia de cerca) código de biblioteca; por ejemplo, una función de generador que reenvía argumentos de constructor.
Orígenes
- [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, cuarta edición. Addison-Wesley. 2013.