c++ - guidelines - O que são rvalues, lvalues, xvalues, glvalues e prvalues?




core c++ guidelines (7)

Em C ++ 03, uma expressão é um rvalue ou um lvalue .

Em C ++ 11, uma expressão pode ser:

  1. valor
  2. lvalue
  3. xvalue
  4. glvalue
  5. valor

Duas categorias se tornaram cinco categorias.

  • Quais são essas novas categorias de expressões?
  • Como essas novas categorias se relacionam com as categorias de valor e valor existentes?
  • As categorias rvalue e lvalue em C ++ 0x são as mesmas em C ++ 03?
  • Por que essas novas categorias são necessárias? Os deuses do WG21 apenas tentando nos confundir com meros mortais?

Como essas novas categorias se relacionam com as categorias de valor e valor existentes?

Um lvalor C ++ 03 ainda é um lvalor C ++ 11, enquanto um rvalor C ++ 03 é chamado de prvalor em C ++ 11.


Por que essas novas categorias são necessárias? Os deuses do WG21 estão apenas tentando nos confundir com meros mortais?

Eu não sinto que as outras respostas (boas, embora muitas delas sejam) realmente capturam a resposta para essa questão em particular. Sim, essas categorias e outras existem para permitir a movimentação semântica, mas a complexidade existe por um motivo. Esta é a única regra inviolável de mover coisas em C ++ 11:

Você só se moverá quando for inquestionavelmente seguro fazê-lo.

É por isso que essas categorias existem: para poder falar sobre valores onde é seguro sair deles e falar sobre valores onde não está.

Na versão mais antiga das referências de valor r, o movimento aconteceu com facilidade. Muito facilmente. É fácil perceber que havia muito potencial para mover implicitamente as coisas quando o usuário não queria.

Aqui estão as circunstâncias em que é seguro mover algo:

  1. Quando é um temporário ou subobjeto do mesmo. (prvalue)
  2. Quando o usuário explicitamente disse para movê-lo .

Se você fizer isto:

SomeType &&Func() { ... }

SomeType &&val = Func();
SomeType otherVal{val};

O que isso faz? Nas versões mais antigas da especificação, antes de os 5 valores aparecerem, isso provocaria um movimento. Claro que sim. Você passou uma referência rvalue para o construtor e, portanto, liga-se ao construtor que recebe uma referência rvalue. Isso é óbvio.

Há apenas um problema com isso; você não pediu para movê-lo. Ah, você poderia dizer que o && deveria ter sido uma pista, mas isso não muda o fato de que ele quebrou a regra. val não é temporário porque os temporários não têm nomes. Você pode ter estendido a vida útil do temporário, mas isso significa que não é temporário ; é como qualquer outra variável de pilha.

Se não é temporário e você não pediu para movê-lo, então a mudança está errada.

A solução óbvia é fazer val um lvalue. Isso significa que você não pode sair dele. OK, tudo bem; é nomeado, então é um lvalue.

Depois de fazer isso, você não pode mais dizer que SomeType&& significa a mesma coisa em qualquer lugar. Agora você fez uma distinção entre referências de rvalue nomeadas e referências de rvalue sem nome. Bem, as referências de valor nominal são lvalores; essa foi a nossa solução acima. Então, o que chamamos de referências rvalue sem nome (o valor de retorno da Func acima)?

Não é um lvalue, porque você não pode se mover de um lvalue. E precisamos ser capazes de nos mover retornando um && ; De que outra forma você poderia dizer explicitamente para mudar alguma coisa? É isso que o std::move retorna, afinal. Não é um rvalue (estilo antigo), porque pode estar no lado esquerdo de uma equação (as coisas são realmente um pouco mais complicadas, veja esta questão e os comentários abaixo). Não é nem um valor nem um valor; é um novo tipo de coisa.

O que temos é um valor que você pode tratar como um lvalue, exceto que é implicitamente móvel. Nós chamamos isso de xvalue.

Note que xvalues ​​são o que nos faz ganhar as outras duas categorias de valores:

  • Um valor é realmente apenas o novo nome para o tipo anterior de rvalue, ou seja, eles são os valores que não são xvalues.

  • Glvalues ​​são a união de xvalues ​​e lvalues ​​em um grupo, porque eles compartilham muitas propriedades em comum.

Então, na verdade, tudo se resume a xvalores e a necessidade de restringir o movimento para exatamente e somente certos lugares. Esses lugares são definidos pela categoria de valor; prvalues ​​são os movimentos implícitos, e xvalues ​​são os movimentos explícitos ( std::move retorna um xvalue).


