Las categorías de expresiones, como lvalue y rvalue , se relacionan más con los conceptos teóricos fundamentales del lenguaje C ++ que con los aspectos prácticos de su uso. Por esta razón, muchos programadores incluso experimentados tienen una vaga idea de lo que significan. En este artículo intentaré explicar el significado de estos términos de la manera más simple posible, diluyendo la teoría con ejemplos prácticos. Haré una reserva de inmediato: el artículo no pretende proporcionar la descripción más completa y rigurosa de las categorías de expresiones, para más detalles recomiendo contactar directamente a la fuente: estándar de lenguaje C ++.
El artículo contendrá muchos términos en inglés, esto se debe al hecho de que algunos de ellos son difíciles de traducir al ruso, mientras que otros se traducen en diferentes fuentes de diferentes maneras. Por lo tanto, a menudo indicaré términos en inglés, resaltándolos en cursiva .
Un poco de historia
Los términos lvalue y rvalue aparecieron en C. Vale la pena señalar que la confusión se estableció inicialmente en la terminología, porque se refieren a expresiones y no a valores. Históricamente, un valor de l es lo que puede quedar del operador de asignación, y un valor de r es lo que solo puede ser correcto .
lvalue = rvalue;
Sin embargo, tal definición simplifica y distorsiona un poco la esencia. El estándar C89 define lvalue como un localizador de objetos , es decir Un objeto con una ubicación de memoria identificable. En consecuencia, todo lo que no se ajustaba a esta definición se incluyó en la categoría de valor.
Bjarn se apresura al rescate
En C ++, la terminología de las categorías de expresión ha evolucionado bastante, especialmente después de la adopción del estándar C ++ 11, que introdujo los conceptos de enlaces de valor y semántica de movimiento . La historia de la aparición de una nueva terminología se describe de manera interesante en el artículo "Nueva" Terminología de valor de Straustrup .
La nueva terminología más rigurosa se basa en 2 propiedades:
- la presencia de identidad ( identidad ), es decir, algún parámetro por el cual se puede entender si dos expresiones se refieren a la misma entidad o no (por ejemplo, una dirección en la memoria);
- la capacidad de moverse ( se puede mover desde ): apoya la semántica del movimiento.
Las expresiones que expresan identidad se generalizan bajo el término glvalue ( valores generalizados ), las expresiones itinerantes se denominan rvalue . Las combinaciones de estas dos propiedades han identificado 3 categorías principales de expresiones:
| Tener una identidad | Desprovisto de identidad |
---|
No se puede mover | lvalue | - |
Puede ser movido | xvalue | prvalue |
De hecho, el estándar C ++ 17 introdujo el concepto de copia de elisión , formalizando situaciones en las que el compilador puede y debe evitar copiar y mover objetos. A este respecto, el valor prva no necesariamente se puede mover. Detalles y ejemplos se pueden encontrar aquí . Sin embargo, esto no afecta la comprensión del esquema general de categorías de expresiones.
En el estándar C ++ moderno, la estructura de categorías se presenta en forma de dicho esquema:

