Categorias de expressões, como lvalue e rvalue , relacionam-se mais aos conceitos teóricos fundamentais da linguagem C ++ do que aos aspectos práticos de seu uso. Por esse motivo, muitos programadores experientes têm uma vaga idéia do que eles significam. Neste artigo, tentarei explicar o significado desses termos da maneira mais simples possível, diluindo a teoria com exemplos práticos. Farei uma reserva imediatamente: o artigo não pretende fornecer a descrição mais completa e rigorosa das categorias de expressões. Para obter detalhes, recomendo entrar em contato diretamente com a fonte: padrão da linguagem C ++.
O artigo conterá muitos termos no idioma inglês, devido ao fato de que alguns deles são difíceis de traduzir para o russo, enquanto outros são traduzidos em diferentes fontes de maneiras diferentes. Portanto, frequentemente indico termos em inglês, destacando-os em itálico .
Um pouco de história
Os termos lvalue e rvalue apareceram em C. Vale a pena notar que a confusão foi inicialmente estabelecida na terminologia, porque se refere a expressões e não a valores. Historicamente, um lvalue é o que pode ser deixado pelo operador de atribuição, e um rvalue é o que pode estar certo apenas.
lvalue = rvalue;
No entanto, essa definição simplifica e distorce a essência. O padrão C89 definiu lvalue como um localizador de objetos , ou seja, Um objeto com um local de memória identificável. Dessa forma, tudo o que não se encaixava nessa definição foi incluído na categoria rvalue .
Bjarn corre para o resgate
No C ++, a terminologia das categorias de expressão evoluiu bastante fortemente, especialmente após a adoção do C ++ 11 Standard, que introduziu os conceitos de links de valores racionais e move a semântica . A história do surgimento de uma nova terminologia é curiosamente descrita no artigo de Straustrup “New” Value Terminology .
A nova terminologia mais rigorosa é baseada em 2 propriedades:
- a presença de identidade ( identidade ) - ou seja, algum parâmetro pelo qual se pode entender se duas expressões se referem à mesma entidade ou não (por exemplo, um endereço na memória);
- a capacidade de se mover ( pode ser movido de ) - suporta a semântica do movimento.
Expressões que expressam identidade são generalizadas sob o termo glvalue ( valores generalizados ), expressões de roaming são chamadas rvalue . As combinações dessas duas propriedades identificaram três categorias principais de expressões:
| Tenha uma identidade | Desprovido de identidade |
---|
Não pode ser movido | lvalue | - |
Pode ser movido | xvalue | prvalue |
De fato, o C ++ 17 Standard introduziu o conceito de remoção de cópia - formalizando situações em que o compilador pode e deve evitar copiar e mover objetos. Nesse sentido, o valor inicial não pode necessariamente ser movido. Detalhes e exemplos podem ser encontrados aqui . No entanto, isso não afeta o entendimento do esquema geral das categorias de expressões.
No moderno padrão C ++, a estrutura da categoria é apresentada na forma de um esquema:

