O que é "rvalue reference for * this"?

238

Me deparei com uma proposta chamada "rvalue reference for * this" na página de status C ++ 11 do clang .

Eu li bastante sobre referências a rvalue e as compreendi, mas acho que não sei disso. Também não consegui encontrar muitos recursos na web usando os termos.

Há um link para o documento da proposta na página: N2439 (Estendendo a semântica de movimentação para * this), mas também não estou obtendo muitos exemplos a partir daí.

Sobre o que é esse recurso?

ryaner
fonte

Respostas:

293

Primeiro, "ref-qualifiers para * this" é apenas uma "declaração de marketing". O tipo de *thisnunca muda, veja a parte inferior desta postagem. É muito mais fácil entendê-lo com este texto.

Em seguida, o código a seguir escolhe a função a ser chamada com base no qualificador ref do "parâmetro de objeto implícito" da função :

// t.cpp
#include <iostream>

struct test{
  void f() &{ std::cout << "lvalue object\n"; }
  void f() &&{ std::cout << "rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // lvalue
  test().f(); // rvalue
}

Resultado:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

Tudo é feito para permitir que você aproveite o fato de quando o objeto em que a função é chamada é um rvalue (temporário sem nome, por exemplo). Tome o seguinte código como um exemplo adicional:

struct test2{
  std::unique_ptr<int[]> heavy_resource;

  test2()
    : heavy_resource(new int[500]) {}

  operator std::unique_ptr<int[]>() const&{
    // lvalue object, deep copy
    std::unique_ptr<int[]> p(new int[500]);
    for(int i=0; i < 500; ++i)
      p[i] = heavy_resource[i];

    return p;
  }

  operator std::unique_ptr<int[]>() &&{
    // rvalue object
    // we are garbage anyways, just move resource
    return std::move(heavy_resource);
  }
};

Isso pode ser um pouco artificial, mas você deve entender.

Observe que você pode combinar os qualificadores cv ( conste volatile) e ref-qualificadores ( &e &&).


Nota: Muitas citações padrão e explicação de resolução de sobrecarga depois daqui!

† Para entender como isso funciona e por que a resposta de @Nicol Bolas está pelo menos parcialmente errada, precisamos cavar um pouco o padrão C ++ (a parte que explica por que a resposta de @ Nicol está errada está na parte inferior, se você estiver interessado apenas nisso).

Qual função será chamada é determinada por um processo chamado resolução de sobrecarga . Esse processo é bastante complicado, portanto, tocaremos apenas o que é importante para nós.

Primeiro, é importante ver como a resolução de sobrecarga para funções-membro funciona:

§13.3.1 [over.match.funcs]

p2 O conjunto de funções candidatas pode conter funções membro e não membro a serem resolvidas na mesma lista de argumentos. Para que as listas de argumentos e parâmetros sejam comparáveis ​​nesse conjunto heterogêneo, uma função membro é considerada como tendo um parâmetro extra, chamado parâmetro implícito do objeto, que representa o objeto para o qual a função membro foi chamada . [...]

p3 Da mesma forma, quando apropriado, o contexto pode construir uma lista de argumentos que contém um argumento de objeto implícito para indicar o objeto a ser operado.

Por que precisamos comparar funções de membros e de não membros? Sobrecarga de operador, é por isso. Considere isto:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

Você certamente gostaria que o seguinte chamasse a função livre, não é?

char const* s = "free foo!\n";
foo f;
f << s;

É por isso que as funções membro e não membro são incluídas no chamado conjunto de sobrecarga. Para tornar a resolução menos complicada, existe a parte em negrito da cotação padrão. Além disso, esta é a parte importante para nós (mesma cláusula):

p4 Para funções-membro não estáticas, o tipo do parâmetro implícito do objeto é

  • “Lvalue reference to cv X ” para funções declaradas sem um ref-qualifier ou com o & ref-qualifier

