Partilhar via


Categorias de valor e referências a elas

Este tópico apresenta e descreve as várias categorias de valores (e referências a valores) que existem em C++:

  • Glvalue
  • lvalue
  • xlvalue
  • Prvalue
  • valor de rvalue

Você certamente já ouviu falar de lvalues e rvalues. Mas você pode não pensar neles nos termos que este tópico apresenta.

Cada expressão em C++ produz um valor que pertence a uma das cinco categorias listadas acima. Há aspetos da linguagem C++ — suas instalações e regras — que exigem uma compreensão adequada dessas categorias de valor, bem como referências a elas. Esses aspetos incluem pegar o endereço de um valor, copiar um valor, mover um valor e encaminhar um valor para outra função. Este tópico não aprofunda todos esses aspetos, mas fornece informações fundamentais para uma sólida compreensão deles.

As informações neste tópico são enquadradas em termos da análise de Stroustrup das categorias de valor pelas duas propriedades independentes de identidade e mobilidade [Stroustrup, 2013].

Um lvalue tem identidade

O que significa para um valor ter identidade? Se você tem (ou pode pegar) o endereço de memória de um valor e usá-lo com segurança, então o valor tem identidade. Dessa forma, você pode fazer mais do que comparar o conteúdo dos valores — você pode compará-los ou distingui-los por identidade.

Um lvalue tem identidade. Agora é apenas uma questão de interesse histórico que o "l" em "lvalue" é uma abreviação de "left" (como em, o lado esquerdo de uma tarefa). Em C++, um lvalue pode aparecer na esquerda ou à direita de uma atribuição. O "l" em "lvalue" não ajuda a compreender nem a definir o que eles são. Basta entender que o que chamamos de lvalue é um valor que tem identidade.

Exemplos de expressões que são lvalues incluem: uma variável nomeada ou constante; ou uma função que retorna uma referência. Exemplos de expressões que não são valores incluem: uma temporária; ou uma função que retorna 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.
}

Agora, embora seja uma afirmação verdadeira que lvalues tem identidade, isso também é verdade para xvalues. Explicaremos exatamente o que um xvalue é mais adiante neste tópico. Por enquanto, saiba apenas que existe uma categoria de valor chamada glvalue (para "lvalue generalizado"). O conjunto de glvalues é o superconjunto de lvalues (também conhecido como lvalues clássico) e xvalues. Assim, enquanto "um lvalue tem identidade" é verdadeiro, o conjunto completo de coisas que têm identidade é o conjunto de glvalues, como mostrado nesta ilustração.

Um lvalue tem identidade

Um rvalue é móvel; um lvalue não é

Mas há valores que não são glvalues. Em outras palavras, há valores para os quais você não pode obter um endereço de memória (ou não pode confiar que ele seja válido). Vimos alguns desses valores no exemplo de código acima.

Não ter um endereço de memória confiável soa como uma desvantagem. Mas, na verdade, a vantagem de um valor como esse é que você pode mover ele (que geralmente é barato), em vez de copiá-lo (que geralmente é caro). Mover um valor significa que ele não está mais no lugar onde costumava estar. Portanto, tentar acessá-lo no lugar onde costumava estar é algo a ser evitado. Uma discussão sobre quando e como mover um valor está fora do escopo deste tópico. Para este tópico, só precisamos saber que um valor móvel é conhecido como um rvalue (ou rvalue clássico).

O "r" em "rvalue" é uma abreviatura de "right" (como em, o lado direito de uma atribuição). Mas é possível usar rvalues e referências a rvalues fora das atribuições. O "r" em "rvalue" não é, logo, o ponto de foco. Basta entender que o que chamamos de rvalue é um valor que é móvel.

Um lvalue, inversamente, não é móvel, como mostra esta ilustração. Se um lvalue se movesse, isso contradiria a própria definição de lvalue. E seria um problema inesperado para o código que muito razoavelmente esperava ser capaz de continuar a acessar o lvalue.

Um rvalue é móvel; um lvalue não é

Então você não pode mover um lvalue. Mas há uma espécie de glvalue (o conjunto de coisas com identidade) que você pode mover – se souber o que está fazendo (incluindo ter cuidado para não acessá-lo após a mudança) – e esse é o xvalue. Revisitaremos essa ideia mais uma vez mais adiante neste tópico, quando analisarmos o quadro completo das categorias de valor.

Referências Rvalue e regras de ligação de referência

Esta seção apresenta a sintaxe para uma referência a um rvalue. Teremos que esperar que outro tópico entre em um tratamento substancial de movimentação e encaminhamento, mas basta dizer que as referências de valor são uma peça necessária para a solução desses problemas. Antes de olharmos para as referências de rvalue, no entanto, primeiro precisamos ser mais claros sobre T&—o que anteriormente chamávamos apenas de "uma referência". É realmente "uma referência lvalue (não-constante)", que se refere a um valor ao qual o utilizador da referência pode escrever.

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.

Uma referência lvalue pode vincular-se a um lvalue, mas não a um rvalue.

Em seguida, há referências lvalue const (T const&), que se referem a objetos para os quais o usuário da referência não consegue escrever (por exemplo, uma 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.

Uma referência lvalue const pode ligar-se a um lvalue ou a um rvalue.

A sintaxe de uma referência a um rvalue do tipo T é escrita como T&&. Uma referência a um rvalue refere-se a um valor móvel — um valor cujo conteúdo não precisamos preservar depois de o termos utilizado (por exemplo, um temporário). Como o objetivo é modificar o valor associado a uma referência de rvalue, os qualificadores const e volatile (também conhecidos como qualificadores cv) não se aplicam às referências de 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.

Uma referência rvalue liga-se a um rvalue. Na verdade, em termos de resolução de sobrecarga, um rvalue prefere que seja vinculado a uma referência rvalue em vez de a uma referência constante lvalue. Mas uma referência de rvalue não pode se vincular a um lvalue porque, como dissemos, uma referência de rvalue refere-se a um valor cujo conteúdo é assumido que não precisamos preservar (digamos, o parâmetro para um construtor move).

Você também pode passar um rvalue onde um argumento by-value é esperado, via copy construction (ou via move construction se o rvalue for um xvalue).

Um glvalue tem identidade; um prvalue não

Nesta fase, sabemos o que tem identidade. E sabemos o que é móvel e o que não é. Mas ainda não nomeamos o conjunto de valores que não têm identidade . Esse conjunto é conhecido como prvalue, ou 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.
}

