O que significa T &&(double e comercial) em C++ 11?




c++11 rvalue-reference (3)

Eu estive olhando para alguns dos novos recursos do C ++ 11 e um que eu notei é o duplo comercial na declaração de variáveis, como T&& var .

Para começar, o que é essa besta chamada? Gostaria que o Google nos permitisse procurar pontuação como essa.

O que exatamente significa isso ?

À primeira vista, parece ser uma referência dupla (como os ponteiros duplos de estilo C, T** var ), mas estou tendo dificuldades em pensar em um caso de uso para isso.


Denota uma referência de valor. As referências de valor só serão vinculadas a objetos temporários, a menos que explicitamente gerado de outra forma. Eles são usados ​​para tornar os objetos muito mais eficientes em determinadas circunstâncias e para fornecer um recurso conhecido como encaminhamento perfeito, o que simplifica bastante o código do modelo.

Em C ++ 03, você não pode distinguir entre uma cópia de um lvalue não mutável e um rvalue.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);

Em C ++ 0x, isso não é o caso.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);

Considere a implementação por trás desses construtores. No primeiro caso, a cadeia deve executar uma cópia para reter a semântica de valor, o que envolve uma nova alocação de heap. No entanto, no segundo caso, sabemos de antemão que o objeto que foi passado para o nosso construtor é imediatamente devido à destruição, e não precisa permanecer intocado. Podemos efetivamente apenas trocar os ponteiros internos e não realizar nenhuma cópia neste cenário, que é substancialmente mais eficiente. A semântica de movimento beneficia qualquer classe que tenha cópia cara ou proibida de recursos referenciados internamente. Considere o caso de std::unique_ptr - agora que nossa classe pode distinguir entre temporários e não-temporários, podemos fazer a semântica de movimento funcionar corretamente para que o unique_ptr não possa ser copiado mas possa ser movido, o que significa que std::unique_ptr pode ser legalmente armazenado em contêineres Padrão, classificados, etc, enquanto o std::auto_ptr do C ++ 03 não pode.

Agora consideramos o outro uso de referências de valor - encaminhamento perfeito. Considere a questão de vincular uma referência a uma referência.

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template

Não é possível lembrar o que o C ++ 03 diz sobre isso, mas em C ++ 0x, o tipo resultante ao lidar com referências de rvalue é crítico. Uma referência de valor para um tipo T, onde T é um tipo de referência, torna-se uma referência do tipo T.

(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&

Considere a função de modelo mais simples - min e max. Em C ++ 03 você precisa sobrecarregar todas as quatro combinações de const e non-const manualmente. Em C ++ 0x é apenas uma sobrecarga. Combinado com modelos variadic, isso permite um encaminhamento perfeito.

template<typename A, typename B> auto min(A&& aref, B&& bref) {
    // for example, if you pass a const std::string& as first argument,
    // then A becomes const std::string& and by extension, aref becomes
    // const std::string&, completely maintaining it's type information.
    if (std::forward<A>(aref) < std::forward<B>(bref))
        return std::forward<A>(aref);
    else
        return std::forward<B>(bref);
}

Deixei de fora a dedução de tipo de retorno, porque não consigo lembrar como é feito de improviso, mas esse min pode aceitar qualquer combinação de lvalores, valores de r, valores de const.


Ele declara uma referência de valor (documento de proposta de normas).

Aqui está uma introdução às references .

Aqui está uma fantástica e detalhada análise de referências de valor por um dos developers bibliotecas padrão da Microsoft. (Mas veja a cautela nos comentários após esta resposta antes de ler este artigo.)

A maior diferença entre uma referência C ++ 03 (agora chamada de referência lvalue em C ++ 11) é que ela pode se ligar a um rvalue como um temporário sem precisar ser const. Assim, esta sintaxe agora é legal:

T&& r = T();

As referências de valor fornecem principalmente o seguinte:

Mova a semântica . Um construtor de movimento e um operador de atribuição de movimento agora podem ser definidos usando uma referência de valor em vez da referência usual const-lvalue. Um movimento funciona como uma cópia, exceto que não é obrigado a manter a fonte inalterada; na verdade, geralmente modifica a fonte de forma que ela não possua mais os recursos movidos. Isso é ótimo para eliminar cópias estranhas, especialmente em implementações de biblioteca padrão.

Por exemplo, um construtor de cópia pode ter esta aparência:

foo(foo const& other)
{
    this->length = other.length;
    this->ptr = new int[other.length];
    copy(other.ptr, other.ptr + other.length, this->ptr);
}

Se este construtor foi passado um temporário, a cópia seria desnecessária porque sabemos que o temporário apenas será destruído; Por que não utilizar os recursos já temporários alocados? Em C ++ 03, não há como impedir a cópia, pois não podemos determinar que recebemos um temporário. Em C ++ 11, podemos sobrecarregar um construtor de movimento:

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

Observe a grande diferença aqui: o construtor de movimento realmente modifica seu argumento. Isso efetivamente "moveria" o temporário para o objeto que está sendo construído, eliminando assim a cópia desnecessária.

O construtor move seria usado para temporários e para referências não-constantes que são explicitamente convertidas para referências rvalue usando a função std::move (ele apenas realiza a conversão). O código a seguir invoca o construtor de movimento para f1 e f2 :

foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"

Encaminhamento perfeito . As referências de valor nos permitem encaminhar corretamente argumentos para funções modeladas. Tomemos por exemplo esta função de fábrica:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
    return std::unique_ptr<T>(new T(a1));
}

