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

799

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

Para começar, como se chama essa fera? Eu gostaria que o Google nos permitisse procurar pontuação como essa.

O que exatamente isso significa?

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

paxdiablo
fonte
55
Eu adicionei isso ao c ++ - faq, pois tenho certeza de que surgirá mais no futuro.
GManNickG
3
pergunta relacionada sobre mover semântica
fredoverflow 30/03
41
Você pode procurar por isso usando o google, você só precisa colocar sua frase entre aspas: google.com/#q="T%26%26 "agora tem sua pergunta como o primeiro hit. :)
SBI
Há um muito bom, fácil de entender a resposta a uma pergunta semelhante aqui stackoverflow.com/questions/7153991/...
Daniel
2
Eu recebi três perguntas de stackoverflow na parte superior pesquisando no Google por "c ++ parâmetro de dois e comercial" e a sua foi a primeira. Portanto, você nem precisa usar pontuação para isso, se puder especificar "parâmetro de dois e comercial".
sergiol

Respostas:

668

Declara uma referência de valor (doc da proposta de normas).

Aqui está uma introdução às referências ao rvalue .

Aqui está uma fantástica análise detalhada das referências de valor de um dos desenvolvedores de bibliotecas padrão da Microsoft .

CUIDADO: o artigo vinculado no MSDN ("Rvalue References: C ++ 0x Features in VC10, Part 2") é uma introdução muito clara às referências Rvalue, mas faz declarações sobre referências Rvalue que antes eram verdadeiras no rascunho C ++ 11 padrão, mas não são verdadeiras para a final! Especificamente, diz em vários pontos que as referências rvalue podem ser vinculadas a lvalues, que antes eram verdadeiras, mas foram alteradas (por exemplo, int x; int && rrx = x; não é mais compilado no GCC)

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

T&& r = T();

As referências rvalue fornecem principalmente o seguinte:

Mover semântica . Agora, um construtor de movimentação e um operador de atribuição de movimentação podem ser definidos com uma referência rvalue em vez da referência const-lvalue usual. Uma movimentação funciona como uma cópia, exceto que não é obrigada a manter a fonte inalterada; de fato, geralmente modifica a fonte, de modo que não possui mais os recursos movidos. Isso é ótimo para eliminar cópias estranhas, especialmente em implementações de bibliotecas 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 esse construtor recebeu um temporário, a cópia seria desnecessária porque sabemos que o temporário será destruído; por que não fazer uso dos recursos temporários já alocados? No C ++ 03, não há como impedir a cópia, pois não podemos determinar se passamos temporariamente. No C ++ 11, podemos sobrecarregar um construtor de movimentação:

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 move 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 referências temporárias e para valores não constantes que são convertidos explicitamente em referências para valores usando a std::movefunção (apenas realiza a conversão). O código a seguir chama o construtor move para f1e 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 rvalue nos permitem encaminhar corretamente argumentos para funções de modelo. 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 como int&, o que não será vinculado a um literal 5, mesmo se fooo construtor usar um int. Bem, poderíamos usar A1 const&, mas e se foopegar o argumento do construtor por referência não-const? Para fazer uma função fábrica verdadeiramente genérico, teríamos sobrecarregar fábrica no A1&e no A1 const&. Isso pode ser bom se a fábrica adotar 1 tipo de parâmetro, mas cada tipo de parâmetro adicional multiplicaria a sobrecarga necessária definida por 2. Isso é rapidamente impossível de manter.

As referências rvalue corrigem esse problema, permitindo que a biblioteca padrão defina uma std::forwardfunção que possa encaminhar adequadamente as referências lvalue / rvalue. Para mais informações sobre como std::forwardfunciona, consulte 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 rvalue / lvalue-ness do argumento é preservado quando passado para To construtor. Isso significa que, se a fábrica é chamada com um rvalue, To construtor de é chamado com um rvalue. Se factory é chamado com um valor Tl, o construtor de é chamado com um valor l. A função aprimorada de fábrica funciona devido a uma regra especial:

Quando o tipo de parâmetro da função está no formato T&&onde Testá um parâmetro do modelo e o argumento da função é um valor l do tipo A, o tipo A&é usado para dedução do argumento do modelo.

Assim, podemos usar a 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 do rvalue :

  • Para resolução de sobrecarga, lvalues ​​prefere ligação a referências lvalue e rvalues ​​prefere ligação a referências rvalue . Por isso, os temporários preferem chamar um construtor de movimentação / operador de atribuição de movimentação em vez de um operador de construção / atribuição de cópia.
  • As referências rvalue se vincularão implicitamente a rvalues ​​e a temporários resultantes de uma conversão implícita . isto float f = 0f; int&& i = f;é, é bem formado porque float é implicitamente conversível em int; a referência seria a um temporário que é o resultado da conversão.
  • As referências de rvalue nomeadas são lvalues. As referências rvalue sem nome são rvalues. É importante entender por que a std::movechamada é necessária em:foo&& r = foo(); foo f = std::move(r);
