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 no C++:

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Você sem dúvida já ouviu falar de lvalues e rvalues. Mas talvez você não pense 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á aspectos 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 aspectos incluem usar 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 entra em todos esses aspectos detalhadamente, mas fornece informações fundamentais para uma compreensão sólida deles.

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

Um lvalue tem identidade

O que significa para um valor ter identidade? Se você tiver (ou puder usar) o endereço de memória de um valor e usá-lo com segurança, o valor terá 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, é somente uma questão de interesse histórico o fato de que o "l" em "lvalue" é uma abreviação de "left" (como no lado esquerdo de uma atribuição). No C++, um lvalue pode aparecer à esquerda ou à direita de uma atribuição. Então, o "l" em "lvalue" não ajuda a compreender nem definir o que ele é. Você só precisa 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. Alguns exemplos de expressões que não são lvalues: um temporário ou uma função retornada 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 os lvalues têm identidade, isso também vale para os xvalues. Vamos entrar exatamente no que é um xvalue mais tarde neste tópico. Por enquanto, basta estar ciente de que há uma categoria de valor chamada glvalue (para "lvalue generalizado"). O conjunto de glvalues é o superconjunto de lvalues (também conhecidos como lvalues clássicos) e xvalues. Portanto, enquanto "um lvalue tem identidade" é verdadeiro, o conjunto completo de coisas que têm identidade é o conjunto de glvalues, conforme 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 nele para ser 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 movê-lo (que geralmente é barato), em vez de copiá-lo (o 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 ser é 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 rvalue (ou rvalue clássico).

O "r" em "rvalue" é uma abreviação de "right" (como no lado direito de uma atribuição). No entanto, é possível usar rvalues e referências a eles fora de atribuições. O "r" em "rvalue", então, não é no que devemos nos concentrar. Você só precisa entender que o que chamamos de rvalue é um valor móvel.

Um lvalue, por outro lado, não é móvel, conforme mostrado nesta ilustração. Se um lvalue se movesse, então isso contradizia a própria definição de lvalue. E seria um problema inesperado para o código que esperava razoavelmente poder continuar a acessar o lvalue.

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

Então você não pode mover um lvalue. Mas existe um tipo de glvalue (o conjunto de coisas com identidade) que você pode mover — se souber o que está fazendo (inclusive tendo o cuidado de não acessá-lo após movê-lo) — e esse é o xvalue. Revisitaremos essa ideia mais uma vez mais tarde neste tópico quando examinarmos a imagem completa das categorias de valor.

Referências rvalue e regras de associação de referências

Esta seção apresenta a sintaxe de uma referência a um rvalue. Precisaremos de outro tópico para tratar de movimentação e encaminhamento de maneira significativa, mas basta dizer que as referências de rvalue são uma parte necessária da solução desses problemas. Antes de examinarmos as referências de rvalue, no entanto, primeiro precisamos ser mais claros sobre T&a coisa que anteriormente chamamos apenas de "referência". É realmente "uma referência lvalue (não const)", que se refere a um valor em que o usuário de referência pode gravar.

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 ser associada a um lvalue, mas não a um rvalue.

Em seguida, há referências lvalue const (T const&), que se referem a objetos aos quais o usuário da referência não pode gravar (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 const lvalue pode ser associada 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 rvalue refere-se a um valor móvel — um valor cujo conteúdo não precisamos preservar depois de o usarmos (por exemplo, um temporário). Como o objetivo é mover-se (e, portanto, modificar) do valor associado para uma referência rvalue, os qualificadores const e volatile (também conhecidos como cv-qualifiers) não se aplicam às referências 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 pode ser associada a um rvalue. Na verdade, em termos de resolução de sobrecarga, um rvalue prefere estar associado a uma referência rvalue do que a uma referência const lvalue. Mas uma referência a rvalue não pode ser vinculada a um lvalue porque, como dissemos, uma referência a rvalue se refere a um valor cujo conteúdo presumimos não precisar preservar (por exemplo, o parâmetro de um construtor de movimento).

Também é possível passar um rvalue em que se espera um argumento por valor, por meio de uma construção de cópia (ou uma construção de movimentação, quando o rvalue é 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 valor-r 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

A imagem completa das categorias de valor

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

A imagem completa das categorias de valor

glvalue (i)

Um glvalue (lvalue generalizado) tem identidade. Usaremos "i" como uma abreviação para "tem identidade".

lvalue (i&!m)

Um lvalue (uma espécie de glvalue) tem identidade, mas não é móvel. Normalmente, esses são os valores de leitura/gravação que você passa por referência ou referência const ou por valor se a cópia for barata. Um lvalue não pode ser vinculado a uma referência rvalue.

xvalue (i&m)

Um xvalue (uma espécie de glvalue, mas também uma espécie de rvalue) tem identidade e também é móvel. Pode ser um lvalue 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. Somente criamos um xvalue ao transmitir um lvalue para uma referência a rvalue sem nome. Ele ainda pode ser identificado pelo nome lvalue; mas, como um xvalue, agora é capaz de ser movido. Os motivos da movimentação e a aparência da movimentação serão abordados em outro tópico. Mas você pode pensar no "x" em "xvalue" como significando "apenas para especialistas", se isso ajudar. Ao converter um lvalue em um xvalue (uma espécie de rvalue, lembre-se), o valor torna-se capaz de ser associado a uma referência rvalue.

Veja dois outros exemplos de xvalue – chamar uma função que retorna uma referência rvalue sem nome e acessar 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, eles são temporários ou resultam da chamada de uma função retornada por valor ou da avaliação de outra expressão que não seja glvalue.

rvalue (m)

Um rvalue é móvel. Usaremos "m" como uma abreviação para "é móvel".

Uma referência rvalue sempre se refere a um rvalue (um valor cujo conteúdo não precisa ser preservado).

No entanto, uma referência rvalue é um 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 associado a um parâmetro de função de referência rvalue, como o de um construtor de movimentação. Por outro lado (e talvez contra-intuitivamente), se uma referência rvalue tiver um nome, a expressão que consiste nesse nome é um lvalue. Portanto, ele não pode ser associado a um parâmetro de referência rvalue. Entretanto, é fácil reverter essa situação, basta transmiti-lo para uma referência 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 desconsiderar isso, porque essa categoria não é uma ideia útil na linguagem C++.

Regras de recolhimento de referências

Múltiplas referências do mesmo tipo em uma expressão (uma referência lvalue para uma referência lvalue ou uma referência rvalue para uma referência rvalue) se anulam.

  • A& & se recolhe em A&.
  • A&& && se recolhe em A&&.

Muitas referências diferentes em uma expressão se recolhem em uma referência lvalue.

  • A& && se recolhe em A&.
  • A&& & se recolhe em A&.

Referências de encaminhamento

Esta seção final contrasta as referências rvalue, que já discutimos, com o conceito distinto de uma forwarding reference. Antes de o termo "referência de encaminhamento" ser cunhado, alguns usavam o termo "referência universal".

void foo(A&& a) { ... }
  • A&& é uma referência a rvalue, como vimos. Const e volatile não se aplicam a referências a rvalue.
  • foo aceita apenas rvalues do tipo A.
  • As referências rvalue (como A&&) existem para que você possa criar uma sobrecarga otimizada caso um temporário (ou outro rvalue) seja passado.
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& é uma referência de encaminhamento. De acordo com o que você passar para bar, o tipo _Ty poderá ser const/non-const independentemente de volatile/non-volatile.
  • bar aceita qualquer lvalue ou rvalue do tipo _Ty.
  • Passar um lvalue faz com que a referência de encaminhamento se transforme em _Ty& &&, que recolhe para a referência lvalue _Ty&.
  • A passagem de um rvalue faz com que a referência de encaminhamento se transforme na referência a rvalue _Ty&&.
  • As referências de encaminhamento (como _Ty&&) não existem para otimização, mas para encaminhar o que você passa para elas de maneira transparente e eficiente. Você encontrará uma referência de encaminhamento somente se gravar (ou estudar atentamente) o código da biblioteca, por exemplo, uma função de fábrica que encaminha por meio de argumentos de construtor.

Sources

  • [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, Fourth Edition. Addison-Wesley. 2013.