Declarador de referência Rvalue: &&

Contém uma referência a uma expressão rvalue.

Sintaxe

rvalue-reference-type-id:
type-specifier-seq&&attribute-specifier-seqoptptr-abstract-declaratoropt

Comentários

Referências de rvalue permitem que você diferencie um lvalue de um rvalue. As referências a lvalue e as referências a rvalue são sintaticamente e semanticamente semelhantes, mas seguem regras um pouco diferentes. Para mais informações sobre lvalues e rvalues, confira Lvalues e Rvalues. Para obter mais informações sobre referências de lvalue, consulte Lvalue Reference Declarator: &.

As seções a seguir descrevem como as referências de rvalue oferecem suporte à implementação da semântica de movimentação e do encaminhamento perfeito.

Semântica de transferência de recursos

Referências de rvalue oferecem suporte à implementação da semântica de movimentação, que pode aumentar significativamente o desempenho de seus aplicativos. A semântica de movimentação permite que você escreva códigos que transfiram recursos (como a memória dinamicamente alocada) de um objeto para outro. A semântica de movimentação funciona porque permite a transferência de recursos de objetos temporários: que não podem ser referenciados em outro lugar no programa.

Para implementar a semântica de movimentação, você normalmente fornece um construtor de movimentação, e, opcionalmente, um operador de atribuição de movimentação (operator=) para sua classe. As operações de cópia e atribuição cujas origens são rvalues aproveitam automaticamente as vantagens da semântica de movimentação. Diferente do construtor de cópia padrão, o compilador não fornece um construtor de movimentação padrão. Para mais informações sobre como gravar e usar um construtor de movimentação, confira Mover construtores e mover operadores de atribuição.

Você também pode sobrecarregar as funções e operadores comuns para aproveitar a semântica de movimentação. O Visual Studio 2010 apresenta a semântica de movimentação para a biblioteca C++ Standard. Por exemplo, a classe string implementa as operações que usam a semântica de movimentação. Considere o exemplo a seguir, que concatena várias cadeias de caracteres e imprime o resultado:

// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

Antes do Visual Studio 2010, cada chamada para operator+ aloca e retorna um novo objeto temporário string (um rvalue). operator+ não pode acrescentar uma cadeia de caracteres a outra porque não sabe se as cadeias de caracteres de origem são lvalues ou rvalues. Se as cadeias de caracteres de origem forem ambas lvalues, elas poderão ser referenciadas em outro lugar do programa e, portanto, não devem ser modificadas. Você pode modificar operator+ para obter rvalues usando referências rvalue, que não podem ser referenciadas em outro lugar no programa. Com essa alteração, operator+ agora pode acrescentar uma cadeia de caracteres a outra. Essa alteração pode reduzir significativamente o número de alocações de memória dinâmica que a classe string deve criar. Para obter mais informações sobre a string classe, consulte basic_string Classe.

A semântica de movimentação também ajuda quando o compilador não pode usar a Otimização de Valor Retornado (RVO) ou a Otimização de Valor Retornado (NRVO). Nesses casos, o compilador chama o construtor de movimentação caso o tipo o defina.

Para compreender melhor a semântica de movimentação, considere o exemplo de inserção de um elemento em um objeto vector. Se a capacidade do objeto for excedida, o objeto deverá realocar memória suficiente para seus elementos e, em seguida, copiar cada elemento para outro local de vector memória para abrir espaço para o vector elemento inserido. Quando uma operação de inserção copia um elemento, ela primeiro cria um elemento. Então, ela chama o construtor de cópia para copiar os dados do elemento anterior para o novo elemento. Por fim, ela destrói o elemento anterior. A semântica de movimentação permite que você mova objetos diretamente sem precisar executar operações caras de alocação de memória e de cópia.

Para aproveitar a semântica de movimentação no exemplo vector, você pode escrever um construtor de movimentação para mover dados de um objeto para outro.

Para mais informações sobre a introdução da semântica de movimentação na Biblioteca C++ Standard no Visual Studio 2010, confira Biblioteca C++ Standard.

Encaminhamento perfeito

O encaminhamento perfeito reduz a necessidade de funções sobrecarregadas e ajuda a evitar problemas de encaminhamento. O problema de encaminhamento pode ocorrer quando você escreve uma função genérica que pega referências como seus parâmetros. Se ele passar (ou encaminhar) esses parâmetros para outra função, por exemplo, se ele usar um parâmetro do tipo const T&, a função chamada não poderá modificar o valor desse parâmetro. Se a função genérica pegar um parâmetro do tipo T&, então a função não poderá ser chamada usando um rvalue (como um objeto temporário ou um literal de inteiro).

