Explicación concisa de las reglas de colapso de referencia solicitadas: (1) A & & – – A &, (2) A &&& -> A &, (3) A && & -> A &, y (4) A && && -> A &&

El siguiente enlace proporciona las 4 formas de colapso de referencia (si estoy seguro de que estas son las únicas 4 formas): http://thbecker.net/articles/rvalue_references/section_08.html .

Desde el enlace:

  1. A & & se convierte en A &
  2. A &&& se convierte en A &
  3. A && & se convierte en A &&
  4. A && && se convierte en A &&

Aunque puedo hacer una suposición educada, me gustaría una explicación concisa para la razón de ser de cada una de estas reglas de colapso de referencia.

Una pregunta relacionada, si pudiera: ¿Estas reglas de colapso de referencia se utilizan internamente en C ++ 11 por las utilidades de STL tales como std::move() , std::forward() , y similares, en el mundo real típico? casos de uso? (Nota: específicamente pregunto si las reglas de colapso de referencia se utilizan en C ++ 11, a diferencia de C ++ 03 o anterior).

Formulo esta pregunta relacionada porque conozco las utilidades de C ++ 11 como std::remove_reference , pero no sé si las utilidades relacionadas con la referencia, como std::remove_reference se usan rutinariamente en C ++ 11 para evitar la necesidad. para las reglas de colapso de referencia, o si se usan junto con las reglas de colapso de referencia.

Las reglas de colapso de referencia (salvo A& & -> A& , que es C ++ 98/03) existen por una razón: para permitir que funcione el reenvío perfecto.

El reenvío “perfecto” significa reenviar los parámetros de forma efectiva como si el usuario hubiera llamado directamente a la función (menos elisión, que se interrumpe mediante reenvío). Hay tres tipos de valores que el usuario podría aprobar: lvalues, xvalues ​​y prvalues, y hay tres formas en que la ubicación receptora puede tomar un valor: por valor, por (posiblemente const) lvalue reference, y por (posiblemente const) rvalue referencia.

Considera esta función:

 template void Fwd(T &&v) { Call(std::forward(v)); } 

Por valor

Si Call toma su parámetro por valor, entonces debe ocurrir una copia / movimiento en ese parámetro. Cuál depende de cuál es el valor entrante. Si el valor entrante es un valor l, entonces debe copiar el valor l. Si el valor entrante es un valor r (que colectivamente son xvalores y valores primos), entonces debe moverse desde allí.

Si llama a Fwd con un valor l, las reglas de deducción de tipo de C ++ significan que T se deducirá como Type& , donde Type es el tipo del valor l. Obviamente, si el lvalor es const , se deducirá como const Type& . Las reglas de colapso de referencia significan que Type & && convierte en Type & for v , a lvalue reference. Que es exactamente lo que necesitamos para llamar a Call . Llamarlo con una referencia de lvalue forzará una copia, exactamente como si lo hubiéramos llamado directamente.

Si llama a Fwd con un valor r (es decir, una expresión temporal Type o ciertas expresiones Type&& ), entonces T se deducirá como Type . Las reglas de colapso de referencia nos dan Type && , que provoca un movimiento / copia, que es casi exactamente como si lo hubiéramos llamado directamente (menos elisión).

Por referencia lvalue

Si Call toma su valor por referencia lvalue, solo debería poder llamarse cuando el usuario use los parámetros lvalue. Si es una referencia const-lvalue, entonces puede ser involable por cualquier cosa (lvalue, xvalue, prvalue).

Si llamas a Fwd con un valor l, volvemos a obtener Type& como el tipo de v . Esto se unirá a una referencia de valor l no const. Si lo llamamos con un const lvalue, obtenemos const Type& , que solo se vinculará a un argumento de referencia const lvalue en Call .

Si llamas a Fwd con un xvalor, de nuevo recibimos Type&& como el tipo de v . Esto no le permitirá llamar a una función que toma un valor l no const, ya que un valor x no puede vincularse a una referencia lvalue no const. Puede vincularse a una referencia de const lvalue, por lo que si Call utilizara un const& , podríamos llamar a Fwd con un xvalor.

Si llamas a Fwd con un valor prve, nuevamente obtenemos Type&& , así que todo funciona igual que antes. No puede pasar una función temporal a una función que toma un valor l no const, por lo que nuestra función de reenvío también se ahogará en el bash de hacerlo.

Por referencia de valor

Si Call toma su valor por referencia de valor real, solo debería ser invocable cuando el usuario use los parámetros xvalue o rvalue.

Si llamas a Fwd con un lvalue, obtenemos Type& . Esto no se vinculará a un parámetro de referencia rvalue, por lo que se produce un error de comstackción. Un const Type& tampoco se enlazará a un parámetro de referencia rvalue, por lo que aún falla. Y esto es exactamente lo que sucedería si llamamos a Call directamente con un lvalue.

Si llama a Fwd con un valor x, obtenemos Type&& , que funciona (la calificación del cv sigue siendo importante por supuesto).

Lo mismo ocurre con el uso de un prvalue.

std :: forward

std :: forward usa reglas de colapso de referencia de manera similar, para pasar referencias rvalue entrantes como valores x (valores de retorno de función que son Type&& son xvalues) y referencias lvalue entrantes como lvalues ​​(return Type& ).

Las reglas son bastante simples. Rvalue reference es una referencia a algún valor temporal que no persiste más allá de la expresión que lo usa, a diferencia de la lvalue reference que hace referencia a datos persistentes. Entonces, si tiene una referencia a datos persistentes, no importa con qué otras referencias lo combine, los datos reales a los que se hace referencia son un valor l – esto cubre las 3 primeras reglas. La cuarta regla también es natural: la referencia de valor a la referencia de valor real sigue siendo una referencia a los datos no persistentes, por lo tanto, se obtiene una referencia de valor r.

Sí, las utilidades C ++ 11 dependen de estas reglas, la implementación provista por su enlace coincide con los encabezados reales: http://en.cppreference.com/w/cpp/utility/forward

Y sí, las reglas colapsadas junto con la regla de deducción de argumento de plantilla se están aplicando al usar las utilidades std::move y std::forward , tal como se explica en su enlace.

El uso de rasgos de tipo como remove_reference realmente depende de tus necesidades; move y forward tapa para los casos más casuales.