Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
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
- rvalue
Certamente já ouviu falar de lvalues e rvalues. Mas pode não pensar neles nos termos que este tema apresenta.
Cada expressão em C++ produz um valor que pertence a uma das cinco categorias listadas acima. Existem aspetos da linguagem C++ — as suas funcionalidades e regras — que exigem uma compreensão adequada destas categorias de valor, bem como referências a elas. Estes aspetos incluem tomar o endereço de um valor, copiar um valor, mover um valor e encaminhar um valor para outra função. Este tema não aprofunda todos esses aspetos, mas fornece informação fundamental para uma compreensão sólida dos mesmos.
A informação neste tema é enquadrada em termos da análise de Stroustrup das categorias de valor pelas duas propriedades independentes da identidade e da mobilidade [Stroustrup, 2013].
Um valor l tem identidade
O que significa para um valor ter identidade? Se tiveres (ou podes usar) o endereço de memória de um valor em segurança, então o valor tem identidade. Assim, pode fazer mais do que comparar o conteúdo dos valores — pode compará-los ou distingui-los pela identidade.
Um valor tem identidade. Agora é apenas de interesse histórico que o "l" em "lvalue" seja uma abreviatura de "left" (como em, o lado esquerdo de uma tarefa). Em C++, um valor l pode aparecer à esquerda ou à direita de uma tarefa. O "l" em "lvalue", portanto, não ajuda realmente a compreender nem a definir o que são. Só precisa de perceber que aquilo a que chamamos de lvalor é um valor que tem identidade.
Exemplos de expressões que são valores l incluem: uma variável ou constante nomeada; ou uma função que devolve uma referência. Exemplos de expressões que não são valores l incluem: um temporário; 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 têm identidade, isso também é verdade para xvalues. Vamos abordar exatamente o que é um valor x mais adiante neste tópico. Para já, basta ter presente que existe uma categoria de valor chamada glvalue (de «lvalue generalizado»). O conjunto dos valores gl é o superconjunto tanto dos valores l (também conhecidos como valores l clássicos) como dos valores x. Assim, embora "um valor l tenha identidade" seja verdadeiro, o conjunto completo de coisas que têm identidade é o conjunto dos glvalues, como mostrado nesta ilustração.
Um valor r é móvel; um lvalue não é
Mas há valores que não são glvalues. Ou seja, há valores para os quais não se pode obter um endereço de memória (ou não se pode confiar que seja válido). Vimos alguns desses valores no exemplo de código acima.
Não ter um endereço de memória fiável parece uma desvantagem. Mas, na verdade, a vantagem de um valor assim é que podes movê-lo (o que geralmente é barato), em vez de o copiar (o que é geralmente caro). Mover um valor significa que já não está no sítio onde estava. Por isso, tentar aceder ao local onde estava é algo a evitar. Uma discussão sobre quando e como mover um valor está fora do âmbito deste tema. Para este tópico, só precisamos de saber que um valor móvel é conhecido como valor r (ou valor r clássico).
O "r" em "rvalue" é uma abreviatura de "right" (isto é, o lado direito de uma atribuição). Mas podes usar valores r, e referências a valores r, fora das atribuições. O "r" em "rvalue", então, não é o ponto em que deves focar-te. Só precisa de perceber que aquilo a que chamamos rvalue é um valor que é móvel.
Um valor l, por outro lado, não é móvel, como mostrado nesta ilustração. Se um valor l se movesse, isso contradiria a própria definição de valor l. E seria um problema inesperado para um código que esperava razoavelmente poder continuar a aceder ao valor l.
Portanto, não podes mover um lvalue. Mas existe uma espécie de glvalue (o conjunto de coisas com identidade) que podes mover — se souberes o que estás a fazer (incluindo ter cuidado para não aceder a isso depois da mudança) — e esse é o valor x. Voltaremos a essa ideia mais uma vez neste tópico, quando olharmos para o quadro completo das categorias de valor.
Referências a rvalues e regras de associação de referências
Esta secção introduz a sintaxe para uma referência a um valor r. Teremos de esperar por outro tópico para tratar em profundidade da movimentação e do reencaminhamento, mas basta dizer que as referências rvalue são uma parte essencial da solução desses problemas. Antes de olharmos para as referências rvalue, primeiro precisamos de ser mais claros sobre T&— aquilo a que antes chamávamos apenas "uma referência". Na verdade, é "uma referência lvalue (não const)", que se refere a um valor sobre o 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 ligar-se a um valor l, mas não a um valor r.
Depois existem as referências const lvalue (T const&), que se referem a objetos para os quais o utilizador da referência não pode 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 const lvalue pode ligar-se a um valor l ou a um valor r.
A sintaxe para uma referência a um valor r de tipo T é escrita como T&&. Uma referência rvalue refere-se a um valor móvel — um valor cujo conteúdo não precisamos de preservar depois de o termos usado (por exemplo, um temporário). Como o objetivo principal é mover, e assim modificar, o valor associado a uma referência rvalue, os qualificadores const e volatile (também conhecidos como qualificadores cv) não se aplicam a 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 liga-se a um valor r. De facto, em termos de resolução de sobrecarga, um valor r prefere estar vinculado a uma referência de valor r do que a uma referência const de valor l. Mas uma referência a rvalue não pode ser associada a um lvalue porque, como dissemos, uma referência a rvalue refere-se a um valor cujo conteúdo se assume não ser necessário preservar (por exemplo, o parâmetro de um construtor de movimento).
Também podes passar um valor r onde se espera um argumento de valor by, através da construção de cópia (ou através da construção de movimento se o valor r for um valor x).
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 nomeámos o conjunto de valores que não têm identidade. Esse conjunto é conhecido como prvalue, ou valor puro r.
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.
}
O quadro completo das categorias de valor
Resta apenas combinar a informação e as ilustrações acima numa única e grande imagem.
glvalue (i)
Um glvalue (valor l generalizado) tem identidade. Vamos usar "i" como abreviação para "tem identidade".
lvalue (i&!m)
Um valor l (um tipo de glvalue) tem identidade, mas não é móvel. Estes são normalmente valores de leitura e escrita que passa por referência ou por referência constante, ou por valor se a cópia for pouco dispendiosa. Um valor l não pode ser vinculado a uma referência rvalue.
xvalue (I&M)
Um valor x (um tipo de glvalue, mas também um tipo de valor r) tem identidade, e também é móvel. Isto pode ser um valor antigo que decidiu mudar porque copiar é caro, e terá cuidado para não aceder a ela depois. Aqui está como podes transformar um valor l num valor x.
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 valor x ao lançar um valor l para uma referência rvalue não identificada. Ainda pode ser identificado pelo seu nome lvalue; mas, como valor x, agora pode ser movido. As razões para a sua mudança e aquilo em que essa mudança realmente consiste terão de esperar por outro tópico. Mas podes pensar no "x" de "xvalue" como significando "só para especialistas", se isso ajudar. Ao converter um valor l num valor x (um tipo de valor r, lembre-se), o valor torna-se então capaz de ser ligado a uma referência valor r.
Aqui estão mais dois exemplos de xvalues — chamar uma função que retorna uma referência rvalue sem nome 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 valor pr (valor puro r; uma espécie de valor r) não tem identidade, mas é móvel. Estes são tipicamente 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 pode ser movido. Vamos usar "m" como abreviação para "é móvel".
Uma referência rvalue refere-se sempre a um rvalue (um valor cujo conteúdo se assume que não precisamos de preservar).
Mas será que 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 valor x, por isso, sim, é um valor r. Prefere estar ligado a um parâmetro de função de referência rvalue, como o de um construtor move. Por outro lado (e talvez de forma contraintuitiva), se uma referência de valor r tem um nome, então a expressão que consiste nesse nome é um valor l. Portanto, não pode ser ligado a um parâmetro de referência rvalue. Mas é fácil fazê-lo acontecer — basta castar 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 é movível é a 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
Múltiplas referências do mesmo tipo numa expressão (uma referência lvalue a uma referência lvalue, ou uma referência rvalue a uma referência rvalue) anulam-se entre si.
-
A& &colapsa emA&. -
A&& &&colapsa emA&&.
Múltiplas referências diferentes numa expressão colapsam para uma referência de valor l.
-
A& &&é reduzido aA&. -
A&& &transforma-se emA&.
Referências de reencaminhamento
Esta secção final contrasta as referências a rvalues, que já discutimos, com o conceito distinto de referência de encaminhamento. Antes de o 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 às referências rvalue. -
fooaceita apenas rvalores do tipo A. - A razão pela qual as referências rvalue (como
A&&) existem é para que possas criar uma sobrecarga otimizada para o caso de um rvalue temporário (ou outro) ser passado.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&é uma referência de reencaminhamento. Dependendo do que for passado abar, o tipo _Ty pode ser const/não-const independentemente de volátil/não volátil. -
baraceita qualquer valor l ou valor r do tipo _Ty. - Passar um valor l faz com que a referência de encaminhamento se torne
_Ty& &&, que colapsa para a referência_Ty&de valor l . - Passar um rvalue faz com que a referência de reencaminhamento se torne a referência rvalue
_Ty&&. - A razão pela qual as referências de reencaminhamento (como
_Ty&&) existem não para otimização, mas para receber o que lhes passas e reencaminhá-lo de forma transparente e eficiente. É provável que só se depare com uma referência de reencaminhamento se escrever (ou estudar atentamente) código de biblioteca — por exemplo, uma função de fábrica que reencaminha os argumentos para o construtor.
Sources
- [Stroustrup, 2013] B. Stroustrup: A Linguagem de Programação C++, Quarta Edição. Addison-Wesley. 2013.
Windows developer