Normalmente, para resolver esse problema, você deve fornecer as versões sobrecarregadas da função genérica que recebe T& e const T& para cada um dos seus parâmetros. Como resultado, o número de funções sobrecarregadas aumenta exponencialmente com o número de parâmetros. As referências de rvalue permitem que você grave uma versão de uma função que aceita argumentos arbitrários. Essa função então pode encaminhá-los para outra função como se a outra função tivesse sido chamada diretamente.

Considere o seguinte exemplo que declara quatro tipos, W, X, Y e Z. O construtor para cada tipo usa uma combinação diferente de referências lvalue const e não const como parâmetros.

struct W
{
   W(int&, int&) {}
};

struct X
{
   X(const int&, int&) {}
};

struct Y
{
   Y(int&, const int&) {}
};

struct Z
{
   Z(const int&, const int&) {}
};

Suponha que você quer gravar uma função genérica que gera objetos. O exemplo a seguir mostra uma maneira de gravar esta função:

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
   return new T(a1, a2);
}

O exemplo a seguir mostra uma chamada válida para a função factory:

int a = 4, b = 5;
W* pw = factory<W>(a, b);

No entanto, o exemplo a seguir não contém uma chamada válida para a função factory. É porque factory usa referências lvalue que são modificáveis como seus parâmetros, mas é chamado usando rvalues:

Z* pz = factory<Z>(2, 2);

Normalmente, para resolver esse problema, você deve criar uma versão sobrecarregada da função factory para cada combinação de parâmetros A& e const A&. As referências de rvalue permitem que você grave uma versão da função factory, conforme mostrado no exemplo o seguir:

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

Este exemplo usa referências de rvalue como os parâmetros para a função factory. O objetivo da função é encaminhar os parâmetros da função factory para o construtor da std::forward classe template.

O exemplo a seguir mostra a função main que usa a função factory revisada para criar instâncias das classes W, X, Y e Z. A função factory revisada encaminha seus parâmetros (lvalues ou rvalues) para o construtor de classe apropriado.

int main()
{
   int a = 4, b = 5;
   W* pw = factory<W>(a, b);
   X* px = factory<X>(2, b);
   Y* py = factory<Y>(a, 2);
   Z* pz = factory<Z>(2, 2);

   delete pw;
   delete px;
   delete py;
   delete pz;
}

Propriedades das referências rvalue

Você pode sobrecarregar uma função para obter uma referência lvalue e uma referência rvalue.

Ao sobrecarregar uma função para obter uma referência de rvalue ou uma referência de lvalue const, escreva um código distingue entre objetos não modificáveis (lvalues) e valores temporários modificáveis (rvalues). Passe um objeto para uma função que pega uma referência de rvalue, a menos que o objeto seja marcado como const. O exemplo a seguir mostra a função f, que é sobrecarregada para pegar uma referência de lvalue e uma referência de rvalue. A função main chama f com lvalues e um rvalue.

// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void f(const MemoryBlock&)
{
   cout << "In f(const MemoryBlock&). This version can't modify the parameter." << endl;
}

void f(MemoryBlock&&)
{
   cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}

int main()
{
   MemoryBlock block;
   f(block);
   f(MemoryBlock());
}

Esse exemplo gera a saída a seguir:

In f(const MemoryBlock&). This version can't modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.

Nesse exemplo, a primeira chamada para f passa uma variável local (um lvalue) como seu argumento. A segunda chamada para f passa um objeto temporário como seu argumento. Como o objeto temporário não pode ser referenciado em outro lugar no programa, a chamada é associada à versão sobrecarregada de f que utiliza uma referência de rvalue, que está livre para alterar o objeto.

O compilador trata uma referência rvalue nomeada como um lvalue e uma referência rvalue sem nome como um rvalue.

Funções que fazem uma referência rvalue como um parâmetro tratam o parâmetro como um lvalue no corpo da função. O compilador trata uma referência de rvalue nomeada como um lvalue. É porque um objeto nomeado pode ser referenciado por várias partes de um programa. É perigoso permitir que várias partes de um programa modifiquem ou removam recursos desse objeto. Por exemplo, se várias partes de um programa tentarem transferir recursos do mesmo objeto, somente a primeira transferência terá sucesso.

O exemplo a seguir mostra a função g, que é sobrecarregada para pegar uma referência de lvalue e uma referência de rvalue. A função f pega uma referência de rvalue como seu parâmetro (uma referência de rvalue nomeada) e retorna uma referência de rvalue (uma referência de rvalue sem nome). Na chamada a g de f, a resolução de sobrecarga seleciona a versão de g que utiliza uma referência de lvalue porque o corpo de f trata seu parâmetro como um lvalue. Na chamada a g de main, a resolução de sobrecarga seleciona a versão de g que utiliza uma referência de rvalue porque f retorna uma referência de rvalue.

// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
   g(block);
   return move(block);
}

int main()
{
   g(f(MemoryBlock()));
}

Esse exemplo gera a saída a seguir:

In g(const MemoryBlock&).
In g(MemoryBlock&&).