Se chamarmos factory<foo>(5) , o argumento será deduzido para ser int& , que não se ligará a um literal 5, mesmo que o construtor foo tenha um int . Bem, poderíamos usar A1 const& , mas e se foo toma o argumento do construtor por referência não const? Para fazer uma função de fábrica verdadeiramente genérica, teríamos que sobrecarregar a fábrica na A1& e na A1 const& . Isso pode ser bom se a fábrica levar 1 tipo de parâmetro, mas cada tipo de parâmetro adicional multiplicaria a sobrecarga necessária definida por 2. Isso é muito rapidamente inatingível.

As referências de valor corrigem esse problema permitindo que a biblioteca padrão defina uma função std::forward que possa encaminhar corretamente as referências lvalue / rvalue. Para mais informações sobre como o std::forward funciona, veja esta excelente resposta .

Isso nos permite definir a função de fábrica assim:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
    return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}

Agora, o argumento rvalue / lvalue-ness é preservado quando passado para o construtor de T Isso significa que se factory é chamado com um rvalue, o construtor de T é chamado com um rvalue. Se factory é chamado com um lvalue, o construtor de T é chamado com um lvalue. A função de fábrica aprimorada funciona devido a uma regra especial:

Quando o tipo de parâmetro da função é da forma T&& onde T é um parâmetro de modelo, e o argumento da função é um lvalue do tipo A , o tipo A& é usado para a dedução do argumento de modelo.

Assim, podemos usar fábrica assim:

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)

Propriedades importantes de referência de valor :

  • Para resolução de sobrecarga, os valores de l preferem vinculação a referências de lvalue e os valores de r preferem vinculação a referências de valor . Portanto, por que os temporários preferem invocar um operador de atribuição de movimento / atribuição de movimento sobre um construtor de cópia / operador de atribuição.
  • Referências de valor implicitamente se ligam a valores e temporárias que são o resultado de uma conversão implícita . ou seja, float f = 0f; int&& i = f; float f = 0f; int&& i = f; é bem formado porque float é implicitamente conversível em int; a referência seria para um temporário que é o resultado da conversão.
  • Referências de valor nominal são lvalues. As referências de valor nominal são rvalues. Isto é importante para entender porque a chamada std::move é necessária em: foo&& r = foo(); foo f = std::move(r); foo&& r = foo(); foo f = std::move(r);

Uma referência de valor é um tipo que se comporta muito como a referência comum X &, com várias exceções. O mais importante é que quando se trata de resolução de sobrecarga de função, lvalues ​​preferem referências de lvalue de estilo antigo, enquanto que rvalues ​​preferem as novas referências de rvalue:

