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

801

He estado buscando algunas de las nuevas características de C ++ 11 y una que he notado es el doble ampersand al declarar variables, como T&& var.

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

¿Qué significa exactamente ?

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

paxdiablo
fuente
55
He agregado esto a las preguntas frecuentes de c ++, ya que estoy seguro de que saldrá más en el futuro.
GManNickG
3
pregunta relacionada sobre la semántica de movimiento
fredoverflow
41
Puede buscar esto usando google, solo tiene que ajustar su frase entre comillas: google.com/#q="T%26%26 "ahora tiene su pregunta como primer acierto. :)
sbi
Hay una respuesta muy buena y fácil de entender a una pregunta similar aquí stackoverflow.com/questions/7153991/…
Daniel
2
Recibí tres preguntas de stackoverflow en la parte superior buscando en Google "c ++ two ampersands parameter" y la suya fue la primera. Por lo tanto, ni siquiera necesita usar la puntuación para esto si puede deletrear "parámetro de dos ampersands".
sergiol

Respuestas:

668

Declara una referencia de valor (documento de propuesta de estándares).

Aquí hay una introducción a las referencias de rvalue .

Aquí hay una fantástica mirada en profundidad a las referencias de valor de uno de los desarrolladores de bibliotecas estándar de Microsoft .

PRECAUCIÓN: el artículo vinculado en MSDN ("Referencias de Rvalue: Características de C ++ 0x en VC10, Parte 2") es una introducción muy clara a las referencias de Rvalue, pero hace declaraciones sobre las referencias de Rvalue que alguna vez fueron verdaderas en el borrador de C ++ 11 estándar, pero no es cierto para el final! Específicamente, dice en varios puntos que las referencias de valor pueden unirse a valores, lo que alguna vez fue cierto, pero fue cambiado (por ejemplo, int x; int && rrx = x; ya no se compila en GCC) - drewbarbs 13 de julio de 14 a 16:12

La mayor diferencia entre una referencia C ++ 03 (ahora llamada referencia lvalue en C ++ 11) es que puede unirse a un rvalue como un temporal sin tener que ser constante. Por lo tanto, esta sintaxis ahora es legal:

T&& r = T();

Las referencias de rvalue proporcionan principalmente lo siguiente:

Mueve la semántica . Ahora se puede definir un constructor de movimiento y un operador de asignación de movimiento que tome una referencia de valor r en lugar de la referencia habitual de valor constante. Un movimiento funciona como una copia, excepto que no está obligado a mantener la fuente sin cambios; de hecho, generalmente modifica la fuente de modo que ya no posee los recursos movidos. Esto es ideal para eliminar copias extrañas, especialmente en implementaciones de bibliotecas estándar.

Por ejemplo, un constructor de copia podría verse así:

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

Si a este constructor se le pasó un temporal, la copia sería innecesaria porque sabemos que el temporal simplemente se destruirá; ¿Por qué no hacer uso de los recursos que ya se asignaron temporalmente? En C ++ 03, no hay forma de evitar la copia, ya que no podemos determinar que se nos haya pasado temporalmente. En C ++ 11, podemos sobrecargar un constructor de movimiento:

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

Observe la gran diferencia aquí: el constructor de movimiento en realidad modifica su argumento. Esto efectivamente "movería" lo temporal al objeto que se está construyendo, eliminando así la copia innecesaria.

El constructor de movimiento se usaría para las referencias temporales y para valores de valor no constantes que se convierten explícitamente en referencias de valor utilizando la std::movefunción (solo realiza la conversión). El siguiente código invoca el constructor de movimiento para f1y 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"

Reenvío perfecto . Las referencias de rvalue nos permiten reenviar adecuadamente los argumentos para las funciones con plantilla. Tome por ejemplo esta función de fábrica:

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

Si llamamos factory<foo>(5), se deducirá que el argumento es int&, que no se unirá a un literal 5, incluso si fooel constructor toma un int. Bueno, podríamos usarlo A1 const&, pero ¿y si footoma el argumento del constructor por referencia no constante? Para hacer una función de fábrica verdaderamente genérica, tendríamos que sobrecargar la fábrica una A1&y otra vez A1 const&. Eso podría estar bien si la fábrica toma 1 tipo de parámetro, pero cada tipo de parámetro adicional multiplicaría la sobrecarga necesaria establecida por 2. Eso es muy rápidamente imposible de mantener.

Las referencias de rvalue solucionan este problema al permitir que la biblioteca estándar defina una std::forwardfunción que pueda reenviar correctamente las referencias de lvalue / rvalue. Para obtener más información sobre cómo std::forwardfunciona, vea esta excelente respuesta .

Esto nos permite definir la función de fábrica de esta manera:

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

Ahora, el argumento rvalue / lvalue-ness se conserva cuando se pasa al Tconstructor. Eso significa que si factory se llama con un rvalue, Tel constructor se llama con un rvalue. Si la fábrica se llama con un valor l, Tel constructor se llama con un valor l. La función de fábrica mejorada funciona debido a una regla especial:

Cuando el tipo de parámetro de función es de la forma T&&donde Tes un parámetro de plantilla, y el argumento de función es un valor de tipo l A, el tipo A&se utiliza para la deducción de argumento de plantilla.

Por lo tanto, podemos usar la fábrica de esta manera:

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