Examinemos en términos generales las propiedades de las categorías, así como las expresiones del lenguaje que se incluyen en cada una de las categorías. Noto de inmediato que las listas de expresiones a continuación para cada categoría no pueden considerarse completas; para obtener información más precisa y detallada, consulte directamente el Estándar C ++.
glvalue
Las expresiones en la categoría glvalue tienen las siguientes propiedades:
- puede convertirse implícitamente en prvalue ;
- puede ser polimórfico, es decir, para ellos los conceptos de tipo estático y dinámico tienen sentido;
- no puede ser de tipo vacío : esto se deduce directamente de la propiedad de tener identidad, porque para las expresiones de tipo vacío no existe tal parámetro que los distinga entre sí;
- puede tener un tipo incompleto , por ejemplo, en forma de declaración directa (si está permitido para una expresión particular).
rvalue
Las expresiones en la categoría rvalue tienen las siguientes propiedades:
- no puede obtener la dirección rvalue en la memoria; esto se deriva directamente de la falta de propiedad de identidad;
- no puede estar en el lado izquierdo de una asignación o declaración de asignación compuesta;
- se puede usar para inicializar un enlace lvalue constante o un enlace rvalue , mientras que la vida útil del objeto se extiende hasta la vida útil del enlace;
- si se usa como argumento al llamar a una función que tiene 2 versiones sobrecargadas: una acepta una referencia de valor constante y la otra una referencia de valor, luego se selecciona la versión que acepta la referencia de valor. Es esta propiedad la que se usa para implementar la semántica de movimiento :
class A { public: A() = default; A(const A&) { std::cout << "A::A(const A&)\n"; } A(A&&) { std::cout << "A::A(A&&)\n"; } }; ......... A a; A b(a); // A(const A&) A c(std::move(a)); // A(A&&)
Técnicamente, A&& es un valor de r y se puede usar para inicializar tanto una referencia de valor constante como una referencia de valor de r . Pero gracias a esta propiedad, no hay ambigüedad; se acepta una opción de constructor que acepte una referencia de valor.
lvalue
Propiedades:
- todas las propiedades de valor de gl (ver arriba);
- puede tomar la dirección (usando el operador unario incorporado
&
); - los valores modificables pueden estar en el lado izquierdo del operador de asignación u operadores de asignación compuesta;
- se puede usar para inicializar una referencia a un valor l (tanto constante como no constante).
Las siguientes expresiones pertenecen a la categoría lvalue :
- el nombre de una variable, función o campo de clase de cualquier tipo. Incluso si la variable es una referencia de valor, el nombre de esta variable en la expresión es un valor;
void func() {} ......... auto* func_ptr = &func; // : auto& func_ref = func; // : int&& rrn = int(123); auto* pn = &rrn; // : auto& rn = rrn; // : lvalue-
- llamar a una función o un operador sobrecargado que devuelve una referencia de lvalue , o una expresión de conversión al tipo de una referencia de lvalue ;
- operadores de asignación incorporados, operadores de asignación compuesta (
=
, +=
, /=
, etc.), pre-incremento e pre-incremento incorporado ( ++a
, --b
), operador de desreferencia de puntero incorporado ( *p
); - operador incorporado de acceso por índice (
a[n]
o n[a]
), cuando uno de los operandos es una matriz lvalue ; - llamar a una función o una declaración sobrecargada que devuelve una referencia de valor r a una función;
- literal de cadena como
"Hello, world!"
.
Un literal de cadena difiere de todos los demás literales en C ++ precisamente en que es un valor l (aunque inmutable). Por ejemplo, puede obtener su dirección:
auto* p = &”Hello, world!”; // ,
prvalue
Propiedades:
- todas las propiedades de valor (ver arriba);
- no puede ser polimórfico: los tipos de expresión estática y dinámica siempre coinciden;
- no puede ser de un tipo incompleto (a excepción del tipo nulo , esto se discutirá a continuación);
- no puede tener un tipo abstracto o ser una matriz de elementos de un tipo abstracto.
Las siguientes expresiones pertenecen a la categoría prvalue :
- literal (excepto cadena), por ejemplo
42
, true
o nullptr
; - una llamada a función o un operador sobrecargado que devuelve una no referencia (
str.substr(1, 2)
, str1 + str2
, it++
) o una expresión de conversión a un tipo sin referencia (por ejemplo, static_cast<double>(x)
, std::string{}
, (int)42
); - post-incremento y post-decremento
b--
( a++
, b--
), operaciones matemáticas integradas ( a + b
, a % b
, a & b
, a << b
, etc.), operaciones lógicas integradas ( a && b
, a || b
!a
, etc.), operaciones de comparación ( a < b
, a == b
, a >= b
, etc.), la operación integrada de tomar la dirección ( &a
); - este puntero
- elemento de listado;
- parámetro de plantilla atípico, si no es una clase;
- expresión lambda, por ejemplo
[](int x){ return x * x; }
[](int x){ return x * x; }
.
xvalue
Propiedades:
- todas las propiedades de valor (ver arriba);
- todas las propiedades de valor de gl (ver arriba).
Ejemplos de expresiones de xvalue :
- llamar a una función u operador incorporado que devuelve una referencia de valor r , por ejemplo std :: move (x) ;
y, de hecho, para el resultado de llamar a std :: move (), no puede obtener una dirección en la memoria o inicializar un enlace, pero al mismo tiempo, esta expresión puede ser polimórfica:
struct XA { virtual void f() { std::cout << "XA::f()\n"; } }; struct XB : public XA { virtual void f() { std::cout << "XB::f()\n"; } }; XA&& xa = XB(); auto* p = &std::move(xa); // auto& r = std::move(xa); // std::move(xa).f(); // “XB::f()”
- operador incorporado de acceso por índice (
a[n]
o n[a]
) cuando uno de los operandos es una matriz de valores r .
Algunos casos especiales
Operador de coma
Para el operador de coma incorporado, la categoría de expresión siempre coincide con la categoría de expresión del segundo operando.
int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // , rvalue auto& rt = (1, int(123)); // , rvalue
Expresiones vacías
Las llamadas a funciones que devuelven void , escriben expresiones de conversión en void y arrojan excepciones se consideran expresiones prvalue , pero no se pueden usar para inicializar referencias o como argumentos para funciones.
Operador de comparación ternario
Definición de la categoría de expresión a ? b : c
a ? b : c
: el caso no es trivial, todo depende de las categorías del segundo y tercer argumento ( c
):
- si
b
o c
son de tipo void , entonces la categoría y el tipo de la expresión completa corresponden a la categoría y el tipo del otro argumento. Si ambos argumentos son de tipo void , entonces el resultado es un valor de tipo void ; - si
c
son valores de gl del mismo tipo, entonces el resultado es un valor de gl del mismo tipo; - en otros casos, el resultado es prvalue.
Para el operador ternario, se definen una serie de reglas según las cuales se pueden aplicar conversiones implícitas a los argumentos byc, pero esto está algo más allá del alcance del artículo; si está interesado, le recomiendo consultar la sección Operador condicional [expr.cond] de la Norma.
int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, .. throw void, n ((1 < 2) ? n : v) = 2; // lvalue, , ((1 < 2) ? n : int(123)) = 2; // , .. prvalue
Referencias a campos y métodos de clases y estructuras.
Para expresiones de la forma am
y p->m
(aquí estamos hablando del operador incorporado ->
), se aplican las siguientes reglas:
- si
m
es un elemento de enumeración o un método de clase no estático, la expresión completa se considera prvalue (aunque el enlace no se puede inicializar con dicha expresión); - si
a
es un valor r m
es un campo no estático de un tipo sin referencia, entonces la expresión completa pertenece a la categoría xvalue ; - de lo contrario es un valor.
Para los punteros a los miembros de la clase ( a.*mp
y p->*mp
), las reglas son similares:
- si
mp
es un puntero a un método de clase, entonces toda la expresión se considera prvalue ; - si
a
es un valor r y mp
es un puntero a un campo de datos, entonces la expresión completa se refiere a xvalue ; - de lo contrario es un valor.
Campos de bits
Los campos de bits son una herramienta conveniente para la programación de bajo nivel, sin embargo, su implementación queda algo fuera de la estructura general de las categorías de expresión. Por ejemplo, una llamada a un campo de bits parece ser un valor l , porque puede estar presente en el lado izquierdo del operador de asignación. Al mismo tiempo, no funcionará tomar la dirección del campo de bit o inicializar un enlace no constante por ellos. Puede inicializar una referencia constante a un campo de bits, pero se creará una copia temporal del objeto:
Campos de bits [class.bit]
Si el inicializador para una referencia de tipo const T & es un valor l que se refiere a un campo de bits, la referencia está vinculada a un inicializado temporal para contener el valor del campo de bits; la referencia no está vinculada directamente al campo de bits.
struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; // auto& rb = bf; //
En lugar de una conclusión
Como mencioné en la introducción, la descripción anterior no pretende ser completa, sino que solo da una idea general de las categorías de expresiones. Esta vista proporcionará una mejor comprensión de los párrafos del Estándar y los mensajes de error del compilador.