  • “Referência de valor rv para cv X ” para funções declaradas com o && qualificador ref

onde Xé a classe da qual a função é membro e cv é a qualificação cv na declaração da função de membro. [...]

p5 Durante a resolução de sobrecarga, [...] o parâmetro implícito do objeto [...] mantém sua identidade, pois as conversões no argumento correspondente devem obedecer a estas regras adicionais:

  • nenhum objeto temporário pode ser introduzido para conter o argumento do parâmetro implícito do objeto; e

  • nenhuma conversão definida pelo usuário pode ser aplicada para obter uma correspondência de tipo com ela

[...]

(O último bit significa apenas que você não pode enganar a resolução de sobrecarga com base nas conversões implícitas do objeto em que uma função membro (ou operador) é chamada.)

Vamos dar o primeiro exemplo na parte superior deste post. Após a transformação mencionada acima, o conjunto de sobrecargas se parece com isso:

void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

Em seguida, a lista de argumentos, contendo um argumento de objeto implícito , é comparada com a lista de parâmetros de todas as funções contidas no conjunto de sobrecargas. No nosso caso, a lista de argumentos conterá apenas esse argumento do objeto. Vamos ver como isso se parece:

// first call to 'f' in 'main'
test t;
f1(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

Se todas as sobrecargas no conjunto forem testadas, apenas uma permanecerá, a resolução da sobrecarga foi bem-sucedida e a função vinculada a essa sobrecarga transformada será chamada. O mesmo vale para a segunda chamada para 'f':

// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
            // taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
            // kept in overload-set

Nota, contudo, que, se não tivéssemos fornecido qualquer ref-qualificação (e, como tal, não sobrecarregado da função), que f1 iria coincidir com um rvalue (ainda §13.3.1):

p5 [...] Para funções membro não estáticas declaradas sem um ref-qualifier , uma regra adicional se aplica:

  • mesmo que o parâmetro implícito do objeto não seja constqualificado, um rvalue pode ser vinculado ao parâmetro desde que em todos os outros aspectos o argumento possa ser convertido no tipo do parâmetro implícito do objeto.
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

Agora, sobre por que a resposta de @ Nicol está pelo menos parcialmente errada. Ele diz:

Observe que esta declaração altera o tipo de *this.

Isso está errado, *thisé sempre um lvalue:

§5.3.1 [expr.unary.op] p1

O *operador unário executa indireção : a expressão à qual é aplicada deve ser um ponteiro para um tipo de objeto ou um ponteiro para um tipo de função e o resultado é um valor l que se refere ao objeto ou função para a qual a expressão aponta.

§9.3.2 [class.this] p1

No corpo de uma função de membro não estática (9.3), a palavra this- chave é uma expressão de prvalor cujo valor é o endereço do objeto para o qual a função é chamada. O tipo de thisem uma função de membro de uma classe Xé X*. [...]

Xeo
fonte
Eu acredito que os tipos de paranômetros logo após a seção "após a transformação" devem ser 'foo' em vez de 'test'.
Ryaner
@ryaner: Boa localização, obrigado. Embora não seja o parâmetro, mas o identificador de classe das funções esteja errado. :)
Xeo
oops lamentamos esqueci-me sobre a classe brinquedo teste chamado quando li essa parte e pensei f está contido dentro de foo, assim, o meu comentário ..
ryaner
Isso pode ser feito com construtores MyType(int a, double b) &&:?
Germán Diago 02/12
2
"O tipo de * isso nunca muda" Talvez você deva ficar um pouco mais claro de que isso não muda com base na qualificação do valor de r / l. mas pode mudar entre const / non-const.
Xaxxon
78

Há um caso de uso adicional para o formulário lvalue ref-qualifier. O C ++ 98 possui uma linguagem que permite que constfunções que não são membros sejam chamadas para instâncias de classe que são rvalues. Isso leva a todos os tipos de estranheza que são contrários ao próprio conceito de valorização e se desviam de como os tipos internos funcionam:

struct S {
  S& operator ++(); 
  S* operator &(); 
};
S() = S();      // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S();           // taking address of rvalue...

Os qualificadores ref do Lvalue resolvem estes problemas:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

Agora, os operadores trabalham como os tipos internos, aceitando apenas lvalues.

JohannesD
fonte
28

Digamos que você tenha duas funções em uma classe, ambas com o mesmo nome e assinatura. Mas um deles é declarado const:

void SomeFunc() const;
void SomeFunc();

Se uma instância de classe não for const, a resolução de sobrecarga selecionará preferencialmente a versão não const. Se a instância for const, o usuário poderá chamar apenas a constversão. E o thisponteiro é um constponteiro, portanto, a instância não pode ser alterada.

O que a "referência de valor r para isso` faz é permitir que você adicione outra alternativa:

void RValueFunc() &&;

Isso permite que você tenha uma função que pode ser chamada se o usuário chamar através de um valor-r adequado. Portanto, se este for do tipo Object:

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

Dessa forma, você pode especializar o comportamento com base no fato de o objeto estar sendo acessado via valor-r ou não.

Observe que você não pode sobrecarregar entre as versões de referência com valor r e as versões sem referência. Ou seja, se você tiver um nome de função de membro, todas as suas versões usam os qualificadores de valor l / r thisou nenhum deles. Você não pode fazer isso:

void SomeFunc();
void SomeFunc() &&;

Você deve fazer isso:

void SomeFunc() &;
void SomeFunc() &&;

Observe que esta declaração altera o tipo de *this. Isso significa que &&todas as versões acessam membros como referências de valor-r. Portanto, torna-se possível mover-se facilmente de dentro do objeto. O exemplo dado na primeira versão da proposta é (nota: o seguinte pode não estar correto com a versão final do C ++ 11; é direto do "valor r inicial desta" proposta inicial):

class X {
   std::vector<char> data_;
public:
   // ...
   std::vector<char> const & data() const & { return data_; }
   std::vector<char> && data() && { return data_; }
};

X f();

// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move
Nicol Bolas
fonte
2
Eu acho que você precisa para std::movea segunda versão, não? Além disso, por que a referência rvalue retorna?
Xeo
1
@ Xeo: Porque é isso que o exemplo estava na proposta; Não tenho ideia se ainda funciona com a versão atual. E a razão para o retorno de referência do valor-r é porque o movimento deve depender da pessoa que o captura. Ainda não deveria acontecer, caso ele realmente queira armazená-lo em um && em vez de um valor.
Nicol Bolas
3
Certo, eu meio que pensei no motivo da minha segunda pergunta. Eu me pergunto, no entanto, uma referência de valor ao membro de um temporário prolonga a vida útil desse temporário, ou o membro dele? Eu poderia jurar que vi uma pergunta sobre isso há algum tempo ...
Xeo 22/12/11
1
@ Xeo: Não é totalmente verdade. A resolução de sobrecarga sempre escolherá a versão não const, se existir. Você precisaria fazer um elenco para obter a versão const. Atualizei a postagem para esclarecer.
Nicol Bolas
15
Eu pensei que poderia explicar, afinal eu criei esse recurso para C ++ 11;) Xeo está certo ao insistir em que não altera o tipo de *this, no entanto, eu posso entender de onde vem a confusão. Isso ocorre porque o ref-qualifier altera o tipo de parâmetro implícito (ou "oculto") para o qual o objeto "this" (aspas colocadas de propósito aqui!) É vinculado durante a resolução de sobrecarga e a chamada de função. Portanto, nenhuma alteração, *thispois isso é corrigido, como o Xeo explica. Em vez disso mudar de parâmetro "hiddden" para torná-lo lvalue- ou rvalue referência, assim como constqualificador função torna constetc ..
bronekk