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. É hoje apenas de interesse histórico o fato de que o "l" em "lvalue" é uma abreviação de "left" (isto é, o lado esquerdo de uma atribuição). No C++, um lvalue pode aparecer à esquerda ou à direita de uma atribuição. O "l" em "lvalue", então, não ajuda você a compreender nem definir o que eles são. 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. Exemplos de expressões que não são lvalues incluem: um temporário; ou uma função que retorna um 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 em, o lado direito de uma operação de atribuição). Mas você pode usar rvalues e referências a rvalues 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 de Rvalue e regras de associação de referência

Esta seção apresenta a sintaxe de uma referência a um rvalue. Teremos que esperar outro tópico para tratar de forma substancial de movimentação e encaminhamento, mas, por ora, basta dizer que as referências a rvalues 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 ao qual o usuário da 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 se vincular 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 se vincular 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 — e, assim, modificar — o valor vinculado a uma referência a rvalue, os qualificadores const e volatile (também conhecidos como qualificadores cv) não se aplicam a referências a 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 se vincula a um rvalue. Na verdade, em termos de resolução de sobrecarga, um rvalue prefere ser associado a uma referência rvalue do que a uma referência lvalue const. 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).

Você também pode passar um rvalue quando um argumento por valor é esperado, por meio de cópia (ou por meio de movimentação, 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 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 valores de leitura e escrita que você passa por referência, por referência constante 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. Isso pode ser um valor antigo que você decidiu mover porque a cópia é cara, e você terá cuidado para não acessá-la 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. Nós apenas criamos um xvalue lançando um lvalue para uma referência rvalue sem nome. Ele ainda pode ser identificado pelo nome lvalue; mas, como um xvalue, agora é capaz de ser movido. Os motivos para movê-lo e como essa mudança realmente acontece terão que ficar para 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.

Aqui estão dois outros exemplos de xvalues: 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 o resultado de chamar uma função que retorna um 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 abreviação para "é móvel".

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

Mas uma referência rvalue ela própria é 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. Mas é fácil fazer com que ele faça isso — basta convertê-lo novamente em uma referência rvalue sem nome (um 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

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 colapso 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& & é recolhido em A&.
  • A&& && é recolhido em A&&.

Várias referências diferentes em uma expressão são recolhidas em uma referência lvalue.

  • A& && é recolhido 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.
  • O motivo pelo qual as referências rvalue (como A&&) existem é para que você possa criar uma sobrecarga otimizada para o caso de um valor temporário (ou outro rvalue) ser passado.
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 ou não const, independentemente de ser volatile ou não volatile.
  • bar aceita qualquer lvalue ou rvalue do tipo _Ty.
  • Passar um lvalue como argumento faz a referência de encaminhamento se tornar _Ty& &&, o que colapsa para a referência lvalue _Ty&.
  • Passar um rvalue faz com que a referência de encaminhamento se torne a referência rvalue _Ty&&.
  • O motivo pelo qual as referências de encaminhamento (como _Ty&&) existem não é para otimização, mas para receber o que você passa a elas e encaminhá-lo de forma transparente e eficiente. É provável que você encontre uma referência de encaminhamento apenas se escrever (ou estudar a fundo) código de biblioteca — por exemplo, uma função de fábrica que repassa os argumentos para o construtor.

Sources

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