Partilhar via


Categorias de valores e referências a eles

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

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Sem dúvida, você já ouviu falar de lvalues e rvalues. Mas talvez você não os entenda nos termos que este tópico apresenta.

Cada expressão em C++ gera um valor que pertence a uma das cinco categorias listadas acima. Há aspectos, como facilidades e regras, da linguagem C++ que exigem um bom entendimento dessas categorias de valores e das referências a elas. Esses aspectos incluem pegar o endereço de um valor, além de copiar, mover e encaminhar um valor a outra função. Neste tópico, não abordaremos todos esses aspectos em profundidade, mas forneceremos informações básicas para promover um entendimento sólido.

As informações deste tópico estão formuladas nos termos da análise de Stroustrup das categorias de valor por meio de duas propriedades independentes de identidade e mobilidade [Stroustrup, 2013].

Um lvalue tem uma identidade

Para um valor, o que significa ter uma identidade? Se você tem (ou consegue obter) o endereço de memória de um valor e usa-o com segurança, o valor tem uma identidade. Assim, você pode fazer mais do que apenas comparar o conteúdo dos valores, você pode compará-los ou diferenciá-los por identidade.

Um lvalue tem uma 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). Em 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 com uma identidade.

Alguns exemplos de expressões lvalues: uma variável ou constante com nome 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, a afirmação verdadeira de que os lvalues têm identidade também é válida para os xvalues. Vamos mostrar exatamente o que é um xvalue mais adiante neste tópico. Por enquanto, basta saber que há uma categoria de valor chamada glvalue (que significa "lvalue generalizado"). O conjunto de glvalues é o superconjunto de lvalues (também conhecidos como lvalues clássicos) e xvalues. Portanto, embora a afirmação "um lvalue tem identidade" seja verdadeira, o conjunto completo de itens com identidade é o conjunto de glvalues, conforme mostrado na ilustração.

Um lvalue tem uma identidade

Um rvalue pode ser movido; um lvalue não

Porém, há valores que não são glvalues. Em outras palavras, há valores cujo endereço de memória não pode ser obtido (ou que não pode ser assumido como válido). Vimos alguns valores desse tipo no código de exemplo acima.

A ausência de um endereço de memória confiável parece uma desvantagem. Mas, na verdade, a vantagem de um valor como esse é que você pode movê-lo (o que geralmente sai barato), em vez de copiá-lo (o que geralmente sai caro). Quando um valor é movido, significa que ele não está mais no local em que costumava estar. Portanto, tentar acessá-lo no local antigo é algo que deve ser evitado. Não está no escopo deste tópico discutir quando e como mover um valor. Neste tópico, precisamos apenas 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. Não é necessário se concentrar no "r" de "rvalue". 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 precisasse ser movido, isso seria contraditório em relação à própria definição de lvalue. E seria um problema inesperado para o código que esperaria continuar a acessar o lvalue.

Um rvalue pode ser movido; um lvalue não

Portanto, não é possível mover um lvalue. Contudo, um tipo de glvalue (o conjunto de itens com identidade) que poderá ser movido se você souber o que está fazendo (por exemplo, ter cuidado para não acessá-lo após a movimentação): o xvalue. Vamos analisar essa ideia mais uma vez depois neste tópico ao vermos o panorama geral das categorias de valor.

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

Nesta seção, apresentamos 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. No entanto, antes de examinarmos as referências rvalue, primeiro é preciso esclarecer T&, que estamos chamando formalmente de "referência". É realmente "uma referência lvalue (não const)", que se refere a um valor no qual o usuário 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 ser associada a um lvalue, mas não a um rvalue.

Desse modo, há referências const lvalue (T const&), que se referem a objetos em que 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 é gravada como T&&. Uma referência a rvalue se refere a um valor móvel, ou seja, um valor cujo conteúdo não precisa ser preservado após o uso (por exemplo, um valor 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. No entanto, uma referência rvalue não pode ser associada a um lvalue porque, como dito, assume-se que uma referência rvalue se refere a um valor cujo conteúdo não precisa ser preservado (por exemplo, o parâmetro de um construtor de movimentação).

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

Nesse estágio, sabemos o que tem identidade. E sabemos o que é móvel ou não. No entanto, ainda não nomeamos o conjunto de valores que não tem 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

Panorama completo das categorias de valores

Somente para combinar as informações e as ilustrações acima em uma única imagem.

Panorama completo das categorias de valores

glvalue (i)

Um glvalue (lvalue generalizado) tem identidade. Vamos usar "i" como uma abreviação de "tem identidade".

lvalue (i&!m)

Um lvalue (um tipo 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 associado a uma referência rvalue.

xvalue (i&m)

Um xvalue (um tipo de glvalue, mas também um tipo de rvalue) tem identidade e é 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 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, nada foi movido. Somente criamos um xvalue ao transmitir um lvalue para uma referência a rvalue sem nome. Ainda é possível identificá-lo pelo nome do lvalue; no entanto, como xvalue, agora ele pode 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" de "xvalue" como "expert-only" se isso for útil. Ao converter um lvalue em xvalue (um tipo de rvalue), o valor passa a poder ser associado a uma referência a 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; um tipo 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. Vamos usar "m" como uma abreviação de "é 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, então, 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 de modo contraintuitivo), se uma referência rvalue tiver nome, a expressão formada por esse nome será um lvalue. Então, 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 abordamos. No entanto, podemos ignorá-lo, porque essa categoria não é uma ideia útil na linguagem C++.

Regras de recolhimento de referências

Muitas referências semelhantes em uma expressão (uma referência lvalue a uma referência lvalue ou uma referência rvalue a uma referência rvalue) cancelam umas as outras.

  • 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

Nesta seção final, contrastamos as referências rvalue, que já foram discutidas, com o diferente conceito de uma referência de encaminhamento. Antes do termo "referência de encaminhamento" existir, algumas pessoas usavam o termo "referência universal".

void foo(A&& a) { ... }
  • A&& é uma referência rvalue, como já sabemos. Const e volatile não se aplicam a referências rvalue.
  • foo aceita somente 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.

Origens

  • [Stroustrup, 2013] B. Stroustrup: a linguagem de programação C++, quarta edição. Addison-Wesley. escrito por Tomas Mikolov e colaboradores em 2013.