Propiedades de referencia importantes de rvalue :

  • Para la resolución de sobrecarga, los valores prefieren vincularse a las referencias de valor y los valores prefieren vincularse a las referencias de valor . Por eso los temporales prefieren invocar un constructor de movimiento / operador de asignación de movimiento sobre un constructor de copia / operador de asignación.
  • Las referencias de rvalue se unirán implícitamente a rvalues ​​ya temporarios que son el resultado de una conversión implícita . es decirfloat f = 0f; int&& i = f;es está bien formado porque float es implícitamente convertible a int; la referencia sería a un temporal que es el resultado de la conversión.
  • Las referencias de valor nominadas son valores. Las referencias de valor sin nombre son valores. Esto es importante para entender por qué la std::movellamada es necesaria en:foo&& r = foo(); foo f = std::move(r);
Peter Huene
fuente
65
+1 para Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; sin saber esto, he luchado por entender por qué la gente hace T &&t; std::move(t);mucho tiempo en los movimientos, y cosas por el estilo.
legends2k
@MaximYegorushkin: En ese ejemplo, r se une a un valor r puro (temporal) y, por lo tanto, el temporal debería tener su alcance de por vida extendido, ¿no?
Peter Huene
@ PeterHuene Retiro eso, una referencia de valor r extiende la vida útil de un temporal.
Maxim Egorushkin
32
PRECAUCIÓN : el artículo vinculado en MSDN ("Referencias de Rvalue: Características de C ++ 0x en VC10, Parte 2") es una introducción muy clara a las referencias de Rvalue, pero hace declaraciones sobre las referencias de Rvalue que alguna vez fueron verdaderas en el borrador de C ++ 11 estándar, pero no es cierto para el final! Específicamente, dice en varios puntos que las referencias de valor pueden unirse a valores, lo que alguna vez fue cierto, pero se modificó (por ejemplo, int x; int &&rrx = x; ya no se compila en CCG)
drewbarbs
@PeterHuene En el ejemplo anterior, ¿no es typename identity<T>::type& aequivalente a T&?
ibp73
81

Denota una referencia de valor. Las referencias de Rvalue solo se unirá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 bajo 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 de valores, lo que implica una nueva asignación de montón. Sin embargo, en el segundo caso, sabemos de antemano que el objeto que se pasó a nuestro constructor debe destruirse de inmediato, y no tiene que permanecer intacto. Efectivamente, podemos 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 recursos referenciados 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 movimiento funcione correctamente para queunique_ptr no se pueda copiar pero se pueda mover, lo que significa questd::unique_ptrpuede almacenarse legalmente en contenedores estándar, ordenados, etc., mientras que C ++ 03 std::auto_ptrno.

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 de valor es crítico. Una referencia de valor r a un tipo T, donde T es un tipo de referencia, se convierte en una referencia del 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 manualmente las cuatro combinaciones de const y no const. En C ++ 0x es solo una sobrecarga. Combinado con plantillas variadas, 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 hizo de forma espontánea, pero ese mínimo puede aceptar cualquier combinación de valores, valores, valores constantes.

Perrito
fuente
¿Por qué lo usaste std::forward<A>(aref) < std::forward<B>(bref)? y no creo que esta definición sea correcta cuando intentes avanzar int&y float&. Mejor suelte una plantilla de formulario de tipo.
Yankes
25

El término para T&& cuando se usa con deducción de tipo (como para el reenvío perfecto) se conoce coloquialmente como referencia de reenvío . El término "referencia universal" fue acuñado por Scott Meyers en este artículo , pero luego fue cambiado.

Esto se debe a que puede ser el valor r o el valor l.

Ejemplos son:

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

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

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

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

Se puede encontrar más discusión en la respuesta para: Sintaxis para referencias universales

mmocny
fuente
14

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

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 valor de l 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 qué valores logran con un ejemplo:

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

Los operadores de construcción y asignación se han sobrecargado con versiones que toman referencias de valor. Las referencias de Rvalue permiten que una función se bifurque en tiempo de compilación (a través de la resolución de sobrecarga) con la condición "¿Me están llamando en un lvalue o un rvalue?". Esto nos permitió crear operadores de asignación y constructores más eficientes que mueven recursos en lugar de copiarlos.

El compilador se bifurca automáticamente en el momento de la compilación (dependiendo de si se invoca para un valor l o un valor r) eligiendo si se debe llamar al constructor de movimiento o al operador de asignación de movimiento.

Resumiendo: las referencias de valor permiten la semántica de movimiento (y el reenvío perfecto, discutido en el siguiente enlace del artículo).

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 sin procesar subyacente, unique_ptr no se puede copiar. Eso violaría su invariante de propiedad exclusiva. Por lo tanto, 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 cómo los valores permiten un reenvío perfecto y lo que eso significa) con muchos buenos ejemplos es la explicación de las referencias de valor de C ++ de Thomas Becker . Esta publicación se basó en gran medida en su artículo.

Una introducción más corta es Una breve introducción a las referencias de Rvalue por Stroutrup, et. Alabama

kurt krueckeberg
fuente
¿No es así que el constructor de copia Sample(const Sample& s)también necesita copiar el contenido? La misma pregunta para el 'operador de asignación de copia'.
K.Karamazen
Sí, tiene usted razón. No pude copiar la memoria. El constructor de copia y el operador de asignación de copia deben hacer memcpy (ptr, s.ptr, tamaño) después de probar ese tamaño! = 0. Y el constructor predeterminado debe hacer memset (ptr, 0, tamaño) si tamaño! = 0.
kurt krueckeberg
Bien gracias. Por lo tanto, este comentario y los dos comentarios anteriores se pueden eliminar porque el problema también se ha rectificado en la respuesta.
K.Karamazen