Vamos examinar em termos gerais as propriedades das categorias, bem como as expressões de linguagem incluídas em cada uma das categorias. Percebo imediatamente que as listas de expressões abaixo para cada categoria não podem ser consideradas completas; para obter informações mais precisas e detalhadas, consulte diretamente o Padrão C ++.
glvalue
Expressões na categoria glvalue têm as seguintes propriedades:
- pode ser implicitamente convertido em prvalue ;
- podem ser polimórficos, ou seja, para eles os conceitos de tipo estático e dinâmico fazem sentido;
- não pode ser do tipo nulo - isso resulta diretamente da propriedade de ter identidade, porque para expressões do tipo nulo não existe um parâmetro que os diferencie um do outro;
- pode ter um tipo incompleto , por exemplo, na forma de uma declaração direta (se permitido para uma expressão específica).
rvalue
Expressões na categoria rvalue têm as seguintes propriedades:
- você não pode obter o endereço rvalue na memória - isso resulta diretamente da falta de propriedade de identidade;
- não pode estar no lado esquerdo de uma atribuição ou declaração de atribuição composta;
- pode ser usado para inicializar um link lvalue constante ou um link rvalue , enquanto a vida útil do objeto se estende à vida útil do link;
- se usado como argumento ao chamar uma função que possui 2 versões sobrecarregadas: uma aceita uma referência lvalue constante e a outra uma referência rvalue , a versão que aceita a referência rvalue é selecionada. É essa propriedade usada para implementar a semântica de movimentação :
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&&)
Tecnicamente, A&& é um rvalue e pode ser usado para inicializar uma referência lvalue constante e uma referência rvalue . Mas, graças a essa propriedade, não há ambiguidade; é aceita uma opção de construtor que aceita uma referência rvalue .
lvalue
Propriedades:
- todas as propriedades de glvalue (veja acima);
- você pode pegar o endereço (usando o operador unário interno
&
); - valores modificáveis podem estar no lado esquerdo do operador de atribuição ou de operadores compostos;
- pode ser usado para inicializar uma referência a um valor l (constante e não constante).
As seguintes expressões pertencem à categoria lvalue :
- o nome de uma variável, função ou campo de classe de qualquer tipo. Mesmo que a variável seja uma referência rvalue , o nome dessa variável na expressão é um lvalue ;
void func() {} ......... auto* func_ptr = &func; // : auto& func_ref = func; // : int&& rrn = int(123); auto* pn = &rrn; // : auto& rn = rrn; // : lvalue-
- chamar uma função ou um operador sobrecarregado que retorne uma referência lvalue ou uma expressão de conversão para o tipo de uma referência lvalue ;
- operadores de atribuição internos, operadores de atribuição compostos (
=
, +=
, /=
, etc.), pré-incremento e pré-incremento --b
( ++a
, --b
), operador de desreferência de ponteiro interno ( *p
); - operador interno de acesso por índice (
a[n]
ou n[a]
), quando um dos operandos é uma matriz lvalue ; - chamar uma função ou uma instrução sobrecarregada que retorna uma referência de rvalue para uma função;
- literal de cadeia como
"Hello, world!"
.
Um literal de string difere de todos os outros literais em C ++ precisamente por ser um valor l (embora imutável). Por exemplo, você pode obter seu endereço:
auto* p = &”Hello, world!”; // ,
prvalue
Propriedades:
- todas as propriedades rvalue (veja acima);
- não pode ser polimórfico: os tipos de expressão estática e dinâmica sempre coincidem;
- não pode ser de um tipo incompleto (exceto o tipo de nulo , isso será discutido abaixo);
- não pode ter um tipo abstrato ou ser uma matriz de elementos de um tipo abstrato.
As seguintes expressões pertencem à categoria prvalue :
- literal (exceto string), por exemplo
42
, true
ou nullptr
; - uma chamada de função ou um operador sobrecarregado que retorna uma não referência (
str.substr(1, 2)
, str1 + str2
, it++
) ou uma expressão de conversão para um tipo de não referência (por exemplo, static_cast<double>(x)
, std::string{}
, (int)42
); - pós-incremento e pós-decremento
b--
( a++
, b--
), operações matemáticas b--
( a + b
, a % b
, a & b
, a << b
, etc.), operações lógicas incorporadas ( a && b
, a || b
!a
, etc.), operações de comparação ( a < b
, a == b
, a >= b
, etc.), a operação interna de obter o endereço ( &a
); - esse ponteiro;
- item de listagem;
- parâmetro atípico de modelo, se não for uma classe;
- expressão lambda, por exemplo
[](int x){ return x * x; }
[](int x){ return x * x; }
xvalue
Propriedades:
- todas as propriedades rvalue (veja acima);
- todas as propriedades glvalue (veja acima).
Exemplos de expressões xvalue :
- chamar uma função ou operador interno que retorna uma referência rvalue , por exemplo std :: move (x) ;
e, de fato, para o resultado de chamar std :: move (), você não pode obter um endereço na memória ou inicializar um link para ele, mas, ao mesmo tempo, essa expressão pode 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 interno de acesso por índice (
a[n]
ou n[a]
) quando um dos operandos é uma matriz rvalue .
Alguns casos especiais
Operador de vírgula
Para o operador de vírgula interno, a categoria de expressão sempre corresponde à categoria de expressão do 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
Expressões vazias
As chamadas para funções que retornam void , digitam expressões de conversão para void e lançam exceções são consideradas expressões de pré-valor , mas não podem ser usadas para inicializar referências ou como argumentos para funções.
Operador de comparação ternário
Definição da categoria de expressão a ? b : c
a ? b : c
- o caso não é trivial, tudo depende das categorias do segundo e terceiro argumentos ( b
e c
):
- se
b
ou c
forem do tipo nulo , a categoria e o tipo da expressão inteira corresponderão à categoria e ao tipo do outro argumento. Se os dois argumentos forem do tipo nulo , o resultado será um pré-valor do tipo nulo ; - se
b
são glvalue do mesmo tipo, o resultado é um glvalue do mesmo tipo; - em outros casos, o resultado é prvalue.
Para o operador ternário, são definidas várias regras segundo as quais conversões implícitas podem ser aplicadas aos argumentos bec, mas isso está um pouco além do escopo do artigo; se você estiver interessado, recomendo consultar a seção Operador condicional [expr.cond] da 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
Referências a campos e métodos de classes e estruturas
Para expressões do formato am
p->m
(aqui estamos falando sobre o operador interno ->
), as seguintes regras se aplicam:
- se
m
for um elemento de enumeração ou um método de classe não estática, a expressão inteira será considerada prvalue (embora o link não possa ser inicializado com essa expressão); - se
a
é um rvalue e m
é um campo não estático de um tipo não de referência, a expressão inteira pertence à categoria xvalue ; - caso contrário, é um valor l .
Para ponteiros para os alunos ( a.*mp
p->*mp
), as regras são semelhantes:
- se
mp
é um ponteiro para um método de classe, então toda a expressão é considerada prvalue ; - se
a
é um rvalue e mp
é um ponteiro para um campo de dados, toda a expressão se refere ao xvalue ; - caso contrário, é um valor l .
Campos de bits
Os campos de bits são uma ferramenta conveniente para programação de baixo nível; no entanto, sua implementação fica um pouco fora da estrutura geral das categorias de expressão. Por exemplo, uma chamada para um campo de bits parece ser um valor l , porque pode estar presente no lado esquerdo do operador de atribuição. Ao mesmo tempo, não funcionará para pegar o endereço do campo de bits ou inicializar um link não constante por eles. Você pode inicializar uma referência constante para um campo de bit, mas uma cópia temporária do objeto será criada:
Campos de bits [class.bit]
Se o inicializador para uma referência do tipo const T & for um valor l que se refere a um campo de bits, a referência será vinculada a um temporário inicializado para conter o valor do campo de bits; a referência não está vinculada diretamente ao campo de bits.
struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; // auto& rb = bf; //
Em vez de uma conclusão
Como mencionei na introdução, a descrição acima não afirma ser completa, mas apenas fornece uma idéia geral das categorias de expressões. Essa visão fornecerá uma compreensão um pouco melhor dos parágrafos do Standard e das mensagens de erro do compilador.