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



Answers

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

En C ++ 03, no puede distinguir entre una copia de un lvalue 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 pila. Sin embargo, en el segundo caso, sabemos de antemano que el objeto que fue pasado a nuestro constructor debe ser destruido inmediatamente, y no debe permanecer intacto. Podemos simplemente intercambiar los punteros internos y no realizar ninguna copia en este escenario, que es sustancialmente más eficiente. La semántica de movimiento beneficia a cualquier clase que tenga una copia costosa o prohibida de los recursos a los que se hace referencia internamente. Considere el caso de std::unique_ptr : ahora que nuestra clase puede distinguir entre temporales y no temporales, podemos hacer que la semántica de movimientos funcione correctamente para que unique_ptr no se pueda copiar, pero se pueda mover, lo que significa que std::unique_ptr puede estar legalmente almacenado en contenedores estándar, ordenados, etc., mientras que C ++ 03 std::auto_ptr no puede.

Ahora consideramos el otro uso de 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 sobre esto, pero en C ++ 0x, el tipo resultante cuando se trata de referencias rvalue 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: min y max. En C ++ 03, debe sobrecargar las cuatro combinaciones de const y non-const manualmente. En C ++ 0x, es solo una sobrecarga. Combinado con 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é la deducción del tipo de devolución, porque no puedo recordar cómo se hace de forma directa, pero ese mínimo puede aceptar cualquier combinación de valores, valores, valores de convalidación.

Question

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

Para empezar, ¿cómo se llama esta bestia? Ojalá Google nos permitiera buscar signos de puntuación como este.

¿Qué significa exactamente ?

A primera vista, parece ser una referencia doble (como los T** var doble puntero estilo C), pero me cuesta pensar en un caso de uso para eso.




Una referencia rvalue es un tipo que se comporta de forma 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 l prefieren las referencias de valores antiguos, mientras que los valores preferidos son las nuevas referencias de valores 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 lvalue Un lvalue es una expresión que hace referencia 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 qué valores 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 se han sobrecargado con versiones que toman referencias rvalue. Las referencias Rvalue permiten que una función se bifurque en el tiempo de compilación (a través de la resolución de sobrecarga) en la condición "¿Se me está solicitando un valor l o un valor r?". Esto nos permitió crear operadores de asignaciones y constructores más eficientes que mueven recursos en lugar de copiarlos.

El compilador se bifurca automáticamente en tiempo de compilación (dependiendo de si se está invocando para un lvalue o un valor r) y elige si se debe invocar al constructor de movimientos o al operador de asignación de movimiento.

En resumen: las referencias rvalue permiten la semántica de movimiento (y el reenvío perfecto, discutido en el enlace del artículo a continuación).

Un ejemplo práctico fácil de entender es la plantilla de clase std :: unique_ptr . Dado que unique_ptr mantiene la propiedad exclusiva de su puntero sin procesar subyacente, unique_ptr's no se puede copiar. Eso violaría su invariante de propiedad exclusiva. Entonces ellos no tienen constructores de copia. Pero 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) generalmente 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 por ejemplo cómo los valores permiten un reenvío perfecto y lo que eso significa) con muchos buenos ejemplos es C ++ Rvalue References Explained de Thomas Becker. Esta publicación se basó en gran medida en su artículo.

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




Related