INTRODUÇÃO

O ISOC ++ 11 (oficialmente ISO / IEC 14882: 2011) é a versão mais recente do padrão da linguagem de programação C ++. Ele contém alguns novos recursos e conceitos, por exemplo:

  • referências de valor
  • Categorias de valor de expressão xvalue, glvalue, prvalue
  • mover semântica

Se quisermos entender os conceitos das novas categorias de valores de expressão, devemos estar cientes de que existem referências de valor e lvalue. É melhor saber que valores podem ser passados ​​para referências não-constantes.

int& r_i=7; // compile error
int&& rr_i=7; // OK

Podemos ganhar alguma intuição dos conceitos de categorias de valor se citarmos a subseção titulada Lvalores e valores do trabalho preliminar N3337 (o rascunho mais similar ao padrão ISOC ++ 11 publicado).

3.10 Lvalores e rvalores [basic.lval]

1 As expressões são categorizadas de acordo com a taxonomia da Figura 1.

  • Um lvalue (assim chamado, historicamente, porque lvalues ​​poderia aparecer no lado esquerdo de uma expressão de atribuição) designa uma função ou um objeto. [Exemplo: Se E é uma expressão do tipo ponteiro, então * E é uma expressão lvalue referente ao objeto ou função para a qual E aponta. Como outro exemplo, o resultado de chamar uma função cujo tipo de retorno é uma referência lvalue é um lvalue. - por exemplo
  • Um xvalue (um valor “eXpiring”) também se refere a um objeto, geralmente perto do final de sua vida útil (para que seus recursos possam ser movidos, por exemplo). Um xvalue é o resultado de certos tipos de expressões envolvendo referências rvalue (8.3.2). [Exemplo: O resultado de chamar uma função cujo tipo de retorno é uma referência de valor é um xvalue. - por exemplo
  • Um glvalue (lvalue “generalizado”) é um lvalue ou um xvalue.
  • Um rvalue (assim chamado, historicamente, porque rvalues ​​podem aparecer no lado direito de uma expressão de atribuição) é um xvalue, um
    objeto temporário (12.2) ou seu subobjeto, ou um valor que não é
    associado a um objeto.
  • Um prvalor (rvalue “puro”) é um rvalue que não é um xvalue. [Exemplo: O resultado de chamar uma função cujo tipo de retorno não é um
    referência é um valor. O valor de um literal como 12, 7.3e5 ou
    true também é um valor. - por exemplo

Toda expressão pertence exatamente a uma das classificações fundamentais nessa taxonomia: lvalue, xvalue ou prvalue. Essa propriedade de uma expressão é chamada de categoria de valor.

Mas eu não estou bem certo de que esta subseção é suficiente para entender os conceitos claramente, porque "geralmente" não é realmente geral, "perto do fim de sua vida" não é realmente concreto ", envolvendo referências de valor" não é realmente claro, e "Exemplo: O resultado de chamar uma função cujo tipo de retorno é uma referência rvalue é um xvalue." Parece que uma cobra está mordendo o rabo.

CATEGORIAS DE VALOR PRIMÁRIO

Cada expressão pertence exatamente a uma categoria de valor principal. Essas categorias de valor são as categorias lvalue, xvalue e prvalue.

lvalores

A expressão E pertence à categoria lvalue se e somente se E se referir a uma entidade que JÁ tenha uma identidade (endereço, nome ou pseudônimo) que a torne acessível fora de E.

#include <iostream>

int i=7;

const int& f(){
    return i;
}

int main()
{
    std::cout<<&"www"<<std::endl; // This address ...
    std::cout<<&"www"<<std::endl; // ... and this address are the same.
    "www"; // The expression "www" in this row is an lvalue expression, because it refers to the same entity ...
    "www"; // ... as the entity the expression "www" in this row refers to.

    i; // The expression i in this row is an lvalue expression, because it refers to the same entity ...
    i; // ... as the entity the expression i in this row refers to.

    int* p_i=new int(7);
    *p_i; // The expression *p_i in this row is an lvalue expression, because it refers to the same entity ...
    *p_i; // ... as the entity the expression *p_i in this row refers to.

    const int& r_I=7;
    r_I; // The expression r_I in this row is an lvalue expression, because it refers to the same entity ...
    r_I; // ... as the entity the expression r_I in this row refers to.

    f(); // The expression f() in this row is an lvalue expression, because it refers to the same entity ...
    i; // ... as the entity the expression f() in this row refers to.

    return 0;
}

xvalores

A expressão E pertence à categoria xvalue se e somente se for

- o resultado de chamar uma função, implícita ou explicitamente, cujo tipo de retorno é uma referência de valor para o tipo de objeto que está sendo retornado ou

int&& f(){
    return 3;
}

int main()
{
    f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.

    return 0;
}

- uma conversão para uma referência de valor para o tipo de objeto ou

int main()
{
    static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs to the xvalue category, because it is a cast to an rvalue reference to object type.
    std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).

    return 0;
}

- uma expressão de acesso de membro da classe designando um membro de dados não estáticos do tipo não referência em que a expressão do objeto é um xvalue ou

struct As
{
    int i;
};

As&& f(){
    return As();
}

int main()
{
    f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.

    return 0;
}

- uma expressão ponteiro-a-membro na qual o primeiro operando é um xvalue e o segundo operando é um ponteiro para o membro de dados.

Observe que o efeito das regras acima é que as referências de valor nominal a objetos são tratadas como lvalores e referências de valor não nomeadas a objetos são tratadas como xvalues; As referências de valor a funções são tratadas como lvalores, sejam eles nomeados ou não.

#include <functional>

struct As
{
    int i;
};

As&& f(){
    return As();
}

int main()
{
    f(); // The expression f() belongs to the xvalue category, because it refers to an unnamed rvalue reference to object.
    As&& rr_a=As();
    rr_a; // The expression rr_a belongs to the lvalue category, because it refers to a named rvalue reference to object.
    std::ref(f); // The expression std::ref(f) belongs to the lvalue category, because it refers to an rvalue reference to function.

    return 0;
}

valores

A expressão E pertence à categoria de valor se e somente se E não pertence nem ao lvalue nem à categoria xvalue.

struct As
{
    void f(){
        this; // The expression this is a prvalue expression. Note, that the expression this is not a variable.
    }
};

As f(){
    return As();
}

int main()
{
    f(); // The expression f() belongs to the prvalue category, because it belongs neither to the lvalue nor to the xvalue category.

    return 0;
}

CATEGORIAS DE VALOR MISTO

Existem outras duas categorias importantes de valores mistos. Essas categorias de valor são categorias rvalue e glvalue.

valores

A expressão E pertence à categoria rvalue se e somente se E pertencer à categoria xvalue ou à categoria prvalue.

Note que esta definição significa que a expressão E pertence à categoria rvalue se e somente se E se referir a uma entidade que não tenha qualquer identidade que a torne acessível fora de E YET.

glvalues

A expressão E pertence à categoria glvalue se e somente se E pertencer à categoria lvalue ou à categoria xvalue.

UMA REGRA PRÁTICA

Scott Meyer published uma regra prática muito útil para distinguir rvalues ​​de lvalues.

  • Se você pode pegar o endereço de uma expressão, a expressão é um lvalue.
  • Se o tipo de uma expressão é uma referência lvalue (por exemplo, T & ou const T &, etc.), essa expressão é um lvalue.
  • Caso contrário, a expressão é um valor. Conceitualmente (e tipicamente também de fato), os valores r correspondem a objetos temporários, como aqueles retornados de funções ou criados por meio de conversões implícitas de tipo. A maioria dos valores literais (por exemplo, 10 e 5.3) também são valores r.

As categorias do C ++ 03 são muito restritas para capturar a introdução de referências de valor corretamente nos atributos de expressão.

Com a introdução deles, foi dito que uma referência de valor não nomeada é avaliada como um rvalor, de modo que a resolução de sobrecarga preferiria ligações de referência de valor, o que faria com que ela selecionasse construtores de movimento em vez de construtores de cópia. Mas foi descoberto que isso causa problemas por toda parte, por exemplo, com Tipos dinâmicos e com qualificações.

Para mostrar isso, considere

int const&& f();

int main() {
  int &&i = f(); // disgusting!
}

Em rascunhos pre-xvalue, isso era permitido, porque em C ++ 03, os valores de r de tipos não de classe nunca são qualificados para cv. Mas pretende-se que const se aplique no caso de referência-rvalue, porque aqui nos referimos a objetos (= memória!), E eliminar const de valores não-classe é principalmente porque não há objeto por perto.

A questão dos tipos dinâmicos é de natureza semelhante. Em C ++ 03, os rvalues ​​do tipo de classe têm um tipo dinâmico conhecido - é o tipo estático dessa expressão. Por ter outra maneira, você precisa de referências ou desreferências, que são avaliadas em um lvalue. Isso não é verdade com referências de valor não identificadas, mas elas podem mostrar um comportamento polimórfico. Então, para resolver isso,

  • referências de valor sem nome se tornam xvalues . Eles podem ser qualificados e potencialmente ter seu tipo dinâmico diferente. Eles fazem, como pretendido, preferem referências de valor durante a sobrecarga e não se ligam a referências de valor não constantes.

  • O que anteriormente era um rvalue (literais, objetos criados por conversões para tipos não referência) agora se torna um valor . Eles têm a mesma preferência que xvalues ​​durante a sobrecarga.

  • O que antes era um lvalue permanece um lvalue.

E dois agrupamentos são feitos para capturar aqueles que podem ser qualificados e podem ter tipos dinâmicos diferentes ( glvalues ) e aqueles em que a sobrecarga prefere a vinculação de referência rvalue ( rvalues ).


IMHO, a melhor explicação sobre o seu significado nos deu stroustrup.com/terminology.pdf + levar em conta exemplos de Dániel Sándor e Mohan :

Stroustrup:

Agora eu estava seriamente preocupado. Claramente nós estávamos indo para um impasse ou uma bagunça ou ambos. Passei a hora do almoço fazendo uma análise para ver quais das propriedades (de valores) eram independentes. Havia apenas duas propriedades independentes:

  • has identity - isto é, e endereço, um ponteiro, o usuário pode determinar se duas cópias são idênticas, etc.
  • can be moved from - ou seja, estamos autorizados a deixar a fonte de uma "cópia" em algum estado indeterminado, mas válido

Isso me levou à conclusão de que existem exatamente três tipos de valores (usando o truque de notação regex de usar uma letra maiúscula para indicar um negativo - eu estava com pressa):

  • iM : tem identidade e não pode ser movido de
  • im : tem identidade e pode ser movido de (por exemplo, o resultado da conversão de um lvalue para uma referência de valor)
  • Im : não tem identidade e pode ser movido da quarta possibilidade ( IM : não tem identidade e não pode ser movida) não é útil em C++ (ou, eu acho) em qualquer outra linguagem.

Além dessas três classificações fundamentais de valores, temos duas generalizações óbvias que correspondem às duas propriedades independentes:

  • i : tem identidade
  • m : pode ser movido de

Isso me levou a colocar esse diagrama no quadro:

Nomeação

Observei que tínhamos apenas liberdade limitada para nomear: Os dois pontos à esquerda (rotulados iM e i ) são o que pessoas com mais ou menos formalidade chamam lvalues e os dois pontos à direita (rotulados m e Im ) são o que as pessoas com mais ou menos formalidade chamam rvalues . Isso deve ser refletido em nossa nomeação. Ou seja, a "perna" esquerda do W deve ter nomes relacionados ao lvalue e a "perna" direita do W deve ter nomes relacionados ao rvalue. Observo que toda essa discussão / problema surge da introdução de referências de valor e da semântica de movimento. Essas noções simplesmente não existem no mundo de Strachey consistindo apenas de valores e lvalues . Alguém observou que as idéias que

  • Cada value é um lvalue ou um rvalue
  • Um lvalue não é um rvalue e um rvalue não é um lvalue

estão profundamente enraizadas em nossa consciência, propriedades muito úteis, e traços dessa dicotomia podem ser encontrados em todo o rascunho do padrão. Todos concordamos que devemos preservar essas propriedades (e torná-las precisas). Isso restringiu ainda mais nossas escolhas de nomes. Observei que o texto da biblioteca padrão usa rvalue para significar m (a generalização), de modo que, para preservar a expectativa e o texto da biblioteca padrão, o ponto inferior direito do W deve ser denominado rvalue.

Isso levou a uma discussão focada de nomeação. Primeiro, precisávamos decidir sobre lvalue. Deve lvalue significa iM ou a generalização i ? Liderados por Doug Gregor, listamos os lugares no texto da linguagem central onde a palavra lvalue era qualificada para significar um ou outro. Uma lista foi feita e, na maioria dos casos, e no lvalue texto mais difícil / frágil, atualmente significa iM . Este é o significado clássico de lvalue porque "nos velhos tempos" nada foi movido; move é uma noção nova em C++0x . Além disso, nomear o ponto de desvio do valor W nos dá a propriedade de que cada valor é um lvalue ou um rvalue , mas não ambos.

Portanto, o ponto superior esquerdo do W é lvalue e o ponto inferior direito é rvalue. O que isso faz no canto inferior direito e no canto superior direito? O ponto inferior esquerdo é uma generalização do lvalue clássico, permitindo a movimentação. Então é um valor generalized lvalue. Nós o chamamos glvalue. Você pode discutir sobre a abreviação, mas (eu acho) não com a lógica. Assumimos que, no uso sério, o valor generalized lvalue seria de alguma forma abreviado de qualquer maneira, por isso é melhor fazê-lo imediatamente (ou nos arriscarmos a nos confundir). O ponto superior direito do W é menos geral que o canto inferior direito (agora, como sempre, chamado rvalue ). Esse ponto representa a noção pura original de um objeto do qual você pode se mover porque não pode ser referido novamente (exceto por um destruidor). Gostei da expressão specialized rvalue em contraste com generalized lvalue mas o valor pure rvalue abreviado para prvalue venceu (e provavelmente com razão). Portanto, a perna esquerda do W é lvalue e glvalue e a perna direita é prvalue e rvalue. A propósito, cada valor é um valor gl ou um valor pricipal, mas não ambos.

Isso deixa o meio superior do W : im ; isto é, valores que possuem identidade e podem ser movidos. Nós realmente não temos nada que nos leve a um bom nome para essas feras esotéricas. Eles são importantes para as pessoas que trabalham com o texto padrão (rascunho), mas é improvável que se tornem um nome familiar. Nós não encontramos nenhuma limitação real na nomenclatura para nos guiar, então escolhemos 'x' para o centro, o desconhecido, o estranho, o xpert apenas, ou mesmo o x-rated.


Vou começar com sua última pergunta:

Por que essas novas categorias são necessárias?

O padrão C ++ contém muitas regras que lidam com a categoria de valor de uma expressão. Algumas regras fazem uma distinção entre lvalue e rvalue. Por exemplo, quando se trata de resolução de sobrecarga. Outras regras fazem uma distinção entre glvalue e prvalue. Por exemplo, você pode ter um glvalue com um tipo incompleto ou abstrato, mas não há nenhum prvalue com um tipo incompleto ou abstrato. Antes de termos essa terminologia, as regras que realmente precisavam distinguir entre glvalue / prvalue referiam-se a lvalue / rvalue e ou eram involuntariamente erradas ou continham muitas explicações e exceções à regra a la "... a menos que o rvalue se deva ao nome não identificado referência de valor ... ". Então, parece uma boa idéia dar apenas os conceitos de glvalues ​​e prvalues ​​como seu próprio nome.

Quais são essas novas categorias de expressões? Como essas novas categorias se relacionam com as categorias de valor e valor existentes?

Ainda temos os termos lvalue e rvalue que são compatíveis com o C ++ 98. Nós apenas dividimos os valores em dois subgrupos, xvalues ​​e prvalues, e nos referimos a lvalues ​​e xvalues ​​como glvalues. Xvalues ​​são um novo tipo de categoria de valor para referências de rvalue sem nome. Cada expressão é um desses três: lvalue, xvalue, prvalue. Um diagrama de Venn ficaria assim:

    ______ ______
   /      X      \
  /      / \      \
 |   l  | x |  pr  |
  \      \ /      /
   \______X______/
       gl    r

Exemplos com funções:

int   prvalue();
int&  lvalue();
int&& xvalue();

Mas também não se esqueça que as referências de valor nominal são lvalues:

void foo(int&& t) {
  // t is initialized with an rvalue expression
  // but is actually an lvalue expression itself
}

Um adendo às excelentes respostas acima, em um ponto que me confundiu mesmo depois de eu ter lido Stroustrup e pensado ter entendido a distinção rvalue / lvalue. Quando você vê

int&& a = 3 ,

É muito tentador ler o int&&como um tipo e concluir que aé um valor. Não é:

int&& a = 3;
int&& c = a; //error: cannot bind 'int' lvalue to 'int&&'
int& b = a; //compiles

atem um nome e é ipso facto um lvalue. Não pense no &&como parte do tipo de a; é apenas algo dizendo a você o que aé permitido se ligar.

Isso é importante principalmente para T&&argumentos de tipo em construtores. Se você escreve

Foo::Foo(T&& _t) : t{_t} {}

você irá copiar _tpara t. Você precisa

Foo::Foo(T&& _t) : t{std::move(_t)} {}se você quiser se mover. Será que meu compilador me avisou quando deixei de fora o move!





c++11