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


He estado investigando algunas de las nuevas características de C ++ 11 y una de las 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é es exactamente? ¿media?

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


608
2018-03-30 03:29


origen


Respuestas:


Declara una referencia rvalue (documento de propuesta de normas).

Aquí hay una introducción a rvalue referencias.

Aquí hay una fantástica mirada en profundidad a las referencias de valores de una de las bibliotecas estándar de Microsoft desarrolladores. (Pero vea la Precaución en los comentarios que siguen a esta respuesta antes de leer este artículo).

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

T&& r = T();

Las referencias de rvalue proporcionan principalmente lo siguiente:

Mover la semántica. Ahora se puede definir un constructor de movimiento y un operador de asignación de movimiento que tome una referencia rvalue en lugar de la referencia habitual de const-lvalue. Un movimiento funciona como una copia, excepto que no está obligado a mantener la fuente sin cambios; de hecho, generalmente modifica la fuente de tal manera 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 el temporal ya asignó? En C ++ 03, no hay forma de evitar la copia, ya que no podemos determinar que nos aprobaron de manera temporal. En C ++ 11, podemos sobrecargar un constructor de movimientos:

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" el 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 las referencias lvalue no const que se convierten explícitamente a referencias rvalue utilizando el std::move función (solo realiza la conversión). El siguiente código llama al constructor de movimientos para f1 y 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 rvalue nos permiten reenviar correctamente los argumentos para las funciones con plantilla. Tomemos como 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), el argumento se deduce que es int&, que no se unirá a un literal 5, incluso si fooEl constructor toma una int. Bueno, podríamos usar A1 const&, pero que si foo toma el argumento constructor por referencia no-const? Para hacer una función de fábrica verdaderamente genérica, tendríamos que sobrecargar la fábrica A1&y en A1 const&. Eso podría estar bien si 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 no se puede mantener.

Las referencias rvalue corrigen este problema al permitir que la biblioteca estándar defina un std::forward función que puede reenviar correctamente las referencias lvalue / rvalue. Para obtener más información acerca de cómo std::forward funciona, mira 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 valor de r / valor-valor del argumento se conserva cuando se pasa a Tconstructor Eso significa que si se llama a fábrica con un valor r, TEl constructor se llama con un valor r. Si se llama a factory con un lvalue, TEl constructor se llama con un lvalue. 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&& dónde T es una plantilla   parámetro y el argumento de la función   es un lvalue de tipo A, el tipo A& es   utilizado para la deducción del argumento de la plantilla.

Por lo tanto, podemos utilizar 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 rvalue importantes:

  • Para la resolución de sobrecarga, Los valores de l prefieren la unión a las referencias de lvalue y los valores de referencia prefieren la vinculación a las referencias de rvalue. De ahí que los temporarios prefieran invocar un operador de asignación de movimiento / constructor de movimiento sobre un operador de asignación / constructor de copia.
  • Las referencias rvalue se vincularán implícitamente a los valores r y a los temporales que son el resultado de una conversión implícita.. es decir float f = 0f; int&& i = f; 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 valores nominales son lvalues. Las referencias de valores sin nombre son valores r.  Esto es importante para entender por qué std::move la llamada es necesaria en: foo&& r = foo(); foo f = std::move(r);

542
2018-03-30 04:04



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 se pasó a nuestro constructor se debe destruir inmediatamente, y no tiene que 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. Considera el caso de std::unique_ptr- ahora que nuestra clase puede distinguir entre temporales y no temporales, podemos hacer que la semántica del movimiento funcione correctamente para que el unique_ptrno se puede copiar pero se puede mover, lo que significa que std::unique_ptr se puede almacenar legalmente en contenedores estándar, ordenados, etc., mientras que C ++ 03 std::auto_ptr no poder.

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.


76
2018-01-17 14:21



El término para T&&  cuando se usa con deducción de tipo (como para un reenvío perfecto) se conoce coloquialmente como referencia universal. Esto fue acuñado por Scott Meyers en este articulo.

Eso es porque puede ser r-value o l-value.

Los ejemplos son:

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

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

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

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

Tenga en cuenta que el estándar en sí no tiene noción de esto, es simplemente una forma de discutir la combinación (impar?) De reglas de colapso de referencia, deducción del tipo de referencia y la sintaxis &&.

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


18
2017-11-03 15:31



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

// 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 el de Thomas Becker. Referencias de C ++ Rvalue explicadas. 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


7