c++ - ¿Qué significa T&& (doble ampersand) en C ++ 11?



2 Answers

Denota una referencia rvalue. Las referencias de valor solo se vincularán a objetos temporales, a menos que se genere explícitamente lo contrario. Se utilizan para hacer que los objetos sean mucho más eficientes en ciertas circunstancias, y para proporcionar una instalación conocida como reenvío perfecto, que simplifica enormemente el código de la plantilla.

En C ++ 03, no puede distinguir entre una copia de un valor l no mutable y un valor r.

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

En C ++ 0x, este no es el 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 la implementación detrás de estos constructores. En el primer caso, la cadena debe realizar una copia para retener la semántica del valor, lo que implica una nueva asignación de almacenamiento dinámico. Sin embargo, en el segundo caso, sabemos de antemano que el objeto que se pasó a nuestro constructor se debe de inmediato a la destrucción, y no tiene que permanecer intacto. Podemos efectivamente intercambiar los punteros internos y no realizar ninguna copia en este escenario, que es sustancialmente más eficiente. La semántica de Move beneficia a cualquier clase que tenga una copia costosa o prohibida de los recursos de referencia interna. Considere el caso de std::unique_ptr : ahora que nuestra clase puede distinguir entre temporarios y no temporales, podemos hacer que la semántica de movimiento funcione correctamente de modo que el unique_ptr no pueda copiarse pero se pueda mover, lo que significa que std::unique_ptr puede estar legalmente almacenado en contenedores estándar, ordenados, etc., mientras que std::auto_ptr C ++ 03 no puede.

Ahora consideramos el otro uso de las referencias rvalue - reenvío perfecto. Considere la cuestión de vincular una referencia a una referencia.

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

No puedo recordar lo que C ++ 03 dice acerca de esto, pero en C ++ 0x, el tipo resultante al tratar con referencias de valor es crítico. Una referencia rvalue a un tipo T, donde T es un tipo de referencia, se convierte en una referencia de 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 la función de plantilla más simple: mínimo y máximo. En C ++ 03 tienes que sobrecargar las cuatro combinaciones de const y non-const manualmente. En C ++ 0x es solo una sobrecarga. Combinado con las plantillas variadic, esto permite un reenvío perfecto.

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);
}

Dejé de lado la deducción del tipo de retorno, porque no puedo recordar cómo se hizo de antemano, pero ese mínimo puede aceptar cualquier combinación de valores, valores, valores constantes.

c++ c++11 rvalue-reference c++-faq perfect-forwarding

He estado investigando algunas de las nuevas características de C ++ 11 y una de las que he notado es el doble y en la declaración de variables, como T&& var .

Para empezar, ¿cómo se llama esta bestia? Me gustaría que Google nos permitiera buscar puntuaciones como esta.

¿Qué significa exactamente ?

A primera vista, parece ser una doble referencia (como los punteros dobles de estilo C T** var ), pero me resulta difícil pensar en un caso de uso para eso.




Una referencia rvalue es un tipo que se comporta de manera muy similar a la referencia ordinaria X &, con varias excepciones. El más importante es que cuando se trata de la resolución de sobrecarga de funciones, los valores de l prefieren las referencias de valores de estilo antiguo, mientras que los valores de r prefieren las nuevas referencias de valor de r:

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

Entonces, ¿qué es un valor? Cualquier cosa que no sea un valor. Un lvalue es una expresión que se refiere a una ubicación de memoria y nos permite tomar la dirección de esa ubicación de memoria a través del operador &.

Es casi más fácil entender primero lo que rvalues ​​logra con un ejemplo:

 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
};     

El constructor y los operadores de asignación han sido sobrecargados con versiones que toman referencias de valor. Las referencias de valores permiten que una función se bifurque en tiempo de compilación (mediante resolución de sobrecarga) en la condición "¿Se me llama en un valor o valor?". Esto nos permitió crear constructores y operadores de asignación más eficientes que mover los recursos en lugar de copiarlos.

El compilador se bifurca automáticamente en el momento de la compilación (dependiendo de si se está invocando para un lvalue o un rvalue) y elige si se debe llamar al constructor de movimientos o al operador de asignación de movimientos.

Resumiendo: las referencias de valores permiten la semántica de movimientos (y el reenvío perfecto, que se explica en el enlace del artículo a continuación).

Un ejemplo práctico y fácil de entender es la plantilla de clase std :: unique_ptr . Dado que unique_ptr mantiene la propiedad exclusiva de su puntero en bruto subyacente, unique_ptr no se puede copiar. Eso violaría su invariante de propiedad exclusiva. Así que no tienen copia constructores. Pero sí tienen constructores de movimiento:

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) usualmente se hace usando std :: move

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

Un excelente artículo que explica todo esto y más (como la forma en que los valores permiten el reenvío perfecto y lo que eso significa) con muchos buenos ejemplos es la explicación de las referencias en valores en C ++ de Thomas Becker. Este post se basó en gran medida en su artículo.

Una introducción más corta es references de Rvalue por Stroutrup, et. Alabama






Related