void foo(X& x);  // lvalue reference overload
void foo(X&& x); // rvalue reference overload

X x;
X foobar();

foo(x);        // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)

Então, o que é um valor? Qualquer coisa que não seja um lvalue. Um lvalue é uma expressão que se refere a uma localização de memória e nos permite obter o endereço dessa localização de memória através do operador &.

É quase mais fácil entender primeiro o que os valores realizam com um exemplo:

 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {}
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     std::cout << "copy constructor called on lvalue\n";
  }

  // move constructor that take rvalue
  Sample(Sample&& s) 
  {  // steal s's resources
     ptr = s.ptr;
     size = s.size;        
     s.ptr = nullptr; // destructive write
     s.size = 0;
     cout << "Move constructor called on rvalue." << std::endl;
  }    
  // normal copy assignment operator taking lvalue
  Sample& operator=(const Sample& s)
  {
   if(this != &s) {
      delete [] ptr; // free current pointer
      ptr = new int[s.size]; 
      size = s.size; 
    }
    cout << "Copy Assignment called on lvalue." << std::endl;
    return *this;
  }    
 // overloaded move assignment operator taking rvalue
 Sample& operator=(Sample&& lhs)
 {
   if(this != &s) {
      delete [] ptr; //don't let ptr be orphaned 
      ptr = lhs.ptr;   //but now "steal" lhs, don't clone it.
      size = lhs.size; 
      lhs.ptr = nullptr; // lhs's new "stolen" state
      lhs.size = 0;
   }
   cout << "Move Assignment called on rvalue" << std::endl;
   return *this;
 }
//...snip
};     

Os operadores de construtor e de atribuição foram sobrecarregados com versões que tomam referências de valor. Referências Rvalue permitem que uma função se ramifique no tempo de compilação (via resolução de sobrecarga) na condição "Estou sendo chamado em um lvalue ou um rvalue?". Isso nos permitiu criar operadores mais eficientes de construtores e atribuições acima dos recursos de movimentação, em vez de copiá-los.

O compilador ramifica-se automaticamente em tempo de compilação (dependendo do fato de estar sendo invocado para um lvalue ou um rvalue), escolhendo se o construtor de movimento ou o operador de atribuição de movimento deve ser chamado.

Resumindo: referências de valor permitem mover semântica (e encaminhamento perfeito, discutido no link do artigo abaixo).

Um exemplo prático e fácil de entender é o modelo de classe std :: unique_ptr . Como um unique_ptr mantém a propriedade exclusiva de seu ponteiro bruto subjacente, unique_ptr não pode ser copiado. Isso violaria sua invariante de propriedade exclusiva. Então eles não têm construtores de cópia. Mas eles têm construtores de movimento:

template<class T> class unique_ptr {
  //...snip
 unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};

 std::unique_ptr<int[] pt1{new int[10]};  
 std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.  

 // So we must first cast ptr1 to an rvalue 
 std::unique_ptr<int[]> ptr2{std::move(ptr1)};  

std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
 int size)      
{
  for (auto i = 0; i < size; ++i) {
     param[i] += 10;
  }
  return param; // implicitly calls unique_ptr(unique_ptr&&)
}

// Now use function     
unique_ptr<int[]> ptr{new int[10]};

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
           static_cast<unique_ptr<int[]>&&>(ptr), 10);

cout << "output:\n";

for(auto i = 0; i< 10; ++i) {
   cout << new_owner[i] << ", ";
}

output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 

static_cast<unique_ptr<int[]>&&>(ptr) geralmente é feito usando std :: move

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);

Um excelente artigo explicando tudo isso e mais (como como os valores permitem o encaminhamento perfeito e o que isso significa) com muitos bons exemplos é o C ++ Rvalue References explicado por Thomas Becker. Este post se baseou fortemente em seu artigo.

Uma introdução mais curta é references de Valor por Stroutrup, et. al





perfect-forwarding