Um glvalue tem identidade; um prvalue não

O quadro completo das categorias de valor

Resta apenas combinar as informações e ilustrações acima em um único quadro geral.

O quadro completo das categorias de valor

GLVALUE (I)

Um glvalue (lvalue generalizado) tem identidade. Usaremos "i" como uma forma abreviada de "tem identidade".

lvalue (i&!m)

Um lvalue (uma espécie de glvalue) tem identidade, mas não é móvel. Normalmente, estes são valores de leitura e escrita que são passados por referência ou por referência constante, ou por valor se a duplicação for barata. Um lvalue não pode ser vinculado a uma referência de rvalue.

xvalue (i&m)

Um xvalue (uma espécie de glvalue, mas também um tipo de rvalue) tem identidade, e também é móvel. Este pode ser um valor antigo que você decidiu mover porque copiar é caro, e você terá cuidado para não acessá-lo depois. Veja como você pode transformar um lvalue em um xvalue.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

No exemplo de código acima, ainda não movemos nada. Criámos apenas um xvalue ao converter um lvalue para uma referência anónima de rvalue. Ainda pode ser identificado pelo seu nome lvalue; mas, como um xvalue, agora é capaz de ser movido. As razões para a mudança, e como essa mudança realmente se parece, terão que esperar por outro assunto. Mas pode-se considerar o "x" em "xvalue" como significando "apenas para peritos", se isso for útil. Ao converter um lvalue em um xvalue (um tipo de rvalue, lembre-se), o valor torna-se então capaz de ser vinculado a uma referência de rvalue.

Aqui estão dois outros exemplos de xvalues: chamar uma função que retorna uma referência rvalue anónima e aceder a um membro de um xvalue.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

Prvalue (!i&m)

Um prvalue (rvalue puro; uma espécie de rvalue) não tem identidade, mas é móvel. Normalmente, são temporários, ou o resultado de chamar uma função que retorna por valor, ou o resultado de avaliar qualquer outra expressão que não seja um glvalue.

rvalue (m)

Um rvalue é móvel. Usaremos "m" como uma abreviatura para "é móvel".

Uma referência rvalue sempre se refere a um rvalue (um valor cujo conteúdo se considera que não precisamos preservar).

Mas, será que uma referência de rvalue é em si uma rvalue? Uma referência rvalue sem nome (como as mostradas nos exemplos de código xvalue acima) é um xvalue, portanto, sim, é um rvalue. Ele prefere ser vinculado a um parâmetro de função de referência rvalue, como o de um construtor move. Inversamente (e talvez contra-intuitivamente), se uma referência rvalue tem um nome, então a expressão que consiste nesse nome é um lvalue. Portanto, ele não pode ser vinculado a um parâmetro de referência de rvalue. Mas é fácil fazê-lo — basta lançá-lo para uma referência de rvalue sem nome (um xvalue) novamente.

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

O tipo de valor que não tem identidade e não é móvel é a única combinação que ainda não discutimos. Mas podemos ignorá-la, porque essa categoria não é uma ideia útil na linguagem C++.

Regras de colapso de referências

Várias referências semelhantes numa expressão (uma referência lvalue para uma referência lvalue, ou uma referência rvalue para uma referência rvalue) anulam-se mutuamente.

  • A& & colapsa em A&.
  • A&& && colapsa em A&&.

Várias referências diferentes em uma expressão se convertem para uma referência lvalue.

  • A& && colapsa em A&.
  • A&& & colapsa em A&.

Encaminhamento de referências

Esta seção final contrasta as referências rvalue, que já discutimos, com o conceito diferente de uma referência de redirecionamento de . Antes do termo "referência de encaminhamento" ser cunhado, algumas pessoas usavam o termo "referência universal".

void foo(A&& a) { ... }
  • A&& é uma referência rvalue, como vimos. Const e volátil não se aplicam a referências de rvalue.
  • foo aceita apenas rvalores do tipo A.
  • A razão pela qual as referências de rvalue (como A&&) existem é para que se possa criar uma sobrecarga otimizada para o caso de passagem de um temporário (ou outro rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& é uma referência de encaminhamento . Dependendo do que você passar para bar, o tipo _Ty pode ser const/non-const independentemente de volátil/não volátil.
  • bar aceita qualquer lvalue ou rvalue do tipo _Ty.
  • Passar um lvalue faz com que a referência de encaminhamento passe a ser _Ty& &&, que se reduz à referência lvalue _Ty&.
  • Quando se passa um rvalue, a referência de encaminhamento torna-se a referência de rvalue _Ty&&.
  • A razão pela qual as referências de encaminhamento (como _Ty&&) existem não é para otimização, mas para pegar o que você passa para eles e encaminhá-lo de forma transparente e eficiente. É provável que você encontre uma referência de encaminhamento somente se escrever (ou estudar de perto) o código da biblioteca — por exemplo, uma função de fábrica que encaminha argumentos de construtor.

Fontes

  • [Stroustrup, 2013] B. Stroustrup: A Linguagem de Programação C++, Quarta Edição. Addison-Wesley. 2013.