No exemplo, a função main passa um rvalue para f. O corpo de f trata seu parâmetro nomeado como um lvalue. A chamada de f para g associa o parâmetro a uma referência de lvalue (a primeira versão sobrecarregada de g).

  • Você pode converter um lvalue em uma referência rvalue.

A função std::move da Biblioteca C++ Standard permite converter um objeto em uma referência de rvalue àquele objeto. Você também pode usar a palavra-chave static_cast para converter um lvalue em uma referência de rvalue, conforme mostrado no seguinte exemplo:

// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
   MemoryBlock block;
   g(block);
   g(static_cast<MemoryBlock&&>(block));
}

Esse exemplo gera a saída a seguir:

In g(const MemoryBlock&).
In g(MemoryBlock&&).

Os modelos de função deduzem seus tipos de argumento de modelo e, em seguida, usam regras de recolhimento de referência.

Um modelo de função que passe (ou encaminhe) seus parâmetros para outra função é um padrão comum. É importante compreender como funciona a dedução de tipo de modelo para os modelos de função com referências de rvalue.

Se o argumento de função for um rvalue, o compilador deduzirá o argumento como uma referência de rvalue. Por exemplo, suponha que você passe uma referência rvalue para um objeto de tipo X para um modelo de função que usa type T&& como seu parâmetro. A dedução do argumento de modelo deduz que T é X, portanto, o parâmetro tem o tipo X&&. Se o argumento de função for um lvalue ou lvalue const, o compilador deduzirá seu tipo como uma referência de lvalue ou referência de lvalue const desse tipo.

O exemplo a seguir declara um modelo de estrutura e o especializa para vários tipos de referência. A função print_type_and_value usa uma referência de rvalue como seu parâmetro e a encaminha para a versão especializada apropriada do método S::print. A função main demonstra as várias maneiras de chamar o método S::print.

// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

template<typename T> struct S;

// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.

template<typename T> struct S<T&> {
   static void print(T& t)
   {
      cout << "print<T&>: " << t << endl;
   }
};

template<typename T> struct S<const T&> {
   static void print(const T& t)
   {
      cout << "print<const T&>: " << t << endl;
   }
};

template<typename T> struct S<T&&> {
   static void print(T&& t)
   {
      cout << "print<T&&>: " << t << endl;
   }
};

template<typename T> struct S<const T&&> {
   static void print(const T&& t)
   {
      cout << "print<const T&&>: " << t << endl;
   }
};

// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
   S<T&&>::print(std::forward<T>(t));
}

// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }

int main()
{
   // The following call resolves to:
   // print_type_and_value<string&>(string& && t)
   // Which collapses to:
   // print_type_and_value<string&>(string& t)
   string s1("first");
   print_type_and_value(s1);

   // The following call resolves to:
   // print_type_and_value<const string&>(const string& && t)
   // Which collapses to:
   // print_type_and_value<const string&>(const string& t)
   const string s2("second");
   print_type_and_value(s2);

   // The following call resolves to:
   // print_type_and_value<string&&>(string&& t)
   print_type_and_value(string("third"));

   // The following call resolves to:
   // print_type_and_value<const string&&>(const string&& t)
   print_type_and_value(fourth());
}

Esse exemplo gera a saída a seguir:

print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth

Para resolver cada chamada para a função print_type_and_value, o compilador executa primeiro a dedução do argumento do modelo. O compilador, então, aplica regras de recolhimento de referência quando substitui os tipos de parâmetro com os argumentos de modelo deduzidos. Por exemplo, passar a variável local s1 para a função print_type_and_value faz com que o compilador produza a seguinte assinatura de função:

print_type_and_value<string&>(string& && t)

O compilador usa regras de recolhimento de referência para reduzir a assinatura:

print_type_and_value<string&>(string& t)

Esta versão da função print_type_and_value encaminha seu parâmetro para a versão especializada correta do método S::print.

A tabela a seguir resume as regras de recolhimento de referência para dedução do tipo de argumento de modelo:

Tipo expandido Tipo recolhido
T& & T&
T& && T&
T&& & T&
T&& && T&&

A dedução do argumento do modelo é um elemento importante para a implementação do encaminhamento perfeito. A seção Encaminhamento perfeito descreve o encaminhamento perfeito com mais detalhes.

Resumo

Referências de rvalue diferenciam lvalues de rvalues. Para melhorar o desempenho de seus aplicativos, elas eliminam a necessidade de alocações de memória e operações de cópia desnecessárias. Elas também permitem que você escreva uma função que aceite argumentos arbitrários. Essa função pode encaminhá-los para outra função como se a outra função tivesse sido chamada diretamente.

Confira também

Expressões com operadores unários
Declarador de referência Lvalue: &
Lvalues e rvalues
Operadores de construtores de movimento e de atribuição de movimento (C++)
Biblioteca Padrão do C++