Peter Huene
fonte
65
+1 para Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; sem saber disso, lutei para entender por que as pessoas fazem isso T &&t; std::move(t);por um longo tempo em empresas de mudanças e afins.
Legends2k #
@MaximYegorushkin: Nesse exemplo, r está vinculando a um rvalue puro (temporário) e, portanto, o temporário deve ter seu escopo de vida útil estendido, não?
Peter Huene
@ PeterHuene Retiro isso, uma referência de valor-r prolonga a vida útil de um temporário.
Maxim Egorushkin
32
CUIDADO : o artigo vinculado no MSDN ("Rvalue References: C ++ 0x Features in VC10, Part 2") é uma introdução muito clara às referências Rvalue, mas faz declarações sobre referências Rvalue que antes eram verdadeiras no rascunho C ++ 11 padrão, mas não são verdadeiras para a final! Especificamente, diz-se em vários pontos que referências rvalue pode ligar-se a lvalues, que já foi verdadeiro, mas foi alterado (por exemplo. int x; int &&rrx = x; Não há mais longos compila no GCC)
drewbarbs
@PeterHuene No exemplo acima, não é typename identity<T>::type& aequivalente a T&?
ibp73
81

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

No 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&);

No C ++ 0x, esse 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 precisa executar uma cópia para reter a semântica do 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 ao nosso construtor é imediatamente devido à destruição e não precisa permanecer intocado. Podemos efetivamente trocar os ponteiros internos e não executar nenhuma cópia nesse cenário, o que é substancialmente mais eficiente. A semântica de movimentação beneficia qualquer classe que tenha cópias caras ou proibidas 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 da movimentação funcionar corretamente para que unique_ptrnão possa ser copiada, mas possa ser movida, o que significa questd::unique_ptrpodem ser legalmente armazenados em contêineres padrão, classificados etc., enquanto os C ++ 03 std::auto_ptrnão podem.

Agora, consideramos o outro uso de referências rvalue - 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 consigo lembrar o que o C ++ 03 diz sobre isso, mas no C ++ 0x, o tipo resultante ao lidar com referências de rvalue é crítico. Uma referência rvalue 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. No C ++ 03, você precisa sobrecarregar as quatro combinações de const e non-const manualmente. No C ++ 0x, é apenas uma sobrecarga. Combinado com modelos variados, isso permite o 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);
}

Eu deixei de fora a dedução do tipo de retorno, porque não consigo me lembrar de como isso é feito de imediato, mas esse min pode aceitar qualquer combinação de lvalues, rvalues, const lvalues.

Cachorro
fonte
por que você usou std::forward<A>(aref) < std::forward<B>(bref)? e eu não acho que essa definição estará correta quando você tentar avançar int&e float&. Melhor soltar um modelo de formulário de tipo.
Yankes
25

O termo para T&& quando usado com dedução de tipo (como encaminhamento perfeito) é conhecido coloquialmente como uma referência de encaminhamento . O termo "referência universal" foi cunhado por Scott Meyers neste artigo , mas mais tarde foi alterado.

Isso ocorre porque pode ser um valor r ou um valor l.

Exemplos são:

// template
template<class T> foo(T&& t) { ... }

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;

Mais discussões podem ser encontradas na resposta para: Sintaxe para referências universais

mmocny
fonte
14

Uma referência rvalue é um tipo que se comporta de maneira semelhante à 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 ​​prefere referências de lvalue no estilo antigo, enquanto rvalues ​​prefere 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 rvalue? Qualquer coisa que não seja um valor l. Um valor l é uma expressão que se refere a um local de memória e nos permite obter o endereço desse local de memória através do operador &.

É quase mais fácil entender primeiro o que os rvalues ​​alcançam com um exemplo:

 #include <cstring>
 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {
     if (ptr != nullptr) memset(ptr, 0, sz);
  }
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     if (ptr != nullptr) memcpy(ptr, s.ptr, 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
      size = s.size;

      if (size != 0) {
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
      } else 
         ptr = nullptr;
     }
     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
};     

O construtor e os operadores de atribuição foram sobrecarregados com versões que usam referências de rvalue. As referências de valor permitem que uma função se ramifique no tempo de compilação (via resolução de sobrecarga) com a condição "Estou sendo chamado em um valor l ou um valor r?". Isso nos permitiu criar operadores de construtor e atribuição mais eficientes acima, que movem recursos, em vez de copiá-los.

O compilador se ramifica automaticamente no tempo de compilação (dependendo se está sendo chamado para um lvalue ou um rvalue), escolhendo se o construtor de movimentação ou o operador de atribuição de movimentação devem ser chamados.

Resumindo: as referências rvalue permitem a semântica de movimentação (e o 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 do ponteiro bruto subjacente, o unique_ptr não pode ser copiado. Isso violaria sua invariante propriedade exclusiva. Portanto, 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 muito mais (como como os rvalues ​​permitem o encaminhamento perfeito e o que isso significa) com muitos bons exemplos é o C ++ Rvalue References de Thomas Becker, explicado . Este post contou muito com seu artigo.

Uma introdução mais curta é Uma breve introdução às referências de valor por Stroutrup, et. al

kurt krueckeberg
fonte
Não é assim que o construtor de cópias Sample(const Sample& s)precisa também copiar o conteúdo? A mesma pergunta para o 'operador de atribuição de cópia'.
K.Karamazen
Sim você está certo. Falha ao copiar a memória. O construtor de cópias e o operador de atribuição de cópias devem executar memcpy (ptr, s.ptr, size) após testar esse tamanho! = 0. E o construtor padrão deve executar memset (ptr, 0, size) se size! = 0.
kurt krueckeberg 29/12/19
Ok obrigado. Portanto, este comentário e os dois comentários anteriores podem ser removidos porque o problema também foi corrigido na resposta.
K.Karamazen